Overall Statistics
Total Orders
132
Average Win
1.71%
Average Loss
-0.23%
Compounding Annual Return
2.144%
Drawdown
2.800%
Expectancy
3.584
Start Equity
100000
End Equity
169426.89
Net Profit
69.427%
Sharpe Ratio
-0.514
Sortino Ratio
-0.61
Probabilistic Sharpe Ratio
61.411%
Loss Rate
46%
Win Rate
54%
Profit-Loss Ratio
7.49
Alpha
-0.007
Beta
-0.015
Annual Standard Deviation
0.014
Annual Variance
0
Information Ratio
-0.309
Tracking Error
0.162
Treynor Ratio
0.493
Total Fees
$951.32
Estimated Strategy Capacity
$33000000.00
Lowest Capacity Asset
SHY SGNKIKYGE9NP
Portfolio Turnover
1.23%
from AlgorithmImports import *
from dateutil.relativedelta import relativedelta

# Custom fee model.
class CustomFeeModel(FeeModel):
    def GetOrderFee(self, parameters):
        fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
        return OrderFee(CashAmount(fee, "USD"))

# Quandl "value" data
class QuandlValue(PythonQuandl):
    def __init__(self):
        self.ValueColumnName = 'Value'

# Quantpedia data.
# NOTE: IMPORTANT: Data order must be ascending (datewise)
class QuantpediaFutures(PythonData):
    _last_update_date:Dict[str, datetime.date] = {}

    @staticmethod
    def get_last_update_date() -> Dict[str, datetime.date]:
       return QuantpediaFutures._last_update_date

    def GetSource(self, config, date, isLiveMode):
        return SubscriptionDataSource("data.quantpedia.com/backtesting_data/futures/{0}.csv".format(config.Symbol.Value), SubscriptionTransportMedium.RemoteFile, FileFormat.Csv)

    def Reader(self, config, line, date, isLiveMode):
        data = QuantpediaFutures()
        data.Symbol = config.Symbol
        
        if not line[0].isdigit(): return None
        split = line.split(';')
        
        data.Time = datetime.strptime(split[0], "%d.%m.%Y") + timedelta(days=1)
        data['back_adjusted'] = float(split[1])
        data['spliced'] = float(split[2])
        data.Value = float(split[1])

        # store last update date
        if config.Symbol.Value not in QuantpediaFutures._last_update_date:
            QuantpediaFutures._last_update_date[config.Symbol.Value] = datetime(1,1,1).date()

        if data.Time.date() > QuantpediaFutures._last_update_date[config.Symbol.Value]:
            QuantpediaFutures._last_update_date[config.Symbol.Value] = data.Time.date()

        return data

class InterestRate3M(PythonData):
    _last_update_date:Dict[str, datetime.date] = {}

    @staticmethod
    def get_last_update_date() -> Dict[str, datetime.date]:
       return InterestRate3M._last_update_date

    def GetSource(self, config:SubscriptionDataConfig, date:datetime, isLiveMode:bool) -> SubscriptionDataSource:
        return SubscriptionDataSource("data.quantpedia.com/backtesting_data/interbank_rate/{0}.csv".format(config.Symbol.Value), SubscriptionTransportMedium.RemoteFile, FileFormat.Csv)

    def Reader(self, config:SubscriptionDataConfig, line:str, date:datetime, isLiveMode:bool) -> BaseData:
        data = InterestRate3M()
        data.Symbol = config.Symbol
        
        if not line[0].isdigit(): return None
        split = line.split(';')
        
        data.Time = datetime.strptime(split[0], "%Y-%m-%d") + relativedelta(months=2)
        data['value'] = float(split[1])
        data.Value = float(split[1])

        # store last update date
        if config.Symbol.Value not in InterestRate3M._last_update_date:
            InterestRate3M._last_update_date[config.Symbol.Value] = datetime(1,1,1).date()

        if data.Time.date() > InterestRate3M._last_update_date[config.Symbol.Value]:
            InterestRate3M._last_update_date[config.Symbol.Value] = data.Time.date()

        return data
