BackTesting Strategy Setup: Building a Python Trading Strategy Analyzer | Technology

kamal chanchal
9 min readNov 25, 2023

--

In this post, we’ll explore a comprehensive Python project designed for backtesting trading strategies using historical data from the Indian Stocks Market. The project adopts a clean and modular design through object-oriented programming (OOP) principles, making it an ideal platform for strategy testing and analysis.

BackTesting
Post Trade Analysis Metrics

Overview

Our Python-based backtesting project revolves around historical OHLCV (Open, High, Low, Close, Volume) data sourced from Finvasia Broker (Shoonya Broker). The focus on object-oriented programming ensures a robust, scalable, and maintainable codebase. Let’s delve into the key features that make this project stand out.

Features

1. Data Source

The backbone of our backtesting project is the historical data obtained from Finvasia Broker, providing accurate and reliable market information.

2. OOP Structure

We believe in the power of clean code. The project is meticulously organized using OOP concepts, making it easier to understand, modify, and extend.

Python Class which handles Financial Data

class OHLCV:
time ="time"
into="into"
inth="inth"
intl ="intl"
intc="intc"
intvwap="intvwap"
v="v"
intoi="intoi"
oi="oi"

Python Time Series Conversion Class:

class DateTimeFromat :
dd_mm_yyyy__HH_MM_SS = "%d-%m-%Y %H:%M:%S"

Class which is responsible to get financial data in CSV Format

class DataPath :
filename = "NIFTY_Intraday.csv"
path = ".//Data//"+filename+".csv"

3. Financial Indicators

To enhance the depth of analysis, we leverage third-party libraries for calculating financial indicators such as RSI, MACD, SMA, EMA, and more.

from stock_indicators import CandlePart
from stock_indicators import Quote
from stock_indicators import indicators

4. Smart Statistics

The project generates sophisticated statistics for trade generation and trade life, offering valuable insights into strategy performance.

import QuantAlgo1 as Q

if __name__ == '__main__' :
print( "starting.." )
run1 = Q.IndexAnalysis( )
run1.Run_StrategyAnalysis( print_tocsv = True )
run1.Summary( )

5. Trade Metrics

We go beyond basic metrics. Our back testing framework provides metrics like Probability of Win Trade, Probability of Loss Trade, Total Return(s) Point, Total Points Lost, Total Points Earned, Total Trades, Total Loss Trades, and Total Profit Trades.

class ReportFile :
TradeDate = "TradeDate"
TotalTrades = "TotalTrades"
MaxProfit = "MaxProfit"
MaxLoss = "MaxLoss"
NoOf_ProfitTrades = "NoOf_ProfitTrades"
NoOf_LossTrades = "NoOf_LossTrades"
Profit_Loss_Ratio = "Profit_Loss_Ratio"
Total_PointGained = "Total_PointGained"
Total_Return = "Total_Return"
Total_Loss_point = "Loss_Point"
Intraday Wise Trades Statistics
Summary of BT(Back testing) Test

The Generate_Trade_Signal function is a crucial component of our backtesting project designed for analyzing trading strategies in the Indian Stock Market. This function is responsible for generating trade signals based on the provided historical OHLCV data. Let's explore the key aspects and functionality of this function.

The function calls the StartTrading method, passing the filtered data for the current trading date. This method is responsible for executing the trading strategy and analyzing the results.

 def Generate_Trade_Signal( self ) :
# for idx ,row in self.df_Ohlc.iterrows():

# datetime = row[fd.OHLCV.time].astype() strftime("%Y-%m-%d %H:%M:%S")
# print(row[fd.OHLCV.time] )
# datetime_str = datetime.datetime.strptime( row[fd.OHLCV.time], '%d-%m-%Y %H:%M:%S' )

# print(row[fd.OHLCV.time].date())
# datetime_str = datetime.datetime.strptime( row[fd.OHLCV.time], FinancialData.DateTimeFromat.dd_mm_yyyy__HH_MM_SS )
# print(datetime_str)

# for idx ,row in self.df_Ohlc.iterrows():
# d= str(row[ fd.OHLCV.time ])[0:10]
# # print(d)

