| Overall Statistics |
|
Total Orders
115739
Average Win
0.06%
Average Loss
-0.04%
Compounding Annual Return
2.689%
Drawdown
62.200%
Expectancy
0.037
Start Equity
100000
End Equity
196418.83
Net Profit
96.419%
Sharpe Ratio
0.06
Sortino Ratio
0.077
Probabilistic Sharpe Ratio
0.000%
Loss Rate
55%
Win Rate
45%
Profit-Loss Ratio
1.30
Alpha
0.03
Beta
-0.463
Annual Standard Deviation
0.169
Annual Variance
0.029
Information Ratio
-0.115
Tracking Error
0.279
Treynor Ratio
-0.022
Total Fees
$10608.27
Estimated Strategy Capacity
$19000000.00
Lowest Capacity Asset
MOG.B VCY032R250MD
Portfolio Turnover
7.95%
|
from AlgorithmImports import *
class StockData():
def __init__(self):
self.ROA = 0
self.Leverage = 0
self.Liquidity = 0
self.Equity_offering = 0
self.Gross_margin = 0
self.Turnover = 0
def Update(self, ROA, leverage, liquidity, eq_offering, gross_margin, turnover):
self.ROA = ROA
self.Leverage = leverage
self.Liquidity = liquidity
self.Equity_offering = eq_offering
self.Gross_margin = gross_margin
self.Turnover = turnover
class SymbolData():
def __init__(self, symbol, period):
self.Symbol = symbol
self.Price = RollingWindow[float](period)
def update(self, value):
self.Price.Add(value)
def is_ready(self) -> bool:
return self.Price.IsReady
def performance(self, values_to_skip = 0) -> float:
closes = [x for x in self.Price][values_to_skip:]
return (closes[0] / closes[-1] - 1)
# Custom fee model.
class CustomFeeModel(FeeModel):
def GetOrderFee(self, parameters):
fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
return OrderFee(CashAmount(fee, "USD"))
# https://quantpedia.com/strategies/combining-fundamental-fscore-and-equity-short-term-reversals/
#
# The investment universe consists of common stocks (share code 10 or 11) listed in NYSE, AMEX, and NASDAQ exchanges.
# Stocks with prices less than $5 at the end of the formation period are excluded.
# The range of FSCORE is from zero to nine points. Each signal is equal to one (zero) point if the signal indicates a positive
# (negative) financial performance. A firm scores one point if it has realized a positive return-on-assets (ROA), a positive
# cash flow from operations, a positive change in ROA, a positive difference between net income from operations (Accrual),
# a decrease in the ratio of long-term debt to total assets, a positive change in the current ratio, no-issuance of new common
# equity, a positive change in gross margin ratio and lastly a positive change in asset turnover ratio. Firstly, construct a quarterly
# FSCORE using the most recently available quarterly financial statement information.
# Monthly reversal data are matched each month with a most recently available quarterly FSCORE. The firm is classified as a fundamentally
# strong firm if the firm’s FSCORE is greater than or equal to seven (7-9), fundamentally middle firm (4-6) and fundamentally weak firm (0-3).
# Secondly, identify the large stocks subset – those in the top 40% of all sample stocks in terms of market capitalization
# at the end of formation month t. After that, stocks are sorted on the past 1-month returns and firm’s most recently available quarterly FSCORE.
# Take a long position in past losers with favorable fundamentals (7-9) and simultaneously a short position in past winners with unfavorable
# fundamentals (0-3). The strategy is equally weighted and rebalanced monthly.
#
# QC implementation changes:
# - Instead of all listed stock, we select 3000 largest stocks traded on NYSE, AMEX, or NASDAQ.
from AlgorithmImports import *
from typing import List, Dict
from numpy import floor, isnan
from functools import reduce
from pandas.core.frame import DataFrame
import data_tools
class CombiningFSCOREShortTermReversals(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2000, 1, 1)
self.SetCash(100000)
self.exchange_codes:List[str] = ['NYS', 'NAS', 'ASE']
self.financial_statement_names:List[str] = [
'EarningReports.BasicAverageShares.ThreeMonths',
'EarningReports.BasicEPS.TwelveMonths',
'OperationRatios.ROA.ThreeMonths',
'OperationRatios.GrossMargin.ThreeMonths',
'FinancialStatements.CashFlowStatement.CashFlowFromContinuingOperatingActivities.ThreeMonths',
'FinancialStatements.IncomeStatement.NormalizedIncome.ThreeMonths',
'FinancialStatements.BalanceSheet.LongTermDebt.ThreeMonths',
'FinancialStatements.BalanceSheet.TotalAssets.ThreeMonths',
'FinancialStatements.BalanceSheet.OrdinarySharesNumber.ThreeMonths',
'FinancialStatements.IncomeStatement.TotalRevenueAsReported.ThreeMonths',
'ValuationRatios.PERatio',
'OperationRatios.CurrentRatio.ThreeMonths',
]
self.SetSecurityInitializer(lambda x: x.SetMarketPrice(self.GetLastKnownPrice(x)))
self.leverage:int = 10
self.min_share_price:int = 5
self.period = 21
self.long:List[Symbol] = []
self.short:List[Symbol] = []
self.stock_data:Dict[Symbol, data_tools.StockData] = {}
self.data:Dict[Symbol, data_tools.SymbolData] = {}
market:Symbol = self.AddEquity('SPY', Resolution.Daily).Symbol
self.fundamental_count:int = 3000
self.fundamental_sorting_key = lambda x: x.MarketCap
self.selection_flag = False
self.Settings.MinimumOrderMarginPortfolioPercentage = 0.
self.UniverseSettings.Resolution = Resolution.Daily
self.AddUniverse(self.FundamentalSelectionFunction)
self.Schedule.On(self.DateRules.MonthEnd(market), self.TimeRules.AfterMarketOpen(market), self.Selection)
self.settings.daily_precise_end_time = False
def OnSecuritiesChanged(self, changes: SecurityChanges) -> None:
for security in changes.AddedSecurities:
security.SetFeeModel(data_tools.CustomFeeModel())
security.SetLeverage(self.leverage)
def FundamentalSelectionFunction(self, fundamental: List[Fundamental]) -> List[Symbol]:
# Update the rolling window every day.
for stock in fundamental:
symbol:Symbol = stock.Symbol
# Store daily price.
if symbol in self.data:
self.data[symbol].update(stock.AdjustedPrice)
if not self.selection_flag:
return Universe.Unchanged
selected:List[Fundamental] = [
x for x in fundamental if x.HasFundamentalData and x.Market == 'usa' and x.Price > self.min_share_price \
and all((not isnan(self.rgetattr(x, statement_name)) and self.rgetattr(x, statement_name) != 0) for statement_name in self.financial_statement_names) \
and x.SecurityReference.ExchangeId in self.exchange_codes
]
if len(selected) > self.fundamental_count:
selected = [x for x in sorted(selected, key=self.fundamental_sorting_key, reverse=True)[:self.fundamental_count]]
# Warmup price rolling windows.
for stock in selected:
symbol:Symbol = stock.Symbol
if symbol in self.data:
continue
self.data[symbol] = data_tools.SymbolData(symbol, self.period)
history:DataFrame = self.History(symbol, self.period, Resolution.Daily)
if history.empty:
self.Log(f"Not enough data for {symbol} yet.")
continue
closes:Series = history.loc[symbol].close
for time, close in closes.items():
self.data[symbol].update(close)
# BM sorting
sorted_by_market_cap:List[Fundamental] = sorted(selected, key = lambda x: x.MarketCap, reverse = True)
lenght:int = int((len(sorted_by_market_cap) / 100) * 40)
top_by_market_cap:List[Fundamental] = [x for x in sorted_by_market_cap[:lenght]]
fine_symbols:List[Symbol] = [x.Symbol for x in top_by_market_cap]
score_performance:Dict[Symbol, Tuple[float]] = {}
for stock in top_by_market_cap:
symbol:Symbol = stock.Symbol
if not self.data[symbol].is_ready():
continue
if symbol not in self.stock_data:
self.stock_data[symbol] = data_tools.StockData() # Contains latest data.
roa:float = stock.OperationRatios.ROA.ThreeMonths
cfo:float = stock.FinancialStatements.CashFlowStatement.CashFlowFromContinuingOperatingActivities.ThreeMonths
leverage:float = stock.FinancialStatements.BalanceSheet.LongTermDebt.ThreeMonths / stock.FinancialStatements.BalanceSheet.TotalAssets.ThreeMonths
liquidity:float = stock.OperationRatios.CurrentRatio.ThreeMonths
equity_offering:float = stock.FinancialStatements.BalanceSheet.OrdinarySharesNumber.ThreeMonths
gross_margin:float = stock.OperationRatios.GrossMargin.ThreeMonths
turnover:float = stock.FinancialStatements.IncomeStatement.TotalRevenueAsReported.ThreeMonths / stock.FinancialStatements.BalanceSheet.TotalAssets.ThreeMonths
# Check if data has previous year's data ready.
stock_data = self.stock_data[symbol]
if (stock_data.ROA == 0) or (stock_data.Leverage == 0) or (stock_data.Liquidity == 0) or (stock_data.Equity_offering == 0) or (stock_data.Gross_margin == 0) or (stock_data.Turnover == 0):
stock_data.Update(roa, leverage, liquidity, equity_offering, gross_margin, turnover)
continue
score:int = 0
if roa > 0:
score += 1
if cfo > 0:
score += 1
if roa > stock_data.ROA: # ROA change is positive
score += 1
if cfo > roa:
score += 1
if leverage < stock_data.Leverage:
score += 1
if liquidity > stock_data.Liquidity:
score += 1
if equity_offering < stock_data.Equity_offering:
score += 1
if gross_margin > stock_data.Gross_margin:
score += 1
if turnover > stock_data.Turnover:
score += 1
score_performance[symbol] = (score, self.data[symbol].performance())
# Update new (this year's) data.
stock_data.Update(roa, leverage, liquidity, equity_offering, gross_margin, turnover)
# Clear out not updated data.
for symbol in self.stock_data:
if symbol not in fine_symbols:
self.stock_data[symbol] = data_tools.StockData()
# Performance sorting and F score sorting.
self.long = [x[0] for x in score_performance.items() if x[1][0] >= 7 and x[1][1] < 0]
self.short = [x[0] for x in score_performance.items() if x[1][0] <= 3 and x[1][1] > 0]
return self.long + self.short
def OnData(self, data: Slice) -> None:
if not self.selection_flag:
return
self.selection_flag = False
# Trade execution.
targets:List[PortfolioTarget] = []
for i, portfolio in enumerate([self.long, self.short]):
for symbol in portfolio:
if symbol in data and data[symbol]:
targets.append(PortfolioTarget(symbol, ((-1) ** i) / len(portfolio)))
self.SetHoldings(targets, True)
self.long.clear()
self.short.clear()
def Selection(self) -> None:
self.selection_flag = True
# https://gist.github.com/wonderbeyond/d293e7a2af1de4873f2d757edd580288
def rgetattr(self, obj, attr, *args):
def _getattr(obj, attr):
return getattr(obj, attr, *args)
return reduce(_getattr, [obj] + attr.split('.'))