Konatus Research Group · free, no sign-in

Conatus Scanner

Read a business by its causes. Every US-listed company with SEC filings, scored on whether its growth follows from its own nature or leans on outside causes.

3,915 companies · filings as of , refreshed daily · How it scores

Connecting to live prices…

Loading 3,915 companies…

Companies ranked by Conatus score. Select a company for its causal reading.
#

Does it predict returns?

Each April the scanner is rerun on the filings that existed then, and the next twelve months are measured. This is the honest test the score needs, and its biases are listed under the results.

Loading the backtest…

The Spinozan reading

Spinoza’s word for a thing’s effort to persevere in its being is conatus, and he says that effort is nothing but the thing’s actual essence (Ethics III, P7). He also says that knowing an effect depends on, and involves, knowing its cause (I, Axiom 4). A price chart shows effects. Filings show causes: what the business does with capital, and who pays for its growth.

The line this scanner draws comes from Ethics III, Definition 2. We act when something follows from our own nature, of which we are the adequate cause. We are acted upon when it follows only in part from us. A company that compounds on its own cash, without new shares or new debt, is acting. A company whose growth is paid for by fresh stock or borrowed money is, in that measure, being acted upon: it grows because outside causes keep choosing to feed it.

Five causes, twenty points each.
CauseMeasureWhat it asksPoints
PowerReturn on invested capital, latest fiscal year.How much capital the business makes from the capital it holds.5% scores 0, 25% scores 20
PersistenceLowest ROIC of the last five fiscal years.Whether the cause keeps producing the same effect in bad years.0% scores 0, 15% scores 20
Self-fundingShare of years with positive free cash flow, and the latest FCF margin.Whether it pays for its own growth, or needs someone else to.10 for every year positive, 10 for a 15% margin
StrivingRevenue per diluted share, compound growth over up to five years.Growth that reaches each owner, after dilution.0% scores 0, 15% a year scores 20
IndependenceDiluted share count growth, and net debt in years of free cash flow.Whether it lives without new shares or borrowed money.10 for no dilution, 10 for net cash

Bands. 75 and over: strong conatus, acting from its own nature. 50 to 75: striving, partly its own cause. 25 to 50: dependent, leaning on outside causes. Under 25: passive, acted upon.

Price stays out of the score. The FCF yield sits next to it: last fiscal year’s free cash flow divided by today’s market cap. An excellent company can be bought at a poor price, and chapter 20 of The Spinozan Trader says so plainly. The score says what the business is. The yield says what you are paying for it.

Beyond the score

The five causes decide the score and nothing else does. The columns below sit beside it for cross-checking, under the Valuation and Quality views, and none of them moves the score.

  • FCF yield, FCF after stock pay, EBIT/EV and shareholder yield divide last fiscal year’s free cash flow, operating income, and buybacks plus dividends by today’s market cap or enterprise value (market cap plus net debt). They move with the price through the day.
  • Piotroski F-score: nine pass-or-fail accounting tests on the last two fiscal years (profitability, cash over earnings, leverage, liquidity, dilution, margin and turnover). It needs seven computable tests, and shows how many it had.
  • Gross profit over assets (Novy-Marx), accruals (net income minus operating cash flow, over assets; high accruals mean earnings the cash does not confirm), and stock pay over revenue.
  • Quality at a price (Q×P) averages a company’s percentile on the score with its percentile on FCF yield, across adequate, non-financial readings. It is a sorting aid. It has not been tested as a return signal here.
  • TTM figures add the quarters filed on 10-Q since the last 10-K and drop the same quarters a year earlier. Revenue, operating income and net income are trailing twelve months where every quarter matches; cash flow stays annual, because 10-Qs report it year to date. Net debt uses the latest balance sheet on file.
  • Δ score compares each score with a saved snapshot, the one nearest thirty days back. A dot marks a newer fiscal year on file since then: a new 10-K has been read.

Alerts

Star companies to build a watchlist; it is kept in your browser. Watchlist alerts asks your browser for permission to send a notification when a starred company’s score moves by five points or more, or when it files a new 10-K. Only the browser’s push address and your tickers are stored, never your name or email; turning alerts off deletes them. On an iPhone, add the page to your Home Screen first. The Atom feed lists every such change across the whole market for any feed reader.