# https://quantpedia.com/strategies/crude-oil-predicts-equity-returns/
#
# Several types of oil can be used (Brent, WTI, Dubai etc.) without big differences in results. The source paper for
# this anomaly uses Arab Light crude oil. Monthly oil returns are used in the regression equation as an independent
# variable and equity returns are used as a dependent variable. The model is re-estimated every month and
# observations of the last month are added. The investor determines whether the expected stock market return in 
# a specific month (based on regression results and conditional on the oil price change in the previous month) is higher
# or lower than the risk-free rate. The investor is fully invested in the market portfolio if the expected
# return is higher (bull market); he invests in cash if the expected return is lower (bear market).

from data_tools import QuantpediaFutures, QuandlValue, CustomFeeModel, InterestRate3M
from AlgorithmImports import *
from typing import List, Dict
import numpy as np
from scipy import stats
from collections import deque

class CrudeOilPredictsEquityReturns(QCAlgorithm):

    def Initialize(self):
        self.SetStartDate(2000, 1, 1)
        self.SetCash(100000)

        self.min_period:int = 13
        self.leverage:int = 2

        self.data:Dict[Symbol, deque] = {}

        self.symbols:List[str] = [
            "CME_ES1",  # E-mini S&P 500 Futures, Continuous Contract #1
            "CME_CL1"   # Crude Oil Futures, Continuous Contract #1
        ]
        
        self.cash:Symbol = self.AddEquity('SHY', Resolution.Daily).Symbol
        self.risk_free_rate:Symbol = self.AddData(InterestRate3M, 'IR3TIB01USM156N', Resolution.Daily).Symbol
        
        for symbol in self.symbols:
            data = self.AddData(QuantpediaFutures, symbol, Resolution.Daily)
            data.SetLeverage(self.leverage)
            data.SetFeeModel(CustomFeeModel())
            self.data[symbol] = deque()
        
        self.Settings.MinimumOrderMarginPortfolioPercentage = 0.
        self.recent_month:int = -1

    def OnData(self, data:Slice) -> None:
        rebalance_flag:bool = False
        
        for symbol in self.symbols:
            if symbol in data:
                if self.recent_month != self.Time.month:
                    rebalance_flag = True
                    
                if data[symbol]:
                    price:float = data[symbol].Value
                    self.data[symbol].append(price)

        if rebalance_flag:
            self.recent_month = self.Time.month
        
        ir_last_update_date:Dict[str, datetime.date] = InterestRate3M.get_last_update_date()
        last_update_date:Dict[str, datetime.date] = QuantpediaFutures.get_last_update_date()

        rf_rate:float = .0
        # check if data is still coming
        if self.Securities[self.risk_free_rate].GetLastData() and ir_last_update_date[self.risk_free_rate.Value] > self.Time.date():
            rf_rate = self.Securities[self.risk_free_rate].Price / 100
        else:
            return

        if not all(last_update_date[x] > self.Time.date() for x in self.symbols):
            self.Liquidate()
            return

        market_prices:np.ndarray = np.array(self.data[self.symbols[0]])
        oil_prices:np.ndarray = np.array(self.data[self.symbols[1]])
        
        # At least one year of data is ready.
        if len(market_prices) < self.min_period or len(oil_prices) < self.min_period:
            return
        
        # Trim price series lenghts.
        min_size:float = min(len(market_prices), len(oil_prices))
        market_prices = market_prices[-min_size:]
        oil_prices = oil_prices[-min_size:]
        
        market_returns = market_prices[1:] / market_prices[:-1] - 1
        oil_returns = oil_prices[1:] / oil_prices[:-1] - 1
        
        # Simple Linear Regression
        # Y = C + (M * X)
        # Y = α + (β ∗ X)

        # Y = Dependent variable (output/outcome/prediction/estimation)
        # C/α = Constant (Y-Intercept)
        # M/β = Slope of the regression line (the effect that X has on Y)
        # X = Independent variable (input variable used in the prediction of Y)
        slope, intercept, r_value, p_value, std_err = stats.linregress(oil_returns[:-1], market_returns[1:])
        expected_market_return = intercept + (slope * oil_returns[-1])
        
        if expected_market_return > rf_rate:
            if self.Portfolio[self.cash].Invested:
                self.Liquidate(self.cash)
            
            self.SetHoldings(self.symbols[0], 1)
        else:
            if self.Portfolio[self.symbols[0]].Invested:
                self.Liquidate(self.symbols[0])
            if self.cash in data and data[self.cash]:
                self.SetHoldings(self.cash, 1)