mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-23 21:18:04 +00:00
Add TTM Scalper indicator implementation in C# and Pine Script; update Blma class for average calculation; remove missing indicators report and oscillator docs rewrite plans.
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public sealed class PivotdemIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void PivotdemIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new PivotdemIndicator();
|
||||
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Contains("PIVOTDEM", indicator.Name, StringComparison.Ordinal);
|
||||
Assert.False(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PivotdemIndicator_MinHistoryDepths_EqualsZero()
|
||||
{
|
||||
var indicator = new PivotdemIndicator();
|
||||
|
||||
Assert.Equal(0, PivotdemIndicator.MinHistoryDepths);
|
||||
IWatchlistIndicator watchlistIndicator = indicator;
|
||||
Assert.Equal(0, watchlistIndicator.MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PivotdemIndicator_ShortName_IsPivotdem()
|
||||
{
|
||||
var indicator = new PivotdemIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
Assert.Contains("PIVOTDEM", indicator.ShortName, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PivotdemIndicator_SourceCodeLink_IsValid()
|
||||
{
|
||||
var indicator = new PivotdemIndicator();
|
||||
|
||||
Assert.Contains("github.com", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
Assert.Contains("Pivotdem", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PivotdemIndicator_Initialize_CreatesInternalIndicator()
|
||||
{
|
||||
var indicator = new PivotdemIndicator();
|
||||
|
||||
indicator.Initialize();
|
||||
|
||||
// 3 line series: PP, R1, S1
|
||||
Assert.Equal(3, indicator.LinesSeries.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PivotdemIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new PivotdemIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
double basePrice = 100 + i * 2;
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), basePrice, basePrice + 5, basePrice - 5, basePrice + 2);
|
||||
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
// PP is index 0
|
||||
double pp = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(pp) || double.IsNaN(pp));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PivotdemIndicator_ProcessUpdate_NewBar_ComputesValue()
|
||||
{
|
||||
var indicator = new PivotdemIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
// Simulate a new bar
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(5), 110, 120, 100, 115);
|
||||
var newArgs = new UpdateArgs(UpdateReason.NewBar);
|
||||
indicator.ProcessUpdate(newArgs);
|
||||
|
||||
double pp = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(pp) || double.IsNaN(pp));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PivotdemIndicator_ThreeLineSeries_ArePresent()
|
||||
{
|
||||
var indicator = new PivotdemIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
// PP=0, R1=1, S1=2 — DeMark only produces 3 levels
|
||||
Assert.Equal(3, indicator.LinesSeries.Count);
|
||||
Assert.Contains("PP", indicator.LinesSeries[0].Name, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Contains("R1", indicator.LinesSeries[1].Name, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Contains("S1", indicator.LinesSeries[2].Name, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PivotdemIndicator_Description_IsSet()
|
||||
{
|
||||
var indicator = new PivotdemIndicator();
|
||||
|
||||
Assert.NotNull(indicator.Description);
|
||||
Assert.NotEmpty(indicator.Description);
|
||||
Assert.Contains("pivot", indicator.Description, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
using System.Drawing;
|
||||
using System.Runtime.CompilerServices;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
[SkipLocalsInit]
|
||||
public sealed class PivotdemIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[InputParameter("Show cold values", sortIndex: 21)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private Pivotdem _indicator = null!;
|
||||
private readonly LineSeries _ppSeries;
|
||||
private readonly LineSeries _r1Series;
|
||||
private readonly LineSeries _s1Series;
|
||||
|
||||
public static int MinHistoryDepths => 0;
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => "PIVOTDEM";
|
||||
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/reversals/pivotdem/Pivotdem.cs";
|
||||
|
||||
public PivotdemIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = false;
|
||||
Name = "PIVOTDEM - DeMark Pivot Points";
|
||||
Description = "DeMark pivot points: 3 support/resistance levels (PP, R1, S1) with conditional logic based on Open vs Close.";
|
||||
|
||||
_ppSeries = new LineSeries(name: "PP", color: Color.Yellow, width: 2, style: LineStyle.Solid);
|
||||
_r1Series = new LineSeries(name: "R1", color: Color.FromArgb(255, 128, 128), width: 1, style: LineStyle.Solid);
|
||||
_s1Series = new LineSeries(name: "S1", color: Color.FromArgb(128, 255, 128), width: 1, style: LineStyle.Solid);
|
||||
|
||||
AddLineSeries(_ppSeries);
|
||||
AddLineSeries(_r1Series);
|
||||
AddLineSeries(_s1Series);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnInit()
|
||||
{
|
||||
_indicator = new Pivotdem();
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
_ = _indicator.Update(this.GetInputBar(args), args.IsNewBar());
|
||||
|
||||
_ppSeries.SetValue(_indicator.PP, _indicator.IsHot, ShowColdValues);
|
||||
_r1Series.SetValue(_indicator.R1, _indicator.IsHot, ShowColdValues);
|
||||
_s1Series.SetValue(_indicator.S1, _indicator.IsHot, ShowColdValues);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,533 @@
|
||||
// PIVOTDEM Tests - DeMark Pivot Points
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
// -- A) Constructor Validation ------------------------------------------------
|
||||
public sealed class PivotdemConstructorTests
|
||||
{
|
||||
[Fact]
|
||||
public void Constructor_Default_SetsProperties()
|
||||
{
|
||||
var p = new Pivotdem();
|
||||
|
||||
Assert.Equal(2, p.WarmupPeriod);
|
||||
Assert.Contains("Pivotdem", p.Name, StringComparison.Ordinal);
|
||||
Assert.False(p.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_InitialState_AllNaN()
|
||||
{
|
||||
var p = new Pivotdem();
|
||||
|
||||
Assert.True(double.IsNaN(p.PP));
|
||||
Assert.True(double.IsNaN(p.R1));
|
||||
Assert.True(double.IsNaN(p.S1));
|
||||
}
|
||||
}
|
||||
|
||||
// -- B) Basic Calculation -----------------------------------------------------
|
||||
public sealed class PivotdemBasicTests
|
||||
{
|
||||
[Fact]
|
||||
public void Update_ReturnsTValue()
|
||||
{
|
||||
var p = new Pivotdem();
|
||||
var bar = new TBar(DateTime.UtcNow, 100, 110, 90, 100, 1000);
|
||||
|
||||
TValue result = p.Update(bar);
|
||||
|
||||
Assert.IsType<TValue>(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_Last_IsAccessible()
|
||||
{
|
||||
var p = new Pivotdem();
|
||||
var bar = new TBar(DateTime.UtcNow, 100, 110, 90, 100, 1000);
|
||||
|
||||
_ = p.Update(bar);
|
||||
|
||||
Assert.True(double.IsFinite(p.Last.Value) || double.IsNaN(p.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_KnownValues_Bullish_CorrectLevels()
|
||||
{
|
||||
// Previous bar: O=100, H=110, L=90, C=105 => C>O (bullish)
|
||||
// x = 2*H + L + C = 220 + 90 + 105 = 415
|
||||
// PP = 415/4 = 103.75
|
||||
// R1 = 415/2 - L = 207.5 - 90 = 117.5
|
||||
// S1 = 415/2 - H = 207.5 - 110 = 97.5
|
||||
var p = new Pivotdem();
|
||||
var dt = DateTime.UtcNow;
|
||||
|
||||
// First bar: stores OHLC, no output yet
|
||||
_ = p.Update(new TBar(dt, 100, 110, 90, 105, 1000), isNew: true);
|
||||
Assert.True(double.IsNaN(p.PP));
|
||||
|
||||
// Second bar: computes from first bar's OHLC
|
||||
_ = p.Update(new TBar(dt.AddMinutes(1), 105, 115, 95, 108, 1000), isNew: true);
|
||||
|
||||
Assert.Equal(103.75, p.PP, precision: 10);
|
||||
Assert.Equal(117.5, p.R1, precision: 10);
|
||||
Assert.Equal(97.5, p.S1, precision: 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_KnownValues_Bearish_CorrectLevels()
|
||||
{
|
||||
// Previous bar: O=105, H=110, L=90, C=100 => C<O (bearish)
|
||||
// x = H + 2*L + C = 110 + 180 + 100 = 390
|
||||
// PP = 390/4 = 97.5
|
||||
// R1 = 390/2 - L = 195 - 90 = 105
|
||||
// S1 = 390/2 - H = 195 - 110 = 85
|
||||
var p = new Pivotdem();
|
||||
var dt = DateTime.UtcNow;
|
||||
|
||||
_ = p.Update(new TBar(dt, 105, 110, 90, 100, 1000), isNew: true);
|
||||
_ = p.Update(new TBar(dt.AddMinutes(1), 100, 115, 95, 102, 1000), isNew: true);
|
||||
|
||||
Assert.Equal(97.5, p.PP, precision: 10);
|
||||
Assert.Equal(105.0, p.R1, precision: 10);
|
||||
Assert.Equal(85.0, p.S1, precision: 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_KnownValues_Doji_CorrectLevels()
|
||||
{
|
||||
// Previous bar: O=100, H=110, L=90, C=100 => C==O (doji)
|
||||
// x = H + L + 2*C = 110 + 90 + 200 = 400
|
||||
// PP = 400/4 = 100
|
||||
// R1 = 400/2 - L = 200 - 90 = 110
|
||||
// S1 = 400/2 - H = 200 - 110 = 90
|
||||
var p = new Pivotdem();
|
||||
var dt = DateTime.UtcNow;
|
||||
|
||||
_ = p.Update(new TBar(dt, 100, 110, 90, 100, 1000), isNew: true);
|
||||
_ = p.Update(new TBar(dt.AddMinutes(1), 100, 115, 95, 102, 1000), isNew: true);
|
||||
|
||||
Assert.Equal(100.0, p.PP, precision: 10);
|
||||
Assert.Equal(110.0, p.R1, precision: 10);
|
||||
Assert.Equal(90.0, p.S1, precision: 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_LevelsHaveCorrectOrdering()
|
||||
{
|
||||
// For a normal bar with H > L, S1 < PP < R1
|
||||
var p = new Pivotdem();
|
||||
var dt = DateTime.UtcNow;
|
||||
|
||||
_ = p.Update(new TBar(dt, 100, 110, 90, 100, 1000), isNew: true);
|
||||
_ = p.Update(new TBar(dt.AddMinutes(1), 105, 115, 95, 105, 1000), isNew: true);
|
||||
|
||||
Assert.True(p.S1 < p.PP);
|
||||
Assert.True(p.PP < p.R1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Name_ContainsPivotdem()
|
||||
{
|
||||
var p = new Pivotdem();
|
||||
Assert.Contains("Pivotdem", p.Name, StringComparison.Ordinal);
|
||||
}
|
||||
}
|
||||
|
||||
// -- C) State + Bar Correction ------------------------------------------------
|
||||
public sealed class PivotdemStateCorrectionTests
|
||||
{
|
||||
[Fact]
|
||||
public void IsNew_True_AdvancesState()
|
||||
{
|
||||
var p = new Pivotdem();
|
||||
|
||||
_ = p.Update(new TBar(DateTime.UtcNow, 100, 110, 90, 100, 1000), isNew: true);
|
||||
var first = p.Last;
|
||||
|
||||
_ = p.Update(new TBar(DateTime.UtcNow.AddMinutes(1), 105, 115, 95, 105, 1000), isNew: true);
|
||||
var second = p.Last;
|
||||
|
||||
Assert.NotEqual(first.Time, second.Time);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNew_False_CorrectionRestoresState()
|
||||
{
|
||||
var p = new Pivotdem();
|
||||
var dt = DateTime.UtcNow;
|
||||
|
||||
// Two bars: first stores OHLC, second computes
|
||||
_ = p.Update(new TBar(dt, 100, 110, 90, 105, 1000), isNew: true);
|
||||
_ = p.Update(new TBar(dt.AddMinutes(1), 105, 115, 95, 105, 1000), isNew: true);
|
||||
double ppBefore = p.PP;
|
||||
|
||||
// Correct the second bar (isNew=false)
|
||||
_ = p.Update(new TBar(dt.AddMinutes(1), 108, 118, 92, 108, 1000), isNew: false);
|
||||
|
||||
// PP should still be based on bar 0's OHLC
|
||||
Assert.Equal(ppBefore, p.PP, precision: 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IterativeCorrections_ProduceSameResult()
|
||||
{
|
||||
var p = new Pivotdem();
|
||||
var dt = DateTime.UtcNow;
|
||||
|
||||
_ = p.Update(new TBar(dt, 100, 110, 90, 105, 1000), isNew: true);
|
||||
_ = p.Update(new TBar(dt.AddMinutes(1), 105, 115, 95, 105, 1000), isNew: true);
|
||||
|
||||
double[] ppResults = new double[3];
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
_ = p.Update(new TBar(dt.AddMinutes(1), 108, 120, 88, 110, 1000), isNew: false);
|
||||
ppResults[i] = p.PP;
|
||||
}
|
||||
|
||||
Assert.Equal(ppResults[0], ppResults[1]);
|
||||
Assert.Equal(ppResults[1], ppResults[2]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNew_False_AllLevelsStable()
|
||||
{
|
||||
var p = new Pivotdem();
|
||||
var dt = DateTime.UtcNow;
|
||||
|
||||
_ = p.Update(new TBar(dt, 100, 110, 90, 105, 1000), isNew: true);
|
||||
_ = p.Update(new TBar(dt.AddMinutes(1), 105, 115, 95, 105, 1000), isNew: true);
|
||||
|
||||
_ = p.Update(new TBar(dt.AddMinutes(1), 108, 120, 88, 110, 1000), isNew: false);
|
||||
double r1a = p.R1, s1a = p.S1;
|
||||
|
||||
_ = p.Update(new TBar(dt.AddMinutes(1), 108, 120, 88, 110, 1000), isNew: false);
|
||||
Assert.Equal(r1a, p.R1);
|
||||
Assert.Equal(s1a, p.S1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reset_ClearsAllState()
|
||||
{
|
||||
var p = new Pivotdem();
|
||||
var dt = DateTime.UtcNow;
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
double price = 100.0 + i;
|
||||
_ = p.Update(new TBar(dt.AddMinutes(i), price, price + 5, price - 5, price + 1, 1000));
|
||||
}
|
||||
|
||||
Assert.True(p.IsHot);
|
||||
|
||||
p.Reset();
|
||||
|
||||
Assert.False(p.IsHot);
|
||||
Assert.True(double.IsNaN(p.PP));
|
||||
Assert.True(double.IsNaN(p.R1));
|
||||
Assert.True(double.IsNaN(p.S1));
|
||||
}
|
||||
}
|
||||
|
||||
// -- D) Warmup / Convergence --------------------------------------------------
|
||||
public sealed class PivotdemWarmupTests
|
||||
{
|
||||
[Fact]
|
||||
public void IsHot_FlipsAfterWarmup()
|
||||
{
|
||||
var p = new Pivotdem();
|
||||
|
||||
// First bar - not hot
|
||||
_ = p.Update(new TBar(DateTime.UtcNow, 100, 110, 90, 100, 1000));
|
||||
Assert.False(p.IsHot, "Should not be hot after 1 bar");
|
||||
|
||||
// Second bar - should be hot
|
||||
_ = p.Update(new TBar(DateTime.UtcNow.AddMinutes(1), 105, 115, 95, 105, 1000));
|
||||
Assert.True(p.IsHot, "Should be hot after 2 bars");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WarmupPeriod_Equals2()
|
||||
{
|
||||
var p = new Pivotdem();
|
||||
Assert.Equal(2, p.WarmupPeriod);
|
||||
}
|
||||
}
|
||||
|
||||
// -- E) Robustness ------------------------------------------------------------
|
||||
public sealed class PivotdemRobustnessTests
|
||||
{
|
||||
[Fact]
|
||||
public void NaN_Input_UsesLastValidValue()
|
||||
{
|
||||
var p = new Pivotdem();
|
||||
var dt = DateTime.UtcNow;
|
||||
|
||||
// Feed valid bars
|
||||
_ = p.Update(new TBar(dt, 100, 110, 90, 105, 1000), isNew: true);
|
||||
_ = p.Update(new TBar(dt.AddMinutes(1), 105, 115, 95, 105, 1000), isNew: true);
|
||||
|
||||
Assert.True(p.IsHot);
|
||||
|
||||
// Feed NaN bar
|
||||
_ = p.Update(new TBar(dt.AddMinutes(2), double.NaN, double.NaN, double.NaN, double.NaN, 0), isNew: true);
|
||||
|
||||
// Should still be hot and produce valid pivots from last-valid values
|
||||
Assert.True(p.IsHot);
|
||||
Assert.True(double.IsFinite(p.PP));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Infinity_Input_UsesLastValidValue()
|
||||
{
|
||||
var p = new Pivotdem();
|
||||
var dt = DateTime.UtcNow;
|
||||
|
||||
_ = p.Update(new TBar(dt, 100, 110, 90, 105, 1000), isNew: true);
|
||||
_ = p.Update(new TBar(dt.AddMinutes(1), 105, 115, 95, 105, 1000), isNew: true);
|
||||
|
||||
_ = p.Update(new TBar(dt.AddMinutes(2),
|
||||
double.PositiveInfinity, double.PositiveInfinity, double.NegativeInfinity, double.PositiveInfinity, 0),
|
||||
isNew: true);
|
||||
|
||||
Assert.True(p.IsHot);
|
||||
Assert.True(double.IsFinite(p.PP));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FirstBar_NaN_ReturnsNaN()
|
||||
{
|
||||
var p = new Pivotdem();
|
||||
|
||||
_ = p.Update(new TBar(DateTime.UtcNow, double.NaN, double.NaN, double.NaN, double.NaN, 0));
|
||||
|
||||
Assert.True(double.IsNaN(p.Last.Value));
|
||||
Assert.True(double.IsNaN(p.PP));
|
||||
}
|
||||
}
|
||||
|
||||
// -- F) Consistency -----------------------------------------------------------
|
||||
public sealed class PivotdemConsistencyTests
|
||||
{
|
||||
private static TBarSeries CreateGbmBars(int count = 500)
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.05, sigma: 0.20, seed: 42);
|
||||
return gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Streaming_MatchesBatch()
|
||||
{
|
||||
var bars = CreateGbmBars();
|
||||
|
||||
// Streaming
|
||||
var streaming = new Pivotdem();
|
||||
var streamPP = new double[bars.Count];
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
_ = streaming.Update(bars[i], isNew: true);
|
||||
streamPP[i] = streaming.PP;
|
||||
}
|
||||
|
||||
// Batch
|
||||
var batchResults = Pivotdem.Batch(bars);
|
||||
|
||||
for (int i = 1; i < bars.Count; i++)
|
||||
{
|
||||
if (double.IsNaN(streamPP[i]))
|
||||
{
|
||||
Assert.True(double.IsNaN(batchResults[i].Value), $"Mismatch at {i}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.Equal(streamPP[i], batchResults[i].Value, precision: 10);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Streaming_MatchesSpan()
|
||||
{
|
||||
var bars = CreateGbmBars();
|
||||
|
||||
// Streaming
|
||||
var streaming = new Pivotdem();
|
||||
var streamPP = new double[bars.Count];
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
_ = streaming.Update(bars[i], isNew: true);
|
||||
streamPP[i] = streaming.PP;
|
||||
}
|
||||
|
||||
// Span
|
||||
var spanPP = new double[bars.Count];
|
||||
Pivotdem.Batch(bars.OpenValues, bars.HighValues, bars.LowValues, bars.CloseValues, spanPP);
|
||||
|
||||
for (int i = 1; i < bars.Count; i++)
|
||||
{
|
||||
if (double.IsNaN(streamPP[i]))
|
||||
{
|
||||
Assert.True(double.IsNaN(spanPP[i]), $"PP mismatch at {i}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.Equal(streamPP[i], spanPP[i], precision: 10);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Streaming_MatchesBatchAll_AllLevels()
|
||||
{
|
||||
var bars = CreateGbmBars(count: 200);
|
||||
|
||||
// Streaming
|
||||
var streaming = new Pivotdem();
|
||||
var sPP = new double[bars.Count];
|
||||
var sR1 = new double[bars.Count];
|
||||
var sS1 = new double[bars.Count];
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
_ = streaming.Update(bars[i], isNew: true);
|
||||
sPP[i] = streaming.PP;
|
||||
sR1[i] = streaming.R1;
|
||||
sS1[i] = streaming.S1;
|
||||
}
|
||||
|
||||
// BatchAll
|
||||
var bPP = new double[bars.Count];
|
||||
var bR1 = new double[bars.Count];
|
||||
var bS1 = new double[bars.Count];
|
||||
|
||||
Pivotdem.BatchAll(bars.OpenValues, bars.HighValues, bars.LowValues, bars.CloseValues,
|
||||
bPP, bR1, bS1);
|
||||
|
||||
for (int i = 1; i < bars.Count; i++)
|
||||
{
|
||||
if (double.IsNaN(sPP[i])) { Assert.True(double.IsNaN(bPP[i])); continue; }
|
||||
|
||||
Assert.Equal(sPP[i], bPP[i], precision: 10);
|
||||
Assert.Equal(sR1[i], bR1[i], precision: 10);
|
||||
Assert.Equal(sS1[i], bS1[i], precision: 10);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TValue_Update_MatchesTBar_Update()
|
||||
{
|
||||
var p1 = new Pivotdem();
|
||||
var p2 = new Pivotdem();
|
||||
|
||||
double[] prices = [100, 102, 98, 105, 99, 103, 107, 95, 110, 108];
|
||||
|
||||
for (int i = 0; i < prices.Length; i++)
|
||||
{
|
||||
double pr = prices[i];
|
||||
_ = p1.Update(new TBar(DateTime.UtcNow.AddMinutes(i), pr, pr, pr, pr, 0), isNew: true);
|
||||
_ = p2.Update(new TValue(DateTime.UtcNow.AddMinutes(i), pr), isNew: true);
|
||||
}
|
||||
|
||||
Assert.Equal(p1.PP, p2.PP);
|
||||
Assert.Equal(p1.R1, p2.R1);
|
||||
Assert.Equal(p1.S1, p2.S1);
|
||||
}
|
||||
}
|
||||
|
||||
// -- G) Span API Tests --------------------------------------------------------
|
||||
public sealed class PivotdemSpanTests
|
||||
{
|
||||
[Fact]
|
||||
public void Batch_Span_MismatchedLengths_Throws()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() =>
|
||||
Pivotdem.Batch(new double[10], new double[5], new double[10], new double[10], new double[10]));
|
||||
Assert.Equal("high", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_Span_OutputTooShort_Throws()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() =>
|
||||
Pivotdem.Batch(new double[10], new double[10], new double[10], new double[10], new double[5]));
|
||||
Assert.Equal("ppOutput", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_Span_Empty_NoException()
|
||||
{
|
||||
var ex = Record.Exception(() =>
|
||||
Pivotdem.Batch(ReadOnlySpan<double>.Empty, ReadOnlySpan<double>.Empty,
|
||||
ReadOnlySpan<double>.Empty, ReadOnlySpan<double>.Empty, Span<double>.Empty));
|
||||
Assert.Null(ex);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BatchAll_OutputTooShort_Throws()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() =>
|
||||
Pivotdem.BatchAll(new double[10], new double[10], new double[10], new double[10],
|
||||
new double[10], new double[5], new double[10]));
|
||||
Assert.Equal("r1Out", ex.ParamName);
|
||||
}
|
||||
}
|
||||
|
||||
// -- H) Event / Chainability -------------------------------------------------
|
||||
public sealed class PivotdemEventTests
|
||||
{
|
||||
[Fact]
|
||||
public void Pub_FiresOnUpdate()
|
||||
{
|
||||
var p = new Pivotdem();
|
||||
int fireCount = 0;
|
||||
|
||||
p.Pub += (object? _, in TValueEventArgs _e) => { fireCount++; };
|
||||
|
||||
_ = p.Update(new TBar(DateTime.UtcNow, 100, 110, 90, 100, 1000));
|
||||
|
||||
Assert.Equal(1, fireCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pub_FiresOnEachUpdate()
|
||||
{
|
||||
var p = new Pivotdem();
|
||||
int fireCount = 0;
|
||||
|
||||
p.Pub += (object? _, in TValueEventArgs _e) => { fireCount++; };
|
||||
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
double price = 100.0 + i;
|
||||
_ = p.Update(new TBar(DateTime.UtcNow.AddMinutes(i), price, price + 5, price - 5, price + 1, 1000));
|
||||
}
|
||||
|
||||
Assert.Equal(5, fireCount);
|
||||
}
|
||||
}
|
||||
|
||||
// -- I) Prime Tests -----------------------------------------------------------
|
||||
public sealed class PivotdemPrimeTests
|
||||
{
|
||||
[Fact]
|
||||
public void Prime_TBarSeries_SetsState()
|
||||
{
|
||||
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 p = new Pivotdem();
|
||||
p.Prime(bars);
|
||||
|
||||
Assert.True(p.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Prime_EmptySource_NoException()
|
||||
{
|
||||
var p = new Pivotdem();
|
||||
var bars = new TBarSeries();
|
||||
|
||||
var ex = Record.Exception(() => p.Prime(bars));
|
||||
Assert.Null(ex);
|
||||
Assert.False(p.IsHot);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
// PIVOTDEM Validation Tests - DeMark Pivot Points
|
||||
// Self-consistency validation across all API modes.
|
||||
//
|
||||
// Note: No external libraries (Skender, TA-Lib, Tulip, Ooples) implement
|
||||
// DeMark pivot points. Validation focuses on mathematical correctness,
|
||||
// conditional logic verification, and mode consistency.
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public sealed class PivotdemValidationTests
|
||||
{
|
||||
private static TBarSeries CreateGbmBars(int count = 500, int seed = 42)
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.05, sigma: 0.20, seed: seed);
|
||||
return gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
}
|
||||
|
||||
// -- Mathematical Correctness -------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void MathCorrectness_PP_MatchesConditionalFormula()
|
||||
{
|
||||
var bars = CreateGbmBars(count: 100);
|
||||
var p = new Pivotdem();
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
_ = p.Update(bars[i], isNew: true);
|
||||
|
||||
if (i >= 1) // Need previous bar
|
||||
{
|
||||
double pO = bars[i - 1].Open;
|
||||
double pH = bars[i - 1].High;
|
||||
double pL = bars[i - 1].Low;
|
||||
double pC = bars[i - 1].Close;
|
||||
|
||||
double x;
|
||||
if (pC < pO) { x = pH + 2.0 * pL + pC; }
|
||||
else if (pC > pO) { x = 2.0 * pH + pL + pC; }
|
||||
else { x = pH + pL + 2.0 * pC; }
|
||||
|
||||
double expectedPP = x * 0.25;
|
||||
Assert.Equal(expectedPP, p.PP, precision: 10);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MathCorrectness_AllLevels_MatchFormula()
|
||||
{
|
||||
var bars = CreateGbmBars(count: 100);
|
||||
var p = new Pivotdem();
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
_ = p.Update(bars[i], isNew: true);
|
||||
|
||||
if (i >= 1)
|
||||
{
|
||||
double pO = bars[i - 1].Open;
|
||||
double pH = bars[i - 1].High;
|
||||
double pL = bars[i - 1].Low;
|
||||
double pC = bars[i - 1].Close;
|
||||
|
||||
double x;
|
||||
if (pC < pO) { x = pH + 2.0 * pL + pC; }
|
||||
else if (pC > pO) { x = 2.0 * pH + pL + pC; }
|
||||
else { x = pH + pL + 2.0 * pC; }
|
||||
|
||||
double halfX = x * 0.5;
|
||||
Assert.Equal(x * 0.25, p.PP, precision: 10);
|
||||
Assert.Equal(halfX - pL, p.R1, precision: 10);
|
||||
Assert.Equal(halfX - pH, p.S1, precision: 10);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -- Self-Consistency: Streaming == Batch --------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void StreamingMatchesBatch_PP()
|
||||
{
|
||||
var bars = CreateGbmBars();
|
||||
|
||||
// Streaming
|
||||
var streaming = new Pivotdem();
|
||||
var streamPP = new double[bars.Count];
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
_ = streaming.Update(bars[i], isNew: true);
|
||||
streamPP[i] = streaming.PP;
|
||||
}
|
||||
|
||||
// Batch
|
||||
var batchResults = Pivotdem.Batch(bars);
|
||||
|
||||
for (int i = 1; i < bars.Count; i++)
|
||||
{
|
||||
if (double.IsNaN(streamPP[i]))
|
||||
{
|
||||
Assert.True(double.IsNaN(batchResults[i].Value),
|
||||
$"Mismatch at {i}: streaming=NaN, batch={batchResults[i].Value}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.Equal(streamPP[i], batchResults[i].Value, precision: 10);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -- Self-Consistency: Streaming == Span ---------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void StreamingMatchesSpan_PP()
|
||||
{
|
||||
var bars = CreateGbmBars();
|
||||
|
||||
// Streaming
|
||||
var streaming = new Pivotdem();
|
||||
var streamPP = new double[bars.Count];
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
_ = streaming.Update(bars[i], isNew: true);
|
||||
streamPP[i] = streaming.PP;
|
||||
}
|
||||
|
||||
// Span
|
||||
var spanPP = new double[bars.Count];
|
||||
Pivotdem.Batch(bars.OpenValues, bars.HighValues, bars.LowValues, bars.CloseValues, spanPP);
|
||||
|
||||
for (int i = 1; i < bars.Count; i++)
|
||||
{
|
||||
if (double.IsNaN(streamPP[i]))
|
||||
{
|
||||
Assert.True(double.IsNaN(spanPP[i]));
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.Equal(streamPP[i], spanPP[i], precision: 10);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -- Self-Consistency: Streaming == BatchAll (all 3 levels) --------------------
|
||||
|
||||
[Fact]
|
||||
public void StreamingMatchesBatchAll_AllLevels()
|
||||
{
|
||||
var bars = CreateGbmBars(count: 300);
|
||||
|
||||
// Streaming
|
||||
var streaming = new Pivotdem();
|
||||
var sPP = new double[bars.Count];
|
||||
var sR1 = new double[bars.Count];
|
||||
var sS1 = new double[bars.Count];
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
_ = streaming.Update(bars[i], isNew: true);
|
||||
sPP[i] = streaming.PP;
|
||||
sR1[i] = streaming.R1;
|
||||
sS1[i] = streaming.S1;
|
||||
}
|
||||
|
||||
// BatchAll
|
||||
var bPP = new double[bars.Count];
|
||||
var bR1 = new double[bars.Count];
|
||||
var bS1 = new double[bars.Count];
|
||||
|
||||
Pivotdem.BatchAll(bars.OpenValues, bars.HighValues, bars.LowValues, bars.CloseValues,
|
||||
bPP, bR1, bS1);
|
||||
|
||||
for (int i = 1; i < bars.Count; i++)
|
||||
{
|
||||
if (double.IsNaN(sPP[i]))
|
||||
{
|
||||
Assert.True(double.IsNaN(bPP[i]));
|
||||
continue;
|
||||
}
|
||||
|
||||
Assert.Equal(sPP[i], bPP[i], precision: 10);
|
||||
Assert.Equal(sR1[i], bR1[i], precision: 10);
|
||||
Assert.Equal(sS1[i], bS1[i], precision: 10);
|
||||
}
|
||||
}
|
||||
|
||||
// -- Determinism ---------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void SameInput_ProducesSameOutput()
|
||||
{
|
||||
var bars = CreateGbmBars(count: 200, seed: 123);
|
||||
|
||||
var p1 = new Pivotdem();
|
||||
var p2 = new Pivotdem();
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
_ = p1.Update(bars[i], isNew: true);
|
||||
_ = p2.Update(bars[i], isNew: true);
|
||||
}
|
||||
|
||||
Assert.Equal(p1.PP, p2.PP);
|
||||
Assert.Equal(p1.R1, p2.R1);
|
||||
Assert.Equal(p1.S1, p2.S1);
|
||||
}
|
||||
|
||||
// -- Calculate Returns Valid Indicator -----------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void Calculate_ReturnsValidIndicatorAndResults()
|
||||
{
|
||||
var bars = CreateGbmBars(count: 100);
|
||||
|
||||
var (results, indicator) = Pivotdem.Calculate(bars);
|
||||
|
||||
Assert.NotNull(results);
|
||||
Assert.Equal(bars.Count, results.Count);
|
||||
Assert.True(indicator.IsHot);
|
||||
}
|
||||
|
||||
// -- Level Ordering Invariant --------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void AllBars_LevelsOrdered_S1_PP_R1()
|
||||
{
|
||||
var bars = CreateGbmBars(count: 200);
|
||||
var p = new Pivotdem();
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
_ = p.Update(bars[i], isNew: true);
|
||||
|
||||
if (p.IsHot)
|
||||
{
|
||||
Assert.True(p.S1 <= p.PP, $"S1 > PP at bar {i}");
|
||||
Assert.True(p.PP <= p.R1, $"PP > R1 at bar {i}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,423 @@
|
||||
// PIVOTDEM: DeMark Pivot Points
|
||||
// Calculates 3 support/resistance levels from previous bar's OHLC.
|
||||
// Uses conditional logic based on Open vs Close relationship.
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// PIVOTDEM: DeMark Pivot Points
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Computes 3 horizontal support/resistance levels from the previous bar's
|
||||
/// open, high, low, and close. The key innovation is the conditional calculation
|
||||
/// of the intermediate value X, which varies depending on the relationship
|
||||
/// between open and close, weighting different price components accordingly.
|
||||
///
|
||||
/// Calculation (using previous bar's O, H, L, C):
|
||||
/// <code>
|
||||
/// If C < O: X = H + 2L + C
|
||||
/// If C > O: X = 2H + L + C
|
||||
/// If C == O: X = H + L + 2C
|
||||
///
|
||||
/// PP = X / 4
|
||||
/// R1 = X / 2 − L
|
||||
/// S1 = X / 2 − H
|
||||
/// </code>
|
||||
///
|
||||
/// <b>Key characteristics:</b>
|
||||
/// - O(1) computation: pure arithmetic from previous bar's OHLC
|
||||
/// - 3 outputs: PP, R1, S1 (minimalist)
|
||||
/// - WarmupPeriod = 2 (need previous bar's OHLC)
|
||||
/// - No configurable parameters
|
||||
/// - Conditional weighting: bearish bars weight Low, bullish bars weight High
|
||||
/// - Only pivot variant that uses Open in the calculation
|
||||
/// </remarks>
|
||||
/// <seealso href="Pivotdem.md">Detailed documentation</seealso>
|
||||
[SkipLocalsInit]
|
||||
public sealed class Pivotdem : ITValuePublisher
|
||||
{
|
||||
[StructLayout(LayoutKind.Auto)]
|
||||
private record struct State(
|
||||
double PrevOpen,
|
||||
double PrevHigh,
|
||||
double PrevLow,
|
||||
double PrevClose,
|
||||
double LastValidOpen,
|
||||
double LastValidHigh,
|
||||
double LastValidLow,
|
||||
double LastValidClose);
|
||||
|
||||
private State _s;
|
||||
private State _ps;
|
||||
private int _count;
|
||||
|
||||
private readonly TBarPublishedHandler _barHandler;
|
||||
|
||||
/// <summary>Display name for the indicator.</summary>
|
||||
public string Name { get; }
|
||||
|
||||
/// <summary>Bars required for the indicator to warm up.</summary>
|
||||
public int WarmupPeriod { get; }
|
||||
|
||||
/// <summary>Central Pivot Point: X / 4 (conditionally weighted)</summary>
|
||||
public double PP { get; private set; }
|
||||
|
||||
/// <summary>Resistance 1: X / 2 − prevLow</summary>
|
||||
public double R1 { get; private set; }
|
||||
|
||||
/// <summary>Support 1: X / 2 − prevHigh</summary>
|
||||
public double S1 { get; private set; }
|
||||
|
||||
/// <summary>Primary output value (PP as TValue).</summary>
|
||||
public TValue Last { get; private set; }
|
||||
|
||||
/// <summary>True when enough bars have been processed for valid output.</summary>
|
||||
public bool IsHot => _count >= 2;
|
||||
|
||||
public event TValuePublishedHandler? Pub;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a DeMark Pivot Points indicator.
|
||||
/// </summary>
|
||||
public Pivotdem()
|
||||
{
|
||||
_count = 0;
|
||||
_s = new State(double.NaN, double.NaN, double.NaN, double.NaN,
|
||||
double.NaN, double.NaN, double.NaN, double.NaN);
|
||||
_ps = _s;
|
||||
|
||||
SetAllNaN();
|
||||
|
||||
Name = "Pivotdem";
|
||||
WarmupPeriod = 2;
|
||||
_barHandler = HandleBar;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a DeMark Pivot Points indicator chained to a TBarSeries source.
|
||||
/// </summary>
|
||||
public Pivotdem(TBarSeries source)
|
||||
: this()
|
||||
{
|
||||
Prime(source);
|
||||
source.Pub += _barHandler;
|
||||
}
|
||||
|
||||
private void HandleBar(object? sender, in TBarEventArgs e) => Update(e.Value, e.IsNew);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void PubEvent(TValue value, bool isNew = true) =>
|
||||
Pub?.Invoke(this, new TValueEventArgs { Value = value, IsNew = isNew });
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TValue Update(TBar input, bool isNew = true)
|
||||
{
|
||||
if (isNew)
|
||||
{
|
||||
_ps = _s;
|
||||
_count++;
|
||||
}
|
||||
else
|
||||
{
|
||||
_s = _ps;
|
||||
}
|
||||
|
||||
var s = _s;
|
||||
|
||||
// Validate inputs - substitute last-valid on NaN/Infinity
|
||||
double open = input.Open;
|
||||
double high = input.High;
|
||||
double low = input.Low;
|
||||
double close = input.Close;
|
||||
|
||||
if (double.IsFinite(open)) { s.LastValidOpen = open; }
|
||||
else { open = s.LastValidOpen; }
|
||||
|
||||
if (double.IsFinite(high)) { s.LastValidHigh = high; }
|
||||
else { high = s.LastValidHigh; }
|
||||
|
||||
if (double.IsFinite(low)) { s.LastValidLow = low; }
|
||||
else { low = s.LastValidLow; }
|
||||
|
||||
if (double.IsFinite(close)) { s.LastValidClose = close; }
|
||||
else { close = s.LastValidClose; }
|
||||
|
||||
// If still no valid data, return NaN
|
||||
if (double.IsNaN(open) || double.IsNaN(high) || double.IsNaN(low) || double.IsNaN(close))
|
||||
{
|
||||
_s = s;
|
||||
SetAllNaN();
|
||||
Last = new TValue(input.Time, double.NaN);
|
||||
PubEvent(Last, isNew);
|
||||
return Last;
|
||||
}
|
||||
|
||||
// First bar: store OHLC but cannot compute pivots yet (no previous bar)
|
||||
if (_count < 2)
|
||||
{
|
||||
s.PrevOpen = open;
|
||||
s.PrevHigh = high;
|
||||
s.PrevLow = low;
|
||||
s.PrevClose = close;
|
||||
_s = s;
|
||||
SetAllNaN();
|
||||
Last = new TValue(input.Time, double.NaN);
|
||||
PubEvent(Last, isNew);
|
||||
return Last;
|
||||
}
|
||||
|
||||
// Compute DeMark pivot levels from PREVIOUS bar's OHLC
|
||||
double pO = s.PrevOpen;
|
||||
double pH = s.PrevHigh;
|
||||
double pL = s.PrevLow;
|
||||
double pC = s.PrevClose;
|
||||
|
||||
// Conditional X calculation
|
||||
double x;
|
||||
if (pC < pO)
|
||||
{
|
||||
x = pH + 2.0 * pL + pC; // Bearish: weight Low
|
||||
}
|
||||
else if (pC > pO)
|
||||
{
|
||||
x = 2.0 * pH + pL + pC; // Bullish: weight High
|
||||
}
|
||||
else
|
||||
{
|
||||
x = pH + pL + 2.0 * pC; // Doji: weight Close
|
||||
}
|
||||
|
||||
double halfX = x * 0.5;
|
||||
PP = x * 0.25;
|
||||
R1 = halfX - pL;
|
||||
S1 = halfX - pH;
|
||||
|
||||
// Store current bar's OHLC as "previous" for next bar
|
||||
s.PrevOpen = open;
|
||||
s.PrevHigh = high;
|
||||
s.PrevLow = low;
|
||||
s.PrevClose = close;
|
||||
_s = s;
|
||||
|
||||
Last = new TValue(input.Time, PP);
|
||||
PubEvent(Last, isNew);
|
||||
return Last;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TValue Update(TValue input, bool isNew = true) =>
|
||||
Update(new TBar(input.Time, input.Value, input.Value, input.Value, input.Value, 0), isNew);
|
||||
|
||||
public TSeries Update(TBarSeries source)
|
||||
{
|
||||
if (source.Count == 0)
|
||||
{
|
||||
return new TSeries([], []);
|
||||
}
|
||||
|
||||
int len = source.Count;
|
||||
var t = new List<long>(len);
|
||||
var v = new List<double>(len);
|
||||
|
||||
CollectionsMarshal.SetCount(t, len);
|
||||
CollectionsMarshal.SetCount(v, len);
|
||||
|
||||
Batch(source.OpenValues, source.HighValues, source.LowValues, source.CloseValues,
|
||||
CollectionsMarshal.AsSpan(v));
|
||||
|
||||
source.Times.CopyTo(CollectionsMarshal.AsSpan(t));
|
||||
|
||||
// Prime internal state for continued streaming
|
||||
Prime(source);
|
||||
|
||||
var lastTime = new DateTime(source.Times[^1], DateTimeKind.Utc);
|
||||
Last = new TValue(lastTime, CollectionsMarshal.AsSpan(v)[^1]);
|
||||
|
||||
return new TSeries(t, v);
|
||||
}
|
||||
|
||||
public void Prime(TBarSeries source)
|
||||
{
|
||||
Reset();
|
||||
|
||||
if (source.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
Update(source[i], isNew: true);
|
||||
}
|
||||
}
|
||||
|
||||
public void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
|
||||
{
|
||||
Reset();
|
||||
|
||||
if (source.Length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
long t = DateTime.UtcNow.Ticks;
|
||||
long stepTicks = (step ?? TimeSpan.FromMinutes(1)).Ticks;
|
||||
|
||||
for (int i = 0; i < source.Length; i++)
|
||||
{
|
||||
double val = source[i];
|
||||
Update(new TBar(t, val, val, val, val, 0), isNew: true);
|
||||
t += stepTicks;
|
||||
}
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
_count = 0;
|
||||
_s = new State(double.NaN, double.NaN, double.NaN, double.NaN,
|
||||
double.NaN, double.NaN, double.NaN, double.NaN);
|
||||
_ps = _s;
|
||||
SetAllNaN();
|
||||
Last = default;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void SetAllNaN()
|
||||
{
|
||||
PP = double.NaN;
|
||||
R1 = double.NaN;
|
||||
S1 = double.NaN;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Batch computation of DeMark Pivot Points over span data.
|
||||
/// Writes PP values to <paramref name="ppOutput"/>.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void Batch(
|
||||
ReadOnlySpan<double> open,
|
||||
ReadOnlySpan<double> high,
|
||||
ReadOnlySpan<double> low,
|
||||
ReadOnlySpan<double> close,
|
||||
Span<double> ppOutput)
|
||||
{
|
||||
if (open.Length != high.Length || high.Length != low.Length || high.Length != close.Length)
|
||||
{
|
||||
throw new ArgumentException("Input spans must have the same length.", nameof(high));
|
||||
}
|
||||
if (ppOutput.Length < high.Length)
|
||||
{
|
||||
throw new ArgumentException("Output span must be at least as long as input.", nameof(ppOutput));
|
||||
}
|
||||
|
||||
int len = high.Length;
|
||||
if (len == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// First bar: no previous data
|
||||
ppOutput[0] = double.NaN;
|
||||
|
||||
// Remaining bars: compute from previous bar's OHLC
|
||||
for (int i = 1; i < len; i++)
|
||||
{
|
||||
double pO = open[i - 1];
|
||||
double pH = high[i - 1];
|
||||
double pL = low[i - 1];
|
||||
double pC = close[i - 1];
|
||||
|
||||
double x;
|
||||
if (pC < pO) { x = pH + 2.0 * pL + pC; }
|
||||
else if (pC > pO) { x = 2.0 * pH + pL + pC; }
|
||||
else { x = pH + pL + 2.0 * pC; }
|
||||
|
||||
ppOutput[i] = x * 0.25;
|
||||
}
|
||||
}
|
||||
|
||||
public static TSeries Batch(TBarSeries source)
|
||||
{
|
||||
if (source == null || source.Count == 0)
|
||||
{
|
||||
return new TSeries([], []);
|
||||
}
|
||||
|
||||
int len = source.Count;
|
||||
var t = new List<long>(len);
|
||||
var v = new List<double>(len);
|
||||
|
||||
CollectionsMarshal.SetCount(t, len);
|
||||
CollectionsMarshal.SetCount(v, len);
|
||||
|
||||
Batch(source.OpenValues, source.HighValues, source.LowValues, source.CloseValues,
|
||||
CollectionsMarshal.AsSpan(v));
|
||||
|
||||
source.Times.CopyTo(CollectionsMarshal.AsSpan(t));
|
||||
|
||||
return new TSeries(t, v);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Batch computation of all 3 DeMark Pivot Point levels over span data.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void BatchAll(
|
||||
ReadOnlySpan<double> open,
|
||||
ReadOnlySpan<double> high,
|
||||
ReadOnlySpan<double> low,
|
||||
ReadOnlySpan<double> close,
|
||||
Span<double> ppOut,
|
||||
Span<double> r1Out,
|
||||
Span<double> s1Out)
|
||||
{
|
||||
if (open.Length != high.Length || high.Length != low.Length || high.Length != close.Length)
|
||||
{
|
||||
throw new ArgumentException("Input spans must have the same length.", nameof(high));
|
||||
}
|
||||
|
||||
int len = high.Length;
|
||||
|
||||
if (ppOut.Length < len) { throw new ArgumentException("Output span too short.", nameof(ppOut)); }
|
||||
if (r1Out.Length < len) { throw new ArgumentException("Output span too short.", nameof(r1Out)); }
|
||||
if (s1Out.Length < len) { throw new ArgumentException("Output span too short.", nameof(s1Out)); }
|
||||
|
||||
if (len == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// First bar: no previous data
|
||||
ppOut[0] = double.NaN;
|
||||
r1Out[0] = double.NaN;
|
||||
s1Out[0] = double.NaN;
|
||||
|
||||
for (int i = 1; i < len; i++)
|
||||
{
|
||||
double pO = open[i - 1];
|
||||
double pH = high[i - 1];
|
||||
double pL = low[i - 1];
|
||||
double pC = close[i - 1];
|
||||
|
||||
double x;
|
||||
if (pC < pO) { x = pH + 2.0 * pL + pC; }
|
||||
else if (pC > pO) { x = 2.0 * pH + pL + pC; }
|
||||
else { x = pH + pL + 2.0 * pC; }
|
||||
|
||||
double halfX = x * 0.5;
|
||||
ppOut[i] = x * 0.25;
|
||||
r1Out[i] = halfX - pL;
|
||||
s1Out[i] = halfX - pH;
|
||||
}
|
||||
}
|
||||
|
||||
public static (TSeries Results, Pivotdem Indicator) Calculate(TBarSeries source)
|
||||
{
|
||||
var indicator = new Pivotdem();
|
||||
var results = indicator.Update(source);
|
||||
return (results, indicator);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
# PIVOTDEM: DeMark Pivot Points
|
||||
|
||||
> "Most pivot formulas treat every bar the same. DeMark looked at the open-close relationship and asked: why would a bearish bar predict the same levels as a bullish one?"
|
||||
|
||||
DeMark Pivot Points calculate three horizontal support and resistance levels from the previous bar's open, high, low, and close. The defining characteristic is a conditional intermediate value X that changes its weighting depending on whether the prior bar closed below, above, or equal to its open. Bearish bars weight the low; bullish bars weight the high; doji bars weight the close. Three levels (PP, R1, S1) emerge from this single conditional calculation. The only pivot variant that uses the open price.
|
||||
|
||||
## Historical Context
|
||||
|
||||
Tom DeMark introduced his pivot point variant as part of a broader system of conditional indicators published in *The New Science of Technical Analysis* (1994) and *New Market Timing Techniques* (1997). Where floor trader pivots summarize the prior bar with an equal-weight HLC average, DeMark argued that the relationship between open and close carries directional information that should influence the levels.
|
||||
|
||||
The logic is straightforward: if the bar closed below the open (bearish), the low was more "tested" and should carry more weight. If the bar closed above the open (bullish), the high was more relevant. If open equals close (a doji), the close itself — representing the equilibrium point where neither bulls nor bears won — gets the extra weight.
|
||||
|
||||
This conditional approach differs from all other pivot variants (Classic, Woodie, Camarilla, Fibonacci) which apply the same formula regardless of bar direction. DeMark's innovation was treating the prior bar as a signal, not just a data source.
|
||||
|
||||
The tradeoff is minimalism: DeMark produces only 3 levels (PP, R1, S1) compared to the Classic formula's 7 or Camarilla's 9. What you lose in level density you gain in directional sensitivity. The formula adapts to bar structure rather than imposing a fixed geometry.
|
||||
|
||||
## Architecture and Physics
|
||||
|
||||
### 1. Previous Bar's OHLC
|
||||
|
||||
The indicator stores the open ($O$), high ($H$), low ($L$), and close ($C$) of the most recently completed bar. On each new bar, these stored values become the basis for computing the current bar's pivot levels. This is the only pivot variant that requires the open.
|
||||
|
||||
### 2. Conditional Intermediate Value X
|
||||
|
||||
The core innovation is the conditional calculation of $X$:
|
||||
|
||||
$$X = \begin{cases} H_{prev} + 2 \cdot L_{prev} + C_{prev} & \text{if } C_{prev} < O_{prev} \text{ (bearish)} \\ 2 \cdot H_{prev} + L_{prev} + C_{prev} & \text{if } C_{prev} > O_{prev} \text{ (bullish)} \\ H_{prev} + L_{prev} + 2 \cdot C_{prev} & \text{if } C_{prev} = O_{prev} \text{ (doji)} \end{cases}$$
|
||||
|
||||
Each case sums four price components but doubles one of them:
|
||||
|
||||
- **Bearish bar** doubles the low (the level that absorbed selling pressure)
|
||||
- **Bullish bar** doubles the high (the level that absorbed buying pressure)
|
||||
- **Doji bar** doubles the close (the neutral equilibrium)
|
||||
|
||||
### 3. Pivot Levels
|
||||
|
||||
From the intermediate value $X$:
|
||||
|
||||
$$PP = \frac{X}{4}$$
|
||||
|
||||
$$R_1 = \frac{X}{2} - L_{prev}$$
|
||||
|
||||
$$S_1 = \frac{X}{2} - H_{prev}$$
|
||||
|
||||
### 4. Level Ordering Invariant
|
||||
|
||||
For any bar where $H_{prev} > L_{prev}$ (non-degenerate range):
|
||||
|
||||
$$S_1 < PP < R_1$$
|
||||
|
||||
This holds regardless of the bar direction, since $R_1 - PP = PP - S_1 = \frac{H_{prev} - L_{prev}}{4}$ is always positive for non-zero range. The range between R1 and S1 equals $\frac{H_{prev} - L_{prev}}{2}$, exactly half the prior bar's range.
|
||||
|
||||
### 5. Three Outputs
|
||||
|
||||
All three levels are computed simultaneously and remain constant until a new bar arrives. The primary output (`Last.Val`) returns PP; individual properties expose PP, R1, and S1.
|
||||
|
||||
### Signal Interpretation
|
||||
|
||||
| Condition | Interpretation |
|
||||
| :--- | :--- |
|
||||
| Price above PP | Bullish bias; prior bar direction influences level placement |
|
||||
| Price below PP | Bearish bias for current bar |
|
||||
| Price tests R1 | Resistance; level is higher after bullish prior bar (H weighted) |
|
||||
| Price tests S1 | Support; level is lower after bearish prior bar (L weighted) |
|
||||
| Bearish prior bar | Levels shift downward (low weighted), implying defensive positioning |
|
||||
| Bullish prior bar | Levels shift upward (high weighted), implying aggressive positioning |
|
||||
| Doji prior bar | Levels center on close (neutral), tightest level spacing |
|
||||
|
||||
## Mathematical Foundation
|
||||
|
||||
### Parameters
|
||||
|
||||
DeMark Pivot Points has no configurable parameters. The conditional formula is fixed by definition.
|
||||
|
||||
| Parameter | Value | Notes |
|
||||
| :--- | :---: | :--- |
|
||||
| Inputs | O, H, L, C | Previous bar's open, high, low, close |
|
||||
| Outputs | 3 | PP, R1, S1 |
|
||||
| Parameters | 0 | No tuning required |
|
||||
|
||||
### Warmup Period
|
||||
|
||||
$$W = 2$$
|
||||
|
||||
The indicator requires 2 bars: the first bar provides OHLC for storage; the second bar triggers computation from the stored values. Prior to warmup completion, all outputs are NaN.
|
||||
|
||||
### Derivation Notes
|
||||
|
||||
The R1 and S1 formulas can be rewritten to show their relationship to PP:
|
||||
|
||||
$$R_1 = PP + \frac{H_{prev} - L_{prev}}{4}$$
|
||||
|
||||
$$S_1 = PP - \frac{H_{prev} - L_{prev}}{4}$$
|
||||
|
||||
This means R1 and S1 are always equidistant from PP, separated by one-quarter of the prior bar's range on each side. The conditional logic affects where PP itself sits (closer to the low for bearish bars, closer to the high for bullish), but the R1-PP and PP-S1 distances are always identical.
|
||||
|
||||
## Performance Profile
|
||||
|
||||
### Implementation Design
|
||||
|
||||
Pure arithmetic with no loops, no buffers, no auxiliary data structures. Each `Update` call performs one conditional branch, 3 multiplications, 3 additions/subtractions, and 4 comparisons for NaN validation.
|
||||
|
||||
| Metric | Score | Notes |
|
||||
| :--- | :--- | :--- |
|
||||
| **Complexity** | O(1) | Fixed arithmetic; one branch |
|
||||
| **Allocations** | 0 | Hot path is allocation-free |
|
||||
| **Warmup** | 2 bars | Minimum possible |
|
||||
| **Accuracy** | 10/10 | Exact arithmetic; no approximation |
|
||||
| **Timeliness** | 10/10 | No lag; levels available immediately on new bar |
|
||||
| **Smoothness** | N/A | Discrete levels; smooth/noisy not applicable |
|
||||
|
||||
### State Management
|
||||
|
||||
Internal state uses a `record struct` with local copy pattern for JIT struct promotion. The state tracks previous bar's OHLC and last-valid values for NaN/Infinity input substitution. Bar correction via `isNew` flag enables same-timestamp rewrites without state corruption.
|
||||
|
||||
### SIMD Applicability
|
||||
|
||||
Not applicable for streaming (single bar computation). The `BatchAll` span API could theoretically vectorize but the conditional branch per bar prevents efficient SIMD. The arithmetic is too simple (3 operations after the branch) to justify vectorization overhead.
|
||||
|
||||
### FMA Usage
|
||||
|
||||
Not used. The per-level computation (`x * 0.5 - L` or `x * 0.25`) involves only one multiply and one subtract, which does not form an `a*b + c` pattern that benefits from FMA.
|
||||
|
||||
## Validation
|
||||
|
||||
Self-consistency validation confirms all API modes produce identical results:
|
||||
|
||||
| Mode | Status | Notes |
|
||||
| :--- | :--- | :--- |
|
||||
| **Streaming** (`Update`) | Passed | Bar-by-bar with `isNew` support |
|
||||
| **Batch** (`Batch(TBarSeries)`) | Passed | PP values match streaming |
|
||||
| **Span** (`Batch(Span)`) | Passed | PP values match streaming |
|
||||
| **BatchAll** (`BatchAll(Span)`) | Passed | All 3 levels match streaming |
|
||||
| **Event** (`Pub` subscription) | Passed | Fires on every update |
|
||||
|
||||
| Library | Status | Notes |
|
||||
| :--- | :--- | :--- |
|
||||
| **QuanTAlib** | Passed | All modes self-consistent; level ordering invariant holds |
|
||||
| **Skender** | N/A | Uses calendar-window periods; conceptually different |
|
||||
| **TA-Lib** | N/A | Not implemented |
|
||||
| **Tulip** | N/A | Not implemented |
|
||||
| **Ooples** | N/A | Not validated |
|
||||
|
||||
No external libraries implement bar-to-bar DeMark pivot points. Mathematical correctness is validated by computing expected values from the conditional formula for each bar and comparing against the indicator output at precision 10.
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
1. **First bar returns NaN.** The indicator needs the previous bar's OHLC to compute pivots. The first bar stores OHLC but produces no output. This is correct behavior. `WarmupPeriod = 2`.
|
||||
|
||||
2. **Open price is required.** Unlike Classic Pivot Points (which use only HLC), DeMark requires the open to determine which branch of the conditional to take. Using `TValue` input (which sets all four OHLC fields to the same price) always triggers the doji branch ($C = O$). Use `TBar` input for meaningful DeMark calculations.
|
||||
|
||||
3. **Levels are constant within a bar.** Pivot levels do not change as the current bar's price moves. They change only when a new bar starts. Multiple `isNew=false` corrections on the current bar do not alter the pivot levels.
|
||||
|
||||
4. **Only 3 levels, not 7.** DeMark produces PP, R1, and S1 only. There are no R2/R3/S2/S3 levels. If you need more levels, use Classic Pivot Points or Camarilla.
|
||||
|
||||
5. **Floating-point equality for doji detection.** The doji branch triggers when `Close == Open` exactly. In practice with real market data, exact equality is rare. The bearish and bullish branches handle the vast majority of bars. The doji branch matters most for synthetic data or instruments with minimum tick sizes that create frequent doji bars.
|
||||
|
||||
6. **NaN/Infinity inputs use last-valid substitution.** If any of O, H, L, C is NaN or Infinity, the last valid value for that field is substituted. This prevents NaN propagation but may produce stale levels.
|
||||
|
||||
7. **Level spacing is always half the prior range.** R1 minus S1 always equals $(H_{prev} - L_{prev}) / 2$, regardless of bar direction. The conditional logic shifts PP up or down but does not change the R1-S1 width. Low-range prior bars produce tightly clustered levels.
|
||||
|
||||
## References
|
||||
|
||||
- DeMark, T. R. (1994). *The New Science of Technical Analysis*. John Wiley and Sons.
|
||||
- DeMark, T. R. (1997). *New Market Timing Techniques: Innovative Studies in Market Rhythm and Price Exhaustion*. John Wiley and Sons.
|
||||
- Person, J. L. (2004). *A Complete Guide to Technical Trading Tactics*. John Wiley and Sons.
|
||||
- Wikipedia: [Pivot point (technical analysis)](https://en.wikipedia.org/wiki/Pivot_point_(technical_analysis))
|
||||
Reference in New Issue
Block a user