Docs
Welcome to the BDPO documentation. This resource provides detailed information on creating Python-based strategies and indicators, as well as instructions for backtesting and deploying them on a chart. It covers various backtesting functions, the data involved, script creation, and the use cases for different types of backtests. Additionally, it includes sections on developing multi-timeframe strategies, building custom indicators for charts, and accessing and utilizing the marketplace.
To prevent abuse of the backtester lab, we have implemented a whitelist system. This means that only
certain Python libraries can be imported and used when defining your strategies and indicators. The
following libraries are whitelisted and can be used in your scripts:
The BDPO Backtester Lab is a Python-based backtesting platform that allows you to create,
backtest and optimise your own trading strategies. Designed for traders of all levels, the
backtester lab will provide in-depth details on performance, risk, returns, statistics and individual
trades.
As well as backtesting your strategy on a historical data, the backtester lab allows for optimisation
of your strategy parameters, forward testing on unseen data (both backtests and optimisations), as
well as stress testing of your strategy on hundreds of samples of data. This allows you to
understand the robustness of your strategy and how it may perform in the future.
When creating strategies and indicators in the Strategy Editor, these scripts will be written using
the Python programming language. As a result, strategies and indicators should be written in a
specific format using classes/functions in order to run smoothly when backtesting. The documentation
below will provide an overview of the structure of both strategies and indicators, as well as how to
write and backtest them.
It is important to note that the backtester lab is designed to be as flexible as possible, allowing
for the creation of a wide range of strategies and indicators. If you are not familiar with Python,
there are many community written scripts available in the marketplace that you can download and
backtest yourself without the need to write any code.
The Strategy Editor is where you will create and edit your strategies and indicators. This is an
in-browser code editor that allows you to write your scripts in Python. Once you have written your
script, you can save it to your profile and backtest strategies using the backtester lab, or apply
indicators to your charts in the charting platform.
BDPO+ and BDPO Ultra users can also access the AI assistant to help develop their scripts. The AI
assistant will provide suggestions on how to improve your script, as well as provide feedback on
potential issues or improvements that can be made. This is a great tool for those who are new to
writing scripts, or for those who want to improve the performance of their existing scripts.
In the event of an error in your script, check the console (in the backtester lab for strategies or
in the chart console for indicators) for more information on the error. On script save, the editor
will also check for syntax errors and highlight them in the editor.
To begin writing your own scripts, visit the Strategy Editor page.
Once you have written your strategy or indicator in the Strategy Editor, or downloaded a script from
the marketplace, you can backtest it using the backtester lab. This will provide you with detailed
information on the performance of your strategy, including returns, risk, statistics and individual
trades.
To begin backtesting your scripts, visit the Backtester Lab page.
The Backtester Lab gives you a choice of backtest types to really test your strategy's robustness
and accuracy. The backtest types are:
Once you have written your indicator script in the Strategy Editor, or downloaded a script from the
marketplace, you can apply it to your charts in the charting platform. This will allow you to see the
indicator plotted on the chart, as well as any buy/sell signals that are generated by the indicator.
To begin applying your scripts to your charts, visit the Charting page.
Strategies in the BDPO Backtester Lab are written in Python and should be structured in a specific way in order to be backtested. The strategy itself should be setup as a Python class with this structure:
import BDPO
class Strategy(BDPO.Strategy):
param1: int = 1
param2: int = 2
def __init__(self, data):
pass
def next(self, data):
pass
Within this strategy structure there are three main components. The first being the strategy
class variables. The class variables are used to define any parameters that you would like to
be able to adjust when backtesting your strategy. These parameters can be adjusted in the
backtester lab and will be passed to the strategy when it is initialized. The second component
is the __init__ method. This method is used to initialize any variables that you would like to
use in your strategy upon start. If there are none, you can pass this method.
The third component is the next method. This method is called at each new datapoint
and is where you define the logic of your strategy. This is where you will define your buy/sell
signals based on the data that is passed to the strategy.
You'll notice that 'data' is passed as an argument to both the __init__ and next methods. This
'data' object contains the OHLCV data for the asset that you are backtesting, and is needed
for the strategy to run.
Arguably, the most crucial part of creating a strategy of your own is to be able to understand
and work with the data in which you are testing on.
The BDPO backtest was designed with speed and accuracy in mind. To achieve this, the data is
stored, and passed to the strategy in the form of a Numpy array. To understand how to work
with this data, we will look at the format of the data, and also how to access it.
Lets see what happens when printing the following:
def next(self, data):
self.log(data)
The result of running BDPO.console() (which prints the result to the console) will look
something like:
BDPO.Engine2.backtest_engine.Data object at 0x11cf048
This isn't very helpful to us, so lets look at how we can access the data.
the data object stores all timeframes for you to work with:
def next(self, data):
self.log(data[0].iloc[-1])
Console:
>> t 2024-04-01 09:00:00 o 1.07898 h 1.07909 l 1.07856 c 1.07859 v 3389.0 Name: 290, dtype: object
We have data!
With each candle that passes, we have logged our data. This backtest was run on the 1hour
timeframe, and as a result of logging data[0], we have access to the 1hour timeframe data.
As the backtest progresses, we will see the dataset evolve and continue to be printed to the
console. It is important to note that the data is stored in a pandas dataframe, and as such, we
can access the data in the same way we would with any other pandas dataframe.
Note: The next() function runs on each new datapoint on the tick and/or minute
timeframe. Therefore, by printing data[0] in the next() function, we are printing the latest
candle, which is updated with any new incoming data. As a result,
the data is 'built' up as it would be in a live trading environment.
It is possible to also access the datetime / open / high / low / close / volume of the
previous candle by calling them in the same way.
Let's run through a simple SMA Golden Cross strategy template to get started. The first step for every strategy is to import the BDPO library. This is a built-in library that provides powerful and flexible functionality for backtesting. By importing it, you will be able to access all the functions and methods that are required to build your strategy. The strategy will not run without it:
import BDPO
# import other libraries here
The benefits of using a Python based strategy editor is that you have access to all the
powerful libraries that Python has to offer. You can import multiple libraries, and use it
to enhance your strategy.
NOTE: BDPO has a whitelist of libraries that are allowed to be
imported. This is to ensure that the platform is secure and to prevent any malicious code
from being executed. Navigate to the
Library Whitelist
section of this documentation to find out
more. After importing your libraries, it is time to set up the strategy Class.
To create a strategy, you need to create a class that inherits from the BDPO.Strategy class.
This class will contain all the methods and attributes that are required to run your strategy.
We can also define our strategy params here too. These parameters can be modified in the
backtester lab when running the strategy. Here is an example of a strategy class:
import BDPO
class Strategy(BDPO.Strategy):
fast_length: int = 50 # Length of the fast SMA
slow_length: int = 200 # Length of the slow SMA
take_profit_pips: float = 0.0005 # Take profit in pips
stop_loss_pips: float = 0.0005 # Stop loss in pips
max_positions: int = 3 # Maximum number of positions
risk: float = 0.01 # Trade size
...
So far we have imported the BDPO library and created a strategy class. We have then also defined the params. The next step is to define the __init__ method. This method is called when the strategy is initialized. It is used to set up class variables which are not modifiable by the user, at the start of the strategy.
import BDPO
class Strategy(BDPO.Strategy):
fast_length: int = 50 # Length of the fast SMA
slow_length: int = 200 # Length of the slow SMA
take_profit_pips: float = 0.0005 # Take profit in pips
stop_loss_pips: float = 0.0005 # Stop loss in pips
max_positions: int = 3 # Maximum number of positions
risk: float = 0.01 # Trade size
def __init__(self, data):
self.buys = 0
self.sells = 0
Here, the __init__ function is defined with 'self' and 'data' as input arguments. This is
necessary for the strategy to run.
Within the __init__ function, we define any variables we
wish to refer back to during the running of the strategy. If no variables have been defined,
the __init__ function must still be defined, but can be passed using 'pass'.
In this example, we are defining variables 'buys' and 'sells' and setting them to 0.
This is a simple example where the strategy uses the 'buys' and 'sells' class variables
but they are not modifiable by the user in the Backtester Lab.
You can define any variables you wish. Now for the next() class function and the strategy logic
within it:
import BDPO
import pandas_ta as ta
class Strategy(BDPO.Strategy):
fast_length: int = 50 # Length of the fast SMA
slow_length: int = 200 # Length of the slow SMA
take_profit_pips: float = 0.0005 # Take profit in pips
stop_loss_pips: float = 0.0005 # Stop loss in pips
max_positions: int = 3 # Maximum number of positions
risk: float = 0.01 # Trade size
def __init__(self, data):
self.buys = 0
self.sells = 0
def next(self, data):
df = data[0] # Main timeframe
max_length = max(self.fast_length, self.slow_length)
if len(df) < max_length:
self.log("Not enough data to calculate indicators.")
return
fast_sma = ta.sma(df['c'], length=self.fast_length)
slow_sma = ta.sma(df['c'], length=self.slow_length)
if fast_sma is None or slow_sma is None:
self.log("Could not calculate SMAs")
return
current_close = df['c'].iloc[-1]
current_fast_sma = fast_sma.iloc[-1]
current_slow_sma = slow_sma.iloc[-1]
if current_fast_sma > current_slow_sma and fast_sma.iloc[-2] <= slow_sma.iloc[-2] and self.sells == 0:
if len(self.open_positions()) < self.max_positions:
trade_lots = self.account_capital() * self.risk * 0.01
id = self.trade(trade_type='BUY', trade_lots=trade_lots)
self.buys += 1
self.log("id:", id, "Opening Buy @" + str(current_close))
elif current_fast_sma < current_slow_sma and fast_sma.iloc[-2] >= slow_sma.iloc[-2] and self.buys == 0:
if len(self.open_positions()) < self.max_positions:
trade_lots = self.account_capital() * self.risk * 0.01
id = self.trade(trade_type='SELL', trade_lots=trade_lots)
self.sells += 1
self.log("id:", id, "Opening Sell @" + str(current_close))
open_positions = self.open_positions()
if open_positions:
if self.pnl() > self.account_capital() * 0.005:
self.log(f"Closing all positions in profit {self.pnl()}")
self.close_all()
self.buys = 0
self.sells = 0
Here we have defined a few things so lets go through them one by one:
We now have the next() function which is also compulsory in every strategy (along with the
init() function). The next() function is called on every tick/new datapoint. This is where we
define our trading logic.
Within the next function, we also include some error handling to ensure that if the data is
not available (for example, at the start of the strategy), the function will return and not
trade, as the moving averages will not have enough data to calculate.
We then define the current_close, current_fast_sma and current_slow_sma variables. Following
this, we define our conditions for opening a trade. In this case, we are using a simple moving
average crossover strategy with a grid approach. We have not included any risk management at
this point, and this is not recommended. However, as an example of strategy creation, we have
kept it simple.
Finally, when we have open positions, we check for a positive PnL above 0.5% of the account
capital, and if this is the case, we close all positions. This is a simple way to take profit
when the strategy is in profit. We also reset the buys and sells variables to 0, so that the
strategy can start again.
Below are the built-in functions defined in the BDPO library to help create, manage,
execute and optimise strategies:
The 'self.log' function is used to log messages to the Backtester Lab console. This can be
useful for debugging (similar to the Python print() function)
self.log("Hello World")
This function returns the symbol information of the current symbol. The return of this function
is a dictionary containing the following symbol information:
- contract_size
- exchange
- name
- pip
- precision
- type
self.symbol_information()
This function returns the open positions of the current strategy. The return of this function is
a list of dictionaries containing the following position information:
- ID
- TYPE
- SYMBOL
- LOTS
- STOPLOSS
- TAKEPROFIT
- OPENPRICE
- OPENDATE
- HASH
- TRANSACTION_COST
- OPEN_CANDLESTAMP
self.open_positions()
This function returns the pending positions of the current strategy. The return of this function
is a list of dictionaries containing the following position information:
- ID
- TYPE
- SYMBOL
- LOTS
- STOPLOSS
- TAKEPROFIT
- OPENPRICE
- OPENDATE
- HASH
- TRANSACTION_COST
- OPEN_CANDLESTAMP
self.pending_positions()
This function returns the closed positions of the current strategy. The return of this function
is a list of dictionaries containing the following position information:
- ID
- TYPE
- SYMBOL
- LOTS
- STOPLOSS
- TAKEPROFIT
- OPENPRICE
- OPENDATE
- HASH
- TRANSACTION_COST
- CLOSEDATE
- CLOSEPRICE
- PNL
- DURATION
self.closed_positions()
This function returns the account capital of the current strategy. The return of this function is
a float.
self.account_capital()
This function returns the account equity of the current strategy. The return of this function is
a float.
self.account_equity()
This function returns the profit and loss of any open trades. The return of this function is
a float.
self.pnl()
This function is used to execute market and pending orders.
self.trade(
trade_type,
trade_lots,
trade_price,
trade_stop_loss,
trade_take_profit,
)
trade_type:
This could be any of the following:
trade_lots:
This is the lots to be traded. Min lots = 0.01 and max lots = 20.00
trade_price:
This is only applicable for pending positions, and will be ignored for market orders.
For BUYSTOP the price must be above the current price, and for SELLSTOP the price must
be below the current price. For BUYLIMIT the price must be below the current price, and
for SELLLIMIT the price must be above the current price.
trade_stop_loss:
This is optional and is the stop loss for the trade.
trade_take_profit:
This is optional and is the take profit for the trade.
Note: All open and pending trades will be assigned an ID (incremental) which is also
returned when executing a successful self.trade()
This function is used to close a trade by its trade_id. The function takes the trade ID (int)
as a parameter. The trade ID is returned after successfully opening a trade using the self.trade()
function.
self.close_by_id(trade_id)
This function is used to close all open AND pending trades at the time of calling.
self.close_all()
Custom indicators are a powerful tool that can be used to create your own technical
indicators. These indicators can be used to visually represent data on your charts, and can
be applied to any chart in one click. Custom indicators are also written in Python, saved in
your library, and once you have perfected your indicator, you can share it with the community
by submitting it to the marketplace.
Custom indicators are tools built on top of your chart data and therefore should be
structured in a specific way in order to be compatible and deployable. The structure of a custom
indicator is different to custom stategies, in the fact that indicators are Python functions.
The structure is as follows:
Here is a skeleton structure of an Indicator script:
import BDPO
def MyIndicator(data, length: int=10):
datetime, open, high, low, close, volume = BDPO.data_to_pandas(data)
# define your indicator logic here
BDPO.plot(
type="Spline",
data=pd.DataFrame({'SMA': sma}),
props={
'showSpline': True,
'splineColor': '#00FF00',
'splineWidth': 1.5,
'skipNan': True,
}
)
return BDPO.overlay(
precision='chart',
zIndex=1
)
Within this indicator structure there are 5 main components. The first being the library
imports. Next, the indicator function and input validation. This is where you define the logic of your indicator.
Once the logic is defined, you can plot the indicator on the chart. This is done using the
BDPO.plot() function.
Finally, returning the indicator overlay with indicator plots. Let's break down each of these components:
Library Imports: This is where you import the BDPO library (compulsory)
and any other libraries. The Library Whitelist outlines which libraries are available for use in
custom indicators.
Indicator Function: This is where you define the logic of your indicator.
The function itself should be defined with the following arguments:
Indicator argument types are defined as follows:
Let's create a simple custom indicator that calculates the moving average of the close price of an asset (Simple Moving Average). The first step is to create a new indicator script, and import the BDPO library and any other libraries that are included in the whitelist in which you may need in your script. Next we will define the indicator function:
import BDPO
import pandas as pd
import numpy as np
def Indicator(
data,
length: int = 20
):
...
So far we have imported the BDPO library, and the pandas and numpy libraries. We have also
defined the indicator function 'def Indicator(...)'. As previously mentioned, we must pass 'data'
to the function in order to access the OHLCV data of the asset that the indicator is running
on.
In this function we have also declared the 'length' parameter (optional), which is the number of periods
that the moving average will be calculated over. It is important to declare each variable
type (int, float etc...) as well as a default value. Any variables declared here will be
customisable when deploying the indicator on the charting page
After defining the type of the variable, a default value must be assigned to the variable.
Here is an example of each:
def MyExample(
data,
paramter1: int=10,
paramter2: float=0.5,
paramter3: bool=True,
paramter4: str="Hello",
paramter5: list=[1, 2, 3]
):
...
Next, we will define the body of the function, where the logic of the indicator will be implemented. In this case, we will calculate the simple moving average of the close price of the asset.
import BDPO
import pandas as pd
import numpy as np
def MyIndicator(data, length: int = 10):
datetime, open, high, low, close, volume = BDPO.data_to_pandas(data)
if len(close) <= length:
return BDPO.overlay(
precision='chart',
zIndex=1
)
# Calculate the Simple Moving Average
sma = pd.Series(close).rolling(window=length).mean().fillna(np.nan)
...
Data validation! Before defining any logic, it's important to validate any inputs. In this example, the SMA indicator is calculating an average over a number of periods. If the user inputs a length less than the indicator length defined by the user, the indicator will return an empty plot. This is good for error handling, and reducing the risk of any errors that may occur when the indicator is applied to the chart. When the user applies the indicator to their chart, they will be able to input a custom value for the length, and any other parameters that are defined in the function. Therefore it is important to validate the inputs to ensure that the indicator will run correctly.
In our indicator body, we have defined the simple calculations for the moving average (the
average of the last n periods). We have also included the BDPO.data_to_pandas function to
handle the data into the Indicator function. The data_to_pandas function is used to convert
and return the OHLCV components of the data, into a pandas dataframe. For this example, the
current chart timeframe (or data[0]) is being used.
The final step in defining the function is to format the indicator and plots. This is done by ensuring that the data is plotted as a pandas DataFrame WITHOUT DATETIMES
import BDPO
import pandas as pd
import numpy as np
def MyIndicator(data, length: int = 10):
datetime, open, high, low, close, volume = BDPO.data_to_pandas(data)
if len(close) <= length:
return BDPO.overlay(
precision='chart',
zIndex=1
)
# Calculate the Simple Moving Average
sma = pd.Series(close).rolling(window=length).mean().fillna(np.nan)
# Plot the SMA line
BDPO.plot(
type='Spline',
data=pd.DataFrame({'SMA': sma}),
props={
'showSpline': True,
'splineColor': '#00FF00',
'splineWidth': 1.5,
'skipNan': True,
}
)
return BDPO.overlay(
precision='chart',
zIndex=1
)
We now have a dataframe with one column; 'sma'. It is now ready to be formatted for the chart.
Above, we have seen how to create and return an indicator. Now, we will look at the options you have when plotting the indicator overlay. The indicator plot is a built-in function with 3 arguments: type, data, and props. The type argument is the type of plot you want to display (e.g. Spline, Area, etc.). The data argument is the data you want to plot, and the props argument is the properties of the plot. The props argument is optional and can be left out if you do not want to customize the plot. Below, we will go over the different types of plots you can use and the properties you can customize:
Below is a list of built-in charting indicators that can be applied to your chart: