Overall Statistics
Total Orders
13
Average Win
0%
Average Loss
-5.14%
Compounding Annual Return
-1.777%
Drawdown
52.100%
Expectancy
-1
Start Equity
100000
End Equity
88324.54
Net Profit
-11.675%
Sharpe Ratio
-0.198
Sortino Ratio
-0.244
Probabilistic Sharpe Ratio
0.066%
Loss Rate
100%
Win Rate
0%
Profit-Loss Ratio
0
Alpha
0.028
Beta
-0.667
Annual Standard Deviation
0.143
Annual Variance
0.021
Information Ratio
-0.397
Tracking Error
0.286
Treynor Ratio
0.043
Total Fees
$8.95
Estimated Strategy Capacity
$29000000.00
Lowest Capacity Asset
COTY VHDII80EJ8MD
Portfolio Turnover
0.08%
# https://quantpedia.com/strategies/shorting-goodwill/
#
# The investment universe consists of stocks listed on NYSE, AMEX, and NASDAQ. Every year, stocks are sorted into deciles based on their Return on Assets during the previous
# fiscal year. Stocks with the lowest ROA are then sorted into deciles based on their goodwill divided by total assets. The investor goes short on stocks with the highest 
# goodwill and lowest ROA and goes long on stocks with the same market cap. The portfolio is equally weighted and is rebalanced yearly.
#
# QC implementation changes:
#   - Universe consists of 3000 largest stocks traded on NYSE, AMEX, or NASDAQ.

from AlgorithmImports import *

class ShortingGoodwill(QCAlgorithm):

    def Initialize(self):
        # self.SetStartDate(2010, 1, 1)
        self.SetStartDate(2018, 1, 1)
        self.SetCash(100000)
        
        self.symbol:Symbol = self.AddEquity('SPY', Resolution.Daily).Symbol

        self.short:List[Symbol] = []
        
        # self.fundamental_count:int = 3000
        self.fundamental_count:int = 500
        self.fundamental_sorting_key = lambda x: x.MarketCap
        self.quantile:int = 10
        self.rebalance_month:int = 7
        # self.leverage:int = 5
        self.leverage:int = 1

        self.selection_flag:bool = True
        self.UniverseSettings.Resolution = Resolution.Daily
        self.AddUniverse(self.FundamentalSelectionFunction)
        self.Settings.MinimumOrderMarginPortfolioPercentage = 0.
        self.settings.daily_precise_end_time = False
        
        self.Schedule.On(self.DateRules.MonthStart(self.symbol), self.TimeRules.AfterMarketOpen(self.symbol), self.Selection)
        
    def OnSecuritiesChanged(self, changes: SecurityChanges) -> None:
        for security in changes.AddedSecurities:
            security.SetFeeModel(CustomFeeModel())
            security.SetLeverage(self.leverage)

    def FundamentalSelectionFunction(self, fundamental: List[Fundamental]) -> None:
        if not self.selection_flag:
            return Universe.Unchanged

        # selected:List[Fundamental] = [x for x in fundamental if x.HasFundamentalData and x.Market == 'usa' and \
        #     ((x.SecurityReference.ExchangeId == "NYS") or (x.SecurityReference.ExchangeId == "NAS") or (x.SecurityReference.ExchangeId == "ASE")) and \
        #     x.OperationRatios.ROA.OneYear != 0 and not np.isnan(x.OperationRatios.ROA.OneYear) and \
        #     x.FinancialStatements.BalanceSheet.Goodwill.TwelveMonths != 0 and not np.isnan(x.FinancialStatements.BalanceSheet.Goodwill.TwelveMonths) and \
        #     x.FinancialStatements.BalanceSheet.TotalAssets.TwelveMonths != 0 and not np.isnan(x.FinancialStatements.BalanceSheet.TotalAssets.TwelveMonths)
        #     ]
        
        selected:List[Fundamental] = [x for x in fundamental if x.HasFundamentalData and x.Market == 'usa' and \
            ((x.SecurityReference.ExchangeId == "NYS") or (x.SecurityReference.ExchangeId == "NAS") or (x.SecurityReference.ExchangeId == "ASE")) and \
            x.OperationRatios.ROA.OneYear != 0 and not np.isnan(x.OperationRatios.ROA.OneYear) and \
            x.FinancialStatements.BalanceSheet.Goodwill.TwelveMonths != 0 and not np.isnan(x.FinancialStatements.BalanceSheet.Goodwill.TwelveMonths) and \
            x.FinancialStatements.BalanceSheet.TotalAssets.TwelveMonths != 0 and not np.isnan(x.FinancialStatements.BalanceSheet.TotalAssets.TwelveMonths) and \
            x.dollar_volume > 10_000_000
            ]

        if len(selected) > self.fundamental_count:
            # # Select the 100 most liquid assets.
            # selected = sorted(selected, key=lambda x: x.dollar_volume, reverse=True)[:100]

            selected = [x for x in sorted(selected, key=self.fundamental_sorting_key, reverse=True)[:self.fundamental_count]]
            
        if len(selected) >= self.quantile * 2:
            quantile:int = len(selected) // self.quantile
            sorted_by_roa:List[Fundamental] = sorted(selected, key = lambda x: x.OperationRatios.ROA.OneYear, reverse=False)[:quantile]

            quantile:int = len(sorted_by_roa) // self.quantile
            top_by_goodwill:List[Fundamental] = sorted(sorted_by_roa, key = lambda x: x.FinancialStatements.BalanceSheet.Goodwill.TwelveMonths / x.FinancialStatements.BalanceSheet.TotalAssets.TwelveMonths, reverse=False)[-quantile:]
            self.short = list(map(lambda x: x.Symbol, top_by_goodwill))

        return self.short
            
    def OnData(self, data: Slice) -> None:
        if not self.selection_flag:
            return
        self.selection_flag = False
        
        # Trade execution.
        targets:List[PortfolioTarget] = [PortfolioTarget(symbol, -1 / len(self.short)) for symbol in self.short if symbol in data and data[symbol]]
        self.SetHoldings(targets, True)

        self.short.clear()

    def Selection(self) -> None:
        if self.Time.year == self.rebalance_month:
            self.selection_flag = True
        # self.selection_flag = True
        
