In previous articles the concept of cointegration was considered. It was shown how cointegrated pairs of equities or ETFs could lead to profitable mean-reverting trading opportunities.
Two specific tests were outlined–the Cointegrated Augmented Dickey-Fuller (CADF) test and the Johansen test–that helped statistically identify cointegrated portfolios.
In this article QSTrader will be used to implement an actual trading strategy based on a (potentially) cointegrating relationship between an equity and an ETF in the commodities market.
The analysis will begin by forming a hypothesis about a fundamental structural relationship between the prices of Alcoa Inc., a large aluminum producer, and US natural gas. This structural relationship will be tested for cointegration via the CADF test using R. It will be shown that although the prices appear partially correlated, that the null hypothesis of no cointegrating relationship cannot be rejected.
Despite this a static hedging ratio will be calculated between the two series and a trading strategy developed, firstly to show how such a strategy might be implemented in QSTrader, irrespective of performance, and secondly to evalulate the performance on a slightly correlated, but non-cointegrating pair of assets.
This strategy was inspired by Ernie Chan's famous GLD-GDX cointegration strategy[1] and a post[2] by Quantopian CEO, John Fawcett, referencing the smelting of aluminum as a potential for cointegrated assets.
The Hypothesis
An extremely important set of processes in chemical engineering are the Bayer process and the Hall–Héroult process. They are the key steps in smelting aluminum from the raw mineral of bauxite, via the technique of electrolysis.
Electrolysis requires a substantial amount of electricity, much of which is generated by coal, hydroelectric, nuclear or combined-cycle gas turbine (CCGT) power. The latter requires natural gas as its main fuel source. Since the purchase of natural gas for aluminum smelting is likely a substantial cost for aluminum producers, their profitability is derived in part from the price of natural gas.
The hypothesis presented here is that the stock price of a large aluminum producer, such as Alcoa Inc. (ARNC) and that of an ETF representing US natural gas prices, such as UNG might well be cointegrated and thus lead to a potential mean-reverting systematic trading strategy.
Cointegration Tests in R
If you need a refresher on the topic of cointegration then please take a look at the following articles:
- Cointegrated Time Series Analysis for Mean Reversion Trading with R
- Cointegrated Augmented Dickey Fuller Test for Pairs Trading Evaluation in R
- Johansen Test for Cointegrating Time Series Analysis in R
To test the above hypothesis the Cointegrated Augmented Dickey Fuller procedure will be carried out on ARNC and UNG using R. The procedure has been outlined in depth in the previous articles and so the code will be replicated here with less explanation.
The first task is to import the R quantmod library, for data download, as well as the tseries
library, for the ADF test. The daily bar data of ARNC and UNG is downloaded for the period November 11th 2014 to January 1st 2017. The data is then set to the adjusted close values (handling splits/dividends):
library("quantmod")
library("tseries")
## Obtain ARNC and UNG
getSymbols("ARNC", from="2014-11-11", to="2017-01-01")
getSymbols("UNG", from="2014-11-11", to="2017-01-01")
## Utilise the backwards-adjusted closing prices
aAdj = unclass(ARNC$ARNC.Adjusted)
bAdj = unclass(UNG$UNG.Adjusted)
The following displays a plot of the prices of ARNC (blue) and UNG (red) over the period:
## Plot the ETF backward-adjusted closing prices
plot(aAdj, type="l", xlim=c(0, length(aAdj)), ylim=c(0.0, 45.0), xlab="November 11th 2014 to January 1st 2017", ylab="Backward-Adjusted Prices in USD", col="blue")
par(new=T)
plot(bAdj, type="l", xlim=c(0, length(bAdj)), ylim=c(0.0, 45.0), axes=F, xlab="", ylab="", col="red")
par(new=F)
It can be seen that the prices of ARNC and UNG follow a broadly similar pattern, which trends downwards for 2015 and then stays flat for 2016. Displaying a scatterplot will provide a clearer picture of any potential correlation:
## Plot a scatter graph of the ETF adjusted prices
plot(aAdj, bAdj, xlab="ARNC Backward-Adjusted Prices", ylab="UNG Backward-Adjusted Prices")
The scatterplot is more ambiguous. There is a slight partial positive correlation, as would be expected for a company that is heavily exposed to natural gas prices, but whether this is sufficient to allow a structural relationship is less clear.
By performing a linear regression between the two, a slope coefficient/hedging ratio is obtained:
## Carry out linear regression on the two price series
comb = lm(aAdj~bAdj)
> comb
Call:
lm(formula = aAdj ~ bAdj)
Coefficients:
(Intercept) bAdj
11.039 1.213
In the linear regression where UNG is the independent variable the slope is given by 1.213. The final task is to carry out the ADF test and determine whether there is any structural cointegrating relationship:
## Now we perform the ADF test on the residuals,
## or "spread" of the model, using a single lag order
> adf.test(comb$residuals, k=1)
Augmented Dickey-Fuller Test
data: comb$residuals
Dickey-Fuller = -2.5413, Lag order = 1, p-value = 0.3492
alternative hypothesis: stationary
This analysis shows that there is not sufficient evidence to reject the null hypothesis of no cointegrating relationship. However, despite this it is instructive to continue implementing the strategy with the hedging ratio calculated above, for two reasons:
- Firstly, any other potential cointegration-based analysis, as derived from the CADF or the Johansen test, can be backtested using the following code, as it has been written to be flexible enough to cope with large cointegrated portfolios.
- Secondly, it is valuable to see how a strategy performs when there is insufficient evidence to reject the null hypothesis. Perhaps the pair is still tradeable even though a relationship has not been detected on this small dataset.
The trading strategy mechanism will now be outlined.
The Trading Strategy
In order to actually generate tradeable signals from a mean-reverting "spread" of prices from a linear combination of ARNC and UNG, a technique known as Bollinger Bands will be utilised.
Bollinger Bands involve taking a rolling simple moving average of a price series and then forming "bands" surrounding the series that are a scalar multiple of the rolling standard deviation of the price series. The lookback period for the moving average and standard deviation is identical. In essence they are an estimate of current volatility of a price series.
By definition, a mean-reverting series will occasionally deviate from its mean and then eventually revert. Bollinger Bands provide a mechanism for entering and exiting trades by employing standard deviation "thresholds" at which trades can be entered into and exited from.
To generate trades the first task is to calculate a z-score/standard score of the current latest spread price. This is achieved by taking the latest portfolio market price, subtracting the rolling mean and dividing by the rolling standard deviation (as described above).
Once this z-score is calculated a position will be opened or closed out under the following conditions:
- $z_{\text{score}} \lt -z_{\text{entry}}$ - Long entry
- $z_{\text{score}} \gt +z_{\text{entry}}$ - Short entry
- $z_{\text{score}} \ge -z_{\text{exit}}$ - Long close
- $z_{\text{score}} \le +z_{\text{exit}}$ - Short close
Where $z_{\text{score}}$ is the latest standardised spread price, $z_{\text{entry}}$ is the trade entry threshold and $z_{\text{exit}}$ is the trade exit threshold.
A longd position here means purchasing one share of ARNC and shorting 1.213 shares of UNG. Clearly it is impossible to trade a fractional number of shares! Hence such a fraction is rounded to the nearest integer when multiplied by a large unit base quantity (such as 10,000 "units" of the portfolio traded).
Thus profitable trades are likely to occur assuming that the above conditions are regularly met, which a cointegrating pair with high volatility should provide.
For this particular strategy the lookback period used for the rolling moving average and the rolling standard deviation is equal to 15 bars. $z_{\text{entry}}=1.5$, while $z_{\text{exit}}=0.5$. All of these parameters are arbitrarily picked for this article, but a full research project would optimise these via some form of parameter grid-search.
Data
In order to carry out this strategy it is necessary to have daily OHLCV pricing data for the equities and ETFs in the period covered by this backtest:
Ticker | Name | Period | Link |
---|---|---|---|
ARNC | Arconic Inc. (prev Alcoa Inc.) | 11th November 2014 - 1st September 2016 | Yahoo Finance |
UNG | United States Natural Gas ETF | 11th November 2014 - 1st September 2016 | Yahoo Finance |
This data will need to placed in the directory specified by the QSTrader settings file if you wish to replicate the results.
Python QSTrader Implementation
Note that the full listings of each of these Python files can be found at the end of the article.
Note also that this strategy contains an implicit lookahead bias and so its performance will be grossly exaggerated compared to a real implementation. The lookahead bias occurs due to the use of calculating the hedging ratio across the same sample of data as the trading strategy is simulated on. In a real implementation two separate sets of data will be needed in order to verify that any structural relationship persists out-of-sample.
The implementation of the strategy is similar to other QSTrader strategies. It involves the creation of a subclass of AbstractStrategy
in the coint_bollinger_strategy.py
file. This class is then used by the coint_bollinger_backtest.py
file to actually simulate the backtest.
coint_bollinger_strategy.py
will be described first. NumPy is imported, as are the necessary QSTrader libraries for handling signals and strategies. PriceParser
is brought in to adjust the internal handling of QSTrader's price storage mechanism to avoid floating-point round-off error.
The deque–double-ended queue–class is also imported and used to store a rolling window of closing price bars, necessary for the moving average and standard deviation lookback calculations. More on this below.
# coint_bollinger_strategy.py
from __future__ import print_function
from collections import deque
from math import floor
import numpy as np
from qstrader.price_parser import PriceParser
from qstrader.event import (SignalEvent, EventType)
from qstrader.strategy.base import AbstractStrategy
The next step is to define the CointegrationBollingerBandsStrategy
subclass of AbstractStrategy
, which carries out the signals generation. As with all strategies it requires a list of tickers
that it acts upon as well as a handle to the events_queue
upon which to place the SignalEvent
objects.
This subclass requires additional parameters. lookback
is the integer number of bars over which to perform the rolling moving average and standard deviation calculations. weights
is the set of fixed hedging ratios, or primary Johansen test eigenvector components, to use as the "unit" of a portfolio of cointegrating assets.
entry_z
describes the multiple of z-score entry threshold (i.e. number of standard deviations) upon which to open a trade. exit_z
is the corresponding number of standard deviations upon which to exit the trade. base_quantity
is the integer number of "units" of the portfolio to trade.
In addition the class also keeps track of the latest prices of all tickers in a separate array in self.latest_prices
. It also contains a double-ended queue consisting of the last lookback
values of the market value of a "unit" of the portfolio in self.port_mkt_value
. A self.invested
flag allows the strategy itself to keep track of whether it is "in the market" or not:
class CointegrationBollingerBandsStrategy(AbstractStrategy):
"""
Requires:
tickers - The list of ticker symbols
events_queue - A handle to the system events queue
lookback - Lookback period for moving avg and moving std
weights - The weight vector describing
a "unit" of the portfolio
entry_z - The z-score trade entry threshold
exit_z - The z-score trade exit threshold
base_quantity - Number of "units" of the portfolio
to be traded
"""
def __init__(
self, tickers, events_queue,
lookback, weights, entry_z, exit_z,
base_quantity
):
self.tickers = tickers
self.events_queue = events_queue
self.lookback = lookback
self.weights = weights
self.entry_z = entry_z
self.exit_z = exit_z
self.qty = base_quantity
self.time = None
self.latest_prices = np.full(len(self.tickers), -1.0)
self.port_mkt_val = deque(maxlen=self.lookback)
self.invested = None
self.bars_elapsed = 0
The following _set_correct_time_and_price
method is similar to that found in the QSTrader Kalman Filter article. The goal of this method is to make sure that the self.latest_prices
array is populated with the latest market values of each ticker. The strategy will only execute if this array contains a full set of prices, all containing the same time-stamp (i.e. representing the same timeframe over a bar).
The previous version of this method was fixed for an array of two prices but the code below works for any number of tickers, which is necessary for cointegrating portfolios that might contain three or more assets:
def _set_correct_time_and_price(self, event):
"""
Sets the correct price and event time for prices
that arrive out of order in the events queue.
"""
# Set the first instance of time
if self.time is None:
self.time = event.time
# Set the correct latest prices depending upon
# order of arrival of market bar event
price = event.adj_close_price/PriceParser.PRICE_MULTIPLIER
if event.time == self.time:
for i in range(0, len(self.tickers)):
if event.ticker == self.tickers[i]:
self.latest_prices[i] = price
else:
self.time = event.time
self.bars_elapsed += 1
self.latest_prices = np.full(len(self.tickers), -1.0)
for i in range(0, len(self.tickers)):
if event.ticker == self.tickers[i]:
self.latest_prices[i] = price
go_long_units
is a helper method that longs the appropriate quantity of portfolio "units" by purchasing their individual components separately in the correct quantities. It achieves this by shorting any component that has a negative value in the self.weights
array and by longing any component that has a positive value. Note that it multiplies this by the self.qty
value, which is the base number of units to transact for a portfolio "unit":
def go_long_units(self):
"""
Go long the appropriate number of "units" of the
portfolio to open a new position or to close out
a short position.
"""
for i, ticker in enumerate(self.tickers):
if self.weights[i] < 0.0:
self.events_queue.put(SignalEvent(
ticker, "SLD",
int(floor(-1.0*self.qty*self.weights[i])))
)
else:
self.events_queue.put(SignalEvent(
ticker, "BOT",
int(floor(self.qty*self.weights[i])))
go_short_units
is almost identical to the above method except that it swaps the long/short commands, so that the positions can be closed or shorted:
def go_short_units(self):
"""
Go short the appropriate number of "units" of the
portfolio to open a new position or to close out
a long position.
"""
for i, ticker in enumerate(self.tickers):
if self.weights[i] < 0.0:
self.events_queue.put(SignalEvent(
ticker, "BOT",
int(floor(-1.0*self.qty*self.weights[i])))
)
else:
self.events_queue.put(SignalEvent(
ticker, "SLD",
int(floor(self.qty*self.weights[i])))
)
zscore_trade
takes the latest calculated z-score of the portfolio market price and uses this to long, short or close a trade. The logic below encapsulates the "Bollinger Bands" aspect of the strategy.
If the z-score is less than the negative of the entry threshold, a long position is created. If the z-score is greater than the positive of the entry threshold, a short position is created. Correspondingly, if the strategy is already in the market and the z-score exceeds the negative of the exit threshold, any long position is closed. If the strategy is already in the market and the z-score is less than the exit threshold, a short position is closed:
def zscore_trade(self, zscore, event):
"""
Determine whether to trade if the entry or exit zscore
threshold has been exceeded.
"""
# If we're not in the market...
if self.invested is None:
if zscore < -self.entry_z:
# Long Entry
print("LONG: %s" % event.time)
self.go_long_units()
self.invested = "long"
elif zscore > self.entry_z:
# Short Entry
print("SHORT: %s" % event.time)
self.go_short_units()
self.invested = "short"
# If we are in the market...
if self.invested is not None:
if self.invested == "long" and zscore >= -self.exit_z:
print("CLOSING LONG: %s" % event.time)
self.go_short_units()
self.invested = None
elif self.invested == "short" and zscore <= self.exit_z:
print("CLOSING SHORT: %s" % event.time)
self.go_long_units()
self.invested = None
Finally, the calculate_signals
method makes sure the self.latest_prices
array is fully up to date and only trades if all the latest prices exist. If these prices do exist, the self.port_mkt_val
deque is updated to contain the latest "market value" of a unit portfolio. This is simply the dot product of the latest prices of each constituent and their weight vector.
The z-score of the latest portfolio unit market value is then calculated by subtracting the rolling mean and dividing by the rolling standard deviation. This z-score is then sent to the above method zscore_trade
to generate the trading signals:
def calculate_signals(self, event):
"""
Calculate the signals for the strategy.
"""
if event.type == EventType.BAR:
self._set_correct_time_and_price(event)
# Only trade if we have all prices
if all(self.latest_prices > -1.0):
# Calculate portfolio market value via dot product
# of ETF prices with portfolio weights
self.port_mkt_val.append(
np.dot(self.latest_prices, self.weights)
)
# If there is enough data to form a full lookback
# window, then calculate zscore and carry out
# respective trades if thresholds are exceeded
if self.bars_elapsed > self.lookback:
zscore = (
self.port_mkt_val[-1] - np.mean(self.port_mkt_val)
) / np.std(self.port_mkt_val)
self.zscore_trade(zscore, event)
The remaining file is coint_bollinger_backtest.py
, which wraps the strategy class in backtesting logic. It is extremely similar to all other QSTrader backtest files discussed on the site. While the full listing is given below at the end of the article, the snippet directly below references the important aspect where the CointegrationBollingerBandsStrategy
is created.
The weights
array is hardcoded from the values obtained from the R CADF procedure above, while the lookback
period is (arbitrarily) set to 15 values. The entry and exit z-score thresholds are set of 1.5 and 0.5 standard deviations, respectively. Since the account equity is set at 500,000 USD the base_quantity
of shares is set to 10,000.
These values can all be tested and optimised, e.g. through a grid-search procedure, if desired.
# coint_bollinger_strategy.py
..
..
# Use the Cointegration Bollinger Bands trading strategy
weights = np.array([1.0, -1.213])
lookback = 15
entry_z = 1.5
exit_z = 0.5
base_quantity = 10000
strategy = CointegrationBollingerBandsStrategy(
tickers, events_queue,
lookback, weights,
entry_z, exit_z, base_quantity
)
strategy = Strategies(strategy, DisplayStrategy())
..
..
To run the backtest a working installation of QSTrader is needed and these two files described above need to be placed in the same directory. Assuming the availability of the ARNC and UNG data, the backtest will execute upon typing the following command into the terminal:
$ python coint_bollinger_backtest.py --tickers=ARNC,UNG
You will receive the following (truncated) output:
..
..
Backtest complete.
Sharpe Ratio: 1.22071888063
Max Drawdown: 0.0701967400339
Max Drawdown Pct: 0.0701967400339
Strategy Results
Transaction Costs
The strategy results presented here are given net of transaction costs. The costs are simulated using Interactive Brokers US equities fixed pricing for shares in North America. They do not take into account commission differences for ETFs, but they are reasonably representative of what could be achieved in a real trading strategy.
Tearsheet
Click the image for a larger view.
Once again recall that this strategy contains an implicit lookahead bias due to the fact that the CADF procedure was carried out over the same sample of data as the trading strategy.
With that in mind, the strategy posts a Sharpe Ratio of 1.22, with a maximum daily drawdown of 7.02%. The majority of the strategy gains occur in a single month within January 2015, after which the strategy performs poorly. It remains in drawdown throughout 2016. This is not surprising since no statistically significant cointegrating relationship was found between ARNC and UNG across the period studied, at least using the ADF test procedure.
In order to improve this strategy a more refined view of the economics of the aluminum smelting process could be taken. For instance, while there is a clear need for electricity to carry out the electrolysis process, this power can be derived from many sources, including hydroelectric, coal, nuclear and, likely in the future, wind and solar. A more comprehensive portfolio including the price of aluminum, large aluminum producers, and ETFs representing varying energy sources might be considered.
References
- [1] Chan, E. P. (2013) Algorithmic Trading: Winning Strategies and their Rationale, Wiley
- [2] Fawcett, J. (2012) Ernie Chan's "Gold vs. gold-miners" stat arb, https://www.quantopian.com/posts/ernie-chans-gold-vs-gold-miners-stat-arb
- [3] Reiakvam, O.H., Thyness, S.B. (2011) "Pairs Trading in the Aluminum Market: A Cointegration Approach", Masters Thesis, Norwegian University of Science and Technology
Full Code
# coint_cadf.R
library("quantmod")
library("tseries")
## Obtain ARNC and UNG
getSymbols("ARNC", from="2014-11-11", to="2017-01-01")
getSymbols("UNG", from="2014-11-11", to="2017-01-01")
## Utilise the backwards-adjusted closing prices
aAdj = unclass(ARNC$ARNC.Adjusted)
bAdj = unclass(UNG$UNG.Adjusted)
## Plot the ETF backward-adjusted closing prices
plot(aAdj, type="l", xlim=c(0, length(aAdj)), ylim=c(0.0, 45.0), xlab="November 11th 2014 to January 1st 2017", ylab="Backward-Adjusted Prices in USD", col="blue")
par(new=T)
plot(bAdj, type="l", xlim=c(0, length(bAdj)), ylim=c(0.0, 45.0), axes=F, xlab="", ylab="", col="red")
par(new=F)
## Plot a scatter graph of the ETF adjusted prices
plot(aAdj, bAdj, xlab="ARNC Backward-Adjusted Prices", ylab="UNG Backward-Adjusted Prices")
## Carry out linear regression on the two price series
comb = lm(aAdj~bAdj)
## Now we perform the ADF test on the residuals,
## or "spread" on the model, using a single lag order
adf.test(comb$residuals, k=1)
# coint_bollinger_strategy.py
from __future__ import print_function
from collections import deque
from math import floor
import numpy as np
from qstrader.price_parser import PriceParser
from qstrader.event import (SignalEvent, EventType)
from qstrader.strategy.base import AbstractStrategy
class CointegrationBollingerBandsStrategy(AbstractStrategy):
"""
Requires:
tickers - The list of ticker symbols
events_queue - A handle to the system events queue
lookback - Lookback period for moving avg and moving std
weights - The weight vector describing
a "unit" of the portfolio
entry_z - The z-score trade entry threshold
exit_z - The z-score trade exit threshold
base_quantity - Number of "units" of the portfolio
to be traded
"""
def __init__(
self, tickers, events_queue,
lookback, weights, entry_z, exit_z,
base_quantity
):
self.tickers = tickers
self.events_queue = events_queue
self.lookback = lookback
self.weights = weights
self.entry_z = entry_z
self.exit_z = exit_z
self.qty = base_quantity
self.time = None
self.latest_prices = np.full(len(self.tickers), -1.0)
self.port_mkt_val = deque(maxlen=self.lookback)
self.invested = None
self.bars_elapsed = 0
def _set_correct_time_and_price(self, event):
"""
Sets the correct price and event time for prices
that arrive out of order in the events queue.
"""
# Set the first instance of time
if self.time is None:
self.time = event.time
# Set the correct latest prices depending upon
# order of arrival of market bar event
price = event.adj_close_price/PriceParser.PRICE_MULTIPLIER
if event.time == self.time:
for i in range(0, len(self.tickers)):
if event.ticker == self.tickers[i]:
self.latest_prices[i] = price
else:
self.time = event.time
self.bars_elapsed += 1
self.latest_prices = np.full(len(self.tickers), -1.0)
for i in range(0, len(self.tickers)):
if event.ticker == self.tickers[i]:
self.latest_prices[i] = price
def go_long_units(self):
"""
Go long the appropriate number of "units" of the
portfolio to open a new position or to close out
a short position.
"""
for i, ticker in enumerate(self.tickers):
if self.weights[i] < 0.0:
self.events_queue.put(SignalEvent(
ticker, "SLD",
int(floor(-1.0*self.qty*self.weights[i])))
)
else:
self.events_queue.put(SignalEvent(
ticker, "BOT",
int(floor(self.qty*self.weights[i])))
)
def go_short_units(self):
"""
Go short the appropriate number of "units" of the
portfolio to open a new position or to close out
a long position.
"""
for i, ticker in enumerate(self.tickers):
if self.weights[i] < 0.0:
self.events_queue.put(SignalEvent(
ticker, "BOT",
int(floor(-1.0*self.qty*self.weights[i])))
)
else:
self.events_queue.put(SignalEvent(
ticker, "SLD",
int(floor(self.qty*self.weights[i])))
)
def zscore_trade(self, zscore, event):
"""
Determine whether to trade if the entry or exit zscore
threshold has been exceeded.
"""
# If we're not in the market...
if self.invested is None:
if zscore < -self.entry_z:
# Long Entry
print("LONG: %s" % event.time)
self.go_long_units()
self.invested = "long"
elif zscore > self.entry_z:
# Short Entry
print("SHORT: %s" % event.time)
self.go_short_units()
self.invested = "short"
# If we are in the market...
if self.invested is not None:
if self.invested == "long" and zscore >= -self.exit_z:
print("CLOSING LONG: %s" % event.time)
self.go_short_units()
self.invested = None
elif self.invested == "short" and zscore <= self.exit_z:
print("CLOSING SHORT: %s" % event.time)
self.go_long_units()
self.invested = None
def calculate_signals(self, event):
"""
Calculate the signals for the strategy.
"""
if event.type == EventType.BAR:
self._set_correct_time_and_price(event)
# Only trade if we have all prices
if all(self.latest_prices > -1.0):
# Calculate portfolio market value via dot product
# of ETF prices with portfolio weights
self.port_mkt_val.append(
np.dot(self.latest_prices, self.weights)
)
# If there is enough data to form a full lookback
# window, then calculate zscore and carry out
# respective trades if thresholds are exceeded
if self.bars_elapsed > self.lookback:
zscore = (
self.port_mkt_val[-1] - np.mean(self.port_mkt_val)
) / np.std(self.port_mkt_val)
self.zscore_trade(zscore, event)
# coint_bollinger_backtest.py
import datetime
import click
import numpy as np
from qstrader import settings
from qstrader.compat import queue
from qstrader.price_parser import PriceParser
from qstrader.price_handler.yahoo_daily_csv_bar import YahooDailyCsvBarPriceHandler
from qstrader.strategy import Strategies, DisplayStrategy
from qstrader.position_sizer.naive import NaivePositionSizer
from qstrader.risk_manager.example import ExampleRiskManager
from qstrader.portfolio_handler import PortfolioHandler
from qstrader.compliance.example import ExampleCompliance
from qstrader.execution_handler.ib_simulated import IBSimulatedExecutionHandler
from qstrader.statistics.tearsheet import TearsheetStatistics
from qstrader.trading_session.backtest import Backtest
from coint_bollinger_strategy import CointegrationBollingerBandsStrategy
def run(config, testing, tickers, filename):
# Set up variables needed for backtest
events_queue = queue.Queue()
csv_dir = config.CSV_DATA_DIR
initial_equity = PriceParser.parse(500000.00)
# Use Yahoo Daily Price Handler
start_date = datetime.datetime(2015, 1, 1)
end_date = datetime.datetime(2016, 9, 1)
price_handler = YahooDailyCsvBarPriceHandler(
csv_dir, events_queue, tickers,
start_date=start_date, end_date=end_date
)
# Use the Cointegration Bollinger Bands trading strategy
weights = np.array([1.0, -1.213])
lookback = 15
entry_z = 1.5
exit_z = 0.5
base_quantity = 10000
strategy = CointegrationBollingerBandsStrategy(
tickers, events_queue,
lookback, weights,
entry_z, exit_z, base_quantity
)
strategy = Strategies(strategy, DisplayStrategy())
# Use the Naive Position Sizer
# where suggested quantities are followed
position_sizer = NaivePositionSizer()
# Use an example Risk Manager
risk_manager = ExampleRiskManager()
# Use the default Portfolio Handler
portfolio_handler = PortfolioHandler(
initial_equity, events_queue, price_handler,
position_sizer, risk_manager
)
# Use the ExampleCompliance component
compliance = ExampleCompliance(config)
# Use a simulated IB Execution Handler
execution_handler = IBSimulatedExecutionHandler(
events_queue, price_handler, compliance
)
# Use the Tearsheet Statistics
title = ["aluminum Smelting Strategy - ARNC/UNG"]
statistics = TearsheetStatistics(
config, portfolio_handler, title
)
# Set up the backtest
backtest = Backtest(
price_handler, strategy,
portfolio_handler, execution_handler,
position_sizer, risk_manager,
statistics, initial_equity
)
results = backtest.simulate_trading(testing=testing)
statistics.save(filename)
return results
@click.command()
@click.option('--config', default=settings.DEFAULT_CONFIG_FILENAME, help='Config filename')
@click.option('--testing/--no-testing', default=False, help='Enable testing mode')
@click.option('--tickers', default='SPY', help='Tickers (use comma)')
@click.option('--filename', default='', help='Pickle (.pkl) statistics filename')
def main(config, testing, tickers, filename):
tickers = tickers.split(",")
config = settings.from_file(config, testing)
run(config, testing, tickers, filename)
if __name__ == "__main__":
main()