#!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 # Simple Moving Average (SMA) 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. The **Simple Moving Average (SMA)** is the most basic form of moving average, calculating the arithmetic mean over a specified period. Unlike the EMA, the SMA assigns equal weight to all data points in the window, making it a good baseline for trend analysis. **Key characteristics:** - Equal weighting for all values in the period - O(1) update complexity using running sum - O(1) bar correction using scalar state - Smooth output with good noise reduction - More lag than EMA due to equal weighting 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 SMAs 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 SMA 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 SMA (Period 3) ---"); var smaBatch = new Sma(3); var resultBatch = smaBatch.Update(manualData); PrintSeries(resultBatch, 5); // Show the calculation for each step Console.WriteLine("\nCalculation breakdown:"); Console.WriteLine(" SMA[0] = 100 / 1 = 100.00"); Console.WriteLine(" SMA[1] = (100 + 102) / 2 = 101.00"); Console.WriteLine(" SMA[2] = (100 + 102 + 101) / 3 = 101.00"); Console.WriteLine(" SMA[3] = (102 + 101 + 103) / 3 = 102.00"); Console.WriteLine(" SMA[4] = (101 + 103 + 105) / 3 = 103.00"); #!markdown ### Streaming Processing Streaming processing updates the SMA one data point at a time. This is essential for real-time trading systems where data arrives sequentially. #!csharp Console.WriteLine("\n--- Streaming SMA (Period 3) ---"); var smaStream = new Sma(3); foreach (var item in manualData) { var result = smaStream.Update(item); Console.WriteLine($"Time: {item.Time:HH:mm:ss}, Input: {item.Value:F2}, SMA: {result.Value:F2}, IsHot: {smaStream.IsHot}"); } // Verify that the last values match var batchLast = resultBatch.Last().Value; var streamLast = smaStream.Value.Value; Console.WriteLine($"\nMatch: {Math.Abs(batchLast - streamLast) < 1e-10} (Batch: {batchLast:F2}, Stream: {streamLast:F2})"); // Show SMA properties Console.WriteLine($"\nSMA Properties:"); Console.WriteLine($" Name: {smaStream.Name}"); Console.WriteLine($" WarmupPeriod: {smaStream.WarmupPeriod}"); Console.WriteLine($" IsHot: {smaStream.IsHot}"); #!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). **SMA achieves O(1) bar correction** by saving scalar state after each `isNew=true` update. #!csharp Console.WriteLine("\n--- Streaming with Intra-bar Updates ---"); var smaIntra = new Sma(3); // 1. Process the first 4 bars normally for (int i = 0; i < 4; i++) { smaIntra.Update(manualData[i]); } Console.WriteLine($"After 4th bar: {smaIntra.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); smaIntra.Update(update1, isNew: true); // First update for this bar is "New" Console.WriteLine($"Update 1 (104.0): {smaIntra.Value.Value:F2}"); // Update 2: Price moves to 106.0 (Same time, same bar) var update2 = new TValue(manualData[4].Time, 106.0); smaIntra.Update(update2, isNew: false); // Not new, just an update Console.WriteLine($"Update 2 (106.0): {smaIntra.Value.Value:F2}"); // Update 3: Final Close at 105.0 var update3 = manualData[4]; smaIntra.Update(update3, isNew: false); // Final update Console.WriteLine($"Update 3 (105.0): {smaIntra.Value.Value:F2}"); // Verify match with batch result Console.WriteLine($"Match with Batch: {Math.Abs(smaIntra.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 smaLargeBatch = new Sma(20); var batchLargeResult = smaLargeBatch.Update(closeSeries); Console.WriteLine($"Batch Last Value: {batchLargeResult.Last().Value:F2}"); // Streaming var smaLargeStream = new Sma(20); TValue lastStreamVal = default; foreach(var item in closeSeries) { lastStreamVal = smaLargeStream.Update(item); } Console.WriteLine($"Streaming Last Value: {lastStreamVal.Value:F2}"); // Verify match Console.WriteLine($"Match: {Math.Abs(batchLargeResult.Last().Value - lastStreamVal.Value) < 1e-10}"); #!markdown ## 4. Vectorized SMA (Multiple Periods) `SmaVector` allows calculating multiple SMAs (e.g., 5, 10, 20) simultaneously. This is useful for comparing different timeframes. ### Vectorized Batch #!csharp int[] periods = { 5, 10, 20 }; Console.WriteLine($"\n--- Vectorized Batch SMA (Periods: {string.Join(", ", periods)}) ---"); var smaVectorBatch = new SmaVector(periods); var vectorBatchResults = smaVectorBatch.Calculate(closeSeries); for (int i = 0; i < periods.Length; i++) { Console.WriteLine($"SMA({periods[i]}) Last Value: {vectorBatchResults[i].Last().Value:F2}"); } #!markdown ### Vectorized Streaming #!csharp Console.WriteLine($"\n--- Vectorized Streaming SMA (Periods: {string.Join(", ", periods)}) ---"); var smaVectorStream = new SmaVector(periods); TValue[] lastVectorVal = null; foreach(var item in closeSeries) { lastVectorVal = smaVectorStream.Update(item); } for (int i = 0; i < periods.Length; i++) { Console.WriteLine($"SMA({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 `Sma` and `SmaVector` 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 SMA var smaNaN = new Sma(10); // Feed valid values first smaNaN.Update(new TValue(DateTime.Now, 100.0)); smaNaN.Update(new TValue(DateTime.Now.AddMinutes(1), 110.0)); Console.WriteLine($"After valid values: {smaNaN.Value.Value:F2}"); // Feed NaN - should use last valid value (110) var resultAfterNaN = smaNaN.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 = smaNaN.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 = smaNaN.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 smaBatchNaN = new Sma(3); var resultsWithNaN = smaBatchNaN.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 SMA with Invalid Values ---"); int[] periodsNaN = { 5, 10 }; var smaVectorNaN = new SmaVector(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 = smaVectorNaN.Update(new TValue(time, val)); var inputStr = double.IsFinite(val) ? val.ToString("F2") : val.ToString(); Console.WriteLine($"Input: {inputStr,-10} → SMA(5): {results[0].Value:F2}, SMA(10): {results[1].Value:F2}"); time = time.AddMinutes(1); } Console.WriteLine("\nAll outputs are finite - invalid inputs were substituted with last valid values."); #!markdown ## 6. SMA vs EMA Comparison The SMA and EMA are both trend-following indicators, but they weight data differently: - **SMA**: Equal weight to all values in the window - **EMA**: More weight to recent values (exponentially decreasing) #!csharp Console.WriteLine("\n--- SMA vs EMA Comparison (Period 10) ---"); var compareData = new TSeries(); var baseTime = DateTime.Now; for (int i = 0; i < 20; i++) { // Create data with a sudden spike at position 10 double value = (i == 10) ? 150.0 : 100.0; compareData.Add(baseTime.AddMinutes(i), value); } var smaCompare = new Sma(10); var emaCompare = new Ema(10); Console.WriteLine("Position | Input | SMA | EMA | Difference"); Console.WriteLine("---------+--------+---------+---------+-----------"); for (int i = 0; i < compareData.Count; i++) { var smaVal = smaCompare.Update(compareData[i]); var emaVal = emaCompare.Update(compareData[i]); var input = compareData[i].Value; var diff = smaVal.Value - emaVal.Value; Console.WriteLine($" {i,2} | {input,6:F0} | {smaVal.Value,7:F2} | {emaVal.Value,7:F2} | {diff,+9:F2}"); } Console.WriteLine("\nNote: After the spike (position 10), EMA reacts faster due to higher weight on recent values."); Console.WriteLine("SMA takes longer to reflect changes as all values have equal weight.");