First working version of backtester. See notebook demo in /backtester/demos

This commit is contained in:
Juan Pablo Amoroso
2019-06-07 11:42:00 -03:00
parent 8edc657bc3
commit 2c59fe7a91
11 changed files with 866 additions and 202 deletions
+1 -1
View File
@@ -1 +1 @@
from .backtester import *
from .backtester import Backtest
-15
View File
@@ -1,15 +0,0 @@
import argparse
import os
import logging
from .backtester import run
from .utils import get_data_dir
parser = argparse.ArgumentParser(prog="backtester.py")
parser.add_argument(
"-t", "--symbols", nargs="+", help="Symbols to fetch", required=True)
parser.add_argument("-s", "--scraper", choices=["cboe"])
args = parser.parse_args()
data_dir = get_data_dir()
spx_data = os.path.join(data_dir, "SPX_2008-2018.csv")
run(spx_data)
+108 -38
View File
@@ -1,45 +1,115 @@
"""Event based backtester"""
import pandas as pd
from queue import Queue
from .datahandler import BalancedDataHandler
from .strategy import Balanced
from .portfolio import BalancedPortfolio
from .strategy import Strategy
from .strategy.signal import Order
from .datahandler import HistoricalOptionsData
def run(data_path,
data_handler=BalancedDataHandler,
port_class=BalancedPortfolio,
strat_class=Balanced,
**strat_args):
events = Queue()
bars = data_handler(data_path, events)
class Backtest:
"""Processes signals from the Strategy object"""
weights = {
"VOO": 0.3,
"GLD": 0.1,
"VNQ": 0.05,
"VNQI": 0.05,
"TLT": 0.2,
"TIP": 0.1,
"BNDX": 0.1,
"RJI": 0.1
}
port = port_class(bars, events, weights=weights)
strat = strat_class(bars, events, **strat_args)
def __init__(self, capital=1_000_000, shares_per_contract=100):
self.capital = capital
self.shares_per_contract = shares_per_contract
self._strategy = None
self._data = None
self._inventory = set()
while True:
bars.update_bars()
if not bars.continue_backtest:
break
@property
def strategy(self):
return self._strategy
while True:
if events.empty():
break
event = events.get()
if event.type == "MARKET":
strat.generate_signals(event)
port.update_timeindex(event)
elif event.type == "SIGNAL":
port.update_signal(event)
@strategy.setter
def strategy(self, strat):
assert isinstance(strat, Strategy)
self._strategy = strat
return self
return port
@property
def data(self):
return self._data
@data.setter
def data(self, data):
assert isinstance(data, HistoricalOptionsData)
self._data = data
return self
def run(self):
"""Runs the backtest and returns a `pd.DataFrame` of the orders executed."""
assert self._data is not None
assert self._strategy is not None
self.trade_log = pd.DataFrame(
columns=["date", "contract", "order", "qty", "profit", "capital"])
for date, entry_signals, exit_signals in self._strategy.signals(
self._data):
self._execute_exit(date, exit_signals)
entry_orders = self.process_entry_signals(entry_signals)
self._execute_entry(date, entry_orders, entry_signals)
return self.trade_log
def process_entry_signals(self, entry_signals):
"""Returns the a dictionary containing the orders to execute."""
# TODO: Move this logic to Strategy.
# Pass `qty` of contracts to buy/sell to `Backtest.__init__`
orders = {}
if not entry_signals.empty:
for leg in entry_signals.legs:
leg_signals = entry_signals[leg]
# Filter out zero priced options
leg_signals = leg_signals.query("price > 0.0")
if leg_signals.empty:
return {}
if (leg_signals["order"] == Order.BTO.name).any():
orders[leg] = (leg_signals["price"].idxmin(), 1)
else:
orders[leg] = (leg_signals["price"].idxmax(), 1)
return orders
def _execute_entry(self, date, orders, entry_signals):
"""Executes entry orders and updates `self.inventory` and `self.trade_log`"""
for leg, (idx, qty) in orders.items():
row = entry_signals[leg].iloc[idx, :]
contract = row["contract"]
order = row["order"]
price = row["price"]
expiration = row["expiration"]
cost = price * qty * self.shares_per_contract
cost *= -1 if order == Order.STO.name else 1
if self.capital >= cost:
self.capital -= cost
self._inventory.add((contract, leg, qty, expiration))
self._update_trade_log(date, contract, order, qty, -cost)
def _execute_exit(self, date, exit_signals):
"""Executes exits and updates `self.inventory` and `self.trade_log`"""
remove_set = set()
for contract, leg, qty, expiration in self._inventory:
if contract in exit_signals[leg]["contract"]:
row = exit_signals[leg].query("contract == @contract")
price = row["price"].values[0]
order = row["order"].values[0]
profit = price * qty * self.shares_per_contract
profit *= 1 if order == Order.STC.name else -1
self.capital += profit
self._update_trade_log(date, contract, order, qty, profit)
remove_set.add((contract, leg, qty, expiration))
elif expiration <= date:
remove_set.add((contract, leg, qty, expiration))
self._inventory.difference_update(remove_set)
def _update_trade_log(self, date, contract, order, qty, profit):
"""Adds entry for the given order to `self.trade_log`."""
self.trade_log.loc[len(self.trade_log)] = [
date, contract, order, qty, profit, self.capital
]
def __repr__(self):
return "Backtest(capital={}, strategy={})".format(
self._strategy, self.capital)
+3 -1
View File
@@ -1,5 +1,7 @@
class Schema:
"""Data schema class (used to run validations)"""
"""Data schema class.
Used to run validations and provide uniform access to fields in the data set.
"""
columns = [
"underlying", "underlying_last", "date", "contract", "type",
+739
View File
@@ -0,0 +1,739 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [],
"source": [
"from backtester.datahandler import HistoricalOptionsData\n",
"from backtester.strategy import Strategy, StrategyLeg\n",
"from backtester.option import Type, Direction\n",
"from backtester import Backtest"
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [],
"source": [
"data = HistoricalOptionsData(\"data/options_data_v2.h5\",\n",
" key=\"/SPX\",\n",
" where=\"quotedate < 1991\")\n",
"schema = data.schema"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"Schema([Field(name='underlying', mapping='underlying'), Field(name='underlying_last', mapping='underlying_last'), Field(name='date', mapping='quotedate'), Field(name='contract', mapping='optionroot'), Field(name='type', mapping='type'), Field(name='expiration', mapping='expiration'), Field(name='strike', mapping='strike'), Field(name='bid', mapping='bid'), Field(name='ask', mapping='ask'), Field(name='volume', mapping='volume'), Field(name='open_interest', mapping='openinterest'), Field(name='last', mapping='last'), Field(name='impliedvol', mapping='impliedvol'), Field(name='delta', mapping='delta'), Field(name='gamma', mapping='gamma'), Field(name='theta', mapping='theta'), Field(name='vega', mapping='vega'), Field(name='dte', mapping='dte')])"
]
},
"execution_count": 3,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"schema"
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"Strategy(legs=[])"
]
},
"execution_count": 4,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"strat = Strategy(schema)\n",
"strat"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"We'll implement a simple [short straddle](https://www.optionseducation.org/strategies/all-strategies/short-straddle), selling calls and puts 10% otm between 30 and 20 days prior to expiration, and covering the position 2 days before expiration."
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"Strategy(legs=[StrategyLeg(type=Type.CALL, direction=Direction.SELL, entry_filter=Filter(query='(type == 'call') & ((((underlying == 'SPX') & (strike >= 1.1 * underlying_last)) & (dte >= 20)) & (dte <= 30))'), exit_filter=Filter(query='(type == 'call') & ((underlying == 'SPX') & (dte <= 2))')), StrategyLeg(type=Type.PUT, direction=Direction.SELL, entry_filter=Filter(query='(type == 'put') & ((((underlying == 'SPX') & (strike <= underlying_last * 0.9)) & (dte >= 20)) & (dte <= 30))'), exit_filter=Filter(query='(type == 'put') & ((underlying == 'SPX') & (dte <= 2))'))])"
]
},
"execution_count": 5,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"leg1 = StrategyLeg(schema, option_type=Type.CALL, direction=Direction.SELL)\n",
"leg1.entry_filter = (schema.underlying == \"SPX\") & (\n",
" schema.strike >= 1.1 * schema.underlying_last) & (schema.dte >=\n",
" 20) & (schema.dte <= 30)\n",
"leg1.exit_filter = (schema.underlying == \"SPX\") & (schema.dte <= 2)\n",
"\n",
"leg2 = StrategyLeg(schema, option_type=Type.PUT, direction=Direction.SELL)\n",
"leg2.entry_filter = (schema.underlying == \"SPX\") & (\n",
" schema.strike <= schema.underlying_last * 0.9) & (schema.dte >=\n",
" 20) & (schema.dte <= 30)\n",
"leg2.exit_filter = (schema.underlying == \"SPX\") & (schema.dte <= 2)\n",
"strat.add_leg(leg1)\n",
"strat.add_leg(leg2)"
]
},
{
"cell_type": "code",
"execution_count": 6,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"Backtest(capital=Strategy(legs=[StrategyLeg(type=Type.CALL, direction=Direction.SELL, entry_filter=Filter(query='(type == 'call') & ((((underlying == 'SPX') & (strike >= 1.1 * underlying_last)) & (dte >= 20)) & (dte <= 30))'), exit_filter=Filter(query='(type == 'call') & ((underlying == 'SPX') & (dte <= 2))')), StrategyLeg(type=Type.PUT, direction=Direction.SELL, entry_filter=Filter(query='(type == 'put') & ((((underlying == 'SPX') & (strike <= underlying_last * 0.9)) & (dte >= 20)) & (dte <= 30))'), exit_filter=Filter(query='(type == 'put') & ((underlying == 'SPX') & (dte <= 2))'))]), strategy=1000000)"
]
},
"execution_count": 6,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"bt = Backtest()\n",
"bt.strategy = strat\n",
"bt.data = data\n",
"bt"
]
},
{
"cell_type": "code",
"execution_count": 7,
"metadata": {},
"outputs": [
{
"name": "stderr",
"output_type": "stream",
"text": [
"/Users/jamoroso/work/backtester_options/backtester/strategy/strategy.py:59: UserWarning: Pandas doesn't allow columns to be created via a new attribute name - see https://pandas.pydata.org/pandas-docs/stable/indexing.html#attribute-access\n",
" entry_df.legs = exit_df.legs = exit_df.columns.levels[0]\n"
]
},
{
"data": {
"text/html": [
"<div>\n",
"<style scoped>\n",
" .dataframe tbody tr th:only-of-type {\n",
" vertical-align: middle;\n",
" }\n",
"\n",
" .dataframe tbody tr th {\n",
" vertical-align: top;\n",
" }\n",
"\n",
" .dataframe thead th {\n",
" text-align: right;\n",
" }\n",
"</style>\n",
"<table border=\"1\" class=\"dataframe\">\n",
" <thead>\n",
" <tr style=\"text-align: right;\">\n",
" <th></th>\n",
" <th>date</th>\n",
" <th>contract</th>\n",
" <th>order</th>\n",
" <th>qty</th>\n",
" <th>profit</th>\n",
" <th>capital</th>\n",
" </tr>\n",
" </thead>\n",
" <tbody>\n",
" <tr>\n",
" <th>0</th>\n",
" <td>1990-01-19</td>\n",
" <td>SPX900217C00375000</td>\n",
" <td>STO</td>\n",
" <td>1</td>\n",
" <td>10.0</td>\n",
" <td>1000010.0</td>\n",
" </tr>\n",
" <tr>\n",
" <th>1</th>\n",
" <td>1990-01-19</td>\n",
" <td>SPX900217P00225000</td>\n",
" <td>STO</td>\n",
" <td>1</td>\n",
" <td>10.0</td>\n",
" <td>1000020.0</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2</th>\n",
" <td>1990-01-22</td>\n",
" <td>SPX900217C00365000</td>\n",
" <td>STO</td>\n",
" <td>1</td>\n",
" <td>10.0</td>\n",
" <td>1000030.0</td>\n",
" </tr>\n",
" <tr>\n",
" <th>3</th>\n",
" <td>1990-01-22</td>\n",
" <td>SPX900217P00225000</td>\n",
" <td>STO</td>\n",
" <td>1</td>\n",
" <td>10.0</td>\n",
" <td>1000040.0</td>\n",
" </tr>\n",
" <tr>\n",
" <th>4</th>\n",
" <td>1990-01-23</td>\n",
" <td>SPX900217C00365000</td>\n",
" <td>STO</td>\n",
" <td>1</td>\n",
" <td>10.0</td>\n",
" <td>1000050.0</td>\n",
" </tr>\n",
" <tr>\n",
" <th>5</th>\n",
" <td>1990-01-23</td>\n",
" <td>SPX900217P00225000</td>\n",
" <td>STO</td>\n",
" <td>1</td>\n",
" <td>10.0</td>\n",
" <td>1000060.0</td>\n",
" </tr>\n",
" <tr>\n",
" <th>6</th>\n",
" <td>1990-01-24</td>\n",
" <td>SPX900217C00365000</td>\n",
" <td>STO</td>\n",
" <td>1</td>\n",
" <td>10.0</td>\n",
" <td>1000070.0</td>\n",
" </tr>\n",
" <tr>\n",
" <th>7</th>\n",
" <td>1990-01-24</td>\n",
" <td>SPX900217P00225000</td>\n",
" <td>STO</td>\n",
" <td>1</td>\n",
" <td>10.0</td>\n",
" <td>1000080.0</td>\n",
" </tr>\n",
" <tr>\n",
" <th>8</th>\n",
" <td>1990-01-25</td>\n",
" <td>SPX900217C00360000</td>\n",
" <td>STO</td>\n",
" <td>1</td>\n",
" <td>10.0</td>\n",
" <td>1000090.0</td>\n",
" </tr>\n",
" <tr>\n",
" <th>9</th>\n",
" <td>1990-01-25</td>\n",
" <td>SPX900217P00225000</td>\n",
" <td>STO</td>\n",
" <td>1</td>\n",
" <td>10.0</td>\n",
" <td>1000100.0</td>\n",
" </tr>\n",
" <tr>\n",
" <th>10</th>\n",
" <td>1990-01-26</td>\n",
" <td>SPX900217C00360000</td>\n",
" <td>STO</td>\n",
" <td>1</td>\n",
" <td>10.0</td>\n",
" <td>1000110.0</td>\n",
" </tr>\n",
" <tr>\n",
" <th>11</th>\n",
" <td>1990-01-26</td>\n",
" <td>SPX900217P00225000</td>\n",
" <td>STO</td>\n",
" <td>1</td>\n",
" <td>10.0</td>\n",
" <td>1000120.0</td>\n",
" </tr>\n",
" <tr>\n",
" <th>12</th>\n",
" <td>1990-02-20</td>\n",
" <td>SPX900317C00365000</td>\n",
" <td>STO</td>\n",
" <td>1</td>\n",
" <td>10.0</td>\n",
" <td>1000130.0</td>\n",
" </tr>\n",
" <tr>\n",
" <th>13</th>\n",
" <td>1990-02-20</td>\n",
" <td>SPX900317P00275000</td>\n",
" <td>STO</td>\n",
" <td>1</td>\n",
" <td>50.0</td>\n",
" <td>1000180.0</td>\n",
" </tr>\n",
" <tr>\n",
" <th>14</th>\n",
" <td>1990-08-23</td>\n",
" <td>SPX900922C00340000</td>\n",
" <td>STO</td>\n",
" <td>1</td>\n",
" <td>80.0</td>\n",
" <td>1000260.0</td>\n",
" </tr>\n",
" <tr>\n",
" <th>15</th>\n",
" <td>1990-08-23</td>\n",
" <td>SPX900922P00275000</td>\n",
" <td>STO</td>\n",
" <td>1</td>\n",
" <td>320.0</td>\n",
" <td>1000580.0</td>\n",
" </tr>\n",
" <tr>\n",
" <th>16</th>\n",
" <td>1990-08-24</td>\n",
" <td>SPX900922C00345000</td>\n",
" <td>STO</td>\n",
" <td>1</td>\n",
" <td>20.0</td>\n",
" <td>1000600.0</td>\n",
" </tr>\n",
" <tr>\n",
" <th>17</th>\n",
" <td>1990-08-24</td>\n",
" <td>SPX900922P00275000</td>\n",
" <td>STO</td>\n",
" <td>1</td>\n",
" <td>220.0</td>\n",
" <td>1000820.0</td>\n",
" </tr>\n",
" <tr>\n",
" <th>18</th>\n",
" <td>1990-08-27</td>\n",
" <td>SPX900922C00355000</td>\n",
" <td>STO</td>\n",
" <td>1</td>\n",
" <td>20.0</td>\n",
" <td>1000840.0</td>\n",
" </tr>\n",
" <tr>\n",
" <th>19</th>\n",
" <td>1990-08-27</td>\n",
" <td>SPX900922P00275000</td>\n",
" <td>STO</td>\n",
" <td>1</td>\n",
" <td>110.0</td>\n",
" <td>1000950.0</td>\n",
" </tr>\n",
" <tr>\n",
" <th>20</th>\n",
" <td>1990-08-28</td>\n",
" <td>SPX900922C00355000</td>\n",
" <td>STO</td>\n",
" <td>1</td>\n",
" <td>10.0</td>\n",
" <td>1000960.0</td>\n",
" </tr>\n",
" <tr>\n",
" <th>21</th>\n",
" <td>1990-08-28</td>\n",
" <td>SPX900922P00275000</td>\n",
" <td>STO</td>\n",
" <td>1</td>\n",
" <td>90.0</td>\n",
" <td>1001050.0</td>\n",
" </tr>\n",
" <tr>\n",
" <th>22</th>\n",
" <td>1990-08-29</td>\n",
" <td>SPX900922C00360000</td>\n",
" <td>STO</td>\n",
" <td>1</td>\n",
" <td>10.0</td>\n",
" <td>1001060.0</td>\n",
" </tr>\n",
" <tr>\n",
" <th>23</th>\n",
" <td>1990-08-29</td>\n",
" <td>SPX900922P00290000</td>\n",
" <td>STO</td>\n",
" <td>1</td>\n",
" <td>140.0</td>\n",
" <td>1001200.0</td>\n",
" </tr>\n",
" <tr>\n",
" <th>24</th>\n",
" <td>1990-08-31</td>\n",
" <td>SPX900922C00355000</td>\n",
" <td>STO</td>\n",
" <td>1</td>\n",
" <td>10.0</td>\n",
" <td>1001210.0</td>\n",
" </tr>\n",
" <tr>\n",
" <th>25</th>\n",
" <td>1990-08-31</td>\n",
" <td>SPX900922P00290000</td>\n",
" <td>STO</td>\n",
" <td>1</td>\n",
" <td>160.0</td>\n",
" <td>1001370.0</td>\n",
" </tr>\n",
" <tr>\n",
" <th>26</th>\n",
" <td>1990-09-20</td>\n",
" <td>SPX901020C00345000</td>\n",
" <td>STO</td>\n",
" <td>1</td>\n",
" <td>20.0</td>\n",
" <td>1001390.0</td>\n",
" </tr>\n",
" <tr>\n",
" <th>27</th>\n",
" <td>1990-09-20</td>\n",
" <td>SPX901020P00250000</td>\n",
" <td>STO</td>\n",
" <td>1</td>\n",
" <td>60.0</td>\n",
" <td>1001450.0</td>\n",
" </tr>\n",
" <tr>\n",
" <th>28</th>\n",
" <td>1990-09-21</td>\n",
" <td>SPX901020C00345000</td>\n",
" <td>STO</td>\n",
" <td>1</td>\n",
" <td>40.0</td>\n",
" <td>1001490.0</td>\n",
" </tr>\n",
" <tr>\n",
" <th>29</th>\n",
" <td>1990-09-21</td>\n",
" <td>SPX901020P00250000</td>\n",
" <td>STO</td>\n",
" <td>1</td>\n",
" <td>80.0</td>\n",
" <td>1001570.0</td>\n",
" </tr>\n",
" <tr>\n",
" <th>30</th>\n",
" <td>1990-09-24</td>\n",
" <td>SPX901020C00340000</td>\n",
" <td>STO</td>\n",
" <td>1</td>\n",
" <td>30.0</td>\n",
" <td>1001600.0</td>\n",
" </tr>\n",
" <tr>\n",
" <th>31</th>\n",
" <td>1990-09-24</td>\n",
" <td>SPX901020P00250000</td>\n",
" <td>STO</td>\n",
" <td>1</td>\n",
" <td>80.0</td>\n",
" <td>1001680.0</td>\n",
" </tr>\n",
" <tr>\n",
" <th>32</th>\n",
" <td>1990-09-25</td>\n",
" <td>SPX901020C00340000</td>\n",
" <td>STO</td>\n",
" <td>1</td>\n",
" <td>10.0</td>\n",
" <td>1001690.0</td>\n",
" </tr>\n",
" <tr>\n",
" <th>33</th>\n",
" <td>1990-09-25</td>\n",
" <td>SPX901020P00275000</td>\n",
" <td>STO</td>\n",
" <td>1</td>\n",
" <td>120.0</td>\n",
" <td>1001810.0</td>\n",
" </tr>\n",
" <tr>\n",
" <th>34</th>\n",
" <td>1990-09-26</td>\n",
" <td>SPX901020C00340000</td>\n",
" <td>STO</td>\n",
" <td>1</td>\n",
" <td>10.0</td>\n",
" <td>1001820.0</td>\n",
" </tr>\n",
" <tr>\n",
" <th>35</th>\n",
" <td>1990-09-26</td>\n",
" <td>SPX901020P00250000</td>\n",
" <td>STO</td>\n",
" <td>1</td>\n",
" <td>60.0</td>\n",
" <td>1001880.0</td>\n",
" </tr>\n",
" <tr>\n",
" <th>36</th>\n",
" <td>1990-09-27</td>\n",
" <td>SPX901020C00335000</td>\n",
" <td>STO</td>\n",
" <td>1</td>\n",
" <td>20.0</td>\n",
" <td>1001900.0</td>\n",
" </tr>\n",
" <tr>\n",
" <th>37</th>\n",
" <td>1990-09-27</td>\n",
" <td>SPX901020P00250000</td>\n",
" <td>STO</td>\n",
" <td>1</td>\n",
" <td>60.0</td>\n",
" <td>1001960.0</td>\n",
" </tr>\n",
" <tr>\n",
" <th>38</th>\n",
" <td>1990-09-28</td>\n",
" <td>SPX901020C00340000</td>\n",
" <td>STO</td>\n",
" <td>1</td>\n",
" <td>10.0</td>\n",
" <td>1001970.0</td>\n",
" </tr>\n",
" <tr>\n",
" <th>39</th>\n",
" <td>1990-09-28</td>\n",
" <td>SPX901020P00275000</td>\n",
" <td>STO</td>\n",
" <td>1</td>\n",
" <td>120.0</td>\n",
" <td>1002090.0</td>\n",
" </tr>\n",
" <tr>\n",
" <th>40</th>\n",
" <td>1990-10-18</td>\n",
" <td>SPX901117C00340000</td>\n",
" <td>STO</td>\n",
" <td>1</td>\n",
" <td>10.0</td>\n",
" <td>1002100.0</td>\n",
" </tr>\n",
" <tr>\n",
" <th>41</th>\n",
" <td>1990-10-18</td>\n",
" <td>SPX901117P00275000</td>\n",
" <td>STO</td>\n",
" <td>1</td>\n",
" <td>210.0</td>\n",
" <td>1002310.0</td>\n",
" </tr>\n",
" <tr>\n",
" <th>42</th>\n",
" <td>1990-10-19</td>\n",
" <td>SPX901117C00345000</td>\n",
" <td>STO</td>\n",
" <td>1</td>\n",
" <td>10.0</td>\n",
" <td>1002320.0</td>\n",
" </tr>\n",
" <tr>\n",
" <th>43</th>\n",
" <td>1990-10-19</td>\n",
" <td>SPX901117P00280000</td>\n",
" <td>STO</td>\n",
" <td>1</td>\n",
" <td>190.0</td>\n",
" <td>1002510.0</td>\n",
" </tr>\n",
" <tr>\n",
" <th>44</th>\n",
" <td>1990-10-23</td>\n",
" <td>SPX901117C00345000</td>\n",
" <td>STO</td>\n",
" <td>1</td>\n",
" <td>20.0</td>\n",
" <td>1002530.0</td>\n",
" </tr>\n",
" <tr>\n",
" <th>45</th>\n",
" <td>1990-10-23</td>\n",
" <td>SPX901117P00280000</td>\n",
" <td>STO</td>\n",
" <td>1</td>\n",
" <td>110.0</td>\n",
" <td>1002640.0</td>\n",
" </tr>\n",
" <tr>\n",
" <th>46</th>\n",
" <td>1990-10-24</td>\n",
" <td>SPX901117C00345000</td>\n",
" <td>STO</td>\n",
" <td>1</td>\n",
" <td>10.0</td>\n",
" <td>1002650.0</td>\n",
" </tr>\n",
" <tr>\n",
" <th>47</th>\n",
" <td>1990-10-24</td>\n",
" <td>SPX901117P00280000</td>\n",
" <td>STO</td>\n",
" <td>1</td>\n",
" <td>110.0</td>\n",
" <td>1002760.0</td>\n",
" </tr>\n",
" <tr>\n",
" <th>48</th>\n",
" <td>1990-10-25</td>\n",
" <td>SPX901117C00345000</td>\n",
" <td>STO</td>\n",
" <td>1</td>\n",
" <td>10.0</td>\n",
" <td>1002770.0</td>\n",
" </tr>\n",
" <tr>\n",
" <th>49</th>\n",
" <td>1990-10-25</td>\n",
" <td>SPX901117P00275000</td>\n",
" <td>STO</td>\n",
" <td>1</td>\n",
" <td>110.0</td>\n",
" <td>1002880.0</td>\n",
" </tr>\n",
" <tr>\n",
" <th>50</th>\n",
" <td>1990-10-26</td>\n",
" <td>SPX901117C00340000</td>\n",
" <td>STO</td>\n",
" <td>1</td>\n",
" <td>10.0</td>\n",
" <td>1002890.0</td>\n",
" </tr>\n",
" <tr>\n",
" <th>51</th>\n",
" <td>1990-10-26</td>\n",
" <td>SPX901117P00250000</td>\n",
" <td>STO</td>\n",
" <td>1</td>\n",
" <td>40.0</td>\n",
" <td>1002930.0</td>\n",
" </tr>\n",
" </tbody>\n",
"</table>\n",
"</div>"
],
"text/plain": [
" date contract order qty profit capital\n",
"0 1990-01-19 SPX900217C00375000 STO 1 10.0 1000010.0\n",
"1 1990-01-19 SPX900217P00225000 STO 1 10.0 1000020.0\n",
"2 1990-01-22 SPX900217C00365000 STO 1 10.0 1000030.0\n",
"3 1990-01-22 SPX900217P00225000 STO 1 10.0 1000040.0\n",
"4 1990-01-23 SPX900217C00365000 STO 1 10.0 1000050.0\n",
"5 1990-01-23 SPX900217P00225000 STO 1 10.0 1000060.0\n",
"6 1990-01-24 SPX900217C00365000 STO 1 10.0 1000070.0\n",
"7 1990-01-24 SPX900217P00225000 STO 1 10.0 1000080.0\n",
"8 1990-01-25 SPX900217C00360000 STO 1 10.0 1000090.0\n",
"9 1990-01-25 SPX900217P00225000 STO 1 10.0 1000100.0\n",
"10 1990-01-26 SPX900217C00360000 STO 1 10.0 1000110.0\n",
"11 1990-01-26 SPX900217P00225000 STO 1 10.0 1000120.0\n",
"12 1990-02-20 SPX900317C00365000 STO 1 10.0 1000130.0\n",
"13 1990-02-20 SPX900317P00275000 STO 1 50.0 1000180.0\n",
"14 1990-08-23 SPX900922C00340000 STO 1 80.0 1000260.0\n",
"15 1990-08-23 SPX900922P00275000 STO 1 320.0 1000580.0\n",
"16 1990-08-24 SPX900922C00345000 STO 1 20.0 1000600.0\n",
"17 1990-08-24 SPX900922P00275000 STO 1 220.0 1000820.0\n",
"18 1990-08-27 SPX900922C00355000 STO 1 20.0 1000840.0\n",
"19 1990-08-27 SPX900922P00275000 STO 1 110.0 1000950.0\n",
"20 1990-08-28 SPX900922C00355000 STO 1 10.0 1000960.0\n",
"21 1990-08-28 SPX900922P00275000 STO 1 90.0 1001050.0\n",
"22 1990-08-29 SPX900922C00360000 STO 1 10.0 1001060.0\n",
"23 1990-08-29 SPX900922P00290000 STO 1 140.0 1001200.0\n",
"24 1990-08-31 SPX900922C00355000 STO 1 10.0 1001210.0\n",
"25 1990-08-31 SPX900922P00290000 STO 1 160.0 1001370.0\n",
"26 1990-09-20 SPX901020C00345000 STO 1 20.0 1001390.0\n",
"27 1990-09-20 SPX901020P00250000 STO 1 60.0 1001450.0\n",
"28 1990-09-21 SPX901020C00345000 STO 1 40.0 1001490.0\n",
"29 1990-09-21 SPX901020P00250000 STO 1 80.0 1001570.0\n",
"30 1990-09-24 SPX901020C00340000 STO 1 30.0 1001600.0\n",
"31 1990-09-24 SPX901020P00250000 STO 1 80.0 1001680.0\n",
"32 1990-09-25 SPX901020C00340000 STO 1 10.0 1001690.0\n",
"33 1990-09-25 SPX901020P00275000 STO 1 120.0 1001810.0\n",
"34 1990-09-26 SPX901020C00340000 STO 1 10.0 1001820.0\n",
"35 1990-09-26 SPX901020P00250000 STO 1 60.0 1001880.0\n",
"36 1990-09-27 SPX901020C00335000 STO 1 20.0 1001900.0\n",
"37 1990-09-27 SPX901020P00250000 STO 1 60.0 1001960.0\n",
"38 1990-09-28 SPX901020C00340000 STO 1 10.0 1001970.0\n",
"39 1990-09-28 SPX901020P00275000 STO 1 120.0 1002090.0\n",
"40 1990-10-18 SPX901117C00340000 STO 1 10.0 1002100.0\n",
"41 1990-10-18 SPX901117P00275000 STO 1 210.0 1002310.0\n",
"42 1990-10-19 SPX901117C00345000 STO 1 10.0 1002320.0\n",
"43 1990-10-19 SPX901117P00280000 STO 1 190.0 1002510.0\n",
"44 1990-10-23 SPX901117C00345000 STO 1 20.0 1002530.0\n",
"45 1990-10-23 SPX901117P00280000 STO 1 110.0 1002640.0\n",
"46 1990-10-24 SPX901117C00345000 STO 1 10.0 1002650.0\n",
"47 1990-10-24 SPX901117P00280000 STO 1 110.0 1002760.0\n",
"48 1990-10-25 SPX901117C00345000 STO 1 10.0 1002770.0\n",
"49 1990-10-25 SPX901117P00275000 STO 1 110.0 1002880.0\n",
"50 1990-10-26 SPX901117C00340000 STO 1 10.0 1002890.0\n",
"51 1990-10-26 SPX901117P00250000 STO 1 40.0 1002930.0"
]
},
"execution_count": 7,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"bt.run()"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.6.7"
}
},
"nbformat": 4,
"nbformat_minor": 2
}
-4
View File
@@ -1,4 +0,0 @@
from .portfolio import Portfolio
from .kellyportfolio import KellyPortfolio
from .simpleportfolio import SimplePortfolio
from .balancedportfolio import BalancedPortfolio
-17
View File
@@ -1,17 +0,0 @@
from .portfolio import Portfolio
class BalancedPortfolio(Portfolio):
"""Buys and holds a basket of securities, and allocates them
according to given weights.
"""
def __init__(self, *args, weights={}):
self.weights = weights
super().__init__(*args)
def _get_allocation(self, signal, price):
"""Allocates capital in porportion to given weight"""
weight = self.weights.get(signal.symbol, 0)
cash_proportion = self.initial_capital * weight
return cash_proportion / price
-16
View File
@@ -1,16 +0,0 @@
import math
from .portfolio import Portfolio
class KellyPortfolio(Portfolio):
"""Allocates signals using Kelly's criterion"""
def __init__(self, *args):
super().__init__(*args)
def _get_allocation(self, strength, price):
"""Calculates allocation using Kelly's criterion"""
(win_percent, win_loss_ratio) = strength
kelly = max(0, win_percent - (1 - win_percent) / win_loss_ratio)
total_allocation = self.current_position["Cash"] * kelly
return math.floor(total_allocation / price)
-88
View File
@@ -1,88 +0,0 @@
from abc import ABCMeta, abstractmethod
import pandas as pd
class Portfolio(metaclass=ABCMeta):
"""Processes signals from the Strategy object"""
@abstractmethod
def __init__(self, data_handler, events, capital=1000000):
self.data_handler = data_handler
self.events = events
self.initial_capital = capital
self.current_position = {"Cash": self.initial_capital}
self.all_positions = {}
self.current_balance = {"Cash": self.initial_capital}
self.all_balances = {}
@abstractmethod
def _get_allocation(self, strength, price):
"""Calculates symbol allocation"""
raise NotImplementedError("Portfolio must implement _get_allocation()")
def update_signal(self, signal):
"""Processes signal event and updates the current position"""
date = self.data_handler.current_date
if date not in self.all_positions:
self.all_positions[date] = self.current_position.copy()
self.current_position = self.all_positions[date]
(price, direction) = self._get_price(signal)
qty = self._get_allocation(signal, price)
(current_amount, current_open_price) = self.current_position.get(
signal.symbol, (0, 0))
new_open_price = (current_open_price * current_amount +
direction * price * qty) / (current_amount + qty)
self.current_position[signal.symbol] = (
current_amount + direction * qty, new_open_price)
self.current_position["Cash"] -= direction * price * qty
def update_timeindex(self, event):
"""Calculates new balance for the current timeindex.
Appends current position to all_positions list."""
date = self.data_handler.current_date
self.all_balances[date] = self.current_balance.copy()
self.current_balance = self.all_balances[date]
self.current_balance["Total Exposure"] = 0
for symbol, values in self.current_position.items():
if symbol == "Cash":
self.current_balance["Cash"] = values
continue
(amount, open_price) = values
current_bar = self.data_handler.get_latest_bars(symbol)
if amount < 0:
price = current_bar["ask"]
else:
price = current_bar["bid"]
market_value = amount * price
self.current_balance[symbol + " Amount"] = amount
self.current_balance[symbol + " Open"] = open_price
self.current_balance[symbol + " Exposure"] = market_value
self.current_balance["Total Exposure"] += market_value
self.all_positions[date] = self.current_position
self.all_balances[date] = self.current_balance
def _get_price(self, signal):
"""Returns price and direction for given symbol.
Ask price if signal.type == BUY, bid price if signal.type == SELL.
Also returns 1 or -1 for types BUY, SELL respectively"""
current_bar = self.data_handler.get_latest_bars(signal.symbol)
if signal.direction == "BUY":
direction = 1
price = current_bar["ask"]
else:
direction = -1
price = current_bar["bid"]
return (price, direction)
def create_report(self):
"""Creates a pandas DataFrame from all_balances."""
curve = pd.DataFrame(self.all_balances)
curve = curve.transpose()
curve["Total Portfolio"] = curve["Total Exposure"] + curve["Cash"]
curve["Interval Change"] = curve["Total Portfolio"].pct_change()
curve["% Price"] = (1.0 + curve["Interval Change"]).cumprod() - 1
return curve
-13
View File
@@ -1,13 +0,0 @@
import math
from .portfolio import Portfolio
class SimplePortfolio(Portfolio):
"""Allocates all capital to the first signal processed"""
def __init__(self, *args):
super().__init__(*args)
def _get_allocation(self, strength, price):
"""Allocates all capital to the given signal"""
return math.floor(self.current_position["Cash"] / price)
+15 -9
View File
@@ -41,9 +41,9 @@ class Strategy:
self.legs = []
return self
def run(self, data):
"""Returns a dataframe of trades executed as a result of
runnning the strategy on the data.
def signals(self, data):
"""Iterates over `data` and yields a tuple of
(date, entry_signals, exit_signals) for each time step.
"""
assert self.schema == data.schema
@@ -56,6 +56,7 @@ class Strategy:
exit_legs = self._filter_legs(group, signal=Signal.EXIT)
exit_df = pd.concat(exit_legs, axis=1)
entry_df.legs = exit_df.legs = exit_df.columns.levels[0]
yield (date, entry_df, exit_df)
@@ -63,17 +64,22 @@ class Strategy:
"""Returns a list of `pd.DataFrame`.
Each dataframe contains signals for each leg in the strategy.
"""
schema = self.schema
dfs = []
for number, leg in enumerate(self.legs, start=1):
flt = leg.entry_filter if signal == Signal.ENTRY else leg.exit_filter
df = flt(data)
price = leg.direction.value
fields = (self.schema["contract"], self.schema["type"],
self.schema["strike"], self.schema[price])
subset_df = df.loc[:, fields]
subset_df.rename(columns={self.schema[price]: "price"},
inplace=True)
fields = {
schema["contract"]: "contract",
schema["underlying"]: "underlying",
schema["expiration"]: "expiration",
schema["type"]: "type",
schema["strike"]: "strike",
schema[price]: "price"
}
subset_df = df.loc[:, fields.keys()]
subset_df.rename(columns=fields, inplace=True)
order = get_order(leg.direction, signal)
subset_df["order"] = order.name