# Custom fee model
class CustomFeeModel(FeeModel):
    def GetOrderFee(self, parameters):
        fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
        return OrderFee(CashAmount(fee, "USD"))
# region imports
from AlgorithmImports import *
# endregion

class ShortingGoodwill(QCAlgorithm):
    def initialize(self):
        self.set_start_date(2018, 1, 1)
        self.set_cash(100000)
        self.symbol = self.add_equity('SPY', Resolution.DAILY).Symbol
        self.short = []
        self.fundamental_count = 3000
        self.fundamental_sorting_key = lambda x: x.market_cap
        self.quantile = 10
        self.leverage = 1
        self.selection_flag = True
        self.universe_settings.resolution = Resolution.DAILY
        self.add_universe(self.fundamental_selection_function)
        self.settings.minimum_order_margin_portfolio_percentage = 0.
        self.settings.daily_precise_end_time = False
        self.schedule.on(self.date_rules.month_start(self.symbol), self.time_rules.after_market_open(self.symbol), self.selection)
    
    def on_securities_changed(self, changes: SecurityChanges) -> None:
        for security in changes.added_securities:
            security.set_fee_model(CustomFeeModel())
            security.set_leverage(self.leverage)
    
    def fundamental_selection_function(self, fundamental: List[Fundamental]) -> List[Symbol]:
        if not self.selection_flag:
            return Universe.UNCHANGED
        selected = [x for x in fundamental if x.has_fundamental_data and x.market == 'usa' and
                    ((x.security_reference.exchange_id == "NYS") or (x.security_reference.exchange_id == "NAS") or (x.security_reference.exchange_id == "ASE")) and
                    x.operation_ratios.ROA.one_year != 0 and not np.isnan(x.operation_ratios.ROA.one_year) and
                    x.financial_statements.balance_sheet.goodwill.twelve_months != 0 and not np.isnan(x.financial_statements.balance_sheet.goodwill.twelve_months) and
                    x.financial_statements.balance_sheet.total_assets.twelve_months != 0 and not np.isnan(x.financial_statements.balance_sheet.total_assets.twelve_months)
                    ]
        if len(selected) > self.fundamental_count:
            selected = [x for x in sorted(selected, key=self.fundamental_sorting_key, reverse=True)[:self.fundamental_count]]
        if len(selected) >= self.quantile * 2:
            quantile = len(selected) // self.quantile
            sorted_by_roa = sorted(selected, key=lambda x: x.operation_ratios.ROA.one_year, reverse=False)[:quantile]
            quantile = len(sorted_by_roa) // self.quantile
            top_by_goodwill = sorted(sorted_by_roa, key=lambda x: x.financial_statements.balance_sheet.goodwill.twelve_months / x.financial_statements.balance_sheet.total_assets.twelve_months, reverse=False)[-quantile:]
            self.short = list(map(lambda x: x.symbol, top_by_goodwill))
        return self.short
    
    def on_data(self, data: Slice) -> None:
        if not self.selection_flag:
            return
        self.selection_flag = False
        targets = [PortfolioTarget(symbol, -1 / len(self.short)) for symbol in self.short if symbol in data and data[symbol]]
        self.set_holdings(targets, True)
        self.short.clear()
    
    def selection(self) -> None:
        self.selection_flag = True

class CustomFeeModel(FeeModel):
    
    def get_order_fee(self, parameters):
        fee = parameters.Security.PRICE * parameters.Order.ABSOLUTE_QUANTITY * 0.00005
        return OrderFee(CashAmount(fee, "USD"))