print( "Total Trading Day : " , len( self.List_Distinct_TradeDate ) )
for trading_date in self.List_Distinct_TradeDate :
""" Get back Data"""
print( "Proceeding Back testing for : " , trading_date )
# mask = pd.to_datetime( self.df_Ohlc[fd.OHLCV.time]).date() == trading_date
mask = self.df_Ohlc[ fd.OHLCV.time ].astype( str ).str[ 0 :10 ] == str( trading_date )
# replace('-','')
df_feed = self.df_Ohlc.loc[ mask ].dropna( )
# self.StartTrading( df_feed.sort_index( ascending = False ) , trading_date )
self.StartTrading( df_feed , trading_date )

# df_feed.head()
# print( len( df_feed ) )

# df_feed = self.df_Ohlc.where( self.df_Ohlc[fd.OHLCV.time].astype(datetime).data() == trading_date)

print( "Trade Bank Complted" )

Initiate Trading

    def StartTrading( self , df , trddate ) :
# print(len( df))
# convert to List
# quotes_list = [
# Quote( d , o , h , l , c , v )
# for d , o , h , l , c , v
# in zip( df[ fd.OHLCV.time] , df[ fd.OHLCV.into] , df[ fd.OHLCV.inth ] , df[ fd.OHLCV.intl ] , df[ fd.OHLCV.intc ] , df[ fd.OHLCV.v ] )
# ]

df_exchange = pd.DataFrame( data = None , columns = self.df_Trades.columns )

quotes_list = [ ]
m_open = 0
m_close = 0
m_high = 0
m_low = 0

m_prev_open = 0

flag_open = True

prev_candle_ohlc4 = 0

for id , bar in df.iterrows( ) :
# quotes_list.insert(bar[fd.OHLCV.time],bar[fd.OHLCV.into],bar[fd.OHLCV.inth],bar[fd.OHLCV.intl],bar[fd.OHLCV.intc],bar[fd.OHLCV.v])

open = bar[ fd.OHLCV.into ]
high = bar[ fd.OHLCV.inth ]
low = bar[ fd.OHLCV.intl ]
close = bar[ fd.OHLCV.intc ]
Ltp = bar[ fd.OHLCV.into ]

if flag_open == True :
# print( "First Tick Update" )
m_open = open
m_high = high
m_low = low
m_close = close
flag_open = False

if m_low > low :
m_low = low

if m_high < high :
m_high = high

m_close = Ltp
_entrytime = bar[ fd.OHLCV.time ].time( )

currenttime = bar[ fd.OHLCV.time ].time( ).strftime( "%H%M%S" )
# print(currenttime)

""" Exit or Update PnL of The Open tardes"""
self.ExitTrades_update_pnl( df_exchange , Ltp , currenttime , trddate )


inttime = int( str( currenttime ) )
print( "BT Processing Time : " , inttime )

if inttime > 151500 :
continue

q = Quote( bar[ fd.OHLCV.time ] , bar[ fd.OHLCV.into ] , bar[ fd.OHLCV.inth ] , bar[ fd.OHLCV.intl ] ,
bar[ fd.OHLCV.intc ] , bar[ fd.OHLCV.v ]
)
quotes_list.append( q )

results = indicators.get_sma( quotes_list , 5 , CandlePart.CLOSE )
# results_rsi = indicators.get_rsi( quotes_list , 10 )

final_res = results[ -1 ].sma
# rsi = results_rsi[ -1 ].rsi
rsi = 0

mess = " TradeDate : "+str( trddate )+" Time"+str( bar[ fd.OHLCV.time ] )+" Open :"+str( open
)+" High :"+str(
high
)+" Low :"+str( low )+" Close :"+str( close )+" "
# print( mess+str( results[ -1 ].sma ) )

"""Entry Condition """
""" if Closing Price > SMA -> Trigger Trades"""

body_length = np.absolute( m_open-m_close )

if final_res is not None :

if (rsi is not None) :

m_ohlc = (m_open+m_high+m_low+Ltp) / 4
# if final_res > Ltp and rsi > 51 and body_length > 25 and Ltp >= m_ohlc and Ltp >= prev_candle_ohlc4 and Ltp > m_prev_open:
if final_res > Ltp :
_trddate = trddate
_maxprofit = 0
_maxloss = 0
_stoplosss = 0
_targetpoint = 0
# print( "New Trade Entry :" , _entrytime )
if Z.RiskManagement.IsAllowed == True :
_stoplosss = Ltp-Z.RiskManagement.StopLoss
_targetpoint = Ltp+Z.RiskManagement.Targetpoint

if len( df_exchange ) > 0 :
query_opentrades = df_exchange[ PlaceOrder.Status ] == TradeStatus.Open

