Overall Statistics
Total Orders
1153
Average Win
1.41%
Average Loss
-0.33%
Compounding Annual Return
1111.463%
Drawdown
36.900%
Expectancy
1.357
Start Equity
3000
End Equity
25824.54
Net Profit
760.818%
Sharpe Ratio
7.899
Sortino Ratio
16.747
Probabilistic Sharpe Ratio
98.200%
Loss Rate
56%
Win Rate
44%
Profit-Loss Ratio
4.33
Alpha
0
Beta
0
Annual Standard Deviation
0.839
Annual Variance
0.703
Information Ratio
7.965
Tracking Error
0.839
Treynor Ratio
0
Total Fees
€3544.47
Estimated Strategy Capacity
€23000.00
Lowest Capacity Asset
AWX RBSIMWGA33VP
Portfolio Turnover
25.96%
# 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, LimitSectorWeightingPortfolioConstructionModel
# endregion

class ValueInvesting(QCAlgorithm):

    def initSettings(self):
        self.set_account_currency("EUR")
        self.InitCash = 3000 # Initial Starting Cash
        self.set_start_date(2024, 1, 1)
#        self.set_end_date(2023, 4, 1)
        self.fPVP = 0.1 # FreePortfolioValuePercentage
        self.risk = 0.9 # set maximum Risk for whole portfolio
        self.rebalancefrequency = 2 # Rebalance Portfolio every X Days
        self.maxPercentPerSector = 1 # limit exposure per sector
        self.leverage = 1.9
        self.maxPrice = self.get_parameter("maxPrice",3) # maximum price for Stock in Universe Selector
        self.minPrice = self.get_parameter("minPrice",1)
        self.maxStocks = 10000 # how many different stocks can be held at any time
        self.fscore_threshold = self.get_parameter("fscore_threshold", 7) # set the minimum F-Score a Stock must have

    def initialize(self):
        self.initSettings()
        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.MARGIN)
        # Custom security initializer
        self.set_security_initializer(CustomSecurityInitializer(self.brokerage_model, FuncSecuritySeeder(self.get_last_known_prices), DataNormalizationMode.Raw))
        ### Universe Settings ###
        self.universe_settings.resolution = Resolution.Second
        ###
        #self.universe_settings.leverage = 8
        # 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.minPrice, self.maxStocks))
        # Assume we want to just buy and hold the selected stocks, rebalance daily
        self.add_alpha(ConstantAlphaModel(InsightType.PRICE, InsightDirection.UP, timedelta(days=int(self.rebalancefrequency))))
        # Avoid overconcentration of risk in related stocks in the same sector, we invest the same size in every sector
        self.set_portfolio_construction(LimitSectorWeightingPortfolioConstructionModel(timedelta(days=self.rebalancefrequency),self.maxPercentPerSector, self.leverage))
        # set Risk Management
        self.add_risk_management(MaximumDrawdownPercentPortfolio(self.risk))
        # set minimum Order Margin
        self.settings.SetMinimumOrderMargin=0.1
        # set Free Portfolio Value in Percent (Money which shouldn't be used)
        #self.Settings.FreePortfolioValuePercentage = self.fPVP
        # set Execution Model
        self.set_execution(SpreadExecutionModel(0.009)) 
        









# region imports
from AlgorithmImports import *
# endregion


class CustomSecurityInitializer(BrokerageModelSecurityInitializer):
    '''Our custom initializer that will set the data normalization mode.
    We sub-class the BrokerageModelSecurityInitializer so we can also
    take advantage of the default model/leverage setting behaviors'''

    def __init__(self, brokerage_model, security_seeder, data_normalization_mode):
        '''Initializes a new instance of the CustomSecurityInitializer class with the specified normalization mode
        brokerage_model -- The brokerage model used to get fill/fee/slippage/settlement models
        security_seeder -- The security seeder to be used
        data_normalization_mode -- The desired data normalization mode'''
        self.base = BrokerageModelSecurityInitializer(brokerage_model, security_seeder)
        self.data_normalization_mode = data_normalization_mode

    def initialize(self, security):
        '''Initializes the specified security by setting up the models
        security -- The security to be initialized
        seed_security -- True to seed the security, false otherwise'''
        # first call the default implementation
        self.base.initialize(security)

        # now apply our data normalization mode
        security.set_data_normalization_mode(self.data_normalization_mode)
        security.set_slippage_model(VolumeShareSlippageModel())
        security.set_buying_power_model(CustomBuyingPowerModel())
        #security.set_buying_power_model(SecurityMarginModel(3.8))
        security.set_fill_model(ForwardDataOnlyFillModel())

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 
        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)


class ForwardDataOnlyFillModel(EquityFillModel):
    def fill(self, parameters: FillModelParameters):
        order_local_time = Extensions.convert_from_utc(parameters.order.time, parameters.security.exchange.time_zone)
        data = parameters.security.cache.get_data[QuoteBar]()
        if not data is None and order_local_time <= data.end_time:
            return super().fill(parameters)
        return Fill([])
from AlgorithmImports import *
from f_score import *
import math
from scipy.stats import norm
from datetime import timedelta

