mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-24 05:28:05 +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,126 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public sealed class PivotcamIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void PivotcamIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new PivotcamIndicator();
|
||||
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Contains("PIVOTCAM", indicator.Name, StringComparison.Ordinal);
|
||||
Assert.False(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PivotcamIndicator_MinHistoryDepths_EqualsZero()
|
||||
{
|
||||
var indicator = new PivotcamIndicator();
|
||||
|
||||
Assert.Equal(0, PivotcamIndicator.MinHistoryDepths);
|
||||
IWatchlistIndicator watchlistIndicator = indicator;
|
||||
Assert.Equal(0, watchlistIndicator.MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PivotcamIndicator_ShortName_IsPivotcam()
|
||||
{
|
||||
var indicator = new PivotcamIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
Assert.Contains("PIVOTCAM", indicator.ShortName, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PivotcamIndicator_SourceCodeLink_IsValid()
|
||||
{
|
||||
var indicator = new PivotcamIndicator();
|
||||
|
||||
Assert.Contains("github.com", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
Assert.Contains("Pivotcam", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PivotcamIndicator_Initialize_CreatesInternalIndicator()
|
||||
{
|
||||
var indicator = new PivotcamIndicator();
|
||||
|
||||
indicator.Initialize();
|
||||
|
||||
// 9 line series: PP, R1, R2, R3, R4, S1, S2, S3, S4
|
||||
Assert.Equal(9, indicator.LinesSeries.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PivotcamIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new PivotcamIndicator();
|
||||
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 PivotcamIndicator_ProcessUpdate_NewBar_ComputesValue()
|
||||
{
|
||||
var indicator = new PivotcamIndicator();
|
||||
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 PivotcamIndicator_NineLineSeries_ArePresent()
|
||||
{
|
||||
var indicator = new PivotcamIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
// PP=0, R1=1, R2=2, R3=3, R4=4, S1=5, S2=6, S3=7, S4=8
|
||||
Assert.Equal(9, indicator.LinesSeries.Count);
|
||||
Assert.Contains("PP", indicator.LinesSeries[0].Name, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Contains("R1", indicator.LinesSeries[1].Name, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Contains("R4", indicator.LinesSeries[4].Name, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Contains("S1", indicator.LinesSeries[5].Name, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Contains("S4", indicator.LinesSeries[8].Name, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PivotcamIndicator_Description_IsSet()
|
||||
{
|
||||
var indicator = new PivotcamIndicator();
|
||||
|
||||
Assert.NotNull(indicator.Description);
|
||||
Assert.NotEmpty(indicator.Description);
|
||||
Assert.Contains("Camarilla", indicator.Description, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
using System.Drawing;
|
||||
using System.Runtime.CompilerServices;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
[SkipLocalsInit]
|
||||
public sealed class PivotcamIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[InputParameter("Show cold values", sortIndex: 21)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private Pivotcam _indicator = null!;
|
||||
private readonly LineSeries _ppSeries;
|
||||
private readonly LineSeries _r1Series;
|
||||
private readonly LineSeries _r2Series;
|
||||
private readonly LineSeries _r3Series;
|
||||
private readonly LineSeries _r4Series;
|
||||
private readonly LineSeries _s1Series;
|
||||
private readonly LineSeries _s2Series;
|
||||
private readonly LineSeries _s3Series;
|
||||
private readonly LineSeries _s4Series;
|
||||
|
||||
public static int MinHistoryDepths => 0;
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => "PIVOTCAM";
|
||||
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/reversals/pivotcam/Pivotcam.cs";
|
||||
|
||||
public PivotcamIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = false;
|
||||
Name = "PIVOTCAM - Camarilla Pivot Points";
|
||||
Description = "Camarilla pivot points: 9 support/resistance levels (PP, R1-R4, S1-S4) derived from previous bar's HLC.";
|
||||
|
||||
_ppSeries = new LineSeries(name: "PP", color: Color.Yellow, width: 2, style: LineStyle.Solid);
|
||||
_r1Series = new LineSeries(name: "R1", color: Color.FromArgb(255, 160, 160), width: 1, style: LineStyle.Solid);
|
||||
_r2Series = new LineSeries(name: "R2", color: Color.FromArgb(255, 128, 128), width: 1, style: LineStyle.Solid);
|
||||
_r3Series = new LineSeries(name: "R3", color: Color.FromArgb(255, 80, 80), width: 1, style: LineStyle.Solid);
|
||||
_r4Series = new LineSeries(name: "R4", color: Color.Red, width: 1, style: LineStyle.Dash);
|
||||
_s1Series = new LineSeries(name: "S1", color: Color.FromArgb(160, 255, 160), width: 1, style: LineStyle.Solid);
|
||||
_s2Series = new LineSeries(name: "S2", color: Color.FromArgb(128, 255, 128), width: 1, style: LineStyle.Solid);
|
||||
_s3Series = new LineSeries(name: "S3", color: Color.FromArgb(80, 255, 80), width: 1, style: LineStyle.Solid);
|
||||
_s4Series = new LineSeries(name: "S4", color: Color.Green, width: 1, style: LineStyle.Dash);
|
||||
|
||||
AddLineSeries(_ppSeries);
|
||||
AddLineSeries(_r1Series);
|
||||
AddLineSeries(_r2Series);
|
||||
AddLineSeries(_r3Series);
|
||||
AddLineSeries(_r4Series);
|
||||
AddLineSeries(_s1Series);
|
||||
AddLineSeries(_s2Series);
|
||||
AddLineSeries(_s3Series);
|
||||
AddLineSeries(_s4Series);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnInit()
|
||||
{
|
||||
_indicator = new Pivotcam();
|
||||
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);
|
||||
_r2Series.SetValue(_indicator.R2, _indicator.IsHot, ShowColdValues);
|
||||
_r3Series.SetValue(_indicator.R3, _indicator.IsHot, ShowColdValues);
|
||||
_r4Series.SetValue(_indicator.R4, _indicator.IsHot, ShowColdValues);
|
||||
_s1Series.SetValue(_indicator.S1, _indicator.IsHot, ShowColdValues);
|
||||
_s2Series.SetValue(_indicator.S2, _indicator.IsHot, ShowColdValues);
|
||||
_s3Series.SetValue(_indicator.S3, _indicator.IsHot, ShowColdValues);
|
||||
_s4Series.SetValue(_indicator.S4, _indicator.IsHot, ShowColdValues);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,582 @@
|
||||
// PIVOTCAM Tests - Camarilla Pivot Points
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
// -- A) Constructor Validation ------------------------------------------------
|
||||
public sealed class PivotcamConstructorTests
|
||||
{
|
||||
[Fact]
|
||||
public void Constructor_Default_SetsProperties()
|
||||
{
|
||||
var p = new Pivotcam();
|
||||
|
||||
Assert.Equal(2, p.WarmupPeriod);
|
||||
Assert.Contains("Pivotcam", p.Name, StringComparison.Ordinal);
|
||||
Assert.False(p.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_InitialState_AllNaN()
|
||||
{
|
||||
var p = new Pivotcam();
|
||||
|
||||
Assert.True(double.IsNaN(p.PP));
|
||||
Assert.True(double.IsNaN(p.R1));
|
||||
Assert.True(double.IsNaN(p.R2));
|
||||
Assert.True(double.IsNaN(p.R3));
|
||||
Assert.True(double.IsNaN(p.R4));
|
||||
Assert.True(double.IsNaN(p.S1));
|
||||
Assert.True(double.IsNaN(p.S2));
|
||||
Assert.True(double.IsNaN(p.S3));
|
||||
Assert.True(double.IsNaN(p.S4));
|
||||
}
|
||||
}
|
||||
|
||||
// -- B) Basic Calculation -----------------------------------------------------
|
||||
public sealed class PivotcamBasicTests
|
||||
{
|
||||
[Fact]
|
||||
public void Update_ReturnsTValue()
|
||||
{
|
||||
var p = new Pivotcam();
|
||||
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 Pivotcam();
|
||||
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_CorrectPivotLevels()
|
||||
{
|
||||
// Given previous bar H=110, L=90, C=100, range=20
|
||||
// PP = (110+90+100)/3 = 100
|
||||
// R1 = 100 + 20*1.0833/12 ≈ 101.8055
|
||||
// S1 = 100 - 20*1.0833/12 ≈ 98.1945
|
||||
// R2 = 100 + 20*1.1666/12 ≈ 101.9443
|
||||
// S2 = 100 - 20*1.1666/12 ≈ 98.0557
|
||||
// R3 = 100 + 20*1.25/12 ≈ 102.0833
|
||||
// S3 = 100 - 20*1.25/12 ≈ 97.9167
|
||||
// R4 = 100 + 20*1.5/12 = 102.5
|
||||
// S4 = 100 - 20*1.5/12 = 97.5
|
||||
var p = new Pivotcam();
|
||||
var dt = DateTime.UtcNow;
|
||||
|
||||
// First bar: stores HLC, no output yet
|
||||
_ = p.Update(new TBar(dt, 100, 110, 90, 100, 1000), isNew: true);
|
||||
Assert.True(double.IsNaN(p.PP));
|
||||
|
||||
// Second bar: computes from first bar's HLC
|
||||
_ = p.Update(new TBar(dt.AddMinutes(1), 105, 115, 95, 105, 1000), isNew: true);
|
||||
|
||||
double range = 20.0;
|
||||
double pC = 100.0;
|
||||
Assert.Equal(100.0, p.PP, precision: 10);
|
||||
Assert.Equal(pC + range * 1.0833 / 12.0, p.R1, precision: 4);
|
||||
Assert.Equal(pC - range * 1.0833 / 12.0, p.S1, precision: 4);
|
||||
Assert.Equal(pC + range * 1.1666 / 12.0, p.R2, precision: 4);
|
||||
Assert.Equal(pC - range * 1.1666 / 12.0, p.S2, precision: 4);
|
||||
Assert.Equal(pC + range * 1.25 / 12.0, p.R3, precision: 4);
|
||||
Assert.Equal(pC - range * 1.25 / 12.0, p.S3, precision: 4);
|
||||
Assert.Equal(pC + range * 1.5 / 12.0, p.R4, precision: 4);
|
||||
Assert.Equal(pC - range * 1.5 / 12.0, p.S4, precision: 4);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_SecondKnownValues_CorrectPivotLevels()
|
||||
{
|
||||
// Given previous bar H=120, L=100, C=115, range=20
|
||||
var p = new Pivotcam();
|
||||
var dt = DateTime.UtcNow;
|
||||
|
||||
_ = p.Update(new TBar(dt, 110, 120, 100, 115, 1000), isNew: true);
|
||||
_ = p.Update(new TBar(dt.AddMinutes(1), 115, 125, 105, 120, 1000), isNew: true);
|
||||
|
||||
double expectedPP = (120.0 + 100.0 + 115.0) / 3.0;
|
||||
double range = 20.0;
|
||||
double pC = 115.0;
|
||||
|
||||
Assert.Equal(expectedPP, p.PP, precision: 10);
|
||||
Assert.Equal(pC + range * 1.0833 / 12.0, p.R1, precision: 4);
|
||||
Assert.Equal(pC - range * 1.0833 / 12.0, p.S1, precision: 4);
|
||||
Assert.Equal(pC + range * 1.1666 / 12.0, p.R2, precision: 4);
|
||||
Assert.Equal(pC - range * 1.1666 / 12.0, p.S2, precision: 4);
|
||||
Assert.Equal(pC + range * 1.25 / 12.0, p.R3, precision: 4);
|
||||
Assert.Equal(pC - range * 1.25 / 12.0, p.S3, precision: 4);
|
||||
Assert.Equal(pC + range * 1.5 / 12.0, p.R4, precision: 4);
|
||||
Assert.Equal(pC - range * 1.5 / 12.0, p.S4, precision: 4);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_LevelsHaveCorrectOrdering()
|
||||
{
|
||||
// For any normal bar: S4 < S3 < S2 < S1 < PP < R1 < R2 < R3 < R4
|
||||
// (when close is near the range center, PP may be above or below close,
|
||||
// but S/R levels are always ordered by their multiplier magnitude)
|
||||
var p = new Pivotcam();
|
||||
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.S4 < p.S3);
|
||||
Assert.True(p.S3 < p.S2);
|
||||
Assert.True(p.S2 < p.S1);
|
||||
Assert.True(p.R1 < p.R2);
|
||||
Assert.True(p.R2 < p.R3);
|
||||
Assert.True(p.R3 < p.R4);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Name_ContainsPivotcam()
|
||||
{
|
||||
var p = new Pivotcam();
|
||||
Assert.Contains("Pivotcam", p.Name, StringComparison.Ordinal);
|
||||
}
|
||||
}
|
||||
|
||||
// -- C) State + Bar Correction ------------------------------------------------
|
||||
public sealed class PivotcamStateCorrectionTests
|
||||
{
|
||||
[Fact]
|
||||
public void IsNew_True_AdvancesState()
|
||||
{
|
||||
var p = new Pivotcam();
|
||||
|
||||
_ = 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 Pivotcam();
|
||||
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);
|
||||
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 HLC (H=110, L=90, C=100)
|
||||
Assert.Equal(ppBefore, p.PP, precision: 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IterativeCorrections_ProduceSameResult()
|
||||
{
|
||||
var p = new Pivotcam();
|
||||
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);
|
||||
|
||||
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 Pivotcam();
|
||||
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);
|
||||
|
||||
_ = p.Update(new TBar(dt.AddMinutes(1), 108, 120, 88, 110, 1000), isNew: false);
|
||||
double r1a = p.R1, s1a = p.S1, r2a = p.R2, s2a = p.S2;
|
||||
double r3a = p.R3, s3a = p.S3, r4a = p.R4, s4a = p.S4;
|
||||
|
||||
_ = p.Update(new TBar(dt.AddMinutes(1), 108, 120, 88, 110, 1000), isNew: false);
|
||||
Assert.Equal(r1a, p.R1);
|
||||
Assert.Equal(s1a, p.S1);
|
||||
Assert.Equal(r2a, p.R2);
|
||||
Assert.Equal(s2a, p.S2);
|
||||
Assert.Equal(r3a, p.R3);
|
||||
Assert.Equal(s3a, p.S3);
|
||||
Assert.Equal(r4a, p.R4);
|
||||
Assert.Equal(s4a, p.S4);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reset_ClearsAllState()
|
||||
{
|
||||
var p = new Pivotcam();
|
||||
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.R2));
|
||||
Assert.True(double.IsNaN(p.R3));
|
||||
Assert.True(double.IsNaN(p.R4));
|
||||
Assert.True(double.IsNaN(p.S1));
|
||||
Assert.True(double.IsNaN(p.S2));
|
||||
Assert.True(double.IsNaN(p.S3));
|
||||
Assert.True(double.IsNaN(p.S4));
|
||||
}
|
||||
}
|
||||
|
||||
// -- D) Warmup / Convergence --------------------------------------------------
|
||||
public sealed class PivotcamWarmupTests
|
||||
{
|
||||
[Fact]
|
||||
public void IsHot_FlipsAfterWarmup()
|
||||
{
|
||||
var p = new Pivotcam();
|
||||
|
||||
// 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 Pivotcam();
|
||||
Assert.Equal(2, p.WarmupPeriod);
|
||||
}
|
||||
}
|
||||
|
||||
// -- E) Robustness ------------------------------------------------------------
|
||||
public sealed class PivotcamRobustnessTests
|
||||
{
|
||||
[Fact]
|
||||
public void NaN_Input_UsesLastValidValue()
|
||||
{
|
||||
var p = new Pivotcam();
|
||||
var dt = DateTime.UtcNow;
|
||||
|
||||
// Feed valid bars
|
||||
_ = 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.IsHot);
|
||||
|
||||
// Feed NaN bar
|
||||
_ = p.Update(new TBar(dt.AddMinutes(2), double.NaN, double.NaN, double.NaN, double.NaN, 0), isNew: true);
|
||||
|
||||
Assert.True(p.IsHot);
|
||||
Assert.True(double.IsFinite(p.PP));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Infinity_Input_UsesLastValidValue()
|
||||
{
|
||||
var p = new Pivotcam();
|
||||
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);
|
||||
|
||||
_ = 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 Pivotcam();
|
||||
|
||||
_ = 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 PivotcamConsistencyTests
|
||||
{
|
||||
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 Pivotcam();
|
||||
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 = Pivotcam.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 Pivotcam();
|
||||
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];
|
||||
Pivotcam.Batch(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 Pivotcam();
|
||||
var sPP = new double[bars.Count];
|
||||
var sR1 = new double[bars.Count];
|
||||
var sS1 = new double[bars.Count];
|
||||
var sR2 = new double[bars.Count];
|
||||
var sS2 = new double[bars.Count];
|
||||
var sR3 = new double[bars.Count];
|
||||
var sS3 = new double[bars.Count];
|
||||
var sR4 = new double[bars.Count];
|
||||
var sS4 = 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;
|
||||
sR2[i] = streaming.R2;
|
||||
sS2[i] = streaming.S2;
|
||||
sR3[i] = streaming.R3;
|
||||
sS3[i] = streaming.S3;
|
||||
sR4[i] = streaming.R4;
|
||||
sS4[i] = streaming.S4;
|
||||
}
|
||||
|
||||
// BatchAll
|
||||
var bPP = new double[bars.Count];
|
||||
var bR1 = new double[bars.Count];
|
||||
var bS1 = new double[bars.Count];
|
||||
var bR2 = new double[bars.Count];
|
||||
var bS2 = new double[bars.Count];
|
||||
var bR3 = new double[bars.Count];
|
||||
var bS3 = new double[bars.Count];
|
||||
var bR4 = new double[bars.Count];
|
||||
var bS4 = new double[bars.Count];
|
||||
|
||||
Pivotcam.BatchAll(bars.HighValues, bars.LowValues, bars.CloseValues,
|
||||
bPP, bR1, bS1, bR2, bS2, bR3, bS3, bR4, bS4);
|
||||
|
||||
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);
|
||||
Assert.Equal(sR2[i], bR2[i], precision: 10);
|
||||
Assert.Equal(sS2[i], bS2[i], precision: 10);
|
||||
Assert.Equal(sR3[i], bR3[i], precision: 10);
|
||||
Assert.Equal(sS3[i], bS3[i], precision: 10);
|
||||
Assert.Equal(sR4[i], bR4[i], precision: 10);
|
||||
Assert.Equal(sS4[i], bS4[i], precision: 10);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TValue_Update_MatchesTBar_Update()
|
||||
{
|
||||
var p1 = new Pivotcam();
|
||||
var p2 = new Pivotcam();
|
||||
|
||||
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 PivotcamSpanTests
|
||||
{
|
||||
[Fact]
|
||||
public void Batch_Span_MismatchedLengths_Throws()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() =>
|
||||
Pivotcam.Batch(new double[10], new double[5], new double[10], new double[10]));
|
||||
Assert.Equal("high", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_Span_OutputTooShort_Throws()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() =>
|
||||
Pivotcam.Batch(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(() =>
|
||||
Pivotcam.Batch(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>(() =>
|
||||
Pivotcam.BatchAll(new double[10], new double[10], new double[10],
|
||||
new double[10], new double[5], new double[10],
|
||||
new double[10], new double[10], new double[10],
|
||||
new double[10], new double[10], new double[10]));
|
||||
Assert.Equal("r1Out", ex.ParamName);
|
||||
}
|
||||
}
|
||||
|
||||
// -- H) Event / Chainability -------------------------------------------------
|
||||
public sealed class PivotcamEventTests
|
||||
{
|
||||
[Fact]
|
||||
public void Pub_FiresOnUpdate()
|
||||
{
|
||||
var p = new Pivotcam();
|
||||
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 Pivotcam();
|
||||
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 PivotcamPrimeTests
|
||||
{
|
||||
[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 Pivotcam();
|
||||
p.Prime(bars);
|
||||
|
||||
Assert.True(p.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Prime_EmptySource_NoException()
|
||||
{
|
||||
var p = new Pivotcam();
|
||||
var bars = new TBarSeries();
|
||||
|
||||
var ex = Record.Exception(() => p.Prime(bars));
|
||||
Assert.Null(ex);
|
||||
Assert.False(p.IsHot);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
// PIVOTCAM Validation Tests - Camarilla Pivot Points
|
||||
// Self-consistency validation across all API modes.
|
||||
//
|
||||
// Note: No external library (Skender, TA-Lib, Tulip, Ooples) implements
|
||||
// Camarilla Pivot Points. Validation focuses on mathematical correctness
|
||||
// and mode consistency.
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public sealed class PivotcamValidationTests
|
||||
{
|
||||
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_EqualsHLC_Over3()
|
||||
{
|
||||
var bars = CreateGbmBars(count: 100);
|
||||
var p = new Pivotcam();
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
_ = p.Update(bars[i], isNew: true);
|
||||
|
||||
if (i >= 1)
|
||||
{
|
||||
double prevH = bars[i - 1].High;
|
||||
double prevL = bars[i - 1].Low;
|
||||
double prevC = bars[i - 1].Close;
|
||||
double expectedPP = (prevH + prevL + prevC) / 3.0;
|
||||
|
||||
Assert.Equal(expectedPP, p.PP, precision: 10);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MathCorrectness_AllLevels_MatchCamarillaFormula()
|
||||
{
|
||||
var bars = CreateGbmBars(count: 100);
|
||||
var p = new Pivotcam();
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
_ = p.Update(bars[i], isNew: true);
|
||||
|
||||
if (i >= 1)
|
||||
{
|
||||
double pH = bars[i - 1].High;
|
||||
double pL = bars[i - 1].Low;
|
||||
double pC = bars[i - 1].Close;
|
||||
double pp = (pH + pL + pC) / 3.0;
|
||||
double range = pH - pL;
|
||||
|
||||
Assert.Equal(pp, p.PP, precision: 10);
|
||||
Assert.Equal(pC + range * 1.0833 / 12.0, p.R1, precision: 4);
|
||||
Assert.Equal(pC - range * 1.0833 / 12.0, p.S1, precision: 4);
|
||||
Assert.Equal(pC + range * 1.1666 / 12.0, p.R2, precision: 4);
|
||||
Assert.Equal(pC - range * 1.1666 / 12.0, p.S2, precision: 4);
|
||||
Assert.Equal(pC + range * 1.25 / 12.0, p.R3, precision: 4);
|
||||
Assert.Equal(pC - range * 1.25 / 12.0, p.S3, precision: 4);
|
||||
Assert.Equal(pC + range * 1.5 / 12.0, p.R4, precision: 4);
|
||||
Assert.Equal(pC - range * 1.5 / 12.0, p.S4, precision: 4);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -- Self-Consistency: Streaming == Batch --------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void StreamingMatchesBatch_PP()
|
||||
{
|
||||
var bars = CreateGbmBars();
|
||||
|
||||
// Streaming
|
||||
var streaming = new Pivotcam();
|
||||
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 = Pivotcam.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 Pivotcam();
|
||||
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];
|
||||
Pivotcam.Batch(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 9 levels) --------------------
|
||||
|
||||
[Fact]
|
||||
public void StreamingMatchesBatchAll_AllLevels()
|
||||
{
|
||||
var bars = CreateGbmBars(count: 300);
|
||||
|
||||
// Streaming
|
||||
var streaming = new Pivotcam();
|
||||
var sPP = new double[bars.Count];
|
||||
var sR1 = new double[bars.Count];
|
||||
var sS1 = new double[bars.Count];
|
||||
var sR2 = new double[bars.Count];
|
||||
var sS2 = new double[bars.Count];
|
||||
var sR3 = new double[bars.Count];
|
||||
var sS3 = new double[bars.Count];
|
||||
var sR4 = new double[bars.Count];
|
||||
var sS4 = 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;
|
||||
sR2[i] = streaming.R2;
|
||||
sS2[i] = streaming.S2;
|
||||
sR3[i] = streaming.R3;
|
||||
sS3[i] = streaming.S3;
|
||||
sR4[i] = streaming.R4;
|
||||
sS4[i] = streaming.S4;
|
||||
}
|
||||
|
||||
// BatchAll
|
||||
var bPP = new double[bars.Count];
|
||||
var bR1 = new double[bars.Count];
|
||||
var bS1 = new double[bars.Count];
|
||||
var bR2 = new double[bars.Count];
|
||||
var bS2 = new double[bars.Count];
|
||||
var bR3 = new double[bars.Count];
|
||||
var bS3 = new double[bars.Count];
|
||||
var bR4 = new double[bars.Count];
|
||||
var bS4 = new double[bars.Count];
|
||||
|
||||
Pivotcam.BatchAll(bars.HighValues, bars.LowValues, bars.CloseValues,
|
||||
bPP, bR1, bS1, bR2, bS2, bR3, bS3, bR4, bS4);
|
||||
|
||||
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);
|
||||
Assert.Equal(sR2[i], bR2[i], precision: 10);
|
||||
Assert.Equal(sS2[i], bS2[i], precision: 10);
|
||||
Assert.Equal(sR3[i], bR3[i], precision: 10);
|
||||
Assert.Equal(sS3[i], bS3[i], precision: 10);
|
||||
Assert.Equal(sR4[i], bR4[i], precision: 10);
|
||||
Assert.Equal(sS4[i], bS4[i], precision: 10);
|
||||
}
|
||||
}
|
||||
|
||||
// -- Determinism ---------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void SameInput_ProducesSameOutput()
|
||||
{
|
||||
var bars = CreateGbmBars(count: 200, seed: 123);
|
||||
|
||||
var p1 = new Pivotcam();
|
||||
var p2 = new Pivotcam();
|
||||
|
||||
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);
|
||||
Assert.Equal(p1.R2, p2.R2);
|
||||
Assert.Equal(p1.S2, p2.S2);
|
||||
Assert.Equal(p1.R3, p2.R3);
|
||||
Assert.Equal(p1.S3, p2.S3);
|
||||
Assert.Equal(p1.R4, p2.R4);
|
||||
Assert.Equal(p1.S4, p2.S4);
|
||||
}
|
||||
|
||||
// -- Calculate Returns Valid Indicator -----------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void Calculate_ReturnsValidIndicatorAndResults()
|
||||
{
|
||||
var bars = CreateGbmBars(count: 100);
|
||||
|
||||
var (results, indicator) = Pivotcam.Calculate(bars);
|
||||
|
||||
Assert.NotNull(results);
|
||||
Assert.Equal(bars.Count, results.Count);
|
||||
Assert.True(indicator.IsHot);
|
||||
}
|
||||
|
||||
// -- Level Ordering Invariant --------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void AllBars_SupportResistanceLevelsOrdered()
|
||||
{
|
||||
// Camarilla: S4 < S3 < S2 < S1 < Close-based < R1 < R2 < R3 < R4
|
||||
// Note: PP is based on HLC/3 and may be above or below close,
|
||||
// but resistance levels are always ordered R1 < R2 < R3 < R4
|
||||
// and support levels are always ordered S4 < S3 < S2 < S1
|
||||
var bars = CreateGbmBars(count: 200);
|
||||
var p = new Pivotcam();
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
_ = p.Update(bars[i], isNew: true);
|
||||
|
||||
if (p.IsHot)
|
||||
{
|
||||
Assert.True(p.S4 <= p.S3, $"S4 > S3 at bar {i}");
|
||||
Assert.True(p.S3 <= p.S2, $"S3 > S2 at bar {i}");
|
||||
Assert.True(p.S2 <= p.S1, $"S2 > S1 at bar {i}");
|
||||
Assert.True(p.R1 <= p.R2, $"R1 > R2 at bar {i}");
|
||||
Assert.True(p.R2 <= p.R3, $"R2 > R3 at bar {i}");
|
||||
Assert.True(p.R3 <= p.R4, $"R3 > R4 at bar {i}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,443 @@
|
||||
// PIVOTCAM: Camarilla Pivot Points
|
||||
// Calculates 9 support/resistance levels from previous bar's HLC.
|
||||
// Close-centric formula with range-fraction multipliers.
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// PIVOTCAM: Camarilla Pivot Points
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Computes 9 horizontal support/resistance levels from the previous bar's
|
||||
/// high, low, and close. The central pivot point (PP) is the arithmetic mean
|
||||
/// of HLC; resistance and support levels are derived from the close plus/minus
|
||||
/// fractions of the previous range using the Camarilla equation.
|
||||
///
|
||||
/// Calculation (using previous bar's H, L, C):
|
||||
/// <code>
|
||||
/// PP = (H + L + C) / 3
|
||||
/// R1 = C + range × 1.0833 / 12 S1 = C − range × 1.0833 / 12
|
||||
/// R2 = C + range × 1.1666 / 12 S2 = C − range × 1.1666 / 12
|
||||
/// R3 = C + range × 1.2500 / 12 S3 = C − range × 1.2500 / 12
|
||||
/// R4 = C + range × 1.5000 / 12 S4 = C − range × 1.5000 / 12
|
||||
/// where range = H − L
|
||||
/// </code>
|
||||
///
|
||||
/// <b>Key characteristics:</b>
|
||||
/// - O(1) computation: pure arithmetic from previous bar's HLC
|
||||
/// - 9 outputs: PP, R1, R2, R3, R4, S1, S2, S3, S4
|
||||
/// - WarmupPeriod = 2 (need previous bar's HLC)
|
||||
/// - No configurable parameters
|
||||
/// - Close-centric: levels radiate symmetrically from close, not PP
|
||||
/// - R3/S3 are the primary mean-reversion levels
|
||||
/// </remarks>
|
||||
/// <seealso href="Pivotcam.md">Detailed documentation</seealso>
|
||||
[SkipLocalsInit]
|
||||
public sealed class Pivotcam : ITValuePublisher
|
||||
{
|
||||
// Camarilla multiplier constants: numerator / 12.0
|
||||
private const double C1 = 1.0833 / 12.0; // ≈ 0.090275
|
||||
private const double C2 = 1.1666 / 12.0; // ≈ 0.097217
|
||||
private const double C3 = 1.2500 / 12.0; // ≈ 0.104167
|
||||
private const double C4 = 1.5000 / 12.0; // = 0.125
|
||||
|
||||
[StructLayout(LayoutKind.Auto)]
|
||||
private record struct State(
|
||||
double PrevHigh,
|
||||
double PrevLow,
|
||||
double PrevClose,
|
||||
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: (prevH + prevL + prevC) / 3</summary>
|
||||
public double PP { get; private set; }
|
||||
|
||||
/// <summary>Resistance 1: prevC + range × 1.0833 / 12</summary>
|
||||
public double R1 { get; private set; }
|
||||
|
||||
/// <summary>Resistance 2: prevC + range × 1.1666 / 12</summary>
|
||||
public double R2 { get; private set; }
|
||||
|
||||
/// <summary>Resistance 3: prevC + range × 1.2500 / 12</summary>
|
||||
public double R3 { get; private set; }
|
||||
|
||||
/// <summary>Resistance 4: prevC + range × 1.5000 / 12</summary>
|
||||
public double R4 { get; private set; }
|
||||
|
||||
/// <summary>Support 1: prevC − range × 1.0833 / 12</summary>
|
||||
public double S1 { get; private set; }
|
||||
|
||||
/// <summary>Support 2: prevC − range × 1.1666 / 12</summary>
|
||||
public double S2 { get; private set; }
|
||||
|
||||
/// <summary>Support 3: prevC − range × 1.2500 / 12</summary>
|
||||
public double S3 { get; private set; }
|
||||
|
||||
/// <summary>Support 4: prevC − range × 1.5000 / 12</summary>
|
||||
public double S4 { 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 Camarilla Pivot Points indicator.
|
||||
/// </summary>
|
||||
public Pivotcam()
|
||||
{
|
||||
_count = 0;
|
||||
_s = new State(double.NaN, double.NaN, double.NaN, double.NaN, double.NaN, double.NaN);
|
||||
_ps = _s;
|
||||
|
||||
SetAllNaN();
|
||||
|
||||
Name = "Pivotcam";
|
||||
WarmupPeriod = 2;
|
||||
_barHandler = HandleBar;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a Camarilla Pivot Points indicator chained to a TBarSeries source.
|
||||
/// </summary>
|
||||
public Pivotcam(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 high = input.High;
|
||||
double low = input.Low;
|
||||
double close = input.Close;
|
||||
|
||||
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(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 HLC but cannot compute pivots yet (no previous bar)
|
||||
if (_count < 2)
|
||||
{
|
||||
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 Camarilla pivot levels from PREVIOUS bar's HLC
|
||||
double pH = s.PrevHigh;
|
||||
double pL = s.PrevLow;
|
||||
double pC = s.PrevClose;
|
||||
|
||||
double range = pH - pL;
|
||||
|
||||
PP = (pH + pL + pC) / 3.0;
|
||||
R1 = Math.FusedMultiplyAdd(range, C1, pC); // pC + range * C1
|
||||
S1 = Math.FusedMultiplyAdd(-range, C1, pC); // pC - range * C1
|
||||
R2 = Math.FusedMultiplyAdd(range, C2, pC); // pC + range * C2
|
||||
S2 = Math.FusedMultiplyAdd(-range, C2, pC); // pC - range * C2
|
||||
R3 = Math.FusedMultiplyAdd(range, C3, pC); // pC + range * C3
|
||||
S3 = Math.FusedMultiplyAdd(-range, C3, pC); // pC - range * C3
|
||||
R4 = Math.FusedMultiplyAdd(range, C4, pC); // pC + range * C4
|
||||
S4 = Math.FusedMultiplyAdd(-range, C4, pC); // pC - range * C4
|
||||
|
||||
// Store current bar's HLC as "previous" for next bar
|
||||
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.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);
|
||||
_ps = _s;
|
||||
SetAllNaN();
|
||||
Last = default;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void SetAllNaN()
|
||||
{
|
||||
PP = double.NaN;
|
||||
R1 = double.NaN;
|
||||
R2 = double.NaN;
|
||||
R3 = double.NaN;
|
||||
R4 = double.NaN;
|
||||
S1 = double.NaN;
|
||||
S2 = double.NaN;
|
||||
S3 = double.NaN;
|
||||
S4 = double.NaN;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Batch computation of Camarilla Pivot Points over span data.
|
||||
/// Writes PP values to <paramref name="ppOutput"/>.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void Batch(
|
||||
ReadOnlySpan<double> high,
|
||||
ReadOnlySpan<double> low,
|
||||
ReadOnlySpan<double> close,
|
||||
Span<double> ppOutput)
|
||||
{
|
||||
if (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 HLC
|
||||
for (int i = 1; i < len; i++)
|
||||
{
|
||||
double pH = high[i - 1];
|
||||
double pL = low[i - 1];
|
||||
double pC = close[i - 1];
|
||||
ppOutput[i] = (pH + pL + pC) / 3.0;
|
||||
}
|
||||
}
|
||||
|
||||
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.HighValues, source.LowValues, source.CloseValues,
|
||||
CollectionsMarshal.AsSpan(v));
|
||||
|
||||
source.Times.CopyTo(CollectionsMarshal.AsSpan(t));
|
||||
|
||||
return new TSeries(t, v);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Batch computation of all 9 Camarilla Pivot Point levels over span data.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void BatchAll(
|
||||
ReadOnlySpan<double> high,
|
||||
ReadOnlySpan<double> low,
|
||||
ReadOnlySpan<double> close,
|
||||
Span<double> ppOut,
|
||||
Span<double> r1Out,
|
||||
Span<double> s1Out,
|
||||
Span<double> r2Out,
|
||||
Span<double> s2Out,
|
||||
Span<double> r3Out,
|
||||
Span<double> s3Out,
|
||||
Span<double> r4Out,
|
||||
Span<double> s4Out)
|
||||
{
|
||||
if (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 (r2Out.Length < len) { throw new ArgumentException("Output span too short.", nameof(r2Out)); }
|
||||
if (s2Out.Length < len) { throw new ArgumentException("Output span too short.", nameof(s2Out)); }
|
||||
if (r3Out.Length < len) { throw new ArgumentException("Output span too short.", nameof(r3Out)); }
|
||||
if (s3Out.Length < len) { throw new ArgumentException("Output span too short.", nameof(s3Out)); }
|
||||
if (r4Out.Length < len) { throw new ArgumentException("Output span too short.", nameof(r4Out)); }
|
||||
if (s4Out.Length < len) { throw new ArgumentException("Output span too short.", nameof(s4Out)); }
|
||||
|
||||
if (len == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// First bar: no previous data
|
||||
ppOut[0] = double.NaN;
|
||||
r1Out[0] = double.NaN;
|
||||
s1Out[0] = double.NaN;
|
||||
r2Out[0] = double.NaN;
|
||||
s2Out[0] = double.NaN;
|
||||
r3Out[0] = double.NaN;
|
||||
s3Out[0] = double.NaN;
|
||||
r4Out[0] = double.NaN;
|
||||
s4Out[0] = double.NaN;
|
||||
|
||||
for (int i = 1; i < len; i++)
|
||||
{
|
||||
double pH = high[i - 1];
|
||||
double pL = low[i - 1];
|
||||
double pC = close[i - 1];
|
||||
|
||||
double range = pH - pL;
|
||||
|
||||
ppOut[i] = (pH + pL + pC) / 3.0;
|
||||
r1Out[i] = Math.FusedMultiplyAdd(range, C1, pC);
|
||||
s1Out[i] = Math.FusedMultiplyAdd(-range, C1, pC);
|
||||
r2Out[i] = Math.FusedMultiplyAdd(range, C2, pC);
|
||||
s2Out[i] = Math.FusedMultiplyAdd(-range, C2, pC);
|
||||
r3Out[i] = Math.FusedMultiplyAdd(range, C3, pC);
|
||||
s3Out[i] = Math.FusedMultiplyAdd(-range, C3, pC);
|
||||
r4Out[i] = Math.FusedMultiplyAdd(range, C4, pC);
|
||||
s4Out[i] = Math.FusedMultiplyAdd(-range, C4, pC);
|
||||
}
|
||||
}
|
||||
|
||||
public static (TSeries Results, Pivotcam Indicator) Calculate(TBarSeries source)
|
||||
{
|
||||
var indicator = new Pivotcam();
|
||||
var results = indicator.Update(source);
|
||||
return (results, indicator);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
# PIVOTCAM: Camarilla Pivot Points
|
||||
|
||||
> "The Camarilla trader does not care where the market opens. The trader cares how far price strays from yesterday's close, and whether it returns."
|
||||
|
||||
Camarilla Pivot Points calculate nine horizontal support and resistance levels from the previous bar's high, low, and close. Unlike classic floor trader pivots that radiate from the PP midpoint, Camarilla levels radiate symmetrically from the previous close using fixed fractions of the prior range. The R3/S3 levels serve as the primary mean-reversion zone; breakouts beyond R4/S4 signal trend continuation. Developed by Nick Scott in 1989 using bond market data, the equation was originally distributed as a shareware Excel plugin.
|
||||
|
||||
## Historical Context
|
||||
|
||||
Nick Scott developed the Camarilla Equation in 1989 while trading bonds. The name references the "Camarilla" (a group of secret advisors), reflecting Scott's belief that institutional traders used similar range-fraction calculations internally. The formula was originally sold as a $50 Excel plug-in, one of the earliest examples of retail algorithmic trading tools.
|
||||
|
||||
The key insight behind Camarilla differs from classic pivots in a fundamental way. Classic pivots treat the prior bar's PP (mean of HLC) as the center of gravity. Camarilla treats the prior close as the center, reasoning that the close represents the market's final consensus. Support and resistance levels are then computed as fixed fractions of the prior range added to or subtracted from the close.
|
||||
|
||||
The specific multiplier constants (1.0833/12, 1.1666/12, 1.25/12, 1.5/12) were derived empirically from bond market data. They produce levels that are tighter than classic pivots, making them more suited to mean-reversion strategies. The R3/S3 levels correspond roughly to the boundaries where intraday price tends to reverse; R4/S4 mark breakout thresholds.
|
||||
|
||||
Classic pivot variants (Woodie, DeMark, Fibonacci) all derive levels from the PP center. Camarilla stands alone in using the close as the anchor point, which makes it inherently different from all other pivot formulations. This close-centric design means Camarilla levels shift when the close changes even if the range stays constant, while classic pivots shift when the range midpoint changes.
|
||||
|
||||
## Architecture and Physics
|
||||
|
||||
### 1. Previous Bar's HLC
|
||||
|
||||
The indicator stores the 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, and the new bar's HLC replaces them for the next computation.
|
||||
|
||||
### 2. Central Pivot Point (PP)
|
||||
|
||||
$$PP = \frac{H_{prev} + L_{prev} + C_{prev}}{3}$$
|
||||
|
||||
The arithmetic mean of the previous bar's HLC. Identical to classic pivot PP. Included for reference and compatibility, though Camarilla levels do not derive from PP.
|
||||
|
||||
### 3. Range
|
||||
|
||||
$$range = H_{prev} - L_{prev}$$
|
||||
|
||||
The previous bar's trading range, used as the scaling factor for all support and resistance levels.
|
||||
|
||||
### 4. Camarilla Multiplier Constants
|
||||
|
||||
| Level | Numerator | Divisor | Effective Multiplier |
|
||||
| :--- | :---: | :---: | :---: |
|
||||
| R1 / S1 | 1.0833 | 12 | 0.090275 |
|
||||
| R2 / S2 | 1.1666 | 12 | 0.097217 |
|
||||
| R3 / S3 | 1.2500 | 12 | 0.104167 |
|
||||
| R4 / S4 | 1.5000 | 12 | 0.125000 |
|
||||
|
||||
### 5. Resistance Levels
|
||||
|
||||
$$R_1 = C_{prev} + range \times \frac{1.0833}{12}$$
|
||||
|
||||
$$R_2 = C_{prev} + range \times \frac{1.1666}{12}$$
|
||||
|
||||
$$R_3 = C_{prev} + range \times \frac{1.2500}{12}$$
|
||||
|
||||
$$R_4 = C_{prev} + range \times \frac{1.5000}{12}$$
|
||||
|
||||
### 6. Support Levels
|
||||
|
||||
$$S_1 = C_{prev} - range \times \frac{1.0833}{12}$$
|
||||
|
||||
$$S_2 = C_{prev} - range \times \frac{1.1666}{12}$$
|
||||
|
||||
$$S_3 = C_{prev} - range \times \frac{1.2500}{12}$$
|
||||
|
||||
$$S_4 = C_{prev} - range \times \frac{1.5000}{12}$$
|
||||
|
||||
### 7. Level Ordering Invariant
|
||||
|
||||
For any bar where $H_{prev} > L_{prev}$ (non-degenerate range):
|
||||
|
||||
$$S_4 < S_3 < S_2 < S_1 < C_{prev} < R_1 < R_2 < R_3 < R_4$$
|
||||
|
||||
Note that PP may be above or below $C_{prev}$ depending on whether the close was nearer the high or low. The support and resistance levels are always ordered by their multiplier magnitude.
|
||||
|
||||
### 8. Nine Outputs
|
||||
|
||||
All nine levels are computed simultaneously and remain constant until a new bar arrives. The primary output (`Last.Val`) returns PP; individual properties expose all nine levels.
|
||||
|
||||
### Signal Interpretation
|
||||
|
||||
| Condition | Interpretation |
|
||||
| :--- | :--- |
|
||||
| Price between S1 and R1 | Normal range; no signal |
|
||||
| Price tests R3 from below | Mean-reversion short entry zone |
|
||||
| Price tests S3 from above | Mean-reversion long entry zone |
|
||||
| Price breaks above R4 | Bullish breakout; trend continuation |
|
||||
| Price breaks below S4 | Bearish breakdown; trend continuation |
|
||||
| R3/S3 rejected | High-probability reversal setup |
|
||||
| Levels cluster tightly | Low volatility prior bar; expect range expansion |
|
||||
|
||||
## Mathematical Foundation
|
||||
|
||||
### Parameters
|
||||
|
||||
Camarilla Pivot Points has no configurable parameters. The formula constants are fixed by definition.
|
||||
|
||||
| Parameter | Value | Notes |
|
||||
| :--- | :---: | :--- |
|
||||
| Inputs | H, L, C | Previous bar's high, low, close |
|
||||
| Outputs | 9 | PP, R1, R2, R3, R4, S1, S2, S3, S4 |
|
||||
| Parameters | 0 | No tuning required |
|
||||
|
||||
### Warmup Period
|
||||
|
||||
$$W = 2$$
|
||||
|
||||
The indicator requires 2 bars: the first bar provides HLC for storage; the second bar triggers computation from the stored values. Prior to warmup completion, all outputs are NaN.
|
||||
|
||||
### Derivation Notes
|
||||
|
||||
The Camarilla multipliers are not derived from mathematical first principles. They are empirical constants fitted to bond market data by Nick Scott. The progression (1.0833, 1.1666, 1.25, 1.5) divided by 12 creates four concentric bands around the close. The spacing between levels is not uniform: the gap between R3/S3 and R4/S4 is wider than between R1/S1 and R2/S2, creating a natural "breakout zone" at the extremes.
|
||||
|
||||
All levels use `Math.FusedMultiplyAdd` for the `close + range * constant` computation, providing single-rounding precision.
|
||||
|
||||
## Performance Profile
|
||||
|
||||
### Implementation Design
|
||||
|
||||
Pure arithmetic with no loops, no buffers, no auxiliary data structures. Each `Update` call performs 1 division (PP), 8 FMA operations, and 3 comparisons for NaN validation.
|
||||
|
||||
| Metric | Score | Notes |
|
||||
| :--- | :--- | :--- |
|
||||
| **Complexity** | O(1) | Fixed arithmetic; no iteration |
|
||||
| **Allocations** | 0 | Hot path is allocation-free |
|
||||
| **Warmup** | 2 bars | Minimum possible |
|
||||
| **Accuracy** | 10/10 | Exact arithmetic via FMA; 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 HLC 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 processes multiple bars but the per-bar computation is too simple (9 arithmetic operations) to benefit from vectorization overhead.
|
||||
|
||||
### FMA Usage
|
||||
|
||||
The implementation uses `Math.FusedMultiplyAdd` for all eight R/S level computations (R1-R4, S1-S4), providing both precision benefit (single rounding instead of two) and potential performance benefit on hardware with FMA support.
|
||||
|
||||
## 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 9 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 | Does not implement Camarilla variant |
|
||||
| **TA-Lib** | N/A | Not implemented |
|
||||
| **Tulip** | N/A | Not implemented |
|
||||
| **Ooples** | N/A | Not validated |
|
||||
|
||||
Mathematical correctness is validated by computing expected values from the Camarilla formula for each bar and comparing against the indicator output.
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
1. **First bar returns NaN.** The indicator needs the previous bar's HLC to compute pivots. The first bar stores HLC but produces no output. This is correct behavior, not a bug. `WarmupPeriod = 2`.
|
||||
|
||||
2. **Levels radiate from close, not PP.** Unlike classic pivots where R/S levels are derived from PP, Camarilla levels are offsets from the previous close. PP is provided for reference only. Do not expect R1 = f(PP) as in classic pivots.
|
||||
|
||||
3. **R3/S3 are the primary trading levels.** The Camarilla system treats R3/S3 as mean-reversion entry zones and R4/S4 as breakout confirmation. R1/S1 and R2/S2 are intermediate levels with less trading significance in the original system.
|
||||
|
||||
4. **Zero-range bars collapse all levels to the close.** When $H_{prev} = L_{prev}$ (doji or single-print bar), all eight R/S levels equal the close, and PP equals the close. This is mathematically correct.
|
||||
|
||||
5. **PP may be above R1 or below S1.** Because R/S levels radiate from the close but PP is based on HLC/3, unusual close positions can cause PP to fall outside the S1-R1 range. This is not a bug; it reflects the different anchoring of classic PP vs. Camarilla levels.
|
||||
|
||||
6. **TValue input collapses range to zero.** When updating with `TValue` instead of `TBar`, all OHLC fields equal the single price, producing zero range and all levels equal to that price. Use `TBar` input for meaningful pivot calculations.
|
||||
|
||||
7. **Multiplier constants are empirical, not mathematical.** The 1.0833/12, 1.1666/12, 1.25/12, 1.5/12 values are fitted constants from bond market data. They have no derivation from probability theory or signal processing. Their effectiveness depends on market microstructure alignment.
|
||||
|
||||
## References
|
||||
|
||||
- Scott, N. (1989). *The Camarilla Equation*. Originally distributed as Excel shareware.
|
||||
- Person, J. L. (2004). *A Complete Guide to Technical Trading Tactics: How to Profit Using Pivot Points, Candlesticks & Other Indicators*. John Wiley and Sons.
|
||||
- Wikipedia: [Pivot point (technical analysis)](https://en.wikipedia.org/wiki/Pivot_point_(technical_analysis))
|
||||
- TradingView: [Camarilla Pivot Points](https://www.tradingview.com/support/solutions/43000521824-pivot-points-standard/)
|
||||
Reference in New Issue
Block a user