Overall Statistics
Total Orders
218
Average Win
0.74%
Average Loss
-0.56%
Compounding Annual Return
13.316%
Drawdown
8.500%
Expectancy
0.611
Start Equity
100000
End Equity
145501.93
Net Profit
45.502%
Sharpe Ratio
1.024
Sortino Ratio
1.193
Probabilistic Sharpe Ratio
64.438%
Loss Rate
30%
Win Rate
70%
Profit-Loss Ratio
1.32
Alpha
0
Beta
0
Annual Standard Deviation
0.075
Annual Variance
0.006
Information Ratio
1.232
Tracking Error
0.075
Treynor Ratio
0
Total Fees
$288.00
Estimated Strategy Capacity
$1400000.00
Lowest Capacity Asset
SBC R735QTJ8XC9X
Portfolio Turnover
1.93%
# region imports
from AlgorithmImports import *
# endregion
def get_r_o_a_score(fine):
    '''Get the Profitability - Return of Asset sub-score of Piotroski F-Score
    Arg:
        fine: Fine fundamental object of a stock
    Return:
        Profitability - Return of Asset sub-score'''
    # Nearest ROA as current year data
    roa = fine.operation_ratios.ROA.three_months
    # 1 score if ROA datum exists and positive, else 0
    score = 1 if roa and roa > 0 else 0
    return score

def get_operating_cash_flow_score(fine):
    '''Get the Profitability - Operating Cash Flow sub-score of Piotroski F-Score
    Arg:
        fine: Fine fundamental object of a stock
    Return:
        Profitability - Operating Cash Flow sub-score'''
    # Nearest Operating Cash Flow as current year data
    operating_cashflow = fine.financial_statements.cash_flow_statement.cash_flow_from_continuing_operating_activities.three_months
    # 1 score if operating cash flow datum exists and positive, else 0
    score = 1 if operating_cashflow and operating_cashflow > 0 else 0
    return score

def get_r_o_a_change_score(fine):
    '''Get the Profitability - Change in Return of Assets sub-score of Piotroski F-Score
    Arg:
        fine: Fine fundamental object of a stock
    Return:
        Profitability - Change in Return of Assets sub-score'''
    # if current or previous year's ROA data does not exist, return 0 score
    roa = fine.operation_ratios.ROA
    if not roa.three_months or not roa.one_year:
        return 0

    # 1 score if change in ROA positive, else 0 score
    score = 1 if roa.three_months > roa.one_year else 0
    return score

def get_accruals_score(fine):
    '''Get the Profitability - Accruals sub-score of Piotroski F-Score
    Arg:
        fine: Fine fundamental object of a stock
    Return:
        Profitability - Accruals sub-score'''
    # Nearest Operating Cash Flow, Total Assets, ROA as current year data
    operating_cashflow = fine.financial_statements.cash_flow_statement.cash_flow_from_continuing_operating_activities.three_months
    total_assets = fine.financial_statements.balance_sheet.total_assets.three_months
    roa = fine.operation_ratios.ROA.three_months
    # 1 score if operating cash flow, total assets and ROA exists, and operating cash flow / total assets > ROA, else 0
    score = 1 if operating_cashflow and total_assets and roa and operating_cashflow / total_assets > roa else 0
    return score

def get_leverage_score(fine):
    '''Get the Leverage, Liquidity and Source of Funds - Change in Leverage sub-score of Piotroski F-Score
    Arg:
        fine: Fine fundamental object of a stock
    Return:
        Leverage, Liquidity and Source of Funds - Change in Leverage sub-score'''
    # if current or previous year's long term debt to equity ratio data does not exist, return 0 score
    long_term_debt_ratio = fine.operation_ratios.long_term_debt_equity_ratio
    if not long_term_debt_ratio.three_months or not long_term_debt_ratio.one_year:
        return 0

    # 1 score if long term debt ratio is lower in the current year, else 0 score
    score = 1 if long_term_debt_ratio.three_months < long_term_debt_ratio.one_year else 0
    return score

def get_liquidity_score(fine):
    '''Get the Leverage, Liquidity and Source of Funds - Change in Liquidity sub-score of Piotroski F-Score
    Arg:
        fine: Fine fundamental object of a stock
    Return:
        Leverage, Liquidity and Source of Funds - Change in Liquidity sub-score'''
    # if current or previous year's current ratio data does not exist, return 0 score
    current_ratio = fine.operation_ratios.current_ratio
    if not current_ratio.three_months or not current_ratio.one_year:
        return 0

    # 1 score if current ratio is higher in the current year, else 0 score
    score = 1 if current_ratio.three_months > current_ratio.one_year else 0
    return score

