
Trade BSE and NSE stocks by cash flow-to-price ratio, going long on the highest quintile and short on the lowest, using value-weighted portfolios rebalanced monthly.
ASSET CLASS: stocks | REGION: Emerging Markets, India | FREQUENCY:
Monthly | MARKET: equities | KEYWORD: Cashflow, India
I. STRATEGY IN A NUTSHELL
The strategy trades BSE and NSE stocks, sorted into quintiles by their cash flow-to-price ratio (operating cash flow from the last fiscal year divided by market value from the previous month). It goes long the highest quintile (undervalued stocks) and short the lowest quintile (overvalued stocks). Portfolios are value-weighted and rebalanced monthly.
II. ECONOMIC RATIONALE
The cash flow-to-price ratio is a robust valuation metric that helps identify undervalued versus overvalued firms. Unlike earnings, cash flows are harder to manipulate, making the measure more reliable. The strategy captures the value premium, a well-documented phenomenon in developed markets, and the study confirms that this relationship also holds in India.
III. SOURCE PAPER
Cross-sectional Return Predictability in Indian Stock Market: An Empirical Investigation [Click to Open PDF]
Goswami, Gautam, Fordham University – Finance Area
<Abstract>
This paper provides a comprehensive analysis of stock return predictability in the Indian stock market by employing both the portfolio and cross-sectional regressions methods using the data from January 1994 and ending in December 2018. We find strong predictive power of size, cash-flow-to-price ratio, momentum and short-term-reversal, and in some cases of book-to-market-ratio, price-earnings-ratio. The total volatility, idiosyncratic volatility, and beta are not consistent stock return predictors in the Indian stock market. In cross-sectional regression analysis, size, short-term reversal, momentum, and cash-flow-to-price ratio predict the future stock returns. Overall, the two variables momentum and cash flow to price ratio demonstrate reliable forecasting power under all methods and both small and large size samples.

IV. BACKTEST PERFORMANCE
| Annualised Return | 16.49% |
| Volatility | 28.83% |
| Beta | -0.022 |
| Sharpe Ratio | 0.57 |
| Sortino Ratio | N/A |
| Maximum Drawdown | N/A |
| Win Rate | 50% |
V. FULL PYTHON CODE
from AlgorithmImports import *
from typing import Dict, List
import data_tools
from datetime import datetime
# endregion
class CashflowToPriceInIndianMarket(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2010, 1, 1)
self.SetCash(1e9) # INR
# set variables
self.quantile:int = 5
self.leverage:int = 10
self.data:Dict[Symbol, data_tools.SymbolData] = {}
self.excluded_tickers:List[str] = ['LODHA']
# download tickers
ticker_file_str:str = self.Download('data.quantpedia.com/backtesting_data/equity/india_stocks/nse_500_tickers.csv')
ticker_lines:List[str] = ticker_file_str.split('\r\n')
tickers:List[str] = [ ticker_line.split(',')[0] for ticker_line in ticker_lines[1:] ]
for t in tickers:
if t in self.excluded_tickers: continue
# price data subscription
data = self.AddData(data_tools.IndiaStocks, t, Resolution.Daily)
data.SetFeeModel(data_tools.CustomFeeModel())
data.SetLeverage(self.leverage)
stock_symbol:Symbol = data.Symbol
# fundamental data subscription
balance_sheet = self.AddData(data_tools.IndiaBalanceSheet, t, Resolution.Daily).Symbol
cashflow_sheet = self.AddData(data_tools.IndiaCashflowStatement, t, Resolution.Daily).Symbol
self.data[stock_symbol] = data_tools.SymbolData(stock_symbol, balance_sheet, cashflow_sheet)
self.recent_month:int = -1
def OnData(self, data: Slice) -> None:
rebalance_flag:bool = False
metric_by_symbol:Dict[Symbol, float] = {}
price_last_update_date:Dict[Symbol, datetime.date] = data_tools.IndiaStocks.get_last_update_date()
bs_last_update_date:Dict[Symbol, datetime.date] = data_tools.IndiaBalanceSheet.get_last_update_date()
cf_last_update_date:Dict[Symbol, datetime.date] = data_tools.IndiaCashflowStatement.get_last_update_date()
for price_symbol, symbol_data in self.data.items():
# store price data
if data.ContainsKey(price_symbol) and data[price_symbol] and data[price_symbol].Value != 0:
price:float = data[price_symbol].Value
self.data[price_symbol].update_price(price)
bs_symbol:Symbol = symbol_data._balance_sheet_symbol
cf_symbol:Symbol = symbol_data._cashflow_sheet_symbol
# check if BS and CF statement is present
if bs_symbol in data and data[bs_symbol] and cf_symbol in data and data[cf_symbol]:
bs_statement:Dict = data[bs_symbol].Statement
cf_statement:Dict = data[cf_symbol].Statement
shares_field:str = 'commonStockSharesOutstanding'
operating_field:str = 'totalCashFromOperatingActivities'
if shares_field in bs_statement and bs_statement[shares_field] is not None and \
operating_field in cf_statement and cf_statement[operating_field] is not None:
shares_outstanding:float = float(bs_statement[shares_field])
cashflow: float = float(cf_statement[operating_field])
# store fundamentals
symbol_data.update_fundamentals(shares_outstanding, cashflow)
if self.IsWarmingUp: continue
if self.Time.month != self.recent_month or rebalance_flag:
self.recent_month = self.Time.month
rebalance_flag = True
# fundamental data are ready and still arriving
if self.Securities[price_symbol].GetLastData() and price_symbol in price_last_update_date and self.Time.date() <= price_last_update_date[price_symbol]:
if self.Securities[bs_symbol].GetLastData() and bs_symbol in bs_last_update_date and self.Time.date() <= bs_last_update_date[bs_symbol] and \
self.Securities[cf_symbol].GetLastData() and cf_symbol in cf_last_update_date and self.Time.date() <= cf_last_update_date[cf_symbol]:
shares_outstanding:float = symbol_data.get_recent_shares()
cashflow:float = symbol_data.get_recent_cf()
price:float = symbol_data.get_recent_price()
# cashflow to price ratio calculation
if price != -1 and shares_outstanding != -1 and cashflow != -1:
market_cap:float = shares_outstanding * price
if market_cap != 0:
cftp_ratio:float = cashflow / market_cap
metric_by_symbol[price_symbol] = (cftp_ratio, market_cap)
# rebalance monthly
if rebalance_flag:
weights:Dict[Symbol, float] = {}
if len(metric_by_symbol) >= self.quantile:
# sort cashflow to price ratio
sorted_cftp:List = sorted(metric_by_symbol.items(), key=lambda x: x[1][0], reverse=True)
quantile:int = int(len(sorted_cftp) / self.quantile)
# get top and bottom quantile
short_assets = [x[0] for x in sorted_cftp[:quantile]]
long_assets = [x[0] for x in sorted_cftp[-quantile:]]
# calculate weights based on values
sum_short:float = sum([metric_by_symbol[i][1] for i in short_assets])
for asset in short_assets:
weights[asset] = -metric_by_symbol[asset][1] / sum_short
sum_long:float = sum([metric_by_symbol[i][1] for i in long_assets])
for asset in long_assets:
weights[asset] = metric_by_symbol[asset][1] / sum_long
# liquidate and rebalance
invested:List[Symbol] = [x.Key for x in self.Portfolio if x.Value.Invested]
for price_symbol in invested:
if price_symbol not in weights:
self.Liquidate(price_symbol)
for price_symbol, weight in weights.items():
if price_symbol in data and data[price_symbol]:
self.SetHoldings(price_symbol, weight)