PyBroker

python PyPI Apache 2.0 with Commons Clause Documentation Status Package status Downloads
Github stars Twitter

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.

AKShare

PyBroker also includes an AKShare DataSource for fetching Chinese stock data. AKShare, a widely-used open-source package, is tailored for obtaining financial data, with a focus on the Chinese market. This free tool provides users with access to higher quality data compared to yfinance for the Chinese market. To use it, you can import AKShare:

[10]:
from pybroker.ext.data import AKShare

akshare = AKShare()
# You can substitute 000001.SZ with 000001, and it will still work!
# and you can set start_date as "20210301" format
# You can also set adjust to 'qfq' or 'hfq' to adjust the data,
# and set timeframe to '1d', '1w' to get daily, weekly data
df = akshare.query(
    symbols=['000001.SZ', '600000.SH'],
    start_date='3/1/2021',
    end_date='3/1/2023',
    adjust="",
    timeframe="1d",
)
df
Loading bar data...
Loaded bar data: 0:00:10

[10]:
date symbol open high low close volume
0 2021-03-01 000001.SZ 21.54 21.68 21.18 21.45 1125387
1 2021-03-01 600000.SH 10.59 10.64 10.50 10.58 547461
2 2021-03-02 000001.SZ 21.62 22.15 21.26 21.65 1473425
3 2021-03-02 600000.SH 10.61 10.70 10.36 10.47 747631
4 2021-03-03 000001.SZ 21.58 23.08 21.46 23.01 1919635
... ... ... ... ... ... ... ...
969 2023-02-27 600000.SH 7.16 7.20 7.16 7.16 158006
970 2023-02-28 000001.SZ 13.75 13.85 13.61 13.78 607936
971 2023-02-28 600000.SH 7.18 7.20 7.14 7.18 174481
972 2023-03-01 000001.SZ 13.80 14.19 13.74 14.17 1223452
973 2023-03-01 600000.SH 7.17 7.27 7.17 7.26 256613

974 rows × 7 columns

NOTE: If the above causes a Native library not available error and you still want to use AKShare, then see this issue for details on how to resolve it.

In the next notebook, we’ll take a look at how to use DataSources to backtest a simple trading strategy.

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:

  1. 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.

  2. Set the limit price of the buy order to 0.01 less than the last close price.

  3. Hold the position for 3 days before liquidating it at market price.

  4. 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>]
_images/notebooks_2._Backtesting_a_Strategy_18_1.png

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.

In the next notebook, we will discuss how to incorporate ranking and position sizing in your trading strategies.

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.

Walkforward Diagram

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

For more information on the various attributes that can be used to set stops in PyBroker, you can refer to the ExecContext reference documentation.

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

See the Position class for more information.

… 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.

initial_cash

Starting cash of strategy.

Type:

float

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 a

    custom Callable that is passed pybroker.common.FeeInfo.

  • None: Fees are disabled (default).

Type:

pybroker.common.FeeMode | Callable[[pybroker.common.FeeInfo], decimal.Decimal] | None

fee_amount

Brokerage fee amount.

Type:

float

subtract_fees

Whether to subtract fees from the cash balance after an order is filled. Defaults to False.

Type:

bool

enable_fractional_shares

Whether to enable trading fractional shares. Set to True for crypto trading. Defaults to False.

Type:

bool

round_fill_price

Whether to round fill prices to the nearest cent. Defaults to True.

Type:

bool

max_long_positions

Maximum number of long positions that can be held at any time in pybroker.portfolio.Portfolio. Unlimited when None. Defaults to None.

Type:

int | None

max_short_positions

Maximum number of short positions that can be held at any time in pybroker.portfolio.Portfolio. Unlimited when None. Defaults to None.

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:

int

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:

int

bootstrap_samples

Number of samples used to compute boostrap metrics. Defaults to 10_000.

Type:

int

bootstrap_sample_size

Size of each random sample used to compute bootstrap metrics. Defaults to 1_000.

Type:

int

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:

bool

exit_cover_fill_price

Fill price for covering an open short position when exit_on_last_bar is True. Defaults to pybroker.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 is True. Defaults to pybroker.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 with pybroker.strategy.TestResult. Defaults to False.

Type:

bool

return_stops

When True, then stop values are returned with pybroker.strategy.TestResult. Defaults to False.

Type:

bool

round_test_result

When True, round values in pybroker.strategy.TestResult up to the nearest cent. Defaults to True.

Type:

bool

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:

datetime.datetime

end_date

End date of cache data.

Type:

datetime.datetime

tf_seconds

Timeframe resolution of cache data represented in seconds.

Type:

int

between_time

tuple[str, str] of times of day (e.g. 9:00-9:30 AM) that were used to filter the cache data.

Type:

tuple[str, str] | None

days

Days (e.g. "mon", "tues" etc.) that were used to filter the cache data.

Type:

tuple[int] | None

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.

adjust: str | None
symbol: str
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.

ind_name: str
symbol: str
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.

model_name: str
symbol: str
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.DataSources. 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_caches()[source]

Disables all caches.

disable_data_source_cache()[source]

Disables caching data retrieved from pybroker.data.DataSources.

disable_indicator_cache()[source]

Disables caching indicator data.

disable_model_cache()[source]

Disables caching trained models.

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.DataSources.

Parameters:
  • namespace – Namespace of the cache.

  • cache_dir – Directory used to store cached data.

Returns:

diskcache.Cache instance.

enable_indicator_cache(namespace: str, cache_dir: str | None = None) Cache[source]

