top of page

Algorithmic Trading in Python Code - Mean Reversion Strategy

Updated: Dec 22, 2023

On my YouTube channel I have a Algorithmic Trading Course series where we use python and create a mean reversion inspired trading algorithm. The algorithm provides buy and sell signals along with backtesting results. For your convenience, I have provided the complete jupyter notebook with all the required code here!


Algorithmic Trading Python Code Here for Intrendias Essentials Members (Free Tier)!


If the download does not work, please refer to the raw code below - Thank you!


# Import lines 
# Yahoo Finance 
import yfinance as yf
import yahoo_fin.stock_info as yaf

# for statistics
import statistics as st

#Visuals, Dataframe, Calculations 
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt

#Time and Date 
from datetime import datetime, timedelta, date
import time

# Custom Variables ------------------------------------------------------------
currentDay = datetime.now().day
currentMonth = datetime.now().month
currentYear = datetime.now().year
currentDate = datetime(currentYear, currentMonth, currentDay)
#Add one day to currentDate (when performing algorithm the last day is cut off)
currentDate = currentDate + timedelta(days=1)
currentDate = currentDate.strftime('%Y-%m-%d')

pastTwoYears = currentYear - 2
pastMonthDay = '01-01'
pastDate = str(pastTwoYears)+'-'+str(pastMonthDay)

# Custom Functions ------------------------------------------------------------
def percent_change_symbol(change):
    '''
    Parameters
    ----------
    change : float
        Intended as the percent change in asset price.

    Returns
    -------
    arrow : string
        Returns an up arrow for positive percent change; downarrow if negative
        return; dash if percent change is zero.
    '''
    
    if change < 0:
        arrow = '▼'
    elif change > 0: 
        arrow = '▲'
    else:
        arrow = '-'
    return arrow

def get_quote(ticker):
    '''
    Parameters
    ----------
    ticker : string
        User enters the ticker or abbreviation that represents the financial asset
        to query from yahoo finance. This function uses the yahoo_fin.stock_info 
        library.

    Returns
    -------
    ticker : string
        Returns the user entered ticker or abbreviation.
    quote : float
        Current price of the financial asset.
    change : string
        Current percent change of the financial asset with "%" and arrow string.
    '''
    table = yaf.get_quote_table(ticker)
    quote = table.get('Quote Price')
    prevc = table.get('Previous Close')
    per_chg = round(((quote - prevc)/prevc)*100,2)
    arrow = percent_change_symbol(per_chg)
    change = str(per_chg)+'% '+arrow
    quote = "{:,}".format(round(quote,2))
    
    #404-Client Error 10/30/2023
    #information = yf.Ticker(ticker)
    #company_name = information.info['longName']
    company_name = ticker 
    return company_name, quote, change

# AVERAGE TRUE RANGE FOR STOP LOSS --------------------------------------------
def create_atr(df, period=5):
    '''
    Parameters
    ----------
    df : data frame
        data frame with High, Low, and Close of a financial asset to calculate
        Average True Range for ATR and Stop Loss.
    period : TYPE, optional
        DESCRIPTION. The default is 5.

    Returns
    -------
    df : dataframe
        returns the original data frame plus a constant ATR value column representing the stop loss line if 
        one were to enter a trade on the end_date.

    '''
    high = df['High']
    low = df['Low']
    prev_close = df['Close'].shift(1)
    
    tr_values = np.maximum.reduce([
        high - low,
        np.abs(high - prev_close),
        np.abs(low - prev_close)
    ])
    
    atr = pd.Series(tr_values).rolling(window=period).mean()
    atr = atr.iloc[-1]
    df['atr'] = atr 
    # Assuming you have 'daily' DataFrame with 'High', 'Low', and 'Close' columns
    df['StopLoss'] = df['Close'].iloc[-1] - (df['atr'].iloc[-1] * 2)
    return df 

# EMA and BOLLINGER BAND Function ---------------------------------------------
# Technical Analysis Functions ################################################
def ema(df, field, period):
    '''
    Parameters
    ----------
    dataframe : pandas data frame
        Data frame with column user wants to perform Exponential Moving Average 
        calculation on.
    field : string
        The field name of the data frame the user wants to calculate ema on.
    period : int
        The number of periods for ema calculation.

    Returns
    -------
    ema : pandas Series float64
        The pandas series housing the ema calulation.

    '''
    ema = df[f'{field}'].ewm(span=period, adjust=False).mean()
    return ema

def create_ema_and_bollinger_bands(df, period = 20):
    '''
    Parameters
    ----------
    df : data frame
        data frame to create ema and bollinger bands columns in.

    Returns
    -------
    df : data frame 
        data frame with original columns plus ema and bollinger bands lower and upper.
    '''
    df[f'exp{period}']  = ema(df, 'Close', period)
    # Calculate the standard deviation
    df[f'std_dev{period}'] = df[f'exp{period}'].std()
    # Calculate the upper Bollinger Band
    df[f'upper_band{period}'] = df[f'exp{period}'] + 1 * df[f'std_dev{period}']
    # Calculate the lower Bollinger Band
    df[f'lower_band{period}'] = df[f'exp{period}'] - 1 * df[f'std_dev{period}']
    return df 

# Back testing function compare Bought Held versus Algorithm performance ------
def back_test(df):
    # Bought and Held Performance -------------------------------------------------------
    # CALCULATE: WHAT IF I BOUGHT AND HELD 
    balance = 10000 #Assume starting investment of $10,000
    df['Bought_And_Held'] = df['Close'] / df['Close'].iloc[0] * balance
    Bought_And_Held_Return = df['Bought_And_Held'].iloc[-1]
    Bought_And_Held_Percent = round(((Bought_And_Held_Return - balance)/balance)*100,2)
    Bought_And_Held_Percent_Sign = percent_change_symbol(Bought_And_Held_Percent)
    Bought_And_Held_Return = f"${Bought_And_Held_Return:,.2f}"
    Bought_And_Held_Percent = f"{Bought_And_Held_Percent}% {Bought_And_Held_Percent_Sign}"
    
    win_count = 0
    total_trades = 0
    previous_action = None 
    
    # Back testing Algorithm performance
    buy_sell_returns = [] 
    stocks_held = 0 
    for _,row in df.iterrows():
        if row['action'] == 'Buy':
            stocks_bought = balance / row['Close']
            balance = 0
            stocks_held += stocks_bought
        elif row['action'] == 'Sell':
            balance += stocks_held * row['Close']
            stocks_held = 0
        elif row['action'] == 'Hold':
            pass
        
        buy_sell_returns.append(balance + stocks_held * row['Close'])
    df['Buy_Sell_Return'] = buy_sell_returns
    balance = 10000 #Assume starting investment of $10,000
    Buy_Sell_Return = df['Buy_Sell_Return'].iloc[-1]
    Max_Draw_Down = min(df['Buy_Sell_Return'])
    Max_Draw_Down_Percent = round(((Max_Draw_Down - balance)/balance)*100,2)
    Max_Draw_Down_Percent_Sign = percent_change_symbol(Max_Draw_Down_Percent)
    Max_Draw_Down = f"${Max_Draw_Down:,.2f}"
    Max_Draw_Down_Percent = f"{Max_Draw_Down_Percent}% {Max_Draw_Down_Percent_Sign}"
    Buy_Sell_Percent_Return = round(((Buy_Sell_Return - balance)/balance)*100,2)
    Buy_Sell_Percent_Return_Sign = percent_change_symbol(Buy_Sell_Percent_Return)
    Buy_Sell_Return = f"${Buy_Sell_Return:,.2f}"
    Buy_Sell_Percent_Return = f"{Buy_Sell_Percent_Return}% {Buy_Sell_Percent_Return_Sign}"
    
    return df, Bought_And_Held_Return, Bought_And_Held_Percent, Buy_Sell_Return, Buy_Sell_Percent_Return, Max_Draw_Down, Max_Draw_Down_Percent

