Closes #ISSUE-008 - Overreaction Scanner Overhaul: GJR-GARCH rebound gauge, catalyst drawers, and Category C small-caps
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user