diff --git a/README.md b/README.md index 9cfdf2f..39dfdab 100644 --- a/README.md +++ b/README.md @@ -52,51 +52,465 @@ $> make test ## Usage -### Example: +### Sample backtest + +You can run this example by putting the code into a Jupyter Notebook/Lab file in this directory. -We'll run a backtest of a stock portfolio holding `$AAPL` and `$GOOG`, and simultaneously buying 10% OTM calls and puts on `$SPX` ([long strangle](https://www.investopedia.com/terms/s/strangle.asp)). -We'll allocate 97% of our capital to stocks and the rest to options, and do a rebalance every month. ```python -from backtester import Backtest, Type, Direction, Stock -from backtester.strategy import Strategy, StrategyLeg +import os +import sys + +BACKTESTER_DIR = os.path.realpath(os.path.join(os.getcwd(), '.', '.')) +TEST_DATA_DIR = os.path.join(BACKTESTER_DIR, 'backtester', 'test', 'test_data') +SAMPLE_STOCK_DATA = os.path.join(TEST_DATA_DIR, 'test_data_stocks.csv') +SAMPLE_OPTIONS_DATA = os.path.join(TEST_DATA_DIR, 'test_data_options.csv') + +sys.path.append(BACKTESTER_DIR) # Add backtester base dir to $PYTHONPATH +``` + + +```python +from backtester import Backtest, Stock, Type, Direction from backtester.datahandler import HistoricalOptionsData, TiingoData +from backtester.strategy import Strategy, StrategyLeg +``` -# Stocks data -stocks_data = TiingoData('stocks.csv') -stocks = [Stock(symbol='AAPL', percentage=0.5), Stock(symbol='GOOG', percentage=0.5)] +First we construct an options datahandler. -# Options data -options_data = HistoricalOptionsData('options.h5', key='/SPX') -schema = options_data.schema -# Long strangle -leg_1 = StrategyLeg('leg_1', schema, option_type=Type.PUT, direction=Direction.BUY) -leg_1.entry_filter = (schema.underlying == 'SPX') & (schema.dte >= 60) & (schema.underlying_last <= - 1.1 * schema.strike) -leg_1.exit_filter = (schema.dte <= 30) +```python +options_data = HistoricalOptionsData(SAMPLE_OPTIONS_DATA) +options_schema = options_data.schema +``` -leg_2 = StrategyLeg('leg_2', schema, option_type=Type.CALL, direction=Direction.BUY) -leg_2.entry_filter = (schema.underlying == 'SPX') & (schema.dte >= 60) & (schema.underlying_last >= - 0.9 * schema.strike) -leg_2.exit_filter = (schema.dte <= 30) +Next, we'll create a toy options strategy. It will simply buy a call and a put with `dte` between $80$ and $52$ and exit them a month later. -strategy = Strategy(schema) -strategy.add_legs([leg_1, leg_2]) -allocation = {'stocks': .97, 'options': .03} -initial_capital = 1_000_000 -bt = Backtest(allocation, initial_capital) +```python +sample_strategy = Strategy(options_schema) + +leg1 = StrategyLeg('leg_1', options_schema, option_type=Type.CALL, direction=Direction.BUY) +leg1.entry_filter = (options_schema.dte < 80) & (options_schema.dte > 52) + +leg1.exit_filter = (options_schema.dte <= 52) + +leg2 = StrategyLeg('leg_2', options_schema, option_type=Type.PUT, direction=Direction.BUY) +leg2.entry_filter = (options_schema.dte < 80) & (options_schema.dte > 52) + +leg2.exit_filter = (options_schema.dte <= 52) + +sample_strategy.add_legs([leg1, leg2]); +``` + +We do the same for stocks: create a datahandler together with a list of the stocks we want in our inventory and their corresponding weights. In this case, we will hold `VOO`, `TUR` and `RSX`, with $0.4$, $0.1$ and $0.5$ weights respectively. + + +```python +stocks_data = TiingoData(SAMPLE_STOCK_DATA) +stocks = [Stock('VOO', 0.4), Stock('TUR', 0.1), Stock('RSX', 0.5)] +``` + +We set our portfolio allocation, i.e. how much of our capital will be invested in stocks, options and cash. We'll allocate 50% of our capital to stocks and the rest to options. + + +```python +allocation = {'stocks': 0.5, 'options': 0.5, 'cash': 0.0} +``` + +Finally, we create the `Backtest` object. + + +```python +bt = Backtest(allocation, initial_capital=1_000_000) + bt.stocks = stocks bt.stocks_data = stocks_data -bt.options_data = options_data -bt.options_strategy = strategy +bt.options_strategy = sample_strategy +bt.options_data = options_data +``` + +And run the backtest with a rebalancing period of one month. + + +```python bt.run(rebalance_freq=1) +``` + + 0% [██████████████████████████████] 100% | ETA: 00:00:00 + Total time elapsed: 00:00:00 + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
leg_1leg_2totals
contractunderlyingexpirationtypestrikecostordercontractunderlyingexpirationtypestrikecostordercostqtydate
0SPX170317C00300000SPX2017-03-17call300195010.0Order.BTOSPX170317P00300000SPX2017-03-17put3005.0Order.BTO195015.02.02017-01-03
1SPX170317C00300000SPX2017-03-17call300-197060.0Order.STCSPX170317P00300000SPX2017-03-17put300-0.0Order.STC-197060.02.02017-02-01
2SPX170421C00500000SPX2017-04-21call500177260.0Order.BTOSPX170421P01375000SPX2017-04-21put137560.0Order.BTO177320.02.02017-02-01
3SPX170421C00500000SPX2017-04-21call500-188980.0Order.STCSPX170421P01375000SPX2017-04-21put1375-5.0Order.STC-188985.02.02017-03-01
4SPX170519C01000000SPX2017-05-19call1000138940.0Order.BTOSPX170519P01650000SPX2017-05-19put1650100.0Order.BTO139040.03.02017-03-01
5SPX170519C01000000SPX2017-05-19call1000-135290.0Order.STCSPX170519P01650000SPX2017-05-19put1650-20.0Order.STC-135310.03.02017-04-03
+
+ + + +The trade log (`bt.trade_log`) shows we executed 6 trades: we bought one call and one put on _2017-01-03_, _2017-02-01_ and _2017-03-01_, and exited those positions on _2017-02-01_, _2017-03-01_ and _2017-04-03_ respectively. + +The balance data structure shows how our positions evolved over time: +- We started with $1000000 on _2017-01-02_ +- `total capital` is the sum of `cash`, `stocks capital` and `options capital` +- `% change` shows the inter day change in `total capital` +- `accumulated return` gives the compounded return in `total capital` since the start of the backtest + + +```python +bt.balance.head() +``` + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
total capitalcashVOOTURRSXoptions qtycalls capitalputs capitalstocks qtyVOO qtyTUR qtyRSX qtyoptions capitalstocks capital% changeaccumulated return
2017-01-021.000000e+061000000.00000NaNNaNNaNNaNNaNNaNNaNNaNNaNNaN0.00.000000NaNNaN
2017-01-039.990300e+05110117.40592199872.76332049993.281167249986.5495932.0389060.00.016186.01025.01758.013403.0389060.0499852.594080-0.0009700.999030
2017-01-041.004228e+06110117.40592201052.23885150072.862958251605.3339112.0391380.00.016186.01025.01758.013403.0391380.0502730.4357200.0052031.004228
2017-01-051.002706e+06110117.40592200897.55353549865.950301250564.6868502.0391260.00.016186.01025.01758.013403.0391260.0501328.190686-0.0015161.002706
2017-01-061.003201e+06110117.40592201680.64794549372.543196248830.2750812.0393200.00.016186.01025.01758.013403.0393200.0499883.4662220.0004941.003201
+
+ + +Evolution of our total capital over time: + + +```python +bt.balance['total capital'].plot(); +``` + + +![png](img/total_capital.png) + + +Evolution of our stock positions over time: + + +```python +bt.balance[[stock.symbol for stock in stocks]].plot(); +``` + + +![png](img/stock_positions.png) + + +More plots and statistics are available in the `backtester.statistics` module. + +### Other strategies + +The `Strategy` and `StrategyLeg` classes allow for more complex strategies; for instance, a [long strangle](https://www.investopedia.com/terms/s/strangle.asp) could be implemented like so: + + +```python +# Long strangle +leg_1 = StrategyLeg('leg_1', options_schema, option_type=Type.PUT, direction=Direction.BUY) +leg_1.entry_filter = (options_schema.underlying == 'SPX') & (options_schema.dte >= 60) & (options_schema.underlying_last <= 1.1 * options_schema.strike) +leg_1.exit_filter = (options_schema.dte <= 30) + +leg_2 = StrategyLeg('leg_2', options_schema, option_type=Type.CALL, direction=Direction.BUY) +leg_2.entry_filter = (options_schema.underlying == 'SPX') & (options_schema.dte >= 60) & (options_schema.underlying_last >= 0.9 * options_schema.strike) +leg_2.exit_filter = (options_schema.dte <= 30) + +strategy = Strategy(options_schema) +strategy.add_legs([leg_1, leg_2]); ``` You can explore more usage examples in the Jupyter [notebooks](backtester/examples/). + ## Recommended reading For complete novices in finance and economics, this [post](https://notamonadtutorial.com/how-to-earn-your-macroeconomics-and-finance-white-belt-as-a-software-developer-136e7454866f) gives a comprehensive introduction. diff --git a/img/stock_positions.png b/img/stock_positions.png new file mode 100644 index 0000000..a65bd41 Binary files /dev/null and b/img/stock_positions.png differ diff --git a/img/total_capital.png b/img/total_capital.png new file mode 100644 index 0000000..dd70092 Binary files /dev/null and b/img/total_capital.png differ