# Algorithms for trading ------------------------------------------------------
def mean_reversion_algorithm(ticker, start_date = pastDate, end_date = currentDate, interval = '1d'):
    '''
    Parameters
    ----------
    ticker : string
        the ticker or abbreviation of the financial asset.
    start_date : date, optional
        the start date for historic data. The default is pastDate.
    end_date : date, optional
        the end date for historic data. The default is currentDate.

    Returns
    -------
    df : data frame
        stores the yahoo finance high, low, open, close, adj close, and vol
        along with the technical analysis and buy, sell, or hold decision
        based on the mie algorithm.
    Bought_And_Held_Return : str
        The dollar value of the bought and held strategy.
    Bought_And_Held_Percent : str
        The percent change of the bought and held strategy.
    Buy_Sell_Return : str
        The dollar value of the mie algorithm strategy.
    Buy_Sell_Percent_Return : str
        The percent change of the mie algorithm strategy.
    '''
    start_time = time.time() 
    # Gather historic prices from yahoo finance
    try:
        df = yf.download(ticker, start_date, end_date, interval)
    
    except:
        error = f'{ticker} could not be found. Symbol may be incorrect or delisted. Please consult Yahoo Finance symbols.'
        return error
    
    company_name, last_price, last_change = get_quote(ticker)
    
    #technical analysis -------------------------------------------------------
    df = create_ema_and_bollinger_bands(df,period = 128) 
    df = create_atr(df, 5)
    
    #buy sell signal logic ----------------------------------------------------
    buy_signal = []
    sell_signal = []
    action = []
    
    # Extract relevant columns for calculations
    low = df['Low']
    high = df['High']
    close = df['Close']
    bollingerlower = df['lower_band128']
    bollingerupper = df['upper_band128']
    
    for i in range(len(df)):
        # gathering data points one day at a time
        low_val = low[i]
        high_val = high[i] 
        close_val = close[i]
        bollingerlower_val = bollingerlower[i]
        bollingerupper_val = bollingerupper[i]
        
        bullish_mean_reversion = close_val < bollingerlower_val 
        
        bearish_mean_reversion = close_val > bollingerupper_val 
        
        if bullish_mean_reversion: 
            buy_signal.append(low_val * 0.95) #ensures the buy signal does not overlap the candle stick
            sell_signal.append(np.nan) #null for sell signal since this is a buy signal scenario 
            action.append('Buy')
            
        elif bearish_mean_reversion: 
            buy_signal.append(np.nan)
            sell_signal.append(high_val * 1.05) #ensures the sell signal does not overlap the candle stick 
            action.append('Sell')
            
        else:
            #none of the criteria from above occured so the action is Hold
            buy_signal.append(np.nan)
            sell_signal.append(np.nan)
            action.append('Hold')
            
    
    df['buy_signal'] = buy_signal
    df['sell_signal'] = sell_signal
    df['action'] = action
    
    # Back testing Algorithm performance
    df, Bought_And_Held_Return, Bought_And_Held_Percent, Buy_Sell_Return, Buy_Sell_Percent_Return, Max_Draw_Down, Max_Draw_Down_Percent = back_test(df)
    
    end_time = time.time()
    execution_time = str(round(end_time - start_time,2))+' seconds'
    
    df_stats = {
        f'{ticker}':[company_name],
        'Last Price':[last_price],
        'Last % Chg':[last_change],
        'Assume Initial Investment':['$10,000'],
        f'{ticker} $ Performance':[Bought_And_Held_Return],
        f'{ticker} % Performance':[Bought_And_Held_Percent],
        'Intrendias $ Performance':[Buy_Sell_Return],
        'Intrendias % Performance':[Buy_Sell_Percent_Return],
        'Max $ Draw Down':[Max_Draw_Down],
        'Max % Draw Down':[Max_Draw_Down_Percent],
        'Algorithm Speed':[execution_time]
    }
    
    df_stats = pd.DataFrame(df_stats)
    df_stats = df_stats.T
    # Reset the index and make it a new column
    df_stats = df_stats.reset_index()
    df_stats = df_stats.rename(columns={0: 'Data', 'index':'Info'})
    return df, df_stats, action, Bought_And_Held_Percent, Buy_Sell_Percent_Return

