From e9b58e5d48302d67d595ecb92e9b5456399b9316 Mon Sep 17 00:00:00 2001 From: Miha Kralj Date: Mon, 23 Sep 2024 08:34:47 -0700 Subject: [PATCH] dev branch --- SyntheticVendor/SyntheticVendor.cs | 1133 ++++++++++++++++++++++++++++ docs/ALMA.md | 2 +- lib/averages/Alma.cs | 121 +++ lib/averages/Dema.cs | 105 +++ lib/averages/Dsma.cs | 144 ++++ lib/averages/Ema.cs | 130 ++++ lib/averages/Frama.cs | 99 +++ lib/averages/Htit.cs | 146 ++++ lib/averages/Hwma.cs | 96 +++ lib/averages/Jma.cs | 130 ++++ lib/averages/Kama.cs | 85 +++ lib/averages/Ltma.cs | 74 ++ lib/averages/Maaf.cs | 110 +++ lib/averages/Mama.cs | 160 ++++ lib/averages/Mgdi.cs | 66 ++ lib/averages/Mma.cs | 78 ++ lib/averages/Qema.cs | 74 ++ lib/averages/Rema.cs | 78 ++ lib/averages/Rma.cs | 66 ++ lib/averages/Sma.cs | 59 ++ lib/averages/Smma.cs | 78 ++ lib/averages/T3.cs | 133 ++++ lib/averages/Tema.cs | 81 ++ lib/averages/Vidya.cs | 92 +++ lib/averages/Zlema.cs | 73 ++ 25 files changed, 3412 insertions(+), 1 deletion(-) create mode 100644 SyntheticVendor/SyntheticVendor.cs create mode 100644 lib/averages/Alma.cs create mode 100644 lib/averages/Dema.cs create mode 100644 lib/averages/Dsma.cs create mode 100644 lib/averages/Ema.cs create mode 100644 lib/averages/Frama.cs create mode 100644 lib/averages/Htit.cs create mode 100644 lib/averages/Hwma.cs create mode 100644 lib/averages/Jma.cs create mode 100644 lib/averages/Kama.cs create mode 100644 lib/averages/Ltma.cs create mode 100644 lib/averages/Maaf.cs create mode 100644 lib/averages/Mama.cs create mode 100644 lib/averages/Mgdi.cs create mode 100644 lib/averages/Mma.cs create mode 100644 lib/averages/Qema.cs create mode 100644 lib/averages/Rema.cs create mode 100644 lib/averages/Rma.cs create mode 100644 lib/averages/Sma.cs create mode 100644 lib/averages/Smma.cs create mode 100644 lib/averages/T3.cs create mode 100644 lib/averages/Tema.cs create mode 100644 lib/averages/Vidya.cs create mode 100644 lib/averages/Zlema.cs diff --git a/SyntheticVendor/SyntheticVendor.cs b/SyntheticVendor/SyntheticVendor.cs new file mode 100644 index 00000000..1e2d143e --- /dev/null +++ b/SyntheticVendor/SyntheticVendor.cs @@ -0,0 +1,1133 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using TradingPlatform.BusinessLayer; +using TradingPlatform.BusinessLayer.Integration; + +namespace SyntheticVendorNamespace +{ + public class SyntheticVendor : Vendor + { + private readonly List exchanges; + private readonly List assets; + private readonly List symbols; + + public SyntheticVendor() + { + exchanges = new List + { + + //Spike, + //Impulse, + //Triangle, + //Sawtooth + //Sine + //Chirp + //White + //Gauss + //B + //HF + //Impulse+HF, + //Sawtooth+HF + //Sine+G + //Chirp+G + //Complex + //Market + + new MessageExchange { Id = "PU", ExchangeName = "1 Pulse" }, + new MessageExchange { Id = "WA", ExchangeName = "2 Wave" }, + new MessageExchange { Id = "MD", ExchangeName = "3 Modulation" }, + new MessageExchange { Id = "NO", ExchangeName = "4 Noise" }, + new MessageExchange { Id = "BR", ExchangeName = "5 Brownian" }, + new MessageExchange { Id = "QT", ExchangeName = "6 QuanTAlib" } + }; + + assets = new List + { + new MessageAsset { Id = "USD", Name = "USD" }, + + }; + + symbols = new List + { + CreateMessageSymbol(id: "W1", name: "1 Digital spike", exchangeId: "QT", assetId: "USD", type: SymbolType.Crypto, + description: "Sudden sharp spike in the signal"), + CreateMessageSymbol("W2", "2 Dirac delta spike", "QT", "USD", SymbolType.Crypto), + CreateMessageSymbol("W8", "4 Sinc pulse", "QT", "USD", SymbolType.Crypto), + + CreateMessageSymbol("W3", "1 Square Wave", "QT", "USD", SymbolType.ETF), + CreateMessageSymbol("W4", "2 Sawtooth Wave", "QT", "USD", SymbolType.ETF), + CreateMessageSymbol("W5", "3 Inverse sawtooth Wave", "QT", "USD", SymbolType.ETF), + CreateMessageSymbol("W6", "4 Triangle Wave", "QT", "USD", SymbolType.ETF), + CreateMessageSymbol("W7", "5 Sine Wave", "QT", "USD", SymbolType.ETF), + + CreateMessageSymbol("W11", "1 Amplitude modulation", "QT", "USD", SymbolType.Forex), + CreateMessageSymbol("W10", "2 Frequency sweep", "QT", "USD", SymbolType.Forex), + CreateMessageSymbol("W12", "3 Frequency modulation", "QT", "USD", SymbolType.Forex), + + CreateMessageSymbol("W13", "1 White noise", "QT", "USD", SymbolType.Indexes), + CreateMessageSymbol("W14", "2 Pink noise", "QT", "USD", SymbolType.Indexes), + CreateMessageSymbol("W15", "3 Brown noise", "QT", "USD", SymbolType.Indexes), + + CreateMessageSymbol("W16", "1 Fractional Brownian motion", "QT", "USD", SymbolType.Synthetic), + CreateMessageSymbol("W17", "2 Geometric Brownian motion", "QT", "USD", SymbolType.Synthetic) + }; + +/* + Bond, + CFD, + Crypto, + Debentures, + Equities, + ETF, + FixedIncome, + Forex, + Forward, + Futures, + Indexes, + Options, + Spot, + Synthetic, + Swap, + Warrants, + +*/ + + + + } + + private MessageSymbol CreateMessageSymbol( + string id, + string name, + string exchangeId, + string assetId, + SymbolType type, + string description) + { + var messageSymbol = new MessageSymbol(id) + { + Name = name, + Description = description, + SymbolType = type, + ExchangeId = exchangeId, + ProductAssetId = assetId, + + // Setting some default values + QuotingCurrencyAssetID = "USD", + HistoryType = HistoryType.Last, + DeltaCalculationType = DeltaCalculationType.TickDirection, + LotSize = 1, + VariableTickList = new List + { + new VariableTick(0.01) // Default tick size + } + }; + + return messageSymbol; + } + + public static VendorMetaData GetVendorMetaData() + { + return new VendorMetaData() + { + VendorName = "Synthetic Vendor", + VendorDescription = "A synthetic vendor for testing and demonstration purposes", + GetDefaultConnections = () => + { + var defaultConnection = Vendor.CreateDefaultConnectionInfo( + "Synthetic Connection", + "Synthetic Vendor", + "", // Replace with actual path if you have a logo + allowCreateCustomConnections: true + ); + return new List { defaultConnection }; + } + }; + } + + + private MessageSymbol CreateMessageSymbol(string id, string name, string exchangeId, string assetId, SymbolType type) + { + return new MessageSymbol(id) + { + Name = name, + ExchangeId = exchangeId, + ProductAssetId = assetId, + QuotingCurrencyAssetID = "USD", + QuotingType = SymbolQuotingType.LotSize, + LotSize = 1, + NettingType = NettingType.OnePosition, + VolumeType = SymbolVolumeType.Volume, + AllowCalculateRealtimeTicks = true, + AllowCalculateRealtimeTrades = false, + AllowCalculateRealtimeVolume = true, + AllowCalculateRealtimeChange = true, + AllowAbbreviatePriceByTickSize = false, + NotionalValueStep = 0.01, + DeltaCalculationType = DeltaCalculationType.AggressorFlag, // Changed from None to AggressorFlag + MinVolumeAnalysisTickSize = 0.01, + MaturityDate = DateTime.MaxValue, // Set to max value for non-expiring symbols + HistoryType = HistoryType.Last, + MinLot = 0.01, + LotStep = 0.01, + MaxLot = 1000000, + SymbolType = type +/* + SymbolType.Unknown, + [EnumMember] Forex, + [EnumMember] Equities, + [EnumMember] CFD, + [EnumMember] Indexes, + [EnumMember] Futures, + [EnumMember] Options, + [EnumMember] ETF, + [EnumMember] Crypto, + [EnumMember] Synthetic, + [EnumMember] Spot, + [EnumMember] Forward, + [EnumMember] FixedIncome, + [EnumMember] Warrants, + + [EnumMember] Debentures, + [EnumMember] Bond, + [EnumMember] Swap, +*/ + }; + } + + public override ConnectionResult Connect(ConnectRequestParameters connectRequestParameters) + { + // Simulating connection process + Thread.Sleep(100); // Simulate some connection delay + + return ConnectionResult.CreateSuccess("Successfully connected to Synthetic Vendor"); + } + + public override void Disconnect() + { + // Simulating disconnection process + Thread.Sleep(500); // Simulate some disconnection delay + + } + + public override PingResult Ping() + { + return new PingResult() + { + State = PingEnum.Connected, + PingTime = TimeSpan.FromMilliseconds(2), + RoundTripTime = TimeSpan.FromMilliseconds(2) + }; + } + + + + + public override void OnConnected(CancellationToken token) + { + // This method is called after a successful connection + // You can initialize resources or start any necessary processes here + base.OnConnected(token); + + // For example, you might want to push some initial messages or data + // PushMessage(new MessageVendorEvent("SyntheticVendor connected successfully")); + } + + + + public override IList GetExchanges(CancellationToken token) + { + return exchanges; + } + + public override IList GetAssets(CancellationToken token) + { + return assets; + } + + public override IList GetSymbols(CancellationToken token) + { + return symbols; + } + + public override void SubscribeSymbol(SubscribeQuotesParameters parameters) + { + // Empty method for data subscription to be filled later + } + + public override void UnSubscribeSymbol(SubscribeQuotesParameters parameters) + { + // Empty method for data unsubscription to be filled later + } + + + public override IList LoadHistory(HistoryRequestParameters requestParameters) + { + var historyItems = new List(); + var symbolId = requestParameters.SymbolId; + + if (string.IsNullOrEmpty(symbolId)) return historyItems; + + DateTime from = requestParameters.FromTime; + DateTime to = requestParameters.ToTime; + + TimeSpan periodTimeSpan = requestParameters.Aggregation.GetPeriod.Duration; + + // Define the maximum number of items to generate per request + const int MAX_ITEMS_PER_REQUEST = 10000; + + Func waveGenerator = GetWaveGenerator(symbolId); + + DateTime currentTime = from; + while (currentTime < to) + { + DateTime intervalEnd = currentTime.AddTicks(periodTimeSpan.Ticks * MAX_ITEMS_PER_REQUEST); + if (intervalEnd > to) + intervalEnd = to; + + while (currentTime <= intervalEnd) + { + var historyItem = waveGenerator(currentTime, periodTimeSpan); //calling generator fuction + historyItems.Add(historyItem); + + currentTime = currentTime.Add(periodTimeSpan); + + if (requestParameters.CancellationToken.IsCancellationRequested) return historyItems; + } + + currentTime = intervalEnd; + } + + return historyItems; + } + + private Func GetWaveGenerator(string symbolId) + { + switch (symbolId) + { + //case "W0": return GenerateConstant; + case "W1": return GenerateSpike; + case "W2": return GenerateDiracDelta; + case "W3": return GenerateSquareWave; + case "W4": return GenerateSawtoothWave; + case "W5": return GenerateInverseSawtoothWave; + case "W6": return GenerateTriangleWave; + case "W7": return GenerateSineWave; + case "W8": return GenerateSincWave; + case "W9": return GenerateGaussianPulse; + case "W10": return GenerateFrequencySweep; + case "W11": return GenerateAMSignal; + case "W12": return GenerateFMSignal; + case "W13": return GenerateWhiteNoise; + case "W14": return GeneratePinkNoise; + case "W15": return GenerateBrownNoise; + case "W16": return GenerateFBM; + case "W17": return GenerateGBM; + + default: return GenerateSineWave; + } + } + + public override HistoryMetadata GetHistoryMetadata(CancellationToken cancellationToken) + { + return new HistoryMetadata() + { + AllowedHistoryTypes = new HistoryType[] + { + HistoryType.Bid, + HistoryType.Ask, + HistoryType.Midpoint, + HistoryType.Last, + HistoryType.BidAsk, + HistoryType.Mark, + }, + AllowedPeriods = new Period[] + { + Period.TICK1, + Period.SECOND1, Period.SECOND5, Period.SECOND10, Period.SECOND15, Period.SECOND30, + Period.MIN1, Period.MIN2, Period.MIN3, Period.MIN4, Period.MIN5, + Period.MIN10, Period.MIN15, Period.MIN30, + Period.HOUR1, Period.HOUR2, Period.HOUR3, Period.HOUR4, + Period.HOUR6, Period.HOUR8, Period.HOUR12, + Period.DAY1, + Period.WEEK1, + Period.MONTH1, + Period.YEAR1 + }, + UseHistoryLocalCache = false + }; + } + + +/*******************************************************************************************************************************************/ +/*******************************************************************************************************************************************/ +/*******************************************************************************************************************************************/ +/*******************************************************************************************************************************************/ +/*******************************************************************************************************************************************/ +/*******************************************************************************************************************************************/ +/*******************************************************************************************************************************************/ + + private HistoryItemBar GenerateSpike(DateTime time, TimeSpan slice) + { + // Ensure we're working with UTC time + DateTime utcTime = time.ToUniversalTime(); + + // Calculate the number of hours since the epoch + double hoursSinceEpoch = (utcTime - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalHours; + + // Calculate the position within the 25-hour cycle + int cyclePosition = (int)Math.Floor(hoursSinceEpoch % 25); + + // Determine if this is a spike hour (hour 24 in the cycle) or the hour after + bool isSpike = cyclePosition == 24; + bool isAfterSpike = cyclePosition == 0; + + double openValue, closeValue; + if (isSpike) + { + openValue = 0; + closeValue = 100; + } + else if (isAfterSpike) + { + openValue = 100; + closeValue = 0; + } + else + { + openValue = closeValue = 0.000001; + } + + return new HistoryItemBar + { + TicksLeft = time.Ticks, + TicksRight = time.Add(slice).Ticks - 1, + Open = openValue, + High = Math.Max(openValue, closeValue), + Low = Math.Min(openValue, closeValue), + Close = closeValue, + Volume = Math.Abs(closeValue - openValue), + Ticks = time.Add(slice).Ticks - time.Ticks + }; + } + + + + + private static readonly double[] distributionValues = new double[] + { + 0.010, // Extreme left tail + 0.050, // Left tail + 0.200, // Left of center + 0.480, // Center (peak) + 0.200, // Right of center + 0.050, // Right tail + 0.010 // Extreme right tail + }; + + private HistoryItemBar GenerateDiracDelta(DateTime time, TimeSpan slice) + { + // Ensure we're working with UTC time + DateTime utcTime = time.ToUniversalTime(); + + // Calculate the start of the current day + DateTime dayStart = utcTime.Date; + + // Determine which bar of the day we're on + int barOfDay = (int)((utcTime - dayStart).Ticks / slice.Ticks); + + double openValue, closeValue; + double scaleFactor = 100; // Scale factor to convert to percentage + + // Generate the spike pattern for the first 4 bars of each day + switch (barOfDay) + { + case 0: + openValue = 0.000001 * scaleFactor; + closeValue = 0.05 * scaleFactor; + break; + case 1: + openValue = 0.05 * scaleFactor; + closeValue = 0.50 * scaleFactor; + break; + case 2: + openValue = 0.50 * scaleFactor; + closeValue = 0.05 * scaleFactor; + break; + case 3: + openValue = 0.05 * scaleFactor; + closeValue = 0.0000001 * scaleFactor; + break; + default: + // Outside of the spike period, use baseline value + openValue = closeValue = 0.000001; + break; + } + + return new HistoryItemBar + { + TicksLeft = time.Ticks, + TicksRight = time.Add(slice).Ticks - 1, + Open = openValue, + High = Math.Max(openValue, closeValue), + Low = Math.Min(openValue, closeValue), + Close = closeValue, + Volume = Math.Abs(closeValue - openValue), + Ticks = time.Add(slice).Ticks - time.Ticks + }; + } + + + + private HistoryItemBar GenerateSineWave(DateTime time, TimeSpan slice) + { + // Ensure we're working with UTC time + DateTime utcTime = time.ToUniversalTime(); + + // Calculate the number of hours since the epoch + double minutesSinceEpoch = (utcTime - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalMinutes; + + // Calculate the position within the 25-hour cycle + double cyclePosition = minutesSinceEpoch % 1500; + + + // Calculate the sine wave values + double frequency = 2 * Math.PI / 1500; // Complete cycle over 25 hours + double value = 50 + 50 * Math.Sin(cyclePosition * frequency); // Oscillate between 0 and 100 + double nextValue = 50 + 50 * Math.Sin((cyclePosition + slice.TotalMinutes) * frequency); + + double factor = 0.6 * Math.Abs (nextValue - value); + + return new HistoryItemBar + { + TicksLeft = time.Ticks, + TicksRight = time.Add(slice).Ticks - 1, + Open = value, + + High = Math.Max(value, nextValue)+factor, + Low = Math.Min(value, nextValue)-factor, + + Close = nextValue, + Volume = Math.Abs(nextValue - value) * 100, // Volume proportional to price change + Ticks = time.Add(slice).Ticks - time.Ticks + }; + } + + + private HistoryItemBar GenerateSquareWave(DateTime time, TimeSpan slice) + { + // Ensure we're working with UTC time + DateTime utcTime = time.ToUniversalTime(); + + // Calculate the time within the day (in hours) + double hoursInDay = utcTime.TimeOfDay.TotalHours; + + double openValue, closeValue; + + if (hoursInDay < 12) + { + // First half of the day + openValue = 99; + closeValue = 100; + } + else + { + // Second half of the day + openValue = 1; + closeValue = 0.0001; + } + + // Handle transition bars + if (Math.Abs(hoursInDay - 12) < slice.TotalHours / 2) + { + // Transition from 100 to 0 at noon + openValue = 100; + closeValue = 0.0001; + } + else if (hoursInDay < slice.TotalHours / 2 || hoursInDay > 24 - slice.TotalHours / 2) + { + // Transition from 0 to 100 at midnight + openValue = 0.0001; + closeValue = 100; + } + + return new HistoryItemBar + { + TicksLeft = time.Ticks, + TicksRight = time.Add(slice).Ticks - 1, + Open = openValue, + High = Math.Max(openValue, closeValue), + Low = Math.Min(openValue, closeValue), + Close = closeValue, + Volume = Math.Abs(closeValue - openValue), + Ticks = time.Add(slice).Ticks - time.Ticks + }; + } + + private HistoryItemBar GenerateSawtoothWave(DateTime time, TimeSpan slice) + { + double hours = (time - DateTime.UnixEpoch).TotalHours; + double period = 24; // 24-hour period + double position = hours % period; + double value = 200 * (position / period) - 100; + double nextValue = 200 * ((position + slice.TotalHours) % period / period) - 100; + + return new HistoryItemBar + { + TicksLeft = time.Ticks, + TicksRight = time.Add(slice).Ticks - 1, + Open = value, + High = Math.Max(value, nextValue), + Low = Math.Min(value, nextValue), + Close = nextValue, + Volume = 100, + Ticks = 100 + }; + } + + private HistoryItemBar GenerateInverseSawtoothWave(DateTime time, TimeSpan slice) + { + double hours = (time - DateTime.UnixEpoch).TotalHours; + double period = 24; // 24-hour period + double position = hours % period; + double value = 100 - (200 * (position / period)); + double nextValue = 100 - (200 * ((position + slice.TotalHours) % period / period)); + + return new HistoryItemBar + { + TicksLeft = time.Ticks, + TicksRight = time.Add(slice).Ticks - 1, + Open = value, + High = Math.Max(value, nextValue), + Low = Math.Min(value, nextValue), + Close = nextValue, + Volume = 100, + Ticks = 100 + }; + } + + private HistoryItemBar GeneratePulseWave(DateTime time, TimeSpan slice) + { + double hours = (time - DateTime.UnixEpoch).TotalHours; + double period = 24; // 24-hour period + double position = hours % period; + double value = position < period / 5 ? 100 : -100; // 20% duty cycle + + return new HistoryItemBar + { + TicksLeft = time.Ticks, + TicksRight = time.Add(slice).Ticks - 1, + Open = value, + High = 100, + Low = -100, + Close = value, + Volume = 100, + Ticks = 100 + }; + } + + private HistoryItemBar GenerateTriangleWave(DateTime time, TimeSpan slice) + { + double hours = (time - DateTime.UnixEpoch).TotalHours; + double period = 24; + double position = hours % period; + double value = 200 * (Math.Abs(position / period - 0.5) - 0.25) * 100; + double nextValue = 200 * (Math.Abs(((position + slice.TotalHours) % period) / period - 0.5) - 0.25) * 100; + + return new HistoryItemBar + { + TicksLeft = time.Ticks, + TicksRight = time.Add(slice).Ticks - 1, + Open = value, + High = Math.Max(value, nextValue), + Low = Math.Min(value, nextValue), + Close = nextValue, + Volume = 100, + Ticks = 100 + }; + } + + private HistoryItemBar GenerateSincWave(DateTime time, TimeSpan slice) + { + double minutes = (time - DateTime.UnixEpoch).TotalMinutes; + double period = 1500.0; // 24-hour period + double frequency = 2 * Math.PI / period; // Full cycle over 24 hours + + // Adjust time to center the main peak at 12 hours + double t = minutes % period - period / 2; + + // Scale factor + double scaleFactor = 7.0; + + // Calculate Sinc value + double x = scaleFactor * frequency * t; + double sincValue = x != 0 ? 100 * Math.Sin(x) / x : 100; + + // Calculate next value + double nextT = ((minutes + slice.TotalMinutes) % period) - period / 2; + double nextX = scaleFactor * frequency * nextT; + double nextSincValue = nextX != 0 ? 100 * Math.Sin(nextX) / nextX : 100; + + // Ensure minimum value + double minValue = 0.00001; + sincValue = Math.Sign(sincValue) * Math.Max(Math.Abs(sincValue), minValue); + nextSincValue = Math.Sign(nextSincValue) * Math.Max(Math.Abs(nextSincValue), minValue); + + return new HistoryItemBar + { + TicksLeft = time.Ticks, + TicksRight = time.Add(slice).Ticks - 1, + Open = sincValue, + High = Math.Max(sincValue, nextSincValue), + Low = Math.Min(sincValue, nextSincValue), + Close = nextSincValue, + Volume = Math.Abs(nextSincValue - sincValue), // Volume as the change in value + Ticks = slice.Ticks + }; + } + + private HistoryItemBar GenerateGaussianPulse(DateTime time, TimeSpan slice) + { + double hours = (time - DateTime.UnixEpoch).TotalHours; + double totalPeriod = 24.0; // 24-hour total cycle + double pulsePeriod = 12.0; // 12-hour pulse duration + double position = hours % totalPeriod; + + // Parameters for the Gaussian pulse + double amplitude = 100.0; // Maximum amplitude + double center = pulsePeriod / 2.0; // Center of the pulse (at 6 hours within the pulse period) + double width = pulsePeriod / 6.0; // Width of the pulse (adjusts the spread) + + double baselineValue = 0.00001; // Value outside the pulse period + + // Calculate the Gaussian pulse value + double value; + if (position < pulsePeriod) + { + value = amplitude * Math.Exp(-Math.Pow(position - center, 2) / (2 * Math.Pow(width, 2))) + baselineValue; + } + else + { + value = baselineValue; + } + + // Calculate the next value for the slice + double nextPosition = (hours + slice.TotalHours) % totalPeriod; + double nextValue; + if (nextPosition < pulsePeriod) + { + nextValue = amplitude * Math.Exp(-Math.Pow(nextPosition - center, 2) / (2 * Math.Pow(width, 2))) + baselineValue; + } + else + { + nextValue = baselineValue; + } + + return new HistoryItemBar + { + TicksLeft = time.Ticks, + TicksRight = time.Add(slice).Ticks - 1, + Open = value, + High = Math.Max(value, nextValue), + Low = Math.Min(value, nextValue), + Close = nextValue, + Volume = Math.Abs(nextValue - value), // Volume as the change in value + Ticks = slice.Ticks + }; + } + + private HistoryItemBar GenerateFrequencySweep(DateTime time, TimeSpan slice) + { + double hours = (time - DateTime.UnixEpoch).TotalHours; + double sweepPeriod = 48.0; // 48-hour period + + // Starting frequency (very low) + double minFreq = Math.PI / 48.0; + + // Calculate the ending frequency to ensure continuity + double maxFreq = Math.PI * 1.0 * Math.Exp(2 * Math.PI / sweepPeriod); + + // Calculate the exponential factor for frequency sweep + double expFactor = Math.Log(maxFreq / minFreq) / sweepPeriod; + + // Calculate the overall phase up to the current time + double totalPhase = (minFreq / expFactor) * (Math.Exp(expFactor * (hours % sweepPeriod)) - 1); + + // Shift the phase to start the cycle at 100 (cosine-like behavior) + totalPhase += Math.PI / 2; + + // Calculate the value of the signal at the current time + double value = 100.0 * Math.Sin(totalPhase); + + // Calculate the value of the signal at the end of the slice + double nextPhase = (minFreq / expFactor) * (Math.Exp(expFactor * ((hours + slice.TotalHours) % sweepPeriod)) - 1); + nextPhase += Math.PI / 2; // Apply the same phase shift + double nextValue = 100.0 * Math.Sin(nextPhase); + + return new HistoryItemBar + { + TicksLeft = time.Ticks, + TicksRight = time.Add(slice).Ticks - 1, + Open = value, + High = Math.Max(value, nextValue), + Low = Math.Min(value, nextValue), + Close = nextValue, + Volume = Math.Abs(nextValue - value), // Volume as the change in value + Ticks = slice.Ticks + }; + } + + + readonly Random random = new Random(); + private double currentAmplitude = 100; + private HistoryItemBar GenerateAMSignal(DateTime time, TimeSpan slice) + { + double hours = (time - DateTime.UnixEpoch).TotalHours; + double period = 12.0; + double frequency = 2 * Math.PI / period; // Frequency for a 5-hour period + + // Determine the start of the current 5-hour cycle + double cycleStartTime = Math.Floor(hours / period) * period; + + // Calculate the phase of the signal within the current 5-hour cycle + double phase = frequency * (hours % period); + + // If we're at the start of a new 5-hour cycle, generate a new amplitude + if (hours % period == 0) + { + currentAmplitude = random.NextDouble() * 100; + } + + // Calculate the value of the signal at the current time + double value = currentAmplitude * Math.Sin(phase); + + // Calculate the value of the signal at the end of the slice + double nextPhase = frequency * ((hours + slice.TotalHours) % period); + double nextValue = currentAmplitude * Math.Sin(nextPhase); + + // Create the HistoryItemBar + var historyItem = new HistoryItemBar + { + TicksLeft = time.Ticks, + TicksRight = time.Add(slice).Ticks - 1, + Open = value, + High = Math.Max(value, nextValue), + Low = Math.Min(value, nextValue), + Close = nextValue, // Set Close to the newly calculated value + Volume = Math.Abs(nextValue), // Volume as the change in value + Ticks = slice.Ticks + }; + + return historyItem; + } + + + private double currentFrequency = Math.PI / 220.0; // Initial frequency + private double accumulatedPhase = 0; + private double lastCloseValue = 0; // To store the last close value + + private HistoryItemBar GenerateFMSignal(DateTime time, TimeSpan slice) + { + double amplitude = 100.0; // Maximum amplitude + double minFreq = Math.PI / 256.0; + double maxFreq = Math.PI / 32.0; + + // Randomly adjust the frequency + double frequencyStep = (maxFreq - minFreq) * 0.2; // 20% of the frequency range + currentFrequency += (random.NextDouble() - 0.5) * 2 * frequencyStep; + currentFrequency = Math.Max(minFreq, Math.Min(maxFreq, currentFrequency)); // Clamp frequency + + // Calculate phase increment for this slice + double phaseIncrement = currentFrequency * slice.TotalHours; + + // Calculate the open value (which is the last close value) + double openValue = lastCloseValue; + + // Calculate the close value + accumulatedPhase += phaseIncrement; + double closeValue = amplitude * Math.Sin(2 * Math.PI * accumulatedPhase); + + // Determine high and low values + double midPhase = accumulatedPhase - (phaseIncrement / 2); + double midValue = amplitude * Math.Sin(2 * Math.PI * midPhase); + double highValue = Math.Max(Math.Max(openValue, closeValue), midValue); + double lowValue = Math.Min(Math.Min(openValue, closeValue), midValue); + + // Store the close value for the next iteration + lastCloseValue = closeValue; + + return new HistoryItemBar + { + TicksLeft = time.Ticks, + TicksRight = time.Add(slice).Ticks - 1, + Open = openValue, + High = highValue, + Low = lowValue, + Close = closeValue, + Volume = Math.Abs(closeValue - openValue), // Volume as the change in value + Ticks = slice.Ticks + }; + } + + + private HistoryItemBar GenerateWhiteNoise(DateTime time, TimeSpan slice) + { + double volatility = 2; + double meanReversionStrength = 0.1; + + double openNoise = random.NextDouble(); + double open = previousClose + volatility * openNoise + meanReversionStrength * (meanPrice - previousClose); + double closeNoise = random.NextDouble(); + double close = open + volatility * closeNoise + meanReversionStrength * (meanPrice - open); + + // Determine High and Low + double high = Math.Max(open, close); + double low = Math.Min(open, close); + + // Add variation to High and Low + double highNoise = Math.Abs(random.NextDouble()); + high += volatility * highNoise; + + double lowNoise = Math.Abs(random.NextDouble()); + low -= volatility * lowNoise; + + double volume = Math.Abs(random.NextDouble()) * 1000 + 100; + + previousClose = close; + + + // Create the HistoryItemBar + var historyItem = new HistoryItemBar + { + TicksLeft = time.Ticks, + TicksRight = time.Add(slice).Ticks - 1, + Open = open, + High = high, + Low = low, + Close = close, + Volume = volume, + Ticks = slice.Ticks + }; + + return historyItem; + } + + + +private double previousClose = 50; +private const double meanPrice = 50; + +private HistoryItemBar GeneratePinkNoise(DateTime time, TimeSpan slice) +{ + double volatility = 2; + double meanReversionStrength = 0.1; + + // Generate open price + double openNoise = GeneratePinkNoiseValue(); + double open = previousClose + volatility * openNoise + meanReversionStrength * (meanPrice - previousClose); + + // Generate close price + double closeNoise = GeneratePinkNoiseValue(); + double close = open + volatility * closeNoise + meanReversionStrength * (meanPrice - open); + + // Determine High and Low + double high = Math.Max(open, close); + double low = Math.Min(open, close); + + // Add variation to High and Low + double highNoise = Math.Abs(GeneratePinkNoiseValue()); + high += volatility * highNoise; + + double lowNoise = Math.Abs(GeneratePinkNoiseValue()); + low -= volatility * lowNoise; + + double volume = Math.Abs(GeneratePinkNoiseValue()) * 1000 + 100; + + // Update previous close for the next iteration + previousClose = close; + + return new HistoryItemBar + { + TicksLeft = time.Ticks, + TicksRight = time.Add(slice).Ticks - 1, + Open = open, + High = high, + Low = low, + Close = close, + Volume = volume, + Ticks = slice.Ticks + }; +} + + + private const int NumOctaves = 6; + private double[] pinkNoiseState = new double[NumOctaves]; + private double GeneratePinkNoiseValue() + { + double total = 0; + + for (int i = 0; i < NumOctaves; i++) + { + double white = random.NextDouble() * 2 - 1; + pinkNoiseState[i] = (pinkNoiseState[i] + white) * 0.5; + total += pinkNoiseState[i] * Math.Pow(2, -i); + } + + // Normalize + return total / NumOctaves; + } + + + +private double lastValue = 0; + +private HistoryItemBar GenerateBrownNoise(DateTime time, TimeSpan slice) +{ + double dt = slice.TotalDays / 365.0; // Time step in years + double sigma = 25.0; // Annual volatility + + double increment = GenerateGaussian(0, sigma * Math.Sqrt(dt)); + double open = lastValue * (1 + GenerateGaussian(0, 0.05)); + double close = open + increment; + + // Simulate intra-period high and low + double high = Math.Max(open, close); + high += high * Math.Abs(GenerateGaussian(0, 0.06)); + double low = Math.Min(open, close); + low -= low * Math.Abs(GenerateGaussian(0, 0.06)); + + lastValue = close; + + return new HistoryItemBar + { + TicksLeft = time.Ticks, + TicksRight = time.Add(slice).Ticks - 1, + Open = open, + High = high, + Low = low, + Close = close, + Volume = Math.Abs(close - open) * 1000, // Simplified volume calculation + Ticks = slice.Ticks + }; +} +// Helper method to generate Gaussian distributed random numbers +private double GenerateGaussian(double mean, double stdDev) +{ + double u1 = 1.0 - random.NextDouble(); // Uniform(0,1] random doubles + double u2 = 1.0 - random.NextDouble(); + double randStdNormal = Math.Sqrt(-2.0 * Math.Log(u1)) * Math.Sin(2.0 * Math.PI * u2); + return mean + stdDev * randStdNormal; +} + + + +private double GBMLastClose = 100; // Starting price +private readonly double GBMMu = 0.05; // Annual drift +private readonly double GBMSigma = 0.2; // Annual volatility + +private HistoryItemBar GenerateGBM(DateTime time, TimeSpan slice) +{ + // Convert time slice to years + double dt = slice.TotalDays / 365.0; + + // Generate a random normal variable for the main price movement + double epsilon = GenerateGaussian(0, 1); + + // Calculate the price movement using GBM equation + double drift = (GBMMu - 0.5 * GBMSigma * GBMSigma) * dt; + double diffusion = GBMSigma * Math.Sqrt(dt) * epsilon; + double returnValue = Math.Exp(drift + diffusion); + + // Add variability between previous close and current open + double openVariability = GBMLastClose * GBMSigma * Math.Sqrt(dt) * GenerateGaussian(0, 1) * 0.1; + double open = GBMLastClose + openVariability; + + // Calculate new close price + double close = open * returnValue; + + // Generate High and Low values + double highLowRange = Math.Max(Math.Abs(close - open), GBMLastClose * GBMSigma * Math.Sqrt(dt) * Math.Abs(GenerateGaussian(0, 1))); + double high = Math.Max(open, close) + highLowRange * 0.5; + double low = Math.Min(open, close) - highLowRange * 0.5; + + // Generate volume (you may want to adjust this based on your needs) + double volume = Math.Max(100, 1000 * Math.Abs(close - open) + 500 * GenerateGaussian(0, 1)); + + // Update last close for next iteration + GBMLastClose = close; + + return new HistoryItemBar + { + TicksLeft = time.Ticks, + TicksRight = time.Add(slice).Ticks - 1, + Open = open, + High = high, + Low = low, + Close = close, + Volume = volume, + Ticks = slice.Ticks + }; +} + + private double FBMLastClose = 100; // Starting price + private double FBMHurst = 0.85; // Hurst parameter (0.5 < H < 1 for persistent fBm) + private readonly double FBMSigma = 0.25; // Volatility parameter + private readonly double FBMDrift = 0.001; // drift + + private HistoryItemBar GenerateFBM(DateTime time, TimeSpan slice) + { + double dt = Math.Pow(slice.TotalDays / 365.0, 0.5); + + double epsilon = GenerateFractionalGaussianNoise(FBMHurst); + + double drift = FBMDrift * dt; + double diffusion = FBMSigma * Math.Pow(dt, FBMHurst) * epsilon; + + double openVariability = FBMLastClose * FBMSigma * Math.Pow(dt, FBMHurst) * GenerateFractionalGaussianNoise(FBMHurst) * 0.1; + double open = FBMLastClose + openVariability; + + double close = open * Math.Exp(drift + diffusion); + + double highLowRange = Math.Max(Math.Abs(close - open), + FBMLastClose * FBMSigma * Math.Pow(dt, FBMHurst) * Math.Abs(GenerateFractionalGaussianNoise(FBMHurst)) * 2); + double high = Math.Max(open, close) + highLowRange * 0.5; + double low = Math.Min(open, close) - highLowRange * 0.5; + + double volume = Math.Max(100, 2000 * Math.Abs(close - open) + + 1000 * Math.Abs(GenerateFractionalGaussianNoise(FBMHurst))); + + FBMLastClose = close; + + return new HistoryItemBar + { + TicksLeft = time.Ticks, + TicksRight = time.Add(slice).Ticks - 1, + Open = open, + High = high, + Low = low, + Close = close, + Volume = volume, + Ticks = slice.Ticks + }; + } + + private double GenerateFractionalGaussianNoise(double hurst) + { + double sum = 0; + int n = 1000; // Number of terms in the approximation + + for (int i = 1; i <= n; i++) + { + double ri = GenerateGaussian(0, 1); + sum += (Math.Pow(i, hurst - 0.5) - Math.Pow(i - 1, hurst - 0.5)) * ri; + } + + return sum / Math.Sqrt(n); + } + + + + // Add other necessary overrides and implementations as needed + } +} \ No newline at end of file diff --git a/docs/ALMA.md b/docs/ALMA.md index 0aa250a0..ca86d632 100644 --- a/docs/ALMA.md +++ b/docs/ALMA.md @@ -2,4 +2,4 @@ period = 10 -![Alt text](./img/ALMA_chart.svg) \ No newline at end of file +![Alt text](./img/ALMA_chart.svg) diff --git a/lib/averages/Alma.cs b/lib/averages/Alma.cs new file mode 100644 index 00000000..f111922c --- /dev/null +++ b/lib/averages/Alma.cs @@ -0,0 +1,121 @@ + +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// ALMA: Arnaud Legoux Moving Average +/// Uses the curve of the Normal (Gauss) distribution. This moving average reduces lag +/// of the data in conjunction with smoothing to reduce noise. +/// +/// +/// Smoothness: ★★★★☆ (4/5) +/// Sensitivity: ★★★★☆ (4/5) +/// Overshooting: ★★★★☆ (4/5) +/// Lag: ★★★★★ (5/5) +/// +/// Validation: +/// Skender.Stock.Indicators +/// + +public class Alma : AbstractBase +{ + private readonly int _period; + private readonly double _offset; + private readonly double _sigma; + private CircularBuffer? _buffer; + private CircularBuffer? _weight; + private double _norm; + + /// The number of data points used in the ALMA calculation. + /// Controls the smoothness and high-frequency filtering. Default is 0.85. + /// Controls the shape of the Gaussian distribution. Default is 6. + /// Thrown when period is less than 1. + public Alma(int period, double offset = 0.85, double sigma = 6) + { + if (period < 1) + { + throw new ArgumentException("Period must be greater than or equal to 1.", nameof(period)); + } + _period = period; + _offset = offset; + _sigma = sigma; + WarmupPeriod = period; + Name = "Alma"; + Init(); + } + + /// The data source object that publishes updates. + /// The number of data points used in the ALMA calculation. + /// Controls the smoothness and high-frequency filtering. Default is 0.85. + /// Controls the shape of the Gaussian distribution. Default is 6. + public Alma(object source, int period, double offset = 0.85, double sigma = 6) : this(period, offset, sigma) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); + } + + public override void Init() + { + base.Init(); + _buffer = new CircularBuffer(_period); + _weight = new CircularBuffer(_period); + _norm = 0; + } + + protected override void ManageState(bool isNew) + { + if (isNew) + { + _index++; + } + } + + /// + /// Performs the core ALMA calculation. Called from parent abstractBase Calc() + /// + /// The calculated ALMA value. + protected override double Calculation() + { + ManageState(Input.IsNew); + + _buffer!.Add(Input.Value, Input.IsNew); + if (_weight!.Count < _buffer.Count) + { + for (var i = 0; i < _buffer.Count - _weight.Count; i++) + { + _weight.Add(0.0); + } + } + + if (_buffer.Count <= _period) + { + UpdateWeights(); + } + + double weightedSum = 0; + for (var i = 0; i < _buffer.Count; i++) + { + weightedSum += _weight[i] * _buffer[i]; + } + + double result = weightedSum / _norm; + + IsHot = _index >= WarmupPeriod; + return result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void UpdateWeights() + { + int len = _buffer!.Count; + _norm = 0; + double m = _offset * (len - 1); + double s = len / _sigma; + for (int i = 0; i < len; i++) + { + double wt = Math.Exp(-((i - m) * (i - m)) / (2 * s * s)); + _weight![i] = wt; + _norm += wt; + } + } +} \ No newline at end of file diff --git a/lib/averages/Dema.cs b/lib/averages/Dema.cs new file mode 100644 index 00000000..d3e37bfb --- /dev/null +++ b/lib/averages/Dema.cs @@ -0,0 +1,105 @@ +namespace QuanTAlib; + +/// +/// DEMA: Double Exponential Moving Average +/// DEMA reduces the lag of a traditional EMA by applying a second EMA over EMA. +/// It responds more quickly to price changes than a standard EMA while maintaining +/// smoothness, at the cost of overshooting the signal line. +/// +/// +/// Smoothness: ★★★☆☆ (3/5) +/// Sensitivity: ★★★★☆ (4/5) +/// Overshooting: ★★★☆☆ (3/5) +/// Lag: ★★★★☆ (4/5) +/// +/// Sources: +/// https://www.investopedia.com/terms/d/double-exponential-moving-average.asp +/// https://www.tradingview.com/support/solutions/43000502589-double-exponential-moving-average-dema/ +/// +/// Validation: +/// Skender.Stock.Indicators +/// +public class Dema : AbstractBase +{ + // inherited _index + // inherited _value + private readonly int _period; + private double _lastEma1, _p_lastEma1; + private double _lastEma2, _p_lastEma2; + private double _k, _e, _p_e; + + public Dema(int period) + { + if (period < 1) + { + throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 1."); + } + _period = period; + Name = "Dema"; + double percentile = 0.85; //targeting 85th percentile of correctness of converging EMA + WarmupPeriod = (int)Math.Ceiling(-period * Math.Log(1 - percentile)); + Init(); + } + + public Dema(object source, int period) : this(period) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); + } + //inhereted public void Sub(object source, in ValueEventArgs args) + + public override void Init() + { + base.Init(); + _k = 2.0 / (_period + 1); + _e = 1.0; + _lastEma1 = 0; + _lastEma2 = 0; + } + + protected override void ManageState(bool isNew) + { + if (isNew) + { + _p_lastEma1 = _lastEma1; + _p_lastEma2 = _lastEma2; + _p_e = _e; + _index++; + } + else + { + _lastEma1 = _p_lastEma1; + _lastEma2 = _p_lastEma2; + _e = _p_e; + } + } + + /// + /// Core DEMA calculation + /// + protected override double Calculation() + { + ManageState(Input.IsNew); + + double result, _ema1, _ema2; + // compensator for early ema values + _e = (_e > 1e-10) ? (1 - _k) * _e : 0; + double _invE = (_e > 1e-10) ? 1 / (1 - _e) : 1; + + // Calculate EMA1 + _ema1 = _k * (Input.Value - _lastEma1) + _lastEma1; + + // Calculate EMA2 using compensatedEma1 + _ema2 = _k * (_ema1 * _invE - _lastEma2) + _lastEma2; + + // Calculate DEMA + double _dema = 2 * _ema1 * _invE - (_ema2 * _invE); + + result = _dema; + _lastEma1 = _ema1; + _lastEma2 = _ema2; + + IsHot = _index >= WarmupPeriod; + return result; + } +} diff --git a/lib/averages/Dsma.cs b/lib/averages/Dsma.cs new file mode 100644 index 00000000..b6b74bad --- /dev/null +++ b/lib/averages/Dsma.cs @@ -0,0 +1,144 @@ +namespace QuanTAlib; + +/// +/// DSMA: Deviation Scaled Moving Average +/// Adaptive moving average that adjusts its smoothing factor based on the volatility of the input data. +/// It aims to be more responsive during trending periods and more stable during ranging periods. +/// +/// +/// Smoothness: ★★★★☆ (4/5) +/// Sensitivity: ★★★★☆ (4/5) +/// Overshooting: ★★★★☆ (4/5) +/// Lag: ★★★★☆ (4/5) +/// +/// The DSMA uses a SuperSmoother filter to reduce noise and a dynamic alpha calculation based on the +/// scaled deviation of the input data. This allows it to adapt to changing market conditions. +/// +/// The algorithm involves these main steps: +/// 1. Apply a SuperSmoother filter to the zero-mean input data. +/// 2. Calculate the Root Mean Square (RMS) of the filtered data. +/// 3. Scale the filtered data by the RMS to get a measure in terms of standard deviations. +/// 4. Use the scaled deviation to calculate an adaptive alpha for the moving average. +/// +/// Source: +/// https://www.mesasoftware.com/papers/DEVIATION%20SCALED%20MOVING%20AVERAGE.pdf +/// + +public class Dsma : AbstractBase +{ + private readonly int _period; + private readonly CircularBuffer _buffer; + private readonly double _a1, _b1, _c1, _c2, _c3; + private double _lastDsma, _p_lastDsma; + private double _filt, _filt1, _filt2, _zeros, _zeros1; + private double _p_filt, _p_filt1, _p_filt2, _p_zeros, _p_zeros1; + private bool _isInit, _p_isInit; + + /// + /// Initializes a new instance of the class. + /// + /// The number of data points used in the DSMA calculation. + /// Thrown when period is less than 1. + public Dsma(int period) + { + if (period < 1) + { + throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 1."); + } + _period = period; + _buffer = new CircularBuffer(period); + + // SuperSmoother filter coefficients + _a1 = Math.Exp(-1.414 * Math.PI / (0.5 * period)); + _b1 = 2 * _a1 * Math.Cos(1.414 * Math.PI / (0.5 * period)); + _c2 = _b1; + _c3 = -_a1 * _a1; + _c1 = 1 - _c2 - _c3; + + Name = "Dsma"; + WarmupPeriod = period * 2; // A conservative estimate + Init(); + } + + public Dsma(object source, int period) : this(period) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); + } + + public override void Init() + { + base.Init(); + _lastDsma = 0; + _filt = _filt1 = _filt2 = 0; + _zeros = _zeros1 = 0; + _isInit = false; + } + + protected override void ManageState(bool isNew) + { + if (isNew) + { + _p_lastDsma = _lastDsma; + _p_isInit = _isInit; + _p_zeros = _zeros; + _p_zeros1 = _zeros1; + _p_filt = _filt; + _p_filt1 = _filt1; + _p_filt2 = _filt2; + _index++; + } + else + { + _lastDsma = _p_lastDsma; + _isInit = _p_isInit; + _zeros = _p_zeros; + _zeros1 = _p_zeros1; + _filt = _p_filt; + _filt1 = _p_filt1; + _filt2 = _p_filt2; + } + } + + protected override double Calculation() + { + ManageState(Input.IsNew); + + if (!_isInit) + { + _lastDsma = Input.Value; + _isInit = true; + return _lastDsma; + } + + // Produce nominal zero mean + _zeros = Input.Value - _lastDsma; + + // SuperSmoother Filter + _filt = _c1 * (_zeros + _zeros1) / 2 + _c2 * _filt1 + _c3 * _filt2; + + // Update buffer for RMS calculation + _buffer.Add(_filt * _filt, Input.IsNew); + + // Compute RMS (Root Mean Square) + double rms = Math.Sqrt(_buffer.Sum() / _period); + + // Rescale Filt in terms of Standard Deviations + double scaledFilt = rms != 0 ? _filt / rms : 0; + + // Calculate adaptive alpha + double alpha = Math.Abs(scaledFilt) * 5 / _period; + + // DSMA calculation + double dsma = alpha * Input.Value + (1 - alpha) * _lastDsma; + + // Update state variables + _zeros1 = _zeros; + _filt2 = _filt1; + _filt1 = _filt; + _lastDsma = dsma; + + IsHot = _index >= WarmupPeriod; + return dsma; + } +} diff --git a/lib/averages/Ema.cs b/lib/averages/Ema.cs new file mode 100644 index 00000000..60c3dcd8 --- /dev/null +++ b/lib/averages/Ema.cs @@ -0,0 +1,130 @@ +namespace QuanTAlib; + +/// +/// EMA: Exponential Moving Average +/// EMA needs very short history buffer and calculates the EMA value using just the +/// previous EMA value. The weight of the new datapoint (alpha) is alpha = 2 / (period + 1) +/// +/// +/// Smoothness: ★★★☆☆ (3/5) +/// Sensitivity: ★★★★☆ (4/5) +/// Overshooting: ★★★★★ (5/5) +/// Lag: ★★★☆☆ (3/5) +/// +/// Key characteristics: +/// - Uses no buffer, relying only on the previous EMA value. +/// - The weight of new data points is calculated as alpha = 2 / (period + 1). +/// - Provides a balance between responsiveness and smoothing. No overshooting. Significant lag +/// +/// Calculation method: +/// This implementation can use SMA for the first Period bars as a seeding value for EMA when useSma is true. +/// +/// Sources: +/// - https://stockcharts.com/school/doku.php?id=chart_school:technical_indicators:moving_averages +/// - https://www.investopedia.com/ask/answers/122314/what-exponential-moving-average-ema-formula-and-how-ema-calculated.asp +/// - https://blog.fugue88.ws/archives/2017-01/The-correct-way-to-start-an-Exponential-Moving-Average-EMA +/// + +public class Ema : AbstractBase +{ + // inherited _index + // inherited _value + private readonly int _period; + private CircularBuffer _sma; + private double _lastEma, _p_lastEma; + private double _k, _e, _p_e; + private bool _isInit, _p_isInit, _useSma; + + public Ema(int period, bool useSma = true) + { + if (period < 1) + { + throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 1."); + } + _period = period; + _k = 2.0 / (_period + 1); + _useSma = useSma; + _sma = new(period); + Name = "Ema"; + WarmupPeriod = (int)Math.Ceiling(Math.Log(0.05) / Math.Log(1 - _k)); //95th percentile + Init(); + } + + public Ema(double alpha) : base() + { + _k = alpha; + _useSma = false; + _sma = new(1); + _period = 1; + WarmupPeriod = (int)Math.Ceiling(Math.Log(0.05) / Math.Log(1 - _k)); //95th percentile + Init(); + } + + public Ema(object source, int period, bool useSma = true) : this(period, useSma) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); + } + //inhereted public void Sub(object source, in ValueEventArgs args) + + public override void Init() + { + base.Init(); + _e = 1.0; + _lastEma = 0; + _isInit = false; + _p_isInit = false; + _sma = new(_period); + } + + protected override void ManageState(bool isNew) + { + if (isNew) + { + _p_lastEma = _lastEma; + _p_isInit = _isInit; + _p_e = _e; + _index++; + } + else + { + _lastEma = _p_lastEma; + _isInit = _p_isInit; + _e = _p_e; + } + } + + /// + /// Core EMA calculation + /// + protected override double Calculation() + { + double result, _ema; + ManageState(Input.IsNew); + + // when _UseSma == true, use SMA calculation until we have enough data points + if (!_isInit && _useSma) + { + _sma.Add(Input.Value, Input.IsNew); + _ema = _sma.Average(); + result = _ema; + if (_index >= _period) + { + _isInit = true; + } + } + else + { + // compensator for early ema values + _e = (_e > 1e-10) ? (1 - _k) * _e : 0; + + _ema = _k * (Input.Value - _lastEma) + _lastEma; + + // _useSma decides if we use compensator or not + result = (_useSma || _e == 0) ? _ema : _ema / (1 - _e); + } + _lastEma = _ema; + IsHot = _index >= WarmupPeriod; + return result; + } +} diff --git a/lib/averages/Frama.cs b/lib/averages/Frama.cs new file mode 100644 index 00000000..005e79d5 --- /dev/null +++ b/lib/averages/Frama.cs @@ -0,0 +1,99 @@ +using System; + +namespace QuanTAlib +{ + public class Frama : AbstractBase + { + private readonly int _period; + private readonly double _fc; + private CircularBuffer _buffer; + private double _lastFrama; + private double _prevLastFrama; + + public Frama(int period, double fc = 0.5) + { + if (period < 2) + throw new ArgumentException("Period must be at least 2", nameof(period)); + + _period = period; + _fc = fc; + _buffer = new CircularBuffer(period); + WarmupPeriod = period; + } + + public override void Init() + { + base.Init(); + _buffer.Clear(); + _lastFrama = 0; + _prevLastFrama = 0; + } + + protected override void ManageState(bool isNew) + { + if (isNew) + { + _prevLastFrama = _lastFrama; + _index++; + } + else + { + _lastFrama = _prevLastFrama; + } + } + + protected override double Calculation() + { + ManageState(Input.IsNew); + + _buffer.Add(Input.Value, Input.IsNew); + + if (_buffer.Count < _period) + { + _lastFrama = _buffer.Average(); + return _lastFrama; + } + + int half = _period / 2; + double hh = double.MinValue, ll = double.MaxValue; + double hh1 = double.MinValue, ll1 = double.MaxValue; + double hh2 = double.MinValue, ll2 = double.MaxValue; + + for (int i = 0; i < _period; i++) + { + double price = _buffer[i]; + hh = Math.Max(hh, price); + ll = Math.Min(ll, price); + + if (i < half) + { + hh1 = Math.Max(hh1, price); + ll1 = Math.Min(ll1, price); + } + else + { + hh2 = Math.Max(hh2, price); + ll2 = Math.Min(ll2, price); + } + } + + double n1 = (hh - ll) / _period; + double n2 = (hh1 - ll1 + hh2 - ll2) / (_period / 2); + + double d = (Math.Log(n2 + double.Epsilon) - Math.Log(n1 + double.Epsilon)) / Math.Log(2); + + double alpha = Math.Exp(-4.6 * (d - 1)); + alpha = Math.Max(Math.Min(alpha, 1), 0.01); // Ensure alpha is between 0.01 and 1 + + _lastFrama = alpha * (Input.Value - _lastFrama) + _lastFrama; + + IsHot = _index >= WarmupPeriod; + return _lastFrama; + } + + protected override double GetLastValid() + { + return _lastFrama; + } + } +} \ No newline at end of file diff --git a/lib/averages/Htit.cs b/lib/averages/Htit.cs new file mode 100644 index 00000000..a441f54e --- /dev/null +++ b/lib/averages/Htit.cs @@ -0,0 +1,146 @@ +//not working yet +//TODO consistency test + +using QuanTAlib; + +public class Htit : AbstractBase +{ + private readonly CircularBuffer _priceBuffer = new(7); + private readonly CircularBuffer _spBuffer = new(7); + private readonly CircularBuffer _dtBuffer = new(7); + private readonly CircularBuffer _i1Buffer = new(7); + private readonly CircularBuffer _q1Buffer = new(7); + private readonly CircularBuffer _i2Buffer = new(2); + private readonly CircularBuffer _q2Buffer = new(2); + private readonly CircularBuffer _reBuffer = new(2); + private readonly CircularBuffer _imBuffer = new(2); + private readonly CircularBuffer _pdBuffer = new(2); + private readonly CircularBuffer _sdBuffer = new(2); + private readonly CircularBuffer _itBuffer = new(4); + + private double _lastPd = 0; + private double _p_lastPd = 0; + + public Htit() + { + Name = "Htit"; + WarmupPeriod = 12; + } + + public Htit(object source) : this() + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); + } + + protected override void ManageState(bool isNew) + { + if (isNew) + { + _p_lastPd = _lastPd; + _index++; + } + else + { + _lastPd = _p_lastPd; + } + } + protected override double Calculation() + { + ManageState(Input.IsNew); + + double pr = Input.Value; + _priceBuffer.Add(pr, Input.IsNew); + + if (_index <= 5) + { + _spBuffer.Add(0, Input.IsNew); + _dtBuffer.Add(0, Input.IsNew); + _i1Buffer.Add(0, Input.IsNew); + _q1Buffer.Add(0, Input.IsNew); + _i2Buffer.Add(0, Input.IsNew); + _q2Buffer.Add(0, Input.IsNew); + _reBuffer.Add(0, Input.IsNew); + _imBuffer.Add(0, Input.IsNew); + _pdBuffer.Add(0, Input.IsNew); + _sdBuffer.Add(0, Input.IsNew); + _itBuffer.Add(pr, Input.IsNew); + return pr; + } + + double adj = (0.075 * _lastPd) + 0.54; + + // Smooth and detrender + double sp = ((4 * _priceBuffer[0]) + (3 * _priceBuffer[1]) + (2 * _priceBuffer[2]) + _priceBuffer[3]) / 10; + _spBuffer.Add(sp, Input.IsNew); + + double dt = ((0.0962 * _spBuffer[0]) + (0.5769 * _spBuffer[2]) - (0.5769 * _spBuffer[4]) - (0.0962 * _spBuffer[6])) * adj; + _dtBuffer.Add(dt, Input.IsNew); + + // In-phase and quadrature + double q1 = ((0.0962 * _dtBuffer[0]) + (0.5769 * _dtBuffer[2]) - (0.5769 * _dtBuffer[4]) - (0.0962 * _dtBuffer[6])) * adj; + _q1Buffer.Add(q1, Input.IsNew); + + double i1 = _dtBuffer[3]; + _i1Buffer.Add(i1, Input.IsNew); + + // Advance the phases by 90 degrees + double jI = ((0.0962 * _i1Buffer[0]) + (0.5769 * _i1Buffer[2]) - (0.5769 * _i1Buffer[4]) - (0.0962 * _i1Buffer[6])) * adj; + double jQ = ((0.0962 * _q1Buffer[0]) + (0.5769 * _q1Buffer[2]) - (0.5769 * _q1Buffer[4]) - (0.0962 * _q1Buffer[6])) * adj; + + // Phasor addition for 3-bar averaging + double i2 = i1 - jQ; + double q2 = q1 + jI; + + i2 = (0.2 * i2) + (0.8 * _i2Buffer[0]); + q2 = (0.2 * q2) + (0.8 * _q2Buffer[0]); + + _i2Buffer.Add(i2, Input.IsNew); + _q2Buffer.Add(q2, Input.IsNew); + + // Homodyne discriminator + double re = (i2 * _i2Buffer[1]) + (q2 * _q2Buffer[1]); + double im = (i2 * _q2Buffer[1]) - (q2 * _i2Buffer[1]); + + re = (0.2 * re) + (0.8 * _reBuffer[0]); + im = (0.2 * im) + (0.8 * _imBuffer[0]); + + _reBuffer.Add(re, Input.IsNew); + _imBuffer.Add(im, Input.IsNew); + + // Calculate period + double pd = (im != 0 && re != 0) ? 2 * Math.PI / Math.Atan(im / re) : 0; + + // Adjust period to thresholds + pd = (pd > 1.5 * _lastPd) ? 1.5 * _lastPd : pd; + pd = (pd < 0.67 * _lastPd) ? 0.67 * _lastPd : pd; + pd = (pd < 6) ? 6 : pd; + pd = (pd > 50) ? 50 : pd; + + // Smooth the period + pd = (0.2 * pd) + (0.8 * _lastPd); + _pdBuffer.Add(pd, Input.IsNew); + + double sd = (0.33 * pd) + (0.67 * _sdBuffer[0]); + _sdBuffer.Add(sd, Input.IsNew); + + // Smooth dominant cycle period + int dcPeriods = (int)(sd + 0.5); + double sumPr = _priceBuffer.GetSpan().Slice(0, Math.Min(dcPeriods, _priceBuffer.Count)).ToArray().Sum(); + double it = dcPeriods > 0 ? sumPr / dcPeriods : pr; + _itBuffer.Add(it, Input.IsNew); + + _p_lastPd = _lastPd; + _lastPd = pd; + + // Final indicator + if (_index >= 11) // 12th bar + { + return ((4 * _itBuffer[0]) + (3 * _itBuffer[1]) + (2 * _itBuffer[2]) + _itBuffer[3]) / 10; + } + else + { + return pr; + } + } +} \ No newline at end of file diff --git a/lib/averages/Hwma.cs b/lib/averages/Hwma.cs new file mode 100644 index 00000000..c2473349 --- /dev/null +++ b/lib/averages/Hwma.cs @@ -0,0 +1,96 @@ +namespace QuanTAlib; + +public class Hwma : AbstractBase +{ + private readonly int _period; + private readonly double _nA, _nB, _nC; + private double _pF, _pV, _pA; + private double _ppF, _ppV, _ppA; + + public Hwma(int period) : this(period, 2.0 / (1 + period), 1.0 / period, 1.0 / period) + { + } + + public Hwma(double nA, double nB, double nC) : this((int)((2 - nA) / nA), nA, nB, nC) + { + } + + public Hwma(int period, double nA, double nB, double nC) + { + if (period < 1) + { + throw new ArgumentException("Period must be greater than or equal to 1.", nameof(period)); + } + _period = period; + _nA = nA; + _nB = nB; + _nC = nC; + WarmupPeriod = period; + Name = $"Hwma({_period})"; + Init(); + } + + public Hwma(object source, int period) : this(period) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); + } + + public override void Init() + { + base.Init(); + _pF = _pV = _pA = 0; + _ppF = _ppV = _ppA = 0; + } + + protected override void ManageState(bool isNew) + { + if (isNew) + { + _lastValidValue = Input.Value; + _index++; + _ppF = _pF; + _ppV = _pV; + _ppA = _pA; + } + else + { + _pF = _ppF; + _pV = _ppV; + _pA = _ppA; + + } + } + + protected override double Calculation() + { + ManageState(Input.IsNew); + + if (_index == 1) + { + _pF = Input.Value; + _pA = _pV = 0; + } + + double nA = _nA, nB = _nB, nC = _nC; + if (_period == 1) + { + nA = 1; + nB = 0; + nC = 0; + } + + double F = (1 - nA) * (_pF + _pV + 0.5 * _pA) + nA * Input.Value; + double V = (1 - nB) * (_pV + _pA) + nB * (F - _pF); + double A = (1 - nC) * _pA + nC * (V - _pV); + + double hwma = F + V + 0.5 * A; + + _pF = F; + _pV = V; + _pA = A; + + IsHot = _index >= WarmupPeriod; + return hwma; + } +} \ No newline at end of file diff --git a/lib/averages/Jma.cs b/lib/averages/Jma.cs new file mode 100644 index 00000000..4184b109 --- /dev/null +++ b/lib/averages/Jma.cs @@ -0,0 +1,130 @@ +using QuanTAlib; +//TODO consistency test +public class Jma : AbstractBase +{ + public readonly int Period; + private readonly double _phase; + private readonly int _vshort, _vlong; + private CircularBuffer _values; + private CircularBuffer _voltyShort; + private CircularBuffer _vsumBuff; + private CircularBuffer _avoltyBuff; + + private double _beta, _len1, _pow1; + private double _upperBand, _lowerBand, _prevMa1, _prevDet0, _prevDet1, _prevJma; + private double _p_UpperBand, _p_LowerBand, _p_prevMa1, _p_prevDet0, _p_prevDet1, _p_prevJma; + + public Jma(int period, double phase = 0, int vshort = 10) + { + if (period < 1) + { + throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 1."); + } + Period = period; + _vshort = vshort; + _vlong = 65; + _phase = Math.Clamp((phase * 0.01) + 1.5, 0.5, 2.5); + + _values = new CircularBuffer(period); + _voltyShort = new CircularBuffer(vshort); + _vsumBuff = new CircularBuffer(_vlong); + _avoltyBuff = new CircularBuffer(2); + + Name = "JMA"; + WarmupPeriod = period * 2; + Init(); + } + + public override void Init() + { + _upperBand = _lowerBand = _prevMa1 = _prevDet0 = _prevDet1 = _prevJma = 0.0; + _p_UpperBand = _p_LowerBand = _p_prevMa1 = _p_prevDet0 = _p_prevDet1 = _p_prevJma = 0.0; + _beta = 0.45 * (Period - 1) / (0.45 * (Period - 1) + 2); + _len1 = Math.Max((Math.Log(Math.Sqrt(Period - 1)) / Math.Log(2.0)) + 2.0, 0); + _pow1 = Math.Max(_len1 - 2.0, 0.5); + _avoltyBuff.Clear(); + _avoltyBuff.Add(0, true); + _avoltyBuff.Add(0, true); + base.Init(); + } + + protected override void ManageState(bool isNew) + { + if (isNew) + { + _lastValidValue = Input.Value; + _index++; + // Save current state + _p_UpperBand = _upperBand; + _p_LowerBand = _lowerBand; + _p_prevMa1 = _prevMa1; + _p_prevDet0 = _prevDet0; + _p_prevDet1 = _prevDet1; + _p_prevJma = _prevJma; + } + else + { + // Restore previous state + _upperBand = _p_UpperBand; + _lowerBand = _p_LowerBand; + _prevMa1 = _p_prevMa1; + _prevDet0 = _p_prevDet0; + _prevDet1 = _p_prevDet1; + _prevJma = _p_prevJma; + + } + } + protected override double Calculation() + { + ManageState(Input.IsNew); + + _values.Add(Input.Value, Input.IsNew); + + if (_index == 1) + { + _prevMa1 = _prevJma = Input.Value; + return Input.Value; + } + + double hprice = _values.Max(); + double lprice = _values.Min(); + + double del1 = hprice - _upperBand; + double del2 = lprice - _lowerBand; + double volty = Math.Max(Math.Abs(del1), Math.Abs(del2)); + + _voltyShort.Add(volty, Input.IsNew); + double vsum = _vsumBuff.Newest() + 0.1 * (volty - _voltyShort.Oldest()); + _vsumBuff.Add(vsum, Input.IsNew); + + double prevAvolty = _avoltyBuff.Newest(); + double avolty = prevAvolty + 2.0 / (Math.Max(4.0 * Period, 30) + 1.0) * (vsum - prevAvolty); + _avoltyBuff.Add(avolty, Input.IsNew); + + double dVolty = (avolty > 0) ? volty / avolty : 0; + dVolty = Math.Min(Math.Max(dVolty, 1.0), Math.Pow(_len1, 1.0 / _pow1)); + + double pow2 = Math.Pow(dVolty, _pow1); + double len2 = Math.Sqrt(0.5 * (Period - 1)) * _len1; + double _Kv = Math.Pow(len2 / (len2 + 1), Math.Sqrt(pow2)); + + _upperBand = (del1 > 0) ? hprice : hprice - (_Kv * del1); + _lowerBand = (del2 < 0) ? lprice : lprice - (_Kv * del2); + + double alpha = Math.Pow(_beta, pow2); + double ma1 = (1 - alpha) * Input.Value + alpha * _prevMa1; + _prevMa1 = ma1; + + double det0 = (1 - _beta) * (Input.Value - ma1) + _beta * _prevDet0; + _prevDet0 = det0; + double ma2 = ma1 + (_phase + 1) * det0; + + double det1 = ((1 - alpha) * (1 - alpha) * (ma2 - _prevJma)) + (alpha * alpha * _prevDet1); + _prevDet1 = det1; + double jma = _prevJma + det1; + _prevJma = jma; + + IsHot = _index >= WarmupPeriod; + return jma; + } +} \ No newline at end of file diff --git a/lib/averages/Kama.cs b/lib/averages/Kama.cs new file mode 100644 index 00000000..5aa4d495 --- /dev/null +++ b/lib/averages/Kama.cs @@ -0,0 +1,85 @@ +using System; + +namespace QuanTAlib; + +public class Kama : AbstractBase +{ + private readonly int _period; + private readonly double _scFast, _scSlow; + private CircularBuffer? _buffer; + private double _lastKama, _p_lastKama; + + public Kama(int period, int fast = 2, int slow = 30) + { + if (period < 1) + { + throw new ArgumentException("Period must be greater than or equal to 1.", nameof(period)); + } + _period = period; + _scFast = 2.0 / (((period < fast) ? period : fast) + 1); + _scSlow = 2.0 / (slow + 1); + WarmupPeriod = period; + Name = $"Kama({_period}, {fast}, {slow})"; + Init(); + } + + public Kama(object source, int period, int fast = 2, int slow = 30) : this(period, fast, slow) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); + } + + public override void Init() + { + base.Init(); + _buffer = new CircularBuffer(_period + 1); + _lastKama = 0; + } + + protected override void ManageState(bool isNew) + { + if (isNew) + { + _lastValidValue = Input.Value; + _index++; + _p_lastKama = _lastKama; + } + else + { + _lastKama = _p_lastKama; + } + } + + protected override double Calculation() + { + ManageState(Input.IsNew); + + _buffer!.Add(Input.Value, Input.IsNew); + + double kama; + if (_index <= _period) + { + kama = Input.Value; + } + else + { + double change = Math.Abs(_buffer[^1] - _buffer[0]); + double volatility = 0; + for (int i = 1; i < _buffer.Count; i++) + { + volatility += Math.Abs(_buffer[i] - _buffer[i - 1]); + } + + double er = volatility != 0 ? change / volatility : 0; + double sc = (er * (_scFast - _scSlow)) + _scSlow; + sc *= sc; // Square the smoothing constant + + kama = _lastKama + (sc * (Input.Value - _lastKama)); + } + + _lastKama = kama; + IsHot = _index >= WarmupPeriod; + + return kama; + } +} \ No newline at end of file diff --git a/lib/averages/Ltma.cs b/lib/averages/Ltma.cs new file mode 100644 index 00000000..ade9ed0b --- /dev/null +++ b/lib/averages/Ltma.cs @@ -0,0 +1,74 @@ +namespace QuanTAlib; + +// https://www.mesasoftware.com/papers/TimeWarp.pdf + +public class Ltma : AbstractBase +{ + private readonly double _gamma; + private double _prevL0, _prevL1, _prevL2, _prevL3; + private double _p_prevL0, _p_prevL1, _p_prevL2, _p_prevL3; + + public double Gamma => _gamma; + + public Ltma(double gamma = 0.1) + { + if (gamma < 0 || gamma > 1) + throw new ArgumentOutOfRangeException(nameof(gamma), "Gamma must be between 0 and 1."); + _gamma = gamma; + Name = $"Laguerre({gamma:F2})"; + WarmupPeriod = 4; // Minimum number of samples needed + Init(); + } + + public Ltma(object source, double gamma = 0.1) : this(gamma) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); + } + + public override void Init() + { + base.Init(); + _prevL0 = _prevL1 = _prevL2 = _prevL3 = 0; + } + + protected override void ManageState(bool isNew) + { + if (isNew) + { + _p_prevL0 = _prevL0; + _p_prevL1 = _prevL1; + _p_prevL2 = _prevL2; + _p_prevL3 = _prevL3; + _index++; + } + else + { + _prevL0 = _p_prevL0; + _prevL1 = _p_prevL1; + _prevL2 = _p_prevL2; + _prevL3 = _p_prevL3; + } + } + + protected override double Calculation() + { + ManageState(Input.IsNew); + + // Laguerre filter calculation + double _l0 = (1 - _gamma) * Input.Value + _gamma * _prevL0; + double _l1 = -_gamma * _l0 + _prevL0 + _gamma * _prevL1; + double _l2 = -_gamma * _l1 + _prevL1 + _gamma * _prevL2; + double _l3 = -_gamma * _l2 + _prevL2 + _gamma * _prevL3; + _prevL0 = _l0; + _prevL1 = _l1; + _prevL2 = _l2; + _prevL3 = _l3; + + double filteredValue = (_l0 + 2 * _l1 + 2 * _l2 + _l3) / 6; + + IsHot = _index >= WarmupPeriod; + + return filteredValue; + } +} diff --git a/lib/averages/Maaf.cs b/lib/averages/Maaf.cs new file mode 100644 index 00000000..854efc72 --- /dev/null +++ b/lib/averages/Maaf.cs @@ -0,0 +1,110 @@ +//TODO: consistency test + +namespace QuanTAlib; + +// https://efs.kb.esignal.com/hc/en-us/articles/6362791434395-2005-Mar-The-Secret-Behind-The-Filter-MedianAdaptiveFilter-efs + +public class Maaf : AbstractBase +{ + private readonly CircularBuffer _priceBuffer; + private readonly CircularBuffer _smoothBuffer; + private double _prevFilter, _prevValue2, _threshold; + private double _p_prevFilter, _p_prevValue2; + + private readonly int _period; + + public Maaf(int Period = 39, double Threshold = 0.002) + { + _period = Period; + _threshold = Threshold; + _priceBuffer = new CircularBuffer(4); + _smoothBuffer = new CircularBuffer(Period); + Name = "MAAF"; + WarmupPeriod = Period; + Init(); + } + + public Maaf(object source, int Period = 39, double Threshold = 0.002) : this(Period, Threshold) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); + } + + public override void Init() + { + _priceBuffer.Clear(); + _smoothBuffer.Clear(); + _prevFilter = 0; + _prevValue2 = 0; + base.Init(); + } + + protected override void ManageState(bool isNew) + { + if (isNew) + { + _lastValidValue = Input.Value; + _index++; + _p_prevFilter = _prevFilter; + _p_prevValue2 = _prevValue2; + } + else + { + _prevFilter = _p_prevFilter; + _prevValue2 = _p_prevValue2; + } + } + + protected override double Calculation() + { + ManageState(IsNew); + + _priceBuffer.Add(Input.Value, Input.IsNew); + + if (_priceBuffer.Count < 4) + { + return Input.Value; + } + + double smooth = (_priceBuffer[^1] + (2 * _priceBuffer[^2]) + (2 * _priceBuffer[^3]) + _priceBuffer[^4]) / 6; + _smoothBuffer.Add(smooth, Input.IsNew); + + if (_smoothBuffer.Count < _period) + { + return smooth; + } + + int length = _period; + double value3 = 0.2; + double value2 = _prevValue2; + + while (value3 > _threshold && length > 0) + { + double alpha = 2.0 / (length + 1); + + var sortedValues = _smoothBuffer.TakeLast(length).OrderBy(x => x).ToList(); + double value1 = sortedValues[length / 2]; + value2 = alpha * (smooth - _prevValue2) + _prevValue2; + + if (value1 != 0) + { + value3 = Math.Abs(value1 - value2) / value1; + } + + length -= 2; + } + + if (length < 3) length = 3; + + double finalAlpha = 2.0 / (length + 1); + double filter = finalAlpha * (smooth - _prevFilter) + _prevFilter; + + _p_prevFilter = _prevFilter; + _prevFilter = filter; + _p_prevValue2 = _prevValue2; + _prevValue2 = value2; + + IsHot = _index >= WarmupPeriod; + return filter; + } +} diff --git a/lib/averages/Mama.cs b/lib/averages/Mama.cs new file mode 100644 index 00000000..36565686 --- /dev/null +++ b/lib/averages/Mama.cs @@ -0,0 +1,160 @@ +using QuanTAlib; +using System; + +public class Mama : AbstractBase +{ + private readonly double _fastLimit, _slowLimit; + private CircularBuffer _pr, _sm, _dt, _i1, _q1, _i2, _q2, _re, _im, _pd, _ph; + private double _mama, _fama; + private double _prevMama, _prevFama, _sumPr; + private double _p_prevMama, _p_prevFama, _p_sumPr; + + public TValue Fama { get; private set; } + + public Mama(double fastLimit = 0.5, double slowLimit = 0.05) + { + Fama = new TValue(); + Name = $"Mama({_fastLimit:F2}, {_slowLimit:F2})"; + _fastLimit = fastLimit; + _slowLimit = slowLimit; + _pr = new(7); + _sm = new(7); + _dt = new(7); + _q1 = new(7); + _i1 = new(7); + _i2 = new(2); + _q2 = new(2); + _re = new(2); + _im = new(2); + _pd = new(2); + _ph = new(2); + Init(); + } + + public Mama(object source, double fastLimit = 0.5, double slowLimit = 0.05) : this(fastLimit, slowLimit) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); + } + + public override void Init() + { + Fama = new TValue(); + base.Init(); + } + + protected override void ManageState(bool isNew) + { + if (isNew) + { + _p_prevMama = _prevMama; + _p_prevFama = _prevFama; + _p_sumPr = _sumPr; + _lastValidValue = Input.Value; + _index++; + } + else + { + _prevMama = _p_prevMama; + _prevFama = _p_prevFama; + _sumPr = _p_sumPr; + } + } + + protected override double Calculation() + { + ManageState(Input.IsNew); + + _pr.Add(Input.Value, Input.IsNew); + + if (_index > 6) + { + double adj = (0.075 * _pd[^1]) + 0.54; + + // Smooth + _sm.Add(((4 * _pr[^1]) + (3 * _pr[^2]) + (2 * _pr[^3]) + _pr[^4]) / 10, Input.IsNew); + + // Detrender + _dt.Add(((0.0962 * _sm[^1]) + (0.5769 * _sm[^3]) - (0.5769 * _sm[^5]) - (0.0962 * _sm[^7])) * adj, Input.IsNew); + + // In-phase and quadrature + _q1.Add(((0.0962 * _dt[^1]) + (0.5769 * _dt[^3]) - (0.5769 * _dt[^5]) - (0.0962 * _dt[^7])) * adj, Input.IsNew); + _i1.Add(_dt[^4], Input.IsNew); + + // Advance the phases by 90 degrees + double jI = ((0.0962 * _i1[^1]) + (0.5769 * _i1[^3]) - (0.5769 * _i1[^5]) - (0.0962 * _i1[^7])) * adj; + double jQ = ((0.0962 * _q1[^1]) + (0.5769 * _q1[^3]) - (0.5769 * _q1[^5]) - (0.0962 * _q1[^7])) * adj; + + // Phasor addition for 3-bar averaging + _i2.Add(_i1[^1] - jQ, Input.IsNew); + _q2.Add(_q1[^1] + jI, Input.IsNew); + _i2[^1] = 0.2 * _i2[^1] + 0.8 * _i2[^2]; + _q2[^1] = 0.2 * _q2[^1] + 0.8 * _q2[^2]; + + // Homodyne discriminator + _re.Add((_i2[^1] * _i2[^2]) + (_q2[^1] * _q2[^2]), Input.IsNew); + _im.Add((_i2[^1] * _q2[^2]) - (_q2[^1] * _i2[^2]), Input.IsNew); + _re[^1] = (0.2 * _re[^1]) + (0.8 * _re[^2]); + _im[^1] = (0.2 * _im[^1]) + (0.8 * _im[^2]); + + // Calculate period + if (_im[^1] != 0 && _re[^1] != 0) + { + _pd.Add(2 * Math.PI / Math.Atan(_im[^1] / _re[^1]), Input.IsNew); + } + else + { + _pd.Add(_pd[^2], Input.IsNew); + } + + // Adjust period to thresholds + _pd[^1] = Math.Max(Math.Min(_pd[^1], 1.5 * _pd[^2]), 0.67 * _pd[^2]); + _pd[^1] = Math.Max(Math.Min(_pd[^1], 50), 6); + _pd[^1] = (0.2 * _pd[^1]) + (0.8 * _pd[^2]); + + // Determine phase position + if (_i1[^1] != 0) + { + _ph.Add(Math.Atan(_q1[^1] / _i1[^1]) * 180 / Math.PI, Input.IsNew); + } + else + { + _ph.Add(_ph[^2], Input.IsNew); + } + + // Change in phase + double delta = Math.Max(_ph[^2] - _ph[^1], 1); + + // Adaptive alpha value + double alpha = Math.Max(_fastLimit / delta, _slowLimit); + + // Final indicators + _mama = alpha * (_pr[^1] - _prevMama) + _prevMama; + _fama = 0.5 * alpha * (_mama - _prevFama) + _prevFama; + + _prevMama = _mama; + _prevFama = _fama; + } + else + { + _pd.Add(0, Input.IsNew); + _sm.Add(0, Input.IsNew); + _dt.Add(0, Input.IsNew); + _i1.Add(0, Input.IsNew); + _q1.Add(0, Input.IsNew); + _i2.Add(0, Input.IsNew); + _q2.Add(0, Input.IsNew); + _re.Add(0, Input.IsNew); + _im.Add(0, Input.IsNew); + _ph.Add(0, Input.IsNew); + + _sumPr += Input.Value; + _mama = _fama = _prevMama = _prevFama = _sumPr / _index; + } + + Fama = new TValue(Time: Input.Time, Value: _fama, IsNew: Input.IsNew); + IsHot = _index >= 6; + + return _mama; + } +} diff --git a/lib/averages/Mgdi.cs b/lib/averages/Mgdi.cs new file mode 100644 index 00000000..f022a3ee --- /dev/null +++ b/lib/averages/Mgdi.cs @@ -0,0 +1,66 @@ +namespace QuanTAlib; + +public class Mgdi : AbstractBase +{ + private readonly int _period; + private readonly double _kFactor; + private double _prevMd, _p_prevMd; + public Mgdi(int period, double kFactor = 0.6) + { + if (period <= 0) + { + throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than 0."); + } + if (kFactor <= 0) + { + throw new ArgumentOutOfRangeException(nameof(kFactor), "K-Factor must be greater than 0."); + } + _period = period; + _kFactor = kFactor; + Name = "Mgdi"; + WarmupPeriod = period; + Init(); + } + + public Mgdi(object source, int period, double kFactor = 1.0) : this(period, kFactor) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); + } + + public override void Init() + { + base.Init(); + _prevMd = _p_prevMd = 0; + } + + protected override void ManageState(bool isNew) + { + if (isNew) + { + _p_prevMd = _prevMd; + _index++; + } else { + _prevMd = _p_prevMd; + } + } + + protected override double Calculation() + { + ManageState(Input.IsNew); + + double value = Input.Value; + if (_index < 2){ + _prevMd = value; + } + else + { + double md = _prevMd + ((value - _prevMd) / + (_kFactor * _period * Math.Pow(value / _prevMd, 4))); + _prevMd = md; + } + + IsHot = _index >= _period; + return _prevMd; + } +} \ No newline at end of file diff --git a/lib/averages/Mma.cs b/lib/averages/Mma.cs new file mode 100644 index 00000000..d2fde801 --- /dev/null +++ b/lib/averages/Mma.cs @@ -0,0 +1,78 @@ +using System; +using System.Linq; + +namespace QuanTAlib +{ + public class Mma : AbstractBase + { + private readonly int _period; + private readonly CircularBuffer _buffer; + private double _lastMma; + + public Mma(int period) + { + if (period < 2) + { + throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 2."); + } + _period = period; + _buffer = new CircularBuffer(period); + Name = "Mma"; + WarmupPeriod = period; + Init(); + } + + public Mma(object source, int period) : this(period) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); + } + + public override void Init() + { + base.Init(); + _lastMma = 0; + _buffer.Clear(); + } + + protected override void ManageState(bool isNew) + { + if (isNew) + { + _index++; + } + } + + protected override double Calculation() + { + ManageState(Input.IsNew); + _buffer.Add(Input.Value, Input.IsNew); + + if (_index >= _period) + { + double T = _buffer.Sum(); + double S = CalculateWeightedSum(); + _lastMma = (T / _period) + (6 * S) / ((_period + 1) * _period); + } + else + { + // Use simple average until we have enough data points + _lastMma = _buffer.Average(); + } + + IsHot = _index >= _period; + return _lastMma; + } + + private double CalculateWeightedSum() + { + double sum = 0; + for (int i = 0; i < _period; i++) + { + double weight = (_period - (2 * i + 1)) / 2.0; + sum += weight * _buffer[^(i + 1)]; + } + return sum; + } + } +} \ No newline at end of file diff --git a/lib/averages/Qema.cs b/lib/averages/Qema.cs new file mode 100644 index 00000000..ba0dae5b --- /dev/null +++ b/lib/averages/Qema.cs @@ -0,0 +1,74 @@ +namespace QuanTAlib; + +public class Qema : AbstractBase +{ + private readonly double _k1, _k2, _k3, _k4; + private readonly Ema _ema1, _ema2, _ema3, _ema4; + private double _lastQema, _p_lastQema; + + public Qema(double k1=0.2, double k2=0.2, double k3=0.2, double k4=0.2) + { + if (k1 <= 0 || k2 <= 0 || k3 <= 0 || k4 <= 0 ) + { + throw new ArgumentOutOfRangeException("All k values must be in the range (0, 1]."); + } + + _k1 = k1; + _k2 = k2; + _k3 = k3; + _k4 = k4; + + _ema1 = new Ema(k1); + _ema2 = new Ema(k2); + _ema3 = new Ema(k3); + _ema4 = new Ema(k4); + + Name = $"QEMA ({k1:F2},{k2:F2},{k3:F2},{k4:F2})"; + double smK = Math.Min(Math.Min(_k1, _k2), Math.Min(_k3, _k4)); + + WarmupPeriod = (int) ((2 - smK) / smK); + Init(); + } + + public Qema(object source, double k1, double k2, double k3, double k4) + : this(k1, k2, k3, k4) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); + } + + public override void Init() + { + base.Init(); + _lastQema = 0; + _p_lastQema = 0; + } + + protected override void ManageState(bool isNew) + { + if (isNew) + { + _p_lastQema = _lastQema; + _index++; + } + else + { + _lastQema = _p_lastQema; + } + } + + protected override double Calculation() + { + ManageState(Input.IsNew); + + double ema1 = _ema1.Calc(new TValue(Input.Time, Input.Value, Input.IsNew)); + double ema2 = _ema2.Calc(new TValue(Input.Time, ema1, Input.IsNew)); + double ema3 = _ema3.Calc(new TValue(Input.Time, ema2, Input.IsNew)); + double ema4 = _ema4.Calc(new TValue(Input.Time, ema3, Input.IsNew)); + + _lastQema = 4 * ema1 - 6 * ema2 + 4 * ema3 - ema4; + + IsHot = _index >= WarmupPeriod; + return _lastQema; + } +} \ No newline at end of file diff --git a/lib/averages/Rema.cs b/lib/averages/Rema.cs new file mode 100644 index 00000000..51532904 --- /dev/null +++ b/lib/averages/Rema.cs @@ -0,0 +1,78 @@ +namespace QuanTAlib; + +//https://user42.tuxfamily.org/chart/manual/Regularized-Exponential-Moving-Average.html + +public class Rema : AbstractBase +{ + private readonly int _period; + private readonly double _lambda; + private double _lastRema, _prevRema; + private double _savedLastRema, _savedPrevRema; + + public int Period => _period; + public double Lambda => _lambda; + + public Rema(int period, double lambda = 0.5) + { + if (period < 1) + throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 1."); + if (lambda < 0) + throw new ArgumentOutOfRangeException(nameof(lambda), "Lambda must be non-negative."); + + _period = period; + _lambda = lambda; + Name = $"REMA({period},{lambda:F2})"; + WarmupPeriod = period; + Init(); + } + + public override void Init() + { + base.Init(); + _lastRema = 0; + _prevRema = 0; + _savedLastRema = 0; + _savedPrevRema = 0; + } + + protected override void ManageState(bool isNew) + { + if (isNew) + { + _savedLastRema = _lastRema; + _savedPrevRema = _prevRema; + _index++; + } + else + { + _lastRema = _savedLastRema; + _prevRema = _savedPrevRema; + } + } + + protected override double Calculation() + { + ManageState(Input.IsNew); + + double alpha = 2.0 / (Math.Min(_period, _index) + 1); + + if (_index > 2) + { + double rema = (_lastRema + alpha * (Input.Value - _lastRema) + _lambda * (_lastRema + (_lastRema - _prevRema))) / (1 + _lambda); + _prevRema = _lastRema; + _lastRema = rema; + } + else if (_index == 2) + { + _prevRema = _lastRema; + _lastRema = Input.Value; + } + else + { // _index == 1 + _lastRema = Input.Value; + } + + IsHot = _index >= WarmupPeriod; + return _lastRema; + } +} \ No newline at end of file diff --git a/lib/averages/Rma.cs b/lib/averages/Rma.cs new file mode 100644 index 00000000..81c9f5fe --- /dev/null +++ b/lib/averages/Rma.cs @@ -0,0 +1,66 @@ +using System; +using System.Runtime.CompilerServices; + +namespace QuanTAlib { + +public class Rma : AbstractBase { + private readonly int _period; + private double _alpha; + private double _lastRMA; + private double _savedLastRMA; + + public Rma(int period) + { + if (period < 1) { + throw new ArgumentException("Period must be greater than or equal to 1.", nameof(period)); + } + _period = period; + WarmupPeriod = period * 2; + _alpha = 1.0 / _period; // Wilder's smoothing factor + Name = $"Rma({_period})"; + Init(); + } + + public Rma(object source, int period) : this(period) { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); + } + + public override void Init() { + base.Init(); + _lastRMA = 0; + _savedLastRMA = 0; + } + + protected override void ManageState(bool isNew) { + if (isNew) { + _savedLastRMA = _lastRMA; + _lastValidValue = Input.Value; + _index++; + } else { + _lastRMA = _savedLastRMA; + } + } + + protected override double Calculation() { + ManageState(Input.IsNew); + + double rma; + + if (_index == 1) { + rma = Input.Value; + } else if (_index <= _period) { + // Simple average during initial period + rma = (_lastRMA * (_index - 1) + Input.Value) / _index; + } else { + // Wilder's smoothing method + rma = _alpha * (Input.Value - _lastRMA) + _lastRMA; + } + + _lastRMA = rma; + IsHot = _index >= WarmupPeriod; + + return rma; + } +} +} \ No newline at end of file diff --git a/lib/averages/Sma.cs b/lib/averages/Sma.cs new file mode 100644 index 00000000..811a1718 --- /dev/null +++ b/lib/averages/Sma.cs @@ -0,0 +1,59 @@ +namespace QuanTAlib; + +public class Sma : AbstractBase +{ + // inherited _index + // inherited _value + public readonly int Period; + private CircularBuffer _buffer; + + public Sma(int period) + { + if (period < 1) + { + throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 1."); + } + Period = period; + WarmupPeriod = period; + _buffer = new CircularBuffer(period); + Name = "Sma"; + WarmupPeriod = period; + Init(); + } + + public Sma(object source, int period) : this(period: period) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); + } + //inhereted public void Sub(object source, in ValueEventArgs args) + + public override void Init() + { + base.Init(); + } + + protected override void ManageState(bool isNew) + { + if (isNew) + { + _lastValidValue = Input.Value; + _index++; + } + } + + /// + /// Core SMA calculation - using _buffer.Average + /// + + protected override double Calculation() + { + double result; + ManageState(IsNew); + _buffer.Add(Input.Value, Input.IsNew); + result = _buffer.Average(); + + IsHot = _index >= WarmupPeriod; + return result; + } +} \ No newline at end of file diff --git a/lib/averages/Smma.cs b/lib/averages/Smma.cs new file mode 100644 index 00000000..cd6d674d --- /dev/null +++ b/lib/averages/Smma.cs @@ -0,0 +1,78 @@ +using System; + +namespace QuanTAlib; + +public class Smma : AbstractBase +{ + private readonly int _period; + private CircularBuffer? _buffer; + private double _lastSmma, _p_lastSmma; + + public Smma(int period) + { + if (period < 1) + { + throw new ArgumentException("Period must be greater than or equal to 1.", nameof(period)); + } + _period = period; + WarmupPeriod = period; + Name = $"Smma({_period})"; + Init(); + } + + public Smma(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 = new CircularBuffer(_period); + _lastSmma = 0; + } + + protected override void ManageState(bool isNew) + { + if (isNew) + { + _lastValidValue = Input.Value; + _p_lastSmma = _lastSmma; + _index++; + } + else + { + _lastSmma = _p_lastSmma; + } + } + + + protected override double Calculation() + { + ManageState(Input.IsNew); + + _buffer!.Add(Input.Value, Input.IsNew); + + double smma; + + if (_index <= _period) + { + smma = _buffer.Average(); + + if (_index == _period) + { + _lastSmma = smma; // Initialize _lastSmma for the transition + } + } + else + { + smma = ((_lastSmma * (_period - 1)) + Input.Value) / _period; + } + + _lastSmma = smma; + IsHot = _index >= WarmupPeriod; + + return smma; + } +} \ No newline at end of file diff --git a/lib/averages/T3.cs b/lib/averages/T3.cs new file mode 100644 index 00000000..3fefa8c7 --- /dev/null +++ b/lib/averages/T3.cs @@ -0,0 +1,133 @@ +namespace QuanTAlib; + +public class T3 : AbstractBase +{ + private readonly int _period; + private readonly double _vfactor; + private readonly bool _useSma; + private readonly double _k, _k1m, _c1, _c2, _c3, _c4; + private readonly CircularBuffer _buffer1, _buffer2, _buffer3, _buffer4, _buffer5, _buffer6; + private double _lastEma1, _lastEma2, _lastEma3, _lastEma4, _lastEma5, _lastEma6; + private double _p_lastEma1, _p_lastEma2, _p_lastEma3, _p_lastEma4, _p_lastEma5, _p_lastEma6; + + public T3(int period, double vfactor = 0.7, bool useSma = true) + { + if (period < 1) + { + throw new ArgumentException("Period must be greater than or equal to 1.", nameof(period)); + } + _period = period; + _vfactor = vfactor; + _useSma = useSma; + WarmupPeriod = period; + + _k = 2.0 / (_period + 1); + _k1m = 1.0 - _k; + _c1 = -_vfactor * _vfactor * _vfactor; + _c2 = 3 * _vfactor * _vfactor + 3 * _vfactor * _vfactor * _vfactor; + _c3 = -6 * _vfactor * _vfactor - 3 * _vfactor - 3 * _vfactor * _vfactor * _vfactor; + _c4 = 1 + 3 * _vfactor + _vfactor * _vfactor * _vfactor + 3 * _vfactor * _vfactor; + + _buffer1 = new(period); + _buffer2 = new(period); + _buffer3 = new(period); + _buffer4 = new(period); + _buffer5 = new(period); + _buffer6 = new(period); + + + Name = $"T3({_period}, {_vfactor})"; + Init(); + } + + public T3(object source, int period, double vfactor = 0.7, bool useSma = true) : this(period, vfactor, useSma) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); + } + + public override void Init() + { + base.Init(); + _lastEma1 = _lastEma2 = _lastEma3 = _lastEma4 = _lastEma5 = _lastEma6 = 0; + _buffer1.Clear(); + _buffer2.Clear(); + _buffer3.Clear(); + _buffer4.Clear(); + _buffer5.Clear(); + _buffer6.Clear(); + } + + protected override void ManageState(bool isNew) + { + if (isNew) + { + _lastValidValue = Input.Value; + _index++; + _p_lastEma1 = _lastEma1; + _p_lastEma2 = _lastEma2; + _p_lastEma3 = _lastEma3; + _p_lastEma4 = _lastEma4; + _p_lastEma5 = _lastEma5; + _p_lastEma6 = _lastEma6; + } + else + { + _lastEma1 = _p_lastEma1; + _lastEma2 = _p_lastEma2; + _lastEma3 = _p_lastEma3; + _lastEma4 = _p_lastEma4; + _lastEma5 = _p_lastEma5; + _lastEma6 = _p_lastEma6; + } + } + + + protected override double Calculation() + { + ManageState(Input.IsNew); + + double ema1, ema2, ema3, ema4, ema5, ema6; + + if (_index == 1) + { + ema1 = ema2 = ema3 = ema4 = ema5 = ema6 = Input.Value; + } + else if (_index <= _period && _useSma) + { + _buffer1.Add(Input.Value, Input.IsNew); + ema1 = _buffer1.Average(); + _buffer2.Add(ema1, Input.IsNew); + ema2 = _buffer2.Average(); + _buffer3.Add(ema2, Input.IsNew); + ema3 = _buffer3.Average(); + _buffer4.Add(ema3, Input.IsNew); + ema4 = _buffer4.Average(); + _buffer5.Add(ema4, Input.IsNew); + ema5 = _buffer5.Average(); + _buffer6.Add(ema5, Input.IsNew); + ema6 = _buffer6.Average(); + } + else + { + ema1 = _k * (Input.Value - _lastEma1) + _lastEma1; + ema2 = _k * (ema1 - _lastEma2) + _lastEma2; + ema3 = _k * (ema2 - _lastEma3) + _lastEma3; + ema4 = _k * (ema3 - _lastEma4) + _lastEma4; + ema5 = _k * (ema4 - _lastEma5) + _lastEma5; + ema6 = _k * (ema5 - _lastEma6) + _lastEma6; + } + + _lastEma1 = ema1; + _lastEma2 = ema2; + _lastEma3 = ema3; + _lastEma4 = ema4; + _lastEma5 = ema5; + _lastEma6 = ema6; + + double t3 = _c1 * ema6 + _c2 * ema5 + _c3 * ema4 + _c4 * ema3; + + IsHot = _index >= WarmupPeriod; + return t3; + } +} \ No newline at end of file diff --git a/lib/averages/Tema.cs b/lib/averages/Tema.cs new file mode 100644 index 00000000..a783cf54 --- /dev/null +++ b/lib/averages/Tema.cs @@ -0,0 +1,81 @@ +namespace QuanTAlib; + +public class Tema : AbstractBase +{ + private readonly int _period; + private double _lastEma1, _p_lastEma1; + private double _lastEma2, _p_lastEma2; + private double _lastEma3, _p_lastEma3; + private double _k, _e, _p_e; + + public Tema(int period) + { + if (period < 1) + { + throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 1."); + } + _period = period; + Name = "Tema"; + double percentile = 0.85; //targeting 85th percentile of correctness of converging EMA + WarmupPeriod = (int)Math.Ceiling(-period * Math.Log(1 - percentile)); + Init(); + } + + public Tema(object source, int period) : this(period) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); + } + + public override void Init() + { + base.Init(); + _k = 2.0 / (_period + 1); + _e = 1.0; + _lastEma1 = _lastEma2 = _lastEma3 = 0; + } + + protected override void ManageState(bool isNew) + { + if (isNew) + { + _p_lastEma1 = _lastEma1; + _p_lastEma2 = _lastEma2; + _p_lastEma3 = _lastEma3; + _p_e = _e; + _index++; + } + else + { + _lastEma1 = _p_lastEma1; + _lastEma2 = _p_lastEma2; + _lastEma3 = _p_lastEma3; + _e = _p_e; + } + } + + protected override double Calculation() + { + double result, _ema1, _ema2, _ema3; + ManageState(Input.IsNew); + + _e = (_e > 1e-10) ? (1 - _k) * _e : 0; + double _invE = (_e > 1e-10) ? 1 / (1 - _e) : 1; + + _ema1 = _k * (Input.Value - _lastEma1) + _lastEma1; + + _ema2 = _k * (_ema1 * _invE - _lastEma2) + _lastEma2; + + _ema3 = _k * (_ema2 * _invE - _lastEma3) + _lastEma3; + + double _tema = 3 * _ema1 * _invE - 3 * _ema2 * _invE + _ema3 * _invE; + + result = _tema; + _lastEma1 = _ema1; + _lastEma2 = _ema2; + _lastEma3 = _ema3; + + IsHot = _index >= WarmupPeriod; + return result; + } +} \ No newline at end of file diff --git a/lib/averages/Vidya.cs b/lib/averages/Vidya.cs new file mode 100644 index 00000000..dda4db05 --- /dev/null +++ b/lib/averages/Vidya.cs @@ -0,0 +1,92 @@ +using System; +using System.Linq; +using System.Runtime.CompilerServices; + +namespace QuanTAlib; + +public class Vidya : AbstractBase +{ + private readonly int _shortPeriod; + private readonly int _longPeriod; + private readonly double _alpha; + private double _lastVIDYA, _p_lastVIDYA; + private CircularBuffer? _shortBuffer; + private CircularBuffer? _longBuffer; + + public Vidya(int shortPeriod, int longPeriod = 0, double alpha = 0.2) + { + if (shortPeriod < 1) + { + throw new ArgumentException("Short period must be greater than or equal to 1.", nameof(shortPeriod)); + } + _shortPeriod = shortPeriod; + _longPeriod = (longPeriod == 0) ? shortPeriod * 4 : longPeriod; + _alpha = alpha; + WarmupPeriod = _longPeriod; + Name = $"Vidya({_shortPeriod},{_longPeriod})"; + Init(); + } + + public Vidya(object source, int shortPeriod, int longPeriod = 0, double alpha = 0.2) + : this(shortPeriod, longPeriod, alpha) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); + } + + public override void Init() + { + base.Init(); + _lastVIDYA = 0; + _shortBuffer = new CircularBuffer(_shortPeriod); + _longBuffer = new CircularBuffer(_longPeriod); + } + + protected override void ManageState(bool isNew) + { + if (isNew) + { + _lastValidValue = Input.Value; + _index++; + _p_lastVIDYA = _lastVIDYA; + } + else + { + _lastVIDYA = _p_lastVIDYA; + } + } + + protected override double Calculation() + { + ManageState(Input.IsNew); + + _shortBuffer!.Add(Input.Value, Input.IsNew); + _longBuffer!.Add(Input.Value, Input.IsNew); + + double vidya; + if (_index <= _longPeriod) + { + vidya = _shortBuffer.Average(); + } + else + { + double shortStdDev = CalculateStdDev(_shortBuffer); + double longStdDev = CalculateStdDev(_longBuffer); + double s = _alpha * (shortStdDev / longStdDev); + vidya = (s * Input.Value) + ((1 - s) * _lastVIDYA); + } + + _lastVIDYA = vidya; + IsHot = _index >= WarmupPeriod; + + return vidya; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private double CalculateStdDev(CircularBuffer buffer) + { + double mean = buffer.Average(); + double sumSquaredDiff = buffer.Sum(x => Math.Pow(x - mean, 2)); + return Math.Sqrt(sumSquaredDiff / buffer.Count); + } +} \ No newline at end of file diff --git a/lib/averages/Zlema.cs b/lib/averages/Zlema.cs new file mode 100644 index 00000000..741a4be3 --- /dev/null +++ b/lib/averages/Zlema.cs @@ -0,0 +1,73 @@ +using System; +using System.Runtime.CompilerServices; + +namespace QuanTAlib; + +public class Zlema : AbstractBase +{ + private readonly int _period; + private CircularBuffer? _buffer; + private double _alpha; + private int _lag; + private double _lastZLEMA, _p_lastZLEMA; + + public Zlema(int period) + { + if (period < 1) + { + throw new ArgumentException("Period must be greater than or equal to 1.", nameof(period)); + } + _period = period; + WarmupPeriod = period; + _alpha = 2.0 / (_period + 1); + _lag = (_period - 1) / 2; + Name = $"Zlema({_period})"; + Init(); + } + + public Zlema(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 = new CircularBuffer(_period); + _lastZLEMA = 0; + } + + protected override void ManageState(bool isNew) + { + if (isNew) + { + _lastValidValue = Input.Value; + _index++; + _p_lastZLEMA = _lastZLEMA; + } + else + { + _lastZLEMA = _p_lastZLEMA; + } + } + + protected override double Calculation() + { + ManageState(Input.IsNew); + + _buffer!.Add(Input.Value, Input.IsNew); + + int lag = Math.Max(Math.Min((int)((_period - 1) * 0.5), _buffer.Count - 1), 0) + 1; + double zlValue = 2 * Input.Value - _buffer[_buffer.Count - lag]; + + // Dynamic alpha factor for index <= period + double k = (_index <= _period) ? (2.0 / (_index + 1)) : _alpha; + double zlema = (zlValue - _lastZLEMA) * k + _lastZLEMA; + + _lastZLEMA = zlema; + IsHot = _index >= WarmupPeriod; + + return zlema; + } +} \ No newline at end of file