Enables caching indicator data.

Parameters:
  • namespace – Namespace of the cache.

  • cache_dir – Directory used to store cached indicator data.

Returns:

diskcache.Cache instance.

enable_model_cache(namespace: str, cache_dir: str | None = None) Cache[source]

Enables caching trained models.

Parameters:
  • namespace – Namespace of the cache.

  • cache_dir – Directory used to store cached models.

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.

symbol

Trading symbol.

Type:

str

shares

Number of shares in order.

Type:

decimal.Decimal

fill_price

Fill price of order.

Type:

decimal.Decimal

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.

ind_name

Indicator name.

Type:

str

symbol

Ticker symbol.

Type:

str

class ModelSymbol(model_name: str, symbol: str)[source]

Bases: NamedTuple

pybroker.model.ModelSource/symbol identifier.

model_name

Model name.

Type:

str

symbol

Ticker symbol.

Type:

str

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.

name

Trained model name.

Type:

str

instance

Trained model instance.

Type:

Any

predict_fn

Callable that overrides calling the model’s default predict 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.

Type:

tuple[str] | None

input_cols: tuple[str] | None

Alias for field number 3

default_parallel() Parallel[source]

Returns a joblib.Parallel instance with n_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 of tuple[int, str], where each tuple contains an int value and str 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 to datetime.

to_decimal(value: int | float | Decimal) Decimal[source]

Converts value to Decimal.

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 a pybroker.data.DataSource.

verify_date_range(start_date: datetime, end_date: datetime)[source]

Verifies date range bounds.

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

pybroker.config.StrategyConfig.

calc_target_shares(target_size: float, price: float, cash: float | None = None) Decimal | int[source]

Calculates the number of shares given a target_size allocation and share price.

Parameters:
  • target_size – Proportion of cash used to calculate the number of shares, where the max target_size is 1. For example, a target_size of 0.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 the pybroker.portfolio.Portfolio equity is used to calculate the number of shares.

Returns:

Number of shares given target_size and share price. If pybroker.config.StrategyConfig.enable_fractional_shares is True, 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 to None.

Returns:

Iterator of currently held long pybroker.portfolio.Position s.

property loss_rate: Decimal

Running loss rate of trades.

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 all pybroker.portfolio.Orders that have been placed and filled.

pending_orders(symbol: str | None = None) Iterator[PendingOrder][source]
pos(symbol: str, pos_type: Literal['long', 'short']) Position | None[source]

Retrieves a current long or short pybroker.portfolio.Position for a symbol.

Parameters:
  • symbol – Ticker symbol of the position to return.

  • pos_type – Specifies whether to return a long or short position.

Returns:

pybroker.portfolio.Position if one exists, otherwise None.

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 to None.

  • pos_type – Type of positions to return. If None, both long and short positions are returned.

Returns:

Iterator of currently held pybroker.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 to None.

Returns:

Iterator of currently held short pybroker.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 all pybroker.portfolio.Trades that have been completed.

property win_rate: Decimal

Running win rate of trades.

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 and pybroker.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.

buy_shares

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.

sell_shares

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 the pybroker.portfolio.Portfolio is specified by pybroker.config.StrategyConfig.max_long_positions and pybroker.config.StrategyConfig.max_short_positions respectively. Long and short signals are ranked separately by score.

session

dict used to store custom data that persists for each bar during the pybroker.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 the exit_price and exits at the exit_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 the exit_price and exits at the exit_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 the exit_price and exits at the exit_price when triggered.

property bars: int

Number of bars of data that have completed.

calc_target_shares(target_size: float, price: float | None = None, cash: float | None = None) Decimal | int[source]

Calculates the number of shares given a target_size allocation and share price.

Parameters:
  • target_size – Proportion of cash used to calculate the number of shares, where the max target_size is 1. For example, a target_size of 0.1 would represent 10% of cash.

  • price – Share price used to calculate the number of shares. If None, the share price of the ExecContext‘s symbol is used.

  • cash – Cash used to calculate the number of number of shares. If None, then the pybroker.portfolio.Portfolio equity is used to calculate the number of shares.

Returns:

Number of shares given target_size and share price. If pybroker.config.StrategyConfig.enable_fractional_shares is True, then a Decimal is returned.

cancel_all_pending_orders(symbol: str | None = None)[source]

Cancels all pybroker.scope.PendingOrders for symbol. When symbol is None, all pending orders are canceled.

cancel_pending_order(order_id: int) bool[source]

Cancels a pybroker.scope.PendingOrder with order_id.

cancel_stop(stop_id: int) bool[source]

Cancels a pybroker.portfolio.Stop with stop_id.

cancel_stops(val: str | Position | Entry, stop_type: StopType | None = None)[source]

Cancels pybroker.portfolio.Stops.

Parameters:
property close: ndarray[Any, dtype[float64]]

Current bar’s close price.

cover_all_shares()[source]

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.

property cover_shares: int | float | Decimal | None

Alias for buy_shares. When set, this causes the buy order to be placed before any sell orders.

property dt: datetime

Current bar’s date expressed as a datetime.

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 in pybroker.common.BarData.

Returns:

If col is None, a pybroker.common.BarData instance containing data of all bars up to the current one. Otherwise, an numpy.ndarray containing values of the column col.

property high: ndarray[Any, dtype[float64]]

Current bar’s high price.

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, the ExecContext‘s symbol 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, the ExecContext‘s symbol 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 a symbol.

Parameters:

symbol – Ticker symbol of the position to return. If None, the ExecContext‘s symbol is used. Defaults to None.

