Refactor IndicatorExtensions: Remove unused methods and optimize price retrieval

This commit is contained in:
Miha Kralj
2025-12-24 13:50:19 -08:00
parent c47b106597
commit 8917575994
101 changed files with 1311 additions and 450292 deletions
@@ -25,8 +25,8 @@ public class BilateralIndicatorTests
{
var indicator = new BilateralIndicator { Period = 20 };
Assert.Equal(20, indicator.MinHistoryDepths);
Assert.Equal(20, ((IWatchlistIndicator)indicator).MinHistoryDepths);
Assert.Equal(0, BilateralIndicator.MinHistoryDepths);
Assert.Equal(0, ((IWatchlistIndicator)indicator).MinHistoryDepths);
}
[Fact]
@@ -113,16 +113,6 @@ public class BilateralIndicatorTests
Assert.True(double.IsFinite(secondValue));
}
[Fact]
public void BilateralIndicator_OnPaintChart_DoesNotThrow()
{
var indicator = new BilateralIndicator();
indicator.Initialize();
var method = indicator.GetType().GetMethod("OnPaintChart");
Assert.NotNull(method);
Assert.Equal(typeof(BilateralIndicator), method.DeclaringType);
}
[Fact]
public void BilateralIndicator_MultipleUpdates_ProducesCorrectSequence()
@@ -181,6 +171,6 @@ public class BilateralIndicatorTests
Assert.Equal(20, indicator.Period);
Assert.Equal(1.0, indicator.SigmaSRatio);
Assert.Equal(2.0, indicator.SigmaRMult);
Assert.Equal(20, indicator.MinHistoryDepths);
Assert.Equal(0, BilateralIndicator.MinHistoryDepths);
}
}
+10 -21
View File
@@ -1,8 +1,10 @@
using System.Drawing;
using System.Runtime.CompilerServices;
using TradingPlatform.BusinessLayer;
namespace QuanTAlib;
[SkipLocalsInit]
public class BilateralIndicator : Indicator, IWatchlistIndicator
{
[InputParameter("Period", sortIndex: 1, 1, 1000, 1, 0)]
@@ -23,9 +25,9 @@ public class BilateralIndicator : Indicator, IWatchlistIndicator
private Bilateral? _bilateral;
protected LineSeries? Series;
protected string? SourceName;
private int _warmupBarIndex = -1;
private Func<IHistoryItem, double>? _priceSelector;
public int MinHistoryDepths => Period;
public static int MinHistoryDepths => 0;
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
public override string ShortName => $"Bilateral {Period}:{SourceName}";
@@ -46,30 +48,17 @@ public class BilateralIndicator : Indicator, IWatchlistIndicator
{
_bilateral = new Bilateral(Period, SigmaSRatio, SigmaRMult);
SourceName = Source.ToString();
_warmupBarIndex = -1;
_priceSelector = Source.GetPriceSelector();
base.OnInit();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override void OnUpdate(UpdateArgs args)
{
TValue input = this.GetInputValue(args, Source);
bool isNew = args.Reason == UpdateReason.NewBar || args.Reason == UpdateReason.HistoricalBar;
TValue result = _bilateral!.Update(input, isNew);
Series!.SetValue(result.Value);
Series!.SetMarker(0, Color.Transparent);
var item = HistoricalData[Count - 1, SeekOriginHistory.Begin];
TValue result = _bilateral!.Update(new TValue(item.TimeLeft.Ticks, _priceSelector!(item)), isNew: args.IsNewBar());
if (_warmupBarIndex < 0 && _bilateral!.IsHot)
_warmupBarIndex = Count;
}
public override void OnPaintChart(PaintChartEventArgs args)
{
var savedColor = Series!.Color;
Series.Color = Color.Transparent;
base.OnPaintChart(args);
Series.Color = savedColor;
int warmupPeriod = _warmupBarIndex > 0 ? _warmupBarIndex : Count;
this.PaintLine(args, Series!, warmupPeriod, showColdValues: ShowColdValues);
Series!.SetValue(result.Value, _bilateral.IsHot, ShowColdValues);
}
}
+40 -12
View File
@@ -5,6 +5,13 @@ namespace QuanTAlib;
public class BilateralTests
{
private readonly GBM _gbm;
public BilateralTests()
{
_gbm = new GBM();
}
[Fact]
public void Constructor_ValidatesInput()
{
@@ -101,22 +108,43 @@ public class BilateralTests
}
[Fact]
public void TSeries_Update_Matches_Iterative()
public void AllModes_ProduceSameResult()
{
var indicator = new Bilateral(5);
var series = new TSeries();
for (int i = 0; i < 20; i++)
int period = 10;
var bars = _gbm.Fetch(1000, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var series = bars.Close;
// 1. Batch Mode
var batchSeries = new Bilateral(period).Update(series);
double expected = batchSeries.Last.Value;
// 2. Span Mode
var tValues = series.Values.ToArray();
var spanInput = new ReadOnlySpan<double>(tValues);
var spanOutput = new double[tValues.Length];
Bilateral.Calculate(spanInput, spanOutput, period);
double spanResult = spanOutput[^1];
// 3. Streaming Mode
var streamingInd = new Bilateral(period);
for (int i = 0; i < series.Count; i++)
{
series.Add(new TValue(DateTime.UtcNow.AddMinutes(i), i));
streamingInd.Update(series[i]);
}
var resultSeries = indicator.Update(series);
var indicatorIterative = new Bilateral(5);
for (int i = 0; i < 20; i++)
double streamingResult = streamingInd.Last.Value;
// 4. Eventing Mode
var pubSource = new TSeries();
var eventingInd = new Bilateral(pubSource, period);
for (int i = 0; i < series.Count; i++)
{
indicatorIterative.Update(series[i]);
Assert.Equal(indicatorIterative.Last.Value, resultSeries[i].Value);
pubSource.Add(series[i]);
}
double eventingResult = eventingInd.Last.Value;
// Assert
Assert.Equal(expected, spanResult, 1e-9);
Assert.Equal(expected, streamingResult, 1e-9);
Assert.Equal(expected, eventingResult, 1e-9);
}
}
+102
View File
@@ -260,4 +260,106 @@ public sealed class Bilateral : AbstractBase
_p_state = default;
Last = default;
}
public static void Calculate(ReadOnlySpan<double> source, Span<double> destination, int period, double sigmaSRatio = 0.5, double sigmaRMult = 1.0)
{
if (period <= 0)
throw new ArgumentException("Period must be greater than 0", nameof(period));
// Precalculate spatial weights
double sigmaS = Math.Max(period * sigmaSRatio, 1e-10);
double twoSigmaSSq = 2.0 * sigmaS * sigmaS;
Span<double> spatialWeights = period <= 256 ? stackalloc double[period] : new double[period];
for (int i = 0; i < period; i++)
{
double diffSpatial = i;
spatialWeights[i] = Math.Exp(-(diffSpatial * diffSpatial) / twoSigmaSSq);
}
// Handle NaNs by tracking last valid value
double lastValid = double.NaN;
// Find initial valid value
for (int i = 0; i < source.Length; i++)
{
if (double.IsFinite(source[i]))
{
lastValid = source[i];
break;
}
}
// If all NaNs, fill with NaN
if (double.IsNaN(lastValid))
{
destination.Fill(double.NaN);
return;
}
Span<double> window = period <= 256 ? stackalloc double[period] : new double[period];
int windowIdx = 0;
int count = 0;
double sum = 0;
double sumSq = 0;
for (int i = 0; i < source.Length; i++)
{
double val = source[i];
if (double.IsNaN(val))
{
val = lastValid;
}
else
{
lastValid = val;
}
// Add to window
double removed = 0;
if (count >= period)
{
removed = window[windowIdx];
sum -= removed;
sumSq -= removed * removed;
}
window[windowIdx] = val;
sum += val;
sumSq += val * val;
int currentNewestIdx = windowIdx;
windowIdx = (windowIdx + 1) % period;
if (count < period) count++;
// Calculate StDev
double variance = Math.Max(0, (sumSq - (sum * sum) / count) / count);
double stdev = Math.Sqrt(variance);
double sigmaR = Math.Max(stdev * sigmaRMult, 1e-10);
double twoSigmaRSq = 2.0 * sigmaR * sigmaR;
double sumWeights = 0.0;
double sumWeightedSrc = 0.0;
double centerVal = val; // Newest value
// Iterate backwards through the window
for (int k = 0; k < count; k++)
{
// k=0 is newest (currentNewestIdx)
// k=1 is previous...
int idx = currentNewestIdx - k;
if (idx < 0) idx += period;
double wVal = window[idx];
double diffRange = centerVal - wVal;
double weightRange = Math.Exp(-(diffRange * diffRange) / twoSigmaRSq);
double weight = spatialWeights[k] * weightRange;
sumWeights += weight;
sumWeightedSrc += weight * wVal;
}
destination[i] = sumWeights == 0.0 ? centerVal : sumWeightedSrc / sumWeights;
}
}
}