Overall Statistics
Total Orders
859
Average Win
0.17%
Average Loss
-0.21%
Compounding Annual Return
-0.260%
Drawdown
31.700%
Expectancy
-0.062
Start Equity
100000
End Equity
93876.60
Net Profit
-6.123%
Sharpe Ratio
-0.37
Sortino Ratio
-0.443
Probabilistic Sharpe Ratio
0.000%
Loss Rate
48%
Win Rate
52%
Profit-Loss Ratio
0.81
Alpha
-0.024
Beta
0.057
Annual Standard Deviation
0.058
Annual Variance
0.003
Information Ratio
-0.394
Tracking Error
0.161
Treynor Ratio
-0.378
Total Fees
$78.93
Estimated Strategy Capacity
$0
Lowest Capacity Asset
CME_AD1.QuantpediaFutures 2S
Portfolio Turnover
0.18%
#region imports
from AlgorithmImports import *
from dateutil.relativedelta import relativedelta
#endregion

# 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

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

# Custom fee model.
class CustomFeeModel(FeeModel):
    def GetOrderFee(self, parameters):
        fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
        return OrderFee(CashAmount(fee, "USD"))
# https://quantpedia.com/strategies/dollar-carry-trade/
#
# The investment universe consists of currencies from developed countries (the Euro area, Australia, Canada, Denmark, Japan, New Zealand, Norway, Sweden,
# Switzerland, and the United Kingdom). The average forward discount (AFD) is calculated for this basket of currencies (each currency has an equal weight).
# The average 3-month rate could be used instead of the AFD in the calculation. The AFD is then compared to the 3-month US Treasury rate. The investor
# goes long on the US dollar and goes short on the basket of currencies if the 3-month US Treasury rate is higher than the AFD. The investor goes short
# on the US dollar and long on the basket of currencies if the 3-month US Treasury rate is higher than the AFD. The portfolio is rebalanced monthly.

import data_tools
import numpy as np
from AlgorithmImports import *
from typing import Dict

class DollarCarryTrade(QCAlgorithm):

    def Initialize(self):
        self.SetStartDate(2000, 1, 1)
        self.SetCash(100000)
        
        self.leverage:int = 2

        # Source: https://fred.stlouisfed.org/series/IR3TIB01AUM156N
        self.symbols:Dict[str, str] = {
            "CME_AD1" : "IR3TIB01AUM156N",   # Australian Dollar Futures, Continuous Contract #1
            "CME_BP1" : "LIOR3MUKM",         # British Pound Futures, Continuous Contract #1
            "CME_CD1" : "IR3TIB01CAM156N",   # Canadian Dollar Futures, Continuous Contract #1
            "CME_EC1" : "IR3TIB01EZM156N",   # Euro FX Futures, Continuous Contract #1
            "CME_JY1" : "IR3TIB01JPM156N",   # Japanese Yen Futures, Continuous Contract #1
            "CME_MP1" : "IR3TIB01MXM156N",   # Mexican Peso Futures, Continuous Contract #1
            "CME_NE1" : "IR3TIB01NZM156N",   # New Zealand Dollar Futures, Continuous Contract #1
            "CME_SF1" : "IR3TIB01CHM156N"    # Swiss Franc Futures, Continuous Contract #1
        }
                                
        for symbol in self.symbols:
            data = self.AddData(data_tools.QuantpediaFutures, symbol, Resolution.Daily)
            data.SetFeeModel(data_tools.CustomFeeModel())
            data.SetLeverage(self.leverage)

            # Interbank rate data.
            cash_rate_symbol = self.symbols[symbol]
            self.AddData(data_tools.InterestRate3M, cash_rate_symbol, Resolution.Daily)
        
        self.treasury_rate:Symbol = self.AddData(data_tools.InterestRate3M, 'IR3TIB01USM156N', Resolution.Daily).Symbol
            
    def OnData(self, data:Slice) -> None:
        ir_last_update_date:Dict[str, datetime.date] = data_tools.InterestRate3M.get_last_update_date()
        qp_futures_last_update_date:Dict[str, datetime.date] = data_tools.QuantpediaFutures.get_last_update_date()
        
        fd:Dict[str, float] = {}
        for future_symbol, cash_rate_symbol in self.symbols.items():
            if self.Securities[cash_rate_symbol].GetLastData() and ir_last_update_date[cash_rate_symbol] > self.Time.date():
                if cash_rate_symbol in data and data[cash_rate_symbol]:
                    if qp_futures_last_update_date[future_symbol] > self.Time.date():
                        cash_rate:float = data[cash_rate_symbol].Value
                        
                        # Update cash rate only once a month.
                        fd[future_symbol] = cash_rate

        if len(fd) == 0:
            if self.Securities[self.treasury_rate].GetLastData() and ir_last_update_date[self.treasury_rate.Value] <= self.Time.date():
                self.Liquidate()
            return
    
        afd:float = np.mean([x[1] for x in fd.items()])

        treasuries_3m_rate:float = self.Securities[self.treasury_rate].Price

        count:int = len(self.symbols)
        if treasuries_3m_rate > afd:
            # Long on the US dollar and goes short on the basket of currencies.
            for symbol in self.symbols:
                if symbol in data and data[symbol]:
                    self.SetHoldings(symbol, -1 / count)
        else:
            # Short on the US dollar and long on the basket of currencies.
            for symbol in self.symbols:
                if symbol in data and data[symbol]:
                    self.SetHoldings(symbol, 1 / count)