Overall Statistics
Total Orders
1754
Average Win
0.21%
Average Loss
-0.18%
Compounding Annual Return
2.666%
Drawdown
32.000%
Expectancy
0.475
Start Equity
100000
End Equity
189343.22
Net Profit
89.343%
Sharpe Ratio
0.001
Sortino Ratio
0.001
Probabilistic Sharpe Ratio
0.001%
Loss Rate
32%
Win Rate
68%
Profit-Loss Ratio
1.19
Alpha
-0.01
Beta
0.234
Annual Standard Deviation
0.077
Annual Variance
0.006
Information Ratio
-0.3
Tracking Error
0.14
Treynor Ratio
0
Total Fees
$195.87
Estimated Strategy Capacity
$0
Lowest Capacity Asset
CME_MP1.QuantpediaFutures 2S
Portfolio Turnover
0.33%
#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: SubscriptionDataConfig, date: datetime, isLiveMode: bool) -> SubscriptionDataSource:
        return SubscriptionDataSource("data.quantpedia.com/backtesting_data/futures/{0}.csv".format(config.Symbol.Value), SubscriptionTransportMedium.RemoteFile, FileFormat.Csv)

    def Reader(self, config: SubscriptionDataConfig, line: str, date: datetime, isLiveMode: bool) -> BaseData:
        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

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

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/fx-carry-trade/
#
# Create an investment universe consisting of several currencies (10-20). Go long three currencies with the highest central bank prime rates and
# go short three currencies with the lowest central bank prime rates. The cash not used as the margin is invested in overnight rates. The strategy 
# is rebalanced monthly.
#
# QC implementation changes:
#   - Investment universe consisting of 8 currency futures.
#   - Most recent IR value is used for signal calculation.

#region imports
from AlgorithmImports import *
import data_tools
#endregion

class ForexCarryTrade(QCAlgorithm):

    def Initialize(self):
        self.SetStartDate(2000, 1, 1) 
        self.SetCash(100000)
        
        # Source: https://fred.stlouisfed.org/series/IR3TIB01AUM156N
        self.tickers: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
        }
        
        self.traded_count:int = 3
        self.leverage:int = 3

        for ticker, rate_symbol in self.tickers.items():
            self.AddData(data_tools.InterestRate3M, rate_symbol, Resolution.Daily)
            
            data = self.AddData(data_tools.QuantpediaFutures, ticker, Resolution.Daily)
            data.SetFeeModel(data_tools.CustomFeeModel())
            data.SetLeverage(self.leverage)
            
        self.recent_month = -1
        self.Settings.MinimumOrderMarginPortfolioPercentage = 0.

    def OnData(self, data: Slice) -> None:
        rebalance_flag:bool = False
        rate:Dict[Symbol, float] = {}

        ir_last_update_date:Dict[str, datetime.date] = data_tools.InterestRate3M.get_last_update_date()

        for ticker, int_rate in self.tickers.items():
            # futures data is present in the algorithm
            if ticker in data and data[ticker]:
                if self.recent_month != self.Time.month:
                    rebalance_flag = True
                    self.recent_month = self.Time.month

                if rebalance_flag:
                    # IR data is still comming in
                    if self.Securities[int_rate].GetLastData() and ir_last_update_date[int_rate] > self.Time.date():
                        # take last IR value
                        rate[self.Symbol(ticker)] = self.Securities[int_rate].Price

        if rebalance_flag:
            targets:List[PortfolioTarget] = []

            if len(rate) >= self.traded_count:
                # interbank rate sorting
                sorted_by_rate:List[Symbol] = sorted(rate, key = rate.get, reverse = True)
                long:List[Symbol] = sorted_by_rate[:self.traded_count]
                short:List[Symbol] = sorted_by_rate[-self.traded_count:]
                
                # order execution
                for i, portfolio in enumerate([long, short]):
                    for symbol in portfolio:
                        if symbol in data and data[symbol]:
                            targets.append(PortfolioTarget(symbol, ((-1) ** i) / len(portfolio)))
            
            self.SetHoldings(targets, True)