General Strategy Guide
Understanding the Strategy Structure
In this documentation, we will explore the framework for creating trading strategies in Python, specifically focusing on two examples: EMA Regular Order for regular trading strategies and Options Bear Call Ladder Strategy for options trading. The strategy code is structured around a base class that defines essential methods and functionalities necessary for effective trading.
Mandatory Functions:
-
Initialization Methods
In this section, you will have the Strategy Class’ “init” method (a.k.a. Constructor). This method will extract the Strategy’s Configured Parameters and save them in the different variables. There is another method in this section called “initialization”, which will be called at the start of every trading day that will occur inside the timeline for which you are executing the strategy.
-
Core Loop Methods
These are the main methods that will be called by the AlgoBulls Core in a sequence for every candle (candle is the minimum time range for which the Open, High, Low and Close values of that instrument are available. Basically it is a representation of a time period and the data corresponds to the trades executed during that period). All the logic design methods are called inside these Core Methods, along with the helping methods.
Optional Function
-
Algorithmic Calculations Methods
This section contains the methods that are defined by the user. These are not mandatory but good to keep your code organized and simple. Their main purpose will be to perform operations on historical data or LTP data of the selected instrument. Based on the results of these operations, it needs to decide whether it should Buy, Sell or take no action on the instrument.
Apart from decision making, some of the other methods can also be useful to calculate the stop loss or target price for a trade.
Point being, the purpose of these methods are totally dependent on the application of the user.
-
Miscellaneous Methods
These are handy methods that are created by a team of expert strategy developers and they are already a part of the base class. These methods do a lot of heavy lifting under the hood and can be used here for various purposes like getting the latest price of a stock, placing an order etc. These methods are not always necessary to be used and are generally a part of other methods mentioned above.
Working Flow of the Strategy
Below Diagram is Sample Diagram on how the strategy execution engine works. Blocks in green are the mandatory methods that need to be implemented for every strategy.
Setting Up: Imports and Environment
Before we dive into building of a strategy we need to fist setup our workspace depending the tool we are using. The available tools for creating a workspace of strategy are Pyalgotrading and Phoenix Web.
Pyalgotrading
If you're coding on your local machine, I recommend setting up a virtual environment and installing the "pyalgotrading" Python package to keep things organised.
pip3 install pyalgotrading
Once you've got it installed, it's time to bring in the magic. Import the package and its constants into your Python file where your trading strategy will take shape.
from pyalgotrading.algobulls import AlgoBullsConnection
from pyalgotrading.strategy import *
Phoenix Web
Now, if you prefer a web-based approach, we've got you covered. Head over to the "Code New Strategy" section on the Phoenix page of the AlgoBulls website. Here, you can craft your strategy without the need to import any classes or packages. The choice is yours – flexibility at its best.
Strategy Code Creation: Let's Get Started
With the groundwork laid, it's time to create your strategy class. Be sure to choose the appropriate base class for your strategy. For regular equity strategies, the base class is StrategyBase
while for options strategies, it's StrategyOptionsBaseV2
Examples:
- Regular
- Options
class MovingAverageCrossoverV2(StrategyBase):
name = 'Moving Average Crossover V2'
Feel free to get creative with your class name, but it's good practice to make it unique among your strategy class names. Below this declaration, we set the strategy's name.
Tip
For Pyalgotrading users
If you are uploading the strategy via pyalgotrading functions, then this name
parameter's value will be the name of the strategy. That means this name will be displayed in My Coded Strategies section in Phoenix.
Initialization Functions
-
init()
Every great trading strategy starts with a solid foundation – the constructor method, also known as the "init" method. Here's what it looks like:
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
Within this method, you extract the parameters provided by the user. These user-defined parameters are stored in a dictionary called "self.strategy_parameters." Here's how you can access these values:
self.timeperiod1 = self.strategy_parameters['TIMEPERIOD1']
self.timeperiod2 = self.strategy_parameters['TIMEPERIOD2']
Feel free to initialise other variables here, such as self.main_order
self.number_of_allowed_expiry_dates
self.transaction_type_map
and more.
Example:
This is how the declaration of class along with its constructor would look like.
- Regular
- Options
class StrategyEMARegularOrder(StrategyBase):
name = 'EMA Regular Order Strategy'
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.timeperiod1 = self.strategy_parameters['TIMEPERIOD1']
self.timeperiod2 = self.strategy_parameters['TIMEPERIOD2']
self.main_order_map = None
-
initialize()
The "initialize" method is your strategy's workspace, where you get everything ready for a fresh trading day. It's called at the start of each trading day, allowing you to reset variables, set up your tools, and perform any other tasks to ensure your strategy is primed and ready.
Examples:
- Regular
- Options
def initialize(self):
self.main_order_map = {}
Algorithmic Calculation Functions
-
get_decision()
Now, let's dive into the heart of your strategy – the "get_decision" method. This method is where the magic happens, where you decide when to enter or exit a trade, set stop losses, and target prices. Here's how it works: Historical data of the instrument up to a certain point in your strategy is collected using:
-
Historical data is a time series data of open, high, low and close values of an instrument. These values are fetched at an interval same as that of the candle interval. The method given below fetches multiple historical data values till the current candle on which the strategy is executing. Historical data of the instrument up to a certain point in your strategy is collected using:
hist_data = self.get_historical_data(instrument)
Here the variable hist_data is a pandas.Dataframe object. A Pandas DataFrame is a 2 dimensional data structure, like a 2 dimensional array, or a table with rows and columns. The hist_data Dataframe will have 4 columns named as : “open”, “high”, “close“ and “low”. For every row there will be a unique timestamp and the difference between each timestamp is of candle interval.
-
To access all the values of one particular column from the hist_data, you can use the following syntax code. Remember the data type of the column fetched here is pandas.Series
hist_data[‘<column_name>’]
Example:
close = hist_data[‘close’]
-
To access the O-H-L-C values of the current latest candle you can use the following code. This also tells us that the index -1 is for latest, -2 will be for second latest and so on. This also implies that index 0 will get the oldest data, index 1 will give second oldest data and so on.
latest_ohlc = hist_data.iloc[-1]
-
Next, you analyse this historical data to determine trading signals and calculate indicator values. You can rely on functions from "talib" for this, as shown below:
ema_x = talib.EMA(hist_data['close'], timeperiod=self.timeperiod1) ema_y = talib.EMA(hist_data['close'], timeperiod=self.timeperiod2)
As you can see, we have passed the “close” column and a strategy parameter value called “self.timeperiod1” and “self.timeperiod2” to the talib function. Each of the talib functions require unique input values, some require pandas.Series, some require constants like integers and floats and some require both. To understand the working of each talib function, refer here.
-
In many strategies, the condition to buy or sell can be triggered based on the crossover direction of 2 signals/indicators. A crossover refers to an instance where an indicator and a price, or multiple indicators, overlap and cross one another. Crossovers are used in technical analysis to confirm patterns and trends such as reversals and breakouts, generating buy or sell signals accordingly. Below we have given an example of how to calculate the crossover of the 2 indicator values we had calculated above.
crossover_value = self.utils.crossover(ema_x, ema_y)
Here if crossover_value is 0 then the indicators ema_x and ema_y have not crossed. If it is 1 then indicator ema_x has crossed ema_y in upward direction. Similarly if it is -1 then indicator ema_x has crossed ema_y in downward direction
By combining these calculated values with historical data, you can make informed decisions about when to initiate or close a trade. Additionally, you can use this information to compute potential target prices or set stop losses.
AlgoBulls' Phoenix empowers you to navigate the complexities of options trading with ease.
Example:
This is how an ideal get_decision() function would look like. This example is from RSI Crossover Strategy.
def get_crossover_value(self, instrument):
hist_data = self.get_historical_data(instrument)
rsi_value = talib.RSI(hist_data['close'], timeperiod=self.time_period)
oversold_list = [self.oversold_value] * rsi_value.size
overbought_list = [self.overbought_value] * rsi_value.size
oversold_crossover_value = self.utils.crossover(rsi_value, oversold_list)
overbought_crossover_value = self.utils.crossover(rsi_value, overbought_list)
return oversold_crossover_value, overbought_crossover_value
Miscellaneous Functions
-
get_child_instrument_details()
The get_child_instrument_details function is at your disposal. It takes several parameters:
- base_instrument: The foundation of your options strategy, often represented by an index.
- tradingsymbol_suffix: Specifies whether the option should be a call option (CE) or a put option (PE), determining the trading symbol's suffix.
- strike_direction: Defines the direction relative to the current strike price, with options like "In The Money" (ITM), "Out of The Money" (OTM), and "At the Money" (ATM).
- no_of_strikes: Specifies how many strikes away from the current strike price the function should consider, setting the distance in strike prices within the chosen strike direction.
This function calls two other functions: self.get_allowed_expiry_dates()
and get_options_instrument_with_strike_direction()
Example:
This is how the function looks ideally. This example was taken from “Options Bear Call Strategy” from ready templates.
def get_child_instrument_details(self, base_instrument, tradingsymbol_suffix, strike_direction, no_of_strikes):
expiry_date = self.get_allowed_expiry_dates()[0]
child_instrument = self.get_options_instrument_with_strike_direction(base_instrument, expiry_date, tradingsymbol_suffix, strike_direction, no_of_strikes)
return child_instrument
-
get_allowed_expiry_dates()
In options strategies, the get_allowed_expiry_dates function plays a vital role. It doesn't require parameters; instead, it relies on the number_of_allowed_expiry_dates variable defined in the constructor method.
- If number_of_allowed_expiry_dates is set to 1, the function returns the monthly expiry date.
- If number_of_allowed_expiry_dates is set to 0, the function returns a weekly expiry date.
In essence, this function helps you select the appropriate expiry date based on the value you assign to number_of_allowed_expiry_dates
variable.
-
get_options_instrument_with_strike_direction()
This function is a key player in options strategies, taking parameters such as:
- base_instrument: The foundation of your options strategy.
- expiry_date: The expiry date of the child instrument, calculated using the "get_allowed_expiry_dates" function.
- tradingsymbol_suffix: Specifies whether the option should be a call option (CE) or a put option (PE), helping identify the appropriate options.
- strike_direction: Determines the direction in relation to the current strike price, offering options like "In The Money" (ITM), "Out of The Money" (OTM), and "At the Money" (ATM).
- no_of_strikes: Specifies how many strikes away from the current strike price the function should consider, defining the distance in strike prices within the chosen strike direction.
-
options_instruments_set_up_local()
Now, let's talk about a crucial piece of the options strategy puzzle – options_instruments_set_up_local
This function creates local instances of all available child instruments based on specified parameters:
- base_instrument: The foundation of your options strategy, typically an index.
- tradingsymbol_suffix: Specifies whether the child instrument should be a call option (CE) or a put option (PE), aiding in the identification of the appropriate options.
- current_close (strike_price): The current price of the base instrument, also known as the strike price.
- direction: Determines the direction in relation to the current strike price, helping select the child instrument (i.e., ATM, ITM, or OTM).
Once executed, this function sets up and stores local instances of child instruments matching your criteria. These instances become valuable assets for further analysis or operations, including utilising the "get_child_instrument_details" function.
Example:
This is how this function looks ideally. This example was taken from “Options Bear Call Ladder” from ready templates.
def options_instruments_set_up_local(self, base_instrument, tradingsymbol_suffix, current_close, direction=OptionsInstrumentDirection.EXACT):
expiry_dates = self.get_allowed_expiry_dates()
for expiry_date in expiry_dates:
self.options_instruments_set_up(base_instrument, direction, expiry_date, tradingsymbol_suffix, current_close)
-
options_instruments_set_up()
The final piece of the puzzle is "options_instruments_set_up." This function takes parameters like:
- base_instrument: The foundation of your options strategy.
- direction: Determines the direction in relation to the current strike price, helping select the child instrument (i.e., ATM, ITM, or OTM).
- expiry_date: The expiry date of the child instrument, calculated using the "get_allowed_expiry_dates" function.
- tradingsymbol_suffix: Specifies whether the child instrument should be a call option (CE) or a put option (PE), aiding in the identification of the appropriate options.
- current_close: The current price of the base instrument, also known as the strike price.
Now, let's talk about the engine that drives your strategy – the core loop functions.
These include "strategy_select_instruments_for_entry", "strategy_enter_position", "strategy_select_instruments_for_exit" and "strategy_exit_position" These functions are called within a loop from the AlgoBulls core until an Exit Event for the strategy occurs.
The 4 Core Loop Functions
-
strategy_select_instruments_for_entry()
This function takes parameters:
- candle: The current candle where the strategy is executing its algorithm.
- instrument_bucket: A list of all the instruments provided by the user when starting the strategy. Here, you iterate through the instruments, calling "self.get_decision()" to determine actions for each. For options strategies, you can also use "self.options_instruments_set_up_local()" to set up child instruments for each base instrument. The selected instruments and their meta-information are returned by the function.
Example:
This function would ideally look like this. This example was taken from “Options Straddle” from Ready Templates.
Examples:
- Regular
- Options
def strategy_select_instruments_for_entry(self, candle, instruments_bucket):
selected_instruments, meta = [], []
for instrument in instruments_bucket:
crossover = self.get_decision(instrument)
action_constants = {1: 'BUY', -1: 'SELL'}
if crossover in [-1, 1]:
selected_instruments.append(instrument)
meta.append({'action': action_constants[crossover]})
return selected_instruments, meta
-
strategy_enter_position()
This function takes parameters:
- candle: The current candle where the strategy is executing its algorithm.
- instrument: One of the instruments from the selected instruments list returned by "strategy_select_instruments_for_entry."
- meta: Meta-information of that particular instrument, aligning with the instrument from the meta list.
Here, you place orders for the selected instruments, updating them in "self.main_order" for easy access in exit functions.
Example:
This function would ideally look like this. This example was taken from “Options Straddle” from Ready Templates.
Examples:
- Regular
- Options
def strategy_enter_position(self, candle, instrument, meta):
self.main_order_map[instrument] = _ = self.broker.OrderRegular(instrument, meta['action'], quantity=self.number_of_lots * instrument.lot_size)
return _
-
OrderRegular
The self.broker.OrderRegular
is used to place the entry/exit orders.
-
Parameters:
instrument
Instrument object on which to place the order.action
Action to be taken, either 'BUY' or 'SELL'.quantity
The quantity of the order, generally calculated asself.number_of_lots * instrument.lot_size
order_variety
(Optional) Type of order. Default is MARKET orderBrokerOrderVarietyConstants.MARKET
price
(Optional) The price at which the limit order will be placed.trigger_price
(Optional) The trigger price for stop-loss orders.
Click on each of the tabs to see the relevant code snippet.
- Market Order
- Limit Order
- Stoploss Market Order
- Stoploss Limit Order
self.broker.OrderRegular(
instrument,
action,
quantity
)
-
strategy_select_instruments_for_exit()
Similar to entry, this function takes parameters:
- candle: The current candle where the strategy is executing its algorithm.
- instrument_bucket: A list of all instruments provided by the user when starting the strategy.
The function iterates through the instrument bucket, checking if the instrument is present in "self.main_order" and whether its exit condition is satisfied. Instruments meeting exit conditions are added to the selected instruments list, along with their respective actions and meta-information.
Example:
This function should ideally look like this. This example was taken from “EMA Crossover Strategy”.
def strategy_select_instruments_for_exit(self, candle, instruments_bucket):
selected_instruments, meta = [], []
for instrument in instruments_bucket:
if self.main_order_map.get(instrument) is not None:
crossover = self.get_decision(instrument)
if crossover in [1, -1]:
selected_instruments.append(instrument)
meta.append({'action': 'EXIT'})
return selected_instruments, meta
-
strategy_exit_position()
Finally, this function takes parameters:
- candle: The current candle where the strategy is executing its algorithm.
- instrument: One of the instruments from the selected instruments list returned by "strategy_select_instruments_for_exit."
- meta: Meta-information of that particular instrument, aligning with the instrument from the meta list.
Here, you place orders for the selected instruments, removing them from "self.main_order" to prepare for the next iteration of the AlgoBulls core loop.
Example: This function should ideally look like this. This example was taken from the "EMA Crossover Strategy". Here, we are telling the core to exit all positions:
def strategy_exit_position(self, candle, instrument, meta):
if meta['action'] == 'EXIT':
self.main_order_map[instrument].exit_position()
self.main_order_map[instrument] = None
return True
return False
If you want the strategy to place the exit order instead of letting the core handle it, then this is what the function should look like:
def strategy_exit_position(self, candle, instrument, meta):
if meta['action'] == 'EXIT':
self.broker.OrderRegular(
instrument,
action,
quantity,
related_order=entry_order,
position=BrokerExistingOrderPositionConstants.EXIT
)
return True
return False
Tip
For Pyalgotrading users
If you are uploading the strategy via pyalgotrading functions, then this name
parameter's value will be the name of the strategy. That means this name will be displayed in My Coded Strategies section in Phoenix."
Conclusion
Here’s a quick recap of what you’ve learned:
-
Understanding Strategy Structure: You now know how to create both regular and options trading strategies.
-
Key Components:
- Mandatory Methods: Essential for initializing and executing your strategy.
- Optional Methods: Enhance decision-making and adaptability.
-
Platforms: You can develop strategies using Pyalgotrading locally or the user-friendly Phoenix Web platform.
-
Code Organization: Keep your code clean and structured for easier modifications and analysis.
-
Experiment and Adapt: Trading is both an art and a science. Don’t hesitate to tweak your strategies based on market conditions.
With these insights, you’re well-equipped to build and refine your trading strategies. Happy coding, and here’s to your success in the markets!