Overall Statistics
Total Orders
26
Average Win
10.18%
Average Loss
-10.78%
Compounding Annual Return
2.759%
Drawdown
47.700%
Expectancy
0.389
Start Equity
100000
End Equity
154283.34
Net Profit
54.283%
Sharpe Ratio
0.1
Sortino Ratio
0.104
Probabilistic Sharpe Ratio
0.006%
Loss Rate
29%
Win Rate
71%
Profit-Loss Ratio
0.95
Alpha
-0.01
Beta
0.345
Annual Standard Deviation
0.198
Annual Variance
0.039
Information Ratio
-0.315
Tracking Error
0.214
Treynor Ratio
0.058
Total Fees
$147.46
Estimated Strategy Capacity
$0
Lowest Capacity Asset
493.QuantpediaEquity 2S
Portfolio Turnover
0.39%
Drawdown Recovery
2622
# https://quantpedia.com/strategies/momentum-effect-in-anomalies-trading-systems/
#
# In each year, the trader searches through the universe of financial journals for implementable trading strategies. The investment universe, which consists of existing anomalies, 
# is then created. The investor then chooses the best performing anomaly for the last two years (based on his backtesting results of all published anomalies) and will trade it in
# the following year.
#
# QC implementation changes:
#   - Investment universe consists of Quantpedia's equity long-short anomalies.

#region imports
from AlgorithmImports import *
#endregion

class MomentumEffectinAnomaliesTradingSystems(QCAlgorithm):

    def Initialize(self):
        self.SetStartDate(2010, 1, 1)
        self.SetCash(100_000)

        # ids with backtest period end year
        self.backtest_to = {}
        
        # daily price data
        self.perf = {}
        self.period = 2 * 12 * 21
        self.SetWarmUp(self.period, Resolution.Daily)
        
        csv_string_file = self.Download('data.quantpedia.com/backtesting_data/equity/quantpedia_strategies/backtest_end_year.csv')
        lines = csv_string_file.split('\r\n')
        last_id = None
        for line in lines[1:]:
            split = line.split(';')
            id = str(split[0])
            backtest_to = int(split[1])
            
            # add quantpedia strategy data
            data = self.AddData(QuantpediaEquity, id, Resolution.Daily)
            data.SetLeverage(5)
            data.SetFeeModel(CustomFeeModel())
            
            self.backtest_to[id] = backtest_to
            self.perf[id] = self.ROC(id, self.period, Resolution.Daily)
            
            if not last_id:
                last_id = id
        
        self.recent_month = -1
        self.Settings.MinimumOrderMarginPortfolioPercentage = 0.
    
    def OnData(self, data):
        if self.IsWarmingUp:
            return
        if self.Time.month == self.recent_month:
            return
        self.recent_month = self.Time.month
        
        if self.Time.month != 1:
            return
        
        _last_update_date:Dict[str, datetime.date] = QuantpediaEquity.get_last_update_date()

        # calculate performance of those strategies, which were published last year and sooner
        performance = {
            x : self.perf[x].Current.Value for x in self.perf if \
            self.perf[x].IsReady and \
            self.backtest_to[x] < self.Time.year and \
            x in data and data[x] and \
            _last_update_date[x] > self.Time.date()
        }

        # performance sorting
        if len(performance) != 0:
            sorted_by_perf = sorted(performance.items(), key = lambda x: x[1], reverse = True)
            top_performer_id = sorted_by_perf[0][0]

            if not self.Portfolio[top_performer_id].Invested:
                self.Liquidate()

            self.SetHoldings(top_performer_id, 1)
            self._log_after_rebalance(msg='Rebalance')
        else:
            self._log_after_rebalance(msg='Liquidation')
            self.Liquidate()

    def _log_after_rebalance(self, msg: str, log_from_year: int = 2000) -> None:
        if self.time.year < log_from_year:
            return

        holdings: List[Tuple] = [(symbol, holdings) for symbol, holdings in self.portfolio.items() if holdings.invested]
        holdings_str: str = f'{len(holdings)} Holdings {self.time} - {msg} - '

        for symbol, holding in holdings:
            # holding_perc: float = holdings.absolute_holdings_value / self.portfolio.total_holdings_value
            holding_perc: float = holding.absolute_holdings_value / self.portfolio.total_portfolio_value * 100
            holdings_str += f' {symbol.value}:{holding_perc:2.2f}%'
                
        self.log(holdings_str)

# Quantpedia strategy equity curve data.
# NOTE: IMPORTANT: Data order must be ascending (datewise)
class QuantpediaEquity(PythonData):
    def GetSource(self, config, date, isLiveMode):
        return SubscriptionDataSource(
            "data.quantpedia.com/backtesting_data/equity/quantpedia_strategies/{0}.csv".format(config.Symbol.Value), SubscriptionTransportMedium.RemoteFile, FileFormat.Csv
        )

    _last_update_date:Dict[str, datetime.date] = {}

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

    def Reader(self, config, line, date, isLiveMode):
        data = QuantpediaEquity()
        data.Symbol = config.Symbol
        
        if not line[0].isdigit(): return None
        split = line.split(';')
        
        data.Time = datetime.strptime(split[0], "%Y-%m-%d") + timedelta(days=1)
        data['close'] = float(split[1])
        data.Value = float(split[1])
        
        # store last update date
        if config.Symbol.Value not in QuantpediaEquity._last_update_date:
            QuantpediaEquity._last_update_date[config.Symbol.Value] = datetime(1,1,1).date()

        if data.Time.date() > QuantpediaEquity._last_update_date[config.Symbol.Value]:
            QuantpediaEquity._last_update_date[config.Symbol.Value] = data.Time.date()
        
        return data
        
# Custom fee model.
class CustomFeeModel(FeeModel):
    def GetOrderFee(self, parameters):
        fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
        return OrderFee(CashAmount(fee, "USD"))