# df_exchange.where( query_opentrades )

total_open_trades = len( df_exchange.where( query_opentrades ) )

if total_open_trades >= Z.RiskManagement.Total_OpenTrades_AtaTime :
continue

if Z.RiskManagement.EntryTime > inttime :
continue

df_exchange.loc[ len( df_exchange ) ] = [ trddate , _entrytime , 1 , Ltp , TradeStatus.Open ,
Ltp ,
0 , -1 , -1 , -1 , _maxprofit , _maxloss ,
_stoplosss , _targetpoint ]

prev_candle_ohlc4 = (open+high+low+close) / 4
m_prev_open = open

# print("Trade Bank :",self.df_Trades )

self.Add_trades_to_TradeBank( df_exchange )

Calculate Pnl

The Calculate_Pnl function is a crucial component of our backtesting project responsible for calculating the profit or loss (PNL) based on the provided quantity, buy price, and the current Last Traded Price (LTP) of a financial instrument. This function plays a vital role in evaluating the financial performance of a trading strategy by determining the PNL for each trade.

Parameters

  • qty: The quantity of the financial instrument involved in the trade.
  • buyprice: The purchase price of the financial instrument.
  • Ltp: The Last Traded Price, representing the current market price.
 def Calculate_Pnl( self , qty , buyprice , Ltp ) :
_pnl = 0.0
try :
if qty > 0 :
_pnl = round( qty * (Ltp-buyprice) , 2 )
else :
_pnl = round( -1 * qty * (Ltp-buyprice) , 2 )
except :
print( "Ltp failed" )

return (_pnl)

EXIT OPEN STANDING TRADES AND UPDATE PNL

The ExitTrades_update_pnl function is a pivotal component of our backtesting project responsible for managing and exiting open trades based on specified conditions. This function contributes to risk management and ensures that trades are closed in a timely manner, capturing profits or limiting losses. Let's explore the key features and logic implemented in this function.

Parameters

  • df_Open_Trades: DataFrame containing information about open trades.
  • Ltp: The Last Traded Price, representing the current market price.
  • currenttime: The current time in integer format.
  • tradedate: The date on which the trade is taking place.
    def ExitTrades_update_pnl( self , df_Open_Trades , Ltp: float , currenttime , tradedate ) :

inttime = int( str( currenttime ) )
# print("Current Time" , inttime)

for idx , trd in df_Open_Trades.iterrows( ) :

if (trd[ PlaceOrder.Status ] == TradeStatus.Close) :
continue

_maxprofit = trd[ PlaceOrder.MaxProfit ]
_maxLoss = trd[ PlaceOrder.MaxLoss ]
pnl = self.Calculate_Pnl( trd[ PlaceOrder.Qty ] , trd[ PlaceOrder.EntryPrice ] , Ltp )

if Z.RiskManagement.IsAllowed == True :

if inttime > Z.RiskManagement.ExitTime :
df_Open_Trades.loc[ idx , PlaceOrder.Status ] = TradeStatus.Close
df_Open_Trades.loc[ idx , PlaceOrder.ExitTime ] = currenttime
df_Open_Trades.loc[ idx , PlaceOrder.ExitPrice ] = Ltp
df_Open_Trades.loc[ idx , PlaceOrder.Ltp ] = Ltp
df_Open_Trades.loc[ idx , PlaceOrder.ExitDate ] = tradedate
df_Open_Trades.loc[ idx , PlaceOrder.PnL ] = pnl

_stoploss = trd[ PlaceOrder.StopLoss ]
_target = trd[ PlaceOrder.TargetPoint ]
# exit Trades If above conditions is followed

if Ltp <= _stoploss :
df_Open_Trades.loc[ idx , PlaceOrder.Status ] = TradeStatus.Close
df_Open_Trades.loc[ idx , PlaceOrder.ExitTime ] = currenttime
df_Open_Trades.loc[ idx , PlaceOrder.ExitPrice ] = Ltp
df_Open_Trades.loc[ idx , PlaceOrder.Ltp ] = Ltp
df_Open_Trades.loc[ idx , PlaceOrder.ExitDate ] = tradedate
df_Open_Trades.loc[ idx , PlaceOrder.PnL ] = pnl
elif Ltp >= _target :