def get_share_issued_score(fine):
    '''Get the Leverage, Liquidity and Source of Funds - Change in Number of Shares sub-score of Piotroski F-Score
    Arg:
        fine: Fine fundamental object of a stock
    Return:
        Leverage, Liquidity and Source of Funds - Change in Number of Shares sub-score'''
    # if current or previous year's issued shares data does not exist, return 0 score
    shares_issued = fine.financial_statements.balance_sheet.share_issued
    if not shares_issued.three_months or not shares_issued.twelve_months:
        return 0

    # 1 score if shares issued did not increase in the current year, else 0 score
    score = 1 if shares_issued.three_months <= shares_issued.twelve_months else 0
    return score

def get_gross_margin_score(fine):
    '''Get the Leverage, Liquidity and Source of Funds - Change in Gross Margin sub-score of Piotroski F-Score
    Arg:
        fine: Fine fundamental object of a stock
    Return:
        Leverage, Liquidity and Source of Funds - Change in Gross Margin sub-score'''
    # if current or previous year's gross margin data does not exist, return 0 score
    gross_margin = fine.operation_ratios.gross_margin
    if not gross_margin.three_months or not gross_margin.one_year:
        return 0

    # 1 score if gross margin is higher in the current year, else 0 score
    score = 1 if gross_margin.three_months > gross_margin.one_year else 0
    return score

def get_asset_turnover_score(fine):
    '''Get the Leverage, Liquidity and Source of Funds - Change in Asset Turnover Ratio sub-score of Piotroski F-Score
    Arg:
        fine: Fine fundamental object of a stock
    Return:
        Leverage, Liquidity and Source of Funds - Change in Asset Turnover Ratio sub-score'''
    # if current or previous year's asset turnover data does not exist, return 0 score
    asset_turnover = fine.operation_ratios.assets_turnover
    if not asset_turnover.three_months or not asset_turnover.one_year:
        return 0

    # 1 score if asset turnover is higher in the current year, else 0 score
    score = 1 if asset_turnover.three_months > asset_turnover.one_year else 0
    return score
# region imports
from AlgorithmImports import *
from security_initializer import *
from universe import FScoreUniverseSelectionModel
# endregion

