#!meta {"kernelInfo":{"defaultKernelName":"csharp","items":[{"name":"csharp"},{"name":"fsharp","languageName":"F#","aliases":["f#","fs"]},{"name":"html","languageName":"HTML"},{"name":"http","languageName":"HTTP"},{"name":"javascript","languageName":"JavaScript","aliases":["js"]},{"name":"mermaid","languageName":"Mermaid"},{"name":"pwsh","languageName":"PowerShell","aliases":["powershell"]},{"name":"value"}]}} #!markdown # Exponential Moving Average (EMA) Examples This is a **.NET Interactive** notebook. To run it, you need the [Polyglot Notebooks](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.dotnet-interactive-vscode) extension installed in VS Code. For detailed documentation on the EMA indicator, including mathematical formulas and interpretation, please refer to [Ema.md](Ema.md). The **Exponential Moving Average (EMA)** is a weighted moving average that gives more importance to recent price data. Unlike the Simple Moving Average (SMA), which assigns equal weight to all data points, the EMA reacts more significantly to recent price changes. This notebook demonstrates: 1. **Manual Data Processing**: Understanding Batch vs. Streaming modes. 2. **Streaming with `isNew`**: Handling intra-bar updates. 3. **Large Dataset Processing**: Using Geometric Brownian Motion (GBM) generated data. 4. **Vectorized Operations**: Calculating multiple EMAs simultaneously. #!csharp // Reference the library #r "..\..\bin\QuanTAlib.dll" using System; using System.Linq; using QuanTAlib; // Helper to print TSeries void PrintSeries(TSeries series, int count = 5) { Console.WriteLine($"Series Length: {series.Count}"); foreach (var item in series.Take(count)) { Console.WriteLine($"Time: {item.Time:HH:mm:ss}, Value: {item.Value:F2}"); } if (series.Count > count) Console.WriteLine("..."); } #!markdown ## 1. Manual Data: Batch vs. Streaming We'll start with a small, manually created dataset to clearly see how Batch and Streaming operations work. ### Batch Processing Batch processing calculates the EMA for the entire dataset at once. This is efficient for historical analysis. #!csharp // Create a small manual dataset var manualData = new TSeries(); manualData.Add(DateTime.Now, 100.0); manualData.Add(DateTime.Now.AddMinutes(1), 102.0); manualData.Add(DateTime.Now.AddMinutes(2), 101.0); manualData.Add(DateTime.Now.AddMinutes(3), 103.0); manualData.Add(DateTime.Now.AddMinutes(4), 105.0); Console.WriteLine("--- Input Data ---"); PrintSeries(manualData, 5); // Batch Calculation Console.WriteLine("\n--- Batch EMA (Period 3) ---"); var emaBatch = new Ema(3); var resultBatch = emaBatch.Update(manualData); PrintSeries(resultBatch, 5); #!markdown ### Streaming Processing Streaming processing updates the EMA one data point at a time. This is essential for real-time trading systems where data arrives sequentially. #!csharp Console.WriteLine("\n--- Streaming EMA (Period 3) ---"); var emaStream = new Ema(3); foreach (var item in manualData) { var result = emaStream.Update(item); Console.WriteLine($"Time: {item.Time:HH:mm:ss}, Input: {item.Value:F2}, EMA: {result.Value:F2}, IsHot: {emaStream.IsHot}"); } // Verify that the last values match var batchLast = resultBatch.Last().Value; var streamLast = emaStream.Value.Value; Console.WriteLine($"\nMatch: {Math.Abs(batchLast - streamLast) < 1e-10} (Batch: {batchLast:F2}, Stream: {streamLast:F2})"); #!markdown ## 2. Streaming with `isNew` (Intra-bar Updates) In real-time feeds, you often receive multiple updates for the *same* bar (e.g., price changes within the current minute) before the bar closes. * `isNew = true`: The input is a new bar (advances time). * `isNew = false`: The input is an update to the current bar (recalculates without advancing). #!csharp Console.WriteLine("\n--- Streaming with Intra-bar Updates ---"); var emaIntra = new Ema(3); // 1. Process the first 4 bars normally for (int i = 0; i < 4; i++) { emaIntra.Update(manualData[i]); } Console.WriteLine($"After 4th bar: {emaIntra.Value.Value:F2}"); // 2. Simulate intra-bar updates for the 5th bar (Final value is 105.0) // Update 1: Price moves to 104.0 var update1 = new TValue(manualData[4].Time, 104.0); emaIntra.Update(update1, isNew: true); // First update for this bar is "New" Console.WriteLine($"Update 1 (104.0): {emaIntra.Value.Value:F2}"); // Update 2: Price moves to 106.0 (Same time, same bar) var update2 = new TValue(manualData[4].Time, 106.0); emaIntra.Update(update2, isNew: false); // Not new, just an update Console.WriteLine($"Update 2 (106.0): {emaIntra.Value.Value:F2}"); // Update 3: Final Close at 105.0 var update3 = manualData[4]; emaIntra.Update(update3, isNew: false); // Final update Console.WriteLine($"Update 3 (105.0): {emaIntra.Value.Value:F2}"); // Verify match with batch result Console.WriteLine($"Match with Batch: {Math.Abs(emaIntra.Value.Value - batchLast) < 1e-10}"); #!markdown ## 3. Large Dataset: Geometric Brownian Motion (GBM) We'll generate a larger dataset (1000 bars) using a Geometric Brownian Motion generator to simulate realistic market data. #!csharp // Generate 1000 bars of data var gbm = new GBM(startPrice: 100.0, mu: 0.05, sigma: 0.2); var gbmData = gbm.Fetch(1000, DateTime.Now.Ticks, TimeSpan.FromMinutes(1)); var closeSeries = gbmData.Close; Console.WriteLine($"Generated {closeSeries.Count} bars of GBM data."); Console.WriteLine($"First 5 values: {string.Join(", ", closeSeries.Take(5).Select(x => x.Value.ToString("F2")))}"); #!markdown ### Batch vs. Streaming Performance on Large Data #!csharp // Batch var emaLargeBatch = new Ema(20); var batchLargeResult = emaLargeBatch.Update(closeSeries); Console.WriteLine($"Batch Last Value: {batchLargeResult.Last().Value:F2}"); // Streaming var emaLargeStream = new Ema(20); TValue lastStreamVal = default; foreach(var item in closeSeries) { lastStreamVal = emaLargeStream.Update(item); } Console.WriteLine($"Streaming Last Value: {lastStreamVal.Value:F2}"); #!markdown ## 4. Vectorized EMA (Multiple Periods) `EmaVector` allows calculating multiple EMAs (e.g., 9, 12, 26) simultaneously. This is optimized for performance using SIMD where available. ### Vectorized Batch #!csharp int[] periods = { 9, 12, 26 }; Console.WriteLine($"\n--- Vectorized Batch EMA (Periods: {string.Join(", ", periods)}) ---"); var emaVectorBatch = new EmaVector(periods); var vectorBatchResults = emaVectorBatch.Calculate(closeSeries); for (int i = 0; i < periods.Length; i++) { Console.WriteLine($"EMA({periods[i]}) Last Value: {vectorBatchResults[i].Last().Value:F2}"); } #!markdown ### Vectorized Streaming #!csharp Console.WriteLine($"\n--- Vectorized Streaming EMA (Periods: {string.Join(", ", periods)}) ---"); var emaVectorStream = new EmaVector(periods); TValue[] lastVectorVal = null; foreach(var item in closeSeries) { lastVectorVal = emaVectorStream.Update(item); } for (int i = 0; i < periods.Length; i++) { Console.WriteLine($"EMA({periods[i]}) Last Value: {lastVectorVal[i].Value:F2}"); } // Verification bool allMatch = true; for (int i = 0; i < periods.Length; i++) { if (Math.Abs(vectorBatchResults[i].Last().Value - lastVectorVal[i].Value) > 1e-10) { allMatch = false; break; } } Console.WriteLine($"\nAll Vectorized Stream/Batch values match: {allMatch}"); #!markdown ## 5. Handling Invalid Values (NaN/Infinity) Both `Ema` and `EmaVector` use **last-value substitution** for invalid inputs. When a non-finite value (NaN, PositiveInfinity, NegativeInfinity) is encountered, it is replaced with the last valid value. This provides output continuity instead of propagating invalid values through the calculation. #!csharp Console.WriteLine("\n--- Handling Invalid Values ---"); // Single EMA var emaNaN = new Ema(10); // Feed valid values first emaNaN.Update(new TValue(DateTime.Now, 100.0)); emaNaN.Update(new TValue(DateTime.Now.AddMinutes(1), 110.0)); Console.WriteLine($"After valid values: {emaNaN.Value.Value:F2}"); // Feed NaN - should use last valid value (110) var resultAfterNaN = emaNaN.Update(new TValue(DateTime.Now.AddMinutes(2), double.NaN)); Console.WriteLine($"After NaN input: {resultAfterNaN.Value:F2} (IsFinite: {double.IsFinite(resultAfterNaN.Value)})"); // Feed Infinity - should use last valid value (110) var resultAfterInf = emaNaN.Update(new TValue(DateTime.Now.AddMinutes(3), double.PositiveInfinity)); Console.WriteLine($"After Infinity input: {resultAfterInf.Value:F2} (IsFinite: {double.IsFinite(resultAfterInf.Value)})"); // Continue with valid value var resultAfterValid = emaNaN.Update(new TValue(DateTime.Now.AddMinutes(4), 120.0)); Console.WriteLine($"After valid value (120): {resultAfterValid.Value:F2}"); #!csharp Console.WriteLine("\n--- Batch Processing with Invalid Values ---"); // Create series with NaN values interspersed var seriesWithNaN = new TSeries(); seriesWithNaN.Add(DateTime.Now.Ticks, 100.0); seriesWithNaN.Add(DateTime.Now.Ticks + 1, 110.0); seriesWithNaN.Add(DateTime.Now.Ticks + 2, double.NaN); seriesWithNaN.Add(DateTime.Now.Ticks + 3, 120.0); seriesWithNaN.Add(DateTime.Now.Ticks + 4, double.PositiveInfinity); seriesWithNaN.Add(DateTime.Now.Ticks + 5, 130.0); var emaBatchNaN = new Ema(3); var resultsWithNaN = emaBatchNaN.Update(seriesWithNaN); Console.WriteLine("Input → Output:"); for (int i = 0; i < seriesWithNaN.Count; i++) { var input = seriesWithNaN[i].Value; var output = resultsWithNaN[i].Value; var inputStr = double.IsFinite(input) ? input.ToString("F2") : input.ToString(); Console.WriteLine($" {inputStr,-10} → {output:F2} (IsFinite: {double.IsFinite(output)})"); } #!csharp Console.WriteLine("\n--- Vectorized EMA with Invalid Values ---"); int[] periodsNaN = { 5, 10 }; var emaVectorNaN = new EmaVector(periodsNaN); // Feed values including invalid ones var inputsNaN = new double[] { 100, 110, double.NaN, 120, double.PositiveInfinity, 130 }; var time = DateTime.Now; foreach (var val in inputsNaN) { var results = emaVectorNaN.Update(new TValue(time, val)); var inputStr = double.IsFinite(val) ? val.ToString("F2") : val.ToString(); Console.WriteLine($"Input: {inputStr,-10} → EMA(5): {results[0].Value:F2}, EMA(10): {results[1].Value:F2}"); time = time.AddMinutes(1); } Console.WriteLine("\nAll outputs are finite - invalid inputs were substituted with last valid values.");