Mathew K Analytics

Lesson 49 · Finance and Stock Market Analytics

Moving Averages and Smoothing Techniques in Stock Market Analysis

Learn to smooth noisy price data with moving averages Discover why smoothing is crucial for financial decision making Understand how to compute and use…

⬇ Download notebookOpen in Colab ↗

📓 Full notebook

Download .ipynb

Moving Averages and Smoothing Techniques in Stock Market Analysis#

  • Learn to smooth noisy price data with moving averages
  • Discover why smoothing is crucial for financial decision making
  • Understand how to compute and use multiple moving averages
  • Build and debug real Python code to generate actionable signals
  • See a full pipeline from raw prices to trading insights
  • Practice with real stock data for major tech tickers
import warnings
warnings.filterwarnings('ignore')
import pandas as pd
import numpy as np
import yfinance as yf

Understanding Stock Data and Smoothing#

  • In finance, daily prices for stocks are often noisy and volatile
  • Moving averages help reveal mid-term or long-term trends
  • Real stock datasets have missing values, gaps, or unexpected outliers
  • Beginners often confuse short-term and long-term smoothing
  • Our data will include multiple tickers, date columns, and daily prices
# Load OHLCV data for five top tech stocks for 1 year (multi-ticker, long format)
tickers = ['AAPL','MSFT','GOOGL','AMZN','TSLA']
ohlcv = yf.download(tickers, period='1y', auto_adjust=True, progress=False)
ohlcv = ohlcv.stack(future_stack=True).rename_axis(['Date','Ticker']).reset_index()
ohlcv.columns.name = None
 
print(ohlcv.shape)
print(ohlcv.head(3))
(1255, 7)
        Date Ticker       Close        High         Low        Open    Volume
0 2025-07-15   AAPL  208.283691  211.052705  208.094440  208.393257  42296300
1 2025-07-15   AMZN  226.350006  227.270004  225.460007  226.199997  34907300
2 2025-07-15  GOOGL  181.482269  183.695955  181.083413  182.289963  33448300

Example 1: Simple Moving Average (Beginner)#

  • Let us calculate the 10-day moving average for Apple (AAPL) closing prices
  • Moving averages are often used to reduce noise and spot trends
  • The rolling() method in pandas makes this process simple
# Filter for just AAPL
aapl = ohlcv[ohlcv['Ticker'] == 'AAPL'].copy()
aapl['MA10'] = aapl['Close'].rolling(window=10).mean().round(2)
print(aapl[['Date','Close','MA10']].head(12))
         Date       Close    MA10
0  2025-07-15  208.283691     NaN
5  2025-07-16  209.329544     NaN
10 2025-07-17  209.190109     NaN
15 2025-07-18  210.345505     NaN
20 2025-07-21  211.640366     NaN
25 2025-07-22  213.552795     NaN
30 2025-07-23  213.303772     NaN
35 2025-07-24  212.915314     NaN
40 2025-07-25  213.034851     NaN
45 2025-07-28  213.204178  211.48
50 2025-07-29  210.435150  211.70
55 2025-07-30  208.223923  211.58

Example 2: Visualizing Price and Moving Average (Beginner)#

  • Visual charts make it easier to compare raw and smoothed prices
  • Let us create a quick plot for Apple closing prices and its 10-day moving average
  • Practice reading the smoothed trend line
import matplotlib.pyplot as plt
plt.figure(figsize=(10,4))
plt.plot(aapl['Date'], aapl['Close'], label='AAPL Close', alpha=0.6)
plt.plot(aapl['Date'], aapl['MA10'], label='10-Day MA', color='orange')
plt.legend(loc='upper left')
plt.title('Apple Closing Price vs. 10-Day Moving Average')
plt.xlabel('Date')
plt.ylabel('Price (USD)')
plt.tight_layout()
plt.show()
No description has been provided for this image

Example 3: Customizing Moving Average Windows (Beginner)#

  • Any window size can be used for moving averages
  • Short windows respond quickly but can be noisy
  • Long windows smooth more slowly but lag behind trends
aapl['MA3'] = aapl['Close'].rolling(window=3).mean().round(2)
aapl['MA30'] = aapl['Close'].rolling(window=30).mean().round(2)
print(aapl[['Date','Close','MA3','MA10','MA30']].head(15))
         Date       Close     MA3    MA10  MA30
0  2025-07-15  208.283691     NaN     NaN   NaN
5  2025-07-16  209.329544     NaN     NaN   NaN
10 2025-07-17  209.190109  208.93     NaN   NaN
15 2025-07-18  210.345505  209.62     NaN   NaN
20 2025-07-21  211.640366  210.39     NaN   NaN
25 2025-07-22  213.552795  211.85     NaN   NaN
30 2025-07-23  213.303772  212.83     NaN   NaN
35 2025-07-24  212.915314  213.26     NaN   NaN
40 2025-07-25  213.034851  213.08     NaN   NaN
45 2025-07-28  213.204178  213.05  211.48   NaN
50 2025-07-29  210.435150  212.22  211.70   NaN
55 2025-07-30  208.223923  210.62  211.58   NaN
60 2025-07-31  206.749771  208.47  211.34   NaN
65 2025-08-01  201.580292  205.52  210.46   NaN
70 2025-08-04  202.546463  203.63  209.55   NaN

Example 4: Moving Average for Multiple Stocks (Intermediate)#

  • Let us calculate a 20-day moving average for every stock in our dataset
  • This approach works for all tickers, not just one
  • Be careful to group by ticker before calculating rolling means
ohlcv['MA20'] = ohlcv.groupby('Ticker')['Close'].transform(lambda x: x.rolling(window=20).mean())
ohlcv['MA20'] = ohlcv['MA20'].round(2)
print(ohlcv.loc[ohlcv['Date'] == ohlcv['Date'].min()', ['Date', 'Ticker', 'Close', 'MA20']].head())
  Cell In[6], line 3
    print(ohlcv.loc[ohlcv['Date'] == ohlcv['Date'].min()', ['Date', 'Ticker', 'Close', 'MA20']].head())
                                                                                            ^
SyntaxError: unterminated string literal (detected at line 3)

Example 5: Detecting Moving Average Crossovers (Intermediate)#

  • A common trading strategy is to look for short-term versus long-term moving average crossovers
  • When a fast MA crosses above a slow MA, this can be a bullish signal
  • When a fast MA crosses below a slow MA, this can be a bearish signal
ohlcv['MA50'] = ohlcv.groupby('Ticker')['Close'].transform(lambda x: x.rolling(window=50).mean()).round(2)
ohlcv['crossover'] = np.where(ohlcv['MA20'] > ohlcv['MA50'], 1, -1)
print(ohlcv[ohlcv['Ticker']=='AAPL'][['Date','Close','MA20','MA50','crossover']].tail(10))
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
File c:\Users\makmw\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\indexes\base.py:3812, in Index.get_loc(self, key)
   3811 try:
-> 3812     return self._engine.get_loc(casted_key)
   3813 except KeyError as err:

File pandas/_libs/index.pyx:167, in pandas._libs.index.IndexEngine.get_loc()

File pandas/_libs/index.pyx:196, in pandas._libs.index.IndexEngine.get_loc()

File pandas/_libs/hashtable_class_helper.pxi:7088, in pandas._libs.hashtable.PyObjectHashTable.get_item()

File pandas/_libs/hashtable_class_helper.pxi:7096, in pandas._libs.hashtable.PyObjectHashTable.get_item()

KeyError: 'MA20'

The above exception was the direct cause of the following exception:

KeyError                                  Traceback (most recent call last)
Cell In[7], line 2
      1 ohlcv['MA50'] = ohlcv.groupby('Ticker')['Close'].transform(lambda x: x.rolling(window=50).mean()).round(2)
----> 2 ohlcv['crossover'] = np.where(ohlcv['MA20'] > ohlcv['MA50'], 1, -1)
      3 print(ohlcv[ohlcv['Ticker']=='AAPL'][['Date','Close','MA20','MA50','crossover']].tail(10))

File c:\Users\makmw\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\frame.py:4107, in DataFrame.__getitem__(self, key)
   4105 if self.columns.nlevels > 1:
   4106     return self._getitem_multilevel(key)
-> 4107 indexer = self.columns.get_loc(key)
   4108 if is_integer(indexer):
   4109     indexer = [indexer]

File c:\Users\makmw\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\indexes\base.py:3819, in Index.get_loc(self, key)
   3814     if isinstance(casted_key, slice) or (
   3815         isinstance(casted_key, abc.Iterable)
   3816         and any(isinstance(x, slice) for x in casted_key)
   3817     ):
   3818         raise InvalidIndexError(key)
-> 3819     raise KeyError(key) from err
   3820 except TypeError:
   3821     # If we have a listlike key, _check_indexing_error will raise
   3822     #  InvalidIndexError. Otherwise we fall through and re-raise
   3823     #  the TypeError.
   3824     self._check_indexing_error(key)

KeyError: 'MA20'

Example 6: Highlighting Buy and Sell Signals with Moving Averages (Intermediate)#

  • Combine price, MA20, MA50, and crossovers to visually highlight potential trading opportunities
  • Visualization makes it easier to see why signals were generated
signal_aapl = ohlcv[ohlcv['Ticker']=='AAPL'].copy()
plt.figure(figsize=(12,6))
plt.plot(signal_aapl['Date'], signal_aapl['Close'], label='AAPL Close', alpha=0.6)
plt.plot(signal_aapl['Date'], signal_aapl['MA20'], label='20D MA', color='orange')
plt.plot(signal_aapl['Date'], signal_aapl['MA50'], label='50D MA', color='green')
buy = signal_aapl[signal_aapl['crossover'] == 1]
sell = signal_aapl[signal_aapl['crossover'] == -1]
plt.scatter(buy['Date'], buy['Close'], marker='^', color='blue', label='Potential Buy', alpha=0.9)
plt.scatter(sell['Date'], sell['Close'], marker='v', color='red', label='Potential Sell', alpha=0.6)
plt.legend(loc='upper left')
plt.title('AAPL: 20/50-Day Moving Average Crossovers')
plt.xlabel('Date')
plt.ylabel('Price (USD)')
plt.tight_layout()
plt.show()
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
File c:\Users\makmw\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\indexes\base.py:3812, in Index.get_loc(self, key)
   3811 try:
-> 3812     return self._engine.get_loc(casted_key)
   3813 except KeyError as err:

File pandas/_libs/index.pyx:167, in pandas._libs.index.IndexEngine.get_loc()

File pandas/_libs/index.pyx:196, in pandas._libs.index.IndexEngine.get_loc()

File pandas/_libs/hashtable_class_helper.pxi:7088, in pandas._libs.hashtable.PyObjectHashTable.get_item()

File pandas/_libs/hashtable_class_helper.pxi:7096, in pandas._libs.hashtable.PyObjectHashTable.get_item()

KeyError: 'MA20'

The above exception was the direct cause of the following exception:

KeyError                                  Traceback (most recent call last)
Cell In[8], line 4
      2 plt.figure(figsize=(12,6))
      3 plt.plot(signal_aapl['Date'], signal_aapl['Close'], label='AAPL Close', alpha=0.6)
----> 4 plt.plot(signal_aapl['Date'], signal_aapl['MA20'], label='20D MA', color='orange')
      5 plt.plot(signal_aapl['Date'], signal_aapl['MA50'], label='50D MA', color='green')
      6 buy = signal_aapl[signal_aapl['crossover'] == 1]

File c:\Users\makmw\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\frame.py:4107, in DataFrame.__getitem__(self, key)
   4105 if self.columns.nlevels > 1:
   4106     return self._getitem_multilevel(key)
-> 4107 indexer = self.columns.get_loc(key)
   4108 if is_integer(indexer):
   4109     indexer = [indexer]

File c:\Users\makmw\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\indexes\base.py:3819, in Index.get_loc(self, key)
   3814     if isinstance(casted_key, slice) or (
   3815         isinstance(casted_key, abc.Iterable)
   3816         and any(isinstance(x, slice) for x in casted_key)
   3817     ):
   3818         raise InvalidIndexError(key)
-> 3819     raise KeyError(key) from err
   3820 except TypeError:
   3821     # If we have a listlike key, _check_indexing_error will raise
   3822     #  InvalidIndexError. Otherwise we fall through and re-raise
   3823     #  the TypeError.
   3824     self._check_indexing_error(key)

KeyError: 'MA20'
No description has been provided for this image

Example 7: Exponential Moving Average (EMA) (Advanced)#

  • EMA weights recent prices more heavily, offering faster trend response
  • EMAs are standard in many trading toolkits
aapl['EMA20'] = aapl['Close'].ewm(span=20, adjust=False).mean().round(2)
print(aapl[['Date','Close','MA20','EMA20']].tail(12))
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
Cell In[9], line 2
      1 aapl['EMA20'] = aapl['Close'].ewm(span=20, adjust=False).mean().round(2)
----> 2 print(aapl[['Date','Close','MA20','EMA20']].tail(12))

File c:\Users\makmw\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\frame.py:4113, in DataFrame.__getitem__(self, key)
   4111     if is_iterator(key):
   4112         key = list(key)
-> 4113     indexer = self.columns._get_indexer_strict(key, "columns")[1]
   4115 # take() does not accept boolean indexers
   4116 if getattr(indexer, "dtype", None) == bool:

File c:\Users\makmw\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\indexes\base.py:6212, in Index._get_indexer_strict(self, key, axis_name)
   6209 else:
   6210     keyarr, indexer, new_indexer = self._reindex_non_unique(keyarr)
-> 6212 self._raise_if_missing(keyarr, indexer, axis_name)
   6214 keyarr = self.take(indexer)
   6215 if isinstance(key, Index):
   6216     # GH 42790 - Preserve name from an Index

File c:\Users\makmw\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\indexes\base.py:6264, in Index._raise_if_missing(self, key, indexer, axis_name)
   6261     raise KeyError(f"None of [{key}] are in the [{axis_name}]")
   6263 not_found = list(ensure_index(key)[missing_mask.nonzero()[0]].unique())
-> 6264 raise KeyError(f"{not_found} not in index")

KeyError: "['MA20'] not in index"

Example 8: Bollinger Bands to Visualize Price Extremes (Advanced)#

  • Bollinger Bands use a moving average and price volatility to highlight extreme values
  • These are widely used for risk management and identifying possible overbought/oversold points
aapl['STD20'] = aapl['Close'].rolling(window=20).std()
aapl['BB_upper'] = aapl['MA20'] + 2*aapl['STD20']
aapl['BB_lower'] = aapl['MA20'] - 2*aapl['STD20']
plt.figure(figsize=(10,5))
plt.plot(aapl['Date'], aapl['Close'], label='AAPL Close', alpha=0.6)
plt.plot(aapl['Date'], aapl['MA20'], label='20D MA', color='black')
plt.fill_between(aapl['Date'], aapl['BB_upper'], aapl['BB_lower'], color='grey', alpha=0.2, label='Bollinger Band')
plt.legend(loc='upper left')
plt.title('AAPL with 20-Day Bollinger Bands')
plt.xlabel('Date')
plt.ylabel('Price (USD)')
plt.tight_layout()
plt.show()
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
File c:\Users\makmw\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\indexes\base.py:3812, in Index.get_loc(self, key)
   3811 try:
-> 3812     return self._engine.get_loc(casted_key)
   3813 except KeyError as err:

File pandas/_libs/index.pyx:167, in pandas._libs.index.IndexEngine.get_loc()

File pandas/_libs/index.pyx:196, in pandas._libs.index.IndexEngine.get_loc()

File pandas/_libs/hashtable_class_helper.pxi:7088, in pandas._libs.hashtable.PyObjectHashTable.get_item()

File pandas/_libs/hashtable_class_helper.pxi:7096, in pandas._libs.hashtable.PyObjectHashTable.get_item()

KeyError: 'MA20'

The above exception was the direct cause of the following exception:

KeyError                                  Traceback (most recent call last)
Cell In[10], line 2
      1 aapl['STD20'] = aapl['Close'].rolling(window=20).std()
----> 2 aapl['BB_upper'] = aapl['MA20'] + 2*aapl['STD20']
      3 aapl['BB_lower'] = aapl['MA20'] - 2*aapl['STD20']
      4 plt.figure(figsize=(10,5))

File c:\Users\makmw\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\frame.py:4107, in DataFrame.__getitem__(self, key)
   4105 if self.columns.nlevels > 1:
   4106     return self._getitem_multilevel(key)
-> 4107 indexer = self.columns.get_loc(key)
   4108 if is_integer(indexer):
   4109     indexer = [indexer]

File c:\Users\makmw\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\indexes\base.py:3819, in Index.get_loc(self, key)
   3814     if isinstance(casted_key, slice) or (
   3815         isinstance(casted_key, abc.Iterable)
   3816         and any(isinstance(x, slice) for x in casted_key)
   3817     ):
   3818         raise InvalidIndexError(key)
-> 3819     raise KeyError(key) from err
   3820 except TypeError:
   3821     # If we have a listlike key, _check_indexing_error will raise
   3822     #  InvalidIndexError. Otherwise we fall through and re-raise
   3823     #  the TypeError.
   3824     self._check_indexing_error(key)

KeyError: 'MA20'

Example 9: Handling Missing Data in Rolling Windows (Advanced)#

  • Real datasets sometimes have gaps in price or volume for certain dates
  • Moving averages ignore missing values, but can produce unexpected NaN output
# Simulate missing price for several days
aapl_missing = aapl.copy()
aapl_missing.loc[5:7, 'Close'] = np.nan
aapl_missing['MA10'] = aapl_missing['Close'].rolling(window=10).mean()
print(aapl_missing[['Date', 'Close', 'MA10']].head(15))
         Date       Close        MA10
0  2025-07-15  208.283691         NaN
5  2025-07-16         NaN         NaN
10 2025-07-17  209.190109         NaN
15 2025-07-18  210.345505         NaN
20 2025-07-21  211.640366         NaN
25 2025-07-22  213.552795         NaN
30 2025-07-23  213.303772         NaN
35 2025-07-24  212.915314         NaN
40 2025-07-25  213.034851         NaN
45 2025-07-28  213.204178         NaN
50 2025-07-29  210.435150         NaN
55 2025-07-30  208.223923  211.584596
60 2025-07-31  206.749771  211.340562
65 2025-08-01  201.580292  210.464041
70 2025-08-04  202.546463  209.554651

Debugging: Avoiding Mistakes with Multi-Ticker Data#

  • Never compute moving averages across all stocks at once
  • Group by ticker to keep calculations isolated
  • Watch for column name confusion with multi-ticker yfinance data
# BAD EXAMPLE: This would mix tickers if you forgot to groupby
bad_ma = ohlcv['Close'].rolling(window=10).mean()
print(bad_ma.dropna().head(5)) # Output is incorrect!
9     286.696016
10    286.786658
11    286.539658
12    286.697209
13    287.280547
Name: Close, dtype: float64

Best Practices for Financial Smoothing#

  • Always group by asset or ticker before applying rolling or smoothing
  • Choose window sizes to match your time horizon
  • Plot before trusting any signal
  • Watch out for boundary NaNs on small datasets
  • Use set seed to 42 for reproducible results when random numbers are required
# Example for reproducibility: setting random seed and showing a random selection
np.random.seed(42)
rand_dates = np.random.choice(aapl['Date'], 3, replace=False)
print(rand_dates)
['2026-02-25T00:00:00.000000000' '2025-07-23T00:00:00.000000000'
 '2026-03-10T00:00:00.000000000']

End-to-End Problem: Generating a Moving Average Buy/Sell Signal Table#

  • Start with real stock prices
  • Compute two moving averages
  • Create buy/sell signals
  • Output the complete signals table to CSV
signal_table = ohlcv[ohlcv['Ticker']=='MSFT'][['Date','Close']].copy()
signal_table['SMA20'] = signal_table['Close'].rolling(window=20).mean().round(2)
signal_table['SMA50'] = signal_table['Close'].rolling(window=50).mean().round(2)
signal_table['Signal'] = np.where(signal_table['SMA20'] > signal_table['SMA50'], 'Buy', 'Sell')
signal_table.dropna().to_csv('msft_signals.csv', index=False)
print(signal_table.dropna().head(7))
          Date       Close   SMA20   SMA50 Signal
248 2025-09-23  506.025269  503.36  508.31   Sell
253 2025-09-24  506.939423  503.76  508.42   Sell
258 2025-09-25  503.839081  503.78  508.46   Sell
263 2025-09-26  508.241180  503.87  508.47   Sell
268 2025-09-29  511.361389  504.26  508.58   Sell
273 2025-09-30  514.690369  504.90  508.75   Sell
278 2025-10-01  516.439331  505.61  509.06   Sell
# Clean up resources (optional practice step for good habits)
del ohlcv, aapl, signal_table
 

Found this useful?

All lessons, notebooks and datasets here are free. If they helped you, a coffee keeps new lessons coming.