df, df_stats, action, Bought_And_Held_Percent, Buy_Sell_Percent_Return = mean_reversion_algorithm(
                                                                            ticker = 'AAPL', 
                                                                            start_date = pastDate, 
                                                                            end_date = currentDate, 
                                                                            interval = '1d'
                                                                        )
df

def chart_from_mean_reversion(ticker, start_date, end_date, interval = '1d'):
    df, df_stats, action, Bought_And_Held_Percent, Buy_Sell_Percent_Return = mean_reversion_algorithm(
                                                                                ticker = ticker, 
                                                                                start_date = start_date, 
                                                                                end_date = end_date, 
                                                                                interval = interval
                                                                            )
    columns = [{'name': col, 'id': col} for col in df_stats.columns]
    data = df_stats.to_dict('records')
    
    # BUY SELL SIGNALS 
    # Plot the candlestick chart
    # Plotting the candlestick chart
    fig, ax = plt.subplots(figsize=(20, 6))

    # Candlestick plot
    ax.plot(df.index, df['Open'], label='Open', color='black', linestyle='dashed', linewidth=1)
    ax.plot(df.index, df['Close'], label='Close', color='black', linestyle='dashed', linewidth=1)
    ax.vlines(df.index, df['Low'], df['High'], color='black')

    # Scatter plot for buy signals
    buy_signals = df[df['action'] == 'Buy']
    ax.scatter(buy_signals.index, buy_signals['Low']*0.95, marker='^', color='green', label='Buy Signal')

    # Scatter plot for sell signals
    sell_signals = df[df['action'] == 'Sell']
    ax.scatter(sell_signals.index, sell_signals['High']*1.05, marker='v', color='red', label='Sell Signal')

    # Set labels and title
    ax.set_xlabel('Date')
    ax.set_ylabel('Price')
    ax.set_title('Candlestick Chart with Buy/Sell Signals')

    # Display legend
    ax.legend()

    # Show the plot
    plt.show()

    # Plot stock and algorithm performance
    fig2, ax2 = plt.subplots(figsize=(20, 6))

    ax2.plot(df.index, df['Bought_And_Held'], label=f'{ticker}', color='gray')
    ax2.plot(df.index, df['Buy_Sell_Return'], label='Intrendias', color='blue')

    ax2.set_title(f'Performance for {ticker} between {pastDate} and {currentDate}')
    ax2.set_xlabel('Date')
    ax2.set_ylabel('Price')
    ax2.legend()

    plt.show()

chart_from_mean_reversion(ticker = 'TSLA', start_date = pastDate, end_date = currentDate, interval = '1d')

What is Mean Reversion?

Mean reversion is a financial concept that refers to the tendency of a financial instrument's price, such as a stock, bond, or commodity, to move toward the average or mean over time. In other words, when the price of an asset deviates significantly from its historical average, there is an expectation that it will eventually revert to its average or historical mean.


This concept is often applied in various financial and economic contexts, and it's based on the belief that extreme price movements are temporary and that the price will eventually move back towards its long-term average or trend. Traders and investors who follow mean reversion strategies may take positions based on the expectation that if an asset's price has moved too far from its historical average, it is likely to move back in the opposite direction.


It's important to note that mean reversion is a general concept, and the specific mechanisms behind price movements can vary depending on the asset, market conditions, and other factors. While mean reversion can be observed in some financial markets, it's not a universal law, and prices can also exhibit trends or momentum over extended periods.


Real Life Example of Mean Reversion

Below we see the candle chart for Delta stock DAL on the one day interval. The chart also has the 128 day Simple Moving Average (SMA) with upper and lower Bollinger Bands.


What is a Simple Moving Average?