class PensiveFluorescentYellowParrot(QCAlgorithm):

    def initSettings(self):
        self.InitCash = 100000 # Initial Starting Cash
        self.set_start_date(2020, 7, 1)
        self.set_end_date(2023, 7, 1)
        self.contribution_amount = 0 # Monthly Savings Deposit, set to 0 to only invest once
        self.WithDrawWhenXTimesWorthTheInvest = 0 # pull out all contributed invested cash when profit is X Times the Invest, set to 0 tu turn off behaviour
        self.doTaxes = 0 # Tax Calculation for Austria, set 0 to turn off
        self.taxPercent = 0.275 # Austrian Tax for Income through Stocks (Kapitalertragssteuer)
        self.fPVP = 0.3 # FreePortfolioValuePercentage
        self.risk = 0.2 # set maximum Risk fir whole portfolio
        self.rebalancefrequency = 7 # Rebalance Portfolio every X Days
        self.maxPrice = 5000 # maximum price for Stock in Universe Selector
        self.maxStocks = 5 # how many different stocks can be held at any time
        self.fscore_threshold = 7 # set the minimum F-Score a Stock must have 


    def initZeros(self):
        self.yesterday_total_profit = 0 
        self.yesterday_total_fees = 0
        self.Taxes = 0
        self.TaxToPay = 0
        self.Wins = 0
        self.Losses = 0
        self.Withdrawn = 0
        self.WithDrawFlag = 0
        self.profits = {}

    def initSchedules(self):
        self.Schedule.On(self.date_rules.year_end(), self.TimeRules.At(0,0,5), 
            self.TaxPayDay)
        self.Schedule.On(self.DateRules.MonthStart(), self.TimeRules.At(12,0,0), 
            self.contribute)

    def initialize(self):
        self.initSettings()
        self.initZeros()
        self.initSchedules()
        self.StartCash = self.InitCash
        self.SetCash(self.InitCash)        

        ### Parameters ###
        # The Piotroski F-Score threshold we would like to invest into stocks with F-Score >= of that
        fscore_threshold = self.fscore_threshold
        ### Reality Modeling ###
        # Interactive Broker Brokerage fees and margin
        self.set_brokerage_model(BrokerageName.INTERACTIVE_BROKERS_BROKERAGE, AccountType.CASH)
        # Custom security initializer
        self.set_security_initializer(CustomSecurityInitializer(self.brokerage_model, FuncSecuritySeeder(self.get_last_known_prices)))

        ### Universe Settings ###
        self.universe_settings.resolution = Resolution.Minute

        # Our universe is selected by Piotroski's F-Score and the max price which a stock can be and how much stocks should be maximum in Portfolio
        self.add_universe_selection(FScoreUniverseSelectionModel(self, fscore_threshold, self.maxPrice, self.maxStocks))
        # Assume we want to just buy and hold the selected stocks, rebalance daily
        self.add_alpha(ConstantAlphaModel(InsightType.PRICE, InsightDirection.UP, timedelta(self.rebalancefrequency)))
        #self.add_alpha(EmaCrossAlphaModel())

        # Avoid overconcentration of risk in related stocks in the same sector, we invest the same size in every sector
        self.set_portfolio_construction(EqualWeightingPortfolioConstructionModel())
        # Avoid placing orders with big bid-ask spread to reduce friction cost
        self.set_execution(SpreadExecutionModel(0.01))       # maximum 1% spread allowed
        # set Trailing Stop Risk Management
        self.add_risk_management(MaximumDrawdownPercentPortfolio(self.risk))
        self.settings.SetMinimumOrderMargin=0.1
        self.Settings.FreePortfolioValuePercentage = self.fPVP

    def on_securities_changed(self, changes):
        # Log the universe changes to test the universe selection model
        # In this case, the added security should be the same as the logged stocks with F-score >= 7
        self.log(changes)
    
    def OnEndOfDay(self):

        if self.WithDrawWhenXTimesWorthTheInvest:
            self.Plot("Strategy Equity", "Deposited Cash", self.InitCash) # plot sum of deposited cash
        if self.doTaxes: 
            self.Plot("Taxable Profit", "profit", self.Wins) #plot accumulated profit
            self.Plot("Losses for Taxcalc", "loss", self.Losses) #plot accumulated loss
            self.Plot("accum. paid taxes", "tax", self.Taxes) #plot accumulated paid taxes
            self.Plot("tax pay", "tax", self.TaxToPay) #plot tax which is paid
            if self.TaxToPay >=0:
                self.TaxtoPay = 0

    def TaxPayDay(self): # routine to pay yearly taxes, only suitable for austrian tax law
        if self.doTaxes:
            tax_to_pay = (self.Wins + self.Losses)*self.taxPercent

            if(tax_to_pay >=0):
                self.liquidate()
                self.Portfolio.SetCash(self.Portfolio.CashBook[self.AccountCurrency].Amount - tax_to_pay)
                self.Taxes += tax_to_pay
                self.TaxToPay = tax_to_pay

            self.Wins = 0
            self.Losses = 0

    def OnOrderEvent(self, orderEvent):
        if orderEvent.Status == OrderStatus.Filled:
            profit = self.Portfolio[orderEvent.Symbol].LastTradeProfit

            if orderEvent.Symbol not in self.profits:
                    self.profits[orderEvent.Symbol] = RollingWindow[float](10)
                    self.profits[orderEvent.Symbol].Add(profit)

            if self.doTaxes:
                if orderEvent.Symbol in self.profits:
                    if self.profits[orderEvent.Symbol].Count == 0 or profit != self.profits[orderEvent.Symbol][0]:
                        self.profits[orderEvent.Symbol].Add(profit)
                        if(self.profits[orderEvent.Symbol][0]>=0):
                            self.Wins += self.profits[orderEvent.Symbol][0]
                        if(self.profits[orderEvent.Symbol][0]<0):
                            self.Losses += self.profits[orderEvent.Symbol][0]

    def contribute(self):
        if not self.WithDrawWhenXTimesWorthTheInvest == 0 and not self.WithDrawFlag: #contribute monthly if the contribution hasn't been pulled out
            self.InitCash += self.contribution_amount
            self.Portfolio.SetCash(self.Portfolio.CashBook[self.AccountCurrency].Amount + self.contribution_amount)
        if not self.WithDrawWhenXTimesWorthTheInvest== 0 and self.Portfolio.total_portfolio_value >= self.WithDrawWhenXTimesWorthTheInvest*self.InitCash and not self.WithDrawFlag: # stop contributing and pull out all initial invested money if the worth is X times the invest
            self.liquidate()
            self.Portfolio.SetCash(self.Portfolio.CashBook[self.AccountCurrency].Amount - self.InitCash)
            self.InitCash = 0
            self.WithDrawFlag = 1


