Backtesting Trading Strategies with Python Pandas
This article explores how to use Python Pandas to backtest trading strategies. By leveraging EODHD APIs for historical data and implementing a simple EMA crossover strategy, we demonstrate how to simulate trades and evaluate performance over time.
Using Python Pandas to Backtest Algorithmic Trading Strategies
Countless trading strategies and candlestick patterns are available, but how can you evaluate their effectiveness? This is where a financial data API, such as the one provided by EODHD APIs, becomes invaluable. It allows you to simulate the performance of a strategy or candlestick pattern using historical data.
We aimed to develop a backtesting framework leveraging the Pandas library for Python, commonly used in data science. After creating a proof of concept, we found it to be highly effective.
Let’s walk through the thought process and steps involved in creating this framework.
What Will We Need?
- The official EODHD Python library.
- Trading data converted into a Pandas dataframe (including date, open, high, close, low, and volume).
- Configurable test settings (balance_base, account_balance, buy_order_quote).
- Methods to apply our buy and sell signals to our data (set_buy_signals, set_sell_signals).
- The ability to iterate through our data (in this case, our Pandas dataframe).
- A logging mechanism to record the outcome of each completed order.
Building the Code
To begin, create a file named “pandas-bt.py” and import the “eodhd” library. Whether or not you already have the “eodhd” library installed, it’s advisable to reinstall it to ensure you have the latest version.
% python3 -m pip install eodhd -U
The initial code will look like this:
# pandas-bt.py
import sys
import pandas as pd
from eodhd import APIClient
def get_ohlc_data() -> pd.DataFrame:
"""Return a DataFrame of OHLC data"""
api = APIClient("<Your_API_Key>")
df = api.get_historical_data(symbol="HSPX.LSE", interval="d", iso8601_start="2020-01-01", iso8601_end="2023-09-08")
df.drop(columns=["symbol", "interval", "close"], inplace=True)
df.rename(columns={"adjusted_close": "close"}, inplace=True)
print(df)
return pd.DataFrame([], columns=["date", "open", "high", "low", "close"])
def main() -> int:
"""Backtest a strategy using pandas"""
df = get_ohlc_data()
print(df)
return 0
if __name__ == '__main__':
sys.exit(main())
929 days of S&P 500 data should be enough.
Trading Strategy
This approach is compatible with any trading strategy or candlestick pattern. The key is to introduce a new feature called “buy_signal” that assigns a value of 1 when a buy signal is present and 0 otherwise. Similarly, we’ll add a “sell_signal” feature with the same logic.
For this demonstration, we’ll utilize an EMA12/EMA26 crossover strategy.
def get_ohlc_data() -> pd.DataFrame:
"""Return a DataFrame of OHLC data"""
api = APIClient("<Your_API_Key>")
df = api.get_historical_data(symbol="HSPX.LSE", interval="d", iso8601_start="2020-01-01", iso8601_end="2023-09-08")
df.drop(columns=["symbol", "interval", "close"], inplace=True)
df.rename(columns={"adjusted_close": "close"}, inplace=True)
return df
def main() -> int:
"""Backtest a strategy using pandas"""
df = get_ohlc_data()
df["ema12"] = df["close"].ewm(span=12, adjust=False).mean()
df["ema26"] = df["close"].ewm(span=26, adjust=False).mean()
df["ema12gtema26"] = df["ema12"] > df["ema26"]
df["buy_signal"] = df["ema12gtema26"].ne(df["ema12gtema26"].shift())
df.loc[df["ema12gtema26"] == False, "buy_signal"] = False # noqa: E712
df["buy_signal"] = df["buy_signal"].astype(int)
df["ema12ltema26"] = df["ema12"] < df["ema26"]
df["sell_signal"] = df["ema12ltema26"].ne(df["ema12ltema26"].shift())
df.loc[df["ema12ltema26"] == False, "sell_signal"] = False # noqa: E712
df["sell_signal"] = df["sell_signal"].astype(int)
df.drop(columns=["ema12", "ema26", "ema12gtema26", "ema12ltema26"], inplace=True)
print(df)
return 0
We now have an OHLC dataset for the S&P 500 daily, with the “buy_signal” feature set to 1 whenever the EMA12 crosses above the EMA26, and a “sell_signal” feature set to 1 whenever the EMA12 crosses below the EMA26.
Backtesting the Strategy
Now we need to iterate through the dataset and simulate the trading orders. Add the following code in place of the “print(df)” line, and above the “return 0” statement.
balance_base = 0
account_balance = 1000
buy_order_quote = 1000
is_order_open = False
orders = []
sell_value = 0
for index, row in df.iterrows():
if row["buy_signal"] and is_order_open == 0:
is_order_open = 1
if sell_value < 1000 and sell_value > 0:
buy_order_quote = sell_value
else:
buy_order_quote = 1000
buy_amount = buy_order_quote / row["close"]
balance_base += buy_amount
account_balance += sell_value
order = {
"timestamp": index,
"account_balance": account_balance,
"buy_order_quote": buy_order_quote,
"buy_order_base": buy_amount
}
account_balance -= buy_order_quote
if row["sell_signal"] and is_order_open == 1:
is_order_open = 0
sell_value = buy_amount * row["close"]
balance_base -= buy_amount
order["sell_order_quote"] = sell_value
order["profit"] = order["sell_order_quote"] - order["buy_order_quote"]
order["margin"] = (order["profit"] / order["buy_order_quote"]) * 100
orders.append(order)
print(index)
df_orders = pd.DataFrame(orders)
print(df_orders)
Explanation of Parameters
- balance_base: This represents the initial quantity of the stock we have at the start of the simulation, which is set to 0.
- account_balance: This is the initial amount of quote currency we have for the simulation, set to £1000.
- buy_order_quote: This is the requested size of the buy order, set to £1000. If the account balance is less than £1000, the order size will be whatever amount is available in the account.
Evaluating the Backtest Results
This simulation demonstrates that if we traded every EMA12/EMA26 crossover from January 3, 2020, to September 16, 2023, we would have turned our initial £1000 into £1212.40. This translates to a 21.24% return over 2.75 years. While this return might not seem extraordinary, it is still a reasonable performance.
It’s important to note that these results do not account for exchange fees. To obtain a more accurate assessment, you should factor in the relevant exchange fees.
The main takeaway is that you can apply any trading strategy using this framework to evaluate its performance over the same period. Once you’ve identified a winning strategy, you can consider applying it in real trading scenarios 🙂
Please note that this article is for informational purposes only and should not be taken as financial advice. We do not bear responsibility for any trading decisions made based on the content of this article. Readers are advised to conduct their own research or consult with a qualified financial professional before making any investment decisions.
For those eager to delve deeper into such insightful articles and broaden their understanding of different strategies in financial markets, we invite you to follow our account and subscribe for email notifications.
Stay tuned for more valuable articles that aim to enhance your data science skills and market analysis capabilities.