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,
....
....
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.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.lotsize
variable is set to the same value as the quantity
. This seems to be a redundancy since quantity
is already assigned earlier."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.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
.exit_price
, exit_time
, and strategy_state
, are initialized. These variables will be updated as the backtesting process proceeds.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.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
).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
.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.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.strategy_state
, exit_price
, and exit_time
using the last open price of the historical data.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.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()
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)
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
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.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.