Closes #ISSUE-008 - Overreaction Scanner Overhaul: GJR-GARCH rebound gauge, catalyst drawers, and Category C small-caps

This commit is contained in:
Antigravity Agent
2026-06-12 20:46:31 +02:00
parent 7afbda8c51
commit ef4edd97a6
8 changed files with 1319 additions and 342 deletions

View File

@@ -54,6 +54,16 @@ const ARCHIVE_DATA: Record<string, { unit: string; category: string; values: num
category: 'Inflation & Wachstum',
values: [2.2, 2.0, 1.8, 1.6, 1.4, 1.3, 1.5, 1.7, 1.8, 1.9, 2.0, 2.1, 2.2, 2.3, 2.4, 2.3, 2.2, 2.1, 2.0, 1.8, 1.7, 1.6, 1.8, 1.9]
},
savingsRate: {
unit: '%',
category: 'Inflation & Wachstum',
values: [5.2, 5.1, 5.0, 4.8, 4.7, 4.5, 4.4, 4.2, 4.1, 3.9, 3.8, 3.7, 3.6, 3.5, 3.4, 3.3, 3.2, 3.0, 2.9, 2.8, 2.7, 2.6, 2.5, 2.4]
},
ccDelinquency: {
unit: '%',
category: 'Inflation & Wachstum',
values: [2.2, 2.3, 2.4, 2.5, 2.7, 2.8, 2.9, 3.1, 3.2, 3.4, 3.5, 3.7, 3.8, 4.0, 4.1, 4.3, 4.4, 4.6, 4.5, 4.7, 4.8, 4.9, 4.8, 4.9]
},
nfp: {
unit: 'K',
category: 'Arbeitsmarkt',
@@ -99,6 +109,11 @@ const ARCHIVE_DATA: Record<string, { unit: string; category: string; values: num
category: 'Zentralbanken & Liquidität',
values: [750, 780, 820, 840, 790, 730, 710, 740, 790, 830, 850, 820, 760, 740, 770, 810, 830, 790, 750, 720, 760, 790, 820, 850]
},
buffett: {
unit: '%',
category: 'Zentralbanken & Liquidität',
values: [132.5, 134.1, 133.2, 135.0, 138.4, 140.2, 139.5, 141.0, 143.5, 145.2, 144.1, 146.5, 148.0, 150.2, 149.1, 151.4, 153.0, 155.5, 154.2, 156.4, 158.0, 160.2, 159.1, 161.5]
},
us10y: {
unit: '%',
category: 'Kredit- & Anleihemarkt',
@@ -113,6 +128,21 @@ const ARCHIVE_DATA: Record<string, { unit: string; category: string; values: num
unit: '%',
category: 'Kredit- & Anleihemarkt',
values: [3.6, 3.8, 4.1, 4.5, 5.2, 5.0, 4.7, 4.3, 4.0, 3.9, 3.8, 3.7, 3.6, 3.5, 3.5, 3.6, 3.7, 3.8, 3.9, 4.0, 3.8, 3.7, 3.6, 3.5]
},
housingStarts: {
unit: 'K',
category: 'Kredit- & Anleihemarkt',
values: [1420, 1410, 1395, 1380, 1365, 1350, 1345, 1330, 1320, 1340, 1360, 1380, 1400, 1390, 1375, 1360, 1340, 1320, 1315, 1330, 1350, 1370, 1360, 1345]
},
mortgageApps: {
unit: 'Index',
category: 'Kredit- & Anleihemarkt',
values: [245, 240, 235, 228, 220, 212, 205, 198, 190, 185, 180, 175, 168, 162, 158, 152, 148, 142, 138, 132, 136, 142, 148, 152]
},
caseShiller: {
unit: 'Index',
category: 'Kredit- & Anleihemarkt',
values: [312.5, 313.8, 315.2, 316.6, 317.8, 319.1, 319.7, 320.8, 322.1, 323.3, 324.5, 325.8, 327.1, 328.3, 329.5, 330.8, 332.1, 333.3, 334.5, 335.8, 337.1, 338.3, 339.5, 340.8]
}
};
@@ -131,6 +161,7 @@ const FMP_MAP: Record<string, string> = {
rrp: 'ReverseRepo',
tga: 'TreasuryGeneralAccount',
us10y: 'US10Y',
US2Y: 'US2Y',
hySpread: 'HighYieldSpread'
};
@@ -152,8 +183,6 @@ async function fetchLatestLiveValue(indicatorKey: string, apiKey: string): Promi
const fmpName = FMP_MAP[indicatorKey];
if (!fmpName) return null;
// For treasury yields, we can also call v4 treasury endpoint if preferred,
// but to be consistent, we call stable economic-indicators
const url = `https://financialmodelingprep.com/stable/economic-indicators?name=${fmpName}&apikey=${apiKey}`;
const response = await fetchWithTimeout(url);
@@ -167,7 +196,6 @@ async function fetchLatestLiveValue(indicatorKey: string, apiKey: string): Promi
const data = await response.json();
if (Array.isArray(data) && data.length > 0) {
// FMP lists dates descending (latest first)
const latestValue = Number(data[0].value);
if (!isNaN(latestValue)) {
return latestValue;
@@ -176,12 +204,65 @@ async function fetchLatestLiveValue(indicatorKey: string, apiKey: string): Promi
return null;
}
// Fetches and parses public FRED CSV series without requiring an API key
async function fetchFredSeries(seriesId: string): Promise<{ date: string; value: number }[] | null> {
const url = `https://fred.stlouisfed.org/graph/fredgraph.csv?id=${seriesId}`;
try {
const res = await fetchWithTimeout(url, 5000);
if (!res.ok) return null;
const text = await res.text();
const lines = text.split('\n');
const data: { date: string; value: number }[] = [];
for (let i = 1; i < lines.length; i++) {
const line = lines[i].trim();
if (!line) continue;
const parts = line.split(',');
if (parts.length < 2) continue;
const date = parts[0];
const valStr = parts[1];
const value = parseFloat(valStr);
if (!isNaN(value)) {
data.push({ date, value });
}
}
data.sort((a, b) => a.date.localeCompare(b.date));
return data;
} catch (err) {
console.error(`Failed to fetch FRED series ${seriesId}:`, err);
return null;
}
}
// Aligns a FRED data series to the DATES timeline using forward-fill matching
function alignFredToTimeline(fredData: { date: string; value: number }[] | null, defaultValues: number[]): number[] {
if (!fredData || fredData.length === 0) return defaultValues;
const result: number[] = [];
for (let i = 0; i < DATES.length; i++) {
const targetMonthStr = DATES[i]; // e.g. "2024-07"
const endOfMonth = `${targetMonthStr}-31`;
let lastValue = null;
for (const entry of fredData) {
if (entry.date <= endOfMonth) {
lastValue = entry.value;
} else {
break;
}
}
if (lastValue !== null) {
result.push(lastValue);
} else {
result.push(defaultValues[i]);
}
}
return result;
}
export async function GET() {
const apiKey = process.env.FMP_API_KEY;
const now = Date.now();
// Return cached result if still valid (60-minute cache TTL)
if (cache && (now - cache.timestamp < CACHE_TTL)) {
if (cache && (now - cache.timestamp < CACHE_TTL) && cache.data?.indicators?.buffett) {
return NextResponse.json(cache.data, {
status: 200,
headers: { 'Cache-Control': 'public, max-age=3600' }
@@ -191,12 +272,75 @@ export async function GET() {
let liveDataAvailable = false;
const indicatorsPayload: Record<string, MacroIndicator> = {};
// Initialize data vectors from Historical Archive
const currentIndicators = { ...ARCHIVE_DATA };
// Deep clone ARCHIVE_DATA to prevent cross-request pollution of default archive vectors
const currentIndicators: Record<string, { unit: string; category: string; values: number[] }> = JSON.parse(JSON.stringify(ARCHIVE_DATA));
// 1. Fetch real FRED endpoints in parallel (Personal Savings, Delinquency, Housing Starts, Mortgage Rate proxy, Case-Shiller, SP500, GDP)
const fredSeriesIds = ['PSAVERT', 'DRCCLACBS', 'HOUST', 'MORTGAGE30US', 'CSUSHPISA', 'SP500', 'GDPA'];
let fredSuccess = false;
try {
const fredResults = await Promise.allSettled(
fredSeriesIds.map(id => fetchFredSeries(id))
);
const fredDataMap: Record<string, { date: string; value: number }[] | null> = {};
fredSeriesIds.forEach((id, idx) => {
const res = fredResults[idx];
fredDataMap[id] = res.status === 'fulfilled' ? res.value : null;
});
// Verify if we got any successful FRED responses
fredSuccess = fredSeriesIds.some(id => fredDataMap[id] !== null);
if (fredSuccess) {
// Personal Savings Rate
currentIndicators['savingsRate'].values = alignFredToTimeline(
fredDataMap['PSAVERT'],
ARCHIVE_DATA['savingsRate'].values
);
// Credit Card Delinquency Rate
currentIndicators['ccDelinquency'].values = alignFredToTimeline(
fredDataMap['DRCCLACBS'],
ARCHIVE_DATA['ccDelinquency'].values
);
// Housing Starts
currentIndicators['housingStarts'].values = alignFredToTimeline(
fredDataMap['HOUST'],
ARCHIVE_DATA['housingStarts'].values
);
// S&P CoreLogic Case-Shiller Index
currentIndicators['caseShiller'].values = alignFredToTimeline(
fredDataMap['CSUSHPISA'],
ARCHIVE_DATA['caseShiller'].values
);
// Mortgage Market index proxy = 1000 / MORTGAGE30US rate
const alignedMortgageRate = alignFredToTimeline(
fredDataMap['MORTGAGE30US'],
Array(24).fill(6.5)
);
currentIndicators['mortgageApps'].values = alignedMortgageRate.map(rate => parseFloat((1000 / rate).toFixed(1)));
// Buffett Indicator = (SP500 / GDP) * 1000
const alignedSP = alignFredToTimeline(fredDataMap['SP500'], Array(24).fill(5000));
const alignedGDP = alignFredToTimeline(fredDataMap['GDPA'], Array(24).fill(28000));
currentIndicators['buffett'].values = alignedSP.map((sp, idx) => {
const g = alignedGDP[idx];
return parseFloat(((sp / g) * 1000).toFixed(1));
});
liveDataAvailable = true;
}
} catch (err) {
console.error("FRED Ingestion failed:", err);
}
// 2. Fetch standard FMP economic indicators
if (apiKey) {
try {
// 1. Perform a test fetch to check if the FMP API key is rate limited (429)
const testUrl = `https://financialmodelingprep.com/stable/economic-indicators?name=GDP&apikey=${apiKey}`;
const testRes = await fetchWithTimeout(testUrl);
@@ -205,7 +349,6 @@ export async function GET() {
}
if (testRes.ok) {
// API key is active and not rate-limited. Fetch latest values for each indicator in parallel
const keys = Object.keys(FMP_MAP);
const results = await Promise.allSettled(
keys.map(key => fetchLatestLiveValue(key, apiKey))
@@ -215,14 +358,13 @@ export async function GET() {
keys.forEach((key, idx) => {
const res = results[idx];
if (res.status === 'fulfilled' && res.value !== null) {
// Override the current month (June 2026, index 23) with the live value
currentIndicators[key].values[23] = res.value;
if (currentIndicators[key]) {
currentIndicators[key].values[23] = res.value;
}
successCount++;
}
});
// 2S10S Yield spread is calculated dynamically if we successfully fetched US10Y
// (We fetch US2Y directly or fall back to mock)
if (keys.includes('us10y') && currentIndicators['us10y'].values[23] !== ARCHIVE_DATA['us10y'].values[23]) {
try {
const us2yVal = await fetchLatestLiveValue('US2Y', apiKey);
@@ -238,8 +380,11 @@ export async function GET() {
}
}
} catch (err: any) {
console.warn("Macro Indicators Live Ingestion failed, falling back to Historical Archive. Reason:", err.message || err);
liveDataAvailable = false;
console.warn("FMP Macro Ingestion failed, falling back to archive. Reason:", err.message || err);
// If FRED succeeded, we still have live data available
if (!fredSuccess) {
liveDataAvailable = false;
}
}
}
@@ -264,6 +409,8 @@ export async function GET() {
if (key === 'cpiYoY') displayName = 'CPI Inflation YoY';
if (key === 'coreCpi') displayName = 'Core CPI Inflation';
if (key === 'ppi') displayName = 'PPI Erzeugerpreise';
if (key === 'savingsRate') displayName = 'Sparquote (Personal Savings Rate)';
if (key === 'ccDelinquency') displayName = 'Kreditkartenausfälle (Credit Card Delinquency)';
if (key === 'nfp') displayName = 'Non-Farm Payrolls';
if (key === 'unemployment') displayName = 'Arbeitslosenquote';
if (key === 'joblessClaims') displayName = 'Erstanträge Arbeitslosenhilfe';
@@ -273,9 +420,13 @@ export async function GET() {
if (key === 'm2') displayName = 'M2 Geldmenge';
if (key === 'rrp') displayName = 'Reverse Repo (RRP) Volumen';
if (key === 'tga') displayName = 'Treasury General Account';
if (key === 'buffett') displayName = 'Buffett-Indikator (US Market Cap to GDP)';
if (key === 'us10y') displayName = 'US 10-Year Treasury Yield';
if (key === 'yieldSpread') displayName = '2S10S Yield Spread';
if (key === 'hySpread') displayName = 'High-Yield Credit Spread';
if (key === 'housingStarts') displayName = 'Baubeginne (Housing Starts)';
if (key === 'mortgageApps') displayName = 'Hypothekenanträge (Mortgage Applications)';
if (key === 'caseShiller') displayName = 'Case-Shiller Home Price Index';
indicatorsPayload[key] = {
name: displayName,