diff --git a/Tests/test_Trady.cs b/Tests/test_Trady.cs index 95e9e9b1..00bacbab 100644 --- a/Tests/test_Trady.cs +++ b/Tests/test_Trady.cs @@ -8,11 +8,12 @@ public class TradyTests { private readonly TBarSeries bars; private readonly GbmFeed feed; - private Random rnd; + private readonly Random rnd; private readonly double range; - private int period, iterations; - private int skip; - private IEnumerable Candles; + private readonly int iterations; + private int period; + private readonly int skip; + private readonly IEnumerable Candles; public TradyTests() { @@ -44,20 +45,20 @@ public class TradyTests foreach (TBar item in feed) { QL.Add(ma.Calc(new TValue(item.Time, item.Close))); } - var Trady = new SimpleMovingAverage(Candles, period) - .Compute() - .Select(result => new - { - Date = result.DateTime, - Value = result.Tick.HasValue ? (double)result.Tick.Value : double.NaN - }) - .ToList(); + var Trady = new SimpleMovingAverage(Candles, period) + .Compute() + .Select(result => new + { + Date = result.DateTime, + Value = result.Tick.HasValue ? (double)result.Tick.Value : double.NaN + }) + .ToList(); Assert.Equal(QL.Length, Trady.Count); for (int i = QL.Length - 1; i > skip; i--) { double QL_item = QL[i].Value; - double Tr_item = Trady[i].Value; + double Tr_item = Trady[i].Value; Assert.InRange(Tr_item - QL_item, -range, range); } } @@ -74,20 +75,20 @@ public class TradyTests foreach (TBar item in feed) { QL.Add(ma.Calc(new TValue(item.Time, item.Close))); } - var Trady = new ExponentialMovingAverage(Candles, period) - .Compute() - .Select(result => new - { - Date = result.DateTime, - Value = result.Tick.HasValue ? (double)result.Tick.Value : double.NaN - }) - .ToList(); + var Trady = new ExponentialMovingAverage(Candles, period) + .Compute() + .Select(result => new + { + Date = result.DateTime, + Value = result.Tick.HasValue ? (double)result.Tick.Value : double.NaN + }) + .ToList(); Assert.Equal(QL.Length, Trady.Count); - for (int i = QL.Length - 1; i > skip*2; i--) + for (int i = QL.Length - 1; i > skip * 2; i--) { double QL_item = QL[i].Value; - double Tr_item = Trady[i].Value; + double Tr_item = Trady[i].Value; Assert.InRange(Tr_item - QL_item, -range, range); } } diff --git a/Tests/test_Tulip.cs b/Tests/test_Tulip.cs index 5b7cb558..4729e61b 100644 --- a/Tests/test_Tulip.cs +++ b/Tests/test_Tulip.cs @@ -6,12 +6,13 @@ public class TulipTests { private readonly TBarSeries bars; private readonly GbmFeed feed; - private Random rnd; + private readonly Random rnd; private readonly double range; - private int period, iterations; + private int period; + private readonly int iterations; private readonly double[] data; private readonly double[] outdata; - private int skip; + private readonly int skip; public TulipTests() { @@ -44,7 +45,7 @@ public class TulipTests for (int i = QL.Length - 1; i > skip; i--) { double QL_item = QL[i].Value; - double TU = i skip*2; i--) //Initial Tulip Ema value is (wrongly) set to the first input value - therefore large skip + Assert.Equal(QL.Length, arrout[0].Length); + for (int i = QL.Length - 1; i > skip * 2; i--) //Initial Tulip Ema value is (wrongly) set to the first input value - therefore large skip { double QL_item = QL[i].Value; double TU = arrout[0][i]; - //Assert.InRange(TU - QL_item, -range, range); - Assert.True(Math.Abs(TU - QL_item) <= range, $"Assertion failed at index {i} for period {period}: TU = {TU}, QL_item = {QL_item}, delta = {TU-QL_item}"); + Assert.True(Math.Abs(TU - QL_item) <= range, $"Assertion failed at index {i} for period {period}: TU = {TU}, QL_item = {QL_item}, delta = {TU - QL_item}"); } } diff --git a/Tests/test_iTBar.cs b/Tests/test_iTBar.cs index 4f21c69a..db5d70ae 100644 --- a/Tests/test_iTBar.cs +++ b/Tests/test_iTBar.cs @@ -5,7 +5,7 @@ namespace QuanTAlib { public class BarIndicatorTests { - private Random rnd; + private readonly Random rnd; private const int SeriesLen = 1000; private const int Corrections = 100; diff --git a/Tests/test_iTValue.cs b/Tests/test_iTValue.cs index 6504695f..e1b33a98 100644 --- a/Tests/test_iTValue.cs +++ b/Tests/test_iTValue.cs @@ -5,7 +5,7 @@ namespace QuanTAlib { public class IndicatorTests { - private Random rnd; + private readonly Random rnd; private const int SeriesLen = 1000; private const int Corrections = 100; diff --git a/Tests/test_skender.stock.cs b/Tests/test_skender.stock.cs index a29cee3a..7c198cb7 100644 --- a/Tests/test_skender.stock.cs +++ b/Tests/test_skender.stock.cs @@ -6,7 +6,7 @@ public class SkenderTests { private readonly TBarSeries bars; private readonly GbmFeed feed; - private Random rnd; + private readonly Random rnd; private readonly double range; private int period, iterations; private readonly IEnumerable quotes; diff --git a/Tests/test_talib.cs b/Tests/test_talib.cs index 82e267c9..cd073b8e 100644 --- a/Tests/test_talib.cs +++ b/Tests/test_talib.cs @@ -6,9 +6,10 @@ public class TAlibTests { private readonly TBarSeries bars; private readonly GbmFeed feed; - private Random rnd; + private readonly Random rnd; private readonly double range; - private int period, iterations; + private int period; + private readonly int iterations; private readonly double[] data; private readonly double[] TALIB; @@ -77,8 +78,8 @@ public class TAlibTests foreach (TBar item in feed) { QL.Add(ma.Calc(new TValue(item.Time, item.Close))); } Core.Dema(data, 0, QL.Length - 1, TALIB, out int outBegIdx, out _, period); - Assert.Equal(QL.Length, TALIB.Count()); - for (int i = QL.Length - 1; i > period*20; i--) + Assert.Equal(QL.Length, TALIB.Length); + for (int i = QL.Length - 1; i > period * 20; i--) { double TL = i < outBegIdx ? double.NaN : TALIB[i - outBegIdx]; Assert.InRange(TALIB[i - outBegIdx] - QL[i].Value, -range, range); @@ -97,8 +98,8 @@ public class TAlibTests foreach (TBar item in feed) { QL.Add(ma.Calc(new TValue(item.Time, item.Close))); } Core.Tema(data, 0, QL.Length - 1, TALIB, out int outBegIdx, out _, period); - Assert.Equal(QL.Length, TALIB.Count()); - for (int i = QL.Length - 1; i > period*20; i--) + Assert.Equal(QL.Length, TALIB.Length); + for (int i = QL.Length - 1; i > period * 20; i--) { double TL = i < outBegIdx ? double.NaN : TALIB[i - outBegIdx]; Assert.InRange(TALIB[i - outBegIdx] - QL[i].Value, -range, range); @@ -139,9 +140,9 @@ public class TAlibTests TSeries QL = new(); foreach (TBar item in feed) { QL.Add(ma.Calc(new TValue(item.Time, item.Close))); } - Core.T3(data, 0, QL.Length - 1, TALIB, out int outBegIdx, out _, optInTimePeriod: period, optInVFactor: 0.7); - Assert.Equal(QL.Length, TALIB.Count()); - for (int i = QL.Length - 1; i > period*20; i--) + Core.T3(data, 0, QL.Length - 1, TALIB, out int outBegIdx, out _, optInTimePeriod: period, optInVFactor: 0.7); + Assert.Equal(QL.Length, TALIB.Length); + for (int i = QL.Length - 1; i > period * 20; i--) { double TL = i < outBegIdx ? double.NaN : TALIB[i - outBegIdx]; Assert.InRange(TALIB[i - outBegIdx] - QL[i].Value, -range, range); diff --git a/lib/statistics/Curvature.cs b/lib/statistics/Curvature.cs new file mode 100644 index 00000000..4f3f6df7 --- /dev/null +++ b/lib/statistics/Curvature.cs @@ -0,0 +1,128 @@ +using System; +using System.Collections.Generic; + +namespace QuanTAlib +{ + public class Curvature : AbstractBase + { + private readonly int _period; + private readonly Slope _slopeCalculator; + private readonly CircularBuffer _slopeBuffer; + + public double? Intercept { get; private set; } + public double? StdDev { get; private set; } + public double? RSquared { get; private set; } + public double? Line { get; private set; } + + public Curvature(int period) + { + if (period <= 2) + { + throw new ArgumentOutOfRangeException(nameof(period), period, + "Period must be greater than 2 for Curvature calculation."); + } + _period = period; + WarmupPeriod = period * 2 - 1; // We need this many points to get period number of slopes + _slopeCalculator = new Slope(period); + _slopeBuffer = new CircularBuffer(period); + Name = $"Curvature(period={period})"; + + Init(); + } + + public Curvature(object source, int period) : this(period) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); + } + + public override void Init() + { + base.Init(); + _slopeBuffer.Clear(); + Intercept = null; + StdDev = null; + RSquared = null; + Line = null; + } + + protected override void ManageState(bool isNew) + { + if (isNew) + { + _lastValidValue = Input.Value; + _index++; + } + } + + protected override double Calculation() + { + ManageState(Input.IsNew); + + // Calculate slope + var slopeResult = _slopeCalculator.Calc(Input); + _slopeBuffer.Add(slopeResult.Value, Input.IsNew); + + double curvature = 0; + + if (_slopeBuffer.Count < 2) + { + return curvature; // Return 0 when there are fewer than 2 slope points + } + + int count = Math.Min(_slopeBuffer.Count, _period); + var slopes = _slopeBuffer.GetSpan().ToArray(); + + // Calculate averages + double sumX = 0, sumY = 0; + for (int i = 0; i < count; i++) + { + sumX += i + 1; + sumY += slopes[i]; + } + double avgX = sumX / count; + double avgY = sumY / count; + + // Least squares method + double sumSqX = 0, sumSqY = 0, sumSqXY = 0; + for (int i = 0; i < count; i++) + { + double devX = (i + 1) - avgX; + double devY = slopes[i] - avgY; + sumSqX += devX * devX; + sumSqY += devY * devY; + sumSqXY += devX * devY; + } + + if (sumSqX > 0) + { + curvature = sumSqXY / sumSqX; + Intercept = avgY - (curvature * avgX); + + // Calculate Standard Deviation and R-Squared + double stdDevX = Math.Sqrt(sumSqX / count); + double stdDevY = Math.Sqrt(sumSqY / count); + StdDev = stdDevY; + + if (stdDevX * stdDevY != 0) + { + double r = sumSqXY / (stdDevX * stdDevY) / count; + RSquared = r * r; + } + + // Calculate last Line value (y = mx + b) + Line = (curvature * count) + Intercept; + } + else + { + Intercept = null; + StdDev = null; + RSquared = null; + Line = null; + } + + IsHot = _slopeBuffer.Count == _period; + return curvature; + } + } +} \ No newline at end of file diff --git a/lib/statistics/Entropy.cs b/lib/statistics/Entropy.cs index 67b86482..a5f561bb 100644 --- a/lib/statistics/Entropy.cs +++ b/lib/statistics/Entropy.cs @@ -6,8 +6,8 @@ using System.Linq; // Shannon's Entropy calculation public class Entropy : AbstractBase { - public readonly int Period; - private CircularBuffer _buffer; + private readonly int Period; + private readonly CircularBuffer _buffer; public Entropy(int period) : base() { diff --git a/lib/statistics/Kurtosis.cs b/lib/statistics/Kurtosis.cs index 1a5be948..895a175a 100644 --- a/lib/statistics/Kurtosis.cs +++ b/lib/statistics/Kurtosis.cs @@ -3,8 +3,8 @@ namespace QuanTAlib; // Excess kurtosis calculated with Sheskin Algorithm public class Kurtosis : AbstractBase { - public readonly int Period; - private CircularBuffer _buffer; + private readonly int Period; + private readonly CircularBuffer _buffer; public Kurtosis(int period) : base() { diff --git a/lib/statistics/Max.cs b/lib/statistics/Max.cs index 2eccaaaf..4eb63fcf 100644 --- a/lib/statistics/Max.cs +++ b/lib/statistics/Max.cs @@ -4,8 +4,8 @@ namespace QuanTAlib { public class Max : AbstractBase { - public readonly int Period; - private CircularBuffer _buffer; + private readonly int Period; + private readonly CircularBuffer _buffer; private readonly double _halfLife; private double _currentMax, _p_currentMax; private int _timeSinceNewMax, _p_timeSinceNewMax; diff --git a/lib/statistics/Median.cs b/lib/statistics/Median.cs index 05dd4c46..a1a95783 100644 --- a/lib/statistics/Median.cs +++ b/lib/statistics/Median.cs @@ -5,8 +5,8 @@ namespace QuanTAlib { public class Median : AbstractBase { - public readonly int Period; - private CircularBuffer _buffer; + private readonly int Period; + private readonly CircularBuffer _buffer; public Median(int period) : base() { diff --git a/lib/statistics/Min.cs b/lib/statistics/Min.cs index eb842a16..a6b30278 100644 --- a/lib/statistics/Min.cs +++ b/lib/statistics/Min.cs @@ -4,8 +4,8 @@ namespace QuanTAlib { public class Min : AbstractBase { - public readonly int Period; - private CircularBuffer _buffer; + private readonly int Period; + private readonly CircularBuffer _buffer; private readonly double _halfLife; private double _currentMin, _p_currentMin; private int _timeSinceNewMin, _p_timeSinceNewMin; diff --git a/lib/statistics/Mode.cs b/lib/statistics/Mode.cs index 3e685c49..c5f23e52 100644 --- a/lib/statistics/Mode.cs +++ b/lib/statistics/Mode.cs @@ -2,8 +2,8 @@ namespace QuanTAlib; public class Mode : AbstractBase { - public readonly int Period; - private CircularBuffer _buffer; + private readonly int Period; + private readonly CircularBuffer _buffer; public Mode(int period) : base() { diff --git a/lib/statistics/Percentile.cs b/lib/statistics/Percentile.cs index d90afe0e..5377e840 100644 --- a/lib/statistics/Percentile.cs +++ b/lib/statistics/Percentile.cs @@ -5,9 +5,9 @@ using System.Linq; public class Percentile : AbstractBase { - public readonly int Period; - public readonly double Percent; - private CircularBuffer _buffer; + private readonly int Period; + private readonly double Percent; + private readonly CircularBuffer _buffer; public Percentile(int period, double percent) : base() { diff --git a/lib/statistics/Skew.cs b/lib/statistics/Skew.cs index f7aaeec8..f70cec0a 100644 --- a/lib/statistics/Skew.cs +++ b/lib/statistics/Skew.cs @@ -5,8 +5,8 @@ using System.Linq; public class Skew : AbstractBase { - public readonly int Period; - private CircularBuffer _buffer; + private readonly int Period; + private readonly CircularBuffer _buffer; public Skew(int period) : base() { diff --git a/lib/statistics/Slope.cs b/lib/statistics/Slope.cs new file mode 100644 index 00000000..ec2d97ea --- /dev/null +++ b/lib/statistics/Slope.cs @@ -0,0 +1,128 @@ +using System; +using System.Collections.Generic; + +namespace QuanTAlib +{ + public class Slope : AbstractBase + { + private readonly int _period; + private readonly CircularBuffer _buffer; + private readonly CircularBuffer _timeBuffer; + + public double? Intercept { get; private set; } + public double? StdDev { get; private set; } + public double? RSquared { get; private set; } + public double? Line { get; private set; } + + public Slope(int period) + { + if (period <= 1) + { + throw new ArgumentOutOfRangeException(nameof(period), period, + "Period must be greater than 1 for Slope/Linear Regression."); + } + _period = period; + WarmupPeriod = period; + _buffer = new CircularBuffer(period); + _timeBuffer = new CircularBuffer(period); + Name = $"Slope(period={period})"; + + Init(); + } + + public Slope(object source, int period) : this(period) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); + } + + public override void Init() + { + base.Init(); + _buffer.Clear(); + _timeBuffer.Clear(); + Intercept = null; + StdDev = null; + RSquared = null; + Line = null; + } + + protected override void ManageState(bool isNew) + { + if (isNew) + { + _lastValidValue = Input.Value; + _index++; + } + } + + protected override double Calculation() + { + ManageState(Input.IsNew); + + _buffer.Add(Input.Value, Input.IsNew); + _timeBuffer.Add(Input.Time.Ticks, Input.IsNew); + + double slope = 0; + + if (_buffer.Count < 2) + { + return slope; // Return 0 when there are fewer than 2 points + } + + int count = Math.Min(_buffer.Count, _period); + var values = _buffer.GetSpan().ToArray(); + + // Calculate averages + double sumX = 0, sumY = 0; + for (int i = 0; i < count; i++) + { + sumX += i + 1; + sumY += values[i]; + } + double avgX = sumX / count; + double avgY = sumY / count; + + // Least squares method + double sumSqX = 0, sumSqY = 0, sumSqXY = 0; + for (int i = 0; i < count; i++) + { + double devX = (i + 1) - avgX; + double devY = values[i] - avgY; + sumSqX += devX * devX; + sumSqY += devY * devY; + sumSqXY += devX * devY; + } + + if (sumSqX > 0) + { + slope = sumSqXY / sumSqX; + Intercept = avgY - (slope * avgX); + + // Calculate Standard Deviation and R-Squared + double stdDevX = Math.Sqrt(sumSqX / count); + double stdDevY = Math.Sqrt(sumSqY / count); + StdDev = stdDevY; + + if (stdDevX * stdDevY != 0) + { + double r = sumSqXY / (stdDevX * stdDevY) / count; + RSquared = r * r; + } + + // Calculate last Line value (y = mx + b) + Line = (slope * count) + Intercept; + } + else + { + Intercept = null; + StdDev = null; + RSquared = null; + Line = null; + } + + IsHot = _buffer.Count == _period; + return slope; + } + } +} \ No newline at end of file diff --git a/lib/statistics/Stddev.cs b/lib/statistics/Stddev.cs index d1eff641..ca49b2e5 100644 --- a/lib/statistics/Stddev.cs +++ b/lib/statistics/Stddev.cs @@ -5,9 +5,9 @@ namespace QuanTAlib { public class Stddev : AbstractBase { - public readonly int Period; - public readonly bool IsPopulation; - private CircularBuffer _buffer; + private readonly int Period; + private readonly bool IsPopulation; + private readonly CircularBuffer _buffer; public Stddev(int period, bool isPopulation = false) : base() { diff --git a/lib/statistics/Variance.cs b/lib/statistics/Variance.cs index 675958ca..4dc37329 100644 --- a/lib/statistics/Variance.cs +++ b/lib/statistics/Variance.cs @@ -5,9 +5,9 @@ namespace QuanTAlib { public class Variance : AbstractBase { - public readonly int Period; - public readonly bool IsPopulation; - private CircularBuffer _buffer; + private readonly int Period; + private readonly bool IsPopulation; + private readonly CircularBuffer _buffer; public Variance(int period, bool isPopulation = false) : base() { diff --git a/lib/statistics/Zscore.cs b/lib/statistics/Zscore.cs index 87f59b67..6d9a5cee 100644 --- a/lib/statistics/Zscore.cs +++ b/lib/statistics/Zscore.cs @@ -5,8 +5,8 @@ using System.Linq; public class Zscore : AbstractBase { - public readonly int Period; - private CircularBuffer _buffer; + private readonly int Period; + private readonly CircularBuffer _buffer; public Zscore(int period) : base() { diff --git a/quantower/Statistics/CurvatureIndicator.cs b/quantower/Statistics/CurvatureIndicator.cs new file mode 100644 index 00000000..53764aca --- /dev/null +++ b/quantower/Statistics/CurvatureIndicator.cs @@ -0,0 +1,24 @@ +using TradingPlatform.BusinessLayer; +namespace QuanTAlib; + +public class CurvatureIndicator : IndicatorBase +{ + [InputParameter("Period", sortIndex: 1, 2, 2000, 1, 0)] + public int Period { get; set; } = 20; + + private Curvature? curvature; + protected override AbstractBase QuanTAlib => curvature!; + public override string ShortName => $"CURVATURE {Period} : {SourceName}"; + + public CurvatureIndicator() + { + Name = "CURVATURE - Rate of Change of Slope"; + SeparateWindow = true; + } + + protected override void InitIndicator() + { + curvature = new(Period); + MinHistoryDepths = curvature.WarmupPeriod; + } +} \ No newline at end of file diff --git a/quantower/Statistics/SlopeIndicator.cs b/quantower/Statistics/SlopeIndicator.cs new file mode 100644 index 00000000..7ad50cf5 --- /dev/null +++ b/quantower/Statistics/SlopeIndicator.cs @@ -0,0 +1,24 @@ +using TradingPlatform.BusinessLayer; +namespace QuanTAlib; + +public class SlopeIndicator : IndicatorBase +{ + [InputParameter("Period", sortIndex: 1, 2, 2000, 1, 0)] + public int Period { get; set; } = 20; + + private Slope? slope; + protected override AbstractBase QuanTAlib => slope!; + public override string ShortName => $"SLOPE {Period} : {SourceName}"; + + public SlopeIndicator() + { + Name = "SLOPE - Trend Slope"; + SeparateWindow = true; + } + + protected override void InitIndicator() + { + slope = new(Period); + MinHistoryDepths = slope.WarmupPeriod; + } +} \ No newline at end of file