Aluminum Smelting Cointegration Strategy in QSTrader

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.*

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.

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.

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.

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.

**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

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.

*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.

- [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*

# 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()comments powered by Disqus