diff --git a/lib/averages/Ema.cs b/lib/averages/Ema.cs index 6e7c7d9..3e38f74 100644 --- a/lib/averages/Ema.cs +++ b/lib/averages/Ema.cs @@ -90,6 +90,7 @@ public Ema(double alpha) _k = alpha; _useSma = false; _sma = new(1); + Name = "Ema"; _period = 1; WarmupPeriod = (int)Math.Ceiling(Math.Log(0.05) / Math.Log(1 - _k)); //95th percentile Init(); diff --git a/lib/averages/Jma.cs b/lib/averages/Jma.cs index 142a47d..c8e8eba 100644 --- a/lib/averages/Jma.cs +++ b/lib/averages/Jma.cs @@ -6,7 +6,7 @@ namespace QuanTAlib; public class Jma : AbstractBase { - private readonly int _period; + private readonly double _period; private readonly double _phase; private readonly CircularBuffer _vsumBuff; private readonly CircularBuffer _avoltyBuff; @@ -22,6 +22,7 @@ public class Jma : AbstractBase public double UpperBand { get; set; } public double LowerBand { get; set; } public double Volty { get; set; } + public double Factor { get; set; } /// /// Initializes a new instance of the Jma class with the specified parameters. @@ -31,18 +32,19 @@ public class Jma : AbstractBase /// /// Thrown when period is less than 1. /// - public Jma(int period, int phase = 0) + public Jma(int period, int phase = 0, double factor = 0.45) { if (period < 1) { throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 1."); } + Factor = factor; _period = period; _phase = Math.Clamp((phase * 0.01) + 1.5, 0.5, 2.5); _vsumBuff = new CircularBuffer(10); _avoltyBuff = new CircularBuffer(65); - _beta = 0.45 * (period - 1) / (0.45 * (period - 1) + 2); + _beta = factor * (_period - 1) / (factor * (_period - 1) + 2); WarmupPeriod = period * 2; Name = $"JMA({period})"; @@ -114,9 +116,10 @@ protected override double Calculation() ManageState(Input.IsNew); double price = Input.Value; - if (_index == 1) + if (_index <= 1) { _upperBand = _lowerBand = price; + _prevMa1 = _prevJma = price; } double del1 = price - _upperBand; @@ -124,7 +127,7 @@ protected override double Calculation() double volty = Math.Max(Math.Abs(del1), Math.Abs(del2)); _vsumBuff.Add(volty, Input.IsNew); - _vSum += (_vsumBuff[^1] - _vsumBuff[0]) / 10; + _vSum += (_vsumBuff[^1] - _vsumBuff[0]) / _vsumBuff.Count; _avoltyBuff.Add(_vSum, Input.IsNew); double avgvolty = _avoltyBuff.Average(); @@ -137,15 +140,15 @@ protected override double Calculation() _upperBand = (del1 >= 0) ? price : price - (Kv * del1); _lowerBand = (del2 <= 0) ? price : price - (Kv * del2); - double alpha = Math.Pow(_beta, pow2); - double ma1 = (1 - alpha) * Input.Value + alpha * _prevMa1; + double _alpha = Math.Pow(_beta, pow2); + double ma1 = Input.Value + _alpha * (_prevMa1 - Input.Value); //original: (1 - _alpha) * Input.Value + _alpha * _prevMa1; _prevMa1 = ma1; - double det0 = (price - ma1) * (1 - _beta) + _beta * _prevDet0; + double det0 = price + _beta * (_prevDet0 - price + ma1) - ma1; //original: (price - ma1) * (1 - _beta) + _beta * _prevDet0; _prevDet0 = det0; double ma2 = ma1 + _phase * det0; - double det1 = ((ma2 - _prevJma) * (1 - alpha) * (1 - alpha) ) + (alpha * alpha * _prevDet1); + double det1 = ((ma2 - _prevJma) * (1 - _alpha) * (1 - _alpha) ) + (_alpha * _alpha * _prevDet1); _prevDet1 = det1; double jma = _prevJma + det1; _prevJma = jma; diff --git a/lib/averages/Rma.cs b/lib/averages/Rma.cs index f8077fa..628d831 100644 --- a/lib/averages/Rma.cs +++ b/lib/averages/Rma.cs @@ -1,18 +1,18 @@ -using System; - namespace QuanTAlib; + /// /// RMA: Relative Moving Average (also known as Wilder's Moving Average) -/// RMA is similar to EMA but uses a different smoothing factor. /// /// +/// RMA is similar to EMA but uses a different smoothing factor. +/// /// Key characteristics: /// - Uses no buffer, relying only on the previous RMA value. /// - The weight of new data points (alpha) is calculated as 1 / period. /// - Provides a smoother curve compared to SMA and EMA, reacting more slowly to price changes. /// /// Calculation method: -/// RMA = (Previous RMA * (period - 1) + New Data) / period +/// This implementation can use SMA for the first Period bars as a seeding value for RMA when useSma is true. /// /// Sources: /// - https://www.tradingview.com/pine-script-reference/v5/#fun_ta{dot}rma @@ -20,75 +20,143 @@ namespace QuanTAlib; /// public class Rma : AbstractBase { + // inherited _index + // inherited _value + + /// + /// The period for the RMA calculation. + /// private readonly int _period; - private double _lastRma; - private readonly double _alpha; - private double _savedLastRma; - public Rma(int period) + /// + /// Circular buffer for SMA calculation. + /// + private CircularBuffer _sma; + + /// + /// The last calculated RMA value. + /// + private double _lastRma, _p_lastRma; + + /// + /// Compensator for early RMA values. + /// + private double _e, _p_e; + + /// + /// The smoothing factor for RMA calculation. + /// + private readonly double _k; + + /// + /// Flags to track initialization status. + /// + private bool _isInit, _p_isInit; + + /// + /// Flag to determine whether to use SMA for initial values. + /// + private readonly bool _useSma; + + /// + /// Initializes a new instance of the Rma class with a specified period. + /// + /// The period for RMA calculation. + /// Whether to use SMA for initial values. Default is true. + /// Thrown when period is less than 1. + public Rma(int period, bool useSma = true) { if (period < 1) { - throw new ArgumentException("Period must be greater than or equal to 1.", nameof(period)); + throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 1."); } _period = period; - WarmupPeriod = period * 2; - _alpha = 1.0 / _period; // Wilder's smoothing factor - Name = $"Rma({_period})"; + _k = 1.0 / _period; // Wilder's smoothing factor + _useSma = useSma; + _sma = new(period); + Name = "Rma"; + WarmupPeriod = _period * 2; // RMA typically needs more warmup periods Init(); } - public Rma(object source, int period) : this(period) + /// + /// Initializes a new instance of the Rma class with a specified source and period. + /// + /// The source object for event subscription. + /// The period for RMA calculation. + /// Whether to use SMA for initial values. Default is true. + public Rma(object source, int period, bool useSma = true) : this(period, useSma) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + /// + /// Initializes the Rma instance. + /// public override void Init() { base.Init(); + _e = 1.0; _lastRma = 0; - _savedLastRma = 0; + _isInit = false; + _p_isInit = false; + _sma = new(_period); } + /// + /// Manages the state of the Rma instance. + /// + /// Indicates whether the input is new. protected override void ManageState(bool isNew) { if (isNew) { - _savedLastRma = _lastRma; - _lastValidValue = Input.Value; + _p_lastRma = _lastRma; + _p_isInit = _isInit; + _p_e = _e; _index++; } else { - _lastRma = _savedLastRma; + _lastRma = _p_lastRma; + _isInit = _p_isInit; + _e = _p_e; } } + /// + /// Performs the RMA calculation. + /// + /// The calculated RMA value. protected override double Calculation() { + double result, _rma; ManageState(Input.IsNew); - double rma; - - if (_index == 1) - { - rma = Input.Value; - } - else if (_index <= _period) + // when _UseSma == true, use SMA calculation until we have enough data points + if (!_isInit && _useSma) { - // Simple average during initial period - rma = (_lastRma * (_index - 1) + Input.Value) / _index; + _sma.Add(Input.Value, Input.IsNew); + _rma = _sma.Average(); + result = _rma; + if (_index >= _period) + { + _isInit = true; + } } else { - // Wilder's smoothing method - rma = _alpha * (_lastRma - Input.Value) + _lastRma; - } + // compensator for early rma values + _e = (_e > 1e-10) ? (1 - _k) * _e : 0; - _lastRma = rma; - IsHot = _index >= WarmupPeriod; + _rma = _k * Input.Value + (1 - _k) * _lastRma; - return rma; + // _useSma decides if we use compensator or not + result = (_useSma || _e <= double.Epsilon) ? _rma : _rma / (1 - _e); + } + _lastRma = _rma; + IsHot = _index >= WarmupPeriod; + return result; } } diff --git a/lib/core/abstractBase.cs b/lib/core/abstractBase.cs index 3859473..3ac3d97 100644 --- a/lib/core/abstractBase.cs +++ b/lib/core/abstractBase.cs @@ -57,6 +57,12 @@ public virtual TValue Calc(TValue input) Input2 = new(Time: Input.Time, Value: double.NaN, IsNew: Input.IsNew, IsHot: Input.IsHot); return Process(input.Value, input.Time, input.IsNew); } + public virtual TValue Calc(double value, bool IsNew) + { + Input = new(this.Time, Value: value, IsNew: IsNew, IsHot: false); + Input2 = new(this.Time, double.NaN, false, false); + return Process(Input.Value, Input.Time, Input.IsNew); + } public virtual TValue Calc(TBar barInput) { diff --git a/lib/core/tbar.cs b/lib/core/tbar.cs index 441994e..9b72bc8 100644 --- a/lib/core/tbar.cs +++ b/lib/core/tbar.cs @@ -32,6 +32,8 @@ public TBar() : this(DateTime.UtcNow, 0, 0, 0, 0, 0) { } public TBar(double Open, double High, double Low, double Close, double Volume, bool IsNew = true) : this(DateTime.UtcNow, Open, High, Low, Close, Volume, IsNew) { } public TBar(double value) : this(Time: DateTime.UtcNow, Open: value, High: value, Low: value, Close: value, Volume: value, IsNew: true) { } public TBar(TValue value) : this(Time: value.Time, Open: value.Value, High: value.Value, Low: value.Value, Close: value.Value, Volume: value.Value, IsNew: value.IsNew) { } +public TBar(TBar v) : this(Time: v.Time, Open: v.Open, High: v.High, Low: v.Low, Close: v.Close, Volume: v.Volume, IsNew: true) { } + public static implicit operator double(TBar bar) => bar.Close; public static implicit operator DateTime(TBar tv) => tv.Time; diff --git a/lib/core/tvalue.cs b/lib/core/tvalue.cs index f7c83cf..843768d 100644 --- a/lib/core/tvalue.cs +++ b/lib/core/tvalue.cs @@ -52,13 +52,13 @@ public TSeries(object source) : this() var pubEvent = source.GetType().GetEvent("Pub"); if (pubEvent != null) { - +/* var nameProperty = source.GetType().GetProperty("Name"); if (nameProperty != null) { Name = nameProperty.GetValue(nameProperty)?.ToString()!; } - +*/ pubEvent.AddEventHandler(source, new ValueSignal(Sub)); } } diff --git a/lib/feeds/GbmFeed.cs b/lib/feeds/GbmFeed.cs index 5b2edd2..566ee8d 100644 --- a/lib/feeds/GbmFeed.cs +++ b/lib/feeds/GbmFeed.cs @@ -6,11 +6,11 @@ public class GbmFeed : TBarSeries { private readonly double _mu, _sigma; private readonly RandomNumberGenerator _rng; - private double _lastClose, _lastHigh, _lastLow; + private double _lastClose; public GbmFeed(double initialPrice = 100.0, double mu = 0.05, double sigma = 0.2) { - _lastClose = _lastHigh = _lastLow = initialPrice; + _lastClose = initialPrice; _mu = mu; _sigma = sigma; _rng = RandomNumberGenerator.Create(); @@ -24,9 +24,7 @@ public void Add(int count) DateTime startTime = DateTime.UtcNow - TimeSpan.FromHours(count); for (int i = 0; i < count; i++) { - Add(startTime, true); - Add(startTime, false); - Add(startTime, false); + Add(startTime, isNew: true); startTime = startTime.AddHours(1); } } @@ -36,27 +34,29 @@ public TBar Generate(DateTime time, bool isNew = true) double dt = 1.0 / 252; double drift = (_mu - 0.5 * _sigma * _sigma) * dt; double diffusion = _sigma * Math.Sqrt(dt) * GenerateNormalRandom(); - double newClose = _lastClose * Math.Exp(drift + diffusion); double open = _lastClose; - double high = Math.Max(_lastHigh, Math.Max(open, newClose) * (1 + GenerateRandomDouble() * 0.01)); - double low = Math.Min(_lastLow, Math.Min(open, newClose) * (1 - GenerateRandomDouble() * 0.01)); + double close = open * Math.Exp(drift + diffusion); + + // Generate intra-bar price movements + double maxMove = Math.Abs(close - open) * 1.5; // Allow for some extra movement within the bar + double high = Math.Max(open, close) + maxMove * GenerateRandomDouble(); + double low = Math.Min(open, close) - maxMove * GenerateRandomDouble(); + + // Ensure high is always greater than or equal to both open and close + high = Math.Max(high, Math.Max(open, close)); + + // Ensure low is always less than or equal to both open and close + low = Math.Min(low, Math.Min(open, close)); + double volume = 1000 + GenerateRandomDouble() * 1000; if (isNew) { - _lastClose = newClose; - } - else - { - high = Math.Max(_lastHigh, high); - low = Math.Min(_lastLow, low); + _lastClose = close; } - _lastHigh = high; - _lastLow = low; - TBar bar = new(time, open, high, low, newClose, volume, isNew); - return bar; + return new TBar(time, open, high, low, close, volume, isNew); } private double GenerateNormalRandom() @@ -73,4 +73,4 @@ private double GenerateRandomDouble() _rng.GetBytes(bytes); return (double)BitConverter.ToUInt64(bytes, 0) / ulong.MaxValue; } -} \ No newline at end of file +} diff --git a/lib/quantalib.csproj b/lib/quantalib.csproj index 716dc1a..3f706bc 100644 --- a/lib/quantalib.csproj +++ b/lib/quantalib.csproj @@ -31,6 +31,7 @@ https://raw.githubusercontent.com/mihakralj/QuanTAlib/main/.github/QuanTAlib2.png True false + $(NoWarn);NU1903;NU5104 diff --git a/lib/volatility/Atr.cs b/lib/volatility/Atr.cs index e6c2084..3ea3e5f 100644 --- a/lib/volatility/Atr.cs +++ b/lib/volatility/Atr.cs @@ -4,13 +4,14 @@ namespace QuanTAlib; /// Represents an Average True Range (ATR) calculator, a measure of market volatility. /// /// -/// The ATR class calculates the average true range using an Exponential Moving Average (EMA) +/// The ATR class calculates the average true range using a Relative Moving Average (RMA) /// of the true range. The true range is the greatest of: current high - current low, /// absolute value of current high - previous close, or absolute value of current low - previous close. /// public class Atr : AbstractBase { - private readonly Ema _ma; + public double Tr { get; private set; } + private readonly Rma _ma; private double _prevClose, _p_prevClose; /// @@ -26,7 +27,7 @@ public Atr(int period) { throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 1."); } - _ma = new(1.0 / period); + _ma = new(period, useSma: true); WarmupPeriod = _ma.WarmupPeriod; Name = $"ATR({period})"; } @@ -50,6 +51,7 @@ public override void Init() base.Init(); _ma.Init(); _prevClose = double.NaN; + Tr = 0; } /// @@ -76,7 +78,7 @@ protected override void ManageState(bool isNew) /// The calculated ATR value for the current bar. /// /// - /// This method calculates the true range for the current bar and then uses an EMA + /// This method calculates the true range for the current bar and then uses an RMA /// to smooth the true range values. For the first bar, it uses the high-low range /// as the true range. /// @@ -84,22 +86,25 @@ protected override double Calculation() { ManageState(BarInput.IsNew); - double trueRange = Math.Max( - Math.Max( - BarInput.High - BarInput.Low, - Math.Abs(BarInput.High - _prevClose) - ), - Math.Abs(BarInput.Low - _prevClose) - ); - if (_index < 2) + if (_index == 1) + { + Tr = BarInput.High - BarInput.Low; + _prevClose = BarInput.Close; + } + else { - trueRange = BarInput.High - BarInput.Low; + Tr = Math.Max( + BarInput.High - BarInput.Low, + Math.Max( + Math.Abs(BarInput.High - _prevClose), + Math.Abs(BarInput.Low - _prevClose) + ) + ); } + _ma.Calc(new TValue(Input.Time, Tr, BarInput.IsNew)); - TValue emaTrueRange = _ma.Calc(new TValue(Input.Time, trueRange, Input.IsNew)); IsHot = _ma.IsHot; _prevClose = BarInput.Close; - - return emaTrueRange.Value; + return _ma.Value; } } diff --git a/notebooks/core.dib b/notebooks/core.dib index 2f997b4..1fa0200 100644 --- a/notebooks/core.dib +++ b/notebooks/core.dib @@ -4,7 +4,7 @@ #!csharp -#r "..\src\obj\Debug\QuanTAlib.dll" +#r "..\lib\obj\Debug\QuanTAlib.dll" #r "nuget:Skender.Stock.Indicators" using Skender.Stock.Indicators; @@ -13,113 +13,56 @@ QuanTAlib.Formatters.Initialize(); #!csharp +Atr ma = new(10); GbmFeed gbm = new(); -EmaCalc ema1 = new(gbm.Close, 10, useSma: false); -EmaCalc ema2 = new(gbm.Close, 10, useSma: true); -TValSeries res1 = new(ema1); -TValSeries res2 = new(ema2); -gbm.Add(50); -List mse1 = new(); -List mse2 = new(); - - +gbm.Add(30); +IEnumerable quotes = gbm.Select(item => new Quote { Date = item.Time, Open = (decimal)item.Open, High = (decimal)item.High, Low = (decimal)item.Low, Close = (decimal)item.Close, Volume = (decimal)item.Volume }); +var SkResults = quotes.GetAtr(10).Select(i => i.Atr.Null2NaN()!); for (int i=0; i< gbm.Length; i++) { - double v= gbm.Close[i].Value; - double e1 = res1[i].Value; - mse1.Add((e1-v)*(e1-v)); - double e2 = res2[i].Value; - mse2.Add((e2-v)*(e2-v)); - - //Console.WriteLine($"{i,3} {mse1.Average(),10:F4} {mse2.Average(),10:F4}"); + ma.Calc(gbm[i]); + Console.WriteLine($"{i,3} {ma.Value,10:F3} \t {SkResults.ElementAt(i):F3}"); } - Console.WriteLine($"{mse2.Average()-mse1.Average(),10:F8}"); - -#!csharp - -display(res1); - #!csharp +Atr ma = new(10); GbmFeed gbm = new(); -EmaCalc ema1 = new(gbm.Close, 10, useSma: false); -EmaCalc ema2 = new(gbm.Close, 10, useSma: true); -TValSeries res1 = new(ema1); -TValSeries res2 = new(ema2); gbm.Add(30); -IEnumerable quotes = gbm.Close.Select(item => new Quote { Date = item.Time, Close = (decimal)item.Value }); -var SkResults = quotes.GetEma(10).Select(i => i.Ema.Null2NaN()!); +IEnumerable quotes = gbm.Select(item => new Quote { Date = item.Time, Open = (decimal)item.Open, High = (decimal)item.High, Low = (decimal)item.Low, Close = (decimal)item.Close, Volume = (decimal)item.Volume }); +var SkResults = quotes.GetTr().Select(i => i.Tr.Null2NaN()!); for (int i=0; i< gbm.Length; i++) { - Console.WriteLine($"{i,3} {gbm.Close[i].Value,6:F2} {res1[i].Value,10:F4} {res2[i].Value,10:F4} {SkResults.ElementAt(i),10:F4}"); -} + ma.Calc(new TBar(gbm[i])); -#!csharp - -TValSeries test = new(); - -EmaCalc ma1 = new(test, 7, true); -TValSeries res1 = new(ma1); - -EmaCalc ma2 = new(test, 7, false); -TValSeries res2 = new(ma2); - -test.Add(new[]{1.0,0,0,0,0,0,1,1,1,1,1,0,0,0,0,0}); - -for (int i=0; i quotes = gbm.Select(item => new Quote { Date = item.Time, Open = (decimal)item.Open, High = (decimal)item.High, Low = (decimal)item.Low, Close = (decimal)item.Close, Volume = (decimal)item.Volume }); +var SkResults = quotes.GetAtr(10).Select(i => i.Atr.Null2NaN()!); +for (int i=0; i< gbm.Length; i++) { + double delta = Math.Round(res1[i].Value, 10) - Math.Round(SkResults.ElementAt(i), 10); + //Console.WriteLine($"{i,3} {gbm.High[i].Value,6:F2} {gbm.Low[i].Value,6:F2} {gbm.Close[i].Value,6:F2} {res1[i].Value,10:F4} {SkResults.ElementAt(i),10:F4}\t{delta}"); + Console.WriteLine($"{i,3} h:{gbm.High[i].Value,6:F2} l:{gbm.Low[i].Value,6:F2} c:{gbm.Close[i].Value,6:F2} {res1[i].Atr,10:F4} {SkResults.ElementAt(i),10:F4}\t{delta}"); } -display(result); - -#!csharp - -TValSeries test = new(); -SmaCalc ma = new(test,7); -TValSeries result = new(ma); -test.Add(new[]{81.59, 81.06, 82.87, 83.00, 83.61, 83.15, 82.84, 83.99, 84.55, 84.36, 85.53, 86.54, 86.89, 87.77, 87.29}); -//test.Add(new[]{1.0,0,0,0,0,0,1,1,1,1,1,0,0,0,0,0}); - -display(result); #!csharp -TValue test = new(DateTime.Today, 100, IsHot: false); -TValSeries pub = new(); -TValSeries sub = new(pub); -pub.Add(test); -pub.Add(test); -pub.Add(2, true); -pub.Add(DateTime.Today, 123.1234214234, IsHot: true); - - -display(sub); -display(test); - -#!csharp - -TBar test = new(DateTime.Now, double.NaN,1,2,3,400.1234); - -TBarSeries source = new(); -TValSeries target = new(source.Close); - -source.Add(new TBar(DateTime.Now,1,2,3,4,125, true)); -source.Add(new TBar(DateTime.Now,2,1,5,2,1312, true)); -source.Add(test); -source.Name = "MSFT"; -display(source); -display(test) - -#!csharp - -#r "nuget:Skender.Stock.Indicators" -using Skender.Stock.Indicators; +//EMA test +GbmFeed gbm = new(); +Ema ema1 = new(gbm.Close, 10, useSma: true); +TSeries res1 = new(ema1); +gbm.Add(30); +IEnumerable quotes = gbm.Close.Select(item => new Quote { Date = item.Time, Close = (decimal)item.Value }); +var SkResults = quotes.GetEma(10).Select(i => i.Ema.Null2NaN()!); +for (int i=0; i< gbm.Length; i++) { + double delta = Math.Round(res1[i].Value, 10) - Math.Round(SkResults.ElementAt(i), 10); + Console.WriteLine($"{i,3} {gbm.Close[i].Value,6:F2} {res1[i].Value,10:F4} {SkResults.ElementAt(i),10:F4}\t{delta}"); +} diff --git a/quantower/Averages/JmaIndicator.cs b/quantower/Averages/JmaIndicator.cs index e661c9d..5050d3d 100644 --- a/quantower/Averages/JmaIndicator.cs +++ b/quantower/Averages/JmaIndicator.cs @@ -11,6 +11,9 @@ public class JmaIndicator : Indicator, IWatchlistIndicator [InputParameter("Phase", sortIndex: 2, -100, 100, 1, 0)] public int Phase { get; set; } = 0; + [InputParameter("Beta factor", sortIndex: 3, minimum: 0, maximum:5 , increment: 0.01, decimalPlaces: 2)] + public double Factor { get; set; } = 0.45; + [InputParameter("Data source", sortIndex: 4, variants: [ "Open", SourceType.Open, "High", SourceType.High, @@ -34,7 +37,7 @@ public class JmaIndicator : Indicator, IWatchlistIndicator public int MinHistoryDepths => Math.Max(65,Periods * 2); int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths; - public override string ShortName => $"JMA {Periods}:{Phase}:{SourceName}"; + public override string ShortName => $"JMA {Periods}:{Phase}:{Factor:F2}:{SourceName}"; public JmaIndicator() { @@ -49,7 +52,7 @@ public JmaIndicator() protected override void OnInit() { - ma = new Jma(Periods, Phase); + ma = new Jma(period: Periods, phase: Phase, factor: Factor); SourceName = Source.ToString(); base.OnInit(); } diff --git a/quantower/IndicatorExtensions.cs b/quantower/IndicatorExtensions.cs index 522c4a3..90d37e1 100644 --- a/quantower/IndicatorExtensions.cs +++ b/quantower/IndicatorExtensions.cs @@ -59,14 +59,35 @@ public static TBar GetInputBar(this Indicator indicator, UpdateArgs args) #pragma warning disable CA1416 // Validate platform compatibility + public static void PaintHLine(this Indicator indicator, PaintChartEventArgs args, double value, Pen pen) + { + if (indicator.CurrentChart == null) + return; + + Graphics gr = args.Graphics; + var mainWindow = indicator.CurrentChart.Windows[args.WindowIndex]; + var converter = mainWindow.CoordinatesConverter; + var clientRect = mainWindow.ClientRectangle; + gr.SetClip(clientRect); + int leftX = clientRect.Left; + int rightX = clientRect.Right; + int Y = (int)converter.GetChartY(value); + using (pen) + { + gr.DrawLine(pen, new Point(leftX, Y), new Point(rightX, Y)); + } + } + public static void PaintSmoothCurve(this Indicator indicator, PaintChartEventArgs args, LineSeries series, int warmupPeriod, bool showColdValues = true, double tension = 0.2) { if (!series.Visible || indicator.CurrentChart == null) return; Graphics gr = args.Graphics; - var mainWindow = indicator.CurrentChart.MainWindow; + gr.SmoothingMode = SmoothingMode.AntiAlias; + var mainWindow = indicator.CurrentChart.Windows[args.WindowIndex]; var converter = mainWindow.CoordinatesConverter; + var clientRect = mainWindow.ClientRectangle; gr.SetClip(clientRect); diff --git a/quantower/Volatility/AtrIndicator.cs b/quantower/Volatility/AtrIndicator.cs index de28856..a2f03aa 100644 --- a/quantower/Volatility/AtrIndicator.cs +++ b/quantower/Volatility/AtrIndicator.cs @@ -8,9 +8,12 @@ public class AtrIndicator : Indicator, IWatchlistIndicator [InputParameter("Periods", sortIndex: 1, 1, 2000, 1, 0)] public int Periods { get; set; } = 20; + [InputParameter("Show cold values", sortIndex: 21)] + public bool ShowColdValues { get; set; } = true; + private Atr? atr; protected LineSeries? AtrSeries; - public static int MinHistoryDepths => 2; + public int MinHistoryDepths => Math.Max(5, Periods * 2); int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths; public AtrIndicator() @@ -19,7 +22,7 @@ public AtrIndicator() Description = "Measures market volatility by calculating the average range between high and low prices."; SeparateWindow = true; - AtrSeries = new("ATR", Color.Blue, 2, LineStyle.Solid); + AtrSeries = new($"ATR {Periods}", Color.Blue, 2, LineStyle.Solid); AddLineSeries(AtrSeries); } @@ -35,7 +38,16 @@ protected override void OnUpdate(UpdateArgs args) TValue result = atr!.Calc(input); AtrSeries!.SetValue(result.Value); + AtrSeries!.SetMarker(0, Color.Transparent); //OnPaintChart draws the line, hidden here + } public override string ShortName => $"ATR ({Periods})"; + + public override void OnPaintChart(PaintChartEventArgs args) + { + base.OnPaintChart(args); + this.PaintHLine(args, 0.05, new Pen(Color.DarkRed, width: 2)); + this.PaintSmoothCurve(args, AtrSeries!, atr!.WarmupPeriod, showColdValues: ShowColdValues, tension: 0.2); + } } diff --git a/quantower/Volatility/FlowIndicator.cs b/quantower/Volatility/FlowIndicator.cs new file mode 100644 index 0000000..d769549 --- /dev/null +++ b/quantower/Volatility/FlowIndicator.cs @@ -0,0 +1,78 @@ +using System.Drawing; +using System.Drawing.Drawing2D; +using TradingPlatform.BusinessLayer; + +namespace QuanTAlib; + +public class FlowIndicator : Indicator, IWatchlistIndicator +{ + protected string? SourceName; + public static int MinHistoryDepths => 2; + int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths; + + public FlowIndicator() + { + Name = "Flow Visualization"; + SeparateWindow = false; + } + + protected override void OnInit() + { + // placeholder + } + + protected override void OnUpdate(UpdateArgs args) + { + // placeholder + } + +#pragma warning disable CA1416 // Validate platform compatibility + + public override void OnPaintChart(PaintChartEventArgs args) + { + base.OnPaintChart(args); + Graphics gr = args.Graphics; + gr.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias; + var mainWindow = this.CurrentChart.Windows[args.WindowIndex]; + var converter = mainWindow.CoordinatesConverter; + var clientRect = mainWindow.ClientRectangle; + gr.SetClip(clientRect); + DateTime leftTime = new[] { converter.GetTime(clientRect.Left), this.HistoricalData.Time(this!.Count - 1) }.Max(); + DateTime rightTime = new[] { converter.GetTime(clientRect.Right), this.HistoricalData.Time(0) }.Min(); + + int leftIndex = (int)this.HistoricalData.GetIndexByTime(leftTime.Ticks) + 1; + int rightIndex = (int)this.HistoricalData.GetIndexByTime(rightTime.Ticks); + int width = this.CurrentChart.BarsWidth; + + for (int i = rightIndex; i < leftIndex; i++) + { + int barX1 = (int)converter.GetChartX(this.HistoricalData.Time(i)); + int barY1 = (int)converter.GetChartY(this.HistoricalData.Open(i)); + int barYHigh = (int)converter.GetChartY(this.HistoricalData.High(i)); + int barYLow = (int)converter.GetChartY(this.HistoricalData.Low(i)); + int barX2 = barX1 + width; + int barY2 = (int)converter.GetChartY(this.HistoricalData.Close(i)); + using (Brush transparentBrush = new SolidBrush(Color.FromArgb(250, 70, 70, 70))) + gr.FillRectangle(transparentBrush, barX1, barYHigh - 1, CurrentChart.BarsWidth, Math.Abs(barYLow - barYHigh) + 2); + using (Pen defaultPen = new(Color.Yellow, 3)) + { + defaultPen.StartCap = LineCap.Round; + defaultPen.EndCap = LineCap.Round; + gr.DrawLine(defaultPen, barX1, barY1, barX2, barY2); + } + if (i > 0) + { + int barX0 = (int)converter.GetChartX(this.HistoricalData.Time(i - 1)); + int barY0 = (int)converter.GetChartY(this.HistoricalData.Open(i - 1)); + using (Pen dottedPen = new(Color.Yellow, 1)) + { + dottedPen.DashStyle = DashStyle.Dot; + gr.DrawLine(dottedPen, barX2, barY2, barX0, barY0); + } + + } + + } + } + +} diff --git a/quantower/Volatility/JbandsIndicator.cs b/quantower/Volatility/JbandsIndicator.cs index 608f93c..a77312c 100644 --- a/quantower/Volatility/JbandsIndicator.cs +++ b/quantower/Volatility/JbandsIndicator.cs @@ -25,7 +25,8 @@ public class JbandsIndicator : Indicator, IWatchlistIndicator [InputParameter("vShort", sortIndex: 6, -100, 100, 1, 0)] public int Phase { get; set; } = 10; - private Jma? jma; + private Jma? jmaUp; + private Jma? jmaLo; protected LineSeries? UbSeries; protected LineSeries? LbSeries; protected string? SourceName; @@ -46,18 +47,20 @@ public JbandsIndicator() protected override void OnInit() { - jma = new(Periods, phase: Phase); + jmaUp = new(Periods, phase: Phase); + jmaLo = new(Periods, phase: Phase); SourceName = Source.ToString(); base.OnInit(); } protected override void OnUpdate(UpdateArgs args) { - TValue input = this.GetInputValue(args, Source); - jma!.Calc(input); + TBar input = IndicatorExtensions.GetInputBar(this, args); + jmaUp!.Calc(input.High); + jmaLo!.Calc(input.Low); - UbSeries!.SetValue(jma.UpperBand); - LbSeries!.SetValue(jma.LowerBand); + UbSeries!.SetValue(jmaUp.UpperBand); + LbSeries!.SetValue(jmaLo.LowerBand); } public override string ShortName => $"JBands ({Periods}:{Phase})";