A Simple Moving Average or SMA is a commonly used statistical calculation that is used to analyze data points by creating a series of averages of different subset of the full data set - in this case the close price of DAL stock.


What are Bollinger Bands?

Bollinger Bands are technical analysis tools introduced by John Bollinger. They consist of a set of three bands plotted on a price chart. These bands are based on the volatility of a financial instrument and are used to analyse and identity portential overbought or oversold conditions, as well as to gauge the likelihood of future price movements. The three components of the Bollinger Bands are SMA, Upper band, and Lower band where the bands are calculated by adding a specified number of standard deviations (usually two).



Mean reversion using Bollinger bands
Delta Stock DAL and Bollinger Bands


Here we see instances where the close price return back to the long-term mean. When the close price of Delta surpasses the upper bollinger band there is a repeated tendancy to return downward; when the stock price dips below the lower bollinger band, the price has a bullish return upward.


The code above ends with a python function where the user can input the financial asset's symbol like a stock ticker "AAPL" or crypto symbol "BTC-USD" to return buy and sell signals and backtesting results. Happy coding! If you enjoy algorithmic trading and coding in python, then consider subscribing to the YouTube channel to not miss another video! Thank you for reading and using Intrendias!



Helpful Tutorial Series?

  • Yes

  • No


4 Comments


wu han
wu han
Apr 05, 2024

My issue fixed after downgrading panda by running

pip install "pandas<2"


So it is working. Thank you intrendias

Like

wu han
wu han
Apr 05, 2024

table = yaf.get_quote_table(ticker) failed


--------------------------------------------------------------------------- AttributeError                            Traceback (most recent call last) ~\AppData\Local\Temp\ipykernel_35748\2554079129.py in ?() ----> 1 df, df_stats, action, Bought_And_Held_Percent, Buy_Sell_Percent_Return = mean_reversion_algorithm(       2                                                                             ticker = 'AAPL',       3                                                                             start_date = pastDate,       4                                                                             end_date = currentDate, ~\AppData\Local\Temp\ipykernel_35748\1035753292.py in ?(ticker, start_date, end_date, interval)      33     except:      34         error = f'{ticker} could not be found. Symbol may be incorrect or delisted. Please consult Yahoo Finance symbols.'      35         return error      36  ---> 37     company_name, last_price, last_change = get_quote(ticker)      38       39     #technical analysis -------------------------------------------------------      40     df = create_ema_and_bollinger_bands(df,period = 128) ~\AppData\Local\Temp\ipykernel_35748\471745816.py in ?(ticker)      15         Current price of the financial asset.      16     change : string      17         Current percent change of the financial asset with "%" and arrow string.      18     ''' ---> 19     table = yaf.get_quote_table(ticker)      20     quote = table.get('Quote Price')      21     prevc = table.get('Previous Close')      22     per_chg = round(((quote - prevc)/prevc)*100,2) C:\anaconda3\Lib\site-packages\yahoo_fin\stock_info.py in ?(ticker, dict_result, headers)     291     site = "https://finance.yahoo.com/quote/" + ticker + "?p=" + ticker     292      293     tables = pd.read_html(requests.get(site, headers=headers).text)     294  --> 295     data = tables[0].append(tables[1])     296      297     data.columns = ["attribute" , "value"]     298  C:\anaconda3\Lib\site-packages\pandas\core\generic.py in ?(self, name)    5985             and name not in self._accessors    5986             and self._info_axis._can_hold_identifiers_and_holds_name(name)    5987         ):    5988             return self[name] -> 5989         return object.__getattribute__(self, name)…


Like

CS Chan
CS Chan
Dec 20, 2023

Hi, it seen like the download link is invalid. can not get it downloaded. Any clues ?

Like
Stephens Systems
Stephens Systems
Dec 20, 2023
Replying to

Hello, thank you for commenting the issue and I'm sorry you're experiencing difficulties. I republished this blog post, and the download is successful again. Please refresh the website and attempt to download again. Thank you for using Intrendias!

Like
bottom of page