Overall Statistics
Total Trades
138
Average Win
2.44%
Average Loss
-1.26%
Compounding Annual Return
1.428%
Drawdown
30.400%
Expectancy
0.515
Net Profit
40.917%
Sharpe Ratio
-0.182
Sortino Ratio
-0.205
Probabilistic Sharpe Ratio
0.000%
Loss Rate
48%
Win Rate
52%
Profit-Loss Ratio
1.94
Alpha
-0.012
Beta
0.045
Annual Standard Deviation
0.054
Annual Variance
0.003
Information Ratio
-0.319
Tracking Error
0.162
Treynor Ratio
-0.219
Total Fees
$53.51
Estimated Strategy Capacity
$0
Lowest Capacity Asset
CME_AD1.QuantpediaFutures 2S
Portfolio Turnover
0.10%
#region imports
from AlgorithmImports import *
from dateutil.relativedelta import relativedelta
#endregion
# 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

# source: https://data.oecd.org/conversion/purchasing-power-parities-ppp.htm
class PPPData(PythonData):
    _last_update_date:Dict[str, datetime.date] = {}

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

    def GetSource(self, config, date, isLiveMode):
        return SubscriptionDataSource(f'data.quantpedia.com/backtesting_data/economic/ppp/{config.Symbol.Value}.csv', SubscriptionTransportMedium.RemoteFile, FileFormat.Csv)

    def Reader(self, config, line, date, isLiveMode):
        data = PPPData()
        data.Symbol = config.Symbol
        if not line[0].isdigit(): return None
        split = line.split(';')
        
        # Parse the CSV file's columns into the custom data class
        data.Time = datetime.strptime(split[0], "%Y-%m-%d") + relativedelta(months=2)
        data.Value = float(split[1])

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

        if data.Time.date() > PPPData._last_update_date[config.Symbol.Value]:
            PPPData._last_update_date[config.Symbol.Value] = data.Time.date()
        
        return data
# https://quantpedia.com/strategies/currency-value-factor-ppp-strategy/
#
# Create an investment universe consisting of several currencies (10-20). Use the latest OECD Purchasing Power Parity figure to assess 
# the fair value of each currency versus USD in the month of publishing and then use monthly CPI changes and exchange rate changes to 
# create fair PPP value for the month prior to the current month. Go long three currencies that are the most undervalued (lowest PPP 
# fair value figure) and go short three currencies that are the most overvalued (highest PPP fair value figure). Invest cash not used
# as margin on overnight rates. Rebalance quarterly or monthly.
#
# QC implementation changes:
#   - Yearly rebalance instead of quarterly is performed.

import data_tools
from AlgorithmImports import *
from typing import Dict, List

class CurrencyValueFactorPPPStrategy(QCAlgorithm):

    def Initialize(self):
        self.SetStartDate(2000, 1, 1)
        self.SetCash(100000)
        
        self.leverage:int = 3
        self.traded_count:int = 3
        self.ppp_data:Dict[str, float] = {}

        # currency future symbol and PPP yearly symbol
        self.symbols:Dict[str, str] = {
            "CME_AD1" : "AUS_PPP", # Australian Dollar Futures, Continuous Contract #1
            "CME_BP1" : "GBR_PPP", # British Pound Futures, Continuous Contract #1
            "CME_CD1" : "CAD_PPP", # Canadian Dollar Futures, Continuous Contract #1
            "CME_EC1" : "DEU_PPP", # Euro FX Futures, Continuous Contract #1
            "CME_JY1" : "JPN_PPP", # Japanese Yen Futures, Continuous Contract #1
            "CME_NE1" : "NZL_PPP", # New Zealand Dollar Futures, Continuous Contract #1
            "CME_SF1" : "CHE_PPP"  # Swiss Franc Futures, Continuous Contract #1
        }

        for symbol, ppp_symbol in self.symbols.items():
            data = self.AddData(data_tools.QuantpediaFutures, symbol, Resolution.Daily)
            data.SetFeeModel(data_tools.CustomFeeModel())
            data.SetLeverage(self.leverage)
            
            # PPP quantpedia data
            self.AddData(data_tools.PPPData, ppp_symbol, Resolution.Daily)
        
        self.Settings.MinimumOrderMarginPortfolioPercentage = 0.
        self.recent_month:int = -1
        
    def OnData(self, data: Slice) -> None:
        futures_last_update_date:Dict[str, datetime.date] = data_tools.QuantpediaFutures.get_last_update_date()
        ppp_last_update_date:Dict[str, datetime.date] = data_tools.PPPData.get_last_update_date()

        for symbol, ppp_symbol in self.symbols.items():
            if ppp_symbol in data and data[ppp_symbol] and self.Time.date() < ppp_last_update_date[ppp_symbol]:
                self.ppp_data[symbol] = data[ppp_symbol].Value

        if self.recent_month == self.Time.month:
            return
        self.recent_month = self.Time.month
        
        # January rebalance
        if self.recent_month == 1:
            ppp:Dict[str, float] = {}
            for symbol, ppp_symbol in self.symbols.items():
                if self.Securities[symbol].GetLastData() and self.Time.date() > futures_last_update_date[symbol]:
                    self.Liquidate()
                    return

            long:List[str] = []
            short:List[str] = []

            if len(self.ppp_data) >= self.traded_count*2:
                # ppp sorting
                sorted_by_ppp:List[str] = sorted(self.ppp_data.items(), key = lambda x: x[1], reverse = True)
                long = [x[0] for x in sorted_by_ppp[-self.traded_count:]]
                short = [x[0] for x in sorted_by_ppp[:self.traded_count]]

                self.ppp_data.clear()
    
            # trade execution
            invested:List[str] = [x.Key.Value for x in self.Portfolio if x.Value.Invested]
            for symbol in invested:
                if symbol not in long + short:
                    self.Liquidate(symbol)
                    
            for i, portfolio in enumerate([long, short]):
                for symbol in portfolio:
                    if symbol in data and data[symbol]:
                        self.SetHoldings(symbol, ((-1) ** i) / len(portfolio))