df_Open_Trades.loc[ idx , PlaceOrder.Status ] = TradeStatus.Close
df_Open_Trades.loc[ idx , PlaceOrder.ExitTime ] = currenttime
df_Open_Trades.loc[ idx , PlaceOrder.ExitPrice ] = Ltp
df_Open_Trades.loc[ idx , PlaceOrder.Ltp ] = Ltp
df_Open_Trades.loc[ idx , PlaceOrder.ExitDate ] = tradedate
df_Open_Trades.loc[ idx , PlaceOrder.PnL ] = pnl

# continue
# Pass operation to Next Bar

elif inttime > 152000 and trd[ PlaceOrder.Status ] == TradeStatus.Open :
df_Open_Trades.loc[ idx , PlaceOrder.Status ] = TradeStatus.Close
df_Open_Trades.loc[ idx , PlaceOrder.ExitTime ] = currenttime
df_Open_Trades.loc[ idx , PlaceOrder.ExitPrice ] = Ltp
df_Open_Trades.loc[ idx , PlaceOrder.Ltp ] = Ltp
df_Open_Trades.loc[ idx , PlaceOrder.ExitDate ] = tradedate
df_Open_Trades.loc[ idx , PlaceOrder.PnL ] = pnl





elif trd[ PlaceOrder.Status ] == TradeStatus.Open :
df_Open_Trades.loc[ idx , PlaceOrder.PnL ] = pnl
df_Open_Trades.loc[ idx , PlaceOrder.Ltp ] = Ltp

if _maxprofit < pnl :
df_Open_Trades.loc[ idx , PlaceOrder.MaxProfit ] = pnl

if (_maxLoss > pnl) :
df_Open_Trades.loc[ idx , PlaceOrder.MaxLoss ] = pnl

Post Trade Analysis Metrics

The following code snippet is responsible for preparing and displaying post-trade analysis metrics after the completion of backtesting. These metrics offer insights into the performance and outcomes of the executed trades within the trading strategy.

Metrics Explanation:

  1. Executed trade Analysis: The number of trades executed during backtesting.
  2. Total Trades: The total number of trades considered in the analysis.
  3. Max Profit: The maximum profit achieved in any single trade.
  4. Max Loss: The maximum loss incurred in any single trade.
  5. Profit/Loss Ratio: The ratio of winning trades to losing trades.
  6. Total Point Gained: The total points gained across all trades.
  7. Total Return: The total return, which is the sum of profits and losses.
  8. Loss Point: The total points lost across all trades.
  9. Max Profit: The maximum point gain in any single trade.
  10. Max Loss: The maximum point loss in any single trade.
# Post Trade Analysis Metrics
print("****** Post Trade Analysis Metrics **********")
print("Executed trade Analysis:", extrades)
print("Total Trades:", m_total_trade)

print("Max Profit:", m_max_profit)
print("Max Loss:", m_max_loss)

# Profit/Loss Ratio
print("Profit/Loss Ratio:", m_win_trade, "/", m_loss_trade)

# Total Point Gained
print("Total Point Gained:", m_profit)

# Total Return
print("Total Return:", m_profit + m_loss, "\n")

# Loss Point
print("Loss Point:", m_loss)

# Max Profit and Max Loss
print("Max Profit:", m_Max_Gain_point)
print("Max Loss:", m_Max_Loss_Point)
print("\n")

Explore the full potential of this project by visiting our GitHub repository. This industry-level code has undergone rigorous testing with Nifty Data and various other stocks, making it a robust solution for academic researchers, scholars, and developers — especially those in the realm of Quantitative Development and Quantitative Trading, including Hedge Fund professionals. For detailed information and usage guidelines, refer to the README file provided in the GitHub repository. Take your algorithmic trading strategies to the next level with this powerful and versatile back testing project.

To obtain the exact code, please switch branches from ‘main’ to ‘master’.

We invite you to explore, contribute, and leverage this powerful tool for advanced back testing and analysis in the dynamic landscape of algorithmic trading.

Thanks for reading! For more insights on the world of finance and data-driven trading, connect with me on my LinkedIn profile.

LinkedIn : https://www.linkedin.com/in/kamalchanchal

Let’s stay connected and continue the conversation.📊💼 #LinkedIn #Algotrading

#FinancialMarket #InvestmentStrategies #DataAnalysis #EconomicGrowth #StockMarket #AlgorithmicTrading

--

--

kamal chanchal

C# | Python | Capital Market | Artificial Intelligence | Data Science Engineering