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.
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:
| Signal | ROIC Trend | PE Trend | What It Means |
|---|---|---|---|
| OPPORTUNITY | Rising | Falling | Quality improving, valuation compressing. The market may not be pricing in the improvement. |
| EARNING IT | Rising | Rising | Quality improving, market recognizing it. Is the premium justified? |
| STABLE | Flat | Flat | No strong trend signal. Fundamentals and valuation are near 2-year norms. |
| WATCH | Falling | Falling | Quality and valuation both declining. The market may be right to de-rate. |
| VALUE TRAP | Falling | Rising | Declining 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:
| Group | Metrics | What It Answers |
|---|---|---|
| Vital Signs | ROIC, Gross Margin, Operating Margin, FCF Margin | How healthy is the business vs its own norm? |
| Valuation | PE Ratio, EV/EBITDA | Is the market paying more or less? (trend only) |
| Growth | Revenue YoY, Revenue 3yr CAGR | Is the business growing? |
| Leverage | Debt/Equity | How 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_ratioandcash_conversionto 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=5andperiod=quarterlyto 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
| Stock | Command | What You Might See |
|---|---|---|
| NVDA | python pulse.py NVDA | ROIC near 2-year highs, PE compressing — OPPORTUNITY |
| AAPL | python pulse.py AAPL | ROIC rising above median, PE rising too — EARNING IT |
| META | python pulse.py META | ROIC declining from highs, PE expanding — VALUE TRAP |
| COST | python pulse.py COST | Consistent quality, PE rising — STABLE or EARNING IT |
| TSLA | python pulse.py TSLA | Volatile multiples — check which signal fires today |
| JPM | python pulse.py JPM | Banks 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
| Tier | Daily Limit | Max Tickers | Cost |
|---|---|---|---|
| Guest (no key) | 5 requests/day | 10 tickers | Free |
| Free (registered) | 500 credits/day | 200 tickers | Free |
| Builder | 200,000 credits/mo | 200 tickers | $29/mo |
| Production | 1,000,000 credits/mo | 200 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.
MetricDuck Team
Building financial intelligence you can trust. Sourced directly from SEC Edgar.