Aluminum Smelting Cointegration Strategy in QSTrader

By Michael Halls-Moore on January 12th, 2017

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:

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:

  1. 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.
  2. 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

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

Just Getting Started with Quantitative Trading?

3 Reasons to Subscribe to the QuantStart Email List:

No Thanks, I'll Pass For Now

1. Quant Trading Lessons

You'll get instant access to a free 10-part email course packed with hints and tips to help you get started in quantitative trading!

2. All The Latest Content

Every week I'll send you a wrap of all activity on QuantStart so you'll never miss a post again.

3. No Spam

Real, actionable quant trading tips with no nonsense.