# region imports
from AlgorithmImports import *
# endregion

class CustomSecurityInitializer(BrokerageModelSecurityInitializer):

    def __init__(self, brokerage_model: IBrokerageModel, security_seeder: ISecuritySeeder) -> None:
        super().__init__(brokerage_model, security_seeder)

    def initialize(self, security: Security) -> None:
        # First, call the superclass definition
        # This method sets the reality models of each security using the default reality models of the brokerage model
        super().initialize(security)
        
        # We want a slippage model with price impact by order size for reality modeling
        security.set_slippage_model(VolumeShareSlippageModel())
        security.set_buying_power_model(CustomBuyingPowerModel())

class CustomBuyingPowerModel(BuyingPowerModel):
    def get_maximum_order_quantity_for_target_buying_power(self, parameters):
        quantity = super().get_maximum_order_quantity_for_target_buying_power(parameters).quantity
        quantity = np.floor(quantity / 105) * 100
        return GetMaximumOrderQuantityResult(quantity)

    def has_sufficient_buying_power_for_order(self, parameters):
        return HasSufficientBuyingPowerForOrderResult(True)

    # Let's always return 0 as the maintenance margin so we avoid margin call orders
    def get_maintenance_margin(self, parameters):
        return MaintenanceMargin(0)

    # Override this as well because the base implementation calls GetMaintenanceMargin (overridden)
    # because in C# it wouldn't resolve the overridden Python method
    def get_reserved_buying_power_for_position(self, parameters):
        return parameters.result_in_account_currency(0)
# region imports
from AlgorithmImports import *
from f_score import *
# endregion

class FScoreUniverseSelectionModel(FineFundamentalUniverseSelectionModel):

    def __init__(self, algorithm, fscore_threshold, max_price = 10000, max_stocks = 0):
        super().__init__(self.select_coarse, self.select_fine)
        self.algorithm = algorithm
        self.fscore_threshold = fscore_threshold
        self.max_price = max_price
        self.max_stocks = max_stocks

    def select_coarse(self, coarse):
        '''Defines the coarse fundamental selection function.
        Args:
            algorithm: The algorithm instance
            coarse: The coarse fundamental data used to perform filtering
        Returns:
            An enumerable of symbols passing the filter'''
        # We only want stocks with fundamental data and price > $1
        filtered = [x.symbol for x in coarse if x.has_fundamental_data and x.price > 1 and x.price <= self.max_price]
        return filtered

    def select_fine(self, fine):
        '''Defines the fine fundamental selection function.
        Args:
            algorithm: The algorithm instance
            fine: The fine fundamental data used to perform filtering
        Returns:
            An enumerable of symbols passing the filter'''
        # We use a dictionary to hold the F-Score of each stock
        f_scores = {}

        fine = sorted([symbol for symbol in fine], key=lambda x: x.market_cap, reverse=True)

        for f in fine:
            # Calculate the Piotroski F-Score of the given stock
            f_scores[f.symbol] = self.get_piotroski_f_score(f)
            if f_scores[f.symbol] >= self.fscore_threshold:
                self.algorithm.log(f"Stock: {f.symbol.id} :: F-Score: {f_scores[f.symbol]}")


        selected = [symbol for symbol, fscore in f_scores.items() if fscore >= self.fscore_threshold][:self.max_stocks]

        # Select the stocks with F-Score higher than the threshold

        return selected

    def get_piotroski_f_score(self, fine):
        '''A helper function to calculate the Piotroski F-Score of a stock
        Arg:
            fine: MorningStar fine fundamental data of the stock
        return:
            the Piotroski F-Score of the stock
        '''
        # initial F-Score as 0
        fscore = 0
        # Add up the sub-scores in different aspects
        fscore += get_r_o_a_score(fine)
        fscore += get_operating_cash_flow_score(fine)
        fscore += get_r_o_a_change_score(fine)
        fscore += get_accruals_score(fine)
        fscore += get_leverage_score(fine)
        fscore += get_liquidity_score(fine)
        fscore += get_share_issued_score(fine)
        fscore += get_gross_margin_score(fine)
        fscore += get_asset_turnover_score(fine)
        return fscore