From 2c59fe7a9103fd9d5b585cf2d221e0a77a885373 Mon Sep 17 00:00:00 2001 From: Juan Pablo Amoroso Date: Fri, 7 Jun 2019 11:42:00 -0300 Subject: [PATCH] First working version of backtester. See notebook demo in /backtester/demos --- backtester/__init__.py | 2 +- backtester/__main__.py | 15 - backtester/backtester.py | 146 +++-- backtester/datahandler/schema.py | 4 +- backtester/demos/backtester_demo.ipynb | 739 ++++++++++++++++++++++ backtester/portfolio/__init__.py | 4 - backtester/portfolio/balancedportfolio.py | 17 - backtester/portfolio/kellyportfolio.py | 16 - backtester/portfolio/portfolio.py | 88 --- backtester/portfolio/simpleportfolio.py | 13 - backtester/strategy/strategy.py | 24 +- 11 files changed, 866 insertions(+), 202 deletions(-) delete mode 100644 backtester/__main__.py create mode 100644 backtester/demos/backtester_demo.ipynb delete mode 100644 backtester/portfolio/__init__.py delete mode 100644 backtester/portfolio/balancedportfolio.py delete mode 100644 backtester/portfolio/kellyportfolio.py delete mode 100644 backtester/portfolio/portfolio.py delete mode 100644 backtester/portfolio/simpleportfolio.py diff --git a/backtester/__init__.py b/backtester/__init__.py index e29223e..0bd50a9 100644 --- a/backtester/__init__.py +++ b/backtester/__init__.py @@ -1 +1 @@ -from .backtester import * +from .backtester import Backtest diff --git a/backtester/__main__.py b/backtester/__main__.py deleted file mode 100644 index d674076..0000000 --- a/backtester/__main__.py +++ /dev/null @@ -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) diff --git a/backtester/backtester.py b/backtester/backtester.py index 1efdea9..2c960c1 100644 --- a/backtester/backtester.py +++ b/backtester/backtester.py @@ -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) diff --git a/backtester/datahandler/schema.py b/backtester/datahandler/schema.py index 5c60964..14df704 100644 --- a/backtester/datahandler/schema.py +++ b/backtester/datahandler/schema.py @@ -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", diff --git a/backtester/demos/backtester_demo.ipynb b/backtester/demos/backtester_demo.ipynb new file mode 100644 index 0000000..ee4bcab --- /dev/null +++ b/backtester/demos/backtester_demo.ipynb @@ -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": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
datecontractorderqtyprofitcapital
01990-01-19SPX900217C00375000STO110.01000010.0
11990-01-19SPX900217P00225000STO110.01000020.0
21990-01-22SPX900217C00365000STO110.01000030.0
31990-01-22SPX900217P00225000STO110.01000040.0
41990-01-23SPX900217C00365000STO110.01000050.0
51990-01-23SPX900217P00225000STO110.01000060.0
61990-01-24SPX900217C00365000STO110.01000070.0
71990-01-24SPX900217P00225000STO110.01000080.0
81990-01-25SPX900217C00360000STO110.01000090.0
91990-01-25SPX900217P00225000STO110.01000100.0
101990-01-26SPX900217C00360000STO110.01000110.0
111990-01-26SPX900217P00225000STO110.01000120.0
121990-02-20SPX900317C00365000STO110.01000130.0
131990-02-20SPX900317P00275000STO150.01000180.0
141990-08-23SPX900922C00340000STO180.01000260.0
151990-08-23SPX900922P00275000STO1320.01000580.0
161990-08-24SPX900922C00345000STO120.01000600.0
171990-08-24SPX900922P00275000STO1220.01000820.0
181990-08-27SPX900922C00355000STO120.01000840.0
191990-08-27SPX900922P00275000STO1110.01000950.0
201990-08-28SPX900922C00355000STO110.01000960.0
211990-08-28SPX900922P00275000STO190.01001050.0
221990-08-29SPX900922C00360000STO110.01001060.0
231990-08-29SPX900922P00290000STO1140.01001200.0
241990-08-31SPX900922C00355000STO110.01001210.0
251990-08-31SPX900922P00290000STO1160.01001370.0
261990-09-20SPX901020C00345000STO120.01001390.0
271990-09-20SPX901020P00250000STO160.01001450.0
281990-09-21SPX901020C00345000STO140.01001490.0
291990-09-21SPX901020P00250000STO180.01001570.0
301990-09-24SPX901020C00340000STO130.01001600.0
311990-09-24SPX901020P00250000STO180.01001680.0
321990-09-25SPX901020C00340000STO110.01001690.0
331990-09-25SPX901020P00275000STO1120.01001810.0
341990-09-26SPX901020C00340000STO110.01001820.0
351990-09-26SPX901020P00250000STO160.01001880.0
361990-09-27SPX901020C00335000STO120.01001900.0
371990-09-27SPX901020P00250000STO160.01001960.0
381990-09-28SPX901020C00340000STO110.01001970.0
391990-09-28SPX901020P00275000STO1120.01002090.0
401990-10-18SPX901117C00340000STO110.01002100.0
411990-10-18SPX901117P00275000STO1210.01002310.0
421990-10-19SPX901117C00345000STO110.01002320.0
431990-10-19SPX901117P00280000STO1190.01002510.0
441990-10-23SPX901117C00345000STO120.01002530.0
451990-10-23SPX901117P00280000STO1110.01002640.0
461990-10-24SPX901117C00345000STO110.01002650.0
471990-10-24SPX901117P00280000STO1110.01002760.0
481990-10-25SPX901117C00345000STO110.01002770.0
491990-10-25SPX901117P00275000STO1110.01002880.0
501990-10-26SPX901117C00340000STO110.01002890.0
511990-10-26SPX901117P00250000STO140.01002930.0
\n", + "
" + ], + "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 +} diff --git a/backtester/portfolio/__init__.py b/backtester/portfolio/__init__.py deleted file mode 100644 index ba4bcad..0000000 --- a/backtester/portfolio/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from .portfolio import Portfolio -from .kellyportfolio import KellyPortfolio -from .simpleportfolio import SimplePortfolio -from .balancedportfolio import BalancedPortfolio diff --git a/backtester/portfolio/balancedportfolio.py b/backtester/portfolio/balancedportfolio.py deleted file mode 100644 index cc8b4dc..0000000 --- a/backtester/portfolio/balancedportfolio.py +++ /dev/null @@ -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 diff --git a/backtester/portfolio/kellyportfolio.py b/backtester/portfolio/kellyportfolio.py deleted file mode 100644 index 41b6c35..0000000 --- a/backtester/portfolio/kellyportfolio.py +++ /dev/null @@ -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) diff --git a/backtester/portfolio/portfolio.py b/backtester/portfolio/portfolio.py deleted file mode 100644 index ecc6ffb..0000000 --- a/backtester/portfolio/portfolio.py +++ /dev/null @@ -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 diff --git a/backtester/portfolio/simpleportfolio.py b/backtester/portfolio/simpleportfolio.py deleted file mode 100644 index 8007391..0000000 --- a/backtester/portfolio/simpleportfolio.py +++ /dev/null @@ -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) diff --git a/backtester/strategy/strategy.py b/backtester/strategy/strategy.py index 49418dc..19b3a38 100644 --- a/backtester/strategy/strategy.py +++ b/backtester/strategy/strategy.py @@ -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