PyBroker
Algorithmic Trading in Python with Machine Learning
Are you looking to enhance your trading strategies with the power of Python and machine learning? Then you need to check out PyBroker! This Python framework is designed for developing algorithmic trading strategies, with a focus on strategies that use machine learning. With PyBroker, you can easily create and fine-tune trading rules, build powerful models, and gain valuable insights into your strategy’s performance.
Key Features
A super-fast backtesting engine built in NumPy and accelerated with Numba.
The ability to create and execute trading rules and models across multiple instruments with ease.
Access to historical data from Alpaca, Yahoo Finance, AKShare, or from your own data provider.
The option to train and backtest models using Walkforward Analysis, which simulates how the strategy would perform during actual trading.
More reliable trading metrics that use randomized bootstrapping to provide more accurate results.
Caching of downloaded data, indicators, and models to speed up your development process.
Parallelized computations that enable faster performance.
With PyBroker, you’ll have all the tools you need to create winning trading strategies backed by data and machine learning. Start using PyBroker today and take your trading to the next level!
Installation
PyBroker supports Python 3.9+ on Windows, Mac, and Linux. You can install
PyBroker using pip
:
pip install -U lib-pybroker
Or you can clone the Git repository with:
git clone https://github.com/edtechre/pybroker
A Quick Example
Get a glimpse of what backtesting with PyBroker looks like with these code snippets:
Rule-based Strategy:
from pybroker import Strategy, YFinance, highest
def exec_fn(ctx):
# Get the rolling 10 day high.
high_10d = ctx.indicator('high_10d')
# Buy on a new 10 day high.
if not ctx.long_pos() and high_10d[-1] > high_10d[-2]:
ctx.buy_shares = 100
# Hold the position for 5 days.
ctx.hold_bars = 5
# Set a stop loss of 2%.
ctx.stop_loss_pct = 2
strategy = Strategy(YFinance(), start_date='1/1/2022', end_date='7/1/2022')
strategy.add_execution(
exec_fn, ['AAPL', 'MSFT'], indicators=highest('high_10d', 'close', period=10))
# Run the backtest after 20 days have passed.
result = strategy.backtest(warmup=20)
Model-based Strategy:
import pybroker
from pybroker import Alpaca, Strategy
def train_fn(train_data, test_data, ticker):
# Train the model using indicators stored in train_data.
...
return trained_model
# Register the model and its training function with PyBroker.
my_model = pybroker.model('my_model', train_fn, indicators=[...])
def exec_fn(ctx):
preds = ctx.preds('my_model')
# Open a long position given my_model's latest prediction.
if not ctx.long_pos() and preds[-1] > buy_threshold:
ctx.buy_shares = 100
# Close the long position given my_model's latest prediction.
elif ctx.long_pos() and preds[-1] < sell_threshold:
ctx.sell_all_shares()
alpaca = Alpaca(api_key=..., api_secret=...)
strategy = Strategy(alpaca, start_date='1/1/2022', end_date='7/1/2022')
strategy.add_execution(exec_fn, ['AAPL', 'MSFT'], models=my_model)
# Run Walkforward Analysis on 1 minute data using 5 windows with 50/50 train/test data.
result = strategy.walkforward(timeframe='1m', windows=5, train_size=0.5)
To learn how to use PyBroker, see the notebooks under the User Guide:
Installation
PyBroker supports Python 3.9+ on Windows, Mac, and Linux. You can install
PyBroker using pip
:
pip install -U lib-pybroker
Or you can clone the Git repository with:
git clone https://github.com/edtechre/pybroker
Getting Started with Data Sources
Welcome to PyBroker! The best place to start is to learn about DataSources. A DataSource
is a class that can fetch data from external sources, which you can then use to backtest your trading strategies.
Yahoo Finance
One of the built-in DataSources
in PyBroker is Yahoo Finance. To use it, you can import YFinance:
[1]:
from pybroker import YFinance
yfinance = YFinance()
df = yfinance.query(['AAPL', 'MSFT'], start_date='3/1/2021', end_date='3/1/2022')
df
Loading bar data...
[*********************100%%**********************] 2 of 2 completed
Loaded bar data: 0:00:00
[1]:
date | symbol | open | high | low | close | volume | adj_close | |
---|---|---|---|---|---|---|---|---|
0 | 2021-03-01 | AAPL | 123.750000 | 127.930000 | 122.790001 | 127.790001 | 116307900 | 125.599655 |
1 | 2021-03-01 | MSFT | 235.899994 | 237.470001 | 233.149994 | 236.940002 | 25324000 | 230.847702 |
2 | 2021-03-02 | AAPL | 128.410004 | 128.720001 | 125.010002 | 125.120003 | 102260900 | 122.975403 |
3 | 2021-03-02 | MSFT | 237.009995 | 237.300003 | 233.449997 | 233.869995 | 22812500 | 227.856628 |
4 | 2021-03-03 | AAPL | 124.809998 | 125.709999 | 121.839996 | 122.059998 | 112966300 | 119.967857 |
... | ... | ... | ... | ... | ... | ... | ... | ... |
501 | 2022-02-24 | MSFT | 272.510010 | 295.160004 | 271.519989 | 294.589996 | 56989700 | 289.353271 |
502 | 2022-02-25 | AAPL | 163.839996 | 165.119995 | 160.869995 | 164.850006 | 91974200 | 162.987427 |
503 | 2022-02-25 | MSFT | 295.140015 | 297.630005 | 291.649994 | 297.309998 | 32546700 | 292.024872 |
504 | 2022-02-28 | AAPL | 163.059998 | 165.419998 | 162.429993 | 165.119995 | 95056600 | 163.254364 |
505 | 2022-02-28 | MSFT | 294.309998 | 299.140015 | 293.000000 | 298.790009 | 34627500 | 293.478607 |
506 rows × 8 columns
The above code queries data for AAPL and MSFT stocks, and returns a Pandas DataFrame with the results.
Caching Data
If you want to speed up your data retrieval, you can cache your queries using PyBroker’s caching system. You can enable caching by calling pybroker.enable_data_source_cache(‘name’) where name
is the name of the cache you want to use:
[2]:
import pybroker
pybroker.enable_data_source_cache('yfinance')
[2]:
<diskcache.core.Cache at 0x7f3884390d60>
The next call to query will cache the returned data to disk. Each unique combination of ticker symbol and date range will be cached separately:
[3]:
yfinance.query(['TSLA', 'IBM'], '3/1/2021', '3/1/2022')
Loading bar data...
[*********************100%%**********************] 2 of 2 completed
Loaded bar data: 0:00:00
[3]:
date | symbol | open | high | low | close | volume | adj_close | |
---|---|---|---|---|---|---|---|---|
0 | 2021-03-01 | IBM | 115.057358 | 116.940727 | 114.588913 | 115.430206 | 5977367 | 100.173241 |
1 | 2021-03-01 | TSLA | 230.036667 | 239.666672 | 228.350006 | 239.476669 | 81408600 | 239.476669 |
2 | 2021-03-02 | IBM | 115.430206 | 116.539200 | 114.971321 | 115.038239 | 4732418 | 99.833076 |
3 | 2021-03-02 | TSLA | 239.426666 | 240.369995 | 228.333328 | 228.813339 | 71196600 | 228.813339 |
4 | 2021-03-03 | IBM | 115.200768 | 117.237091 | 114.703636 | 116.978966 | 7744898 | 101.517288 |
... | ... | ... | ... | ... | ... | ... | ... | ... |
501 | 2022-02-24 | TSLA | 233.463333 | 267.493347 | 233.333328 | 266.923340 | 135322200 | 266.923340 |
502 | 2022-02-25 | IBM | 122.050003 | 124.260002 | 121.449997 | 124.180000 | 4460900 | 113.041489 |
503 | 2022-02-25 | TSLA | 269.743347 | 273.166656 | 260.799988 | 269.956665 | 76067700 | 269.956665 |
504 | 2022-02-28 | IBM | 122.209999 | 123.389999 | 121.040001 | 122.510002 | 6757300 | 111.521271 |
505 | 2022-02-28 | TSLA | 271.670013 | 292.286682 | 271.570007 | 290.143341 | 99006900 | 290.143341 |
506 rows × 8 columns
Calling query
again with the same ticker symbols and date range returns the cached data:
[4]:
df = yfinance.query(['TSLA', 'IBM'], '3/1/2021', '3/1/2022')
df
Loaded cached bar data.
[4]:
date | symbol | open | high | low | close | volume | adj_close | |
---|---|---|---|---|---|---|---|---|
0 | 2021-03-01 | IBM | 115.057358 | 116.940727 | 114.588913 | 115.430206 | 5977367 | 100.173241 |
1 | 2021-03-02 | IBM | 115.430206 | 116.539200 | 114.971321 | 115.038239 | 4732418 | 99.833076 |
2 | 2021-03-03 | IBM | 115.200768 | 117.237091 | 114.703636 | 116.978966 | 7744898 | 101.517288 |
3 | 2021-03-04 | IBM | 116.634796 | 117.801147 | 113.537285 | 114.827919 | 8439651 | 99.650551 |
4 | 2021-03-05 | IBM | 115.334610 | 118.307838 | 114.961761 | 117.428299 | 7268968 | 101.907227 |
... | ... | ... | ... | ... | ... | ... | ... | ... |
248 | 2022-02-22 | TSLA | 278.043335 | 285.576660 | 267.033325 | 273.843323 | 83288100 | 273.843323 |
249 | 2022-02-23 | TSLA | 276.809998 | 278.433319 | 253.520004 | 254.679993 | 95256900 | 254.679993 |
250 | 2022-02-24 | TSLA | 233.463333 | 267.493347 | 233.333328 | 266.923340 | 135322200 | 266.923340 |
251 | 2022-02-25 | TSLA | 269.743347 | 273.166656 | 260.799988 | 269.956665 | 76067700 | 269.956665 |
252 | 2022-02-28 | TSLA | 271.670013 | 292.286682 | 271.570007 | 290.143341 | 99006900 | 290.143341 |
506 rows × 8 columns
You can clear your cache using pybroker.clear_data_source_cache:
[5]:
pybroker.clear_data_source_cache()
Or disable caching altogether using pybroker.disable_data_source_cache:
[6]:
pybroker.disable_data_source_cache()
Note that these calls should be made after first calling pybroker.enable_data_source_cache.
Alpaca
PyBroker also includes an Alpaca DataSource
for fetching stock data. To use it, you can import Alpaca and provide your API key and secret:
[7]:
from pybroker import Alpaca
import os
alpaca = Alpaca(os.environ['ALPACA_API_KEY'], os.environ['ALPACA_API_SECRET'])
You can query Alpaca
for stock data using the same syntax as with Yahoo Finance, but Alpaca also supports querying data by different timeframes. For example, to query 1 minute data:
[8]:
df = alpaca.query(
['AAPL', 'MSFT'],
start_date='3/1/2021',
end_date='4/1/2021',
timeframe='1m'
)
df
Loading bar data...
Loaded bar data: 0:00:05
[8]:
date | symbol | open | high | low | close | volume | vwap | |
---|---|---|---|---|---|---|---|---|
0 | 2021-03-01 04:00:00-05:00 | AAPL | 124.30 | 124.56 | 124.30 | 124.50 | 12267.0 | 124.433365 |
1 | 2021-03-01 04:00:00-05:00 | MSFT | 235.87 | 236.00 | 235.87 | 236.00 | 1429.0 | 235.938887 |
2 | 2021-03-01 04:01:00-05:00 | AAPL | 124.56 | 124.60 | 124.30 | 124.30 | 9439.0 | 124.481323 |
3 | 2021-03-01 04:01:00-05:00 | MSFT | 236.17 | 236.17 | 236.17 | 236.17 | 104.0 | 236.161538 |
4 | 2021-03-01 04:02:00-05:00 | AAPL | 124.00 | 124.05 | 123.78 | 123.78 | 4834.0 | 123.935583 |
... | ... | ... | ... | ... | ... | ... | ... | ... |
33340 | 2021-03-31 19:57:00-04:00 | MSFT | 237.28 | 237.28 | 237.28 | 237.28 | 507.0 | 237.367870 |
33341 | 2021-03-31 19:58:00-04:00 | AAPL | 122.36 | 122.39 | 122.33 | 122.39 | 3403.0 | 122.360544 |
33342 | 2021-03-31 19:58:00-04:00 | MSFT | 237.40 | 237.40 | 237.35 | 237.35 | 636.0 | 237.378066 |
33343 | 2021-03-31 19:59:00-04:00 | AAPL | 122.39 | 122.45 | 122.38 | 122.45 | 5560.0 | 122.402606 |
33344 | 2021-03-31 19:59:00-04:00 | MSFT | 237.40 | 237.53 | 237.40 | 237.53 | 1163.0 | 237.473801 |
33345 rows × 8 columns
Alpaca Crypto
If you are interested in fetching cryptocurrency data, you can use AlpacaCrypto. Here’s an example of how to use it:
[9]:
from pybroker import AlpacaCrypto
crypto = AlpacaCrypto(
os.environ['ALPACA_API_KEY'],
os.environ['ALPACA_API_SECRET']
)
df = crypto.query('BTC/USD', start_date='1/1/2021', end_date='2/1/2021', timeframe='1h')
df
Loading bar data...
Loaded bar data: 0:00:06
[9]:
symbol | date | open | high | low | close | volume | vwap | trade_count | |
---|---|---|---|---|---|---|---|---|---|
0 | BTC/USD | 2021-01-01 01:00:00-05:00 | 29255.71 | 29338.25 | 29153.55 | 29234.15 | 42.244289 | 29237.240312 | 1243.0 |
1 | BTC/USD | 2021-01-01 02:00:00-05:00 | 29235.61 | 29236.95 | 28905.00 | 29162.50 | 34.506038 | 29078.423185 | 1070.0 |
2 | BTC/USD | 2021-01-01 03:00:00-05:00 | 29162.50 | 29248.52 | 28948.86 | 29076.77 | 27.596804 | 29091.465155 | 1110.0 |
3 | BTC/USD | 2021-01-01 04:00:00-05:00 | 29075.31 | 29372.32 | 29058.05 | 29284.92 | 20.694200 | 29248.730924 | 880.0 |
4 | BTC/USD | 2021-01-01 05:00:00-05:00 | 29291.54 | 29400.00 | 29232.16 | 29286.63 | 16.617646 | 29338.609132 | 742.0 |
... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
735 | BTC/USD | 2021-01-31 15:00:00-05:00 | 32837.67 | 32964.87 | 32528.54 | 32882.87 | 40.631122 | 32818.132855 | 2197.0 |
736 | BTC/USD | 2021-01-31 16:00:00-05:00 | 32889.01 | 32935.98 | 32554.59 | 32586.68 | 26.673190 | 32737.975296 | 1625.0 |
737 | BTC/USD | 2021-01-31 17:00:00-05:00 | 32599.00 | 33126.32 | 32599.00 | 32998.35 | 25.422568 | 32923.438893 | 1770.0 |
738 | BTC/USD | 2021-01-31 18:00:00-05:00 | 33000.00 | 33263.94 | 32957.10 | 33134.86 | 31.072017 | 33147.086803 | 2203.0 |
739 | BTC/USD | 2021-01-31 19:00:00-05:00 | 33134.03 | 33134.03 | 32303.44 | 32572.03 | 60.460424 | 32552.937863 | 2665.0 |
740 rows × 9 columns
In the above example, we’re querying for hourly data for the BTC/USD currency pair.
Backtesting a Strategy
We’re all set to test a basic trading strategy using PyBroker! To get started, we’ll import the necessary classes listed below:
[1]:
import pybroker
from pybroker import Strategy, StrategyConfig, YFinance
pybroker.enable_data_source_cache('my_strategy')
[1]:
<diskcache.core.Cache at 0x7fda987c2850>
For our backtest, we’ll be using Yahoo Finance as our DataSource. We’ll also be using data source caching to ensure that we only download the necessary data once when we run our backtests.
The next step is to set up a new instance of the Strategy class which will be used to perform a backtest on our trading strategy. Here’s how you can do it:
First, you can create a StrategyConfig object to configure the Strategy
. In this case, we’re setting the initial cash to 500,000
:
[2]:
config = StrategyConfig(initial_cash=500_000)
Then, you can create a new instance of the Strategy
class by passing in the following arguments:
A data source: In this case, we’re using Yahoo Finance as the data source.
A start date: This is the starting date for the backtest.
An end date: This is the end date for the backtest.
The configuration object created earlier.
[3]:
strategy = Strategy(YFinance(), '3/1/2017', '3/1/2022', config)
The Strategy
instance is now ready to download data from Yahoo Finance for the period between March 1, 2017, and March 1, 2022, before running the backtest using the specified configuration options. If you need to modify other configuration options, you can refer to the StrategyConfig reference documentation.
Defining Strategy Rules
In this section, you will learn how to implement a basic trading strategy in PyBroker with the following rules:
Buy shares in a stock if the last close price is less than the low of the previous bar and there is no open long position in that stock.
Set the limit price of the buy order to 0.01 less than the last close price.
Hold the position for 3 days before liquidating it at market price.
Trade the rules on AAPL and MSFT, allocating up to 25% of the portfolio to each.
To accomplish this, you will define a buy_low
function that PyBroker will call separately for AAPL and MSFT on every bar of data. Each bar corresponds to a single day of data:
[4]:
def buy_low(ctx):
# If shares were already purchased and are currently being held, then return.
if ctx.long_pos():
return
# If the latest close price is less than the previous day's low price,
# then place a buy order.
if ctx.bars >= 2 and ctx.close[-1] < ctx.low[-2]:
# Buy a number of shares that is equal to 25% the portfolio.
ctx.buy_shares = ctx.calc_target_shares(0.25)
# Set the limit price of the order.
ctx.buy_limit_price = ctx.close[-1] - 0.01
# Hold the position for 3 bars before liquidating (in this case, 3 days).
ctx.hold_bars = 3
That is a lot to unpack! The buy_low
function will receive an ExecContext (ctx
) containing data for the current ticker symbol (AAPL or MSFT). The ExecContext
will contain all of the close prices up until the most recent bar of the current ticker symbol. The latest close price is retrieved with ctx.close[-1]
.
The buy_low
function will use the ExecContext
to place a buy order. The number of shares to purchase is set using ctx.buy_shares, which is calculated with ctx.calc_target_shares. In this case, the number of shares to buy will be equal to 25% of the
portfolio.
The limit price of the order is set with buy_limit_price. If the criteria are met, the buy order will be filled on the next bar. The time at which the order is filled can be configured with StrategyConfig.buy_delay, and its fill price can be set with
ExecContext.buy_fill_price. By default, buy orders are filled on the next bar (buy_delay=1
) and at a fill price equal to the midpoint between that bar’s low and high price.
Finally, ctx.hold_bars specifies how many bars to hold the position for before liquidating it. When liquidated, the shares are sold at market price equal to ExecContext.sell_fill_price, which is configurable and defaults to the midpoint between the bar’s low and high price.
To add the buy_low
rules to the Strategy
for AAPL and MSFT, you will use add_execution:
[5]:
strategy.add_execution(buy_low, ['AAPL', 'MSFT'])
Adding a Second Execution
You can use different sets of trading rules for different tickers within the same Strategy
instance. In other words, you are not restricted to using only one set of trading rules for a single group of tickers.
To demonstrate this, a new set of rules for a short strategy is provided in a function called short_high
, which is similar to the previous set of rules:
[6]:
def short_high(ctx):
# If shares were already shorted then return.
if ctx.short_pos():
return
# If the latest close price is more than the previous day's high price,
# then place a sell order.
if ctx.bars >= 2 and ctx.close[-1] > ctx.high[-2]:
# Short 100 shares.
ctx.sell_shares = 100
# Cover the shares after 2 bars (in this case, 2 days).
ctx.hold_bars = 2
The rules in short_high
will be traded on TSLA
:
[7]:
strategy.add_execution(short_high, ['TSLA'])
(Note, you can also retrieve bar data for another symbol by calling ExecContext#foreign)
Running a Backtest
To run a backtest, call the backtest method on the Strategy
instance. Here is an example:
[8]:
result = strategy.backtest()
Backtesting: 2017-03-01 00:00:00 to 2022-03-01 00:00:00
Loading bar data...
[*********************100%***********************] 3 of 3 completed
Loaded bar data: 0:00:01
Test split: 2017-03-01 00:00:00 to 2022-02-28 00:00:00
100% (1259 of 1259) |####################| Elapsed Time: 0:00:00 Time: 0:00:00
Finished backtest: 0:00:03
That was fast! The backtest
method will return an instance of TestResult. You can access various information and metrics about the backtest through this instance. For example, to see the daily balances of the portfolio, you can plot the market value using Matplotlib:
[9]:
import matplotlib.pyplot as plt
chart = plt.subplot2grid((3, 2), (0, 0), rowspan=3, colspan=2)
chart.plot(result.portfolio.index, result.portfolio['market_value'])
[9]:
[<matplotlib.lines.Line2D at 0x7fd9b6a3dfd0>]

You can also access the daily balance of each position that was held, the trades that were made for every entry and exit, and all of the orders that were placed:
[10]:
result.positions
[10]:
long_shares | short_shares | close | equity | market_value | margin | unrealized_pnl | ||
---|---|---|---|---|---|---|---|---|
symbol | date | |||||||
MSFT | 2017-03-03 | 1952 | 0 | 64.25 | 125416.00 | 125416.00 | 0.00 | 585.60 |
2017-03-06 | 1952 | 0 | 64.27 | 125455.03 | 125455.03 | 0.00 | 624.63 | |
2017-03-07 | 1952 | 0 | 64.40 | 125708.80 | 125708.80 | 0.00 | 878.40 | |
2017-03-14 | 1937 | 0 | 64.41 | 124762.18 | 124762.18 | 0.00 | 116.23 | |
TSLA | 2017-03-15 | 0 | 100 | 17.05 | 0.00 | 1718.00 | 1704.87 | 13.13 |
... | ... | ... | ... | ... | ... | ... | ... | ... |
MSFT | 2022-02-22 | 575 | 0 | 287.72 | 165439.00 | 165439.00 | 0.00 | -1357.00 |
AAPL | 2022-02-22 | 991 | 0 | 164.32 | 162841.13 | 162841.13 | 0.00 | -4003.63 |
MSFT | 2022-02-23 | 575 | 0 | 280.27 | 161155.24 | 161155.24 | 0.00 | -5640.76 |
AAPL | 2022-02-23 | 991 | 0 | 160.07 | 158629.38 | 158629.38 | 0.00 | -8215.38 |
TSLA | 2022-02-28 | 0 | 100 | 290.14 | 0.00 | 28193.00 | 29014.33 | -821.33 |
938 rows × 7 columns
[11]:
result.trades
[11]:
type | symbol | entry_date | exit_date | entry | exit | shares | pnl | return_pct | agg_pnl | bars | pnl_per_bar | stop | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
id | |||||||||||||
1 | long | MSFT | 2017-03-03 | 2017-03-08 | 63.95 | 64.67 | 1952 | 1405.44 | 1.13 | 1405.44 | 3 | 468.48 | bar |
2 | long | MSFT | 2017-03-14 | 2017-03-17 | 64.35 | 64.96 | 1937 | 1181.57 | 0.95 | 2587.01 | 3 | 393.86 | bar |
3 | short | TSLA | 2017-03-15 | 2017-03-17 | 17.18 | 17.55 | 100 | -37.00 | -2.11 | 2550.01 | 2 | -18.50 | bar |
4 | short | TSLA | 2017-03-27 | 2017-03-29 | 17.68 | 18.50 | 100 | -82.00 | -4.43 | 2468.01 | 2 | -41.00 | bar |
5 | short | TSLA | 2017-04-04 | 2017-04-06 | 19.98 | 19.87 | 100 | 11.00 | 0.55 | 2479.01 | 2 | 5.50 | bar |
... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
384 | long | AAPL | 2022-02-11 | 2022-02-16 | 170.56 | 171.69 | 970 | 1096.10 | 0.66 | 170660.61 | 3 | 365.37 | bar |
385 | long | MSFT | 2022-02-11 | 2022-02-16 | 299.26 | 297.27 | 552 | -1098.48 | -0.66 | 169562.13 | 3 | -366.16 | bar |
386 | short | TSLA | 2022-02-16 | 2022-02-18 | 304.61 | 287.41 | 100 | 1720.00 | 5.98 | 171282.13 | 2 | 860.00 | bar |
387 | long | AAPL | 2022-02-18 | 2022-02-24 | 168.36 | 157.43 | 991 | -10831.63 | -6.49 | 160450.50 | 3 | -3610.54 | bar |
388 | long | MSFT | 2022-02-18 | 2022-02-24 | 290.08 | 283.34 | 575 | -3875.50 | -2.32 | 156575.00 | 3 | -1291.83 | bar |
388 rows × 13 columns
[12]:
result.orders
[12]:
type | symbol | date | shares | limit_price | fill_price | fees | |
---|---|---|---|---|---|---|---|
id | |||||||
1 | buy | MSFT | 2017-03-03 | 1952 | 64.00 | 63.95 | 0.0 |
2 | sell | MSFT | 2017-03-08 | 1952 | NaN | 64.67 | 0.0 |
3 | buy | MSFT | 2017-03-14 | 1937 | 64.70 | 64.35 | 0.0 |
4 | sell | TSLA | 2017-03-15 | 100 | NaN | 17.18 | 0.0 |
5 | sell | MSFT | 2017-03-17 | 1937 | NaN | 64.96 | 0.0 |
... | ... | ... | ... | ... | ... | ... | ... |
773 | buy | AAPL | 2022-02-18 | 991 | 168.87 | 168.36 | 0.0 |
774 | buy | MSFT | 2022-02-18 | 575 | 290.72 | 290.08 | 0.0 |
775 | sell | AAPL | 2022-02-24 | 991 | NaN | 157.43 | 0.0 |
776 | sell | MSFT | 2022-02-24 | 575 | NaN | 283.34 | 0.0 |
777 | sell | TSLA | 2022-02-28 | 100 | NaN | 281.93 | 0.0 |
777 rows × 7 columns
Additionally, result.metrics_df
contains a DataFrame of metrics calculated using the returns of the backtest. You can read about what these metrics mean on the reference documentation.
[13]:
result.metrics_df
[13]:
name | value | |
---|---|---|
0 | trade_count | 388.000000 |
1 | initial_market_value | 500000.000000 |
2 | end_market_value | 655753.670000 |
3 | total_pnl | 156575.000000 |
4 | unrealized_pnl | -821.330000 |
5 | total_return_pct | 31.315000 |
6 | total_profit | 383032.400000 |
7 | total_loss | -226457.400000 |
8 | total_fees | 0.000000 |
9 | max_drawdown | -30181.580000 |
10 | max_drawdown_pct | -4.554816 |
11 | win_rate | 52.577320 |
12 | loss_rate | 47.422680 |
13 | winning_trades | 204.000000 |
14 | losing_trades | 184.000000 |
15 | avg_pnl | 403.543814 |
16 | avg_return_pct | 0.279639 |
17 | avg_trade_bars | 2.414948 |
18 | avg_profit | 1877.609804 |
19 | avg_profit_pct | 3.168775 |
20 | avg_winning_trade_bars | 2.465686 |
21 | avg_loss | -1230.746739 |
22 | avg_loss_pct | -2.923533 |
23 | avg_losing_trade_bars | 2.358696 |
24 | largest_win | 20797.970000 |
25 | largest_win_pct | 14.490000 |
26 | largest_win_bars | 3.000000 |
27 | largest_loss | -10831.630000 |
28 | largest_loss_pct | -6.490000 |
29 | largest_loss_bars | 3.000000 |
30 | max_wins | 7.000000 |
31 | max_losses | 7.000000 |
32 | sharpe | 0.054488 |
33 | sortino | 0.061320 |
34 | profit_factor | 1.312935 |
35 | ulcer_index | 0.627821 |
36 | upi | 0.035531 |
37 | equity_r2 | 0.893202 |
38 | std_error | 63596.828230 |
Filtering Backtest Data
You can filter the data used for the backtest to only include specific bars. For example, you can limit the strategy to trade only on Mondays by filtering the data to only contain bars for Mondays:
[14]:
result = strategy.backtest(days='mon')
result.orders
Backtesting: 2017-03-01 00:00:00 to 2022-03-01 00:00:00
Loaded cached bar data.
Test split: 2017-03-06 00:00:00 to 2022-02-28 00:00:00
100% (238 of 238) |######################| Elapsed Time: 0:00:00 Time: 0:00:00
Finished backtest: 0:00:00
[14]:
type | symbol | date | shares | limit_price | fill_price | fees | |
---|---|---|---|---|---|---|---|
id | |||||||
1 | sell | TSLA | 2017-03-27 | 100 | NaN | 17.68 | 0.0 |
2 | buy | TSLA | 2017-04-10 | 100 | NaN | 20.75 | 0.0 |
3 | sell | TSLA | 2017-04-17 | 100 | NaN | 20.09 | 0.0 |
4 | buy | TSLA | 2017-05-01 | 100 | NaN | 21.40 | 0.0 |
5 | sell | TSLA | 2017-05-08 | 100 | NaN | 20.65 | 0.0 |
... | ... | ... | ... | ... | ... | ... | ... |
178 | sell | TSLA | 2022-02-07 | 100 | NaN | 308.41 | 0.0 |
179 | sell | AAPL | 2022-02-14 | 599 | NaN | 168.07 | 0.0 |
180 | buy | MSFT | 2022-02-14 | 373 | 300.94 | 294.06 | 0.0 |
181 | buy | TSLA | 2022-02-28 | 100 | NaN | 281.93 | 0.0 |
182 | buy | AAPL | 2022-02-28 | 651 | 168.87 | 163.92 | 0.0 |
182 rows × 7 columns
The data doesn’t need to be downloaded again from Yahoo Finance because caching is enabled and the cached data only needs to be filtered.
You can also filter the data by time range, such as 9:30-10:30 AM, using the between_time argument.
Although the metrics earlier indicate that we have a profitable strategy, we may be misled by randomness. In the next notebook, we’ll discuss how to use bootstrapping to further evaluate our trading strategies.
Evaluating with Bootstrap Metrics
Bootstrap metrics can help us to more thoroughly evaluate a trading strategy, as we will see in this notebook.
In the previous notebook, we implemented a trading strategy and backtested it. Here is the implementation again:
[1]:
import pybroker
from pybroker import Strategy, StrategyConfig, YFinance
pybroker.enable_data_source_cache('my_strategy')
def buy_low(ctx):
if ctx.long_pos():
return
if ctx.bars >= 2 and ctx.close[-1] < ctx.low[-2]:
ctx.buy_shares = ctx.calc_target_shares(0.25)
ctx.buy_limit_price = ctx.close[-1] - 0.01
ctx.hold_bars = 3
def short_high(ctx):
if ctx.short_pos():
return
if ctx.bars >= 2 and ctx.close[-1] > ctx.high[-2]:
ctx.sell_shares = 100
ctx.hold_bars = 2
As before, we create a new Strategy instance with the given configurations:
[2]:
config = StrategyConfig(initial_cash=500_000, bootstrap_sample_size=100)
strategy = Strategy(YFinance(), '3/1/2017', '3/1/2022', config)
strategy.add_execution(buy_low, ['AAPL', 'MSFT'])
strategy.add_execution(short_high, ['TSLA'])
This time, the Strategy
is configured with a bootstrap_sample_size of 100
because of the small amount of historical data being used. Next, we run the backtest with bootstrap metrics enabled:
[3]:
result = strategy.backtest(calc_bootstrap=True)
result.metrics_df
Backtesting: 2017-03-01 00:00:00 to 2022-03-01 00:00:00
Loaded cached bar data.
Test split: 2017-03-01 00:00:00 to 2022-02-28 00:00:00
100% (1259 of 1259) |####################| Elapsed Time: 0:00:00 Time: 0:00:00
Calculating bootstrap metrics: sample_size=100, samples=10000...
Calculated bootstrap metrics: 0:00:03
Finished backtest: 0:00:05
[3]:
name | value | |
---|---|---|
0 | trade_count | 388.000000 |
1 | initial_market_value | 500000.000000 |
2 | end_market_value | 655753.670000 |
3 | total_pnl | 156575.000000 |
4 | unrealized_pnl | -821.330000 |
5 | total_return_pct | 31.315000 |
6 | total_profit | 383032.400000 |
7 | total_loss | -226457.400000 |
8 | total_fees | 0.000000 |
9 | max_drawdown | -30181.580000 |
10 | max_drawdown_pct | -4.554816 |
11 | win_rate | 52.577320 |
12 | loss_rate | 47.422680 |
13 | winning_trades | 204.000000 |
14 | losing_trades | 184.000000 |
15 | avg_pnl | 403.543814 |
16 | avg_return_pct | 0.279639 |
17 | avg_trade_bars | 2.414948 |
18 | avg_profit | 1877.609804 |
19 | avg_profit_pct | 3.168775 |
20 | avg_winning_trade_bars | 2.465686 |
21 | avg_loss | -1230.746739 |
22 | avg_loss_pct | -2.923533 |
23 | avg_losing_trade_bars | 2.358696 |
24 | largest_win | 20797.970000 |
25 | largest_win_pct | 14.490000 |
26 | largest_win_bars | 3.000000 |
27 | largest_loss | -10831.630000 |
28 | largest_loss_pct | -6.490000 |
29 | largest_loss_bars | 3.000000 |
30 | max_wins | 7.000000 |
31 | max_losses | 7.000000 |
32 | sharpe | 0.054488 |
33 | sortino | 0.061320 |
34 | profit_factor | 1.312935 |
35 | ulcer_index | 0.627821 |
36 | upi | 0.035531 |
37 | equity_r2 | 0.893202 |
38 | std_error | 63596.828230 |
When we look at the total_pnl
metric above, it seems that we have a profitable trading strategy on our first try. However, we cannot be completely sure that these results are repeatable and not just due to chance. To gain more confidence in our results, we can use the boostrap method to compute metrics.
The bootstrap method works by repeatedly computing a metric on random samples drawn from the backtest’s returns. Then, the metric is computed on each random sample, and the average is taken. By doing this on thousands of random samples, we obtain a more robust and accurate estimate of the metric.
Confidence Intervals
PyBroker applies the bootstrap method to calculate confidence intervals for two performance metrics, the Profit Factor and Sharpe Ratio:
[4]:
result.bootstrap.conf_intervals
[4]:
lower | upper | ||
---|---|---|---|
name | conf | ||
Profit Factor | 97.5% | 0.594243 | 4.400753 |
95% | 0.719539 | 3.715684 | |
90% | 0.877060 | 3.153457 | |
Sharpe Ratio | 97.5% | -0.136541 | 0.243573 |
95% | -0.100146 | 0.220099 | |
90% | -0.060583 | 0.193326 |
PyBroker uses the bias corrected and accelerated (BCa) bootstrap method to calculate the confidence intervals for these metrics. The returns are sampled per-bar rather than per-trade to capture more information in the metrics.
The resulting table shows the lower bound of the confidence interval at the given confidence level. This provides a more conservative estimate of the strategy’s performance. For example, we can be 97.5%
confident that the Sharpe Ratio is at or above a given value of x.
In this example, the Sharpe Ratio has negative lower bounds, and the lower bounds of the Profit Factor are less than 1, which suggests that the strategy is not reliable.
Maximum Drawdown
In this section, we examine the maximum drawdown of the strategy using the bootstrap method. The probabilities of the drawdown not exceeding certain values, represented in cash and percentage of portfolio equity, are displayed below:
[5]:
result.bootstrap.drawdown_conf
[5]:
amount | percent | |
---|---|---|
conf | ||
99.9% | -66401.25 | -10.462527 |
99% | -50062.49 | -7.963632 |
95% | -35794.82 | -5.848482 |
90% | -29931.10 | -4.912087 |
These confidence levels were obtained using per-bar returns from the backtest’s out-of-sample results, similar to how the Profit Factor and Sharpe Ratio were calculated.
We can observe that the bootstrapped max drawdown of -10.46%
at a 99.9%
confidence level is worse than the -4.55%
we saw in our original results. This highlights the importance of using randomized tests to evaluate the performance of your trading strategy.
Ranking and Position Sizing
In this notebook, we will learn about the features of PyBroker that enable you to rank ticker symbols and set position sizes for a group of symbols in your trading strategy. With these features, you can easily optimize your strategy and manage risk more effectively.
[1]:
import pybroker
from pybroker import Strategy, StrategyConfig, YFinance
pybroker.enable_data_source_cache('ranking_and_pos_sizing')
[1]:
<diskcache.core.Cache at 0x7fd17427dd60>
Ranking Ticker Symbols
In this section, we will learn about how to rank ticker symbols when placing buy orders. Let’s begin with an example of how to rank ticker symbols based on volume when placing buy orders.
[2]:
def buy_highest_volume(ctx):
# If there are no long positions across all tickers being traded:
if not tuple(ctx.long_positions()):
ctx.buy_shares = ctx.calc_target_shares(1)
ctx.hold_bars = 2
ctx.score = ctx.volume[-1]
The buy_highest_volume
function ranks ticker symbols by their most recent trading volume and allocates 100% of the portfolio for 2 bars. The ctx.score
is set to ctx.volume[-1]
, which is the most recent trading volume.
[3]:
config = StrategyConfig(max_long_positions=1)
strategy = Strategy(YFinance(), '6/1/2021', '6/1/2022', config)
strategy.add_execution(buy_highest_volume, ['T', 'F', 'GM', 'PFE'])
To limit the number of long positions that can be held at any time to 1
, we set max_long_positions to 1
in the StrategyConfig. In this example, we add the buy_highest_volume
function to the
Strategy object and specify the ticker symbols to trade: ['T', 'F', 'GM', 'PFE']
.
[4]:
result = strategy.backtest()
result.trades
Backtesting: 2021-06-01 00:00:00 to 2022-06-01 00:00:00
Loading bar data...
[*********************100%***********************] 4 of 4 completed
Loaded bar data: 0:00:00
Test split: 2021-06-01 00:00:00 to 2022-05-31 00:00:00
100% (253 of 253) |######################| Elapsed Time: 0:00:00 Time: 0:00:00
Finished backtest: 0:00:02
[4]:
type | symbol | entry_date | exit_date | entry | exit | shares | pnl | return_pct | agg_pnl | bars | pnl_per_bar | stop | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
id | |||||||||||||
1 | long | F | 2021-06-02 | 2021-06-04 | 14.85 | 16.13 | 6734 | 8619.52 | 8.62 | 8619.52 | 2 | 4309.76 | bar |
2 | long | F | 2021-06-07 | 2021-06-09 | 15.93 | 15.51 | 6801 | -2856.42 | -2.64 | 5763.10 | 2 | -1428.21 | bar |
3 | long | F | 2021-06-10 | 2021-06-14 | 15.43 | 15.06 | 6832 | -2527.84 | -2.40 | 3235.26 | 2 | -1263.92 | bar |
4 | long | F | 2021-06-15 | 2021-06-17 | 14.96 | 14.99 | 6900 | 207.00 | 0.20 | 3442.26 | 2 | 103.50 | bar |
5 | long | F | 2021-06-18 | 2021-06-22 | 14.61 | 14.96 | 7003 | 2451.05 | 2.40 | 5893.31 | 2 | 1225.53 | bar |
... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
80 | long | F | 2022-05-10 | 2022-05-12 | 13.43 | 12.47 | 7263 | -6972.48 | -7.15 | -9423.13 | 2 | -3486.24 | bar |
81 | long | F | 2022-05-13 | 2022-05-17 | 13.25 | 13.34 | 6835 | 615.15 | 0.68 | -8807.98 | 2 | 307.58 | bar |
82 | long | F | 2022-05-18 | 2022-05-20 | 13.03 | 12.59 | 6739 | -2965.16 | -3.38 | -11773.14 | 2 | -1482.58 | bar |
83 | long | F | 2022-05-23 | 2022-05-25 | 12.72 | 12.57 | 6936 | -1040.40 | -1.18 | -12813.54 | 2 | -520.20 | bar |
84 | long | F | 2022-05-26 | 2022-05-31 | 12.99 | 13.59 | 6711 | 4026.60 | 4.62 | -8786.94 | 2 | 2013.30 | bar |
84 rows × 13 columns
Setting Position Sizes
In PyBroker, you can set position sizes based on multiple tickers. To illustrate this, let’s take a simple buy and hold strategy that starts trading after 100 days and holds positions for 30 days:
[5]:
def buy_and_hold(ctx):
if not ctx.long_pos() and ctx.bars >= 100:
ctx.buy_shares = 100
ctx.hold_bars = 30
strategy = Strategy(YFinance(), '6/1/2021', '6/1/2022')
strategy.add_execution(buy_and_hold, ['T', 'F', 'GM', 'PFE'])
This will buy 100
shares in each of ['T', 'F', 'GM', 'PFE']
. But what if you don’t want to use equal position sizing? For example, you may want to size positions so that more shares are allocated to tickers with lower volatility to decrease the portfolio’s overall volatility.
To customize position sizing for each ticker, we can define a pos_size_handler function that calculates the position size for each ticker:
[6]:
import numpy as np
def pos_size_handler(ctx):
# Fetch all buy signals.
signals = tuple(ctx.signals("buy"))
# Return if there are no buy signals (i.e. there are only sell signals).
if not signals:
return
# Calculates the inverse volatility, where volatility is defined as the
# standard deviation of close prices for the last 100 days.
get_inverse_volatility = lambda signal: 1 / np.std(signal.bar_data.close[-100:])
# Sums the inverse volatilities for all of the buy signals.
total_inverse_volatility = sum(map(get_inverse_volatility, signals))
for signal in signals:
size = get_inverse_volatility(signal) / total_inverse_volatility
# Calculate the number of shares given the latest close price.
shares = ctx.calc_target_shares(size, signal.bar_data.close[-1], cash=95_000)
ctx.set_shares(signal, shares)
strategy.set_pos_size_handler(pos_size_handler)
The handler runs on every bar that generates a buy or sell signal when buy_shares or sell_shares is set on the ExecContext:
[7]:
result = strategy.backtest()
Backtesting: 2021-06-01 00:00:00 to 2022-06-01 00:00:00
Loaded cached bar data.
Test split: 2021-06-01 00:00:00 to 2022-05-31 00:00:00
100% (253 of 253) |######################| Elapsed Time: 0:00:00 Time: 0:00:00
Finished backtest: 0:00:00
[8]:
result.trades
[8]:
type | symbol | entry_date | exit_date | entry | exit | shares | pnl | return_pct | agg_pnl | bars | pnl_per_bar | stop | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
id | |||||||||||||
1 | long | T | 2021-10-21 | 2021-12-03 | 19.60 | 17.54 | 2248 | -4630.88 | -10.51 | -4630.88 | 30 | -154.36 | bar |
2 | long | F | 2021-10-21 | 2021-12-03 | 16.41 | 19.66 | 2028 | 6591.00 | 19.80 | 1960.12 | 30 | 219.70 | bar |
3 | long | GM | 2021-10-21 | 2021-12-03 | 58.20 | 60.28 | 129 | 268.32 | 3.57 | 2228.44 | 30 | 8.94 | bar |
4 | long | PFE | 2021-10-21 | 2021-12-03 | 42.76 | 53.75 | 235 | 2582.65 | 25.70 | 4811.09 | 30 | 86.09 | bar |
5 | long | T | 2021-12-06 | 2022-01-19 | 17.81 | 20.49 | 2665 | 7142.20 | 15.05 | 11953.29 | 30 | 238.07 | bar |
6 | long | F | 2021-12-06 | 2022-01-19 | 19.05 | 23.66 | 1075 | 4955.75 | 24.20 | 16909.04 | 30 | 165.19 | bar |
7 | long | GM | 2021-12-06 | 2022-01-19 | 59.72 | 57.99 | 209 | -361.57 | -2.90 | 16547.47 | 30 | -12.05 | bar |
8 | long | PFE | 2021-12-06 | 2022-01-19 | 52.57 | 53.97 | 290 | 406.00 | 2.66 | 16953.47 | 30 | 13.53 | bar |
9 | long | T | 2022-01-20 | 2022-03-04 | 20.53 | 17.87 | 2625 | -6982.50 | -12.96 | 9970.97 | 30 | -232.75 | bar |
10 | long | F | 2022-01-20 | 2022-03-04 | 22.22 | 17.01 | 789 | -4110.69 | -23.45 | 5860.28 | 30 | -137.02 | bar |
11 | long | GM | 2022-01-20 | 2022-03-04 | 55.88 | 43.08 | 255 | -3264.00 | -22.91 | 2596.28 | 30 | -108.80 | bar |
12 | long | PFE | 2022-01-20 | 2022-03-04 | 53.80 | 48.09 | 192 | -1096.32 | -10.61 | 1499.96 | 30 | -36.54 | bar |
13 | long | T | 2022-03-07 | 2022-04-19 | 17.88 | 19.51 | 3132 | 5105.16 | 9.12 | 6605.12 | 30 | 170.17 | bar |
14 | long | F | 2022-03-07 | 2022-04-19 | 16.43 | 15.98 | 1303 | -586.35 | -2.74 | 6018.77 | 30 | -19.55 | bar |
15 | long | GM | 2022-03-07 | 2022-04-19 | 41.09 | 41.50 | 220 | 90.20 | 1.00 | 6108.97 | 30 | 3.01 | bar |
16 | long | PFE | 2022-03-07 | 2022-04-19 | 48.18 | 50.62 | 200 | 488.00 | 5.06 | 6596.97 | 30 | 16.27 | bar |
Using this method allows for a lot of possibilities, such as using Mean-Variance Optimization to determine portfolio allocations.
In the next notebook, we will discuss how to implement custom indicators in PyBroker.
Writing Indicators
This notebook explains how to create and integrate custom stock indicators in PyBroker. Indicators in PyBroker are written using NumPy, a powerful library for numerical computing. To optimize performance, we’ll also be utilizing Numba, a JIT compiler that translates Python code into efficient machine code. Numba is especially helpful for accelerating code that involves loops and NumPy arrays. Here’s how we import these libraries:
[1]:
import numpy as np
from numba import njit
The following code shows an indicator function that calculates close prices minus a moving average (CMMA), which can be used for a mean reversion strategy:
[2]:
def cmma(bar_data, lookback):
@njit # Enable Numba JIT.
def vec_cmma(values):
# Initialize the result array.
n = len(values)
out = np.array([np.nan for _ in range(n)])
# For all bars starting at lookback:
for i in range(lookback, n):
# Calculate the moving average for the lookback.
ma = 0
for j in range(i - lookback, i):
ma += values[j]
ma /= lookback
# Subtract the moving average from value.
out[i] = values[i] - ma
return out
# Calculate with close prices.
return vec_cmma(bar_data.close)
The cmma
function takes two arguments: bar_data
, which is an instance of the BarData class that holds OHLCV data and custom fields, and lookback
, which is a user-defined argument for the lookback of the moving average.
The vec_cmma
function is JIT-compiled by Numba and nested inside cmma
. This is necessary since a Numba compiled function supports a NumPy array as an argument but not an instance of a Python class like BarData
. Note the computation of the indicator values is vectorized by Numba, meaning that it’s performed on all of the historical data at once. This approach significantly speeds up the backtesting process.
The next step is to register the indicator function with PyBroker using the following code:
[3]:
import pybroker
cmma_20 = pybroker.indicator('cmma_20', cmma, lookback=20)
Here, we are giving the name cmma_20
to the indicator function and specifying the lookback
parameter as 20
bars. Any arguments in the indicator function that come after bar_data
will be passed as user-defined arguments to pybroker.indicator. Once the indicator function is registered with PyBroker, it will return a new
Indicator instance that references the indicator function we defined.
The following is an example of how to use the registered Indicator
in PyBroker with some data downloaded from Yahoo Finance:
[4]:
from pybroker import YFinance
pybroker.enable_data_source_cache('yfinance')
yfinance = YFinance()
df = yfinance.query('PG', '4/1/2020', '4/1/2022')
Loading bar data...
[*********************100%***********************] 1 of 1 completed
Loaded bar data: 0:00:01
[5]:
cmma_20(df)
[5]:
2020-04-01 NaN
2020-04-02 NaN
2020-04-03 NaN
2020-04-06 NaN
2020-04-07 NaN
...
2022-03-25 1.967502
2022-03-28 3.288005
2022-03-29 4.968507
2022-03-30 3.790999
2022-03-31 2.171002
Length: 505, dtype: float64
As you can see, the Indicator
instance is a Callable
. Once called, the resulting computed indicator values are returned as a Pandas Series.
The Indicator
class also provides functions for measuring its information content. For example, you can compute the interquartile range (IQR):
[6]:
cmma_20.iqr(df)
[6]:
4.655495452880842
Or compute the relative entropy:
[7]:
cmma_20.relative_entropy(df)
[7]:
0.7495800114455111
Using the Indicator in a Strategy
After implementing our indicator, the next step is to integrate it into a trading strategy. The following example shows a simple strategy that goes long when the 20-day CMMA is less than 0 — i.e. when the last close price drops below the 20-day moving average:
[8]:
def buy_cmma_cross(ctx):
if ctx.long_pos():
return
# Place a buy order if the most recent value of the 20 day CMMA is < 0:
if ctx.indicator('cmma_20')[-1] < 0:
ctx.buy_shares = ctx.calc_target_shares(1)
ctx.hold_bars = 3
The indicator values are retrieved by calling ctx.indicator on the ExecContext and passing in the registered name of the cmma_20
indicator.
(Note, you can also retrieve indicator data for another symbol by passing the symbol to ExecContext#indicator())
[9]:
from pybroker import Strategy
strategy = Strategy(yfinance, '4/1/2020', '4/1/2022')
strategy.add_execution(buy_cmma_cross, 'PG', indicators=cmma_20)
Here, the buy_cmma_cross
function is added to the Strategy along with the cmma_20
indicator. We can enable caching of the computed indicator values to disk with the following:
[10]:
pybroker.enable_indicator_cache('my_indicators')
[10]:
<diskcache.core.Cache at 0x7f45b0a73bb0>
Finally, we can run the backtest with the following code. The warmup
argument specifies that 20 bars need to pass before running the backtest execution:
[11]:
result = strategy.backtest(warmup=20)
result.metrics_df.round(4)
Backtesting: 2020-04-01 00:00:00 to 2022-04-01 00:00:00
Loaded cached bar data.
Computing indicators...
100% (1 of 1) |##########################| Elapsed Time: 0:00:00 Time: 0:00:00
Test split: 2020-04-01 00:00:00 to 2022-03-31 00:00:00
100% (505 of 505) |######################| Elapsed Time: 0:00:00 Time: 0:00:00
Finished backtest: 0:00:01
[11]:
name | value | |
---|---|---|
0 | trade_count | 60.0000 |
1 | initial_market_value | 100000.0000 |
2 | end_market_value | 100759.3600 |
3 | total_pnl | 759.3600 |
4 | unrealized_pnl | 0.0000 |
5 | total_return_pct | 0.7594 |
6 | total_profit | 41596.7500 |
7 | total_loss | -40837.3900 |
8 | total_fees | 0.0000 |
9 | max_drawdown | -13446.9300 |
10 | max_drawdown_pct | -11.9774 |
11 | win_rate | 53.3333 |
12 | loss_rate | 46.6667 |
13 | winning_trades | 32.0000 |
14 | losing_trades | 28.0000 |
15 | avg_pnl | 12.6560 |
16 | avg_return_pct | 0.0293 |
17 | avg_trade_bars | 3.0000 |
18 | avg_profit | 1299.8984 |
19 | avg_profit_pct | 1.2609 |
20 | avg_winning_trade_bars | 3.0000 |
21 | avg_loss | -1458.4782 |
22 | avg_loss_pct | -1.3782 |
23 | avg_losing_trade_bars | 3.0000 |
24 | largest_win | 4263.4500 |
25 | largest_win_pct | 4.1000 |
26 | largest_win_bars | 3.0000 |
27 | largest_loss | -4675.6700 |
28 | largest_loss_pct | -4.1700 |
29 | largest_loss_bars | 3.0000 |
30 | max_wins | 7.0000 |
31 | max_losses | 4.0000 |
32 | sharpe | 0.0023 |
33 | profit_factor | 1.0092 |
34 | ulcer_index | 1.8823 |
35 | upi | 0.0019 |
36 | equity_r2 | 0.0015 |
37 | std_error | 3385.1968 |
When the backtest runs, PyBroker computes the indicator values. If there are multiple indicators added to the Strategy
, then PyBroker will compute them in parallel across multiple CPU cores.
Vectorized Helpers
The PyBroker library provides vectorized helper functions to make the process of computing indicators easier. One of these helper functions is highv, which calculates the highest value for every period of n bars.
In the example code, an indicator function called hhv
is defined that uses highv
to calculate the highest high price for every period of 5 bars:
[12]:
from pybroker import highv
def hhv(bar_data, period):
return highv(bar_data.high, period)
hhv_5 = pybroker.indicator('hhv_5', hhv, period=5)
hhv_5(df)
[12]:
2020-04-01 NaN
2020-04-02 NaN
2020-04-03 NaN
2020-04-06 NaN
2020-04-07 120.059998
...
2022-03-25 153.919998
2022-03-28 153.919998
2022-03-29 156.470001
2022-03-30 156.470001
2022-03-31 156.470001
Length: 505, dtype: float64
The pybroker.vect module also includes other vectorized helpers such as lowv, sumv, returnv, and cross, the last of which is used to compute crossovers.
Additionally, PyBroker includes convenient wrappers for highest and lowest indicators. Our hhv
indicator can be rewritten as:
[13]:
from pybroker import highest
hhv_5 = highest('hhv_5', 'high', period=5)
hhv_5(df)
[13]:
2020-04-01 NaN
2020-04-02 NaN
2020-04-03 NaN
2020-04-06 NaN
2020-04-07 120.059998
...
2022-03-25 153.919998
2022-03-28 153.919998
2022-03-29 156.470001
2022-03-30 156.470001
2022-03-31 156.470001
Length: 505, dtype: float64
Computing Multiple Indicators
An IndicatorSet can be used to calculate multiple indicators. The cmma_20
and hhv_5
indicators can be computed together by adding them to the IndicatorSet
. The resulting output will be a Pandas DataFrame containing both:
[14]:
from pybroker import IndicatorSet
indicator_set = IndicatorSet()
indicator_set.add(cmma_20, hhv_5)
indicator_set(df)
Computing indicators...
100% (2 of 2) |##########################| Elapsed Time: 0:00:01 Time: 0:00:01
[14]:
symbol | date | cmma_20 | hhv_5 | |
---|---|---|---|---|
0 | PG | 2020-04-01 | NaN | NaN |
1 | PG | 2020-04-02 | NaN | NaN |
2 | PG | 2020-04-03 | NaN | NaN |
3 | PG | 2020-04-06 | NaN | NaN |
4 | PG | 2020-04-07 | NaN | 120.059998 |
... | ... | ... | ... | ... |
500 | PG | 2022-03-25 | 1.967502 | 153.919998 |
501 | PG | 2022-03-28 | 3.288005 | 153.919998 |
502 | PG | 2022-03-29 | 4.968507 | 156.470001 |
503 | PG | 2022-03-30 | 3.790999 | 156.470001 |
504 | PG | 2022-03-31 | 2.171002 | 156.470001 |
505 rows × 4 columns
Using TA-Lib
TA-Lib is a widely used technical analysis library that implements many financial indicators. Integrating TA-Lib with PyBroker is straightforward. Here is an example:
[15]:
import talib
rsi_20 = pybroker.indicator('rsi_20', lambda data: talib.RSI(data.close, timeperiod=20))
rsi_20(df)
[15]:
2020-04-01 NaN
2020-04-02 NaN
2020-04-03 NaN
2020-04-06 NaN
2020-04-07 NaN
...
2022-03-25 49.373093
2022-03-28 51.014810
2022-03-29 53.407971
2022-03-30 51.610544
2022-03-31 49.029540
Length: 505, dtype: float64
In the next tutorial, you will learn how to train a model using custom indicators in PyBroker.
Training a Model
In the last notebook, we learned how to write stock indicators in PyBroker. Indicators are a good starting point for developing a trading strategy. But to create a successful strategy, it is likely that a more sophisticated approach using predictive modeling will be needed.
Fortunately, one of the main features of PyBroker is the ability to train and backtest machine learning models. These models can utilize indicators as features to make more accurate predictions about market movements. Once trained, these models can be backtested using a popular technique known as Walkforward Analysis, which simulates how a strategy would perform during actual trading.
We’ll explain Walkforward Analysis more in depth later in this notebook. But first, let’s get started with some needed imports!
[1]:
import numpy as np
import pandas as pd
import pybroker
from numba import njit
from pybroker import Strategy, StrategyConfig, YFinance
As with DataSource and Indicator data, PyBroker can also cache trained models to disk. You can enable caching for all three by calling pybroker.enable_caches:
[2]:
pybroker.enable_caches('walkforward_strategy')
In the last notebook, we implemented an indicator that calculates the close-minus-moving-average (CMMA) using NumPy and Numba. Here’s the code for the CMMA indicator again:
[3]:
def cmma(bar_data, lookback):
@njit # Enable Numba JIT.
def vec_cmma(values):
# Initialize the result array.
n = len(values)
out = np.array([np.nan for _ in range(n)])
# For all bars starting at lookback:
for i in range(lookback, n):
# Calculate the moving average for the lookback.
ma = 0
for j in range(i - lookback, i):
ma += values[j]
ma /= lookback
# Subtract the moving average from value.
out[i] = values[i] - ma
return out
# Calculate for close prices.
return vec_cmma(bar_data.close)
cmma_20 = pybroker.indicator('cmma_20', cmma, lookback=20)
Train and Backtest
Next, we want to build a model that predicts the next day’s return using the 20-day CMMA. Using simple linear regression is a good approach to begin experimenting with. Below we import a LinearRegression model from scikit-learn:
[4]:
from sklearn.linear_model import LinearRegression
from sklearn.metrics import r2_score
We create a train_slr
function to train the LinearRegression
model:
[5]:
def train_slr(symbol, train_data, test_data):
# Train
# Previous day close prices.
train_prev_close = train_data['close'].shift(1)
# Calculate daily returns.
train_daily_returns = (train_data['close'] - train_prev_close) / train_prev_close
# Predict next day's return.
train_data['pred'] = train_daily_returns.shift(-1)
train_data = train_data.dropna()
# Train the LinearRegession model to predict the next day's return
# given the 20-day CMMA.
X_train = train_data[['cmma_20']]
y_train = train_data[['pred']]
model = LinearRegression()
model.fit(X_train, y_train)
# Test
test_prev_close = test_data['close'].shift(1)
test_daily_returns = (test_data['close'] - test_prev_close) / test_prev_close
test_data['pred'] = test_daily_returns.shift(-1)
test_data = test_data.dropna()
X_test = test_data[['cmma_20']]
y_test = test_data[['pred']]
# Make predictions from test data.
y_pred = model.predict(X_test)
# Print goodness of fit.
r2 = r2_score(y_test, np.squeeze(y_pred))
print(symbol, f'R^2={r2}')
# Return the trained model and columns to use as input data.
return model, ['cmma_20']
The train_slr
function uses the 20-day CMMA as the input feature, or predictor, for the LinearRegression
model. The function then fits the LinearRegression
model to the training data for that stock symbol.
After fitting the model, the function uses the testing data to evaluate the model’s accuracy, specifically by computing the R-squared score. The R-squared score provides a measure of how well the LinearRegression
model fits the testing data.
The final output of the train_slr
function is the trained LinearRegression
model specifically for that stock symbol, along with the cmma_20
column, which is to be used as input data when making predictions. PyBroker will use this model to predict the next day’s return of the stock during the backtest. The train_slr
function will be called for each stock symbol, and the trained models will be used to predict the next day’s return for each individual stock.
Once the function to train the model has been defined, it needs to be registered with PyBroker. This is done by creating a new ModelSource instance using the pybroker.model function. The arguments to this function are the name of the model ('slr'
in this case), the function that will train the model
(train_slr
), and a list of indicators to use as inputs for the model (in this case, cmma_20
).
[6]:
model_slr = pybroker.model('slr', train_slr, indicators=[cmma_20])
To create a trading strategy that uses the trained model, a new Strategy object is created using the YFinance data source, and specifying the start and end dates for the backtest period.
[7]:
config = StrategyConfig(bootstrap_sample_size=100)
strategy = Strategy(YFinance(), '3/1/2017', '3/1/2022', config)
strategy.add_execution(None, ['NVDA', 'AMD'], models=model_slr)
The add_execution method is then called on the Strategy object to specify the details of the trading execution. In this case, a None
value is passed as the first argument, which means that no trading function will be used during the backtest.
The last step is to run the backtest by calling the backtest method on the Strategy
object, with a train_size
of 0.5
to specify that the model should be trained on the first half of the backtest data, and tested on the second half.
[8]:
strategy.backtest(train_size=0.5)
Backtesting: 2017-03-01 00:00:00 to 2022-03-01 00:00:00
Loading bar data...
[*********************100%***********************] 2 of 2 completed
Loaded bar data: 0:00:00
Computing indicators...
100% (2 of 2) |##########################| Elapsed Time: 0:00:01 Time: 0:00:01
Train split: 2017-03-01 00:00:00 to 2019-08-28 00:00:00
AMD R^2=-0.006808549721842416
NVDA R^2=-0.004416132743176426
Finished training models: 0:00:00
Finished backtest: 0:00:01
Walkforward Analysis
PyBroker employs a powerful algorithm known as Walkforward Analysis to perform backtesting. The algorithm partitions the backtest data into a fixed number of time windows, each containing a train-test split of data.
The Walkforward Analysis algorithm then proceeds to “walk forward” in time, in the same manner that a trading strategy would be executed in the real world. The model is first trained on the earliest window and then evaluated on the test data in that window.
As the algorithm moves forward to evaluate the next window in time, the test data from the previous window is added to the training data. This process continues until all of the time windows are evaluated.
By using this approach, the Walkforward Analysis algorithm is able to simulate the real-world performance of a trading strategy, and produce more reliable and accurate backtesting results.
Let’s consider a trading strategy that generates buy and sell signals from the LinearRegression model that we trained earlier. The strategy is implemented as the hold_long
function:
[9]:
def hold_long(ctx):
if not ctx.long_pos():
# Buy if the next bar is predicted to have a positive return:
if ctx.preds('slr')[-1] > 0:
ctx.buy_shares = 100
else:
# Sell if the next bar is predicted to have a negative return:
if ctx.preds('slr')[-1] < 0:
ctx.sell_shares = 100
strategy.clear_executions()
strategy.add_execution(hold_long, ['NVDA', 'AMD'], models=model_slr)
The hold_long
function opens a long position when the model predicts a positive return for the next bar, and then closes the position when the model predicts a negative return.
The ctx.preds(‘slr’) method is used to access the predictions made by the 'slr'
model for the current stock symbol being executed in the function (NVDA or AMD). The predictions are stored in a NumPy array, and the most recent prediction for the current stock symbol is accessed using ctx.preds('slr')[-1]
, which
is the model’s prediction of the next bar’s return.
Now that we have defined a trading strategy and registered the 'slr'
model, we can run the backtest using the Walkforward Analysis algorithm.
The backtest is run by calling the walkforward method on the Strategy
object, with the desired number of time windows and train/test split ratio. In this case, we will use 3 time windows, each with a 50/50 train-test split.
Additionally, since our 'slr'
model makes a prediction for one bar in the future, we need to specify the lookahead
parameter as 1
. This is necessary to ensure that training data does not leak into the test boundary. The lookahead
parameter should always be set to the number of bars in the future being predicted.
[10]:
result = strategy.walkforward(
warmup=20,
windows=3,
train_size=0.5,
lookahead=1,
calc_bootstrap=True
)
Backtesting: 2017-03-01 00:00:00 to 2022-03-01 00:00:00
Loaded cached bar data.
Loaded cached indicator data.
Train split: 2017-03-06 00:00:00 to 2018-06-01 00:00:00
AMD R^2=-0.007950114729117885
NVDA R^2=-0.04203364470839133
Finished training models: 0:00:00
Test split: 2018-06-04 00:00:00 to 2019-08-30 00:00:00
100% (314 of 314) |######################| Elapsed Time: 0:00:00 Time: 0:00:00
Train split: 2018-06-04 00:00:00 to 2019-08-30 00:00:00
AMD R^2=0.0006422677593683757
NVDA R^2=-0.023591728578221893
Finished training models: 0:00:00
Test split: 2019-09-03 00:00:00 to 2020-11-27 00:00:00
100% (314 of 314) |######################| Elapsed Time: 0:00:00 Time: 0:00:00
Train split: 2019-09-03 00:00:00 to 2020-11-27 00:00:00
AMD R^2=-0.015508227883924253
NVDA R^2=-0.4567200095787838
Finished training models: 0:00:00
Test split: 2020-11-30 00:00:00 to 2022-02-28 00:00:00
100% (314 of 314) |######################| Elapsed Time: 0:00:00 Time: 0:00:00
Calculating bootstrap metrics: sample_size=100, samples=10000...
Calculated bootstrap metrics: 0:00:03
Finished backtest: 0:00:04
During the backtesting process using the Walkforward Analysis algorithm, the 'slr'
model is trained on a given window’s training data, and then the hold_long
function runs on the same window’s test data.
The model is trained on the training data to make predictions about the next day’s returns. The hold_long
function then uses these predictions to make buy or sell decisions for the current day’s trading session.
By evaluating the performance of the trading strategy on the test data for each window, we can see how well the strategy is likely to perform in real-world trading conditions. This process is repeated for each time window in the backtest, using the results to evaluate the overall performance of the trading strategy:
[11]:
result.metrics_df
[11]:
name | value | |
---|---|---|
0 | trade_count | 43.000000 |
1 | initial_market_value | 100000.000000 |
2 | end_market_value | 109831.000000 |
3 | total_pnl | 12645.000000 |
4 | unrealized_pnl | -2814.000000 |
5 | total_return_pct | 12.645000 |
6 | total_profit | 20566.000000 |
7 | total_loss | -7921.000000 |
8 | total_fees | 0.000000 |
9 | max_drawdown | -14177.000000 |
10 | max_drawdown_pct | -12.272121 |
11 | win_rate | 76.744186 |
12 | loss_rate | 23.255814 |
13 | winning_trades | 33.000000 |
14 | losing_trades | 10.000000 |
15 | avg_pnl | 294.069767 |
16 | avg_return_pct | 5.267674 |
17 | avg_trade_bars | 25.488372 |
18 | avg_profit | 623.212121 |
19 | avg_profit_pct | 9.237576 |
20 | avg_winning_trade_bars | 19.151515 |
21 | avg_loss | -792.100000 |
22 | avg_loss_pct | -7.833000 |
23 | avg_losing_trade_bars | 46.400000 |
24 | largest_win | 2715.000000 |
25 | largest_win_pct | 9.320000 |
26 | largest_win_bars | 2.000000 |
27 | largest_loss | -5054.000000 |
28 | largest_loss_pct | -16.140000 |
29 | largest_loss_bars | 43.000000 |
30 | max_wins | 13.000000 |
31 | max_losses | 2.000000 |
32 | sharpe | 0.023425 |
33 | profit_factor | 1.094471 |
34 | ulcer_index | 1.177116 |
35 | upi | 0.009193 |
36 | equity_r2 | 0.772082 |
37 | std_error | 4191.846954 |
[12]:
result.bootstrap.conf_intervals
[12]:
lower | upper | ||
---|---|---|---|
name | conf | ||
Profit Factor | 97.5% | 0.259819 | 1.296660 |
95% | 0.303435 | 1.151299 | |
90% | 0.373167 | 1.002514 | |
Sharpe Ratio | 97.5% | -0.359565 | 0.050383 |
95% | -0.332180 | 0.018154 | |
90% | -0.276757 | -0.018004 |
[13]:
result.bootstrap.drawdown_conf
[13]:
amount | percent | |
---|---|---|
conf | ||
99.9% | -13917.50 | -12.190522 |
99% | -11058.25 | -9.693729 |
95% | -8380.25 | -7.480589 |
90% | -7129.00 | -6.403027 |
In summary, we have now completed the process of training and backtesting a linear regression model using PyBroker, with the help of Walkforward Analysis. The metrics that we have seen are based on the test data from all of the time windows in the backtest. Although our trading strategy needs to be improved, we have gained a good understanding of how to train and evaluate a model in PyBroker.
Please keep in mind that before conducting regression analysis, it is important to verify certain assumptions such as homoscedasticity, normality of residuals, etc. I have not provided the details for these assumptions here for the sake of brevity and recommend that you perform this exercise on your own.
We are also not limited to just building linear regression models in PyBroker. We can train other model types such as gradient boosted machines, neural networks, or any other architecture that we choose. This flexibility allows us to explore and experiment with various models and approaches to find the best performing model for our specific trading goals.
PyBroker also offers customization options, such as the ability to specify an input_data_fn for our model in case we need to customize how its input data is built. This would be required when constructing input for autoregressive models (i.e. ARMA or RNN) that use multiple past values to make predictions. Similarly, we can specify our own
predict_fn to customize how predictions are made (by default, the model’s predict
function is called).
With this knowledge, you can start building and testing your own models and trading strategies in PyBroker, and begin exploring the vast possibilities that this framework offers!
Creating a Custom Data Source
PyBroker comes with pre-built DataSources for Yahoo Finance, Alpaca, and AKShare, which you can use right away without any additional setup. But if you have a specific need or
want to use a different data source, PyBroker also allows you to create your own DataSource
class.
Extending DataSource
In the example code provided below, a new DataSource
called CSVDataSource
is implemented, which loads data from a CSV file. The CSVDataSource
reads a file named prices.csv
into a Pandas DataFrame, and then returns the data from this DataFrame based on the input parameters provided:
[1]:
import pandas as pd
import pybroker
from pybroker.data import DataSource
class CSVDataSource(DataSource):
def __init__(self):
super().__init__()
# Register custom columns in the CSV.
pybroker.register_columns('rsi')
def _fetch_data(self, symbols, start_date, end_date, _timeframe, _adjust):
df = pd.read_csv('data/prices.csv')
df = df[df['symbol'].isin(symbols)]
df['date'] = pd.to_datetime(df['date'])
return df[(df['date'] >= start_date) & (df['date'] <= end_date)]
To make the custom 'rsi'
column from the CSV file available to PyBroker, we register it using pybroker.register_columns. This allows PyBroker to use this custom column when it processes the data.
It’s important to note that when returning the data from your custom DataSource, it must include the following columns: symbol
, date
, open
, high
, low
, and close
, as these columns are expected by PyBroker.
Now we can query the CSV data from an instance of CSVDataSource
:
[2]:
csv_data_source = CSVDataSource()
df = csv_data_source.query(['MCD', 'NKE', 'DIS'], '6/1/2021', '12/1/2021')
df
Loading bar data...
Loaded bar data: 0:00:00
[2]:
date | symbol | open | high | low | close | rsi | |
---|---|---|---|---|---|---|---|
0 | 2021-06-01 | DIS | 180.179993 | 181.009995 | 178.740005 | 178.839996 | 46.321532 |
1 | 2021-06-01 | MCD | 235.979996 | 235.990005 | 232.740005 | 233.240005 | 46.522926 |
2 | 2021-06-01 | NKE | 137.850006 | 138.050003 | 134.210007 | 134.509995 | 53.308085 |
3 | 2021-06-02 | DIS | 179.039993 | 179.100006 | 176.929993 | 177.000000 | 42.635256 |
4 | 2021-06-02 | MCD | 233.970001 | 234.330002 | 232.809998 | 233.779999 | 48.051484 |
... | ... | ... | ... | ... | ... | ... | ... |
382 | 2021-11-30 | MCD | 247.380005 | 247.899994 | 243.949997 | 244.600006 | 40.461178 |
383 | 2021-11-30 | NKE | 168.789993 | 171.550003 | 167.529999 | 169.240005 | 51.505558 |
384 | 2021-12-01 | DIS | 146.699997 | 148.369995 | 142.039993 | 142.149994 | 16.677555 |
385 | 2021-12-01 | MCD | 245.759995 | 250.899994 | 244.110001 | 244.179993 | 39.853689 |
386 | 2021-12-01 | NKE | 170.889999 | 173.369995 | 166.679993 | 166.699997 | 46.704527 |
387 rows × 7 columns
To use CSVDataSource
in a backtest, we create a new Strategy object and pass the custom DataSource
:
[3]:
from pybroker import Strategy
def buy_low_sell_high_rsi(ctx):
pos = ctx.long_pos()
if not pos and ctx.rsi[-1] < 30:
ctx.buy_shares = 100
elif pos and ctx.rsi[-1] > 70:
ctx.sell_shares = pos.shares
strategy = Strategy(csv_data_source, '6/1/2021', '12/1/2021')
strategy.add_execution(buy_low_sell_high_rsi, ['MCD', 'NKE', 'DIS'])
result = strategy.backtest()
result.orders
Backtesting: 2021-06-01 00:00:00 to 2021-12-01 00:00:00
Loading bar data...
Loaded bar data: 0:00:00
Test split: 2021-06-01 00:00:00 to 2021-12-01 00:00:00
100% (129 of 129) |######################| Elapsed Time: 0:00:00 Time: 0:00:00
Finished backtest: 0:00:02
[3]:
type | symbol | date | shares | limit_price | fill_price | fees | |
---|---|---|---|---|---|---|---|
id | |||||||
1 | buy | NKE | 2021-09-21 | 100 | NaN | 154.86 | 0.0 |
2 | sell | NKE | 2021-11-04 | 100 | NaN | 173.82 | 0.0 |
3 | buy | DIS | 2021-11-16 | 100 | NaN | 159.40 | 0.0 |
Note that because we registered the custom rsi
column with PyBroker, it can be accessed in the ExecContext using ctx.rsi
.
Using a Pandas DataFrame
If you do not need the flexibility of implementing your own DataSource, then you can pass a Pandas DataFrame to a Strategy
instead.
To demonstrate, the earlier example can be re-implemented as follows:
[4]:
df = pd.read_csv('data/prices.csv')
df['date'] = pd.to_datetime(df['date'])
pybroker.register_columns('rsi')
strategy = Strategy(df, '6/1/2021', '12/1/2021')
strategy.add_execution(buy_low_sell_high_rsi, ['MCD', 'NKE', 'DIS'])
result = strategy.backtest()
result.orders
Backtesting: 2021-06-01 00:00:00 to 2021-12-01 00:00:00
Test split: 2021-06-01 00:00:00 to 2021-12-01 00:00:00
100% (129 of 129) |######################| Elapsed Time: 0:00:00 Time: 0:00:00
Finished backtest: 0:00:00
[4]:
type | symbol | date | shares | limit_price | fill_price | fees | |
---|---|---|---|---|---|---|---|
id | |||||||
1 | buy | NKE | 2021-09-21 | 100 | NaN | 154.86 | 0.0 |
2 | sell | NKE | 2021-11-04 | 100 | NaN | 173.82 | 0.0 |
3 | buy | DIS | 2021-11-16 | 100 | NaN | 159.40 | 0.0 |
Applying Stops
Stops are used to automatically buy or sell a security once it reaches a specified price level. They can be useful for limiting potential losses by allowing traders to exit bad trades automatically, as well as for taking profits by selling a security automatically when it reaches a certain price level.
PyBroker supports the simulation of stops, which is explained in detail in this notebook:
[1]:
import pybroker
from pybroker import Strategy, YFinance
pybroker.enable_data_source_cache('stops')
strategy = Strategy(YFinance(), '1/1/2018', '1/1/2023')
Stop Loss
A stop loss order is used to automatically exit a trade once the security’s price reaches or falls below a specified level. For example, the following code shows an example of a stop loss order set at 20%
below the entry price:
[2]:
def buy_with_stop_loss(ctx):
if not ctx.long_pos():
ctx.buy_shares = ctx.calc_target_shares(1)
ctx.stop_loss_pct = 20
strategy.add_execution(buy_with_stop_loss, ['TSLA'])
result = strategy.backtest()
result.trades
Backtesting: 2018-01-01 00:00:00 to 2023-01-01 00:00:00
Loading bar data...
[*********************100%***********************] 1 of 1 completed
Loaded bar data: 0:00:00
Test split: 2018-01-02 00:00:00 to 2022-12-30 00:00:00
100% (1259 of 1259) |####################| Elapsed Time: 0:00:00 Time: 0:00:00
Finished backtest: 0:00:02
[2]:
type | symbol | entry_date | exit_date | entry | exit | shares | pnl | return_pct | agg_pnl | bars | pnl_per_bar | stop | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
id | |||||||||||||
1 | long | TSLA | 2018-01-03 | 2018-03-28 | 21.36 | 17.09 | 4679 | -19988.69 | -20.00 | -19988.69 | 58 | -344.63 | loss |
2 | long | TSLA | 2018-03-29 | 2019-05-20 | 17.31 | 13.73 | 4622 | -16531.36 | -20.66 | -36520.04 | 286 | -57.80 | loss |
Take Profit
Similarly, a take profit order can be used to lock in profits on a trade. The following code adds a take profit order at 10%
above the entry price:
[3]:
def buy_with_stop_loss_and_profit(ctx):
if not ctx.long_pos():
ctx.buy_shares = ctx.calc_target_shares(1)
ctx.stop_loss_pct = 20
ctx.stop_profit_pct = 10
strategy.clear_executions()
strategy.add_execution(buy_with_stop_loss_and_profit, ['TSLA'])
result = strategy.backtest()
result.trades
Backtesting: 2018-01-01 00:00:00 to 2023-01-01 00:00:00
Loaded cached bar data.
Test split: 2018-01-02 00:00:00 to 2022-12-30 00:00:00
100% (1259 of 1259) |####################| Elapsed Time: 0:00:00 Time: 0:00:00
Finished backtest: 0:00:00
[3]:
type | symbol | entry_date | exit_date | entry | exit | shares | pnl | return_pct | agg_pnl | bars | pnl_per_bar | stop | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
id | |||||||||||||
1 | long | TSLA | 2018-01-03 | 2018-01-22 | 21.36 | 23.50 | 4679 | 9994.34 | 10.0 | 9994.34 | 12 | 832.86 | profit |
2 | long | TSLA | 2018-01-23 | 2018-03-27 | 23.72 | 18.98 | 4637 | -21997.93 | -20.0 | -12003.58 | 44 | -499.95 | loss |
3 | long | TSLA | 2018-03-28 | 2018-04-04 | 17.36 | 19.10 | 4727 | 8206.07 | 10.0 | -3797.51 | 4 | 2051.52 | profit |
4 | long | TSLA | 2018-04-05 | 2018-06-07 | 19.82 | 21.80 | 4853 | 9618.65 | 10.0 | 5821.13 | 44 | 218.61 | profit |
5 | long | TSLA | 2018-06-08 | 2018-06-12 | 21.39 | 23.53 | 4947 | 10581.63 | 10.0 | 16402.77 | 2 | 5290.82 | profit |
... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
85 | long | TSLA | 2022-07-29 | 2022-10-07 | 288.71 | 230.97 | 1010 | -58319.42 | -20.0 | 133480.29 | 49 | -1190.19 | loss |
86 | long | TSLA | 2022-10-10 | 2022-11-09 | 222.68 | 178.14 | 1046 | -46584.66 | -20.0 | 86895.63 | 22 | -2117.48 | loss |
87 | long | TSLA | 2022-11-10 | 2022-12-19 | 185.51 | 148.41 | 1007 | -37361.71 | -20.0 | 49533.92 | 26 | -1436.99 | loss |
88 | long | TSLA | 2022-12-20 | 2022-12-27 | 143.07 | 114.46 | 997 | -28528.16 | -20.0 | 21005.76 | 4 | -7132.04 | loss |
89 | long | TSLA | 2022-12-28 | 2022-12-29 | 112.25 | 123.48 | 1078 | 12100.55 | 10.0 | 33106.31 | 1 | 12100.55 | profit |
89 rows × 13 columns
Trailing Stop
A trailing stop is an order that is used to exit a trade once the instrument’s price falls a certain percentage or cash amount below its highest market price. Here is an example of setting a trailing stop at 20%
below the highest market price:
[4]:
def buy_with_trailing_stop_loss_and_profit(ctx):
if not ctx.long_pos():
ctx.buy_shares = ctx.calc_target_shares(1)
ctx.stop_trailing_pct = 20
ctx.stop_profit_pct = 10
strategy.clear_executions()
strategy.add_execution(buy_with_trailing_stop_loss_and_profit, ['TSLA'])
result = strategy.backtest()
result.trades
Backtesting: 2018-01-01 00:00:00 to 2023-01-01 00:00:00
Loaded cached bar data.
Test split: 2018-01-02 00:00:00 to 2022-12-30 00:00:00
100% (1259 of 1259) |####################| Elapsed Time: 0:00:00 Time: 0:00:00
Finished backtest: 0:00:00
[4]:
type | symbol | entry_date | exit_date | entry | exit | shares | pnl | return_pct | agg_pnl | bars | pnl_per_bar | stop | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
id | |||||||||||||
1 | long | TSLA | 2018-01-03 | 2018-01-22 | 21.36 | 23.50 | 4679 | 9994.34 | 10.00 | 9994.34 | 12 | 832.86 | profit |
2 | long | TSLA | 2018-01-23 | 2018-03-27 | 23.72 | 19.20 | 4637 | -20961.72 | -19.06 | -10967.37 | 44 | -476.40 | trailing |
3 | long | TSLA | 2018-03-28 | 2018-04-04 | 17.36 | 19.10 | 4783 | 8303.29 | 10.00 | -2664.08 | 4 | 2075.82 | profit |
4 | long | TSLA | 2018-04-05 | 2018-06-07 | 19.82 | 21.80 | 4910 | 9731.62 | 10.00 | 7067.54 | 44 | 221.17 | profit |
5 | long | TSLA | 2018-06-08 | 2018-06-12 | 21.39 | 23.53 | 5005 | 10705.70 | 10.00 | 17773.23 | 2 | 5352.85 | profit |
... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
102 | long | TSLA | 2022-08-02 | 2022-10-03 | 300.25 | 251.73 | 1095 | -53125.76 | -16.16 | 175768.16 | 43 | -1235.48 | trailing |
103 | long | TSLA | 2022-10-04 | 2022-10-24 | 249.75 | 199.80 | 1104 | -55144.80 | -20.00 | 120623.36 | 14 | -3938.91 | trailing |
104 | long | TSLA | 2022-10-25 | 2022-11-08 | 217.18 | 189.92 | 1015 | -27668.90 | -12.55 | 92954.46 | 10 | -2766.89 | trailing |
105 | long | TSLA | 2022-11-09 | 2022-12-13 | 186.50 | 160.66 | 1008 | -26050.75 | -13.86 | 66903.71 | 23 | -1132.64 | trailing |
106 | long | TSLA | 2022-12-14 | 2022-12-22 | 158.46 | 128.79 | 1036 | -30736.04 | -18.72 | 36167.67 | 6 | -5122.67 | trailing |
106 rows × 13 columns
Setting a Limit Price
A stop order can be combined with a limit price to ensure that the order is executed only at a specific price level. Below shows an example of placing a limit price on a stop order:
[5]:
def buy_with_trailing_stop_loss_and_profit(ctx):
if not ctx.long_pos():
ctx.buy_shares = ctx.calc_target_shares(1)
ctx.stop_trailing_pct = 20
ctx.stop_trailing_limit = ctx.close[-1] + 1
ctx.stop_profit_pct = 10
ctx.stop_profit_limit = ctx.close[-1] - 1
strategy.clear_executions()
strategy.add_execution(buy_with_trailing_stop_loss_and_profit, ['TSLA'])
result = strategy.backtest()
result.trades.head()
Backtesting: 2018-01-01 00:00:00 to 2023-01-01 00:00:00
Loaded cached bar data.
Test split: 2018-01-02 00:00:00 to 2022-12-30 00:00:00
100% (1259 of 1259) |####################| Elapsed Time: 0:00:00 Time: 0:00:00
Finished backtest: 0:00:00
[5]:
type | symbol | entry_date | exit_date | entry | exit | shares | pnl | return_pct | agg_pnl | bars | pnl_per_bar | stop | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
id | |||||||||||||
1 | long | TSLA | 2018-01-03 | 2018-01-22 | 21.36 | 23.50 | 4679 | 9994.34 | 10.0 | 9994.34 | 12 | 832.86 | profit |
2 | long | TSLA | 2018-01-23 | 2019-12-18 | 23.72 | 26.09 | 4637 | 10998.96 | 10.0 | 20993.31 | 480 | 22.91 | profit |
3 | long | TSLA | 2019-12-19 | 2020-01-03 | 26.78 | 29.46 | 4518 | 12099.20 | 10.0 | 33092.51 | 9 | 1344.36 | profit |
4 | long | TSLA | 2020-01-06 | 2020-01-08 | 29.72 | 32.69 | 4478 | 13308.62 | 10.0 | 46401.13 | 2 | 6654.31 | profit |
5 | long | TSLA | 2020-01-09 | 2020-01-14 | 32.39 | 35.63 | 4462 | 14452.42 | 10.0 | 60853.55 | 3 | 4817.47 | profit |
Canceling a Stop
The following code shows an example of canceling a stop order:
[6]:
def buy_with_stop_trailing_and_cancel(ctx):
pos = ctx.long_pos()
if not pos:
ctx.buy_shares = ctx.calc_target_shares(1)
ctx.stop_trailing_pct = 20
elif pos.bars > 60:
ctx.cancel_stops(ctx.symbol)
strategy.clear_executions()
strategy.add_execution(buy_with_stop_trailing_and_cancel, ['TSLA'])
result = strategy.backtest()
result.trades
Backtesting: 2018-01-01 00:00:00 to 2023-01-01 00:00:00
Loaded cached bar data.
Test split: 2018-01-02 00:00:00 to 2022-12-30 00:00:00
100% (1259 of 1259) |####################| Elapsed Time: 0:00:00 Time: 0:00:00
Finished backtest: 0:00:00
[6]:
type | symbol | entry_date | exit_date | entry | exit | shares | pnl | return_pct | agg_pnl | bars | pnl_per_bar | stop | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
id | |||||||||||||
1 | long | TSLA | 2018-01-03 | 2018-03-27 | 21.36 | 19.23 | 4679 | -9981.87 | -9.99 | -9981.87 | 57 | -175.12 | trailing |
Setting the Stop Exit Price
By default, stops are checked against the bar’s low and high prices, and they are exited at the stop’s threshold (e.g., -2%) on the same bar when the stop is triggered.
To set a custom exit price, the “exit_price” fields for each stop type can be used. When these fields are set, the stop will be checked against the exit_price
, and it will exit at the exit_price
when triggered. For example, the code below sets the stop_trailing_exit_price to the open price on the bar that triggers the stop:
[7]:
from pybroker import PriceType
def buy_with_stop_trailing_and_exit_price(ctx):
pos = ctx.long_pos()
if not pos:
ctx.buy_shares = ctx.calc_target_shares(1)
ctx.stop_trailing_pct = 20
ctx.stop_trailing_exit_price = PriceType.OPEN
strategy.clear_executions()
strategy.add_execution(buy_with_stop_trailing_and_exit_price, ['TSLA'])
result = strategy.backtest()
result.trades.head()
Backtesting: 2018-01-01 00:00:00 to 2023-01-01 00:00:00
Loaded cached bar data.
Test split: 2018-01-02 00:00:00 to 2022-12-30 00:00:00
100% (1259 of 1259) |####################| Elapsed Time: 0:00:00 Time: 0:00:00
Finished backtest: 0:00:00
[7]:
type | symbol | entry_date | exit_date | entry | exit | shares | pnl | return_pct | agg_pnl | bars | pnl_per_bar | stop | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
id | |||||||||||||
1 | long | TSLA | 2018-01-03 | 2018-03-28 | 21.36 | 17.64 | 4679 | -17412.12 | -17.42 | -17412.12 | 58 | -300.21 | trailing |
2 | long | TSLA | 2018-03-29 | 2018-07-25 | 17.31 | 19.78 | 4771 | 11797.10 | 14.28 | -5615.03 | 81 | 145.64 | trailing |
3 | long | TSLA | 2018-07-26 | 2018-08-20 | 20.48 | 19.45 | 4585 | -4737.83 | -5.05 | -10352.86 | 17 | -278.70 | trailing |
4 | long | TSLA | 2018-08-21 | 2018-09-07 | 21.13 | 17.34 | 4242 | -16077.18 | -17.94 | -26430.04 | 12 | -1339.76 | trailing |
5 | long | TSLA | 2018-09-10 | 2018-12-26 | 18.57 | 20.00 | 3961 | 5664.23 | 7.70 | -20765.81 | 74 | 76.54 | trailing |
Rebalancing Positions
PyBroker can be used to simulate rebalancing a portfolio. This means that PyBroker can simulate adjusting the asset allocation of a portfolio to match a desired target allocation. Additionally, our portfolio can be rebalanced using portfolio optimization, as this notebook will demonstrate.
[1]:
import pybroker as pyb
from datetime import datetime
from pybroker import ExecContext, Strategy, YFinance
pyb.enable_data_source_cache('rebalancing')
[1]:
<diskcache.core.Cache at 0x7fd854443a60>
Equal Position Sizing
Let’s assume that we want to rebalance a long-only portfolio at the beginning of every month to make sure that each asset in our portfolio has a roughly equal allocation.
We first start by writing a helper function to detect when the current bar’s date is the start of a new month:
[2]:
def start_of_month(dt: datetime) -> bool:
if dt.month != pyb.param('current_month'):
pyb.param('current_month', dt.month)
return True
return False
Next, we implement a function that will either buy or sell enough shares in an asset to reach a target allocation.
[3]:
def set_target_shares(
ctxs: dict[str, ExecContext],
targets: dict[str, float]
):
for symbol, target in targets.items():
ctx = ctxs[symbol]
target_shares = ctx.calc_target_shares(target)
pos = ctx.long_pos()
if pos is None:
ctx.buy_shares = target_shares
elif pos.shares < target_shares:
ctx.buy_shares = target_shares - pos.shares
elif pos.shares > target_shares:
ctx.sell_shares = pos.shares - target_shares
If the current allocation is above the target level, the function will sell some shares of the asset, while if the current allocation is below the target level, the function will buy some shares of the asset.
Following that, we write a rebalance
function to set each asset to an equal target allocation at the beginning of each month:
[4]:
def rebalance(ctxs: dict[str, ExecContext]):
dt = tuple(ctxs.values())[0].dt
if start_of_month(dt):
target = 1 / len(ctxs)
set_target_shares(ctxs, {symbol: target for symbol in ctxs.keys()})
Now that we have implemented the rebalance
function, the next step is to backtest our rebalancing strategy using five different stocks in our portfolio. To process all stocks at once on each bar of data, we will use the Strategy#set_after_exec method:
[5]:
strategy = Strategy(YFinance(), start_date='1/1/2018', end_date='1/1/2023')
strategy.add_execution(None, ['TSLA', 'NFLX', 'AAPL', 'NVDA', 'AMZN'])
strategy.set_after_exec(rebalance)
result = strategy.backtest()
Backtesting: 2018-01-01 00:00:00 to 2023-01-01 00:00:00
Loaded cached bar data.
Test split: 2018-01-02 00:00:00 to 2022-12-30 00:00:00
100% (1259 of 1259) |####################| Elapsed Time: 0:00:00 Time: 0:00:00
Finished backtest: 0:00:02
[6]:
result.orders
[6]:
type | symbol | date | shares | limit_price | fill_price | fees | |
---|---|---|---|---|---|---|---|
id | |||||||
1 | buy | NFLX | 2018-01-03 | 99 | NaN | 203.86 | 0.0 |
2 | buy | AAPL | 2018-01-03 | 464 | NaN | 43.31 | 0.0 |
3 | buy | TSLA | 2018-01-03 | 935 | NaN | 21.36 | 0.0 |
4 | buy | AMZN | 2018-01-03 | 336 | NaN | 59.84 | 0.0 |
5 | buy | NVDA | 2018-01-03 | 376 | NaN | 52.18 | 0.0 |
... | ... | ... | ... | ... | ... | ... | ... |
292 | sell | NFLX | 2022-12-02 | 15 | NaN | 315.99 | 0.0 |
293 | sell | NVDA | 2022-12-02 | 97 | NaN | 166.89 | 0.0 |
294 | buy | AAPL | 2022-12-02 | 27 | NaN | 146.82 | 0.0 |
295 | buy | TSLA | 2022-12-02 | 70 | NaN | 193.68 | 0.0 |
296 | buy | AMZN | 2022-12-02 | 41 | NaN | 94.57 | 0.0 |
296 rows × 7 columns
Portfolio Optimization
Portfolio optimization can guide our rebalancing in order to meet some objective for our portfolio. For instance, we can use portfolio optimization with the goal of allocating assets in a way to minimize risk.
Riskfolio-Lib is a popular Python library for performing portfolio optimization. Below shows how to use it to construct a minimum risk portfolio by minimizing the portfolio’s Conditional Value at Risk (CVar) based on the past year of returns:
[7]:
import pandas as pd
import riskfolio as rp
pyb.param('lookback', 252) # Use past year of returns.
def calculate_returns(ctxs: dict[str, ExecContext], lookback: int):
prices = {}
for ctx in ctxs.values():
prices[ctx.symbol] = ctx.adj_close[-lookback:]
df = pd.DataFrame(prices)
return df.pct_change().dropna()
def optimization(ctxs: dict[str, ExecContext]):
lookback = pyb.param('lookback')
first_ctx = tuple(ctxs.values())[0]
if start_of_month(first_ctx.dt):
Y = calculate_returns(ctxs, lookback)
port = rp.Portfolio(returns=Y)
port.assets_stats(method_mu='hist', method_cov='hist', d=0.94)
w = port.optimization(
model='Classic',
rm='CVaR',
obj='MinRisk',
rf=0, # Risk free rate.
l=0, # Risk aversion factor.
hist=True # Use historical scenarios.
)
targets = {
symbol: w.T[symbol].values[0]
for symbol in ctxs.keys()
}
set_target_shares(ctxs, targets)
You can find more information and examples of using Riskfolio-Lib on the official documentation. Now, let’s move on to backtesting the strategy!
[8]:
strategy.set_after_exec(optimization)
result = strategy.backtest(warmup=pyb.param('lookback'))
Backtesting: 2018-01-01 00:00:00 to 2023-01-01 00:00:00
Loaded cached bar data.
Test split: 2018-01-02 00:00:00 to 2022-12-30 00:00:00
100% (1259 of 1259) |####################| Elapsed Time: 0:00:01 Time: 0:00:01
Finished backtest: 0:00:01
[9]:
result.orders.head()
[9]:
type | symbol | date | shares | limit_price | fill_price | fees | |
---|---|---|---|---|---|---|---|
id | |||||||
1 | buy | AAPL | 2019-01-04 | 1420 | NaN | 36.54 | 0.0 |
2 | buy | TSLA | 2019-01-04 | 1168 | NaN | 20.69 | 0.0 |
3 | buy | AMZN | 2019-01-04 | 307 | NaN | 77.81 | 0.0 |
4 | sell | AAPL | 2019-02-04 | 105 | NaN | 42.37 | 0.0 |
5 | buy | TSLA | 2019-02-04 | 51 | NaN | 20.57 | 0.0 |
Above, we can observe that the portfolio optimization resulted in allocating the entire portfolio to 3 of the 5 stocks during the first month of the backtest.
Rotational Trading
Rotational trading involves purchasing the best-performing assets while selling underperforming ones. As you may have guessed, PyBroker is an excellent tool for backtesting such strategies. So, let’s dive in and get started with testing our rotational trading strategy!
[1]:
import pybroker as pyb
from pybroker import ExecContext, Strategy, StrategyConfig, YFinance
Our strategy will involve ranking and buying stocks with the highest price rate-of-change (ROC). To start, we’ll define a 20-day ROC indicator using TA-Lib:
[2]:
import talib as ta
roc_20 = pyb.indicator(
'roc_20', lambda data: ta.ROC(data.adj_close, timeperiod=20))
Next, let’s define the rules of our strategy:
Buy the two stocks with the highest 20-day ROC.
Allocate 50% of our capital to each stock.
If either of the stocks is no longer ranked among the top five 20-day ROCs, then we will liquidate that stock.
Trade these rules daily.
Let’s set up our config and some parameters for the above rules:
[3]:
config = StrategyConfig(max_long_positions=2)
pyb.param('target_size', 1 / config.max_long_positions)
pyb.param('rank_threshold', 5)
[3]:
5
To proceed with our strategy, we will implement a rank
function that ranks each stock by their 20-day ROC in descending order, from highest to lowest.
[4]:
def rank(ctxs: dict[str, ExecContext]):
scores = {
symbol: ctx.indicator('roc_20')[-1]
for symbol, ctx in ctxs.items()
}
sorted_scores = sorted(
scores.items(),
key=lambda score: score[1],
reverse=True
)
threshold = pyb.param('rank_threshold')
top_scores = sorted_scores[:threshold]
top_symbols = [score[0] for score in top_scores]
pyb.param('top_symbols', top_symbols)
The top_symbols
global parameter contains the symbols of the stocks with the top five highest 20-day ROCs.
Now that we have a method for ranking stocks by their ROC, we can proceed with implementing a rotate
function to manage the rotational trading.
[5]:
def rotate(ctx: ExecContext):
if ctx.long_pos():
if ctx.symbol not in pyb.param('top_symbols'):
ctx.sell_all_shares()
else:
target_size = pyb.param('target_size')
ctx.buy_shares = ctx.calc_target_shares(target_size)
ctx.score = ctx.indicator('roc_20')[-1]
We liquidate the currently held stock if it is no longer ranked among the top five 20-day ROCs. Otherwise, we rank all stocks by their 20-day ROC and buy up to the top two ranked. For more information on ranking when placing buy orders, see the Ranking and Position Sizing notebook.
We will use the set_before_exec method to execute our ranking with rank
before running the rotate
function. For this backtest, we will use a universe of 10 stocks:
[6]:
strategy = Strategy(
YFinance(),
start_date='1/1/2018',
end_date='1/1/2023',
config=config
)
strategy.set_before_exec(rank)
strategy.add_execution(rotate, [
'TSLA',
'NFLX',
'AAPL',
'NVDA',
'AMZN',
'MSFT',
'GOOG',
'AMD',
'INTC',
'META'
], indicators=roc_20)
result = strategy.backtest(warmup=20)
Backtesting: 2018-01-01 00:00:00 to 2023-01-01 00:00:00
Loading bar data...
[*********************100%***********************] 10 of 10 completed
Loaded bar data: 0:00:03
Computing indicators...
100% (10 of 10) |########################| Elapsed Time: 0:00:00 Time: 0:00:00
Test split: 2018-01-02 00:00:00 to 2022-12-30 00:00:00
100% (1259 of 1259) |####################| Elapsed Time: 0:00:00 Time: 0:00:00
Finished backtest: 0:00:06
[7]:
result.orders
[7]:
type | symbol | date | shares | limit_price | fill_price | fees | |
---|---|---|---|---|---|---|---|
id | |||||||
1 | buy | NFLX | 2018-02-01 | 184 | NaN | 267.67 | 0.0 |
2 | buy | AMD | 2018-02-01 | 3639 | NaN | 13.53 | 0.0 |
3 | sell | AMD | 2018-02-05 | 3639 | NaN | 11.56 | 0.0 |
4 | buy | AMZN | 2018-02-05 | 627 | NaN | 69.49 | 0.0 |
5 | sell | AMZN | 2018-04-03 | 627 | NaN | 69.23 | 0.0 |
... | ... | ... | ... | ... | ... | ... | ... |
256 | buy | AMD | 2022-11-21 | 3589 | NaN | 72.28 | 0.0 |
257 | sell | AMD | 2022-12-14 | 3589 | NaN | 70.16 | 0.0 |
258 | buy | NFLX | 2022-12-14 | 881 | NaN | 319.57 | 0.0 |
259 | sell | NVDA | 2022-12-28 | 1491 | NaN | 140.73 | 0.0 |
260 | buy | META | 2022-12-28 | 1869 | NaN | 116.83 | 0.0 |
260 rows × 7 columns
FAQs
How to…
… get your version of PyBroker?
[1]:
import pybroker
pybroker.__version__
[1]:
'1.1.27'
… get data for another symbol?
[2]:
from pybroker import ExecContext, Strategy, YFinance, highest
def exec_fn(ctx: ExecContext):
if ctx.symbol == 'NVDA':
other_bar_data = ctx.foreign('AMD')
other_highest = ctx.indicator('high_10d', 'AMD')
strategy = Strategy(YFinance(), start_date='1/1/2022', end_date='1/1/2023')
strategy.add_execution(
exec_fn, ['NVDA', 'AMD'], indicators=highest('high_10d', 'close', period=10))
result = strategy.backtest()
Backtesting: 2022-01-01 00:00:00 to 2023-01-01 00:00:00
Loading bar data...
[*********************100%***********************] 2 of 2 completed
Loaded bar data: 0:00:00
Computing indicators...
100% (2 of 2) |##########################| Elapsed Time: 0:00:01 Time: 0:00:01
Test split: 2022-01-03 00:00:00 to 2022-12-30 00:00:00
100% (251 of 251) |######################| Elapsed Time: 0:00:00 Time: 0:00:00
Finished backtest: 0:00:02
You can also retrieve models, predictions, and other data for other symbols. For more information, refer to the ExecContext reference documentation.
… set a limit price?
Set buy_limit_price or sell_limit_price:
[3]:
from pybroker import ExecContext, Strategy, YFinance
def buy_fn(ctx: ExecContext):
if not ctx.long_pos():
ctx.buy_shares = 100
ctx.buy_limit_price = ctx.close[-1] * 0.99
ctx.hold_bars = 10
strategy = Strategy(YFinance(), start_date='3/1/2022', end_date='1/1/2023')
strategy.add_execution(buy_fn, 'SPY')
result = strategy.backtest()
result.orders.head(10)
Backtesting: 2022-03-01 00:00:00 to 2023-01-01 00:00:00
Loading bar data...
[*********************100%***********************] 1 of 1 completed
Loaded bar data: 0:00:00
Test split: 2022-03-01 00:00:00 to 2022-12-30 00:00:00
100% (212 of 212) |######################| Elapsed Time: 0:00:00 Time: 0:00:00
Finished backtest: 0:00:00
[3]:
type | symbol | date | shares | limit_price | fill_price | fees | |
---|---|---|---|---|---|---|---|
id | |||||||
1 | buy | SPY | 2022-03-04 | 100 | 431.35 | 430.62 | 0.0 |
2 | sell | SPY | 2022-03-18 | 100 | NaN | 441.04 | 0.0 |
3 | buy | SPY | 2022-04-06 | 100 | 446.52 | 446.20 | 0.0 |
4 | sell | SPY | 2022-04-21 | 100 | NaN | 443.56 | 0.0 |
5 | buy | SPY | 2022-04-22 | 100 | 433.68 | 431.76 | 0.0 |
6 | sell | SPY | 2022-05-06 | 100 | NaN | 410.26 | 0.0 |
7 | buy | SPY | 2022-05-09 | 100 | 407.23 | 401.46 | 0.0 |
8 | sell | SPY | 2022-05-23 | 100 | NaN | 394.06 | 0.0 |
9 | buy | SPY | 2022-05-24 | 100 | 392.95 | 391.05 | 0.0 |
10 | sell | SPY | 2022-06-08 | 100 | NaN | 413.10 | 0.0 |
… set the fill price?
Set buy_fill_price and sell_fill_price. See PriceType for options.
[4]:
from pybroker import ExecContext, PriceType, Strategy, YFinance
def exec_fn(ctx: ExecContext):
if ctx.long_pos():
ctx.buy_shares = 100
ctx.buy_fill_price = PriceType.AVERAGE
else:
ctx.sell_shares = 100
ctx.sell_fill_price = PriceType.CLOSE
strategy = Strategy(YFinance(), start_date='3/1/2022', end_date='1/1/2023')
strategy.add_execution(buy_fn, 'SPY')
result = strategy.backtest()
result.orders.head(10)
Backtesting: 2022-03-01 00:00:00 to 2023-01-01 00:00:00
Loading bar data...
[*********************100%***********************] 1 of 1 completed
Loaded bar data: 0:00:00
Test split: 2022-03-01 00:00:00 to 2022-12-30 00:00:00
100% (212 of 212) |######################| Elapsed Time: 0:00:00 Time: 0:00:00
Finished backtest: 0:00:00
[4]:
type | symbol | date | shares | limit_price | fill_price | fees | |
---|---|---|---|---|---|---|---|
id | |||||||
1 | buy | SPY | 2022-03-04 | 100 | 431.35 | 430.62 | 0.0 |
2 | sell | SPY | 2022-03-18 | 100 | NaN | 441.04 | 0.0 |
3 | buy | SPY | 2022-04-06 | 100 | 446.52 | 446.20 | 0.0 |
4 | sell | SPY | 2022-04-21 | 100 | NaN | 443.56 | 0.0 |
5 | buy | SPY | 2022-04-22 | 100 | 433.68 | 431.76 | 0.0 |
6 | sell | SPY | 2022-05-06 | 100 | NaN | 410.26 | 0.0 |
7 | buy | SPY | 2022-05-09 | 100 | 407.23 | 401.46 | 0.0 |
8 | sell | SPY | 2022-05-23 | 100 | NaN | 394.06 | 0.0 |
9 | buy | SPY | 2022-05-24 | 100 | 392.95 | 391.05 | 0.0 |
10 | sell | SPY | 2022-06-08 | 100 | NaN | 413.10 | 0.0 |
… get current positions?
[5]:
from pybroker import ExecContext, Strategy, YFinance
def exec_fn(ctx: ExecContext):
# Get all positions.
all_positions = tuple(ctx.positions())
# Get all long positions.
long_positions = tuple(ctx.long_positions())
# Get all short positions.
short_positions = tuple(ctx.short_positions())
# Get long position for current ctx.symbol.
long_position = ctx.long_pos()
# Get short position for a symbol.
short_position = ctx.short_pos('QQQ')
strategy = Strategy(YFinance(), start_date='3/1/2022', end_date='1/1/2023')
strategy.add_execution(exec_fn, ['SPY', 'QQQ'])
result = strategy.backtest()
Backtesting: 2022-03-01 00:00:00 to 2023-01-01 00:00:00
Loading bar data...
[*********************100%***********************] 2 of 2 completed
Loaded bar data: 0:00:00
Test split: 2022-03-01 00:00:00 to 2022-12-30 00:00:00
100% (212 of 212) |######################| Elapsed Time: 0:00:00 Time: 0:00:00
Finished backtest: 0:00:00
… use custom column data?
Register your custom columns with pybroker.register_columns:
[6]:
import pybroker
from pybroker import ExecContext, Strategy, YFinance
yf = YFinance()
df = yf.query('SPY', start_date='1/1/2022', end_date='1/1/2023')
df['buy_signal'] = 1
def buy_fn(ctx: ExecContext):
if not ctx.long_pos() and ctx.buy_signal[-1] == 1:
ctx.buy_shares = 100
ctx.hold_bars = 1
pybroker.register_columns('buy_signal')
strategy = Strategy(df, start_date='3/1/2022', end_date='1/1/2023')
strategy.add_execution(buy_fn, 'SPY')
result = strategy.backtest()
result.orders.head(10)
Loading bar data...
[*********************100%***********************] 1 of 1 completed
Loaded bar data: 0:00:00
Backtesting: 2022-03-01 00:00:00 to 2023-01-01 00:00:00
Test split: 2022-03-01 00:00:00 to 2022-12-30 00:00:00
100% (212 of 212) |######################| Elapsed Time: 0:00:00 Time: 0:00:00
Finished backtest: 0:00:00
[6]:
type | symbol | date | shares | limit_price | fill_price | fees | |
---|---|---|---|---|---|---|---|
id | |||||||
1 | buy | SPY | 2022-03-02 | 100 | NaN | 435.65 | 0.0 |
2 | sell | SPY | 2022-03-03 | 100 | NaN | 437.45 | 0.0 |
3 | buy | SPY | 2022-03-04 | 100 | NaN | 430.62 | 0.0 |
4 | sell | SPY | 2022-03-07 | 100 | NaN | 425.83 | 0.0 |
5 | buy | SPY | 2022-03-08 | 100 | NaN | 421.16 | 0.0 |
6 | sell | SPY | 2022-03-09 | 100 | NaN | 426.17 | 0.0 |
7 | buy | SPY | 2022-03-10 | 100 | NaN | 423.43 | 0.0 |
8 | sell | SPY | 2022-03-11 | 100 | NaN | 424.15 | 0.0 |
9 | buy | SPY | 2022-03-14 | 100 | NaN | 420.17 | 0.0 |
10 | sell | SPY | 2022-03-15 | 100 | NaN | 422.63 | 0.0 |
… place an order more than one bar ahead?
Use the buy_delay and sell_delay configuration options:
[7]:
from pybroker import ExecContext, Strategy, StrategyConfig, YFinance
def buy_fn(ctx: ExecContext):
if not tuple(ctx.pending_orders()) and not ctx.long_pos():
ctx.buy_shares = 100
ctx.hold_bars = 1
config = StrategyConfig(buy_delay=5)
strategy = Strategy(YFinance(), start_date='3/1/2022', end_date='1/1/2023', config=config)
strategy.add_execution(buy_fn, 'SPY')
result = strategy.backtest()
result.orders.head(10)
Backtesting: 2022-03-01 00:00:00 to 2023-01-01 00:00:00
Loading bar data...
[*********************100%***********************] 1 of 1 completed
Loaded bar data: 0:00:00
Test split: 2022-03-01 00:00:00 to 2022-12-30 00:00:00
100% (212 of 212) |######################| Elapsed Time: 0:00:00 Time: 0:00:00
Finished backtest: 0:00:00
[7]:
type | symbol | date | shares | limit_price | fill_price | fees | |
---|---|---|---|---|---|---|---|
id | |||||||
1 | buy | SPY | 2022-03-08 | 100 | NaN | 421.16 | 0.0 |
2 | sell | SPY | 2022-03-09 | 100 | NaN | 426.17 | 0.0 |
3 | buy | SPY | 2022-03-16 | 100 | NaN | 430.24 | 0.0 |
4 | sell | SPY | 2022-03-17 | 100 | NaN | 437.13 | 0.0 |
5 | buy | SPY | 2022-03-24 | 100 | NaN | 447.63 | 0.0 |
6 | sell | SPY | 2022-03-25 | 100 | NaN | 450.71 | 0.0 |
7 | buy | SPY | 2022-04-01 | 100 | NaN | 451.30 | 0.0 |
8 | sell | SPY | 2022-04-04 | 100 | NaN | 454.59 | 0.0 |
9 | buy | SPY | 2022-04-11 | 100 | NaN | 442.20 | 0.0 |
10 | sell | SPY | 2022-04-12 | 100 | NaN | 441.20 | 0.0 |
… cancel pending orders?
See the cancel_pending_order and cancel_all_pending_orders methods.
[8]:
from pybroker import ExecContext, Strategy, StrategyConfig, YFinance
def buy_fn(ctx: ExecContext):
pending = tuple(ctx.pending_orders())
if not pending and not ctx.long_pos():
ctx.buy_shares = 100
ctx.hold_bars = 1
if pending and ctx.close[-1] < 430:
ctx.cancel_all_pending_orders(ctx.symbol)
config = StrategyConfig(buy_delay=5)
strategy = Strategy(YFinance(), start_date='3/1/2022', end_date='1/1/2023', config=config)
strategy.add_execution(buy_fn, 'SPY')
result = strategy.backtest()
result.orders.head(10)
Backtesting: 2022-03-01 00:00:00 to 2023-01-01 00:00:00
Loading bar data...
[*********************100%***********************] 1 of 1 completed
Loaded bar data: 0:00:00
Test split: 2022-03-01 00:00:00 to 2022-12-30 00:00:00
100% (212 of 212) |######################| Elapsed Time: 0:00:00 Time: 0:00:00
Finished backtest: 0:00:00
[8]:
type | symbol | date | shares | limit_price | fill_price | fees | |
---|---|---|---|---|---|---|---|
id | |||||||
1 | buy | SPY | 2022-03-23 | 100 | NaN | 446.10 | 0.0 |
2 | sell | SPY | 2022-03-24 | 100 | NaN | 447.63 | 0.0 |
3 | buy | SPY | 2022-03-31 | 100 | NaN | 454.96 | 0.0 |
4 | sell | SPY | 2022-04-01 | 100 | NaN | 451.30 | 0.0 |
5 | buy | SPY | 2022-04-08 | 100 | NaN | 448.29 | 0.0 |
6 | sell | SPY | 2022-04-11 | 100 | NaN | 442.20 | 0.0 |
7 | buy | SPY | 2022-04-19 | 100 | NaN | 441.74 | 0.0 |
8 | sell | SPY | 2022-04-20 | 100 | NaN | 445.53 | 0.0 |
… persist data across bars?
Use the ExecContext#session dictionary:
[9]:
from pybroker import ExecContext, Strategy, YFinance
def buy_fn(ctx: ExecContext):
if not ctx.long_pos():
ctx.buy_shares = 100
ctx.hold_bars = 1
count = ctx.session.get('entry_count', 0)
ctx.session['entry_count'] = count + 1
strategy = Strategy(YFinance(), start_date='1/1/2022', end_date='1/1/2023')
strategy.add_execution(buy_fn, 'SPY')
result = strategy.backtest()
Backtesting: 2022-01-01 00:00:00 to 2023-01-01 00:00:00
Loading bar data...
[*********************100%***********************] 1 of 1 completed
Loaded bar data: 0:00:00
Test split: 2022-01-03 00:00:00 to 2022-12-30 00:00:00
100% (251 of 251) |######################| Elapsed Time: 0:00:00 Time: 0:00:00
Finished backtest: 0:00:00
… exit a position?
Use sell_all_shares() or cover_all_shares() to liquidate a position:
[10]:
from pybroker import ExecContext, Strategy, YFinance
def buy_fn(ctx: ExecContext):
pos = ctx.long_pos()
if not pos:
ctx.buy_shares = 100
elif pos.bars > 30:
ctx.sell_all_shares()
strategy = Strategy(YFinance(), start_date='1/1/2022', end_date='1/1/2023')
strategy.add_execution(buy_fn, 'SPY')
result = strategy.backtest()
result.trades
Backtesting: 2022-01-01 00:00:00 to 2023-01-01 00:00:00
Loading bar data...
[*********************100%***********************] 1 of 1 completed
Loaded bar data: 0:00:00
Test split: 2022-01-03 00:00:00 to 2022-12-30 00:00:00
100% (251 of 251) |######################| Elapsed Time: 0:00:00 Time: 0:00:00
Finished backtest: 0:00:00
[10]:
type | symbol | entry_date | exit_date | entry | exit | shares | pnl | return_pct | agg_pnl | bars | pnl_per_bar | stop | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
id | |||||||||||||
1 | long | SPY | 2022-01-04 | 2022-02-18 | 477.78 | 435.24 | 100 | -4254.0 | -8.90 | -4254.0 | 32 | -132.94 | None |
2 | long | SPY | 2022-02-22 | 2022-04-07 | 430.68 | 447.11 | 100 | 1643.0 | 3.81 | -2611.0 | 32 | 51.34 | None |
3 | long | SPY | 2022-04-08 | 2022-05-25 | 448.29 | 395.67 | 100 | -5262.0 | -11.74 | -7873.0 | 32 | -164.44 | None |
4 | long | SPY | 2022-05-26 | 2022-07-14 | 402.75 | 375.04 | 100 | -2771.0 | -6.88 | -10644.0 | 32 | -86.59 | None |
5 | long | SPY | 2022-07-15 | 2022-08-30 | 382.90 | 400.05 | 100 | 1715.0 | 4.48 | -8929.0 | 32 | 53.59 | None |
6 | long | SPY | 2022-08-31 | 2022-10-17 | 398.14 | 362.63 | 100 | -3551.0 | -8.92 | -12480.0 | 32 | -110.97 | None |
7 | long | SPY | 2022-10-18 | 2022-12-02 | 371.49 | 405.00 | 100 | 3351.0 | 9.02 | -9129.0 | 32 | 104.72 | None |
… process multiple symbols at once?
Use set_before_exec or set_after_exec:
[11]:
from pybroker import ExecContext, Strategy, YFinance
def long_short_fn(ctxs: dict[str, ExecContext]):
nvda_ctx = ctxs['NVDA']
amd_ctx = ctxs['AMD']
if nvda_ctx.long_pos() or amd_ctx.short_pos():
return
if nvda_ctx.bars >= 2 and nvda_ctx.close[-1] < nvda_ctx.low[-2]:
nvda_ctx.buy_shares = 100
nvda_ctx.hold_bars = 3
amd_ctx.sell_shares = 100
amd_ctx.hold_bars = 3
strategy = Strategy(YFinance(), start_date='1/1/2022', end_date='1/1/2023')
strategy.add_execution(None, ['NVDA', 'AMD'])
strategy.set_after_exec(long_short_fn)
result = strategy.backtest()
result.trades
Backtesting: 2022-01-01 00:00:00 to 2023-01-01 00:00:00
Loading bar data...
[*********************100%***********************] 2 of 2 completed
Loaded bar data: 0:00:00
Test split: 2022-01-03 00:00:00 to 2022-12-30 00:00:00
100% (251 of 251) |######################| Elapsed Time: 0:00:00 Time: 0:00:00
Finished backtest: 0:00:00
[11]:
type | symbol | entry_date | exit_date | entry | exit | shares | pnl | return_pct | agg_pnl | bars | pnl_per_bar | stop | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
id | |||||||||||||
1 | long | NVDA | 2022-01-05 | 2022-01-10 | 284.74 | 265.57 | 100 | -1917.0 | -6.73 | -1917.0 | 3 | -639.00 | bar |
2 | short | AMD | 2022-01-05 | 2022-01-10 | 139.52 | 128.72 | 100 | 1080.0 | 8.39 | -837.0 | 3 | 360.00 | bar |
3 | long | NVDA | 2022-01-14 | 2022-01-20 | 267.04 | 248.28 | 100 | -1876.0 | -7.03 | -2713.0 | 3 | -625.33 | bar |
4 | short | AMD | 2022-01-14 | 2022-01-20 | 134.21 | 124.96 | 100 | 925.0 | 7.40 | -1788.0 | 3 | 308.33 | bar |
5 | long | NVDA | 2022-01-21 | 2022-01-26 | 240.43 | 231.79 | 100 | -864.0 | -3.59 | -2652.0 | 3 | -288.00 | bar |
... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
76 | short | AMD | 2022-12-07 | 2022-12-12 | 70.33 | 69.10 | 100 | 123.0 | 1.78 | 1863.0 | 3 | 41.00 | bar |
77 | long | NVDA | 2022-12-15 | 2022-12-20 | 170.10 | 160.81 | 100 | -929.0 | -5.46 | 934.0 | 3 | -309.67 | bar |
78 | short | AMD | 2022-12-15 | 2022-12-20 | 67.17 | 64.79 | 100 | 238.0 | 3.67 | 1172.0 | 3 | 79.33 | bar |
79 | long | NVDA | 2022-12-21 | 2022-12-27 | 163.65 | 145.78 | 100 | -1787.0 | -10.92 | -615.0 | 3 | -595.67 | bar |
80 | short | AMD | 2022-12-21 | 2022-12-27 | 66.53 | 63.62 | 100 | 291.0 | 4.57 | -324.0 | 3 | 97.00 | bar |
80 rows × 13 columns
… annualize the Sharpe Ratio?
Set the bars_per_year configuration option. For example, setting a value of 252
would be used to annualize daily returns.
[12]:
from pybroker import ExecContext, Strategy, StrategyConfig, YFinance
def buy_fn(ctx: ExecContext):
if ctx.long_pos() or ctx.bars < 2:
return
if ctx.close[-1] < ctx.high[-2]:
ctx.buy_shares = 100
ctx.hold_bars = 1
config = StrategyConfig(bars_per_year=252)
strategy = Strategy(YFinance(), start_date='1/1/2022', end_date='1/1/2023', config=config)
strategy.add_execution(buy_fn, 'SPY')
result = strategy.backtest()
result.metrics.sharpe
Backtesting: 2022-01-01 00:00:00 to 2023-01-01 00:00:00
Loading bar data...
[*********************100%***********************] 1 of 1 completed
Loaded bar data: 0:00:00
Test split: 2022-01-03 00:00:00 to 2022-12-30 00:00:00
100% (251 of 251) |######################| Elapsed Time: 0:00:00 Time: 0:00:00
Finished backtest: 0:00:00
[12]:
-0.8930247224724036
… limit margin used for short selling?
By default, PyBroker does not limit the amount of margin that can be used for short selling. However, you can manually limit the amount of margin that can be used:
[13]:
import pybroker
from pybroker import ExecContext
from decimal import Decimal
def short_fn(ctx: ExecContext):
margin_requirement = Decimal('0.25')
max_margin = ctx.total_equity / margin_requirement - ctx.total_equity
if not ctx.short_pos():
available_margin = max_margin - ctx.total_margin
ctx.sell_shares = ctx.calc_target_shares(0.5, cash=available_margin)
ctx.hold_bars = 1
strategy = Strategy(YFinance(), start_date='1/1/2022', end_date='1/1/2023')
strategy.add_execution(short_fn, ['NVDA', 'AMD'])
result = strategy.backtest()
result.portfolio.head(10)
Backtesting: 2022-01-01 00:00:00 to 2023-01-01 00:00:00
Loading bar data...
[*********************100%***********************] 2 of 2 completed
Loaded bar data: 0:00:00
Test split: 2022-01-03 00:00:00 to 2022-12-30 00:00:00
100% (251 of 251) |######################| Elapsed Time: 0:00:00 Time: 0:00:00
Finished backtest: 0:00:00
[13]:
cash | equity | margin | market_value | pnl | unrealized_pnl | fees | |
---|---|---|---|---|---|---|---|
date | |||||||
2022-01-03 | 100000.00 | 100000.00 | 0.00 | 100000.00 | 0.00 | 0.00 | 0.0 |
2022-01-04 | 100000.00 | 100000.00 | 289702.46 | 102722.18 | 0.00 | 2722.18 | 0.0 |
2022-01-05 | 111667.90 | 111667.90 | 0.00 | 111667.90 | 11667.90 | 0.00 | 0.0 |
2022-01-06 | 111667.90 | 111667.90 | 338321.57 | 107432.09 | 11667.90 | -4235.81 | 0.0 |
2022-01-07 | 112472.56 | 112472.56 | 0.00 | 112472.56 | 12472.56 | 0.00 | 0.0 |
2022-01-10 | 112472.56 | 112472.56 | 338302.00 | 103062.55 | 12472.56 | -9410.01 | 0.0 |
2022-01-11 | 98536.05 | 98536.05 | 0.00 | 98536.05 | -1463.95 | 0.00 | 0.0 |
2022-01-12 | 98536.05 | 98536.05 | 296592.41 | 99830.87 | -1463.95 | 1294.82 | 0.0 |
2022-01-13 | 103550.41 | 103550.41 | 0.00 | 103550.41 | 3550.41 | 0.00 | 0.0 |
2022-01-14 | 103550.41 | 103550.41 | 317490.89 | 99036.58 | 3550.41 | -4513.83 | 0.0 |
… get and set a global parameter?
[14]:
import pybroker
# Set parameter.
pybroker.param('lookback', 100)
# Get parameter.
pybroker.param('lookback')
[14]:
100
… apply random slippage?
Set a RandomSlippageModel:
[15]:
from pybroker import ExecContext, RandomSlippageModel, Strategy, YFinance
def buy_fn(ctx: ExecContext):
if not ctx.long_pos():
ctx.buy_shares = 100
ctx.hold_bars = 1
slippage = RandomSlippageModel(min_pct=1.0, max_pct=5.0) # Slippage of 1-5%
strategy = Strategy(YFinance(), start_date='3/1/2022', end_date='1/1/2023')
strategy.set_slippage_model(slippage)
strategy.add_execution(buy_fn, 'SPY')
result = strategy.backtest()
result.orders.head(10)
Backtesting: 2022-03-01 00:00:00 to 2023-01-01 00:00:00
Loading bar data...
[*********************100%***********************] 1 of 1 completed
Loaded bar data: 0:00:00
Test split: 2022-03-01 00:00:00 to 2022-12-30 00:00:00
100% (212 of 212) |######################| Elapsed Time: 0:00:00 Time: 0:00:00
Finished backtest: 0:00:00
[15]:
type | symbol | date | shares | limit_price | fill_price | fees | |
---|---|---|---|---|---|---|---|
id | |||||||
1 | buy | SPY | 2022-03-02 | 98 | NaN | 435.65 | 0.0 |
2 | sell | SPY | 2022-03-03 | 98 | NaN | 437.45 | 0.0 |
3 | buy | SPY | 2022-03-04 | 98 | NaN | 430.62 | 0.0 |
4 | sell | SPY | 2022-03-07 | 98 | NaN | 425.83 | 0.0 |
5 | buy | SPY | 2022-03-08 | 98 | NaN | 421.16 | 0.0 |
6 | sell | SPY | 2022-03-09 | 98 | NaN | 426.17 | 0.0 |
7 | buy | SPY | 2022-03-10 | 97 | NaN | 423.43 | 0.0 |
8 | sell | SPY | 2022-03-11 | 97 | NaN | 424.15 | 0.0 |
9 | buy | SPY | 2022-03-14 | 97 | NaN | 420.17 | 0.0 |
10 | sell | SPY | 2022-03-15 | 97 | NaN | 422.63 | 0.0 |
The notebooks above are also available on Github.
pybroker.config module
Contains configuration options.
- class StrategyConfig(initial_cash: float = 100000, fee_mode: FeeMode | Callable[[FeeInfo], Decimal] | None = None, fee_amount: float = 0, subtract_fees: bool = False, enable_fractional_shares: bool = False, round_fill_price: bool = True, max_long_positions: int | None = None, max_short_positions: int | None = None, buy_delay: int = 1, sell_delay: int = 1, bootstrap_samples: int = 10000, bootstrap_sample_size: int = 1000, exit_on_last_bar: bool = False, exit_cover_fill_price: PriceType | Callable[[str, BarData], int | float | Decimal] = PriceType.MIDDLE, exit_sell_fill_price: PriceType | Callable[[str, BarData], int | float | Decimal] = PriceType.MIDDLE, bars_per_year: int | None = None, return_signals: bool = False, return_stops: bool = False, round_test_result: bool = True)[source]
Bases:
object
Configuration options for
pybroker.strategy.Strategy
.- fee_mode
pybroker.common.FeeMode
for calculating brokerage fees. Supports one of:ORDER_PERCENT
: Fee is a percentage of order amount.PER_ORDER
: Fee is a constant amount per order.PER_SHARE
: Fee is a constant amount per share in order.Callable[[FeeInfo], Decimal]
: Fees are calculated using acustom
Callable
that is passedpybroker.common.FeeInfo
.
None
: Fees are disabled (default).
- Type:
pybroker.common.FeeMode | Callable[[pybroker.common.FeeInfo], decimal.Decimal] | None
- subtract_fees
Whether to subtract fees from the cash balance after an order is filled. Defaults to
False
.- Type:
Whether to enable trading fractional shares. Set to
True
for crypto trading. Defaults toFalse
.- Type:
- max_long_positions
Maximum number of long positions that can be held at any time in
pybroker.portfolio.Portfolio
. Unlimited whenNone
. Defaults toNone
.- Type:
int | None
- max_short_positions
Maximum number of short positions that can be held at any time in
pybroker.portfolio.Portfolio
. Unlimited whenNone
. Defaults toNone
.- Type:
int | None
- buy_delay
Number of bars before placing an order for a buy signal. The default value of
1
places a buy order on the next bar. Must be >0
.- Type:
- sell_delay
Number of bars before placing an order for a sell signal. The default value of
1
places a sell order on the next bar. Must be >0
.- Type:
- bootstrap_samples
Number of samples used to compute boostrap metrics. Defaults to
10_000
.- Type:
- bootstrap_sample_size
Size of each random sample used to compute bootstrap metrics. Defaults to
1_000
.- Type:
- exit_on_last_bar
Whether to automatically exit any open positions on the last bar of data available for a symbol. Defaults to
False
.- Type:
- exit_cover_fill_price
Fill price for covering an open short position when
exit_on_last_bar
isTrue
. Defaults topybroker.common.PriceType.MIDDLE
.- Type:
pybroker.common.PriceType | Callable[[str, pybroker.common.BarData], int | float | decimal.Decimal]
- exit_sell_fill_price
Fill price for selling an open long position when
exit_on_last_bar
isTrue
. Defaults topybroker.common.PriceType.MIDDLE
.- Type:
pybroker.common.PriceType | Callable[[str, pybroker.common.BarData], int | float | decimal.Decimal]
- bars_per_year
Number of observations per year that will be used to annualize evaluation metrics. For example, a value of
252
would be used to annualize the Sharpe Ratio for daily returns.- Type:
int | None
- return_signals
When
True
, then bar data, indicator data, and model predictions are returned withpybroker.strategy.TestResult
. Defaults toFalse
.- Type:
- return_stops
When
True
, then stop values are returned withpybroker.strategy.TestResult
. Defaults toFalse
.- Type:
- round_test_result
When
True
, round values inpybroker.strategy.TestResult
up to the nearest cent. Defaults toTrue
.- Type:
PyBroker Modules
pybroker package
Global imports.
Submodules
pybroker.cache module
Contains caching utilities.
- class CacheDateFields(start_date: datetime, end_date: datetime, tf_seconds: int, between_time: tuple[str, str] | None, days: tuple[int] | None)[source]
Bases:
object
Date fields for keying cache data.
- start_date
Start date of cache data.
- Type:
- end_date
End date of cache data.
- Type:
- between_time
tuple[str, str]
of times of day (e.g. 9:00-9:30 AM) that were used to filter the cache data.
- class DataSourceCacheKey(symbol: str, tf_seconds: int, start_date: datetime, end_date: datetime, adjust: str | None)[source]
Bases:
object
Cache key used for
pybroker.data.DataSource
data.
- class IndicatorCacheKey(symbol: str, tf_seconds: int, start_date: datetime, end_date: datetime, between_time: tuple[str, str] | None, days: tuple[int] | None, ind_name: str)[source]
Bases:
object
Cache key used for indicator data.
- class ModelCacheKey(symbol: str, tf_seconds: int, start_date: datetime, end_date: datetime, between_time: tuple[str, str] | None, days: tuple[int] | None, model_name: str)[source]
Bases:
object
Cache key used for trained models.
- clear_caches()[source]
Clears cached data from all caches.
enable_caches()
must be called first before clearing.
- clear_data_source_cache()[source]
Clears data cached from
pybroker.data.DataSource
s.enable_data_source_cache()
must be called first before clearing.
- clear_indicator_cache()[source]
Clears cached indicator data.
enable_indicator_cache()
must be called first before clearing.
- clear_model_cache()[source]
Clears cached trained models.
enable_model_cache()
must be called first before clearing.
- disable_data_source_cache()[source]
Disables caching data retrieved from
pybroker.data.DataSource
s.
- enable_caches(namespace, cache_dir: str | None = None)[source]
Enables all caches.
- Parameters:
namespace – Namespace shared by cached data.
cache_dir – Directory used to store cached data.
- enable_data_source_cache(namespace: str, cache_dir: str | None = None) Cache [source]
Enables caching of data retrieved from
pybroker.data.DataSource
s.- Parameters:
namespace – Namespace of the cache.
cache_dir – Directory used to store cached data.
- Returns:
diskcache.Cache
instance.
pybroker.common module
Contains common classes and utilities.
- class BarData(date: ndarray[Any, dtype[datetime64]], open: ndarray[Any, dtype[float64]], high: ndarray[Any, dtype[float64]], low: ndarray[Any, dtype[float64]], close: ndarray[Any, dtype[float64]], volume: ndarray[Any, dtype[float64]] | None, vwap: ndarray[Any, dtype[float64]] | None, **kwargs)[source]
Bases:
object
Contains data for a series of bars. Each field is a
numpy.ndarray
that contains bar values in the series. The values are sorted in ascending chronological order.- Parameters:
date – Timestamps of each bar.
open – Open prices.
high – High prices.
low – Low prices.
close – Close prices.
volume – Trading volumes.
vwap – Volume-weighted average prices (VWAP).
**kwargs – Custom data fields.
- class DataCol(value)[source]
Bases:
Enum
Default data column names.
- CLOSE = 'close'
- DATE = 'date'
- HIGH = 'high'
- LOW = 'low'
- OPEN = 'open'
- SYMBOL = 'symbol'
- VOLUME = 'volume'
- VWAP = 'vwap'
- class Day(value)[source]
Bases:
Enum
Enumeration of days.
- FRI = 4
- MON = 0
- SAT = 5
- SUN = 6
- THURS = 3
- TUES = 1
- WEDS = 2
- class FeeInfo(symbol: str, shares: Decimal, fill_price: Decimal, order_type: Literal['buy', 'sell'])[source]
Bases:
NamedTuple
Contains info for custom fee calculations.
Number of shares in order.
- Type:
- fill_price
Fill price of order.
- Type:
- order_type
Type of order, either “buy” or “sell”.
- Type:
Literal[‘buy’, ‘sell’]
- class FeeMode(value)[source]
Bases:
Enum
Brokerage fee mode to use for backtesting.
- ORDER_PERCENT
Fee is a percentage of order amount, where order amount is fill_price * shares.
- PER_ORDER
Fee is a constant amount per order.
- PER_SHARE
Fee is a constant amount per share in order.
- ORDER_PERCENT = 'order_percent'
- PER_ORDER = 'per_order'
- PER_SHARE = 'per_share'
- class IndicatorSymbol(ind_name: str, symbol: str)[source]
Bases:
NamedTuple
pybroker.indicator.Indicator
/symbol identifier.
- class ModelSymbol(model_name: str, symbol: str)[source]
Bases:
NamedTuple
pybroker.model.ModelSource
/symbol identifier.
- class PriceType(value)[source]
Bases:
Enum
Enumeration of price types used to specify fill price with
pybroker.context.ExecContext
.- OPEN
Open price of the current bar.
- LOW
Low price of the current bar.
- HIGH
High price of the current bar.
- CLOSE
Close price of the current bar.
- MIDDLE
Midpoint between low price and high price of the current bar.
- AVERAGE
Average of open, low, high, and close prices of the current bar.
- AVERAGE = 'average'
- CLOSE = 'close'
- HIGH = 'high'
- LOW = 'low'
- MIDDLE = 'middle'
- OPEN = 'open'
- class StopType(value)[source]
Bases:
Enum
Stop types.
- BAR
Stop that triggers after n bars.
- LOSS
Stop loss.
- PROFIT
Take profit.
- TRAILING
Trailing stop loss.
- BAR = 'bar'
- LOSS = 'loss'
- PROFIT = 'profit'
- TRAILING = 'trailing'
- class TrainedModel(name: str, instance: Any, predict_fn: Callable[[Any, DataFrame], ndarray[Any, dtype[_ScalarType_co]]] | None, input_cols: tuple[str] | None)[source]
Bases:
NamedTuple
Trained model/symbol identifier.
- instance
Trained model instance.
- Type:
Any
- predict_fn
Callable
that overrides calling the model’s defaultpredict
function.- Type:
Callable[[Any, pandas.core.frame.DataFrame], numpy.ndarray[Any, numpy.dtype[numpy._typing._array_like._ScalarType_co]]] | None
- input_cols
Names of the columns to be used as input for the model when making predictions.
- default_parallel() Parallel [source]
Returns a
joblib.Parallel
instance withn_jobs
equal to the number of CPUs on the host machine.
- get_unique_sorted_dates(col: Series) Sequence[datetime64] [source]
Returns sorted unique values from a DataFrame column of dates. Guarantees compatability between Pandas 1 and 2.
- parse_timeframe(timeframe: str) list[tuple[int, str]] [source]
Parses timeframe string with the following units:
"s"
/"sec"
: seconds"m"
/"min"
: minutes"h"
/"hour"
: hours"d"
/"day"
: days"w"
/"week"
: weeks
An example timeframe string is
1h 30m
.- Returns:
list
oftuple[int, str]
, where each tuple contains anint
value andstr
unit.
- quantize(df: DataFrame, col: str, round: bool) Series [source]
Quantizes a
pandas.DataFrame
column by rounding values to the nearest cent.- Returns:
The quantized column converted to
float
values.
- to_datetime(date: str | datetime | datetime64 | Timestamp) datetime [source]
Converts
date
todatetime
.
- to_seconds(timeframe: str | None) int [source]
Converts a timeframe string to seconds, where
timeframe
supports the following units:"s"
/"sec"
: seconds"m"
/"min"
: minutes"h"
/"hour"
: hours"d"
/"day"
: days"w"
/"week"
: weeks
An example timeframe string is
1h 30m
.- Returns:
The converted number of seconds.
- verify_data_source_columns(df: DataFrame)[source]
Verifies that a
pandas.DataFrame
contains all of the columns required by apybroker.data.DataSource
.
pybroker.context module
Contains context related classes. A context provides data during the
execution of a pybroker.strategy.Strategy
.
- class BaseContext(config: StrategyConfig, portfolio: Portfolio, col_scope: ColumnScope, ind_scope: IndicatorScope, input_scope: ModelInputScope, pred_scope: PredictionScope, pending_order_scope: PendingOrderScope, models: Mapping[ModelSymbol, TrainedModel], sym_end_index: Mapping[str, int])[source]
Bases:
object
Base context class.
- config
Calculates the number of shares given a
target_size
allocation and shareprice
.- Parameters:
target_size – Proportion of cash used to calculate the number of shares, where the max
target_size
is1
. For example, atarget_size
of0.1
would represent 10% of cash.price – Share price used to calculate the number of shares.
cash – Cash used to calculate the number of shares. If
None
, then thepybroker.portfolio.Portfolio
equity is used to calculate the number of shares.
- Returns:
Number of shares given
target_size
and shareprice
. Ifpybroker.config.StrategyConfig.enable_fractional_shares
isTrue
, then a Decimal is returned.
- property cash: Decimal
Total cash currently held in the
pybroker.portfolio.Portfolio
.
- indicator(name: str, symbol: str) ndarray[Any, dtype[float64]] [source]
Returns indicator data.
- Parameters:
name – Name used to identify the indicator that was registered with
pybroker.indicator.indicator()
.symbol – Ticker symbol that was used to generate the indicator data.
- Returns:
numpy.ndarray
of indicator data for all bars up to the current one, sorted in ascending chronological order.
- input(model_name: str, symbol: str) DataFrame [source]
Returns model input data for making predictions.
- Parameters:
model_name – Name of the model for the input data.
symbol – Ticker symbol of the model for the input data.
- Returns:
pandas.DataFrame
containing the input data, where each row represents a bar in the sequence up to the current bar. The rows are sorted in ascending chronological order.
- long_positions(symbol: str | None = None) Iterator[Position] [source]
Retrieves all current long positions.
- Parameters:
symbol – Ticker symbol used to filter positions. If
None
, long positions for all symbols are returned. Defaults toNone
.- Returns:
Iterator
of currently held longpybroker.portfolio.Position
s.
- model(name: str, symbol: str) Any [source]
Returns a trained model.
- Parameters:
name – Name used to identify the model that was registered with
pybroker.model.model()
.symbol – Ticker symbol of the data that was used to train the model.
- Returns:
Instance of the trained model.
- orders() Iterator[Order] [source]
Iterator
of allpybroker.portfolio.Order
s that have been placed and filled.
- pos(symbol: str, pos_type: Literal['long', 'short']) Position | None [source]
Retrieves a current long or short
pybroker.portfolio.Position
for asymbol
.- Parameters:
symbol – Ticker symbol of the position to return.
pos_type – Specifies whether to return a
long
orshort
position.
- Returns:
pybroker.portfolio.Position
if one exists, otherwiseNone
.
- positions(symbol: str | None = None, pos_type: Literal['long', 'short'] | None = None) Iterator[Position] [source]
Retrieves all current positions.
- Parameters:
symbol – Ticker symbol used to filter positions. If
None
, positions for all symbols are returned. Defaults toNone
.pos_type – Type of positions to return. If
None
, bothlong
andshort
positions are returned.
- Returns:
Iterator
of currently heldpybroker.portfolio.Position
s.
- preds(model_name: str, symbol: str) ndarray[Any, dtype[_ScalarType_co]] [source]
Returns model predictions.
- Parameters:
model_name – Name of the model that made the predictions.
symbol – Ticker symbol of the model that made the predictions.
- Returns:
numpy.ndarray
containing the sequence of model predictions up to the current bar. Sorted in ascending chronological order.
- short_positions(symbol: str | None = None) Iterator[Position] [source]
Retrieves all current short positions.
- Parameters:
symbol – Ticker symbol used to filter positions. If
None
, short positions for all symbols are returned. Defaults toNone
.- Returns:
Iterator
of currently held shortpybroker.portfolio.Position
s.
- property total_equity: Decimal
Total equity currently held in the
pybroker.portfolio.Portfolio
.
- property total_margin: Decimal
Total amount of margin currently held in the
pybroker.portfolio.Portfolio
.
- property total_market_value: Decimal
Total market value currently held in the
pybroker.portfolio.Portfolio
. The market value is defined as the amount of equity held in cash and long positions added together with the unrealized PnL of all open short positions.
- trades() Iterator[Trade] [source]
Iterator
of allpybroker.portfolio.Trade
s that have been completed.
- class ExecContext(symbol: str, config: StrategyConfig, portfolio: Portfolio, col_scope: ColumnScope, ind_scope: IndicatorScope, input_scope: ModelInputScope, pred_scope: PredictionScope, pending_order_scope: PendingOrderScope, models: Mapping[ModelSymbol, TrainedModel], sym_end_index: Mapping[str, int], session: MutableMapping)[source]
Bases:
BaseContext
Contains context data during the execution of a
pybroker.strategy.Strategy
. Includes data about the current bar, portfolio positions, and other relevant context. This class is also used to set buy and sell signals for placing orders.The data contained in this class is for the latest bar that has already completed. Placing an order will be executed on a future bar specified by
pybroker.config.StrategyConfig.buy_delay
andpybroker.config.StrategyConfig.sell_delay
.- symbol
Current ticker symbol of the execution.
- buy_fill_price
Fill price to use for a buy (long) order of
symbol
.
Number of shares to buy of
symbol
.
- buy_limit_price
Limit price to use for a buy (long) order of
symbol
.
- sell_fill_price
Fill price to use for a sell (short) order of
symbol
.
Number of shares to sell of
symbol
.
- sell_limit_price
Limit price to use for a sell (short) order of
symbol
.
- hold_bars
Number of bars to hold a long or short position for, after which the position is automatically liquidated.
- score
Score used to rank
symbol
when ranking buy and sell signals. Orders are placed for symbols with the highest scores, where the number of positions held at any time in thepybroker.portfolio.Portfolio
is specified bypybroker.config.StrategyConfig.max_long_positions
andpybroker.config.StrategyConfig.max_short_positions
respectively. Long and short signals are ranked separately byscore
.
- session
dict
used to store custom data that persists for each bar during thepybroker.strategy.Strategy
‘s execution.
- stop_loss
Sets stop loss on a new
pybroker.portfolio.Entry
, where value is measured in points from entry price.
- stop_loss_pct
Sets stop loss on a new
pybroker.portfolio.Entry
, where value is measured in percentage from entry price.
- stop_loss_limit
Limit price to use for the stop loss.
- stop_loss_exit_price
Exit
pybroker.common.PriceType
to use for the stop loss exit. If set, the stop is checked against theexit_price
and exits at theexit_price
when triggered.
- stop_profit
Sets profit stop on a new
pybroker.portfolio.Entry
, where value is measured in points from entry price.
- stop_profit_pct
Sets profit stop on a new
pybroker.portfolio.Entry
, where value is measured in percentage from entry price.
- stop_profit_limit
Limit price to use for the profit stop.
- stop_profit_exit_price
Exit
pybroker.common.PriceType
to use for the profit stop exit. If set, the stop is checked against theexit_price
and exits at theexit_price
when triggered.
- stop_trailing
Sets a trailing stop loss on a new
pybroker.portfolio.Entry
, where value is measured in points from entry price.
- stop_trailing_pct
Sets a trailing stop loss on a new
pybroker.portfolio.Entry
, where value is measured in percentage from entry price.
- stop_trailing_limit
Limit price to use for the trailing stop loss.
- stop_trailing_exit_price
Exit
pybroker.common.PriceType
to use for the trailing stop exit. If set, the stop is checked against theexit_price
and exits at theexit_price
when triggered.
Calculates the number of shares given a
target_size
allocation and shareprice
.- Parameters:
target_size – Proportion of cash used to calculate the number of shares, where the max
target_size
is1
. For example, atarget_size
of0.1
would represent 10% of cash.price – Share price used to calculate the number of shares. If
None
, the share price of theExecContext
‘ssymbol
is used.cash – Cash used to calculate the number of number of shares. If
None
, then thepybroker.portfolio.Portfolio
equity is used to calculate the number of shares.
- Returns:
Number of shares given
target_size
and shareprice
. Ifpybroker.config.StrategyConfig.enable_fractional_shares
isTrue
, then a Decimal is returned.
- cancel_all_pending_orders(symbol: str | None = None)[source]
Cancels all
pybroker.scope.PendingOrder
s forsymbol
. Whensymbol
isNone
, all pending orders are canceled.
- cancel_pending_order(order_id: int) bool [source]
Cancels a
pybroker.scope.PendingOrder
withorder_id
.
- cancel_stop(stop_id: int) bool [source]
Cancels a
pybroker.portfolio.Stop
withstop_id
.
- cancel_stops(val: str | Position | Entry, stop_type: StopType | None = None)[source]
Cancels
pybroker.portfolio.Stop
s.- Parameters:
val – Ticker symbol,
pybroker.portfolio.Position
, orpybroker.portfolio.Entry
for which to cancel stops.stop_type –
pybroker.common.StopType
.
Covers all short shares of
ExecContext.symbol
.
- property cover_fill_price: int | float | floating | Decimal | PriceType | Callable[[str, BarData], int | float | Decimal] | None
Alias for
buy_fill_price
. When set, this causes the buy order to be placed before any sell orders.
- property cover_limit_price: int | float | Decimal | None
Alias for
buy_limit_price
. When set, this causes the buy order to be placed before any sell orders.
Alias for
buy_shares
. When set, this causes the buy order to be placed before any sell orders.
- foreign(symbol: str, col: str | None = None) BarData | ndarray[Any, dtype[_ScalarType_co]] | None [source]
Retrieves bar data for another ticker symbol.
- Parameters:
symbol – Ticker symbol of the bar data.
col – Name of the data column to retrieve. If
None
, all data columns are returned inpybroker.common.BarData
.
- Returns:
If
col
isNone
, apybroker.common.BarData
instance containing data of all bars up to the current one. Otherwise, annumpy.ndarray
containing values of the columncol
.
- indicator(name: str, symbol: str | None = None) ndarray[Any, dtype[float64]] [source]
Returns indicator data.
- Parameters:
name – Name used to identify the indicator, registered with
pybroker.indicator.indicator()
.symbol – Ticker symbol that was used to generate the indicator data. If
None
, theExecContext
‘ssymbol
is used.
- Returns:
numpy.ndarray
of indicator values for all bars up to the current one, sorted in ascending chronological order.
- input(model_name: str, symbol: str | None = None) DataFrame [source]
Returns model input data for making predictions.
- Parameters:
model_name – Name of the model for the input data.
symbol – Ticker symbol of the model for the input data. If
None
, theExecContext
‘ssymbol
is used.
- Returns:
pandas.DataFrame
containing the input data, where each row represents a bar in the sequence up to the current bar. The rows are sorted in ascending chronological order.
- long_pos(symbol: str | None = None) Position | None [source]
Retrieves a current long
pybroker.portfolio.Position
for asymbol
.- Parameters:
symbol – Ticker symbol of the position to return. If
None
, theExecContext
‘ssymbol
is used. Defaults toNone
.- Returns:
pybroker.portfolio.Position
if one exists, otherwiseNone
.
- model(name: str, symbol: str | None = None) Any [source]
Returns a trained model.
- Parameters:
name – Name used to identify the model that was registered with
pybroker.model.model()
.symbol – Ticker symbol of the data that was used to train the model. If
None
, theExecContext
‘ssymbol
is used.
- Returns:
Instance of the trained model.
- preds(model_name: str, symbol: str | None = None) ndarray[Any, dtype[_ScalarType_co]] [source]
Returns model predictions.
- Parameters:
model_name – Name of the model that made the predictions.
symbol – Ticker symbol of the model that made the predictions. If
None
, theExecContext
‘ssymbol
is used.
- Returns:
numpy.ndarray
containing the sequence of model predictions up to the current bar. Sorted in ascending chronological order.
Sells all long shares of
ExecContext.symbol
.
- short_pos(symbol: str | None = None) Position | None [source]
Retrieves a current short
pybroker.portfolio.Position
for asymbol
.- Parameters:
symbol – Ticker symbol of the position to return. If
None
, theExecContext
‘ssymbol
is used. Defaults toNone
.- Returns:
pybroker.portfolio.Position
if one exists, otherwiseNone
.
- to_result() ExecResult | None [source]
Creates an
ExecResult
from the data set onExecContext
.
- class ExecResult(symbol: str, date: datetime64, buy_fill_price: int | float | floating | Decimal | PriceType | Callable[[str, BarData], int | float | Decimal], sell_fill_price: int | float | floating | Decimal | PriceType | Callable[[str, BarData], int | float | Decimal], score: float | None, hold_bars: int | None, buy_shares: Decimal | None, buy_limit_price: Decimal | None, sell_shares: Decimal | None, sell_limit_price: Decimal | None, long_stops: frozenset[Stop] | None, short_stops: frozenset[Stop] | None, cover: bool = False, pending_order_id: int | None = None)[source]
Bases:
object
Holds data that was set during the execution of a
pybroker.strategy.Strategy
.- date
Timestamp of the bar that was used for the execution.
- Type:
- buy_fill_price
Fill price to use for a buy (long) order of
symbol
.- Type:
int | float | numpy.floating | decimal.Decimal | pybroker.common.PriceType | Callable[[str, pybroker.common.BarData], int | float | decimal.Decimal]
- sell_fill_price
Fill price to use for a sell (short) order of
symbol
.- Type:
int | float | numpy.floating | decimal.Decimal | pybroker.common.PriceType | Callable[[str, pybroker.common.BarData], int | float | decimal.Decimal]
- score
Score used to rank
symbol
when ranking long and short signals. Orders are placed for symbols with the highest scores, where the number of positions held at any time in thepybroker.portfolio.Portfolio
is specified bypybroker.config.StrategyConfig.max_long_positions
andpybroker.config.StrategyConfig.max_short_positions
respectively. Buy and sell signals are ranked separately byscore
.- Type:
float | None
- hold_bars
Number of bars to hold a long or short position for, after which the position is automatically liquidated.
- Type:
int | None
Number of shares to buy of
symbol
.- Type:
decimal.Decimal | None
- buy_limit_price
Limit price used for a buy (long) order of
symbol
.- Type:
decimal.Decimal | None
Number of shares to sell of
symbol
.- Type:
decimal.Decimal | None
- sell_limit_price
Limit price used for a sell (short) order of
symbol
.- Type:
decimal.Decimal | None
- long_stops
Stops for long
pybroker.portfolio.Entry
s.- Type:
frozenset[pybroker.portfolio.Stop] | None
- short_stops
Stops for short
pybroker.portfolio.Entry
s.- Type:
frozenset[pybroker.portfolio.Stop] | None
- cover
Whether
buy_shares
are used to cover a short position. IfTrue
, the resulting buy order will be placed before sell orders.- Type:
- pending_order_id
ID of
pybroker.scope.PendingOrder
that was created.- Type:
int | None
- class ExecSignal(id: int, symbol: str, shares: int | float | Decimal, score: float | None, bar_data: BarData, type: Literal['buy', 'sell'])[source]
Bases:
NamedTuple
Holds data of a buy/sell signal.
Number of shares that was set by the
pybroker.strategy.Strategy
execution.- Type:
int | float | decimal.Decimal
- score
Score that was set by the
pybroker.strategy.Strategy
execution.- Type:
float | None
- bar_data
pybroker.common.BarData
forsymbol
.- Type:
- type
buy
orsell
signal type.- Type:
Literal[‘buy’, ‘sell’]
- class PosSizeContext(config: StrategyConfig, portfolio: Portfolio, col_scope: ColumnScope, ind_scope: IndicatorScope, input_scope: ModelInputScope, pred_scope: PredictionScope, pending_order_scope: PendingOrderScope, models: Mapping[ModelSymbol, TrainedModel], sessions: Mapping[str, Mapping], sym_end_index: Mapping[str, int])[source]
Bases:
BaseContext
Holds data for a position size handler set with
pybroker.Strategy.set_pos_size_handler()
. Used to set position sizes when placing orders from buy and sell signals.- sessions
dict
used to store custom data for all symbols.
Sets the number of shares of an order for the buy or sell signal.
- signals(signal_type: Literal['buy', 'sell'] | None = None) Iterator[ExecSignal] [source]
Returns
Iterator
ofExecSignal
s containing data for buy and sell signals.
- set_exec_ctx_data(ctx: ExecContext, date: datetime64)[source]
Sets data on an
ExecContext
instance.- Parameters:
ctx –
ExecContext
.date – Current bar’s date.
- set_pos_size_ctx_data(ctx: PosSizeContext, buy_results: list[ExecResult] | None, sell_results: list[ExecResult] | None)[source]
Sets data on a
PosSizeContext
instance.- Parameters:
ctx –
PosSizeContext
.buy_results –
ExecResult
s of buy signals.sell_results –
ExecResult
s of sell signals.
pybroker.data module
Contains DataSource
s used to fetch external data.
- class Alpaca(api_key: str, api_secret: str)[source]
Bases:
DataSource
Retrieves stock data from Alpaca.
- query(symbols: str | Iterable[str], start_date: str | datetime, end_date: str | datetime, timeframe: str | None = '1d', adjust: str | None = None) DataFrame [source]
Queries data. Cached data is returned if caching is enabled by calling
pybroker.cache.enable_data_source_cache()
.- Parameters:
symbols – Symbols of the data to query.
start_date – Start date of the data to query (inclusive).
end_date – End date of the data to query (inclusive).
timeframe –
Formatted string that specifies the timeframe resolution to query. The timeframe string supports the following units:
"s"
/"sec"
: seconds"m"
/"min"
: minutes"h"
/"hour"
: hours"d"
/"day"
: days"w"
/"week"
: weeks
An example timeframe string is
1h 30m
.adjust – The type of adjustment to make.
- Returns:
pandas.DataFrame
containing the queried data.
- class AlpacaCrypto(api_key: str, api_secret: str)[source]
Bases:
DataSource
Retrieves crypto data from Alpaca.
- Parameters:
api_key – Alpaca API key.
api_secret – Alpaca API secret.
- COLUMNS: Final = ('symbol', 'date', 'open', 'high', 'low', 'close', 'volume', 'vwap', 'trade_count')
- query(symbols: str | Iterable[str], start_date: str | datetime, end_date: str | datetime, timeframe: str | None = '1d', _: str | None = None) DataFrame [source]
Queries data. Cached data is returned if caching is enabled by calling
pybroker.cache.enable_data_source_cache()
.- Parameters:
symbols – Symbols of the data to query.
start_date – Start date of the data to query (inclusive).
end_date – End date of the data to query (inclusive).
timeframe –
Formatted string that specifies the timeframe resolution to query. The timeframe string supports the following units:
"s"
/"sec"
: seconds"m"
/"min"
: minutes"h"
/"hour"
: hours"d"
/"day"
: days"w"
/"week"
: weeks
An example timeframe string is
1h 30m
.adjust – The type of adjustment to make.
- Returns:
pandas.DataFrame
containing the queried data.
- class DataSource[source]
Bases:
ABC
,DataSourceCacheMixin
Base class for querying data from an external source. Extend this class and override
_fetch_data()
to implement a customDataSource
that can be used withpybroker.strategy.Strategy
.- abstract _fetch_data(symbols: frozenset[str], start_date: datetime, end_date: datetime, timeframe: str | None, adjust: str | None) DataFrame [source]
Override this method to return data from a custom source. The returned
pandas.DataFrame
must contain the following columns:symbol
,date
,open
,high
,low
, andclose
.- Parameters:
symbols – Ticker symbols of the data to query.
start_date – Start date of the data to query (inclusive).
end_date – End date of the data to query (inclusive).
timeframe –
Formatted string that specifies the timeframe resolution to query. The timeframe string supports the following units:
"s"
/"sec"
: seconds"m"
/"min"
: minutes"h"
/"hour"
: hours"d"
/"day"
: days"w"
/"week"
: weeks
An example timeframe string is
1h 30m
.adjust – The type of adjustment to make.
- Returns:
pandas.DataFrame
containing the queried data.
- query(symbols: str | Iterable[str], start_date: str | datetime, end_date: str | datetime, timeframe: str | None = '', adjust: str | None = None) DataFrame [source]
Queries data. Cached data is returned if caching is enabled by calling
pybroker.cache.enable_data_source_cache()
.- Parameters:
symbols – Symbols of the data to query.
start_date – Start date of the data to query (inclusive).
end_date – End date of the data to query (inclusive).
timeframe –
Formatted string that specifies the timeframe resolution to query. The timeframe string supports the following units:
"s"
/"sec"
: seconds"m"
/"min"
: minutes"h"
/"hour"
: hours"d"
/"day"
: days"w"
/"week"
: weeks
An example timeframe string is
1h 30m
.adjust – The type of adjustment to make.
- Returns:
pandas.DataFrame
containing the queried data.
- class DataSourceCacheMixin[source]
Bases:
object
Mixin that implements fetching and storing cached
DataSource
data.- get_cached(symbols: Iterable[str], timeframe: str, start_date: str | datetime | Timestamp | datetime64, end_date: str | datetime | Timestamp | datetime64, adjust: str | None) tuple[DataFrame, Iterable[str]] [source]
Retrieves cached data from disk when caching is enabled with
pybroker.cache.enable_data_source_cache()
.- Parameters:
symbols –
Iterable
of symbols for fetching cached data.timeframe –
Formatted string that specifies the timeframe resolution of the cached data. The timeframe string supports the following units:
"s"
/"sec"
: seconds"m"
/"min"
: minutes"h"
/"hour"
: hours"d"
/"day"
: days"w"
/"week"
: weeks
An example timeframe string is
1h 30m
.start_date – Starting date of the cached data (inclusive).
end_date – Ending date of the cached data (inclusive).
adjust – The type of adjustment to make.
- Returns:
tuple[pandas.DataFrame, Iterable[str]]
containing apandas.DataFrame
with the cached data, and anIterable[str]
of symbols for which no cached data was found.
- set_cached(timeframe: str, start_date: str | datetime | Timestamp | datetime64, end_date: str | datetime | Timestamp | datetime64, adjust: str | None, data: DataFrame)[source]
Stores data to disk cache when caching is enabled with
pybroker.cache.enable_data_source_cache()
.- Parameters:
timeframe –
Formatted string that specifies the timeframe resolution of the data to cache. The timeframe string supports the following units:
"s"
/"sec"
: seconds"m"
/"min"
: minutes"h"
/"hour"
: hours"d"
/"day"
: days"w"
/"week"
: weeks
An example timeframe string would be
1h 30m
.start_date – Starting date of the data to cache (inclusive).
end_date – Ending date of the data to cache (inclusive).
adjust – The type of adjustment to make.
data –
pandas.DataFrame
containing the data to cache.
- class YFinance[source]
Bases:
DataSource
Retrieves data from Yahoo Finance.
- ADJ_CLOSE
Column name of adjusted close prices.
- Type:
Final
- query(symbols: str | Iterable[str], start_date: str | datetime, end_date: str | datetime, _timeframe: str | None = '', _adjust: str | None = None) DataFrame [source]
Queries data from Yahoo Finance. The timeframe of the data is limited to per day only.
- Parameters:
symbols – Ticker symbols of the data to query.
start_date – Start date of the data to query (inclusive).
end_date – End date of the data to query (inclusive).
- Returns:
pandas.DataFrame
containing the queried data.
pybroker.eval module
Contains implementation of evaluation metrics.
- class BootConfIntervals(low_2p5: float, high_2p5: float, low_5: float, high_5: float, low_10: float, high_10: float)[source]
Bases:
NamedTuple
Holds confidence intervals of bootstrap tests.
- class BootstrapResult(conf_intervals: DataFrame, drawdown_conf: DataFrame, profit_factor: BootConfIntervals, sharpe: BootConfIntervals, drawdown: DrawdownMetrics)[source]
Bases:
NamedTuple
Contains results of bootstrap tests.
- conf_intervals
pandas.DataFrame
containing confidence intervals forlog_profit_factor()
andsharpe_ratio()
.
- drawdown_conf
pandas.DataFrame
containing upper bounds of confidence intervals for maximum drawdown.
- profit_factor
Contains profit factor confidence intervals.
- sharpe
Contains Sharpe Ratio confidence intervals.
- drawdown
Contains drawdown confidence intervals.
- class ConfInterval(name: str, conf: str, lower: float, upper: float)[source]
Bases:
NamedTuple
Confidence interval upper and low bounds.
- class DrawdownConfs(q_001: float, q_01: float, q_05: float, q_10: float)[source]
Bases:
NamedTuple
Contains upper bounds of confidence intervals for maximum drawdown.
- class DrawdownMetrics(confs: DrawdownConfs, pct_confs: DrawdownConfs)[source]
Bases:
NamedTuple
Contains drawdown metrics.
- confs
Upper bounds of confidence intervals for maximum drawdown, measured in cash.
- pct_confs
Upper bounds of confidence intervals for maximum drawdown, measured in percentage.
- class EvalMetrics(trade_count: int = 0, initial_market_value: float = 0, end_market_value: float = 0, total_pnl: float = 0, unrealized_pnl: float = 0, total_return_pct: float = 0, annual_return_pct: float | None = None, total_profit: float = 0, total_loss: float = 0, total_fees: float = 0, max_drawdown: float = 0, max_drawdown_pct: float = 0, win_rate: float = 0, loss_rate: float = 0, winning_trades: int = 0, losing_trades: int = 0, avg_pnl: float = 0, avg_return_pct: float = 0, avg_trade_bars: float = 0, avg_profit: float = 0, avg_profit_pct: float = 0, avg_winning_trade_bars: float = 0, avg_loss: float = 0, avg_loss_pct: float = 0, avg_losing_trade_bars: float = 0, largest_win: float = 0, largest_win_pct: float = 0, largest_win_bars: int = 0, largest_loss: float = 0, largest_loss_pct: float = 0, largest_loss_bars: int = 0, max_wins: int = 0, max_losses: int = 0, sharpe: float = 0, sortino: float = 0, calmar: float | None = None, profit_factor: float = 0, ulcer_index: float = 0, upi: float = 0, equity_r2: float = 0, std_error: float = 0, annual_std_error: float | None = None, annual_volatility_pct: float | None = None)[source]
Bases:
object
Contains metrics for evaluating a
pybroker.strategy.Strategy
.- initial_market_value
Initial market value of the
pybroker.portfolio.Portfolio
.- Type:
- end_market_value
Ending market value of the
pybroker.portfolio.Portfolio
.- Type:
- total_fees
Total brokerage fees. See
pybroker.config.StrategyConfig.fee_mode
for more info.- Type:
- sharpe
Sharpe Ratio, computed per bar.
- Type:
- sortino
Sortino Ratio, computed per bar.
- Type:
- ulcer_index
Ulcer Index, computed per bar.
- Type:
- upi
Ulcer Performance Index, computed per bar.
- Type:
- class EvalResult(metrics: EvalMetrics, bootstrap: BootstrapResult | None)[source]
Bases:
NamedTuple
Contains evaluation result.
- metrics
Evaluation metrics.
- bootstrap
Randomized bootstrap metrics.
- Type:
- class EvaluateMixin[source]
Bases:
object
Mixin for computing evaluation metrics.
- evaluate(portfolio_df: DataFrame, trades_df: DataFrame, calc_bootstrap: bool, bootstrap_sample_size: int, bootstrap_samples: int, bars_per_year: int | None) EvalResult [source]
Computes evaluation metrics.
- Parameters:
portfolio_df –
pandas.DataFrame
of portfolio market values per bar.trades_df –
pandas.DataFrame
of trades.calc_bootstrap –
True
to calculate randomized bootstrap metrics.bootstrap_sample_size – Size of each random bootstrap sample.
bootstrap_samples – Number of random bootstrap samples to use.
bars_per_year – Number of observations per years that will be used to annualize evaluation metrics. For example, a value of
252
would be used to annualize the Sharpe Ratio for daily returns.
- Returns:
EvalResult
containing evaluation metrics.
- annual_total_return_percent(initial_value: float, pnl: float, bars_per_year: int, total_bars: int) float [source]
Computes annualized total return as percentage.
- Parameters:
initial_value – Initial value.
pnl – Total profit and loss (PnL).
bars_per_year – Number of bars per annum.
total_bars – Total number of bars of the return.
- avg_profit_loss(pnls: ndarray[Any, dtype[float64]]) tuple[float, float] [source]
Computes the average profit and average loss per trade.
- Parameters:
pnls – Array of profits and losses (PnLs) per trade.
- Returns:
tuple[float, float]
of average profit and average loss.
- bca_boot_conf(x: ndarray[Any, dtype[float64]], n: int, n_boot: int, fn: Callable[[ndarray[Any, dtype[float64]]], float]) BootConfIntervals [source]
Computes confidence intervals for a user-defined parameter using the bias corrected and accelerated (BCa) bootstrap method.
- Parameters:
x –
numpy.ndarray
containing the data for the randomized bootstrap sampling.n – Number of elements in each random bootstrap sample.
n_boot – Number of random bootstrap samples to use.
fn –
Callable
for computing the parameter used for the confidence intervals.
- Returns:
BootConfIntervals
containing the computed confidence intervals.
- calmar_ratio(changes: ndarray[Any, dtype[float64]], bars_per_year: int) float [source]
Computes the Calmar Ratio.
- Parameters:
changes – Array of differences between each bar and the previous bar.
bars_per_year – Number of bars per annum.
- conf_profit_factor(x: ndarray[Any, dtype[float64]], n: int, n_boot: int) BootConfIntervals [source]
Computes confidence intervals for
profit_factor()
.
- conf_sharpe_ratio(x: ndarray[Any, dtype[float64]], n: int, n_boot: int, obs: int | None = None) BootConfIntervals [source]
Computes confidence intervals for
sharpe_ratio()
.
- inverse_normal_cdf(p: float) float [source]
Computes the inverse CDF of the standard normal distribution.
- iqr(values: ndarray[Any, dtype[float64]]) float [source]
Computes the interquartile range (IQR) of
values
.
- largest_win_loss(pnls: ndarray[Any, dtype[float64]]) tuple[float, float] [source]
Computes the largest profit and largest loss of all trades.
- Parameters:
pnls – Array of profits and losses (PnLs) per trade.
- Returns:
tuple[float, float]
of largest profit and largest loss.
- log_profit_factor(changes: ndarray[Any, dtype[float64]]) floating [source]
Computes the log transformed profit factor, which is the ratio of gross profit to gross loss.
- Parameters:
changes – Array of differences between each bar and the previous bar.
- max_drawdown_percent(returns: ndarray[Any, dtype[float64]]) float [source]
Computes maximum drawdown, measured in percentage loss.
- Parameters:
returns – Array of returns centered at 0.
- max_wins_losses(pnls: ndarray[Any, dtype[float64]]) tuple[int, int] [source]
Computes the max consecutive wins and max consecutive losses.
- Parameters:
pnls – Array of profits and losses (PnLs) per trade.
- Returns:
tuple[int, int]
of max consecutive wins and max consecutive losses.
- relative_entropy(values: ndarray[Any, dtype[float64]]) float [source]
Computes the relative entropy.
- sharpe_ratio(changes: ndarray[Any, dtype[float64]], obs: int | None = None, downside_only: bool = False) floating [source]
Computes the Sharpe Ratio.
- Parameters:
changes – Array of differences between each bar and the previous bar.
obs – Number of observations used to annualize the Sharpe Ratio. For example, a value of
252
would be used to annualize daily returns.
- sortino_ratio(changes: ndarray[Any, dtype[float64]], obs: int | None = None) float [source]
Computes the Sortino Ratio.
- Parameters:
changes – Array of differences between each bar and the previous bar.
obs – Number of observations used to annualize the Sortino Ratio. For example, a value of
252
would be used to annualize daily returns.
- total_profit_loss(pnls: ndarray[Any, dtype[float64]]) tuple[float, float] [source]
Computes total profit and loss.
- Parameters:
pnls – Array of profits and losses (PnLs) per trade.
- Returns:
tuple[float, float]
of total profit and total loss.
- total_return_percent(initial_value: float, pnl: float) float [source]
Computes total return as percentage.
- Parameters:
initial_value – Initial value.
pnl – Total profit and loss (PnL).
pybroker.ext.data module
Contains extension classes.
Bases:
DataSource
Retrieves data from AKShare.
pybroker.indicator module
Contains indicator related functionality.
- class Indicator(name: str, fn: Callable[[...], ndarray[Any, dtype[float64]]], kwargs: dict[str, Any])[source]
Bases:
object
Class representing an indicator.
- Parameters:
name – Name of indicator.
fn –
Callable
used to compute the series of indicator values.kwargs –
dict
of kwargs to pass tofn
.
- class IndicatorSet[source]
Bases:
IndicatorsMixin
Computes data for multiple indicators.
- __call__(df: DataFrame, disable_parallel: bool = False) DataFrame [source]
Computes indicator data.
- Parameters:
df –
pandas.DataFrame
of input data.disable_parallel – If
True
, indicator data is computed serially. IfFalse
, indicator data is computed in parallel using multiple processes. Defaults toFalse
.
- Returns:
pandas.DataFrame
containing the computed indicator data.
- class IndicatorsMixin[source]
Bases:
object
Mixin implementing indicator related functionality.
- compute_indicators(df: DataFrame, indicator_syms: Iterable[IndicatorSymbol], cache_date_fields: CacheDateFields | None, disable_parallel: bool) dict[IndicatorSymbol, Series] [source]
Computes indicator data for the provided
pybroker.common.IndicatorSymbol
pairs.- Parameters:
df –
pandas.DataFrame
used to compute the indicator values.indicator_syms –
Iterable
ofpybroker.common.IndicatorSymbol
pairs of indicators to compute.cache_date_fields – Date fields used to key cache data. Pass
None
to disable caching.disable_parallel – If
True
, indicator data is computed serially for allpybroker.common.IndicatorSymbol
pairs. IfFalse
, indicator data is computed in parallel using multiple processes.
- Returns:
dict
mapping eachpybroker.common.IndicatorSymbol
pair to a computedpandas.Series
of indicator values.
- highest(name: str, field: str, period: int) Indicator [source]
Creates a rolling high
Indicator
.- Parameters:
name – Indicator name.
field –
pybroker.common.BarData
field for computing the rolling high.period – Lookback period.
- Returns:
Rolling high
Indicator
.
- indicator(name: str, fn: Callable[[...], ndarray[Any, dtype[float64]]], **kwargs) Indicator [source]
Creates an
Indicator
instance and registers it globally withname
.- Parameters:
name – Name for referencing the indicator globally.
fn –
Callable[[BarData, ...], NDArray[float]]
used to compute the series of indicator values.**kwargs – Additional arguments to pass to
fn
.
- Returns:
Indicator
instance.
pybroker.log module
Logging module.
- class Logger(scope)[source]
Bases:
object
Class for logging information about triggered events.
- Parameters:
scope –
pybroker.scope.StaticScope
.
- backtest_executions_start(test_dates: Sequence[datetime64])[source]
- debug_filled_buy_order(date: datetime64, symbol: str, shares: Decimal, fill_price: Decimal, limit_price: Decimal | None)[source]
- debug_filled_sell_order(date: datetime64, symbol: str, shares: Decimal, fill_price: Decimal, limit_price: Decimal | None)[source]
- debug_place_buy_order(date: datetime64, symbol: str, shares: Decimal, fill_price: Decimal, limit_price: Decimal | None)[source]
- debug_place_sell_order(date: datetime64, symbol: str, shares: Decimal, fill_price: Decimal, limit_price: Decimal | None)[source]
- debug_schedule_order(date: datetime64, exec_result)[source]
- debug_unfilled_buy_order(date: datetime64, symbol: str, shares: Decimal, fill_price: Decimal, limit_price: Decimal | None)[source]
- debug_unfilled_sell_order(date: datetime64, symbol: str, shares: Decimal, fill_price: Decimal, limit_price: Decimal | None)[source]
- info_download_bar_data_start(symbols: Iterable[str], start_date: datetime, end_date: datetime, timeframe: str)[source]
- info_indicator_data_start(ind_syms: Iterable[IndicatorSymbol])[source]
- info_loaded_bar_data(symbols: Iterable[str], start_date: datetime, end_date: datetime, timeframe: str)[source]
- info_loaded_indicator_data(ind_syms: Iterable[IndicatorSymbol])[source]
- info_loaded_model(model_sym: ModelSymbol)[source]
- info_loaded_models(model_syms: Iterable[ModelSymbol])[source]
- info_train_model_completed(model_sym: ModelSymbol)[source]
- info_train_model_start(model_sym: ModelSymbol)[source]
- info_train_split_start(model_syms: Iterable[ModelSymbol])[source]
- train_split_start(train_dates: Sequence[datetime64])[source]
pybroker.model module
Contains model related functionality.
- class CachedModel(model: Any, input_cols: tuple[str] | None)[source]
Bases:
NamedTuple
Stores cached model data.
- model
Trained model instance.
- Type:
Any
- input_cols
Names of the columns to be used as input for the model when making predictions.
- class ModelLoader(name: str, load_fn: Callable[[...], Any | tuple[Any, Iterable[str]]], indicator_names: Iterable[str], input_data_fn: Callable[[DataFrame], DataFrame] | None, predict_fn: Callable[[Any, DataFrame], ndarray[Any, dtype[_ScalarType_co]]] | None, kwargs: dict[str, Any])[source]
Bases:
ModelSource
Loads a pre-trained model.
- Parameters:
name – Name of model.
load_fn –
Callable[[symbol: str, train_start_date: datetime, train_end_date: datetime, ...], DataFrame]
used to load and return a pre-trained model. This is expected to return either a trained model instance, or a tuple containing a trained model instance and aIterable
of column names to to be used as input for the model when making predictions.indicator_names –
Iterable
of names ofpybroker.indicator.Indicator
s used as features of the model.input_data_fn –
Callable[[DataFrame], DataFrame]
for preprocessing input data passed to the model when making predictions. If set,input_data_fn
will be called with apandas.DataFrame
containing all test data.predict_fn –
Callable[[Model, DataFrame], ndarray]
that overrides calling the model’s defaultpredict
function. If set,predict_fn
will be called with the trained model and apandas.DataFrame
containing all test data.kwargs –
dict
of kwargs to pass toload_fn
.
- __call__(symbol: str, train_start_date: datetime, train_end_date: datetime) Any | tuple[Any, Iterable[str]] [source]
Loads pre-trained model.
- Parameters:
symbol – Ticker symbol for loading the pre-trained model.
train_start_date – Start date of training window.
train_end_date – End date of training window.
- Returns:
Pre-trained model.
- class ModelSource(name: str, indicator_names: Iterable[str], input_data_fn: Callable[[DataFrame], DataFrame] | None, predict_fn: Callable[[Any, DataFrame], ndarray[Any, dtype[_ScalarType_co]]] | None, kwargs: dict[str, Any])[source]
Bases:
object
Base class of a model source. A model source provides a model instance either by training one or by loading a pre-trained model.
- Parameters:
name – Name of model.
indicator_names –
Iterable
of names ofpybroker.indicator.Indicator
s used as features of the model.input_data_fn –
Callable[[DataFrame], DataFrame]
for preprocessing input data passed to the model when making predictions. If set,input_data_fn
will be called with apandas.DataFrame
containing all test data.predict_fn –
Callable[[Model, DataFrame], ndarray]
that overrides calling the model’s defaultpredict
function. If set,predict_fn
will be called with the trained model and apandas.DataFrame
containing all test data.kwargs –
dict
of additional kwargs.
- prepare_input_data(df: DataFrame) DataFrame [source]
Prepares a
pandas.DataFrame
of input data for passing to a model when making predictions. If set, theinput_data_fn
is used to preprocess the input data. IfFalse
, then indicator columns indf
are used as input features.
- class ModelTrainer(name: str, train_fn: Callable[[...], Any | tuple[Any, Iterable[str]]], indicator_names: Iterable[str], input_data_fn: Callable[[DataFrame], DataFrame] | None, predict_fn: Callable[[Any, DataFrame], ndarray[Any, dtype[_ScalarType_co]]] | None, kwargs: dict[str, Any])[source]
Bases:
ModelSource
Trains a model.
- Parameters:
name – Name of model.
train_fn –
Callable[[symbol: str, train_data: DataFrame, test_data: DataFrame, ...], DataFrame]
used to train and return a model. This is expected to return either a trained model instance, or a tuple containing a trained model instance and aIterable
of column names to to be used as input for the model when making predictions.indicator_names –
Iterable
of names ofpybroker.indicator.Indicator
s used as features of the model.input_data_fn –
Callable[[DataFrame], DataFrame]
for preprocessing input data passed to the model when making predictions. If set,input_data_fn
will be called with apandas.DataFrame
containing all test data.predict_fn –
Callable[[Model, DataFrame], ndarray]
that overrides calling the model’s defaultpredict
function. If set,predict_fn
will be called with the trained model and apandas.DataFrame
containing all test data.kwargs –
dict
of kwargs to pass totrain_fn
.
- class ModelsMixin[source]
Bases:
object
Mixin implementing model related functionality.
- train_models(model_syms: Iterable[ModelSymbol], train_data: DataFrame, test_data: DataFrame, indicator_data: Mapping[IndicatorSymbol, Series], cache_date_fields: CacheDateFields) dict[ModelSymbol, TrainedModel] [source]
Trains models for the provided
pybroker.common.ModelSymbol
pairs.- Parameters:
model_syms –
Iterable
ofpybroker.common.ModelSymbol
pairs of models to train.train_data –
pandas.DataFrame
of training data.test_data –
pandas.DataFrame
of test data.indicator_data –
Mapping
ofpybroker.common.IndicatorSymbol
pairs topandas.Series
ofpybroker.indicator.Indicator
values.cache_date_fields – Date fields used to key cache data.
- Returns:
dict
mapping eachpybroker.common.ModelSymbol
pair to apybroker.common.TrainedModel
.
- model(name: str, fn: Callable[[...], Any | tuple[Any, Iterable[str]]], indicators: Iterable[Indicator] | None = None, input_data_fn: Callable[[DataFrame], DataFrame] | None = None, predict_fn: Callable[[Any, DataFrame], ndarray[Any, dtype[_ScalarType_co]]] | None = None, pretrained: bool = False, **kwargs) ModelSource [source]
Creates a
ModelSource
instance and registers it globally withname
.- Parameters:
name – Name for referencing the model globally.
fn –
Callable
used to either train or load a model instance. If for training, thenfn
has signatureCallable[[symbol: str, train_data: DataFrame, test_data: DataFrame, ...], DataFrame]
. If for loading, thenfn
has signatureCallable[[symbol: str, train_start_date: datetime, train_end_date: datetime, ...], DataFrame]
. This is expected to return either a trained model instance, or a tuple containing a trained model instance and aIterable
of column names to to be used as input for the model when making predictions.indicators –
Iterable
ofpybroker.indicator.Indicator
s used as features of the model.input_data_fn –
Callable[[DataFrame], DataFrame]
for preprocessing input data passed to the model when making predictions. If set,input_data_fn
will be called with apandas.DataFrame
containing all test data.predict_fn –
Callable[[Model, DataFrame], ndarray]
that overrides calling the model’s defaultpredict
function. If set,predict_fn
will be called with the trained model and apandas.DataFrame
containing all test data.pretrained – If
True
, thenfn
is used to load and return a pre-trained model. IfFalse
,fn
is used to train and return a new model. Defaults toFalse
.**kwargs – Additional arguments to pass to
fn
.
- Returns:
ModelSource
instance.
pybroker.portfolio module
Contains portfolio related functionality, such as portfolio metrics and placing orders.
- class Entry(id: int, date: ~numpy.datetime64, symbol: str, shares: ~decimal.Decimal, price: ~decimal.Decimal, type: ~typing.Literal['long', 'short'], bars: int = 0, stops: list[~pybroker.portfolio.Stop] = <factory>, mae: ~decimal.Decimal = <factory>, mfe: ~decimal.Decimal = <factory>)[source]
Bases:
object
Contains information about an entry into a
Position
.- date
Date of the entry.
- Type:
Number of shares.
- Type:
- price
Share price of the entry.
- Type:
- stops
Stops set on the entry.
- Type:
- mae
Maximum adverse excursion (MAE).
- Type:
- mfe
Maximum favorable excursion (MFE).
- Type:
- class Order(id: int, type: Literal['buy', 'sell'], symbol: str, date: datetime64, shares: Decimal, limit_price: Decimal | None, fill_price: Decimal, fees: Decimal)[source]
Bases:
NamedTuple
Holds information about a filled order.
- type
Type of order, either
buy
orsell
.- Type:
Literal[‘buy’, ‘sell’]
- date
Date the order was filled.
- Type:
Number of shares bought or sold.
- Type:
- limit_price
Limit price that was used for the order.
- Type:
decimal.Decimal | None
- fill_price
Price that the order was filled at.
- Type:
- fees
Brokerage fees for order.
- Type:
- class Portfolio(cash: float, fee_mode: FeeMode | Callable[[FeeInfo], Decimal] | None = None, fee_amount: float | None = None, subtract_fees: bool = False, enable_fractional_shares: bool = False, max_long_positions: int | None = None, max_short_positions: int | None = None, record_stops: bool | None = False)[source]
Bases:
object
Class representing a portfolio of holdings. The portfolio contains information about open positions and balances, and is also used to place buy and sell orders.
- Parameters:
cash – Starting cash balance.
fee_mode – Brokerage fee mode.
fee_amount – Brokerage fee amount.
subtract_fees – Whether to subtract fees from the cash balance after an order is filled.
enable_fractional_shares – Whether to enable trading fractional shares.
max_long_positions – Maximum number of long
Position
s that can be held at a time. IfNone
, then unlimited.max_short_positions – Maximum number of short
Position
s that can be held at a time. IfNone
, then unlimited.record_stops – Whether to record stop data per-bar.
- cash
Current cash balance.
- equity
Current amount of equity.
- market_value
Current market value. The market value is defined as the amount of equity held in cash and long positions added together with the unrealized PnL of all open short positions.
- fees
Current brokerage fees.
- fee_amount
Brokerage fee amount.
- subtract_fees
Whether to subtract fees from the cash balance.
Whether to enable trading fractional shares.
- orders
deque
of all filled orders, sorted in ascending chronological order.
- margin
Current amount of margin held in open positions.
- pnl
Realized profit and loss (PnL).
- symbols
Ticker symbols of all currently open positions.
- position_bars
deque
of snapshots ofPosition
states on every bar, sorted in ascending chronological order.
- win_rate
Running win rate of trades.
- loss_rate
Running loss rate of trades.
- buy(date: datetime64, symbol: str, shares: Decimal, fill_price: Decimal, limit_price: Decimal | None = None, stops: Iterable[Stop] | None = None) Order | None [source]
Places a buy order.
- Parameters:
- Returns:
Order
if the order was filled, otherwiseNone
.
- capture_bar(date: datetime64, df: DataFrame)[source]
Captures portfolio state of the current bar.
- Parameters:
date – Date of current bar.
df –
pandas.DataFrame
containing close prices.
- check_stops(date: datetime64, price_scope: PriceScope)[source]
Checks whether stops are triggered.
- exit_position(date: datetime64, symbol: str, buy_fill_price: Decimal, sell_fill_price: Decimal)[source]
Exits any long and short positions for
symbol
atbuy_fill_price
andsell_fill_price
.
- remove_stops(val: str | Position | Entry, stop_type: StopType | None = None)[source]
Removes
Stop
s.- Parameters:
val – Ticker symbol,
Position
, orEntry
for which to cancel stops.stop_type –
pybroker.common.StopType
.
- class PortfolioBar(date: datetime64, cash: Decimal, equity: Decimal, margin: Decimal, market_value: Decimal, pnl: Decimal, unrealized_pnl: Decimal, fees: Decimal)[source]
Bases:
NamedTuple
Snapshot of
Portfolio
state, captured per bar.- date
Date of bar.
- Type:
- fees
Brokerage fees.
- Type:
- class Position(symbol: str, shares: ~decimal.Decimal, type: ~typing.Literal['long', 'short'], close: ~decimal.Decimal = <factory>, equity: ~decimal.Decimal = <factory>, market_value: ~decimal.Decimal = <factory>, margin: ~decimal.Decimal = <factory>, pnl: ~decimal.Decimal = <factory>, entries: ~collections.deque[~pybroker.portfolio.Entry] = <factory>, bars: int = 0)[source]
Bases:
object
Contains information about an open position in
symbol
.Number of shares.
- Type:
- type
Type of position, either
long
orshort
.- Type:
Literal[‘long’, ‘short’]
- close
Last close price of
symbol
.- Type:
- equity
Equity in the position.
- Type:
- market_value
Market value of position.
- Type:
- margin
Amount of margin in position.
- Type:
- pnl
Unrealized profit and loss (PnL).
- Type:
- class PositionBar(symbol: str, date: datetime64, long_shares: Decimal, short_shares: Decimal, close: Decimal, equity: Decimal, market_value: Decimal, margin: Decimal, unrealized_pnl: Decimal)[source]
Bases:
NamedTuple
Snapshot of an open
Position
‘s state, captured per bar.- date
Date of bar.
- Type:
Number of shares long in
Position
.- Type:
Number of shares short in
Position
.- Type:
- close
Last close price of
symbol
.- Type:
- class Stop(id: int, symbol: str, stop_type: StopType, pos_type: Literal['long', 'short'], percent: Decimal | None, points: Decimal | None, bars: int | None, fill_price: int | float | floating | Decimal | PriceType | Callable[[str, BarData], int | float | Decimal] | None, limit_price: Decimal | None, exit_price: PriceType | None)[source]
Bases:
NamedTuple
Contains information about a stop set on
Entry
.- stop_type
-
- Type:
- percent
Percent from entry price.
- Type:
decimal.Decimal | None
- points
Cash amount from entry price.
- Type:
decimal.Decimal | None
- fill_price
Price that the stop will be filled at.
- Type:
int | float | numpy.floating | decimal.Decimal | pybroker.common.PriceType | Callable[[str, pybroker.common.BarData], int | float | decimal.Decimal] | None
- limit_price
Limit price to use for the stop.
- Type:
decimal.Decimal | None
- exit_price
Exit
pybroker.common.PriceType
to use for the stop exit. If set, the stop is checked against theexit_price
and exits at theexit_price
when triggered.- Type:
pybroker.common.PriceType | None
- class StopRecord(date: datetime64, symbol: str, stop_id: int, stop_type: str, pos_type: Literal['long', 'short'], curr_value: Decimal | None, curr_bars: int | None, percent: Decimal | None, points: Decimal | None, bars: int | None, fill_price: Decimal | None, limit_price: Decimal | None, exit_price: PriceType | None)[source]
Bases:
NamedTuple
Records per-bar data about a stop.
- date
Date of the bar.
- Type:
- curr_value
Current value of the stop.
- Type:
decimal.Decimal | None
- percent
Percent from entry price.
- Type:
decimal.Decimal | None
- points
Cash amount from entry price.
- Type:
decimal.Decimal | None
- fill_price
Price that the stop will be filled at.
- Type:
decimal.Decimal | None
- limit_price
Limit price to use for the stop.
- Type:
decimal.Decimal | None
- exit_price
Exit
pybroker.common.PriceType
to use for the stop exit. If set, the stop is checked against theexit_price
and exits at theexit_price
when triggered.- Type:
pybroker.common.PriceType | None
- class Trade(id: int, type: Literal['long', 'short'], symbol: str, entry_date: datetime64, exit_date: datetime64, entry: Decimal, exit: Decimal, shares: Decimal, pnl: Decimal, return_pct: Decimal, agg_pnl: Decimal, bars: int, pnl_per_bar: Decimal, stop: Literal['bar', 'loss', 'profit', 'trailing'] | None, mae: Decimal, mfe: Decimal)[source]
Bases:
NamedTuple
Holds information about a completed trade (entry and exit).
- type
Type of trade, either
long
orshort
.- Type:
Literal[‘long’, ‘short’]
- entry_date
Entry date.
- Type:
- exit_date
Exit date.
- Type:
- entry
Entry price.
- Type:
- exit
Exit price.
- Type:
Number of shares.
- Type:
- pnl
Profit and loss (PnL).
- Type:
- return_pct
Return measured in percentage.
- Type:
- agg_pnl
Aggregate profit and loss (PnL) of the strategy after the trade.
- Type:
- pnl_per_bar
Profit and loss (PnL) per bar held.
- Type:
- stop
Type of stop that was triggered, if any.
- Type:
Literal[‘bar’, ‘loss’, ‘profit’, ‘trailing’] | None
- mae
Maximum adverse excursion (MAE).
- Type:
- mfe
Maximum favorable excursion (MFE).
- Type:
pybroker.scope module
Contains scopes that store data and object references used to execute a
pybroker.strategy.Strategy
.
- class ColumnScope(df: DataFrame)[source]
Bases:
object
Caches and retrieves column data queried from
pandas.DataFrame
.- Parameters:
df –
pandas.DataFrame
containing the column data.
- bar_data_from_data_columns(symbol: str, end_index: int) BarData [source]
Returns a new
pybroker.common.BarData
instance containing column data of default and custom data columns registered withStaticScope
.- Parameters:
symbol – Ticker symbol to query.
end_index – Truncates column values (exclusive). If
None
, then column values are not truncated.
- fetch(symbol: str, name: str, end_index: int | None = None) ndarray[Any, dtype[_ScalarType_co]] | None [source]
Fetches a
numpy.ndarray
of column data forsymbol
.- Parameters:
symbol – Ticker symbol to query.
name – Name of column to query.
end_index – Truncates column values (exclusive). If
None
, then column values are not truncated.
- Returns:
numpy.ndarray
of column data for every bar untilend_index
(when specified).
- fetch_dict(symbol: str, names: Iterable[str], end_index: int | None = None) dict[str, ndarray[Any, dtype[_ScalarType_co]] | None] [source]
Fetches a
dict
of column data forsymbol
.- Parameters:
symbol – Ticker symbol to query.
names – Names of columns to query.
end_index – Truncates column values (exclusive). If
None
, then column values are not truncated.
- Returns:
dict
mapping column names tonumpy.ndarray
s of column values.
- class IndicatorScope(indicator_data: Mapping[IndicatorSymbol, Series], filter_dates: Sequence[datetime64])[source]
Bases:
object
Caches and retrieves
pybroker.indicator.Indicator
data.- Parameters:
indicator_data –
Mapping
ofpybroker.common.IndicatorSymbol
pairs topandas.Series
ofpybroker.indicator.Indicator
values.filter_dates – Filters
pybroker.indicator.Indicator
data onSequence
of dates.
- fetch(symbol: str, name: str, end_index: int | None = None) ndarray[Any, dtype[float64]] [source]
Fetches
pybroker.indicator.Indicator
data.- Parameters:
symbol – Ticker symbol to query.
name – Name of
pybroker.indicator.Indicator
to query.end_index – Truncates the array of
pybroker.indicator.Indicator
data returned (exclusive). IfNone
, then indicator data is not truncated.
- Returns:
numpy.ndarray
ofpybroker.indicator.Indicator
data for every bar untilend_index
(when specified).
- class ModelInputScope(col_scope: ColumnScope, ind_scope: IndicatorScope, models: Mapping[ModelSymbol, TrainedModel])[source]
Bases:
object
Caches and retrieves model input data.
- Parameters:
col_scope –
ColumnScope
.ind_scope –
IndicatorScope
.models –
Mapping
ofpybroker.common.ModelSymbol
pairs topybroker.common.TrainedModel
s.
- fetch(symbol: str, name: str, end_index: int | None = None) DataFrame [source]
Fetches model input data.
- Parameters:
symbol – Ticker symbol to query.
name – Name of
pybroker.model.ModelSource
to query input data.end_index – Truncates the array of model input data returned (exclusive). If
None
, then model input data is not truncated.
- Returns:
numpy.ndarray
of model input data for every bar untilend_index
(when specified).
- class PendingOrder(id: int, type: Literal['buy', 'sell'], symbol: str, created: datetime64, exec_date: datetime64, shares: Decimal, limit_price: Decimal | None, fill_price: int | float | floating | Decimal | PriceType | Callable[[str, BarData], int | float | Decimal])[source]
Bases:
NamedTuple
Holds data for a pending order.
- type
Type of order, either
buy
orsell
.- Type:
Literal[‘buy’, ‘sell’]
- created
Date the order was created.
- Type:
- exec_date
Date the order will be executed.
- Type:
Number of shares to be bought or sold.
- Type:
- limit_price
Limit price to use for the order.
- Type:
decimal.Decimal | None
- fill_price
Price that the order will be filled at.
- Type:
int | float | numpy.floating | decimal.Decimal | pybroker.common.PriceType | Callable[[str, pybroker.common.BarData], int | float | decimal.Decimal]
- class PendingOrderScope[source]
Bases:
object
Stores
PendingOrder
s- add(type: Literal['buy', 'sell'], symbol: str, created: datetime64, exec_date: datetime64, shares: Decimal, limit_price: Decimal | None, fill_price: int | float | floating | Decimal | PriceType | Callable[[str, BarData], int | float | Decimal]) int [source]
Creates a
PendingOrder
.- Parameters:
type – Type of order, either
buy
orsell
.symbol – Ticker symbol of the order.
created – Date the order was created.
exec_date – Date the order will be executed.
shares – Number of shares to be bought or sold.
limit_price – Limit price to use for the order.
fill_price – Price that the order will be filled at.
- Returns:
ID of the
PendingOrder
.
- contains(order_id: int) bool [source]
Returns whether a
PendingOrder
exists withorder_id
.
- orders(symbol: str | None = None) Iterable[PendingOrder] [source]
Returns an
Iterable
ofPendingOrder
s.
- remove(order_id: int) bool [source]
Removes a
PendingOrder
withorder_id`
.
- remove_all(symbol: str | None = None)[source]
Removes all
PendingOrder
s.
- class PredictionScope(models: Mapping[ModelSymbol, TrainedModel], input_scope: ModelInputScope)[source]
Bases:
object
Caches and retrieves model predictions.
- Parameters:
models –
Mapping
ofpybroker.common.ModelSymbol
pairs topybroker.common.TrainedModel
s.input_scope –
ModelInputScope
.
- fetch(symbol: str, name: str, end_index: int | None = None) ndarray[Any, dtype[_ScalarType_co]] [source]
Fetches model predictions.
- Parameters:
symbol – Ticker symbol to query.
name – Name of
pybroker.model.ModelSource
that made the predictions.end_index – Truncates the array of predictions returned (exclusive). If
None
, then predictions are not truncated.
- Returns:
numpy.ndarray
of model predictions for every bar untilend_index
(when specified).
- class PriceScope(col_scope: ColumnScope, sym_end_index: Mapping[str, int], round_fill_price: bool)[source]
Bases:
object
Retrieves most recent prices.
- class StaticScope[source]
Bases:
object
A static registry of data and object references.
- logger
- data_source_cache
diskcache.Cache
that stores data retrieved frompybroker.data.DataSource
.
- data_source_cache_ns
Namespace set for
data_source_cache
.
- indicator_cache
diskcache.Cache
that storespybroker.indicator.Indicator
data.
- indicator_cache_ns
Namespace set for
indicator_cache
.
- model_cache
diskcache.Cache
that stores trained models.
- model_cache_ns
Namespace set for
model_cache
.
- default_data_cols
Default data columns in
pandas.DataFrame
retrieved from apybroker.data.DataSource
.
- custom_data_cols
User-defined data columns in
pandas.DataFrame
retrieved from apybroker.data.DataSource
.
- get_indicator(name: str)[source]
Retrieves a
pybroker.indicator.Indicator
from static scope.
- get_indicator_names(model_name: str) tuple[str] [source]
Returns a
tuple[str]
of allpybroker.indicator.Indicator
names that are registered withpybroker.model.ModelSource
havingmodel_name
.
- get_model_source(name: str)[source]
Retrieves a
pybroker.model.ModelSource
from static scope.
- has_indicator(name: str) bool [source]
Whether
pybroker.indicator.Indicator
is stored in static scope.
- has_model_source(name: str) bool [source]
Whether
pybroker.model.ModelSource
is stored in static scope.
- classmethod instance() StaticScope [source]
Returns singleton instance.
- param(name: str, value: ~typing.Any | None = <object object>) Any | None [source]
Get or set a global parameter.
- register_custom_cols(names: str | Iterable[str], *args)[source]
Registers user-defined column names.
- set_indicator(indicator)[source]
Stores
pybroker.indicator.Indicator
in static scope.
- set_model_source(source)[source]
Stores
pybroker.model.ModelSource
in static scope.
- unfreeze_data_cols()[source]
Allows additional data columns to be registered if
pybroker.scope.StaticScope.freeze_data_cols()
was called.
- get_signals(symbols: Iterable[str], col_scope: ColumnScope, ind_scope: IndicatorScope, pred_scope: PredictionScope) dict[str, DataFrame] [source]
Retrieves dictionary of
pandas.DataFrame
s containing bar data, indicator data, and model predictions for each symbol.
- param(name: str, value: ~typing.Any | None = <object object>) Any | None [source]
Get or set a global parameter.
pybroker.slippage module
Implements slippage models.
- class RandomSlippageModel(min_pct: float, max_pct: float)[source]
Bases:
SlippageModel
Implements a simple random slippage model.
- Parameters:
min_pct – Min percentage of slippage.
max_pct – Max percentage of slippage.
pybroker.strategy module
Contains implementation for backtesting trading strategies.
- class BacktestMixin[source]
Bases:
object
Mixin implementing backtesting functionality.
- backtest_executions(config: StrategyConfig, executions: set[Execution], before_exec_fn: Callable[[Mapping[str, ExecContext]], None] | None, after_exec_fn: Callable[[Mapping[str, ExecContext]], None] | None, sessions: Mapping[str, MutableMapping], models: Mapping[ModelSymbol, TrainedModel], indicator_data: Mapping[IndicatorSymbol, Series], test_data: DataFrame, portfolio: Portfolio, pos_size_handler: Callable[[PosSizeContext], None] | None, exit_dates: Mapping[str, datetime64], train_only: bool = False, slippage_model: SlippageModel | None = None, enable_fractional_shares: bool = False, round_fill_price: bool = True, warmup: int | None = None) dict[str, DataFrame] [source]
Backtests a
set
ofExecution
s that implement trading logic.- Parameters:
config –
pybroker.config.StrategyConfig
.executions –
Execution
s to run.sessions –
Mapping
of symbols toMapping
of custom data that persists for every bar during theExecution
.models –
Mapping
ofpybroker.common.ModelSymbol
pairs topybroker.common.TrainedModel
s.indicator_data –
Mapping
ofpybroker.common.IndicatorSymbol
pairs topandas.Series
ofpybroker.indicator.Indicator
values.test_data –
pandas.DataFrame
of test data.portfolio –
pybroker.portfolio.Portfolio
.pos_size_handler –
Callable
that sets position sizes when placing orders for buy and sell signals.exit_dates –
Mapping
of symbols to exit dates.train_only – Whether the backtest is run with trading rules or only trains models.
enable_fractional_shares – Whether to enable trading fractional shares.
round_fill_price – Whether to round fill prices to the nearest cent.
warmup – Number of bars that need to pass before running the executions.
- Returns:
Dictionary of
pandas.DataFrame
s containing bar data, indicator data, and model predictions for each symbol whenpybroker.config.StrategyConfig.return_signals
isTrue
.
- class Execution(id: int, symbols: frozenset[str], fn: Callable[[ExecContext], None] | None, model_names: frozenset[str], indicator_names: frozenset[str])[source]
Bases:
NamedTuple
Represents an execution of a
Strategy
. Holds a reference to aCallable
that implements trading logic.- fn
Implements trading logic.
- Type:
Callable[[pybroker.context.ExecContext], None] | None
- model_names
Names of
pybroker.model.ModelSource
s used for execution offn
.
- indicator_names
Names of
pybroker.indicator.Indicator
s used for execution offn
.
- class Strategy(data_source: DataSource | DataFrame, start_date: str | datetime, end_date: str | datetime, config: StrategyConfig | None = None)[source]
Bases:
BacktestMixin
,EvaluateMixin
,IndicatorsMixin
,ModelsMixin
,WalkforwardMixin
Class representing a trading strategy to backtest.
- Parameters:
data_source –
pybroker.data.DataSource
orpandas.DataFrame
of backtesting data.start_date – Starting date of the data to fetch from
data_source
(inclusive).end_date – Ending date of the data to fetch from
data_source
(inclusive).config –
Optional
pybroker.config.StrategyConfig
.
- add_execution(fn: Callable[[ExecContext], None] | None, symbols: str | Iterable[str], models: ModelSource | Iterable[ModelSource] | None = None, indicators: Indicator | Iterable[Indicator] | None = None)[source]
Adds an execution to backtest.
- Parameters:
fn –
Callable
invoked on every bar of data during the backtest and passed anpybroker.context.ExecContext
for each ticker symbol insymbols
.symbols – Ticker symbols used to run
fn
, wherefn
is called separately for each symbol.models –
Iterable
ofpybroker.model.ModelSource
s to train/load for backtesting.indicators –
Iterable
ofpybroker.indicator.Indicator
s to compute for backtesting.
- backtest(start_date: str | datetime | None = None, end_date: str | datetime | None = None, timeframe: str = '', between_time: tuple[str, str] | None = None, days: str | Day | Iterable[str | Day] | None = None, lookahead: int = 1, train_size: int = 0, shuffle: bool = False, calc_bootstrap: bool = False, disable_parallel: bool = False, warmup: int | None = None, portfolio: Portfolio | None = None) TestResult [source]
Backtests the trading strategy by running executions that were added with
add_execution()
.- Parameters:
start_date – Starting date of the backtest (inclusive). Must be within
start_date
andend_date
range that was passed to__init__()
.end_date – Ending date of the backtest (inclusive). Must be within
start_date
andend_date
range that was passed to__init__()
.timeframe –
Formatted string that specifies the timeframe resolution of the backtesting data. The timeframe string supports the following units:
"s"
/"sec"
: seconds"m"
/"min"
: minutes"h"
/"hour"
: hours"d"
/"day"
: days"w"
/"week"
: weeks
An example timeframe string is
1h 30m
.between_time –
tuple[str, str]
of times of day e.g. (‘9:30’, ‘16:00’) used to filter the backtesting data (inclusive).days – Days (e.g.
"mon"
,"tues"
etc.) used to filter the backtesting data.lookahead – Number of bars in the future of the target prediction. For example, predicting returns for the next bar would have a
lookahead
of1
. This quantity is needed to prevent training data from leaking into the test boundary.train_size – Amount of
pybroker.data.DataSource
data to use for training, where the maxtrain_size
is1
. For example, atrain_size
of0.9
would result in 90% of data being used for training and the remaining 10% of data being used for testing.shuffle – Whether to randomly shuffle the data used for training. Defaults to
False
. Disabled when model caching is enabled viapybroker.cache.enable_model_cache()
.calc_bootstrap – Whether to compute randomized bootstrap evaluation metrics. Defaults to
False
.disable_parallel – If
True
,pybroker.indicator.Indicator
data is computed serially. IfFalse
,pybroker.indicator.Indicator
data is computed in parallel using multiple processes. Defaults toFalse
.warmup – Number of bars that need to pass before running the executions.
portfolio – Custom
pybroker.portfolio.Portfolio
to use for backtests.
- Returns:
TestResult
containing portfolio balances, order history, and evaluation metrics.
- clear_executions()[source]
Clears executions that were added with
add_execution()
.
- set_after_exec(fn: Callable[[Mapping[str, ExecContext]], None] | None)[source]
Callable[[Mapping[str, ExecContext]]
that runs after all execution functions.- Parameters:
fn –
Callable
that takes aMapping
of all ticker symbols toExecContext
s.
- set_before_exec(fn: Callable[[Mapping[str, ExecContext]], None] | None)[source]
Callable[[Mapping[str, ExecContext]]
that runs before all execution functions.- Parameters:
fn –
Callable
that takes aMapping
of all ticker symbols toExecContext
s.
- set_pos_size_handler(fn: Callable[[PosSizeContext], None] | None)[source]
Sets a
Callable
that determines position sizes to use for buy and sell signals.- Parameters:
fn –
Callable
invoked before placing orders for buy and sell signals, and is passed apybroker.context.PosSizeContext
.
- set_slippage_model(slippage_model: SlippageModel | None)[source]
- walkforward(windows: int, lookahead: int = 1, start_date: str | datetime | None = None, end_date: str | datetime | None = None, timeframe: str = '', between_time: tuple[str, str] | None = None, days: str | Day | Iterable[str | Day] | None = None, train_size: float = 0.5, shuffle: bool = False, calc_bootstrap: bool = False, disable_parallel: bool = False, warmup: int | None = None, portfolio: Portfolio | None = None) TestResult [source]
Backtests the trading strategy using Walkforward Analysis. Backtesting data supplied by the
pybroker.data.DataSource
is divided intowindows
number of equal sized time windows, with each window split into train and test data as specified bytrain_size
. The backtest “walks forward” in time through each window, running executions that were added withadd_execution()
.- Parameters:
windows – Number of walkforward time windows.
start_date – Starting date of the Walkforward Analysis (inclusive). Must be within
start_date
andend_date
range that was passed to__init__()
.end_date – Ending date of the Walkforward Analysis (inclusive). Must be within
start_date
andend_date
range that was passed to__init__()
.timeframe –
Formatted string that specifies the timeframe resolution of the backtesting data. The timeframe string supports the following units:
"s"
/"sec"
: seconds"m"
/"min"
: minutes"h"
/"hour"
: hours"d"
/"day"
: days"w"
/"week"
: weeks
An example timeframe string is
1h 30m
.between_time –
tuple[str, str]
of times of day e.g. (‘9:30’, ‘16:00’) used to filter the backtesting data (inclusive).days – Days (e.g.
"mon"
,"tues"
etc.) used to filter the backtesting data.lookahead – Number of bars in the future of the target prediction. For example, predicting returns for the next bar would have a
lookahead
of1
. This quantity is needed to prevent training data from leaking into the test boundary.train_size – Amount of
pybroker.data.DataSource
data to use for training, where the maxtrain_size
is1
. For example, atrain_size
of0.9
would result in 90% of data being used for training and the remaining 10% of data being used for testing.shuffle – Whether to randomly shuffle the data used for training. Defaults to
False
. Disabled when model caching is enabled viapybroker.cache.enable_model_cache()
.calc_bootstrap – Whether to compute randomized bootstrap evaluation metrics. Defaults to
False
.disable_parallel – If
True
,pybroker.indicator.Indicator
data is computed serially. IfFalse
,pybroker.indicator.Indicator
data is computed in parallel using multiple processes. Defaults toFalse
.warmup – Number of bars that need to pass before running the executions.
portfolio – Custom
pybroker.portfolio.Portfolio
to use for backtests.
- Returns:
TestResult
containing portfolio balances, order history, and evaluation metrics.
- class TestResult(start_date: datetime, end_date: datetime, portfolio: DataFrame, positions: DataFrame, orders: DataFrame, trades: DataFrame, metrics: EvalMetrics, metrics_df: DataFrame, bootstrap: BootstrapResult | None, signals: dict[str, DataFrame] | None, stops: DataFrame | None)[source]
Bases:
object
Contains the results of backtesting a
Strategy
.- start_date
Starting date of backtest.
- Type:
- end_date
Ending date of backtest.
- Type:
- portfolio
pandas.DataFrame
ofpybroker.portfolio.Portfolio
balances for every bar.
- positions
pandas.DataFrame
ofpybroker.portfolio.Position
balances for every bar.
- orders
pandas.DataFrame
of all orders that were placed.
- trades
pandas.DataFrame
of all trades that were made.
- metrics
Evaluation metrics.
- metrics_df
pandas.DataFrame
of evaluation metrics.
- bootstrap
Randomized bootstrap evaluation metrics.
- Type:
- signals
Dictionary of
pandas.DataFrame
s containing bar data, indicator data, and model predictions for each symbol whenpybroker.config.StrategyConfig.return_signals
isTrue
.- Type:
dict[str, pandas.core.frame.DataFrame] | None
- stops
pandas.DataFrame
containing stop data per-bar whenpybroker.config.StrategyConfig.return_stops
isTrue
.- Type:
pandas.core.frame.DataFrame | None
- class WalkforwardMixin[source]
Bases:
object
Mixin implementing logic for Walkforward Analysis.
- walkforward_split(df: DataFrame, windows: int, lookahead: int, train_size: float = 0.9, shuffle: bool = False) Iterator[WalkforwardWindow] [source]
Splits a
pandas.DataFrame
containing data for multiple ticker symbols into anIterator
of train/test time windows for Walkforward Analysis.- Parameters:
df –
pandas.DataFrame
of data to split into train/test windows for Walkforward Analysis.windows – Number of walkforward time windows.
lookahead – Number of bars in the future of the target prediction. For example, predicting returns for the next bar would have a
lookahead
of1
. This quantity is needed to prevent training data from leaking into the test boundary.train_size – Amount of data in
df
to use for training, where the maxtrain_size
is1
. For example, atrain_size
of0.9
would result in 90% of data indf
being used for training and the remaining 10% of data being used for testing.shuffle – Whether to randomly shuffle the data used for training. Defaults to
False
.
- Returns:
Iterator
ofWalkforwardWindow
s containing train and test data.
- class WalkforwardWindow(train_data: ndarray[Any, dtype[int64]], test_data: ndarray[Any, dtype[int64]])[source]
Bases:
NamedTuple
Contains
train_data
andtest_data
of a time window used for Walkforward Analysis.- train_data
Train data.
- Type:
numpy.ndarray[Any, numpy.dtype[numpy.int64]]
- test_data
Test data.
- Type:
numpy.ndarray[Any, numpy.dtype[numpy.int64]]
pybroker.vect module
Contains vectorized utility functions.
- cross(a: ndarray[Any, dtype[float64]], b: ndarray[Any, dtype[float64]]) ndarray[Any, dtype[bool_]] [source]
Checks for crossover of
a
aboveb
.- Parameters:
a –
numpy.ndarray
of data.b –
numpy.ndarray
of data.
- Returns:
numpy.ndarray
containing values of1
whena
crosses aboveb
, otherwise values of0
.
- highv(array: ndarray[Any, dtype[float64]], n: int) ndarray[Any, dtype[float64]] [source]
Calculates the highest values for every
n
period inarray
.- Parameters:
array –
numpy.ndarray
of data.n – Length of period.
- Returns:
numpy.ndarray
of the highest values for everyn
period inarray
.
- lowv(array: ndarray[Any, dtype[float64]], n: int) ndarray[Any, dtype[float64]] [source]
Calculates the lowest values for every
n
period inarray
.- Parameters:
array –
numpy.ndarray
of data.n – Length of period.
- Returns:
numpy.ndarray
of the lowest values for everyn
period inarray
.
Recommended Reading
The following is a list of essential books that provide background information on quantitative finance and algorithmic trading:
Lingjie Ma, Quantitative Investing: From Theory to Industry
Timothy Masters, Testing and Tuning Market Trading Systems: Algorithms in C++
Stefan Jansen, Machine Learning for Algorithmic Trading, 2nd Edition
Ernest P. Chan, Machine Trading: Deploying Computer Algorithms to Conquer the Markets
Perry J. Kaufman, Trading Systems and Methods, 6th Edition
Changelog
1.1.0
Adds support for the following stop types:
Stop loss
Trailing stop loss
Take profit
Upgrades
alpaca-trade-api-python
toalpaca-py
package.
1.0.0
Initial release!
License
“Commons Clause” License Condition v1.0
The Software is provided to you by the Licensor under the License, as defined below, subject to the following condition.
Without limiting other conditions in the License, the grant of rights under the License will not include, and the License does not grant to you, the right to Sell the Software.
For purposes of the foregoing, “Sell” means practicing any or all of the rights granted to you under the License to provide to third parties, for a fee or other consideration (including without limitation fees for hosting or consulting/ support services related to the Software), a product or service whose value derives, entirely or substantially, from the functionality of the Software. Any license notice or attribution required by the License must also include this Commons Clause License Condition notice.
Software: PyBroker
License: Apache 2.0 with Commons Clause
Licensor: Edward West
Apache License
Version 2.0, January 2004
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
Definitions.
“License” shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
“Licensor” shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
“Legal Entity” shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, “control” means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
“You” (or “Your”) shall mean an individual or Legal Entity exercising permissions granted by this License.
“Source” form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
“Object” form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
“Work” shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
“Derivative Works” shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
“Contribution” shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, “submitted” means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as “Not a Contribution.”
“Contributor” shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
You must give any other recipients of the Work or Derivative Works a copy of this License; and
You must cause any modified files to carry prominent notices stating that You changed the files; and
You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
If the Work includes a “NOTICE” text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.
You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an “AS IS” BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets “[]” replaced with your own identifying information. (Don’t include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same “printed page” as the copyright notice for easier identification within third-party archives.
Copyright 2023 Edward West
Licensed under the Apache License, Version 2.0 (the “License”); you may not use this file except in compliance with the License. You may obtain a copy of the License at
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an “AS IS” BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
Contact
