Build It WithMetricDuck APIPythonValue Trap
Part of the Build It With MetricDuck series

Value Trap Detector: Check Any Stock Against Its Own 2-Year History with Python

A stock with a low PE looks cheap. But if ROIC is declining, you're buying a deteriorating business at a discount that keeps getting bigger. Stock Pulse compares any stock to its own 2-year baseline and diagnoses VALUE TRAP or OPPORTUNITY. One API call, no signup.

12 min read

META's ROIC is 24.9% — strong by any standard. But two years ago, it was 29.8%. The trend is down. Meanwhile, PE is rising. The market is paying more for a business that is returning less per dollar of capital.

==========================================================
                    STOCK PULSE: META
                   Meta Platforms, Inc.
==========================================================

VITAL SIGNS  (current vs 2-year median)
----------------------------------------------------------
                      Current    2yr Med           Signal
----------------------------------------------------------
ROIC                    24.9%     29.8%      v 17% below
Gross Margin            82.0%     81.8%      ~ Near norm
Oper Margin             41.4%     41.4%      ~ Near norm
FCF Margin              22.9%     27.1%      v 15% below

VALUATION  (current + 2-year trend)
----------------------------------------------------------
                      Current                       Trend
----------------------------------------------------------
PE Ratio                27.60                      Rising
EV/EBITDA               16.04                     Falling

GROWTH
----------------------------------------------------------
Revenue YoY                                        +22.2%

LEVERAGE
----------------------------------------------------------
Debt/Equity                                           N/A

==========================================================
DIAGNOSIS: VALUE TRAP
----------------------------------------------------------
META's quality is declining while valuation expands —
paying more for a deteriorating business.
Investigate before assuming it's cheap.

Signal: VALUE TRAP | ROIC trend: falling | PE trend: rising
==========================================================

This is Stock Pulse — a Python tool that checks any stock against its own 2-year history using SEC-filed data from the MetricDuck API. Not compared to peers. Not compared to the S&P 500. Compared to itself.

The diagnosis tells you whether the numbers say OPPORTUNITY or VALUE TRAP.

No API key required. No signup. Guest access gives you all 70 metrics and all 12 statistical dimensions including Q.MED8 and Q.TREND8. Just clone, install, run.

Quick Start

Python 3.10+ required.

git clone https://github.com/metric-duck/build-with-metricduck.git
cd build-with-metricduck/labs/03-stock-pulse
pip install -r requirements.txt
python pulse.py NVDA

That's it. Works immediately — no API key, no config file, no signup.

Default (no arguments): python pulse.py runs AAPL.

What Is a Value Trap

A stock with a PE of 12 looks cheap. But if its ROIC has fallen from 20% to 8% over the past two years, the business is earning less per dollar of capital it deploys. The "discount" is the market pricing in that deterioration — and it will likely get bigger.

A value trap is a stock that appears undervalued by traditional metrics (low PE, low EV/EBITDA) but is actually cheap for a reason: the underlying business quality is declining. The low price isn't a buying opportunity — it's the market being right.

The opposite is equally important. A stock with a PE of 50 looks expensive. But if its ROIC is rising from 40% to 80%, the business is compounding capital faster than ever. The "premium" might be justified — or even an opportunity if valuation is compressing despite improving quality.

Stock Pulse detects both cases by crossing two trends: ROIC direction (is the business getting better or worse?) and PE direction (is the market paying more or less?).

The Five Diagnostic Signals

The tool produces one of five signal words based on the intersection of ROIC trend and PE trend:

SignalROIC TrendPE TrendWhat It Means
OPPORTUNITYRisingFallingQuality improving, valuation compressing. The market may not be pricing in the improvement.
EARNING ITRisingRisingQuality improving, market recognizing it. Is the premium justified?
STABLEFlatFlatNo strong trend signal. Fundamentals and valuation are near 2-year norms.
WATCHFallingFallingQuality and valuation both declining. The market may be right to de-rate.
VALUE TRAPFallingRisingDeclining quality, expanding valuation. Paying more for a deteriorating business.

These are diagnostic signals, not buy/sell recommendations. OPPORTUNITY means "the numbers are worth investigating," not "buy." VALUE TRAP means "investigate before assuming it's cheap," not "sell."

Here's what OPPORTUNITY looks like — NVIDIA's ROIC is rising while its PE is falling:

==========================================================
                    STOCK PULSE: NVDA
                       NVIDIA CORP
==========================================================

VITAL SIGNS  (current vs 2-year median)
----------------------------------------------------------
                      Current    2yr Med           Signal
----------------------------------------------------------
ROIC                    96.5%    102.7%       v 6% below
Gross Margin            70.1%     74.0%       v 5% below
Oper Margin             58.8%     61.8%      ~ Near norm
FCF Margin              41.3%     42.3%      ~ Near norm

VALUATION  (current + 2-year trend)
----------------------------------------------------------
                      Current                       Trend
----------------------------------------------------------
PE Ratio                45.67                     Falling
EV/EBITDA               40.17                     Falling

==========================================================
DIAGNOSIS: OPPORTUNITY
----------------------------------------------------------
NVDA's quality is improving while valuation compresses —
the market may not be pricing in the improvement yet.

Signal: OPPORTUNITY | ROIC trend: rising | PE trend: falling
==========================================================

NVDA's ROIC is 96.5% — slightly below its extraordinary 2-year median of 102.7%, but the trend is rising. Meanwhile, PE is compressing from previous highs. Quality up, price down.

How It Works

One API Call with Dimensions

The key difference from Lab 02 (Stock Showdown) is the dimensions parameter. Lab 02 fetched base metric values. Lab 03 fetches base values plus historical context:

import httpx

response = httpx.get(
    "https://api.metricduck.com/api/v1/data/metrics",
    params={
        "tickers": "NVDA",
        "metrics": "roic,gross_margin,oper_margin,fcf_margin,pe_ratio,ev_ebitda,revenues,debt_to_equity",
        "period": "ttm",
        "price": "current",
        "dimensions": "Q.MED8,Q.TREND8,TTM.YOY,TTM.CAGR3",
    },
)
data = response.json()

