Skip to content

haseeb709786/Python-Backtesting-Trading-Simulation

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

6 Commits
 
 
 
 

Repository files navigation

Python-Backtesting-Trading-Simulation

Python project that backtests and visualises an RSI based trading strategy on historical market data

This is NOT financial advice and this project is simply to document my programming journey.

REQUIREMENTS

Make sure you have Python 3.8 installed and then install the required packages:

pip install yfinance matplotlib mplcursors pandas

This is my 2nd project and it is a Python based backtesting tool that evaluates a trading strategy using the Relative Strength Index (RSI) on historical market data. It simulates trades and compares the strategy’s performance against a simple buy-and-hold approach.

  • Stock data is fetched using yfinance.
  • RSI and Moving Averages (50 and 200) are calculated.
  • Buy/Sell signals are generated. Buy if RSI drops to 30 (oversold), and sell when it reaches 70 (overbought).
  • Simulates portfolio performance and compares it against a traditional buy and hold strategy.
  • Visualises the results with charts.

Configuration

You can modify:

stock = yf.Ticker("NVDA")   #Change the stock. Get name from Yahoo Finance
daySelect = 365             # Select the number of days that is used to backtest
starting_capital = 1000     #choose starting cash

Code Explanation

The libraries which are used in this project.

import yfinance as yf
import matplotlib.pyplot as plt
import mplcursors

Change the style of the graphs. Find styles on https://matplotlib.org/stable/gallery/style_sheets/style_sheets_reference.html

plt.style.use("classic")

Function which calculates the RSI. This was sourced from https://wire.insiderfinance.io/calculate-rsi-with-python-and-yahoo-finance-c8fb78b1c199

def RSI(info, window=14, adjust=False):
    delta = info["Close"].diff(1).dropna() #Gets price difference and drops NaN values
    loss = delta.copy()
    gains = delta.copy()

    gains[gains < 0] = 0       # Only keep positive values
    loss[loss > 0] = 0         # Only keep negative values

    gain_ewm = gains.ewm(com=window - 1, adjust=adjust).mean()     #Uses ewm() to calculate the exponentially weighted moving average to smooth out the data. 
    loss_ewm = abs(loss.ewm(com=window - 1, adjust=adjust).mean()) # Same as previous line, but abs() is used to get the absolute value since it is negative

    RS = gain_ewm / loss_ewm   # Formula to calculate RSI
    RSI = 100 - 100 / (1 + RS)

    return RSI

Even though it won't all be used, all of the data is fetched using history() so that long term indicators like MA200 (requires 200 days of data) have enough data to be calculated.

info = stock.history(period="max")

Indicators are created. RSI is created using the RSI function earlier. Moving Averages are created by calculating the average ( .mean() ) of the close price (info["Close"]) of the past days selected ( .rolling() ). For example, .rolling(50) fetches the close price of the past 50 days to calculate the MA50.

info["RSI"] = RSI(info)
info["MA50"] = info["Close"].rolling(50).mean()
info["MA200"] = info["Close"].rolling(200).mean()

Buy and Sell signals are created. Close["Buy"] returns True if the RSI drops to 30 (oversold), and Close["Sell"] returns True if the RSI reaches 70 (oversold). These values are compared to the previous day using .shift(1) to ensure that a trade is only triggered on the day the signal actually crosses the threshold, rather than on every day the RSI is above 70 or below 30. This prevents multiple buys or sells in a row and makes the strategy respond to changes in momentum rather than sustained conditions.

info["Buy"] = (info["RSI"] < 30) & (info["RSI"].shift(1) >= 30)
info["Sell"] = (info["RSI"] > 70) & (info["RSI"].shift(1) <= 70)

.tail(daySelect) limits the data set to the number of days specified in daySelect. For example, if daySelect was 50, then display would contain the data for the past 50 days.

display = info.tail(daySelect)

Separates the days where the RSI triggers buy or sell signals. Using display[display[]], only the True values are kept.

buy_signal = display[display["Buy"]]
sell_signal = display[display["Sell"]]

Calculates how many shares could be bought at the start and tracks their value over time, providing a benchmark for performance.

initial_price = display["Close"].iloc[0]
shares_bh = starting_capital / initial_price
buy_and_hold_value = display["Close"] * shares_bh

Start with all cash, no shares, and an empty list to record the portfolio value over time.

capital = starting_capital
shares = 0
portfolio_value = []

If a Buy signal occurs and there is cash available, as many shares as possible are bought and cash is set to 0. If a sell signal occurs and there are shares held, all shares are sold and cash is updated. Each day, total value is calculated and stored in total_value which is then appended to portfolio_value.

for i in range(len(display)):
    row = display.iloc[i]
    close_price = round(row["Close"], 2)
    
    if row["Buy"] and capital > 0:
        shares = round((capital / close_price), 4) 
        capital = 0
        print(row.name.date(), "BUY:", shares, "shares at $", close_price, "Cash left: $",capital)
        
    elif row["Sell"] and shares > 0:
        capital = round((capital + (shares * close_price)), 2)
        print(row.name.date(), "SELL:", shares, "shares at $", close_price, "Cash: $",capital)
        shares = 0
    
    total_value = capital + shares * close_price
    portfolio_value.append(total_value)

Stores portfolio values in the display dataframe so it can be plotted in a graph afterwards. Calculates final value, profit and profit percentage.

display["Portfolio Value"] = portfolio_value
final_value = portfolio_value[-1]

profit = final_value - starting_capital
profit_percent = (profit / starting_capital) * 100

Print statements which compare the starting amount to the final portfolio value, as well as profit and profit percent. Round(num, 2) is used to improve readability.

print("Start Portfolio Value:", starting_capital)
print("Final Portfolio Value:", round(final_value, 2))
print("Profit: $", round(profit, 2))
print("Profit Percentage:", round(profit_percent, 2), "%")

Using madplotlib, 3 graphs are plotted. ax1 displays the Price Chart, and shows the price through the days as well as the Moving Averages indicators. ax2 displays the RSI Chart, and ax3 displays the Portfolio Value chart, and compares the RSI strategy with the Buy and Hold strategy.

fig, (ax1, ax2, ax3) = plt.subplots(3, 1, sharex=True)


ax1.plot(display["Close"], "blue", label="Close")
ax1.plot(display["MA50"], "green", label="MA50")
ax1.plot(display["MA200"], "red", label="MA200")
ax1.set_title("Price Chart")
ax1.legend(loc="upper left", fontsize=10, framealpha = 0.3)

ax2.plot(display["RSI"], "blue", label="RSI")
ax2.axhspan(0, 30, facecolor = "green", alpha = 0.3)
ax2.axhspan(70, 100, facecolor = "red", alpha = 0.3)
ax2.set_title("RSI Chart")
ax2.set_ylim(0, 100)

ax3.plot(display["Portfolio Value"], label="RSI Strategy")
ax3.plot(buy_and_hold_value, label="Buy and Hold")
ax3.set_title("Portfolio Value Comparison")
ax3.legend(loc="upper left", fontsize=10, framealpha = 0.3)

plt.xticks(rotation=45)
plt.tight_layout()

#mplcursors.cursor(hover = True)

plt.show()

About

Python project that backtests and visualises an RSI based trading strategy on historical market data

Topics

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages