mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-21 03:58:04 +00:00
SIMD Refactor: Merge simd-dev into dev (#55)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: aider (openrouter/anthropic/claude-sonnet-4) <aider@aider.chat> Co-authored-by: Warp <agent@warp.dev>
This commit is contained in:
co-authored by
Claude Opus 4.5
aider
Warp
parent
5bcdf8d614
commit
86fe32a682
@@ -0,0 +1,245 @@
|
||||
using Xunit;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class MidpointIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void MidpointIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new MidpointIndicator();
|
||||
|
||||
Assert.Equal(14, indicator.Period);
|
||||
Assert.Equal(SourceType.Close, indicator.Source);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("MIDPOINT - Rolling Range Midpoint", indicator.Name);
|
||||
Assert.False(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MidpointIndicator_MinHistoryDepths_EqualsPeriod()
|
||||
{
|
||||
var indicator = new MidpointIndicator { Period = 20 };
|
||||
Assert.Equal(20, indicator.MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MidpointIndicator_ShortName_IncludesPeriod()
|
||||
{
|
||||
var indicator = new MidpointIndicator { Period = 14 };
|
||||
Assert.Equal("MIDPOINT(14)", indicator.ShortName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MidpointIndicator_Initialize_CreatesLineSeries()
|
||||
{
|
||||
var indicator = new MidpointIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
Assert.Equal("Midpoint", indicator.LinesSeries[0].Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MidpointIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new MidpointIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
|
||||
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
|
||||
Assert.Equal(1, indicator.LinesSeries[0].Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MidpointIndicator_ProcessUpdate_NewBar_ComputesValue()
|
||||
{
|
||||
var indicator = new MidpointIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(1), 102, 108, 92, 106);
|
||||
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
Assert.Equal(2, indicator.LinesSeries[0].Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MidpointIndicator_ProcessUpdate_NewTick_ProcessesWithoutError()
|
||||
{
|
||||
var indicator = new MidpointIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
|
||||
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewTick));
|
||||
|
||||
Assert.Equal(2, indicator.LinesSeries[0].Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MidpointIndicator_MultipleUpdates_ProducesCorrectSequence()
|
||||
{
|
||||
var indicator = new MidpointIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(
|
||||
now.AddMinutes(i),
|
||||
100 + i * 2,
|
||||
105 + i * 2,
|
||||
95 + i * 2,
|
||||
102 + i * 2);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
Assert.Equal(20, indicator.LinesSeries[0].Count);
|
||||
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(i)));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MidpointIndicator_DifferentSourceTypes_Work()
|
||||
{
|
||||
var sources = new[]
|
||||
{
|
||||
SourceType.Open,
|
||||
SourceType.High,
|
||||
SourceType.Low,
|
||||
SourceType.Close,
|
||||
SourceType.HL2,
|
||||
SourceType.HLC3,
|
||||
};
|
||||
|
||||
foreach (var source in sources)
|
||||
{
|
||||
var indicator = new MidpointIndicator { Period = 5, Source = source };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 110, 90, 105);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
Assert.Equal(1, indicator.LinesSeries[0].Count);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MidpointIndicator_ShowColdValues_False_SetsNaN()
|
||||
{
|
||||
var indicator = new MidpointIndicator { Period = 10, ShowColdValues = false };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
Assert.True(double.IsNaN(indicator.LinesSeries[0].GetValue(0)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MidpointIndicator_ComputesMidpoint_Correctly()
|
||||
{
|
||||
var indicator = new MidpointIndicator { Period = 5, Source = SourceType.Close };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// Close prices: 100, 110, 90, 105, 95
|
||||
// Highest = 110, Lowest = 90, Midpoint = (110 + 90) / 2 = 100
|
||||
double[] closes = { 100, 110, 90, 105, 95 };
|
||||
for (int i = 0; i < closes.Length; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), closes[i], closes[i] + 5, closes[i] - 5, closes[i]);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
double lastMidpoint = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.Equal(100, lastMidpoint);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MidpointIndicator_WindowSlides_Correctly()
|
||||
{
|
||||
var indicator = new MidpointIndicator { Period = 3, Source = SourceType.Close };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// Closes: 100, 120, 80, 90, 110
|
||||
// After 5 bars, window = [80, 90, 110]
|
||||
// Highest = 110, Lowest = 80, Midpoint = 95
|
||||
double[] closes = { 100, 120, 80, 90, 110 };
|
||||
for (int i = 0; i < closes.Length; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), closes[i], closes[i] + 5, closes[i] - 5, closes[i]);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
double lastMidpoint = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.Equal(95, lastMidpoint);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MidpointIndicator_SymmetricRange_MidpointEqualsCenter()
|
||||
{
|
||||
var indicator = new MidpointIndicator { Period = 3, Source = SourceType.Close };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// Symmetric: 50, 100, 150 -> midpoint = (150 + 50) / 2 = 100
|
||||
double[] closes = { 50, 100, 150 };
|
||||
for (int i = 0; i < closes.Length; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), closes[i], closes[i] + 5, closes[i] - 5, closes[i]);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
double midpoint = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.Equal(100, midpoint);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MidpointIndicator_DifferentPeriods_Work()
|
||||
{
|
||||
var periods = new[] { 5, 10, 20, 50 };
|
||||
|
||||
foreach (int period in periods)
|
||||
{
|
||||
var indicator = new MidpointIndicator { Period = period };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < period + 10; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(
|
||||
now.AddMinutes(i),
|
||||
100 + i,
|
||||
105 + i,
|
||||
95 + i,
|
||||
102 + i);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
Assert.Equal(period + 10, indicator.LinesSeries[0].Count);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
using System.Drawing;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
using static QuanTAlib.IndicatorExtensions;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// MIDPOINT (Rolling Range Midpoint) Quantower indicator.
|
||||
/// Calculates (Highest + Lowest) / 2 over a rolling lookback window.
|
||||
/// </summary>
|
||||
public class MidpointIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[InputParameter("Period", sortIndex: 0, minimum: 1, maximum: 1000)]
|
||||
public int Period { get; set; } = 14;
|
||||
|
||||
[DataSourceInput]
|
||||
public SourceType Source { get; set; } = SourceType.Close;
|
||||
|
||||
[InputParameter("Show Cold Values", sortIndex: 100)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private Midpoint? _midpoint;
|
||||
private Func<IHistoryItem, double>? _selector;
|
||||
|
||||
public int MinHistoryDepths => Period;
|
||||
public override string ShortName => $"MIDPOINT({Period})";
|
||||
|
||||
public MidpointIndicator()
|
||||
{
|
||||
Name = "MIDPOINT - Rolling Range Midpoint";
|
||||
Description = "Calculates (Highest + Lowest) / 2 over a rolling lookback window";
|
||||
SeparateWindow = false;
|
||||
OnBackGround = true;
|
||||
}
|
||||
|
||||
protected override void OnInit()
|
||||
{
|
||||
_midpoint = new Midpoint(Period);
|
||||
_selector = Source.GetPriceSelector();
|
||||
|
||||
AddLineSeries(new LineSeries("Midpoint", Color.Blue, 2, LineStyle.Solid));
|
||||
}
|
||||
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
if (_midpoint == null || _selector == null) return;
|
||||
|
||||
var item = HistoricalData[0, SeekOriginHistory.End];
|
||||
double value = _selector(item);
|
||||
bool isNew = args.IsNewBar();
|
||||
|
||||
TValue input = new(item.TimeLeft, value);
|
||||
_midpoint.Update(input, isNew);
|
||||
|
||||
bool isHot = _midpoint.IsHot;
|
||||
|
||||
LinesSeries[0].SetValue(_midpoint.Last.Value, isHot, ShowColdValues);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,310 @@
|
||||
using Xunit;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class MidpointTests
|
||||
{
|
||||
private const double Tolerance = 1e-10;
|
||||
|
||||
[Fact]
|
||||
public void Constructor_InvalidPeriod_ThrowsArgumentException()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new Midpoint(0));
|
||||
Assert.Throws<ArgumentException>(() => new Midpoint(-1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_ValidPeriod_SetsProperties()
|
||||
{
|
||||
var indicator = new Midpoint(14);
|
||||
Assert.Equal("Midpoint(14)", indicator.Name);
|
||||
Assert.Equal(14, indicator.WarmupPeriod);
|
||||
Assert.False(indicator.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_ReturnsMidpointInWindow()
|
||||
{
|
||||
var indicator = new Midpoint(3);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
// Single value: midpoint = (5+5)/2 = 5
|
||||
indicator.Update(new TValue(time, 5.0));
|
||||
Assert.Equal(5.0, indicator.Last.Value, Tolerance);
|
||||
|
||||
// Window [5, 8]: midpoint = (8+5)/2 = 6.5
|
||||
indicator.Update(new TValue(time.AddMinutes(1), 8.0));
|
||||
Assert.Equal(6.5, indicator.Last.Value, Tolerance);
|
||||
|
||||
// Window [5, 8, 3]: midpoint = (8+3)/2 = 5.5
|
||||
indicator.Update(new TValue(time.AddMinutes(2), 3.0));
|
||||
Assert.Equal(5.5, indicator.Last.Value, Tolerance);
|
||||
|
||||
// Window [8, 3, 2]: midpoint = (8+2)/2 = 5.0
|
||||
indicator.Update(new TValue(time.AddMinutes(3), 2.0));
|
||||
Assert.Equal(5.0, indicator.Last.Value, Tolerance);
|
||||
|
||||
// Window [3, 2, 10]: midpoint = (10+2)/2 = 6.0
|
||||
indicator.Update(new TValue(time.AddMinutes(4), 10.0));
|
||||
Assert.Equal(6.0, indicator.Last.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_Period1_ReturnsSameValue()
|
||||
{
|
||||
var indicator = new Midpoint(1);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
double value = i * 2.5;
|
||||
indicator.Update(new TValue(time.AddMinutes(i), value));
|
||||
// Midpoint of single value = that value
|
||||
Assert.Equal(value, indicator.Last.Value, Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_IsNewFalse_CorrectsPreviousValue()
|
||||
{
|
||||
var indicator = new Midpoint(5);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
indicator.Update(new TValue(time, 10.0));
|
||||
indicator.Update(new TValue(time.AddMinutes(1), 20.0));
|
||||
indicator.Update(new TValue(time.AddMinutes(2), 15.0));
|
||||
// Window [10, 20, 15]: midpoint = (20+10)/2 = 15.0
|
||||
Assert.Equal(15.0, indicator.Last.Value, Tolerance);
|
||||
|
||||
// Correct last value to 5.0
|
||||
// Window [10, 20, 5]: midpoint = (20+5)/2 = 12.5
|
||||
indicator.Update(new TValue(time.AddMinutes(2), 5.0), isNew: false);
|
||||
Assert.Equal(12.5, indicator.Last.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_IterativeCorrection_RestoresState()
|
||||
{
|
||||
var indicator = new Midpoint(5);
|
||||
var time = DateTime.UtcNow;
|
||||
double[] values = { 5.0, 10.0, 8.0, 12.0, 7.0, 15.0, 11.0 };
|
||||
|
||||
// Process all values
|
||||
foreach (var v in values)
|
||||
{
|
||||
indicator.Update(new TValue(time, v));
|
||||
time = time.AddMinutes(1);
|
||||
}
|
||||
double finalResult = indicator.Last.Value;
|
||||
|
||||
// Reset and process with corrections
|
||||
indicator.Reset();
|
||||
time = DateTime.UtcNow;
|
||||
foreach (var v in values)
|
||||
{
|
||||
// Submit wrong value first
|
||||
indicator.Update(new TValue(time, 0.0));
|
||||
// Correct it
|
||||
indicator.Update(new TValue(time, v), isNew: false);
|
||||
time = time.AddMinutes(1);
|
||||
}
|
||||
|
||||
Assert.Equal(finalResult, indicator.Last.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_NaN_UsesLastValidValue()
|
||||
{
|
||||
var indicator = new Midpoint(5);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
indicator.Update(new TValue(time, 10.0));
|
||||
indicator.Update(new TValue(time.AddMinutes(1), 20.0));
|
||||
double beforeNaN = indicator.Last.Value;
|
||||
|
||||
indicator.Update(new TValue(time.AddMinutes(2), double.NaN));
|
||||
// Should use last valid value
|
||||
Assert.Equal(beforeNaN, indicator.Last.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_Infinity_UsesLastValidValue()
|
||||
{
|
||||
var indicator = new Midpoint(5);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
indicator.Update(new TValue(time, 15.0));
|
||||
indicator.Update(new TValue(time.AddMinutes(1), double.PositiveInfinity));
|
||||
Assert.True(double.IsFinite(indicator.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsHot_BecomesTrueAfterWarmup()
|
||||
{
|
||||
var indicator = new Midpoint(5);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
indicator.Update(new TValue(time.AddMinutes(i), i));
|
||||
Assert.False(indicator.IsHot);
|
||||
}
|
||||
|
||||
indicator.Update(new TValue(time.AddMinutes(4), 4));
|
||||
Assert.True(indicator.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reset_ClearsState()
|
||||
{
|
||||
var indicator = new Midpoint(5);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
indicator.Update(new TValue(time.AddMinutes(i), i * 2));
|
||||
}
|
||||
|
||||
Assert.True(indicator.IsHot);
|
||||
indicator.Reset();
|
||||
Assert.False(indicator.IsHot);
|
||||
Assert.Equal(default, indicator.Last);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pub_EventFires()
|
||||
{
|
||||
var indicator = new Midpoint(5);
|
||||
int eventCount = 0;
|
||||
indicator.Pub += (object? sender, in TValueEventArgs args) => eventCount++;
|
||||
|
||||
indicator.Update(new TValue(DateTime.UtcNow, 10.0));
|
||||
Assert.Equal(1, eventCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Chaining_Constructor_Works()
|
||||
{
|
||||
var source = new TSeries();
|
||||
var indicator = new Midpoint(source, 5);
|
||||
|
||||
source.Add(new TValue(DateTime.UtcNow, 10.0), true);
|
||||
Assert.Equal(10.0, indicator.Last.Value, Tolerance);
|
||||
|
||||
// Window [10, 20]: midpoint = (20+10)/2 = 15
|
||||
source.Add(new TValue(DateTime.UtcNow.AddMinutes(1), 20.0), true);
|
||||
Assert.Equal(15.0, indicator.Last.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_TSeries_MatchesStreaming()
|
||||
{
|
||||
int period = 5;
|
||||
int count = 50;
|
||||
var gbm = new GBM(10000);
|
||||
var bars = gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var source = bars.Close;
|
||||
|
||||
// Streaming
|
||||
var streaming = new Midpoint(period);
|
||||
var streamingResults = new List<double>();
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
streaming.Update(source[i]);
|
||||
streamingResults.Add(streaming.Last.Value);
|
||||
}
|
||||
|
||||
// Batch
|
||||
var batch = Midpoint.Calculate(source, period);
|
||||
|
||||
// Compare last values (after warmup)
|
||||
for (int i = period; i < source.Count; i++)
|
||||
{
|
||||
Assert.Equal(streamingResults[i], batch[i].Value, Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_Span_MatchesTSeries()
|
||||
{
|
||||
int period = 5;
|
||||
int count = 50;
|
||||
var gbm = new GBM(10001);
|
||||
var bars = gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var source = bars.Close;
|
||||
|
||||
// TSeries batch
|
||||
var batchResult = Midpoint.Calculate(source, period);
|
||||
|
||||
// Span calculation
|
||||
var sourceArray = source.Values.ToArray();
|
||||
var output = new double[count];
|
||||
Midpoint.Calculate(sourceArray.AsSpan(), output.AsSpan(), period);
|
||||
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
Assert.Equal(batchResult[i].Value, output[i], Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_Span_ValidatesArguments()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
{
|
||||
Span<double> output = stackalloc double[10];
|
||||
Midpoint.Calculate(ReadOnlySpan<double>.Empty, output, 5);
|
||||
});
|
||||
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
{
|
||||
ReadOnlySpan<double> source = stackalloc double[10];
|
||||
Span<double> output = stackalloc double[5];
|
||||
Midpoint.Calculate(source, output, 5);
|
||||
});
|
||||
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
{
|
||||
ReadOnlySpan<double> source = stackalloc double[10];
|
||||
Span<double> output = stackalloc double[10];
|
||||
Midpoint.Calculate(source, output, 0);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConstantSequence_ReturnsSameValue()
|
||||
{
|
||||
var indicator = new Midpoint(5);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
indicator.Update(new TValue(time.AddMinutes(i), 7.5));
|
||||
// Midpoint of constant sequence = that constant
|
||||
Assert.Equal(7.5, indicator.Last.Value, Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Midpoint_EqualsAverageOfHighestAndLowest()
|
||||
{
|
||||
int period = 5;
|
||||
var gbm = new GBM(12345);
|
||||
var bars = gbm.Fetch(30, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var source = bars.Close;
|
||||
|
||||
var midpoint = new Midpoint(period);
|
||||
var highest = new Highest(period);
|
||||
var lowest = new Lowest(period);
|
||||
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
midpoint.Update(source[i]);
|
||||
highest.Update(source[i]);
|
||||
lowest.Update(source[i]);
|
||||
|
||||
double expected = (highest.Last.Value + lowest.Last.Value) * 0.5;
|
||||
Assert.Equal(expected, midpoint.Last.Value, Tolerance);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
using TALib;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public sealed class MidpointValidationTests : IDisposable
|
||||
{
|
||||
private readonly ValidationTestData _testData;
|
||||
private readonly ITestOutputHelper _output;
|
||||
private bool _disposed;
|
||||
|
||||
public MidpointValidationTests(ITestOutputHelper output)
|
||||
{
|
||||
_output = output;
|
||||
_testData = new ValidationTestData();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
}
|
||||
|
||||
private void Dispose(bool disposing)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
_testData?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Talib_Batch()
|
||||
{
|
||||
int[] periods = { 5, 10, 14, 20, 50 };
|
||||
|
||||
double[] tData = _testData.RawData.ToArray();
|
||||
double[] output = new double[tData.Length];
|
||||
|
||||
foreach (var period in periods)
|
||||
{
|
||||
// Calculate QuanTAlib Midpoint (batch TSeries)
|
||||
var midpoint = new Midpoint(period);
|
||||
var qResult = midpoint.Update(_testData.Data);
|
||||
|
||||
// Calculate TA-Lib MIDPOINT
|
||||
var retCode = TALib.Functions.MidPoint<double>(tData, 0..^0, output, out var outRange, period);
|
||||
Assert.Equal(Core.RetCode.Success, retCode);
|
||||
|
||||
int lookback = TALib.Functions.MidPointLookback(period);
|
||||
|
||||
// Compare last 100 records
|
||||
ValidationHelper.VerifyData(qResult, output, outRange, lookback);
|
||||
}
|
||||
_output.WriteLine("Midpoint Batch(TSeries) validated successfully against TA-Lib MIDPOINT");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Talib_Streaming()
|
||||
{
|
||||
int[] periods = { 5, 10, 14, 20, 50 };
|
||||
|
||||
double[] tData = _testData.RawData.ToArray();
|
||||
double[] output = new double[tData.Length];
|
||||
|
||||
foreach (var period in periods)
|
||||
{
|
||||
// Calculate QuanTAlib Midpoint (streaming)
|
||||
var midpoint = new Midpoint(period);
|
||||
var qResults = new List<double>();
|
||||
foreach (var item in _testData.Data)
|
||||
{
|
||||
qResults.Add(midpoint.Update(item).Value);
|
||||
}
|
||||
|
||||
// Calculate TA-Lib MIDPOINT
|
||||
var retCode = TALib.Functions.MidPoint<double>(tData, 0..^0, output, out var outRange, period);
|
||||
Assert.Equal(Core.RetCode.Success, retCode);
|
||||
|
||||
int lookback = TALib.Functions.MidPointLookback(period);
|
||||
|
||||
// Compare last 100 records
|
||||
ValidationHelper.VerifyData(qResults, output, outRange, lookback);
|
||||
}
|
||||
_output.WriteLine("Midpoint Streaming validated successfully against TA-Lib MIDPOINT");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Talib_Span()
|
||||
{
|
||||
int[] periods = { 5, 10, 14, 20, 50 };
|
||||
|
||||
double[] sourceData = _testData.RawData.ToArray();
|
||||
double[] talibOutput = new double[sourceData.Length];
|
||||
|
||||
foreach (var period in periods)
|
||||
{
|
||||
// Calculate QuanTAlib Midpoint (Span API)
|
||||
double[] qOutput = new double[sourceData.Length];
|
||||
Midpoint.Calculate(sourceData.AsSpan(), qOutput.AsSpan(), period);
|
||||
|
||||
// Calculate TA-Lib MIDPOINT
|
||||
var retCode = TALib.Functions.MidPoint<double>(sourceData, 0..^0, talibOutput, out var outRange, period);
|
||||
Assert.Equal(Core.RetCode.Success, retCode);
|
||||
|
||||
int lookback = TALib.Functions.MidPointLookback(period);
|
||||
|
||||
// Compare last 100 records
|
||||
ValidationHelper.VerifyData(qOutput, talibOutput, outRange, lookback);
|
||||
}
|
||||
_output.WriteLine("Midpoint Span validated successfully against TA-Lib MIDPOINT");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_KnownValues()
|
||||
{
|
||||
// Test with simple known sequence
|
||||
double[] data = { 1, 5, 3, 8, 2, 9, 4, 7, 6, 10 };
|
||||
int period = 3;
|
||||
|
||||
// For each window:
|
||||
// [1] -> (1+1)/2 = 1
|
||||
// [1,5] -> (5+1)/2 = 3
|
||||
// [1,5,3] -> (5+1)/2 = 3
|
||||
// [5,3,8] -> (8+3)/2 = 5.5
|
||||
// [3,8,2] -> (8+2)/2 = 5
|
||||
// [8,2,9] -> (9+2)/2 = 5.5
|
||||
// [2,9,4] -> (9+2)/2 = 5.5
|
||||
// [9,4,7] -> (9+4)/2 = 6.5
|
||||
// [4,7,6] -> (7+4)/2 = 5.5
|
||||
// [7,6,10] -> (10+6)/2 = 8
|
||||
double[] expected = { 1, 3, 3, 5.5, 5, 5.5, 5.5, 6.5, 5.5, 8 };
|
||||
|
||||
var midpoint = new Midpoint(period);
|
||||
for (int i = 0; i < data.Length; i++)
|
||||
{
|
||||
var result = midpoint.Update(new TValue(DateTime.UtcNow, data[i]));
|
||||
Assert.Equal(expected[i], result.Value, precision: 10);
|
||||
}
|
||||
_output.WriteLine("Midpoint validated with known values");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_ConsistencyWithHighestLowest()
|
||||
{
|
||||
// Verify that Midpoint = (Highest + Lowest) / 2
|
||||
int period = 14;
|
||||
var midpoint = new Midpoint(period);
|
||||
var highest = new Highest(period);
|
||||
var lowest = new Lowest(period);
|
||||
|
||||
foreach (var item in _testData.Data)
|
||||
{
|
||||
var midResult = midpoint.Update(item);
|
||||
var highResult = highest.Update(item);
|
||||
var lowResult = lowest.Update(item);
|
||||
|
||||
double expected = (highResult.Value + lowResult.Value) * 0.5;
|
||||
Assert.Equal(expected, midResult.Value, precision: 10);
|
||||
}
|
||||
_output.WriteLine("Midpoint consistency validated: equals (Highest + Lowest) / 2");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_ConstantInput()
|
||||
{
|
||||
// For constant input, midpoint should equal that constant
|
||||
double constant = 42.5;
|
||||
int period = 10;
|
||||
var midpoint = new Midpoint(period);
|
||||
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
var result = midpoint.Update(new TValue(DateTime.UtcNow, constant));
|
||||
Assert.Equal(constant, result.Value, precision: 10);
|
||||
}
|
||||
_output.WriteLine("Midpoint validated with constant input");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
// MIDPOINT: Rolling Midpoint - (Highest + Lowest) / 2 over lookback window
|
||||
// Composes Highest and Lowest indicators for efficient calculation
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// MIDPOINT: Rolling Midpoint
|
||||
/// Calculates the midpoint ((highest + lowest) / 2) over a specified lookback period.
|
||||
/// Composes Highest and Lowest indicators internally.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Key properties:
|
||||
/// - Returns the center of the price range within the lookback window
|
||||
/// - Useful for mean reversion, channel center, trend direction
|
||||
/// - Can be validated against TA-Lib MIDPOINT function
|
||||
/// </remarks>
|
||||
[SkipLocalsInit]
|
||||
public sealed class Midpoint : AbstractBase
|
||||
{
|
||||
private readonly Highest _highest;
|
||||
private readonly Lowest _lowest;
|
||||
private readonly ITValuePublisher? _source;
|
||||
private readonly TValuePublishedHandler? _handler;
|
||||
|
||||
public override bool IsHot => _highest.IsHot && _lowest.IsHot;
|
||||
|
||||
/// <param name="period">Lookback window size (must be >= 1)</param>
|
||||
public Midpoint(int period)
|
||||
{
|
||||
if (period < 1)
|
||||
throw new ArgumentException("Period must be >= 1", nameof(period));
|
||||
|
||||
_highest = new Highest(period);
|
||||
_lowest = new Lowest(period);
|
||||
Name = $"Midpoint({period})";
|
||||
WarmupPeriod = period;
|
||||
}
|
||||
|
||||
/// <param name="source">Source indicator for chaining</param>
|
||||
/// <param name="period">Lookback window size</param>
|
||||
public Midpoint(ITValuePublisher source, int period) : this(period)
|
||||
{
|
||||
_source = source;
|
||||
_handler = HandleUpdate;
|
||||
_source.Pub += _handler;
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && _source != null && _handler != null)
|
||||
{
|
||||
_source.Pub -= _handler;
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void HandleUpdate(object? sender, in TValueEventArgs e) => Update(e.Value, e.IsNew);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override TValue Update(TValue input, bool isNew = true)
|
||||
{
|
||||
TValue high = _highest.Update(input, isNew);
|
||||
TValue low = _lowest.Update(input, isNew);
|
||||
|
||||
double result = (high.Value + low.Value) * 0.5;
|
||||
|
||||
Last = new TValue(input.Time, result);
|
||||
PubEvent(Last, isNew);
|
||||
return Last;
|
||||
}
|
||||
|
||||
public override TSeries Update(TSeries source)
|
||||
{
|
||||
var result = new TSeries(source.Count);
|
||||
ReadOnlySpan<double> values = source.Values;
|
||||
ReadOnlySpan<long> times = source.Times;
|
||||
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
var tv = Update(new TValue(new DateTime(times[i], DateTimeKind.Utc), values[i]), true);
|
||||
result.Add(tv, true);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
|
||||
{
|
||||
TimeSpan interval = step ?? TimeSpan.FromSeconds(1);
|
||||
DateTime time = DateTime.UtcNow - (interval * source.Length);
|
||||
|
||||
for (int i = 0; i < source.Length; i++)
|
||||
{
|
||||
Update(new TValue(time, source[i]), true);
|
||||
time += interval;
|
||||
}
|
||||
}
|
||||
|
||||
public static TSeries Calculate(TSeries source, int period)
|
||||
{
|
||||
var indicator = new Midpoint(period);
|
||||
return indicator.Update(source);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates rolling midpoint over a span of values.
|
||||
/// </summary>
|
||||
public static void Calculate(ReadOnlySpan<double> source, Span<double> output, int period)
|
||||
{
|
||||
if (source.Length == 0)
|
||||
throw new ArgumentException("Source cannot be empty", nameof(source));
|
||||
if (output.Length < source.Length)
|
||||
throw new ArgumentException("Output length must be >= source length", nameof(output));
|
||||
if (period < 1)
|
||||
throw new ArgumentException("Period must be >= 1", nameof(period));
|
||||
|
||||
int len = source.Length;
|
||||
|
||||
// Use ArrayPool for large arrays to avoid stack overflow
|
||||
double[]? rentedHigh = null;
|
||||
double[]? rentedLow = null;
|
||||
|
||||
#pragma warning disable S1121 // Assignments should not be made from within sub-expressions
|
||||
Span<double> highBuffer = len <= 256
|
||||
? stackalloc double[len]
|
||||
: (rentedHigh = System.Buffers.ArrayPool<double>.Shared.Rent(len)).AsSpan(0, len);
|
||||
|
||||
Span<double> lowBuffer = len <= 256
|
||||
? stackalloc double[len]
|
||||
: (rentedLow = System.Buffers.ArrayPool<double>.Shared.Rent(len)).AsSpan(0, len);
|
||||
#pragma warning restore S1121
|
||||
|
||||
try
|
||||
{
|
||||
Highest.Calculate(source, highBuffer, period);
|
||||
Lowest.Calculate(source, lowBuffer, period);
|
||||
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
output[i] = (highBuffer[i] + lowBuffer[i]) * 0.5;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (rentedHigh != null)
|
||||
System.Buffers.ArrayPool<double>.Shared.Return(rentedHigh);
|
||||
if (rentedLow != null)
|
||||
System.Buffers.ArrayPool<double>.Shared.Return(rentedLow);
|
||||
}
|
||||
}
|
||||
|
||||
public override void Reset()
|
||||
{
|
||||
_highest.Reset();
|
||||
_lowest.Reset();
|
||||
Last = default;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
# MIDPOINT: Rolling Range Midpoint
|
||||
|
||||
> "The center of the range is where price gravitates—equilibrium between bulls and bears."
|
||||
|
||||
MIDPOINT calculates the midpoint of the rolling range: (Highest + Lowest) / 2. This represents the equilibrium price level within the lookback window. Validated against TA-Lib MIDPOINT function.
|
||||
|
||||
## Historical Context
|
||||
|
||||
The range midpoint appears throughout technical analysis as a mean-reversion anchor. Donchian Channel centerlines, pivot points, and equilibrium theories all reference the arithmetic mean of high and low extremes. The concept predates computers—floor traders mentally tracked "the middle of the range" to identify fair value.
|
||||
|
||||
The calculation itself is trivial: average the maximum and minimum. The efficiency challenge lies in computing those extremes. QuanTAlib composes MIDPOINT from HIGHEST and LOWEST, each using O(1) amortized monotonic deque algorithms, yielding O(1) amortized midpoint updates.
|
||||
|
||||
## Architecture & Physics
|
||||
|
||||
### 1. Composition Pattern
|
||||
|
||||
MIDPOINT internally maintains two child indicators:
|
||||
|
||||
$$
|
||||
\text{Midpoint}_t = \frac{\text{Highest}_t + \text{Lowest}_t}{2}
|
||||
$$
|
||||
|
||||
Both HIGHEST and LOWEST use monotonic deques independently.
|
||||
|
||||
### 2. Data Flow
|
||||
|
||||
```
|
||||
Input Value
|
||||
│
|
||||
├──► Highest (monotonic decreasing deque) ──► max
|
||||
│
|
||||
└──► Lowest (monotonic increasing deque) ──► min
|
||||
│
|
||||
(max + min) × 0.5 ◄───┘
|
||||
│
|
||||
▼
|
||||
Midpoint
|
||||
```
|
||||
|
||||
### 3. State Synchronization
|
||||
|
||||
When `isNew=false`:
|
||||
1. Both child indicators receive the correction
|
||||
2. Each rebuilds its deque independently
|
||||
3. Midpoint recalculates from corrected extremes
|
||||
|
||||
State consistency is maintained through delegation.
|
||||
|
||||
## Mathematical Foundation
|
||||
|
||||
### Midpoint Definition
|
||||
|
||||
$$
|
||||
\text{Midpoint}_t = \frac{\max_{i \in [t-n+1, t]} V_i + \min_{i \in [t-n+1, t]} V_i}{2}
|
||||
$$
|
||||
|
||||
### Equivalent Formulation
|
||||
|
||||
$$
|
||||
\text{Midpoint}_t = \text{Lowest}_t + \frac{\text{Range}_t}{2}
|
||||
$$
|
||||
|
||||
where $\text{Range}_t = \text{Highest}_t - \text{Lowest}_t$.
|
||||
|
||||
### Properties
|
||||
|
||||
- **Bounds**: $\text{Lowest}_t \leq \text{Midpoint}_t \leq \text{Highest}_t$
|
||||
- **Symmetry**: Equidistant from both extremes by definition
|
||||
- **Range relationship**: $\text{Midpoint} = \text{Lowest} + 0.5 \times \text{Range}$
|
||||
|
||||
## Performance Profile
|
||||
|
||||
### Operation Count (Streaming Mode, Amortized)
|
||||
|
||||
| Operation | Count | Cost (cycles) | Subtotal |
|
||||
| :--- | :---: | :---: | :---: |
|
||||
| Highest update | 1 | ~14 | 14 |
|
||||
| Lowest update | 1 | ~14 | 14 |
|
||||
| ADD | 1 | 1 | 1 |
|
||||
| MUL | 1 | 3 | 3 |
|
||||
| **Total** | **4** | — | **~32 cycles** |
|
||||
|
||||
### Batch Mode Optimization
|
||||
|
||||
The span-based Calculate method can:
|
||||
1. Compute HIGHEST for entire series
|
||||
2. Compute LOWEST for entire series
|
||||
3. Vectorize `(high[i] + low[i]) * 0.5` using SIMD
|
||||
|
||||
Steps 1-2 are sequential (deque-based), but step 3 is embarrassingly parallel.
|
||||
|
||||
### Quality Metrics
|
||||
|
||||
| Metric | Score | Notes |
|
||||
| :--- | :---: | :--- |
|
||||
| **Accuracy** | 10/10 | Exact arithmetic |
|
||||
| **Timeliness** | 8/10 | Lags turning points |
|
||||
| **Smoothness** | 4/10 | Smoother than raw H/L but still stepped |
|
||||
| **Computational Cost** | 9/10 | 2× deque overhead |
|
||||
| **Memory** | 6/10 | 2× buffer memory |
|
||||
|
||||
## Validation
|
||||
|
||||
| Library | Status | Notes |
|
||||
| :--- | :---: | :--- |
|
||||
| **TA-Lib MIDPOINT** | ✅ | Exact match |
|
||||
| **Internal Consistency** | ✅ | (Highest + Lowest) / 2 verified |
|
||||
| **Known Values** | ✅ | Manual verification |
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
1. **Not a Moving Average**: MIDPOINT tracks range center, not price average. A trending series may have midpoint above or below mean price.
|
||||
|
||||
2. **Step Changes**: When either extreme expires from the window, midpoint jumps. This creates non-smooth transitions.
|
||||
|
||||
3. **Double Memory**: Maintains two ring buffers internally. For period=200: ~6.4KB total.
|
||||
|
||||
4. **Mean-Reversion Assumption**: Using midpoint as "fair value" assumes bounded ranges. In trending markets, price may stay above/below midpoint for extended periods.
|
||||
|
||||
5. **Warmup Period**: `IsHot` becomes true after `period` values. Before warmup, computes midpoint of available data.
|
||||
|
||||
6. **Difference from MEDPRICE**: MIDPOINT uses rolling H/L extremes. TA-Lib MEDPRICE uses single-bar (High + Low) / 2. These are distinct calculations.
|
||||
|
||||
## References
|
||||
|
||||
- Donchian, Richard D. (1960). "High Finance in Copper." Financial Analysts Journal.
|
||||
- TA-Lib: MIDPOINT function documentation.
|
||||
- Murphy, John J. (1999). "Technical Analysis of the Financial Markets." New York Institute of Finance.
|
||||
@@ -0,0 +1,29 @@
|
||||
// The MIT License (MIT)
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Midpoint (MIDPOINT)", "MIDPOINT", overlay=true)
|
||||
|
||||
//@function Calculates the midpoint of the highest high and lowest low over a specified period
|
||||
//@doc https://github.com/mihakralj/pinescript/blob/main/indicators/numerics/midpoint.md
|
||||
//@param src Source series to calculate midpoint for
|
||||
//@param len Lookback period for finding highest and lowest values
|
||||
//@returns float The midpoint value (highest + lowest) * 0.5 over the period
|
||||
//@optimized Uses multiplication instead of division for performance
|
||||
midpoint(series float src, simple int len) =>
|
||||
if len <= 0
|
||||
runtime.error("Length must be greater than 0")
|
||||
float highest_val = ta.highest(src, len)
|
||||
float lowest_val = ta.lowest(src, len)
|
||||
(highest_val + lowest_val) * 0.5
|
||||
|
||||
// ---------- Main loop ----------
|
||||
|
||||
// Inputs
|
||||
i_source = input.source(close, "Source")
|
||||
i_length = input.int(14, "Length", minval=1)
|
||||
|
||||
// Calculation
|
||||
result = midpoint(i_source, i_length)
|
||||
|
||||
// Plot
|
||||
plot(result, "Midpoint", color=color.yellow, linewidth=2)
|
||||
Reference in New Issue
Block a user