book
Checkout our new book! Hands on AI Trading with Python, QuantConnect, and AWS Learn More arrow

Applying Research

Uncorrelated Assets

Introduction

This page explains how to you can use the Research Environment to develop and test a Uncorrelated Assets hypothesis, then put the hypothesis in production.

Create Hypothesis

According to Modern Portfolio Thoery, asset combinations with negative or very low correlation could have lower total portfolio variance given the same level of return. Thus, uncorrelated assets allows you to find a portfolio that will, theoretically, be more diversified and resilient to extreme market events. We're testing this statement in real life scenario, while hypothesizing a portfolio with uncorrelated assets could be a consistent portfolio. In this example, we'll compare the performance of 5-least-correlated-asset portfolio (proposed) and 5-most-correlated-asset portfolio (benchmark), both equal weighting.

Import Libraries

We'll need to import libraries to help with data processing and visualization. Import numpy and matplotlib libraries by the following:

Select Language:
import numpy as np
from matplotlib import pyplot as plt

Get Historical Data

To begin, we retrieve historical data for researching.

  1. Instantiate a QuantBook.
  2. Select Language:
    qb = QuantBook()
  3. Select the desired tickers for research.
  4. Select Language:
    assets = ["SHY", "TLT", "SHV", "TLH", "EDV", "BIL",
              "SPTL", "TBT", "TMF", "TMV", "TBF", "VGSH", "VGIT",
              "VGLT", "SCHO", "SCHR", "SPTS", "GOVT"]
  5. Call the add_equity method with the tickers, and their corresponding resolution.
  6. Select Language:
    for i in range(len(assets)):
        qb.add_equity(assets[i],Resolution.MINUTE)

    If you do not pass a resolution argument, Resolution.MINUTE is used by default.

  7. Call the history method with qb.securities.keys for all tickers, time argument(s), and resolution to request historical data for the symbol.
  8. Select Language:
    history = qb.history(qb.securities.keys(), datetime(2021, 1, 1), datetime(2021, 12, 31), Resolution.DAILY)
    Historical data

Prepare Data

We'll have to process our data to get their correlation and select the least and most related ones.

  1. Select the close column and then call the unstack method, then call pct_change to compute the daily return.
  2. Select Language:
    returns = history['close'].unstack(level=0).pct_change().iloc[1:]
  3. Write a function to obtain the least and most correlated 5 assets.
  4. Select Language:
    def get_uncorrelated_assets(returns, num_assets):
        # Get correlation
        correlation = returns.corr()
        
        # Find assets with lowest and highest absolute sum correlation
        selected = []
        for index, row in correlation.iteritems():
            corr_rank = row.abs().sum()
            selected.append((index, corr_rank))
    
        # Sort and take the top num_assets
        sort_ = sorted(selected, key = lambda x: x[1])
        uncorrelated = sort_[:num_assets]
        correlated = sort_[-num_assets:]
        
        return uncorrelated, correlated
    
    selected, benchmark = get_uncorrelated_assets(returns, 5)
    uncorrelated assets equity curve

Test Hypothesis

To test the hypothesis: Our desired outcome would be a consistent and low fluctuation equity curve should be seen, as compared with benchmark.

  1. Construct a equal weighting portfolio for the 5-uncorrelated-asset-portfolio and the 5-correlated-asset-portfolio (benchmark).
  2. Select Language:
    port_ret = returns[[x[0] for x in selected]] / 5
    bench_ret = returns[[x[0] for x in benchmark]] / 5
  3. Call cumprod to get the cumulative return.
  4. Select Language:
    total_ret = (np.sum(port_ret, axis=1) + 1).cumprod()
    total_ret_bench = (np.sum(bench_ret, axis=1) + 1).cumprod()
  5. Plot the result.
  6. Select Language:
    plt.figure(figsize=(15, 10))
    total_ret.plot(label='Proposed')
    total_ret_bench.plot(label='Benchmark')
    plt.title('Equity Curve')
    plt.legend()
    plt.show()
    Uncorrelated assets equity curve
