| 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