Returns:

pybroker.portfolio.Position if one exists, otherwise None.

property low: ndarray[Any, dtype[float64]]

Current bar’s low price.

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, the ExecContext‘s symbol is used.

Returns:

Instance of the trained model.

property open: ndarray[Any, dtype[float64]]

Current bar’s open price.

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, the ExecContext‘s symbol is used.

Returns:

numpy.ndarray containing the sequence of model predictions up to the current bar. Sorted in ascending chronological order.

sell_all_shares()[source]

Sells all long shares of ExecContext.symbol.

short_pos(symbol: str | None = None) Position | None[source]

Retrieves a current short pybroker.portfolio.Position for a symbol.

Parameters:

symbol – Ticker symbol of the position to return. If None, the ExecContext‘s symbol is used. Defaults to None.

Returns:

pybroker.portfolio.Position if one exists, otherwise None.

to_result() ExecResult | None[source]

Creates an ExecResult from the data set on ExecContext.

property volume: ndarray[Any, dtype[float64]] | None

Current bar’s volume.

property vwap: ndarray[Any, dtype[float64]] | None

Current bar’s volume-weighted average price (VWAP).

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.

symbol

Ticker symbol that was used for the execution.

Type:

str

date

Timestamp of the bar that was used for the execution.

Type:

numpy.datetime64

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 the pybroker.portfolio.Portfolio is specified by pybroker.config.StrategyConfig.max_long_positions and pybroker.config.StrategyConfig.max_short_positions respectively. Buy and sell signals are ranked separately by score.

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

buy_shares

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

sell_shares

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.Entrys.

Type:

frozenset[pybroker.portfolio.Stop] | None

short_stops

Stops for short pybroker.portfolio.Entrys.

Type:

frozenset[pybroker.portfolio.Stop] | None

cover

Whether buy_shares are used to cover a short position. If True, the resulting buy order will be placed before sell orders.

Type:

bool

pending_order_id

ID of pybroker.scope.PendingOrder that was created.

Type:

int | None

cover: bool = False
long_stops: frozenset[Stop] | None
pending_order_id: int | None = None
short_stops: frozenset[Stop] | 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.

id

Unique ID.

Type:

int

symbol

Ticker symbol.

Type:

str

shares

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 for symbol.

Type:

pybroker.common.BarData

type

buy or sell 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.

set_shares(signal: ExecSignal, shares: int | float | Decimal)[source]

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 of ExecSignals containing data for buy and sell signals.

set_exec_ctx_data(ctx: ExecContext, date: datetime64)[source]

Sets data on an ExecContext instance.

Parameters:
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:
pybroker.data module

Contains DataSources 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')
TRADE_COUNT: Final = '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 custom DataSource that can be used with pybroker.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, and close.

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:
  • symbolsIterable 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 a pandas.DataFrame with the cached data, and an Iterable[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.

  • datapandas.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

ADJ_CLOSE: Final = 'adj_close'
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.

low_2p5

Lower bound of 97.5% confidence interval.

Type:

float

high_2p5

Upper bound of 97.5% confidence interval.

Type:

float

low_5

Lower bound of 95% confidence interval.

Type:

float

high_5

Upper bound of 95% confidence interval.

Type:

float

low_10

Lower bound of 90% confidence interval.

Type:

float

high_10

Upper bound of 90% confidence interval.

Type:

float

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 for log_profit_factor() and sharpe_ratio().

Type:

pandas.core.frame.DataFrame

drawdown_conf

pandas.DataFrame containing upper bounds of confidence intervals for maximum drawdown.

Type:

pandas.core.frame.DataFrame

profit_factor

Contains profit factor confidence intervals.

Type:

pybroker.eval.BootConfIntervals

sharpe

Contains Sharpe Ratio confidence intervals.

Type:

pybroker.eval.BootConfIntervals

drawdown

Contains drawdown confidence intervals.

Type:

pybroker.eval.DrawdownMetrics

class ConfInterval(name: str, conf: str, lower: float, upper: float)[source]

Bases: NamedTuple

Confidence interval upper and low bounds.

name

Parameter name.

Type:

str

conf

Confidence interval percentage represented as a str.

Type:

str

lower

Lower bound.

Type:

float

upper

Upper bound.

Type:

float

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.

q_001

99.9% confidence upper bound.

Type:

float

q_01

99% confidence upper bound.

Type:

float

q_05

95% confidence upper bound.

Type:

float

q_10

90% confidence upper bound.

Type:

float

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.

Type:

pybroker.eval.DrawdownConfs

pct_confs

Upper bounds of confidence intervals for maximum drawdown, measured in percentage.

Type:

pybroker.eval.DrawdownConfs

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.

trade_count

Number of trades that were filled.

Type:

int

initial_market_value

Initial market value of the pybroker.portfolio.Portfolio.

Type:

float

end_market_value

Ending market value of the pybroker.portfolio.Portfolio.

Type:

float

total_pnl

Total realized profit and loss (PnL).

Type:

float

unrealized_pnl

Total unrealized profit and loss (PnL).

Type:

float

total_return_pct

Total realized return measured in percentage.

Type:

float

annual_return_pct

Annualized total realized return measured in percentage.

Type:

float | None

total_profit

Total realized profit.

Type:

float

total_loss

Total realized loss.

Type:

float

total_fees

Total brokerage fees. See pybroker.config.StrategyConfig.fee_mode for more info.

Type:

float

max_drawdown

Maximum drawdown, measured in cash.

Type:

float

max_drawdown_pct

Maximum drawdown, measured in percentage.

Type:

float

win_rate

Win rate of trades.

Type:

float

loss_rate

Loss rate of trades.

Type:

float

winning_trades

Number of winning trades.

Type:

int

losing_trades

Number of losing trades.

Type:

int

avg_pnl

Average profit and loss (PnL) per trade, measured in cash.

Type:

float

avg_return_pct

Average return per trade, measured in percentage.

Type:

float

avg_trade_bars

Average number of bars per trade.

Type:

float

avg_profit

Average profit per trade, measured in cash.

Type:

float

avg_profit_pct

Average profit per trade, measured in percentage.

Type:

float

avg_winning_trade_bars

Average number of bars per winning trade.

Type:

float

avg_loss

Average loss per trade, measured in cash.

Type:

float

avg_loss_pct

Average loss per trade, measured in percentage.

Type:

float

avg_losing_trade_bars

Average number of bars per losing trade.

Type:

float

largest_win

Largest profit of a trade, measured in cash.

Type:

float

largest_win_pct

Largest profit of a trade, measured in percentage

Type:

float

largest_win_bars

Number of bars in the largest winning trade.

Type:

int

largest_loss

Largest loss of a trade, measured in cash.

Type:

float

largest_loss_pct

Largest loss of a trade, measured in percentage.

Type:

float

largest_loss_bars

Number of bars in the largest losing trade.

Type:

int

max_wins

Maximum number of consecutive winning trades.

Type:

int

max_losses

Maximum number of consecutive losing trades.

Type:

int

sharpe

Sharpe Ratio, computed per bar.

Type:

float

sortino

Sortino Ratio, computed per bar.

Type:

float

calmar

Calmar Ratio, computed per bar.

Type:

float | None

profit_factor

Ratio of gross profit to gross loss, computed per bar.

Type:

float

ulcer_index

Ulcer Index, computed per bar.

Type:

float

upi

Ulcer Performance Index, computed per bar.

Type:

float

equity_r2

R^2 of the equity curve, computed per bar on market values of portfolio.

Type:

float

std_error

Standard error, computed per bar on market values of portfolio.

Type:

float

annual_std_error

Annualized standard error, computed per bar on market values of portfolio.

Type:

float | None

annual_volatility_pct

Annualized volatility percentage, computed per bar on market values of portfolio.

Type:

float | None

class EvalResult(metrics: EvalMetrics, bootstrap: BootstrapResult | None)[source]

Bases: NamedTuple

Contains evaluation result.

metrics

Evaluation metrics.

Type:

pybroker.eval.EvalMetrics

bootstrap

Randomized bootstrap metrics.

Type:

pybroker.eval.BootstrapResult | None

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_dfpandas.DataFrame of portfolio market values per bar.

  • trades_dfpandas.DataFrame of trades.

  • calc_bootstrapTrue 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:
  • xnumpy.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.

  • fnCallable 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.

normal_cdf(z: float) float[source]

Computes the CDF of the standard normal distribution.

r_squared(values: ndarray[Any, dtype[float64]]) float[source]

Computes R-squared of values.

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).

win_loss_rate(pnls: ndarray[Any, dtype[float64]]) tuple[float, float][source]

Computes the win rate and loss rate as percentages.

Parameters:

pnls – Array of profits and losses (PnLs) per trade.

Returns:

tuple[float, float] of win rate and loss rate.

winning_losing_trades(pnls: ndarray[Any, dtype[float64]]) tuple[int, int][source]

Returns the number of winning and losing trades.

Parameters:

pnls – Array of profits and losses (PnLs) per trade.

Returns:

tuple[int, int] containing numbers of winning and losing trades.

pybroker.ext.data module

Contains extension classes.

class AKShare[source]

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.

  • fnCallable used to compute the series of indicator values.

  • kwargsdict of kwargs to pass to fn.

__call__(data: BarData | DataFrame) Series[source]

Computes indicator values.

iqr(data: BarData | DataFrame) float[source]

Generates indicator data with data and computes its interquartile range (IQR).

relative_entropy(data: BarData | DataFrame) float[source]

Generates indicator data with data and computes its relative entropy.

class IndicatorSet[source]

Bases: IndicatorsMixin

Computes data for multiple indicators.

__call__(df: DataFrame, disable_parallel: bool = False) DataFrame[source]

Computes indicator data.

Parameters:
  • dfpandas.DataFrame of input data.

  • disable_parallel – If True, indicator data is computed serially. If False, indicator data is computed in parallel using multiple processes. Defaults to False.

Returns:

pandas.DataFrame containing the computed indicator data.

add(indicators: Indicator | Iterable[Indicator], *args)[source]

Adds indicators.

clear()[source]

Removes all indicators.

remove(indicators: Indicator | Iterable[Indicator], *args)[source]

Removes indicators.

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:
  • dfpandas.DataFrame used to compute the indicator values.

  • indicator_symsIterable of pybroker.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 all pybroker.common.IndicatorSymbol pairs. If False, indicator data is computed in parallel using multiple processes.

Returns:

dict mapping each pybroker.common.IndicatorSymbol pair to a computed pandas.Series of indicator values.

highest(name: str, field: str, period: int) Indicator[source]

Creates a rolling high Indicator.

Parameters:
  • name – Indicator name.

  • fieldpybroker.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 with name.

Parameters:
  • name – Name for referencing the indicator globally.

  • fnCallable[[BarData, ...], NDArray[float]] used to compute the series of indicator values.

  • **kwargs – Additional arguments to pass to fn.

Returns:

Indicator instance.

lowest(name: str, field: str, period: int) Indicator[source]

Creates a rolling low Indicator.

Parameters:
  • name – Indicator name.

  • fieldpybroker.common.BarData field for computing the rolling low.

  • period – Lookback period.

Returns:

Rolling low Indicator.

returns(name: str, field: str, period: int = 1) Indicator[source]

Creates a rolling returns Indicator.

Parameters:
  • name – Indicator name.

  • fieldpybroker.common.BarData field for computing the rolling returns.

  • period – Returns period. Defaults to 1.

Returns:

Rolling returns Indicator.

pybroker.log module

Logging module.

class Logger(scope)[source]

Bases: object

Class for logging information about triggered events.

Parameters:

scopepybroker.scope.StaticScope.

backtest_executions_loading(count: int)[source]
backtest_executions_start(test_dates: Sequence[datetime64])[source]
calc_bootstrap_metrics_completed()[source]
calc_bootstrap_metrics_start(samples, sample_size)[source]
debug_buy_shares_exceed_cash(date: datetime64, symbol: str, shares: Decimal, fill_price: Decimal, limit_price: Decimal | None, cash: Decimal, clamped_shares: Decimal)[source]
debug_clear_data_source_cache(cache_dir: str)[source]
debug_clear_indicator_cache(cache_dir: str)[source]
debug_clear_model_cache(cache_dir: str)[source]
debug_compute_indicators(is_parallel: bool)[source]
debug_disable_data_source_cache()[source]
debug_disable_indicator_cache()[source]
debug_disable_model_cache()[source]
debug_enable_data_source_cache(ns: str, cache_dir: str)[source]
debug_enable_indicator_cache(ns: str, cache_dir: str)[source]
debug_enable_model_cache(ns: str, cache_dir: str)[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_get_data_source_cache(cache_key)[source]
debug_get_indicator_cache(cache_key)[source]
debug_get_model_cache(cache_key)[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_set_data_source_cache(cache_key)[source]
debug_set_indicator_cache(cache_key)[source]
debug_set_model_cache(cache_key)[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]
debug_unscheduled_order(exec_result)[source]
disable()[source]

Disables logging.

disable_progress_bar()[source]

Disables logging a progress bar.

download_bar_data_completed()[source]
download_bar_data_start()[source]
enable()[source]

Enables logging.

enable_progress_bar()[source]

Enables logging a progress bar.

indicator_data_loading(count: int)[source]
indicator_data_start(ind_syms: Sized)[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_invalidate_data_source_cache()[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]
info_walkforward_between_time(between_time: tuple[str, str])[source]
info_walkforward_on_days(days: tuple[int])[source]
loaded_bar_data()[source]
loaded_indicator_data()[source]
loaded_models()[source]
train_split_completed()[source]
train_split_start(train_dates: Sequence[datetime64])[source]
walkforward_completed()[source]
walkforward_start(start_date: datetime, end_date: datetime)[source]
warn_bootstrap_sample_size(n: int, sample_size: int)[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.

Type:

tuple[str] | None

input_cols: tuple[str] | None

Alias for field number 1

model: Any

Alias for field number 0

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_fnCallable[[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 a Iterable of column names to to be used as input for the model when making predictions.

  • indicator_namesIterable of names of pybroker.indicator.Indicators used as features of the model.

  • input_data_fnCallable[[DataFrame], DataFrame] for preprocessing input data passed to the model when making predictions. If set, input_data_fn will be called with a pandas.DataFrame containing all test data.

  • predict_fnCallable[[Model, DataFrame], ndarray] that overrides calling the model’s default predict function. If set, predict_fn will be called with the trained model and a pandas.DataFrame containing all test data.

  • kwargsdict of kwargs to pass to load_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_namesIterable of names of pybroker.indicator.Indicators used as features of the model.

  • input_data_fnCallable[[DataFrame], DataFrame] for preprocessing input data passed to the model when making predictions. If set, input_data_fn will be called with a pandas.DataFrame containing all test data.

  • predict_fnCallable[[Model, DataFrame], ndarray] that overrides calling the model’s default predict function. If set, predict_fn will be called with the trained model and a pandas.DataFrame containing all test data.

  • kwargsdict 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, the input_data_fn is used to preprocess the input data. If False, then indicator columns in df 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_fnCallable[[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 a Iterable of column names to to be used as input for the model when making predictions.

  • indicator_namesIterable of names of pybroker.indicator.Indicators used as features of the model.

  • input_data_fnCallable[[DataFrame], DataFrame] for preprocessing input data passed to the model when making predictions. If set, input_data_fn will be called with a pandas.DataFrame containing all test data.

  • predict_fnCallable[[Model, DataFrame], ndarray] that overrides calling the model’s default predict function. If set, predict_fn will be called with the trained model and a pandas.DataFrame containing all test data.

  • kwargsdict of kwargs to pass to train_fn.

__call__(symbol: str, train_data: DataFrame, test_data: DataFrame) Any | tuple[Any, Iterable[str]][source]

Trains model.

Parameters:
  • symbol – Ticker symbol of model (models are trained per symbol).

  • train_data – Train data.

  • test_data – Test data.

Returns:

Trained model.

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:
Returns:

dict mapping each pybroker.common.ModelSymbol pair to a pybroker.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 with name.

Parameters:
  • name – Name for referencing the model globally.

  • fnCallable used to either train or load a model instance. If for training, then fn has signature Callable[[symbol: str, train_data: DataFrame, test_data: DataFrame, ...], DataFrame]. If for loading, then fn has signature Callable[[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 a Iterable of column names to to be used as input for the model when making predictions.

  • indicatorsIterable of pybroker.indicator.Indicators used as features of the model.

  • input_data_fnCallable[[DataFrame], DataFrame] for preprocessing input data passed to the model when making predictions. If set, input_data_fn will be called with a pandas.DataFrame containing all test data.

  • predict_fnCallable[[Model, DataFrame], ndarray] that overrides calling the model’s default predict function. If set, predict_fn will be called with the trained model and a pandas.DataFrame containing all test data.

  • pretrained – If True, then fn is used to load and return a pre-trained model. If False, fn is used to train and return a new model. Defaults to False.

  • **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.

id

Unique identifier.

Type:

int

date

Date of the entry.

Type:

numpy.datetime64

symbol

Symbol of the entry.

Type:

str

shares

Number of shares.

Type:

decimal.Decimal

price

Share price of the entry.

Type:

decimal.Decimal

type

Type of Position, either long or short.

Type:

Literal[‘long’, ‘short’]

bars

Current number of bars since entry.

Type:

int

stops

Stops set on the entry.

Type:

list[pybroker.portfolio.Stop]

mae

Maximum adverse excursion (MAE).

Type:

decimal.Decimal

mfe

Maximum favorable excursion (MFE).

Type:

decimal.Decimal

mae: Decimal
mfe: Decimal
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.

id

Unique identifier.

Type:

int

type

Type of order, either buy or sell.

Type:

Literal[‘buy’, ‘sell’]

symbol

Ticker symbol of the order.

Type:

str

date

Date the order was filled.

Type:

numpy.datetime64

shares

Number of shares bought or sold.

Type:

decimal.Decimal

limit_price

Limit price that was used for the order.

Type:

decimal.Decimal | None

fill_price

Price that the order was filled at.

Type:

decimal.Decimal

fees

Brokerage fees for order.

Type:

decimal.Decimal

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 Positions that can be held at a time. If None, then unlimited.

  • max_short_positions – Maximum number of short Positions that can be held at a time. If None, 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.

enable_fractional_shares

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).

long_positions

dict mapping ticker symbols to open long Positions.

short_positions

dict mapping ticker symbols to open short Positions.

symbols

Ticker symbols of all currently open positions.

bars

deque of snapshots of Portfolio state on every bar, sorted in ascending chronological order.

position_bars

deque of snapshots of Position 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:
  • date – Date when the Order is placed.

  • symbol – Ticker symbol to buy.

  • shares – Number of shares to buy.

  • fill_price – If filled, the price used to fill the Order.

  • limit_price – Limit price of the Order.

  • stopsStops to set on the Entry created from the Order, if filled.

Returns:

Order if the order was filled, otherwise None.

capture_bar(date: datetime64, df: DataFrame)[source]

Captures portfolio state of the current bar.

Parameters:
  • date – Date of current bar.

  • dfpandas.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 at buy_fill_price and sell_fill_price.

incr_bars()[source]

Increments the number of bars held by every trade entry.

remove_stop(stop_id: int) bool[source]

Removes a Stop with stop_id.

remove_stops(val: str | Position | Entry, stop_type: StopType | None = None)[source]

Removes Stops.

Parameters:
sell(date: datetime64, symbol: str, shares: Decimal, fill_price: Decimal, limit_price: Decimal | None = None, stops: Iterable[Stop] | None = None) Order | None[source]

Places a sell order.

Parameters:
  • date – Date when the Order is placed.

  • symbol – Ticker symbol to sell.

  • shares – Number of shares to sell.

  • fill_price – If filled, the price used to fill the Order.

  • limit_price – Limit price of the Order.

  • stopsStops to set on the Entry created from the Order, if filled.

Returns:

Order if the order was filled, otherwise None.

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:

numpy.datetime64

cash

Amount of cash in Portfolio.

Type:

decimal.Decimal

equity

Amount of equity in Portfolio.

Type:

decimal.Decimal

margin

Amount of margin in Portfolio.

Type:

decimal.Decimal

market_value

Market value of Portfolio.

Type:

decimal.Decimal

pnl

Realized profit and loss (PnL) of Portfolio.

Type:

decimal.Decimal

unrealized_pnl

Unrealized profit and loss (PnL) of Portfolio.

Type:

decimal.Decimal

fees

Brokerage fees.

Type:

decimal.Decimal

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.

symbol

Ticker symbol of the position.

Type:

str

shares

Number of shares.

Type:

decimal.Decimal

type

Type of position, either long or short.

Type:

Literal[‘long’, ‘short’]

close

Last close price of symbol.

Type:

decimal.Decimal

equity

Equity in the position.

Type:

decimal.Decimal

market_value

Market value of position.

Type:

decimal.Decimal

margin

Amount of margin in position.

Type:

decimal.Decimal

pnl

Unrealized profit and loss (PnL).

Type:

decimal.Decimal

entries

deque of position Entrys sorted in ascending chronological order.

Type:

collections.deque[pybroker.portfolio.Entry]

bars

Current number of bars since entry.

Type:

int

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.

symbol

Ticker symbol of Position.

Type:

str

date

Date of bar.

Type:

numpy.datetime64

long_shares

Number of shares long in Position.

Type:

decimal.Decimal

short_shares

Number of shares short in Position.

Type:

decimal.Decimal

close

Last close price of symbol.

Type:

decimal.Decimal

equity

Amount of equity in Position.

Type:

decimal.Decimal

market_value

Market value of Position.

Type:

decimal.Decimal

margin

Amount of margin in Position.

Type:

decimal.Decimal

unrealized_pnl

Unrealized profit and loss (PnL) of Position.

Type:

decimal.Decimal

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.

id

Unique identifier.

Type:

int

symbol

Symbol of the stop.

Type:

str

stop_type

StopType.

Type:

pybroker.common.StopType

pos_type

Type of Position, either long or short.

Type:

Literal[‘long’, ‘short’]

percent

Percent from entry price.

Type:

decimal.Decimal | None

points

Cash amount from entry price.

Type:

decimal.Decimal | None

bars

Number of bars after which to trigger the stop.

Type:

int | 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 the exit_price and exits at the exit_price when triggered.

Type:

pybroker.common.PriceType | None

stop_type: StopType

Alias for field number 2

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:

numpy.datetime64

symbol

Symbol of the stop.

Type:

str

stop_id

Unique identifier.

Type:

int

stop_type

StopType.

Type:

str

pos_type

Type of Position, either long or short.

Type:

Literal[‘long’, ‘short’]

curr_value

Current value of the stop.

Type:

decimal.Decimal | None

curr_bars

Current bars of the stop.

Type:

int | None

percent

Percent from entry price.

Type:

decimal.Decimal | None

points

Cash amount from entry price.

Type:

decimal.Decimal | None

bars

Number of bars after which to trigger the stop.

Type:

int | 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 the exit_price and exits at the exit_price when triggered.

Type:

pybroker.common.PriceType | None

curr_value: Decimal | None

Alias for field number 5

stop_type: str

Alias for field number 3

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).

id

Unique identifier.

Type:

int

type

Type of trade, either long or short.

Type:

Literal[‘long’, ‘short’]

symbol

Ticker symbol of the trade.

Type:

str

entry_date

Entry date.

Type:

numpy.datetime64

exit_date

Exit date.

Type:

numpy.datetime64

entry

Entry price.

Type:

decimal.Decimal

exit

Exit price.

Type:

decimal.Decimal

shares

Number of shares.

Type:

decimal.Decimal

pnl

Profit and loss (PnL).

Type:

decimal.Decimal

return_pct

Return measured in percentage.

Type:

decimal.Decimal

agg_pnl

Aggregate profit and loss (PnL) of the strategy after the trade.

Type:

decimal.Decimal

bars

Number of bars the trade was held.

Type:

int

pnl_per_bar

Profit and loss (PnL) per bar held.

Type:

decimal.Decimal

stop

Type of stop that was triggered, if any.

Type:

Literal[‘bar’, ‘loss’, ‘profit’, ‘trailing’] | None

mae

Maximum adverse excursion (MAE).

Type:

decimal.Decimal

mfe

Maximum favorable excursion (MFE).

Type:

decimal.Decimal

mae: Decimal

Alias for field number 14

mfe: Decimal

Alias for field number 15

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:

dfpandas.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 with StaticScope.

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 for symbol.

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 until end_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 for symbol.

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 to numpy.ndarrays 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:
fetch(symbol: str, name: str, end_index: int | None = None) ndarray[Any, dtype[float64]][source]

Fetches pybroker.indicator.Indicator data.

Parameters:
Returns:

numpy.ndarray of pybroker.indicator.Indicator data for every bar until end_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:
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 until end_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.

id

Unique ID.

Type:

int

type

Type of order, either buy or sell.

Type:

Literal[‘buy’, ‘sell’]

symbol

Ticker symbol of the order.

Type:

str

created

Date the order was created.

Type:

numpy.datetime64

exec_date

Date the order will be executed.

Type:

numpy.datetime64

shares

Number of shares to be bought or sold.

Type:

decimal.Decimal

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 PendingOrders

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 or sell.

  • 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 with order_id.

orders(symbol: str | None = None) Iterable[PendingOrder][source]

Returns an Iterable of PendingOrders.

remove(order_id: int) bool[source]

Removes a PendingOrder with order_id`.

remove_all(symbol: str | None = None)[source]

Removes all PendingOrders.

class PredictionScope(models: Mapping[ModelSymbol, TrainedModel], input_scope: ModelInputScope)[source]

Bases: object

Caches and retrieves model predictions.

Parameters:
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 until end_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.

fetch(symbol: str, price: int | float | floating | Decimal | PriceType | Callable[[str, BarData], int | float | Decimal]) Decimal[source]
class StaticScope[source]

Bases: object

A static registry of data and object references.

logger

pybroker.log.Logger

data_source_cache

diskcache.Cache that stores data retrieved from pybroker.data.DataSource.

data_source_cache_ns

Namespace set for data_source_cache.

indicator_cache

diskcache.Cache that stores pybroker.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 a pybroker.data.DataSource.

custom_data_cols

User-defined data columns in pandas.DataFrame retrieved from a pybroker.data.DataSource.

property all_data_cols: frozenset[str]

All registered data column names.

freeze_data_cols()[source]

Prevents additional data columns from being registered.

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 all pybroker.indicator.Indicator names that are registered with pybroker.model.ModelSource having model_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.

unregister_custom_cols(names: str | Iterable[str], *args)[source]

Unregisters user-defined column names.

disable_logging()[source]

Disables event logging.

disable_progress_bar()[source]

Disables logging a progress bar.

enable_logging()[source]

Enables event logging.

enable_progress_bar()[source]

Enables logging a progress bar.

get_signals(symbols: Iterable[str], col_scope: ColumnScope, ind_scope: IndicatorScope, pred_scope: PredictionScope) dict[str, DataFrame][source]

Retrieves dictionary of pandas.DataFrames 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.

register_columns(names: str | Iterable[str], *args)[source]

Registers names of user-defined data columns.

unregister_columns(names: str | Iterable[str], *args)[source]

Unregisters names of user-defined data columns.

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.

apply_slippage(ctx: ExecContext, buy_shares: Decimal | None = None, sell_shares: Decimal | None = None)[source]

Applies slippage to ctx.

class SlippageModel[source]

Bases: ABC

Base class for implementing a slippage model.

abstract apply_slippage(ctx: ExecContext, buy_shares: Decimal | None = None, sell_shares: Decimal | None = None)[source]

Applies slippage to ctx.

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 of Executions that implement trading logic.

Parameters:
Returns:

Dictionary of pandas.DataFrames containing bar data, indicator data, and model predictions for each symbol when pybroker.config.StrategyConfig.return_signals is True.

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 a Callable that implements trading logic.

id

Unique ID.

Type:

int

symbols

Ticker symbols used for execution of fn.

Type:

frozenset[str]

fn

Implements trading logic.

Type:

Callable[[pybroker.context.ExecContext], None] | None

model_names

Names of pybroker.model.ModelSources used for execution of fn.

Type:

frozenset[str]

indicator_names

Names of pybroker.indicator.Indicators used for execution of fn.

Type:

frozenset[str]

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:
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:
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 and end_date range that was passed to __init__().

  • end_date – Ending date of the backtest (inclusive). Must be within start_date and end_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_timetuple[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 of 1. 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 max train_size is 1. For example, a train_size of 0.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 via pybroker.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. If False, pybroker.indicator.Indicator data is computed in parallel using multiple processes. Defaults to False.

  • 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:

fnCallable that takes a Mapping of all ticker symbols to ExecContexts.

set_before_exec(fn: Callable[[Mapping[str, ExecContext]], None] | None)[source]

Callable[[Mapping[str, ExecContext]] that runs before all execution functions.

Parameters:

fnCallable that takes a Mapping of all ticker symbols to ExecContexts.

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:

fnCallable invoked before placing orders for buy and sell signals, and is passed a pybroker.context.PosSizeContext.

set_slippage_model(slippage_model: SlippageModel | None)[source]

Sets pybroker.slippage.SlippageModel.

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 into windows number of equal sized time windows, with each window split into train and test data as specified by train_size. The backtest “walks forward” in time through each window, running executions that were added with add_execution().

Parameters:
  • windows – Number of walkforward time windows.

  • start_date – Starting date of the Walkforward Analysis (inclusive). Must be within start_date and end_date range that was passed to __init__().

  • end_date – Ending date of the Walkforward Analysis (inclusive). Must be within start_date and end_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_timetuple[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 of 1. 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 max train_size is 1. For example, a train_size of 0.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 via pybroker.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. If False, pybroker.indicator.Indicator data is computed in parallel using multiple processes. Defaults to False.

  • 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:

datetime.datetime

end_date

Ending date of backtest.

Type:

datetime.datetime

portfolio

pandas.DataFrame of pybroker.portfolio.Portfolio balances for every bar.

Type:

pandas.core.frame.DataFrame

positions

pandas.DataFrame of pybroker.portfolio.Position balances for every bar.

Type:

pandas.core.frame.DataFrame

orders

pandas.DataFrame of all orders that were placed.

Type:

pandas.core.frame.DataFrame

trades

pandas.DataFrame of all trades that were made.

Type:

pandas.core.frame.DataFrame

metrics

Evaluation metrics.

Type:

pybroker.eval.EvalMetrics

metrics_df

pandas.DataFrame of evaluation metrics.

Type:

pandas.core.frame.DataFrame

bootstrap

Randomized bootstrap evaluation metrics.

Type:

pybroker.eval.BootstrapResult | None

signals

Dictionary of pandas.DataFrames containing bar data, indicator data, and model predictions for each symbol when pybroker.config.StrategyConfig.return_signals is True.

Type:

dict[str, pandas.core.frame.DataFrame] | None

stops

pandas.DataFrame containing stop data per-bar when pybroker.config.StrategyConfig.return_stops is True.

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 an Iterator of train/test time windows for Walkforward Analysis.

Parameters:
  • dfpandas.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 of 1. 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 max train_size is 1. For example, a train_size of 0.9 would result in 90% of data in df 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 of WalkforwardWindows 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 and test_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 above b.

Parameters:
Returns:

numpy.ndarray containing values of 1 when a crosses above b, otherwise values of 0.

highv(array: ndarray[Any, dtype[float64]], n: int) ndarray[Any, dtype[float64]][source]

Calculates the highest values for every n period in array.

Parameters:
Returns:

numpy.ndarray of the highest values for every n period in array.

lowv(array: ndarray[Any, dtype[float64]], n: int) ndarray[Any, dtype[float64]][source]

Calculates the lowest values for every n period in array.

Parameters:
Returns:

numpy.ndarray of the lowest values for every n period in array.

returnv(array: ndarray[Any, dtype[float64]], n: int = 1) ndarray[Any, dtype[float64]][source]

Calculates returns.

Parameters:

n – Return period. Defaults to 1.

Returns:

numpy.ndarray of returns.

sumv(array: ndarray[Any, dtype[float64]], n: int) ndarray[Any, dtype[float64]][source]

Calculates the sums for every n period in array.

Parameters:
Returns:

numpy.ndarray of the sums for every n period in array.

Contact

_static/email-image.png