Auto Loan Credit
ABS-EE Loan-Level Panel — SEC EDGAR
Live delinquency trends from SEC EDGAR ABS-EE filings. Loan-level auto credit data, parsed from servicer reports so you don’t have to.
Data source: SEC EDGAR ABS-EE autoloan panel —
loan-level data from asset-backed securities filings.
Parsed from raw XML filings via the bens-data-lake pipeline.
Coverage limited to SEC-registered auto ABS trusts.
See Data Sources for full methodology.
This is structured finance data — the kind of thing you’d normally pay a Bloomberg terminal to look at, except it’s sitting in SEC EDGAR and nobody told you. ABS-EE filings contain loan-level performance data for auto loan ABS trusts: origination date, balance, days past due, payment status. The good stuff.
Coverage varies by reporting month as trusts enter and exit the panel. A delinquency rate computed from 2M loans means something very different from one computed from 200K. Check the volume chart.
Code
import { Plot } from "npm:@observablehq/plot"
data = abs_data.map(d => ({...d, filing_date: new Date(d.filing_date)}))
DPD_META = [
{key: "d30_rate", label: "30+ days", color: "#2563eb"},
{key: "d60_rate", label: "60+ days", color: "#7c3aed"},
{key: "d90_rate", label: "90+ days", color: "#dc2626"}
]
RECESSIONS = [
{start: new Date("2020-02-01"), end: new Date("2020-04-30"), label: "COVID-19"}
]
fmtDate = d => d.toLocaleString("en-US", {month: "short", year: "numeric"})
fmtPct = d => `${(d * 100).toFixed(2)}%`
fmtN = d => d >= 1e6 ? `${(d/1e6).toFixed(1)}M` : d >= 1e3 ? `${(d/1e3).toFixed(0)}K` : String(d)Latest Month
Code
{
if (!data_ok) return html`<div class="chart-error">⚠ Run <code>python analysis/aggregate_abs_ee.py</code> to generate dashboard data.</div>`
const latest = data.reduce((max, d) => d.filing_date > max ? d.filing_date : max, new Date(0))
const row = data.find(d => d.filing_date.getTime() === latest.getTime())
if (!row) return null
const prev = data.slice().sort((a, b) => b.filing_date - a.filing_date)[1]
const strip = html`<div class="kpi-strip"></div>`
for (const {key, label} of DPD_META) {
const val = row[key]
const chg = prev ? val - prev[key] : null
const chgStr = chg !== null ? ` ${chg > 0 ? "▲" : "▼"} ${Math.abs(chg * 100).toFixed(2)}pp` : ""
const chgColor = chg > 0 ? "#dc2626" : "#16a34a"
const card = html`<div class="kpi-card">
<div class="kpi-value">${fmtPct(val)}</div>
<div class="kpi-label">${label} DPD<span style="color:${chgColor};margin-left:6px;font-size:0.7rem">${chgStr}</span></div>
</div>`
strip.appendChild(card)
}
const volCard = html`<div class="kpi-card">
<div class="kpi-value">${fmtN(row.total_loans)}</div>
<div class="kpi-label">Loans in panel</div>
</div>`
strip.appendChild(volCard)
const surveyed = html`<div style="font-family:'DM Mono',monospace;font-size:0.7rem;color:#9A9A9A;margin-bottom:1.5rem;">
Reporting period: ${fmtDate(latest)}
</div>`
return html`<div>${surveyed}${strip}</div>`
}Delinquency Trends
Code
Code
{
if (!data_ok || data.length === 0) return html`<div class="chart-loading">No data</div>`
const meta = DPD_META.find(d => d.key === dpdBucket)
const now = new Date()
const years = {"1Y": 1, "2Y": 2, "3Y": 3}
const cutoff = timeWindow in years
? new Date(now.getFullYear() - years[timeWindow], now.getMonth())
: new Date(0)
const filtered = data.filter(d => d.filing_date >= cutoff)
const w = Math.min(800, window.innerWidth - 48)
return Plot.plot({
marks: [
...RECESSIONS.map(r => Plot.rect([r], {
x1: "start", x2: "end",
y1: 0, y2: 1,
fill: "#888", fillOpacity: 0.07
})),
Plot.areaY(filtered, {
x: "filing_date",
y: dpdBucket,
fill: meta.color,
fillOpacity: 0.12,
curve: "monotone-x"
}),
Plot.line(filtered, {
x: "filing_date",
y: dpdBucket,
stroke: meta.color,
strokeWidth: 2.5,
curve: "monotone-x"
}),
Plot.crosshair(filtered, {x: "filing_date", y: dpdBucket, color: meta.color}),
Plot.tip(filtered, Plot.pointer({
x: "filing_date",
y: dpdBucket,
title: d => `${meta.label} DPD\n${fmtDate(d.filing_date)}: ${fmtPct(d[dpdBucket])}`
}))
],
x: {label: null, type: "time", tickFormat: "%b '%y"},
y: {
label: `${meta.label} DPD rate`,
tickFormat: fmtPct,
grid: true,
zero: true
},
width: w,
height: 320,
marginLeft: 65,
marginRight: 20,
style: {background: "transparent", fontSize: "12px", fontFamily: "DM Mono, monospace"}
})
}All Three Buckets
Code
{
if (!data_ok || data.length === 0) return null
const now = new Date()
const cutoff = timeWindow in {"1Y":1,"2Y":2,"3Y":3}
? new Date(now.getFullYear() - {"1Y":1,"2Y":2,"3Y":3}[timeWindow], now.getMonth())
: new Date(0)
const long = data
.filter(d => d.filing_date >= cutoff)
.flatMap(d => DPD_META.map(m => ({
filing_date: d.filing_date,
label: m.label,
color: m.color,
rate: d[m.key]
})))
const w = Math.min(800, window.innerWidth - 48)
return Plot.plot({
marks: [
...RECESSIONS.map(r => Plot.rect([r], {
x1: "start", x2: "end",
y1: 0, y2: 1,
fill: "#888", fillOpacity: 0.07
})),
Plot.line(long, {
x: "filing_date",
y: "rate",
stroke: "color",
strokeWidth: 2,
curve: "monotone-x"
}),
Plot.crosshair(long, {x: "filing_date", y: "rate", color: "color"}),
Plot.tip(long, Plot.pointer({
x: "filing_date",
y: "rate",
title: d => `${d.label}\n${fmtDate(d.filing_date)}: ${fmtPct(d.rate)}`
}))
],
x: {label: null, type: "time", tickFormat: "%b '%y"},
y: {
label: "DPD rate",
tickFormat: fmtPct,
grid: true,
zero: true
},
color: {
domain: DPD_META.map(d => d.color),
range: DPD_META.map(d => d.color),
legend: false
},
width: w,
height: 280,
marginLeft: 65,
marginRight: 20,
style: {background: "transparent", fontSize: "12px", fontFamily: "DM Mono, monospace"}
})
}Loan Volume
Code
{
if (!data_ok || data.length === 0) return null
const now = new Date()
const cutoff = timeWindow in {"1Y":1,"2Y":2,"3Y":3}
? new Date(now.getFullYear() - {"1Y":1,"2Y":2,"3Y":3}[timeWindow], now.getMonth())
: new Date(0)
const filtered = data.filter(d => d.filing_date >= cutoff)
const w = Math.min(800, window.innerWidth - 48)
return Plot.plot({
marks: [
Plot.areaY(filtered, {
x: "filing_date",
y: "total_loans",
fill: "#6b7280",
fillOpacity: 0.15,
curve: "monotone-x"
}),
Plot.line(filtered, {
x: "filing_date",
y: "total_loans",
stroke: "#6b7280",
strokeWidth: 1.5,
curve: "monotone-x"
}),
Plot.tip(filtered, Plot.pointer({
x: "filing_date",
y: "total_loans",
title: d => `${fmtDate(d.filing_date)}: ${fmtN(d.total_loans)} loans`
}))
],
x: {label: null, type: "time", tickFormat: "%b '%y"},
y: {
label: "Loans in panel",
tickFormat: fmtN,
grid: true,
zero: true
},
width: w,
height: 200,
marginLeft: 65,
marginRight: 20,
style: {background: "transparent", fontSize: "12px", fontFamily: "DM Mono, monospace"}
})
}