Markets: A Rigorous Course

Exercise · m06_forensics.py

Forensic and quality scores: Beneish M-score, Piotroski F-score, Coffee Can, cash checks

Exercise stubs. See notes.md 6.8–6.9 for every definition.

Tests (5)

  • test_beneish_reference_fixture
  • test_beneish_identity_year
  • test_piotroski
  • test_coffee_can_every_year_rule
  • test_cash_checks

Theory: M06 notes. Run with Ctrl/⌘+Enter.

not run yet

Not saved: sign up to keep your code
"""M06 · Forensic and quality scores: Beneish M-score, Piotroski F-score, Coffee Can, cash checks.

Exercise stubs. See notes.md §6.8–§6.9 for every definition.
"""

from __future__ import annotations

BENEISH_COEF = {
    "const": -4.84, "DSRI": 0.920, "GMI": 0.528, "AQI": 0.404, "SGI": 0.892,
    "DEPI": 0.115, "SGAI": -0.172, "TATA": 4.679, "LVGI": -0.327,
}


def beneish_components(cur: dict, prev: dict) -> dict:
    """Return the eight Beneish (1999) indices as dict(DSRI, GMI, AQI, SGI, DEPI, SGAI, TATA, LVGI).

    cur = year t, prev = year t-1. Keys in both dicts: sales, cogs, receivables,
    current_assets, ppe, securities, total_assets, depreciation, sga, net_income, cfo,
    current_liabilities, lt_debt.
      DSRI = (Rec_t/Sales_t) / (Rec_{t-1}/Sales_{t-1})
      GMI  = GM_{t-1} / GM_t,                 GM = (Sales - COGS)/Sales
      AQI  = AQ_t / AQ_{t-1},                 AQ = 1 - (CA + PPE + Securities)/TA
      SGI  = Sales_t / Sales_{t-1}
      DEPI = DR_{t-1} / DR_t,                 DR = Dep/(Dep + PPE)
      SGAI = (SGA_t/Sales_t) / (SGA_{t-1}/Sales_{t-1})
      TATA = (NI_t - CFO_t) / TA_t
      LVGI = LEV_t / LEV_{t-1},               LEV = (CL + LTD)/TA
    """
    raise NotImplementedError


def beneish_m(cur: dict, prev: dict):
    """Return (M, components) with M = const + sum_k BENEISH_COEF[k] * component_k.

    Reference fixture in the tests gives M = -1.1197.
    """
    raise NotImplementedError


def beneish_flag(m: float, threshold: float = -1.78) -> bool:
    """True if m > threshold (likely manipulator)."""
    raise NotImplementedError


def piotroski_signals(cur: dict, prev: dict) -> dict:
    """The nine binary Piotroski (2000) signals as a dict of bools with keys
    F_ROA, F_CFO, F_dROA, F_ACCRUAL, F_dLEVER, F_dLIQUID, F_EQ_OFFER, F_dMARGIN, F_dTURN.

    Keys in cur/prev: net_income, cfo, total_assets, total_assets_begin, lt_debt,
    current_assets, current_liabilities, shares_out, sales, cogs.
    ROA = NI / TA_begin; CFO scaled by TA_begin; asset turnover = Sales / TA_begin;
    leverage = LTD / TA (end of year); current ratio = CA/CL; GM = (Sales-COGS)/Sales.
    F_EQ_OFFER is True when shares_out did not increase. Changes are strict (>, <).
    """
    raise NotImplementedError


def piotroski_f(cur: dict, prev: dict) -> int:
    """F-score = number of True signals (0..9). Reference fixture: 9; swapped years: 4."""
    raise NotImplementedError


def coffee_can_pass(rev, roce, min_g: float = 0.10, min_roce: float = 0.15) -> bool:
    """True iff revenue growth >= min_g in EVERY one of the 10 years AND ROCE >= min_roce every year.

    rev: 11 annual revenues, oldest first. roce: 10 annual ROCE values.
    """
    raise NotImplementedError


def cfo_to_pat(cfo, pat) -> float:
    """Cumulative cash conversion sum(CFO)/sum(PAT)."""
    raise NotImplementedError


def implied_cash_yield(interest_income: float, cash_begin: float, cash_end: float) -> float:
    """Interest income divided by average of opening and closing cash + liquid investments."""
    raise NotImplementedError

No runs yet

The stub's functions raise NotImplementedError until you write them. Fill in one, press Run tests, and watch its tests turn green.

Reference solution

Available after your first run, with an account approved for code runs.

Create a free account

Reading is open to everyone. An account keeps your progress, answers, notes and code drafts, and gives you the study plan, the journals and the simulator. Running code needs an account approved for it.