class FScoreUniverseSelectionModel(FineFundamentalUniverseSelectionModel):
    def __init__(self, algorithm, fscore_threshold, max_price=10000, min_price=1, max_stocks=10000):
        super().__init__(self.select_coarse, self.select_fine)
        self.algorithm = algorithm
        self.fscore_threshold = fscore_threshold
        self.max_price = max_price
        self.min_price = min_price
        self.max_stocks = max_stocks
        self.algorithm = algorithm
        self.kept_stocks_counter = {}

    def select_coarse(self, coarse):
        # Nur Aktien mit fundamentalen Daten und Preis > 1
        filtered = [x.Symbol for x in coarse if x.HasFundamentalData and x.Price > self.min_price and x.Price <= self.max_price and x.financial_statements.balance_sheet.net_tangible_assets.twelve_months > 0 and x.market_cap > 0]
#        filtered = [x.Symbol for x in filtered if x.financial_statements.balance_sheet.net_tangible_assets.twelve_months/x.market_cap >=1]
        return filtered

    def select_fine(self, fine):
        # Wir verwenden ein Dictionary, um den F-Score jeder Aktie zu speichern
        f_scores = {}
        f_scores_invested = {}
        fine = sorted(fine, key=lambda x: x.MarketCap, reverse=True)
        
        for f in fine:
            
            # Berechne den Piotroski F-Score der gegebenen Aktie
            if(self.get_piotroski_f_score(f) >= self.fscore_threshold):
                f_scores[f.symbol] = self.get_piotroski_f_score(f)
                
        
        # Filtere Aktien, die den F-Score-Schwellenwert überschreiten
        selected_stocks = [symbol for symbol, fscore in f_scores.items()][:self.max_stocks]
    
        return selected_stocks  # Rückgabe der Liste mit Optionen


    def get_piotroski_f_score(self, fine):
        # 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



class LimitSectorWeightingPortfolioConstructionModel(EqualWeightingPortfolioConstructionModel):
    '''Provides an implementation of IPortfolioConstructionModel that
   generates percent targets based on the CompanyReference.industry_template_code.
   The target percent holdings of each sector is 1/S where S is the number of sectors and
   the target percent holdings of each security is 1/N where N is the number of securities of each sector.
   For insights of direction InsightDirection.UP, long targets are returned and for insights of direction
   InsightDirection.DOWN, short targets are returned.
   It will ignore Insight for symbols that have no CompanyReference.industry_template_code'''

    def __init__(self, rebalance = Resolution.DAILY, maxPercent = 1, leverage = 1):
        '''Initialize a new instance of InsightWeightingPortfolioConstructionModel
        Args:
            rebalance: Rebalancing parameter. If it is a timedelta, date rules or Resolution, it will be converted into a function.
                              If None will be ignored.
                              The function returns the next expected rebalance time for a given algorithm UTC DateTime.
                              The function returns null if unknown, in which case the function will be called again in the
                              next loop. Returning current time will trigger rebalance.'''
        super().__init__(rebalance)
        self.sector_code_by_symbol = dict()
        self.maxPercent = maxPercent
        self.leverage = leverage

    def should_create_target_for_insight(self, insight):
        '''Method that will determine if the portfolio construction model should create a
        target for this insight
        Args:
            insight: The insight to create a target for'''
        return insight.symbol in self.sector_code_by_symbol

    def determine_target_percent(self, active_insights):
        '''Will determine the target percent for each insight
        Args:
            active_insights: The active insights to generate a target for'''
        result = dict()

        insight_by_sector_code = dict()

        for insight in active_insights:
            if insight.direction == InsightDirection.FLAT:
                result[insight] = 0
                continue

            sector_code = self.sector_code_by_symbol.get(insight.symbol)
            insights = insight_by_sector_code.pop(sector_code, list())

            insights.append(insight)
            insight_by_sector_code[sector_code] = insights

        # give equal weighting to each sector
        sector_percent = 0 if len(insight_by_sector_code) == 0 else self.leverage / len(insight_by_sector_code) if self.leverage / len(insight_by_sector_code) < self.leverage*self.maxPercent else self.leverage*self.maxPercent 

        for _, insights in insight_by_sector_code.items():
            # give equal weighting to each security
            count = len(insights)
            percent = 0 if count == 0 else sector_percent / count
            for insight in insights:
                result[insight] = insight.direction * percent

        return result

    def on_securities_changed(self, algorithm, changes):
        '''Event fired each time the we add/remove securities from the data feed
        Args:
            algorithm: The algorithm instance that experienced the change in securities
            changes: The security additions and removals from the algorithm'''
        for security in changes.removed_securities:
            # Removes the symbol from the self.sector_code_by_symbol dictionary
            # since we cannot emit PortfolioTarget for removed securities
            self.sector_code_by_symbol.pop(security.symbol, None)

        for security in changes.added_securities:
            sector_code = self.get_sector_code(security)
            if sector_code:
                self.sector_code_by_symbol[security.symbol] = sector_code

        super().on_securities_changed(algorithm, changes)

    def get_sector_code(self, security):
        '''Gets the sector code
        Args:
            security: The security to create a sector code for
        Returns:
            The value of the sector code for the security
        Remarks:
            Other sectors can be defined using AssetClassification'''
        fundamentals = security.fundamentals
        company_reference = security.fundamentals.company_reference if fundamentals else None
        return company_reference.industry_template_code if company_reference else None