Overall Statistics
Total Trades
4081
Average Win
0.60%
Average Loss
-0.53%
Compounding Annual Return
33.362%
Drawdown
36.500%
Expectancy
0.359
Net Profit
4068.121%
Sharpe Ratio
1.318
Probabilistic Sharpe Ratio
78.694%
Loss Rate
37%
Win Rate
63%
Profit-Loss Ratio
1.14
Alpha
0.168
Beta
0.755
Annual Standard Deviation
0.182
Annual Variance
0.033
Information Ratio
0.965
Tracking Error
0.149
Treynor Ratio
0.318
Total Fees
$7180.93
Estimated Strategy Capacity
$13000.00
Lowest Capacity Asset
AWX RBSIMWGA33VP
from AlgorithmImports import *
import math
from dateutil.relativedelta import relativedelta

class Value(QCAlgorithm):

    def Initialize(self):

        # add book to value vs performance chart
        # run analysis of that 60/40 portfolio vs this ->log output to csv and run local notebook


        # Variable to Optimizestart_year
        start_year = int(self.GetParameter("start_year")) # We grab our value from the Algorithm Parameters. GetParameter must be declared here for the optimization to work
        StartDate = datetime(start_year, 12, 1)
        EndDate = StartDate + relativedelta(years=2)
        self.SetStartDate(StartDate)
        self.SetEndDate(EndDate)

        # Hard Coded Dates
        self.SetStartDate(2010, 1, 1)
        self.SetEndDate(2022, 12, 12)
        self.cash = 100000


        # Create empty DataFrame to store portfolio value
        self.df = pd.DataFrame()

        self.symbol = self.AddEquity('SPY', Resolution.Daily).Symbol
        
        self.coarse_count = 3000
        
        self.long = []
        self.short = []

        self.hit_stop = []  # do not want to trade equities after they hit thier stop loss


        self.month = 12  # this is the month count -> this needs to be changed if rebalance is > 12 months


        # Variable to Optimize             
        months_between_rebalance = self.GetParameter("months_between_rebalance") # We grab our value from the Algorithm Parameters. GetParameter must be declared here for the optimization to work

        self.months_between_rebalance = int(months_between_rebalance) # this can be and int value -> test for optimization

        self.selection_flag = False
        self.UniverseSettings.Resolution = Resolution.Daily
        self.AddUniverse(self.CoarseSelectionFunction, self.FineSelectionFunction)
        
        # Rebalance on selective month ends
        self.Schedule.On(self.DateRules.MonthEnd(self.symbol), self.TimeRules.AfterMarketOpen(self.symbol), self.Selection)
        self.Schedule.On(self.DateRules.EveryDay(self.symbol), self.TimeRules.BeforeMarketClose(self.symbol, 15), self.CheckStopLimits) 
        self.Schedule.On(self.DateRules.EveryDay(self.symbol), self.TimeRules.BeforeMarketClose(self.symbol, 1), self.LogPL) 

    def LogPL(self):
        self.Log(self.Portfolio.TotalHoldingsValue)

    def OnSecuritiesChanged(self, changes):
        for security in changes.AddedSecurities:
            security.SetFeeModel(CustomFeeModel())            
            security.SetLeverage(5)

    def CoarseSelectionFunction(self, coarse):
        if not self.selection_flag:
            return Universe.Unchanged
        
        selected = [x.Symbol for x in coarse if x.HasFundamentalData and x.Market == 'usa']
        return selected
    
    def FineSelectionFunction(self, fine):

        sorted_by_market_cap = sorted([x for x in fine if x.ValuationRatios.PBRatio != 0 and \
                            ((x.SecurityReference.ExchangeId == "NYS") or (x.SecurityReference.ExchangeId == "NAS") or (x.SecurityReference.ExchangeId == "ASE") )],
                            key = lambda x:x.MarketCap, reverse=True)
                            
        bottom_by_market_cap = [x for x in sorted_by_market_cap[-self.coarse_count:]]

        sorted_by_pb = sorted(bottom_by_market_cap, key = lambda x:(x.ValuationRatios.PBRatio), reverse=False)
        
        # Variable to Optimize    
        total_universe_portion = self.GetParameter("total_universe_portion")
        universe = int(len(sorted_by_pb) / int(total_universe_portion))  # this needs to be tested and adjusted

        highBM = sorted_by_pb[:universe]

        df_raw = [x for x in highBM if \

            # Profitability Variables #
            # F_ROA
            x.FinancialStatements.IncomeStatement.NetIncome.Value != 0 and math.isnan(x.FinancialStatements.IncomeStatement.NetIncome.Value) is False and \
            # F_CFO
            x.FinancialStatements.IncomeStatement.TotalRevenue.Value != 0 and math.isnan(x.FinancialStatements.IncomeStatement.TotalRevenue.Value) is False and \
            # F_deltaROA
            x.FinancialStatements.IncomeStatement.NetIncome.ThreeMonths != 0 and math.isnan(x.FinancialStatements.IncomeStatement.NetIncome.ThreeMonths) is False and \
            x.FinancialStatements.IncomeStatement.NetIncome.TwelveMonths != 0 and math.isnan(x.FinancialStatements.IncomeStatement.NetIncome.TwelveMonths) is False and \
            # F_ACCRUAL -- Variables for calculation already checked above

            # Liquidity Variables #
            # F_deltaLEVER
            x.FinancialStatements.BalanceSheet.CurrentDebt.ThreeMonths != 0 and math.isnan(x.FinancialStatements.BalanceSheet.CurrentDebt.ThreeMonths) is False and \
            x.FinancialStatements.BalanceSheet.CurrentDebt.TwelveMonths != 0 and math.isnan(x.FinancialStatements.BalanceSheet.CurrentDebt.TwelveMonths) is False and \
            x.FinancialStatements.BalanceSheet.TotalAssets.ThreeMonths != 0 and math.isnan(x.FinancialStatements.BalanceSheet.TotalAssets.ThreeMonths) is False and \
            x.FinancialStatements.BalanceSheet.TotalAssets.TwelveMonths != 0 and math.isnan(x.FinancialStatements.BalanceSheet.TotalAssets.TwelveMonths) is False and \
            # F_deltaLIQUID
            x.OperationRatios.CurrentRatio.OneYear!= 0 and math.isnan(x.OperationRatios.CurrentRatio.OneYear) is False and \
            x.OperationRatios.CurrentRatio.ThreeMonths!= 0 and math.isnan(x.OperationRatios.CurrentRatio.ThreeMonths) is False and \
            # EQ_OFFER
            x.FinancialStatements.BalanceSheet.ShareIssued.TwelveMonths!= 0 and math.isnan(x.FinancialStatements.BalanceSheet.ShareIssued.TwelveMonths) is False and \
            x.FinancialStatements.BalanceSheet.ShareIssued.ThreeMonths!= 0 and math.isnan(x.FinancialStatements.BalanceSheet.ShareIssued.ThreeMonths) is False and \

            # Operating Variables #
            # F_deltaMargin
            x.OperationRatios.GrossMargin.OneYear!= 0 and math.isnan(x.OperationRatios.GrossMargin.OneYear) is False and \
            x.OperationRatios.GrossMargin.ThreeMonths!= 0 and math.isnan(x.OperationRatios.GrossMargin.ThreeMonths) is False and \
            # F_deltaTURN
            x.OperationRatios.AssetsTurnover.OneYear!= 0 and math.isnan(x.OperationRatios.AssetsTurnover.OneYear) is False and \
            x.OperationRatios.AssetsTurnover.ThreeMonths!= 0 and math.isnan(x.OperationRatios.AssetsTurnover.ThreeMonths) is False
        ]

        # self.Log(f"{len(df_raw)/len(highBM)} have usable data")

        self.long = [x for x in df_raw if \
            
            # Profitability Variables #
            # F_ROA
            x.FinancialStatements.IncomeStatement.NetIncome.Value > 0 and \
            # F_CFO
            x.FinancialStatements.IncomeStatement.TotalRevenue.Value > 0 and \
            # F_deltaROA
            x.FinancialStatements.IncomeStatement.NetIncome.ThreeMonths * 4 > x.FinancialStatements.IncomeStatement.NetIncome.TwelveMonths and \
            # F_ACCRUAL
            x.FinancialStatements.IncomeStatement.TotalRevenue.Value >  x.FinancialStatements.IncomeStatement.NetIncome.Value and \

            # Liquidity Variables #
            # F_deltaLEVER
            (x.FinancialStatements.BalanceSheet.CurrentDebt.ThreeMonths * 4 - x.FinancialStatements.BalanceSheet.CurrentDebt.TwelveMonths) / (x.FinancialStatements.BalanceSheet.TotalAssets.ThreeMonths * 4 / x.FinancialStatements.BalanceSheet.TotalAssets.TwelveMonths) > 0  and \
            # F_deltaLIQUID
            x.OperationRatios.CurrentRatio.ThreeMonths * 4 - x.OperationRatios.CurrentRatio.OneYear > 0 and \
            # EQ_OFFER
            x.FinancialStatements.BalanceSheet.ShareIssued.ThreeMonths * 4 - x.FinancialStatements.BalanceSheet.ShareIssued.TwelveMonths > 0 and \

            # Operating Variables #
            # F_deltaMargin
            x.OperationRatios.GrossMargin.ThreeMonths * 4 - x.OperationRatios.GrossMargin.OneYear > 0 and \
            # F_deltaTURN
            x.OperationRatios.AssetsTurnover.ThreeMonths * 4 - x.OperationRatios.AssetsTurnover.OneYear > 0 
        ]

        self.short = [x for x in df_raw if \
            
            # Profitability Variables #
            # F_ROA
            x.FinancialStatements.IncomeStatement.NetIncome.Value < 0 and \
            # F_CFO
            x.FinancialStatements.IncomeStatement.TotalRevenue.Value < 0 and \
            # F_deltaROA
            x.FinancialStatements.IncomeStatement.NetIncome.ThreeMonths * 4 < x.FinancialStatements.IncomeStatement.NetIncome.TwelveMonths and \
            # F_ACCRUAL
            x.FinancialStatements.IncomeStatement.TotalRevenue.Value < x.FinancialStatements.IncomeStatement.NetIncome.Value and \

            # Liquidity Variables #
            # F_deltaLEVER
            (x.FinancialStatements.BalanceSheet.CurrentDebt.ThreeMonths * 4 - x.FinancialStatements.BalanceSheet.CurrentDebt.TwelveMonths) / (x.FinancialStatements.BalanceSheet.TotalAssets.ThreeMonths * 4 / x.FinancialStatements.BalanceSheet.TotalAssets.TwelveMonths) < 0  and \
            # F_deltaLIQUID
            x.OperationRatios.CurrentRatio.ThreeMonths * 4 - x.OperationRatios.CurrentRatio.OneYear < 0 and \
            # EQ_OFFER
            x.FinancialStatements.BalanceSheet.ShareIssued.ThreeMonths * 4 - x.FinancialStatements.BalanceSheet.ShareIssued.TwelveMonths < 0 and \

            # Operating Variables #
            # F_deltaMargin
            x.OperationRatios.GrossMargin.ThreeMonths * 4 - x.OperationRatios.GrossMargin.OneYear < 0 and \
            # F_deltaTURN
            x.OperationRatios.AssetsTurnover.ThreeMonths * 4 - x.OperationRatios.AssetsTurnover.OneYear < 0 
        ]

        
        long = [i.Symbol for i in self.long] # add the symbols to a list we return
        # Log the filter result
        # self.Log(f"{len(long)/len(df_raw)} of the stock with data have been tagged as long")

        short = [i.Symbol for i in self.short] # add the symbols to a list we return
        # Log the filter result
        # self.Log(f"{len(short)/len(df_raw)} of the stock with data have been tagged as short")

        return  long + short
    
    def OnData(self, data):

        # check if its time to reballance
        if not self.selection_flag:
            return
        self.selection_flag = False

        # Get Current Positions
        stocks_invested = [x.Key for x in self.Portfolio if x.Value.Invested]
        
        # Trade out of unwanted positions
        for symbol in stocks_invested:
            
            # create two lists 
            short = [i.Symbol for i in self.short] # add the symbols to a list we return
            long = [i.Symbol for i in self.long] # add the symbols to a list we returnn
            
            if symbol not in long + short:  # need to cancel all open orders and close positions

                symbol_open_order_tickets = self.Transactions.GetOpenOrderTickets(symbol)
                for stale_ticket in symbol_open_order_tickets:
                    stale_ticket.Cancel()

                self.Liquidate(symbol) # close position *** THIS IS NOT IDEAL SINCE IT IS A MARKET ORDER

        targets = []
        # These long/short order generation shold be run in parallel if possible to reduce market exposure
        weight = 1 / (len(self.short) + len(self.long))

        if len(self.long) != 0: # check to make sure that the list is not empty 
            for ticker in self.long:
                if ticker not in self.hit_stop:
                    # self.Log(f"{ticker.Symbol} price when order placed: {ticker.Price}")
                    self.SetHoldings(ticker.Symbol, weight)

        if len(self.short) != 0: # check to make sure that the list is not empty 
            for symbol in self.short:
                if ticker not in self.hit_stop:
                    # self.Log(f"{ticker.Symbol} price when order placed: {ticker.Price}")
                    self.SetHoldings(ticker.Symbol, -weight)

        # clear the list containing the stocks we would like to trade
        self.long.clear()
        self.short.clear()

    def OnOrderEvent(self, orderEvent):
        # OrderDate, Type, OrderId, Status, Symbol, FillQuantity, Quantity, FillPrice
        # This is standardized so that we can download and analyze log file of orders
        # self.Log(f",Order,{orderEvent.OrderId},{orderEvent.Status},{orderEvent.Symbol},{orderEvent.FillQuantity},{orderEvent.Quantity},{orderEvent.FillPrice}")

        return

    def CheckStopLimits(self):
        # Get Current Positions
        stocks_invested = [x.Key for x in self.Portfolio if x.Value.Invested]

        if len(stocks_invested) != 0:
            for kvp in self.Portfolio:
                security = kvp.Value
                pnl = security.UnrealizedProfitPercent

                # Variable to Optimize             
                stop_loss_percent = float(self.GetParameter("stop_loss_percent")) # We grab our value from the Algorithm Parameters. GetParameter must be declared here for the optimization to work
                if pnl < -stop_loss_percent:
                    self.Liquidate(security.Symbol) # close position *** THIS IS NOT IDEAL SINCE IT IS A MARKET ORDER
                    self.hit_stop += [security.Symbol]
                    # self.Log(f"Liquidated {security.Symbol} due to {pnl} loss above {stop_loss_percent} LIMIT")

    
    def Selection(self):

        if self.month == self.months_between_rebalance:
            self.selection_flag = True
        
        self.month += 1
        if self.month > self.months_between_rebalance:
            self.month = 1 # reset count

# Custom fee model.
class CustomFeeModel(FeeModel):

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