-image

We can clearly see from the results, the proposed uncorrelated-asset-portfolio has a lower variance/fluctuation, thus more consistent than the benchmark. This proven our hypothesis.

Set Up Algorithm

Once we are confident in our hypothesis, we can export this code into backtesting. One way to accomodate this model into research is to create a scheduled event which uses our model to pick stocks and goes long.

Select Language:
def initialize(self) -> None:

    #1. Required: Five years of backtest history
    self.set_start_date(2014, 1, 1)

    #2. Required: Alpha Streams Models:
    self.set_brokerage_model(BrokerageName.ALPHA_STREAMS)

    #3. Required: Significant AUM Capacity
    self.set_cash(1000000)

    #4. Required: Benchmark to SPY
    self.set_benchmark("SPY")
    
    self.set_portfolio_construction(EqualWeightingPortfolioConstructionModel())
    self.set_execution(ImmediateExecutionModel())

    self.assets = ["SHY", "TLT", "IEI", "SHV", "TLH", "EDV", "BIL",
                    "SPTL", "TBT", "TMF", "TMV", "TBF", "VGSH", "VGIT",
                    "VGLT", "SCHO", "SCHR", "SPTS", "GOVT"]
    
    # Add Equity ------------------------------------------------ 
    for i in range(len(self.assets)):
        self.add_equity(self.assets[i], Resolution.MINUTE)
    
    # Set Scheduled Event Method For Our Model. In this example, we'll rebalance every month.
    self.schedule.on(self.date_rules.month_start(), 
        self.time_rules.before_market_close("SHY", 5), 
        self.every_day_before_market_close)

Now we export our model into the scheduled event method. We will switch qb with self and replace methods with their QCAlgorithm counterparts as needed. In this example, this is not an issue because all the methods we used in research also exist in QCAlgorithm.

Select Language:
def every_day_before_market_close(self) -> None:
    qb = self
    # Fetch history on our universe
    history = qb.history(qb.securities.keys(), 252*2, Resolution.DAILY)
    if history.empty:
        return

    # Select the close column and then call the unstack method, then call pct_change to compute the daily return.
    returns = history['close'].unstack(level=0).pct_change().iloc[1:]

    # Get correlation
    correlation = returns.corr()
    
    # Find 5 assets with lowest absolute sum correlation
    selected = []
    for index, row in correlation.iteritems():
        corr_rank = row.abs().sum()
        selected.append((index, corr_rank))

    sort_ = sorted(selected, key = lambda x: x[1])
    selected = [x[0] for x in sort_[:5]]

    # ==============================
    
    insights = []
    
    for symbol in selected:
        insights.append( Insight.price(symbol, Expiry.END_OF_MONTH, InsightDirection.UP) )

    self.emit_insights(insights)

Examples

The below code snippets concludes the above jupyter research notebook content.

Select Language:
# Instantiate a QuantBook.
qb = QuantBook()

# Select the desired tickers for research.
assets = ["SHY", "TLT", "SHV", "TLH", "EDV", "BIL",
          "SPTL", "TBT", "TMF", "TMV", "TBF", "VGSH", "VGIT",
          "VGLT", "SCHO", "SCHR", "SPTS", "GOVT"]

# Call the add_equity method with the tickers, and its corresponding resolution. Then store their Symbols. Resolution.MINUTE is used by default. 
for i in range(len(assets)):
    qb.add_equity(assets[i],Resolution.MINUTE)

# Call the history method with qb.securities.keys for all tickers, time argument(s), and resolution to request historical data for the symbol.
history = qb.history(qb.securities.keys(), datetime(2021, 1, 1), datetime(2021, 12, 31), Resolution.DAILY)

# Select the close column and then call the unstack method, then call pct_change to compute the daily return.
returns = history['close'].unstack(level=0).pct_change().iloc[1:]