The dimensions parameter adds historical analysis to each metric:

  • Q.MED8 — 8-quarter median (the stock's 2-year "normal")
  • Q.TREND8 — 8-quarter trend (positive = improving, negative = declining)
  • TTM.YOY — Year-over-year change
  • TTM.CAGR3 — 3-year compound annual growth rate

Each metric's values array now contains multiple entries — the base TTM value plus one entry per dimension:

{
  "data": {
    "NVDA": {
      "company_name": "NVIDIA CORP",
      "metrics": {
        "roic": {
          "label": "Return on Invested Capital",
          "values": [
            {"period": "TTM", "end_date": "2025-01-26", "value": 0.9650, "dimension": null},
            {"period": "TTM", "end_date": "2025-01-26", "value": 1.0270, "dimension": "Q.MED8"},
            {"period": "TTM", "end_date": "2025-01-26", "value": 0.0412, "dimension": "Q.TREND8"}
          ]
        }
      }
    }
  }
}

The base value has "dimension": null. Dimensions are identified by their "dimension" key. The extract_dimension() function filters for the dimension you need:

def extract_dimension(api_data, ticker, metric_id, dimension):
    company = api_data.get("data", {}).get(ticker, {})
    metric = company.get("metrics", {}).get(metric_id, {})
    for v in metric.get("values", []):
        if v.get("dimension") == dimension and v.get("value") is not None:
            return v["value"]
    return None

# Get ROIC's 2-year median
roic_median = extract_dimension(data, "NVDA", "roic", "Q.MED8")

# Get ROIC's 2-year trend direction
roic_trend = extract_dimension(data, "NVDA", "roic", "Q.TREND8")

Vital Signs: Current vs 2-Year Median

The Vital Signs section compares each quality metric to its Q.MED8 (2-year median). The comparison is a simple percentage deviation:

pct = (current - median) / abs(median) * 100
if pct > 5:
    signal = f"^ {abs(pct):.0f}% above"    # Above historical norm
elif pct < -5:
    signal = f"v {abs(pct):.0f}% below"    # Below historical norm
else:
    signal = "~ Near norm"                  # Within 5% of median

When you see v 17% below next to ROIC, that means the company's current ROIC is 17% below its own 2-year median. Not compared to peers — compared to itself.

The PE Caveat

Valuation metrics show trend only, not median comparison. PE Ratio and EV/EBITDA Q.MED8 values use single-quarter earnings (not trailing twelve months), making the median approximately 4x the TTM value. The tool correctly shows valuation trend direction (rising or falling) but does not compare valuation to its median. Vital Signs metrics (ROIC, margins) use ratios where quarterly and TTM values are directly comparable, so their median comparison is accurate.

The Diagnosis Logic

The diagnosis crosses ROIC trend with PE trend using Q.TREND8 values. A Q.TREND8 value above 0.003 means "rising," below -0.003 means "falling," and in between means "stable":

def _compute_diagnosis(api_data, ticker):
    roic_trend = extract_dimension(api_data, ticker, "roic", "Q.TREND8")
    pe_trend = extract_dimension(api_data, ticker, "pe_ratio", "Q.TREND8")

    r_trend = format_trend(roic_trend)  # "Rising", "Falling", or "Stable"
    p_trend = format_trend(pe_trend)

    if r_trend == "Rising" and p_trend == "Falling":
        return "OPPORTUNITY", f"{ticker}'s quality is improving..."
    elif r_trend == "Falling" and p_trend == "Rising":
        return "VALUE TRAP", f"{ticker}'s quality is declining..."
    # ... other combinations

The diagnosis captures the relationship between business quality and market pricing — something no single metric can tell you.

The 4 Metric Groups

Stock Pulse organizes 8 metrics into 4 groups. Each group answers a different question:

GroupMetricsWhat It Answers
Vital SignsROIC, Gross Margin, Operating Margin, FCF MarginHow healthy is the business vs its own norm?
ValuationPE Ratio, EV/EBITDAIs the market paying more or less? (trend only)
GrowthRevenue YoY, Revenue 3yr CAGRIs the business growing?
LeverageDebt/EquityHow leveraged is the balance sheet?

Vital Signs are compared to their 2-year median (Q.MED8). Valuation shows trend direction only. Growth uses TTM.YOY and TTM.CAGR3. Leverage shows the current value.

The MetricDuck API has 70+ metrics. The tool ships with 8 that matter most for stock diagnosis — change the VITAL_SIGNS, VALUATION_SNAPSHOT, GROWTH_METRICS, or LEVERAGE_METRICS config lists to add any metric from the full catalog.

Build It with Claude Code

Stock Pulse is a single Python file with no frameworks. AI assistants can read, understand, and extend it in one pass.

Add earnings quality metrics:

"Add sbc_ratio and cash_conversion to VITAL_SIGNS. These help detect earnings quality issues that ROIC alone might miss."

Portfolio batch screening:

"Modify pulse.py to accept a list of tickers: python pulse.py AAPL MSFT NVDA META GOOGL. Output a summary table with each stock's signal. Flag any showing VALUE TRAP."

Combine with Lab 02:

"Run showdown.py to find the cheaper stock. Then run pulse.py on the winner to check if it's actually a value trap."

Historical lookback:

"Add years=5 and period=quarterly to the API call. Show how the diagnosis has changed over time."

The full pulse.py is a single self-contained file with no class hierarchies and no abstractions. AI coding assistants can read the entire file, understand it, and extend it in one pass.

Try It Yourself

StockCommandWhat You Might See
NVDApython pulse.py NVDAROIC near 2-year highs, PE compressing — OPPORTUNITY
AAPLpython pulse.py AAPLROIC rising above median, PE rising too — EARNING IT
METApython pulse.py METAROIC declining from highs, PE expanding — VALUE TRAP
COSTpython pulse.py COSTConsistent quality, PE rising — STABLE or EARNING IT
TSLApython pulse.py TSLAVolatile multiples — check which signal fires today
JPMpython pulse.py JPMBanks often show ROIC N/A — diagnosis falls back to STABLE

Signals change as new SEC filings come in. A VALUE TRAP signal today might become WATCH or STABLE next quarter.

# Machine-readable output for chaining with other tools
python pulse.py NVDA --json

What's Next

Stock Pulse tells you whether one stock is improving or deteriorating. But which stocks should you pulse in the first place?

Lab 04: Stock Screener ranks 50+ stocks by composite Quality + Value score using ROIC, FCF Yield, and 3 other metrics. Screen first, then pulse the top picks for value traps.

# Screen the top 50, then check the #1 pick for value traps
python screener.py
python ../03-stock-pulse/pulse.py MA

And if you want to compare two top picks head-to-head, use Lab 02: Stock Showdown:

python ../02-stock-showdown/showdown.py MA V

Browse all 70+ available metrics at metricduck.com/metrics.

Full Source Code

The complete pulse.py is on GitHub:

github.com/metric-duck/build-with-metricduck/labs/03-stock-pulse

Clone it, read it, modify it. The only dependency is httpx.

API Access

TierDaily LimitMax TickersCost
Guest (no key)5 requests/day10 tickersFree
Free (registered)500 credits/day200 tickersFree
Builder200,000 credits/mo200 tickers$29/mo
Production1,000,000 credits/mo200 tickers$79/mo

Guest access works immediately — no signup required. Each pulse check costs 35 credits. Register free for 500 credits/day — enough for ~14 pulse checks every day. Builder tier unlocks serious development.

Get your API key

MetricDuck Team

Building financial intelligence you can trust. Sourced directly from SEC Edgar.