Merge pull request #80 from lambdaclass/readme-example

Add README toy example that can be run
This commit is contained in:
Juan Pablo Amoroso
2020-10-05 13:43:45 -03:00
committed by GitHub
3 changed files with 439 additions and 27 deletions
+439 -27
View File
@@ -52,51 +52,463 @@ $> 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.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')
```
```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
<div>
<style scoped>
.dataframe tbody tr th:only-of-type {
vertical-align: middle;
}
.dataframe tbody tr th {
vertical-align: top;
}
.dataframe thead tr th {
text-align: left;
}
</style>
<table border="1" class="dataframe">
<thead>
<tr>
<th></th>
<th colspan="7" halign="left">leg_1</th>
<th colspan="7" halign="left">leg_2</th>
<th colspan="3" halign="left">totals</th>
</tr>
<tr>
<th></th>
<th>contract</th>
<th>underlying</th>
<th>expiration</th>
<th>type</th>
<th>strike</th>
<th>cost</th>
<th>order</th>
<th>contract</th>
<th>underlying</th>
<th>expiration</th>
<th>type</th>
<th>strike</th>
<th>cost</th>
<th>order</th>
<th>cost</th>
<th>qty</th>
<th>date</th>
</tr>
</thead>
<tbody>
<tr>
<th>0</th>
<td>SPX170317C00300000</td>
<td>SPX</td>
<td>2017-03-17</td>
<td>call</td>
<td>300</td>
<td>195010.0</td>
<td>Order.BTO</td>
<td>SPX170317P00300000</td>
<td>SPX</td>
<td>2017-03-17</td>
<td>put</td>
<td>300</td>
<td>5.0</td>
<td>Order.BTO</td>
<td>195015.0</td>
<td>2.0</td>
<td>2017-01-03</td>
</tr>
<tr>
<th>1</th>
<td>SPX170317C00300000</td>
<td>SPX</td>
<td>2017-03-17</td>
<td>call</td>
<td>300</td>
<td>-197060.0</td>
<td>Order.STC</td>
<td>SPX170317P00300000</td>
<td>SPX</td>
<td>2017-03-17</td>
<td>put</td>
<td>300</td>
<td>-0.0</td>
<td>Order.STC</td>
<td>-197060.0</td>
<td>2.0</td>
<td>2017-02-01</td>
</tr>
<tr>
<th>2</th>
<td>SPX170421C00500000</td>
<td>SPX</td>
<td>2017-04-21</td>
<td>call</td>
<td>500</td>
<td>177260.0</td>
<td>Order.BTO</td>
<td>SPX170421P01375000</td>
<td>SPX</td>
<td>2017-04-21</td>
<td>put</td>
<td>1375</td>
<td>60.0</td>
<td>Order.BTO</td>
<td>177320.0</td>
<td>2.0</td>
<td>2017-02-01</td>
</tr>
<tr>
<th>3</th>
<td>SPX170421C00500000</td>
<td>SPX</td>
<td>2017-04-21</td>
<td>call</td>
<td>500</td>
<td>-188980.0</td>
<td>Order.STC</td>
<td>SPX170421P01375000</td>
<td>SPX</td>
<td>2017-04-21</td>
<td>put</td>
<td>1375</td>
<td>-5.0</td>
<td>Order.STC</td>
<td>-188985.0</td>
<td>2.0</td>
<td>2017-03-01</td>
</tr>
<tr>
<th>4</th>
<td>SPX170519C01000000</td>
<td>SPX</td>
<td>2017-05-19</td>
<td>call</td>
<td>1000</td>
<td>138940.0</td>
<td>Order.BTO</td>
<td>SPX170519P01650000</td>
<td>SPX</td>
<td>2017-05-19</td>
<td>put</td>
<td>1650</td>
<td>100.0</td>
<td>Order.BTO</td>
<td>139040.0</td>
<td>3.0</td>
<td>2017-03-01</td>
</tr>
<tr>
<th>5</th>
<td>SPX170519C01000000</td>
<td>SPX</td>
<td>2017-05-19</td>
<td>call</td>
<td>1000</td>
<td>-135290.0</td>
<td>Order.STC</td>
<td>SPX170519P01650000</td>
<td>SPX</td>
<td>2017-05-19</td>
<td>put</td>
<td>1650</td>
<td>-20.0</td>
<td>Order.STC</td>
<td>-135310.0</td>
<td>3.0</td>
<td>2017-04-03</td>
</tr>
</tbody>
</table>
</div>
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()
```
<div>
<style scoped>
.dataframe tbody tr th:only-of-type {
vertical-align: middle;
}
.dataframe tbody tr th {
vertical-align: top;
}
.dataframe thead th {
text-align: right;
}
</style>
<table border="1" class="dataframe">
<thead>
<tr style="text-align: right;">
<th></th>
<th>total capital</th>
<th>cash</th>
<th>VOO</th>
<th>TUR</th>
<th>RSX</th>
<th>options qty</th>
<th>calls capital</th>
<th>puts capital</th>
<th>stocks qty</th>
<th>VOO qty</th>
<th>TUR qty</th>
<th>RSX qty</th>
<th>options capital</th>
<th>stocks capital</th>
<th>% change</th>
<th>accumulated return</th>
</tr>
</thead>
<tbody>
<tr>
<th>2017-01-02</th>
<td>1.000000e+06</td>
<td>1000000.00000</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>0.0</td>
<td>0.000000</td>
<td>NaN</td>
<td>NaN</td>
</tr>
<tr>
<th>2017-01-03</th>
<td>9.990300e+05</td>
<td>110117.40592</td>
<td>199872.763320</td>
<td>49993.281167</td>
<td>249986.549593</td>
<td>2.0</td>
<td>389060.0</td>
<td>0.0</td>
<td>16186.0</td>
<td>1025.0</td>
<td>1758.0</td>
<td>13403.0</td>
<td>389060.0</td>
<td>499852.594080</td>
<td>-0.000970</td>
<td>0.999030</td>
</tr>
<tr>
<th>2017-01-04</th>
<td>1.004228e+06</td>
<td>110117.40592</td>
<td>201052.238851</td>
<td>50072.862958</td>
<td>251605.333911</td>
<td>2.0</td>
<td>391380.0</td>
<td>0.0</td>
<td>16186.0</td>
<td>1025.0</td>
<td>1758.0</td>
<td>13403.0</td>
<td>391380.0</td>
<td>502730.435720</td>
<td>0.005203</td>
<td>1.004228</td>
</tr>
<tr>
<th>2017-01-05</th>
<td>1.002706e+06</td>
<td>110117.40592</td>
<td>200897.553535</td>
<td>49865.950301</td>
<td>250564.686850</td>
<td>2.0</td>
<td>391260.0</td>
<td>0.0</td>
<td>16186.0</td>
<td>1025.0</td>
<td>1758.0</td>
<td>13403.0</td>
<td>391260.0</td>
<td>501328.190686</td>
<td>-0.001516</td>
<td>1.002706</td>
</tr>
<tr>
<th>2017-01-06</th>
<td>1.003201e+06</td>
<td>110117.40592</td>
<td>201680.647945</td>
<td>49372.543196</td>
<td>248830.275081</td>
<td>2.0</td>
<td>393200.0</td>
<td>0.0</td>
<td>16186.0</td>
<td>1025.0</td>
<td>1758.0</td>
<td>13403.0</td>
<td>393200.0</td>
<td>499883.466222</td>
<td>0.000494</td>
<td>1.003201</td>
</tr>
</tbody>
</table>
</div>
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.
Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB