Creating Price Action Backtester Function Using Python and Zerodha API

In this section, We shall create a Price Action Backtester function! The function takes one argument, data, which is expected to be a dictionary containing information about a trading strategy and its parameters.

Here is an example of such list of JSON that we have observed in the backtesting tutorial of Gann Square of 9. 

				
					[{'strategy': 'GannSq9',
  'symbol': 'RELIANCE',
  'exchange': 'NSE',
  'quantity': 473,
  'entry_time': 0,
  'strategy_type': 'sell',
  'entry_price': 1056.25,
  'target': 1048.66,
  'stoploss': 1064.39,
  'strategy_start': '01-01-2019 09:25:00',
  'strategy_end': '01-01-2019 15:10:00',
  'results': {}},
 {'strategy': 'GannSq9',
  'symbol': 'RELIANCE',
  'exchange': 'NSE',
  'quantity': 473,
  'entry_time': 0,
  'strategy_type': 'buy',
  'entry_price': 1064.39,
  'target': 1072.02,
  'stoploss': 1056.25,
  'strategy_start': '01-01-2019 09:25:00',
  'strategy_end': '01-01-2019 15:10:00',
  'results': {}},
 {'strategy': 'GannSq9',
  'symbol': 'RELIANCE',
  'exchange': 'NSE',
  'quantity': 472,
  'entry_time': 0,
....
....
				
			
  • Various strategy-specific parameters and information are extracted from the data dictionary, such as symbol, exchange, quantity, strategy_type, entry_time, entry_price, target, stoploss, strategy_start, and strategy_end. These parameters are assigned to local variables for further use.
  • The function calls a get_insToken(symbol, exchange) function to obtain a token related to the trading symbol and exchange. This token is used later for fetching historical data.
  • The lotsize variable is set to the same value as the quantity. This seems to be a redundancy since quantity is already assigned earlier.
  • The function checks if the "results" key is present in the data dictionary. If not, it adds an empty dictionary as the value for the "results" key. This is done to store the results of the backtesting in the data dictionary.
  • The function checks if the lengths of strategy_start and strategy_end are not equal. If they are not equal, the script exits. This is likely a validation step to ensure that the strategy’s time frame is valid.
  • strategy_start and strategy_end are processed using the strategy_date_function() function, which seems to add one day to strategy_start and strategy_end.
  • Several variables, such as exit_price, exit_time, and strategy_state, are initialized. These variables will be updated as the backtesting process proceeds.
  • The function checks if entry_time is not equal to zero. If it’s not zero, it is also processed using the strategy_date_function() function. This is likely to ensure that entry_time is in the correct format.
  • The code distinguishes between two cases for market entry:
    • Case 1A: If entry_price is zero, it means an immediate market entry is required. In this case, historical data is fetched for the 10 minutes before strategy_start, and the last open price in that data is used as the entry_price. If entry_price is still zero after this step, an exception is raised (CallDexter).
    • Case 1B: If entry_price is not zero, it implies a limit order is placed. In this case, historical data is fetched between strategy_start and strategy_end (excluding the last minute), and it checks if the entry price condition is met. If met, it updates entry_time and entry_price.
  • If entry_time is not zero (i.e., a trade has been triggered), it proceeds to check if the stop loss is hit. It fetches historical data from entry_time to strategy_end and checks if the stop loss condition is met. If met, it updates strategy_state, exit_price, and exit_time accordingly.
  • If the strategy_state is not "StopLoss", it checks if the target is hit in the same historical data. If met, it updates strategy_state, exit_price, and exit_time accordingly.
  • If neither the stop loss nor the target is hit, it assumes that the strategy should square off at the end. It updates strategy_state, exit_price, and exit_time using the last open price of the historical data.
  • Finally, the function updates the data dictionary with the results of the backtesting, including strategy_state, strategy_start, strategy_end, entry_time, entry_price, exit_price, exit_time, and strategy_pl (strategy profit/loss). The strategy_pl is calculated based on the difference between exit_price and entry_price, adjusted by the lotsize. If strategy_type is "sell", the profit/loss is negated since it’s a short position.
  • The function returns the modified data dictionary with the backtesting results.
				
					# Initialize the variables
symbol = data["symbol"]
exchange = data["exchange"]
quantity = data["quantity"]

strategy_type = data["strategy_type"].lower()
entry_time = data["entry_time"]
entry_price = data["entry_price"]
target = data["target"]
stoploss = data["stoploss"]

strategy_start = data["strategy_start"]
strategy_end = data["strategy_end"]

token = get_insToken(symbol, exchange)

lotsize = data["quantity"]

if "results" not in data:
    data["results"] = {}

				
			

In this section, the function initializes various variables by extracting values from the data dictionary. 

These variables include trading-related parameters such as symbol, exchange, quantity, strategy_type, entry_time, entry_price, target, stoploss, strategy_start, and strategy_end

Additionally, the function obtains a token using the get_insToken() function and initializes the lotsize variable with the same value as quantity. If the "results" key is not present in the data dictionary, it adds an empty dictionary as the value for "results".

				
					if(len(strategy_start)!=len(strategy_end)):
    sys.exit()

				
			
This part checks if the lengths of strategy_start and strategy_end are not equal. If they are not equal, it exits the script, likely to ensure that the strategy’s time frame is valid.
				
					strategy_start = strategy_date_function(strategy_start) # Add one day to strategy_start
strategy_end = strategy_date_function(strategy_end)
				
			
Here, the strategy_start and strategy_end values are processed using the strategy_date_function(), which is assumed to add one day to both strategy_start and strategy_end. This is a part of the strategy-specific logic.
				
					exit_price = 0
exit_time = 0
strategy_state = "No Entry" # StopLoss, Target, Squareoff

				
			
The script initializes variables for exit_price, exit_time, and strategy_state. These variables are intended to keep track of the exit conditions and the current state of the strategy, which can be “No Entry,” “StopLoss,” “Target,” or “Squareoff.”
				
					if(entry_time != 0):
    entry_time = strategy_date_function(entry_time)
				
			

This part checks if entry_time is not zero. If it’s not zero, it is also processed using the strategy_date_function() function, presumably to ensure that entry_time is in the correct format.

This covers the initialization and preprocessing of variables in the priceaction_backtester() function. 

				
					# Case 1B
if(entry_price != 0): # Like Limit Order
    if(entry_time != 0):
        sys.exit()
    zapp = kite.historical_data(token, strategy_start, strategy_end - datetime.timedelta(minutes=1), "minute")
    for rows in zapp:
        if(strategy_type == "buy"):
            condition = rows["high"] > entry_price
        if(strategy_type == "sell"):
            condition = rows["low"] < entry_price
        if(condition):            
            entry_time = rows["date"] # Trade Triggered
            entry_price = entry_price # Trade Triggered
            print(rows)
            print("Trade Triggered at Limit Price")
            print(entry_time)
            print(entry_price)
            break

				
			

In this section of the code, it handles the case of a limit order (Case 1B). It checks if entry_price is not zero, indicating that a limit order is placed. It also verifies that entry_time is zero (no specific entry time is provided).

The script fetches historical data for the specified symbol and time frame using the kite.historical_data() function. It retrieves minute-level data between strategy_start and strategy_end - 1 minute.

It then iterates through the historical data (zapp) and checks whether the conditions for triggering the trade are met based on strategy_type. For example, if strategy_type is “buy,” it checks if the high price of the current minute is greater than entry_price. If strategy_type is “sell,” it checks if the low price is less than entry_price.

If the condition is met, it updates entry_time and entry_price with the values from the current data point, indicating that the trade has been triggered. It also prints information about the trade.

				
					# Case of Immediate Market Entry (Case 1A)
if(entry_price == 0):
    if(entry_time == 0):
        sys.exit()
    zapp = kite.historical_data(token, strategy_start - datetime.timedelta(minutes=10), strategy_start, "minute")
    entry_price = zapp[-1]["open"]
    print(zapp[-1])
    print("Trade Triggered at Market Price")
    print(entry_time)
    print(entry_price)
    if(entry_price == 0):
        raise CallDexter

				
			

In this section, it handles the case of immediate market entry (Case 1A), where entry_price is set to zero, indicating that a market order is used.

It first checks if entry_price is zero and entry_time is not zero, ensuring that a specific entry time is provided.

It then fetches historical data for the 10 minutes before strategy_start to capture the open price of the last minute. This open price is used as the entry_price for the market order.

Information about the market order is printed, including the entry time and price.

If entry_price is still zero after this step, it raises an exception named CallDexter. The purpose of this exception is not clear from the provided code.

This covers the logic for handling different cases of entry orders (limit order and market order) in the priceaction_backtester() function.

				
					# This Segment Runs only if Trade is Triggered.
if(entry_time != 0):
    zapp = kite.historical_data(token, entry_time + datetime.timedelta(minutes=1), strategy_end, "minute")

    # Now it will check from Trade Triggered Entry Time    
    for rows in zapp:
        if(strategy_type == "buy"):
            condition = rows["low"] < stoploss
        if(strategy_type == "sell"):
            condition = rows["high"] > stoploss
        if(condition):
            strategy_state = "StopLoss" # StopLoss is Hit
            exit_price = stoploss # StopLoss Triggered
            exit_time = rows["date"] # StopLoss Triggered
            print(rows)
            print("StopLoss Triggered")
            print(exit_price)
            print(exit_time)
            break

				
			

In this section of the code, it checks if a trade has been triggered (i.e., entry_time is not zero). If a trade has been triggered, it proceeds to check if the stop loss is hit.

It fetches historical data for the period starting from entry_time plus 1 minute to strategy_end. This historical data is used to evaluate whether the stop loss condition is met.

It then iterates through the historical data (zapp) and checks whether the stop loss condition is met based on strategy_type. For example, if strategy_type is “buy,” it checks if the low price of the current minute is less than stoploss. If strategy_type is “sell,” it checks if the high price is greater than stoploss.

If the stop loss condition is met, it updates strategy_state to “StopLoss,” indicating that the stop loss has been hit. It also records the exit_price as the stop loss level and the exit_time as the timestamp when the stop loss was triggered. Information about the stop loss is printed.

				
					if(strategy_state != "StopLoss"):
    # Now it will check if Target is Hit    
    for rows in zapp:
        if(strategy_type == "buy"):
            condition = rows["high"] > target
        if(strategy_type == "sell"):
            condition = rows["low"] < target
        if(condition):
            strategy_state = "Target" # Target is Hit
            exit_price = target # Target Triggered
            exit_time = rows["date"] # Target Triggered
            print(rows)
            print("Target Triggered")
            print(exit_price)
            print(exit_time)
            break

# Now it will Squareoff At Strategy End Time     
if(strategy_state != "Target"):            
    strategy_state = "Squareoff" # Target is Hit                
    exit_price = zapp[-1]["open"] # Squareoff
    exit_time = zapp[-1]["date"] # Target Triggered
    print(zapp[-1])
    print("Squareoff")
    print(exit_price)
    print(exit_time)

				
			

In this section, it first checks if the strategy_state is still not “StopLoss” after checking the stop loss condition. If that’s the case, it proceeds to check if the target condition is met.

It iterates through the historical data (zapp) again and checks whether the target condition is met based on strategy_type. If the target condition is met, it updates strategy_state to “Target,” indicating that the target has been hit. It also records the exit_price as the target level and the exit_time as the timestamp when the target was triggered. Information about the target is printed.

If neither the stop loss nor the target is hit (i.e., strategy_state is still not “Target”), it assumes that the strategy should square off at the strategy end time. It updates strategy_state to “Squareoff,” indicating that the squareoff condition has been reached. It records the exit_price as the last open price in the historical data (zapp) and the exit_time as the timestamp corresponding to that open price. Information about the squareoff is printed.

This section handles the evaluation of stop loss, target, and squareoff conditions once a trade has been triggered based on the strategy’s parameters.

Next, we’ll cover the final part of the code where the results of the backtesting are recorded and returned:

				
					data["results"]["strategy_state"] = strategy_state
data["results"]["strategy_start"] = strategy_start
data["results"]["strategy_end"] = strategy_end
data["results"]["entry_time"] = entry_time
data["results"]["entry_price"] = entry_price
data["results"]["exit_price"] = exit_price
data["results"]["exit_time"] = exit_time

data["results"]["strategy_pl"] = 0
if(strategy_state != "No Entry"):
    data["results"]["strategy_pl"] = round_down(float(lotsize * (exit_price - entry_price)), 0.05)
    if(strategy_type == "sell"):
        data["results"]["strategy_pl"] *= -1

				
			

In this final part of the code, it updates the data dictionary with the results of the backtesting:

  • strategy_state, strategy_start, strategy_end, entry_time, entry_price, exit_price, and exit_time are recorded in the “results” section of the data dictionary.
  • The strategy_pl (strategy profit/loss) is calculated based on the difference between exit_price and entry_price, adjusted by the lotsize. If strategy_type is “sell,” the profit/loss is negated since it’s a short position.

The function then returns the modified data dictionary with the backtesting results.

This completes the explanation of the priceaction_backtester() function. It’s designed to simulate a trading strategy, evaluate entry and exit conditions, and calculate the profit/loss based on those conditions.

One of the most common mistakes in trading strategies is the sequential checking of stop loss and target conditions. Typically, traders first check if the stop loss has been hit, and if it’s not hit, then they check if the target is reached.

However, a more effective approach is to simultaneously monitor both the target and stop loss conditions to promptly respond to whichever is triggered first. This ensures a more dynamic and responsive strategy execution.

Post a comment

Leave a Comment

Your email address will not be published. Required fields are marked *

×Close