# Write a function to obtain the least and most correlated 5 assets.
def get_uncorrelated_assets(returns, num_assets):
    # Get correlation
    correlation = returns.corr()
    
    # Find assets with lowest and highest absolute sum correlation
    selected = []
    for index, row in correlation.iteritems():
        corr_rank = row.abs().sum()
        selected.append((index, corr_rank))

    # Sort and take the top num_assets
    sort_ = sorted(selected, key = lambda x: x[1])
    uncorrelated = sort_[:num_assets]
    correlated = sort_[-num_assets:]
    
    return uncorrelated, correlated

selected, benchmark = get_uncorrelated_assets(returns, 5)

# Construct a equal weighting portfolio for the 5-uncorrelated-asset-portfolio and the 5-correlated-asset-portfolio (benchmark).
port_ret = returns[[x[0] for x in selected]] / 5
bench_ret = returns[[x[0] for x in benchmark]] / 5

# Call cumprod to get the cumulative return.
total_ret = (np.sum(port_ret, axis=1) + 1).cumprod()
total_ret_bench = (np.sum(bench_ret, axis=1) + 1).cumprod()

# Plot the result.
plt.figure(figsize=(15, 10))
total_ret.plot(label='Proposed')
total_ret_bench.plot(label='Benchmark')
plt.title('Equity Curve')
plt.legend()
plt.show()

The below code snippets concludes the algorithm set up.

Select Language:
class UncorrelatedAssetsDemo(QCAlgorithm):
    
    def initialize(self) -> None:
        #1. Required: Five years of backtest history
        self.set_start_date(2014, 1, 1)
        self.set_end_date(2019, 1, 1)
    
        #2. Required: Alpha Streams Models:
        self.set_brokerage_model(BrokerageName.ALPHA_STREAMS)
    
        #3. Required: Significant AUM Capacity
        self.set_cash(1000000)
    
        #4. Required: Benchmark to SPY
        self.set_benchmark("SPY")
        
        self.set_portfolio_construction(EqualWeightingPortfolioConstructionModel())
        self.set_execution(ImmediateExecutionModel())
    
        self.assets = ["SHY", "TLT", "IEI", "SHV", "TLH", "EDV", "BIL",
                        "SPTL", "TBT", "TMF", "TMV", "TBF", "VGSH", "VGIT",
                        "VGLT", "SCHO", "SCHR", "SPTS", "GOVT"]
        
        # Add Equity ------------------------------------------------ 
        for i in range(len(self.assets)):
            self.add_equity(self.assets[i], Resolution.MINUTE).symbol
        
        # Set Scheduled Event Method For Our Model. In this example, we'll rebalance every month.
        self.schedule.on(self.date_rules.month_start(), 
            self.time_rules.before_market_close("SHY", 5), 
            self.every_day_before_market_close)
            
    def every_day_before_market_close(self) -> None:
        qb = self
        # Fetch history on our universe
        history = qb.history(qb.securities.Keys, 252*2, Resolution.DAILY)
        if history.empty: return
    
        # Select the close column and then call the unstack method, then call pct_change to compute the daily return.
        returns = history['close'].unstack(level=0).pct_change().iloc[1:]
    
        # Get correlation
        correlation = returns.corr()
        
        # Find 5 assets with lowest absolute sum correlation
        selected = []
        for index, row in correlation.iterrows():
            corr_rank = row.abs().sum()
            selected.append((index, corr_rank))
    
        sort_ = sorted(selected, key = lambda x: x[1])
        selected = [x[0] for x in sort_[:5]]
    
        # ==============================
        
        insights = []
        
        for symbol in selected:
            insights.append( Insight.price(symbol, Expiry.END_OF_MONTH, InsightDirection.UP) )
    
        self.emit_insights(insights)

You can also see our Videos. You can also get in touch with us via Discord.

Did you find this page helpful?

Contribute to the documentation: