Overall Statistics
Total Orders
7162
Average Win
0.09%
Average Loss
-0.08%
Compounding Annual Return
24.980%
Drawdown
23.800%
Expectancy
0.382
Start Equity
10000000
End Equity
30307155.48
Net Profit
203.072%
Sharpe Ratio
0.921
Sortino Ratio
1.161
Probabilistic Sharpe Ratio
55.869%
Loss Rate
38%
Win Rate
62%
Profit-Loss Ratio
1.21
Alpha
0
Beta
0
Annual Standard Deviation
0.156
Annual Variance
0.024
Information Ratio
1.145
Tracking Error
0.156
Treynor Ratio
0
Total Fees
$626913.00
Estimated Strategy Capacity
$34000.00
Lowest Capacity Asset
SAND V992QGD9T5YD
Portfolio Turnover
3.42%
Drawdown Recovery
782
# region imports
from AlgorithmImports import *
from security_initializer import CustomSecurityInitializer
from universe import FScoreUniverseSelectionModel
# endregion

class PiotroskiFScoreInvesting(QCAlgorithm):

    def initialize(self):
        self.set_end_date(datetime.now())
        self.set_start_date(self.end_date - timedelta(5*365))
        self.set_cash(10000000)  # Set Strategy Cash

        ### Parameters ###
        # The Piotroski F-Score threshold we would like to invest into stocks with F-Score >= of that
        fscore_threshold = self.get_parameter("fscore_threshold", 7)

        ### Reality Modeling ###
        # Interactive Broker Brokerage fees and margin
        self.set_brokerage_model(BrokerageName.INTERACTIVE_BROKERS_BROKERAGE, AccountType.MARGIN)
        # Custom security initializer
        self.set_security_initializer(CustomSecurityInitializer(self))

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

        # Our universe is selected by Piotroski's F-Score
        self.add_universe_selection(FScoreUniverseSelectionModel(fscore_threshold))
        # Assume we want to just buy and hold the selected stocks, rebalance daily
        self.add_alpha(ConstantAlphaModel(InsightType.PRICE, InsightDirection.UP, timedelta(1)))
        # Avoid overconcentration of risk in related stocks in the same sector, we invest the same size in every sector
        self.set_portfolio_construction(SectorWeightingPortfolioConstructionModel())
        # Avoid placing orders with big bid-ask spread to reduce friction cost
        self.set_execution(SpreadExecutionModel(0.01))       # maximum 1% spread allowed
        # Assume we do not have any risk management measures
        self.add_risk_management(NullRiskManagementModel())

    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
        if self.live_mode:
            self.log(changes)
# region imports
from AlgorithmImports import *
# endregion

class CustomSecurityInitializer(BrokerageModelSecurityInitializer):

    def __init__(self, algorithm: QCAlgorithm) -> None:
        security_seeder = FuncSecuritySeeder(lambda symbol: algorithm.history[TradeBar](symbol, 3, Resolution.DAILY))
        super().__init__(algorithm.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())
# QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
# Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from AlgorithmImports import *
from Selection.FundamentalUniverseSelectionModel import FundamentalUniverseSelectionModel

class FScoreUniverseSelectionModel(FundamentalUniverseSelectionModel):

    def __init__(self, fscore_threshold: float = 6):
        super().__init__()
        self.fscore_threshold = fscore_threshold

    def select(self, algorithm: QCAlgorithm, fundamental: Iterable[Fundamental]) -> Iterable[Symbol]:
        fine = [x for x in fundamental if x.price > 1]
        
        # We use a dictionary to hold the F-Score of each stock
        f_scores = {}

        for f in fine:
            # Calculate the Piotroski F-Score of the given stock
            f_scores[f.symbol] = self.get_piotroski_f_score(f)

        # Select the stocks with F-Score higher than the threshold
        return [symbol for symbol, fscore in f_scores.items() if fscore >= self.fscore_threshold]

    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 += self.get_roa_score(fine)
        fscore += self.get_operating_cash_flow_score(fine)
        fscore += self.get_roa_change_score(fine)
        fscore += self.get_accruals_score(fine)
        fscore += self.get_leverage_score(fine)
        fscore += self.get_liquidity_score(fine)
        fscore += self.get_share_issued_score(fine)
        fscore += self.get_gross_margin_score(fine)
        fscore += self.get_asset_turnover_score(fine)
        return fscore
    
    def get_roa_score(self, fine: Fundamental) -> int:
        '''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(self, fine: Fundamental) -> int:
        '''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_roa_change_score(self, fine: Fundamental) -> int:
        '''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(self, fine: Fundamental) -> int:
        '''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(self, fine: Fundamental) -> int:
        '''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(self, fine: Fundamental) -> int:
        '''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(self, fine: Fundamental) -> int:
        '''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(self, fine: Fundamental) -> int:
        '''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(self, fine: Fundamental) -> int:
        '''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 4*asset_turnover.three_months > asset_turnover.one_year else 0
        return score