How it stays current

Prices, market caps and everything priced off them come from Nasdaq’s public screener through this site’s own server, re-read every five minutes while the page is open. Filings are re-read from the SEC every day, rescored, and published without a site rebuild; an open page picks up the new reading on its own. If the live feed fails, the page says so and prices at the last good quote.

Adequate readings. A reading is adequate when at least four fiscal years are on file and ROIC and free cash flow can both be computed. Fewer years is an inadequate idea in Spinoza’s sense: a fragment mistaken for the whole.

What it is not

  • It is not a proven return signal. The backtest finds a modest positive rank correlation with next-year returns over seven years, on a sample with survivorship bias, and a low-scoring stock still sometimes makes the biggest gain of the year. The book records that quality labels do not establish a premium, that published stock predictors lost about 58% of their returns after publication (McLean and Pontiff), and that the best 4% of listed companies created all of the market’s net wealth (Bessembinder). This measures causes. Whether the market already prices them is a separate question.
  • It reads banks, insurers and REITs poorly. Their debt is their inventory, so ROIC and free cash flow mean something else. They are left out by default.
  • It does not advise anyone to buy or sell anything. It is a reading aid for your own research.

Data and its limits

Financials come from the SEC’s free EDGAR XBRL frames API, as each company tagged them in its 10-K. Prices, market caps and sectors come from Nasdaq’s public stock screener. Filings are re-read daily and the date at the top is the date of that reading; prices are live, as described above.

  • ROIC here is operating income taxed at the 21% US statutory rate, over equity plus debt minus cash, at the fiscal year end. It is simple on purpose, and it will differ from other sites.
  • Share counts are as first filed. Stock splits and unit slips are detected and undone. A change that looks like neither is kept as real dilution, so a rare real doubling that looks like a 2-for-1 split would be missed.
  • Debt comes from standard us-gaap lines. For a company that tags none of them, its full XBRL history is read: a debt balance near the fiscal year end is used if one exists; if no debt balance above zero and no borrowing appears within about a year, it is treated as debt-free and flagged. A company that borrows under its own custom tags stays unknown, with half marks on the debt test. Banks, insurers and REITs get no inference.
  • Each company uses the revenue tag that covers most of its years, so a switch in tagging does not fake growth. Companies that re-registered under a new SEC identity, and foreign filers that report under IFRS, have no history here yet.
  • Unusual readings are flagged in the detail view. Always open the filing before trusting a number.

The TradingView version

The same five causes as a Pine v6 indicator for TradingView’s Pine Screener, built on TradingView’s own fundamentals. The Pine Screener allows five data requests per script, and it uses exactly five. Add it to favorites, open Products, Screeners, Pine, pick a watchlist and set the timeframe to 1W so it can see about nine fiscal years.

Download Conatus_Scanner.txt

Show the Pine source
//@version=6
// Conatus Scanner: the Spinozan way to read a business by its causes.
//
// "The striving by which each thing strives to persevere in its being is nothing
// but the actual essence of the thing" (Ethics III, P7). And: knowledge of an effect
// depends on, and involves, knowledge of its cause (Ethics I, Axiom 4).
//
// A business shows its conatus when its growth follows from its own nature: it
// turns capital into more capital (power), keeps doing so year after year
// (persistence), pays for itself in cash (self-funding), grows what each owner
// holds (striving), and does not need outside causes, new shares or borrowed money,
// to stay alive (independence). Ethics III, Def. 2: we act when a thing follows from
// our nature as its adequate cause, and are acted upon when it follows only in part.
//
// Five causes, 20 points each, 0 to 100:
//   Power         latest ROIC                       5% -> 0 pts, 25% -> 20 pts
//   Persistence   lowest ROIC of the last 5 years   0% -> 0 pts, 15% -> 20 pts
//   Self-funding  share of years with FCF > 0 (10)  + FCF margin 0% -> 0, 15% -> 10
//   Striving      revenue per diluted share CAGR    0% -> 0 pts, 15% -> 20 pts
//   Independence  diluted share CAGR (10)           5% -> 0 pts, 0% or less -> 10
//                 + net debt / FCF (10)             5 yrs -> 0, 0 or net cash -> 10
// Price is kept out of the score and shown beside it as the FCF yield, because an
// excellent company can be bought at a poor price (The Spinozan Trader, ch. 20).
//
// What this is not: a tested return signal. The book (ch. 20) records that quality
// labels do not establish a premium, that published predictors lost about 58% of
// their returns after publication (McLean and Pontiff), and that the best 4% of
// listed companies created all of the market's net wealth (Bessembinder). This
// scanner measures causes. Whether the market already prices them is a separate
// question. ROIC and free cash flow read poorly for banks and insurers.
//
// Pine Screener: add this script to favorites, open Products > Screeners > Pine,
// choose a watchlist or index, and set the timeframe to 1W. The screener computes
// on the last 500 bars only, so 1D sees about two fiscal years and 1W about nine.
// The screener allows at most five request.*() calls, and this script uses exactly five.
indicator("Conatus Scanner", shorttitle = "Conatus", overlay = false, precision = 1)

// ---------------------------------------------------------------- inputs
grpAlert = "Screener alert"
minScore = input.float(70, "Minimum Conatus score", minval = 0, maxval = 100, step = 5, group = grpAlert)
minYield = input.float(3.0, "Minimum FCF yield (%)", minval = -50, step = 0.5, group = grpAlert, tooltip = "Price is kept out of the score. This is the least cash return, at today's price, that you will accept for owning the cause.")
minYears = input.int(4, "Fiscal years needed for an adequate reading", minval = 2, maxval = 6, group = grpAlert, tooltip = "Fewer observed fiscal years than this marks the reading as inadequate, and the alert will not fire.")

grpShow = "Display"
showTable = input.bool(true, "Show causal table", group = grpShow)
colStrong = input.color(color.rgb(38, 166, 154), "Strong", group = grpShow, inline = "c")
colMid = input.color(color.rgb(242, 183, 5), "Striving", group = grpShow, inline = "c")
colWeak = input.color(color.rgb(239, 83, 80), "Passive", group = grpShow, inline = "c")

// ---------------------------------------------------------------- data: exactly five requests
rev = request.financial(syminfo.tickerid, "TOTAL_REVENUE", "FY", ignore_invalid_symbol = true, currency = syminfo.currency)
fcf = request.financial(syminfo.tickerid, "FREE_CASH_FLOW", "FY", ignore_invalid_symbol = true, currency = syminfo.currency)
roic = request.financial(syminfo.tickerid, "RETURN_ON_INVESTED_CAPITAL", "FY", ignore_invalid_symbol = true)
shares = request.financial(syminfo.tickerid, "DILUTED_SHARES_OUTSTANDING", "FY", ignore_invalid_symbol = true)
netDebt = request.financial(syminfo.tickerid, "NET_DEBT", "FY", ignore_invalid_symbol = true, currency = syminfo.currency)

// ---------------------------------------------------------------- helpers
// Keeps the last six distinct fiscal-year values of a stepwise financial series.
fyHistory(float x) =>
    var array<float> h = array.new<float>()
    isNew = not na(x) and (na(x[1]) or x != x[1])
    if isNew
        array.push(h, x)
        if array.size(h) > 6
            array.shift(h)
    h

lastVal(array<float> a) => array.size(a) > 0 ? array.get(a, array.size(a) - 1) : na
backVal(array<float> a, int k) => array.size(a) > k and k >= 0 ? array.get(a, array.size(a) - 1 - k) : na

minLast(array<float> a, int n) =>
    float m = na
    int sz = array.size(a)
    if sz > 0
        for i = math.max(0, sz - n) to sz - 1
            v = array.get(a, i)
            m := na(m) ? v : math.min(m, v)
    m

positiveShare(array<float> a) =>
    int sz = array.size(a)
    float c = 0.0
    if sz > 0
        for i = 0 to sz - 1
            if array.get(a, i) > 0
                c += 1.0
    sz > 0 ? c / sz : na

cagr(float a0, float a1, int k) => na(a0) or na(a1) or k < 1 or a0 <= 0 or a1 <= 0 ? na : math.pow(a1 / a0, 1.0 / k) - 1.0

// Linear points between two anchors, clamped; anchors may run in either direction.
lin(float x, float x0, float x1, float pts) => na(x) ? 0.0 : pts * math.max(0.0, math.min(1.0, (x - x0) / (x1 - x0)))

fmtPct(float x) => na(x) ? "n/a" : str.tostring(x * 100.0, "0.0") + "%"

// ---------------------------------------------------------------- the five causes
revH = fyHistory(rev)
fcfH = fyHistory(fcf)
roicH = fyHistory(roic)
shH = fyHistory(shares)
ndH = fyHistory(netDebt)

roicNow = lastVal(roicH) / 100.0
roicMin = minLast(roicH, 5) / 100.0
fcfNow = lastVal(fcfH)
revNow = lastVal(revH)
fcfMargin = na(fcfNow) or na(revNow) or revNow <= 0 ? na : fcfNow / revNow
fcfPos = positiveShare(fcfH)

k = math.min(math.min(array.size(revH), array.size(shH)) - 1, 5)
rps0 = backVal(revH, k) / backVal(shH, k)
rps1 = revNow / lastVal(shH)
rpsCagr = cagr(rps0, rps1, k)
shCagr = cagr(backVal(shH, k), lastVal(shH), k)

ndNow = lastVal(ndH)
debtYears = na(ndNow) ? na : (ndNow <= 0 ? 0.0 : (not na(fcfNow) and fcfNow > 0 ? ndNow / fcfNow : na))

ptsPower = lin(roicNow, 0.05, 0.25, 20)
ptsPersist = lin(roicMin, 0.0, 0.15, 20)
ptsSelf = (na(fcfPos) ? 0.0 : 10.0 * fcfPos) + lin(fcfMargin, 0.0, 0.15, 10)
ptsStrive = lin(rpsCagr, 0.0, 0.15, 20)
ptsShares = na(shCagr) ? 0.0 : lin(shCagr, 0.05, 0.0, 10)
ptsDebt = na(ndNow) ? 0.0 : (ndNow <= 0 ? 10.0 : (na(debtYears) ? 0.0 : lin(debtYears, 5.0, 0.0, 10)))
ptsIndep = ptsShares + ptsDebt
score = ptsPower + ptsPersist + ptsSelf + ptsStrive + ptsIndep

// The price of the cause, kept out of the score.
fcfYield = na(fcfNow) or na(lastVal(shH)) or lastVal(shH) <= 0 or close <= 0 ? na : fcfNow / (close * lastVal(shH))

yearsSeen = array.size(revH)
adequate = yearsSeen >= minYears and not na(roicNow) and not na(fcfNow)
bandCol = score >= 75 ? colStrong : (score >= 50 ? colMid : colWeak)
bandTxt = score >= 75 ? "Strong conatus: acts from its own nature" : (score >= 50 ? "Striving: partly its own cause" : (score >= 25 ? "Dependent: leans on outside causes" : "Passive: acted upon"))

// ---------------------------------------------------------------- plots (screener columns)
plot(score, "Conatus score", color = adequate ? bandCol : color.new(bandCol, 60), style = plot.style_columns)
plot(ptsPower, "Power pts", color = color.new(color.gray, 100), display = display.data_window)
plot(ptsPersist, "Persistence pts", color = color.new(color.gray, 100), display = display.data_window)
plot(ptsSelf, "Self-funding pts", color = color.new(color.gray, 100), display = display.data_window)
plot(ptsStrive, "Striving pts", color = color.new(color.gray, 100), display = display.data_window)
plot(ptsIndep, "Independence pts", color = color.new(color.gray, 100), display = display.data_window)
plot(roicNow * 100.0, "ROIC %", color = color.new(color.gray, 100), display = display.data_window)
plot(roicMin * 100.0, "Min ROIC 5y %", color = color.new(color.gray, 100), display = display.data_window)
plot(fcfMargin * 100.0, "FCF margin %", color = color.new(color.gray, 100), display = display.data_window)
plot(rpsCagr * 100.0, "Revenue/share CAGR %", color = color.new(color.gray, 100), display = display.data_window)
plot(shCagr * 100.0, "Share count CAGR %", color = color.new(color.gray, 100), display = display.data_window)
plot(debtYears, "Net debt / FCF yrs", color = color.new(color.gray, 100), display = display.data_window)
plot(fcfYield * 100.0, "FCF yield %", color = color.new(color.gray, 100), display = display.data_window)
plot(yearsSeen, "Fiscal years seen", color = color.new(color.gray, 100), display = display.data_window)
hline(75, "Strong", color = color.new(color.gray, 50), linestyle = hline.style_dotted)
hline(50, "Striving", color = color.new(color.gray, 50), linestyle = hline.style_dotted)
hline(25, "Dependent", color = color.new(color.gray, 50), linestyle = hline.style_dotted)

alertcondition(adequate and score >= minScore and not na(fcfYield) and fcfYield * 100.0 >= minYield, "Conatus at a fair price", "Conatus Scanner: {{ticker}} passes the score and FCF-yield thresholds")

// ---------------------------------------------------------------- causal table (chart only)
var table info = table.new(position.top_right, 3, 9, bgcolor = color.new(chart.bg_color, 8), border_color = color.new(chart.fg_color, 80), border_width = 1)
row(int r, string k1, string v, string p, color c) =>
    table.cell(info, 0, r, k1, text_color = color.gray, text_size = size.small, text_halign = text.align_left)
    table.cell(info, 1, r, v, text_color = chart.fg_color, text_size = size.small, text_halign = text.align_left)
    table.cell(info, 2, r, p, text_color = c, text_size = size.small, text_halign = text.align_right)

ptsTxt(float p, float mx) => str.tostring(p, "0.0") + "/" + str.tostring(mx, "0")
ptsCol(float p, float mx) => p >= 0.75 * mx ? colStrong : (p >= 0.4 * mx ? colMid : colWeak)

if showTable and barstate.islast
    row(0, "Conatus", bandTxt, str.tostring(score, "0") + "/100", bandCol)
    row(1, "Power", "ROIC " + fmtPct(roicNow), ptsTxt(ptsPower, 20), ptsCol(ptsPower, 20))
    row(2, "Persistence", "lowest ROIC, " + str.tostring(math.min(array.size(roicH), 5)) + "y: " + fmtPct(roicMin), ptsTxt(ptsPersist, 20), ptsCol(ptsPersist, 20))
    row(3, "Self-funding", "FCF > 0 in " + fmtPct(fcfPos) + " of years; margin " + fmtPct(fcfMargin), ptsTxt(ptsSelf, 20), ptsCol(ptsSelf, 20))
    row(4, "Striving", "revenue per share " + fmtPct(rpsCagr) + " a year over " + str.tostring(math.max(k, 0)) + "y", ptsTxt(ptsStrive, 20), ptsCol(ptsStrive, 20))
    row(5, "Independence", "shares " + fmtPct(shCagr) + " a year; net debt " + (na(ndNow) ? "n/a" : (ndNow <= 0 ? "none (net cash)" : (na(debtYears) ? "with no FCF to repay it" : str.tostring(debtYears, "0.0") + " yrs of FCF"))), ptsTxt(ptsIndep, 20), ptsCol(ptsIndep, 20))
    row(6, "Price of the cause", "FCF yield " + fmtPct(fcfYield) + " at " + str.tostring(close, format.mintick), "", na(fcfYield) ? color.gray : (fcfYield * 100.0 >= minYield ? colStrong : colWeak))
    row(7, "Adequacy", adequate ? str.tostring(yearsSeen) + " fiscal years seen: adequate" : str.tostring(yearsSeen) + " fiscal years seen: inadequate, use 1W", "", adequate ? colStrong : colWeak)
    row(8, "Status", "Measures causes. Not a tested return signal.", "", color.gray)