feat(energy): surface coal and TTF gas spot prices in analyst context (#2715)

* feat(intelligence): surface coal and TTF gas spot prices in analyst context

- Add TTF=F (TTF Natural Gas) to scripts/shared/commodities.json after NG=F
- Add coalSpotPrice and gasSpotTtf fields to AnalystContext interface
- Add extractCommodityQuote and buildSpotCommodityLine helpers in chat-analyst-context.ts
- Derive coal (MTF=F) and TTF (TTF=F) from already-fetched commodities bootstrap, no extra Redis calls
- Gate on SPOT_ENERGY_DOMAINS (economic, geo, all)
- Register both fields in SOURCE_LABELS with CoalSpot/GasTTF labels
- Add coalSpotPrice and gasSpotTtf to DOMAIN_SECTIONS for geo and economic in chat-analyst-prompt.ts
- Push Coal Spot Price and TTF Gas Price sections into context when present

* fix(energy): use correct unit for Newcastle coal (/t not /MWh)

buildSpotCommodityLine hardcoded /MWh for all commodities. Coal is
priced per metric ton. Added denominator parameter with /MWh default,
pass /t for Newcastle coal.
This commit is contained in:
Elie Habib
2026-04-05 13:29:14 +04:00
committed by GitHub
parent 066712e859
commit 4053a5119a
4 changed files with 38 additions and 2 deletions

View File

@@ -10,6 +10,7 @@
{ "symbol": "CL=F", "name": "Crude Oil WTI", "display": "OIL" },
{ "symbol": "BZ=F", "name": "Brent Crude", "display": "BRENT" },
{ "symbol": "NG=F", "name": "Natural Gas", "display": "NATGAS" },
{ "symbol": "TTF=F", "name": "TTF Natural Gas", "display": "TTF GAS" },
{ "symbol": "RB=F", "name": "Gasoline RBOB", "display": "GASOLINE" },
{ "symbol": "HO=F", "name": "Heating Oil", "display": "HEATING OIL" },
{ "symbol": "URA", "name": "Uranium (Global X)", "display": "URANIUM" },

View File

@@ -28,6 +28,8 @@ export interface AnalystContext {
liveHeadlines: string;
relevantArticles: string;
energyExposure: string;
coalSpotPrice: string;
gasSpotTtf: string;
activeSources: string[];
degraded: boolean;
}
@@ -230,6 +232,27 @@ function buildEnergyExposure(data: unknown): string {
return lines.join('\n');
}
function extractCommodityQuote(commodities: unknown, symbol: string): Record<string, unknown> | null {
if (!commodities || typeof commodities !== 'object') return null;
const d = commodities as Record<string, unknown>;
const quotes = Array.isArray(d.quotes) ? d.quotes : [];
const q = quotes.find((q: unknown) => {
const quote = q as Record<string, unknown>;
return safeStr(quote.symbol) === symbol;
});
return q ? (q as Record<string, unknown>) : null;
}
function buildSpotCommodityLine(commodities: unknown, symbol: string, label: string, unit: string, denominator = '/MWh'): string {
const q = extractCommodityQuote(commodities, symbol);
if (!q) return '';
const price = safeNum(q.price);
const change = safeNum(q.change ?? q.changePercent);
if (!price) return '';
const sign = change >= 0 ? '+' : '';
return `${label}: ${unit}${price.toFixed(2)}${denominator} (${sign}${change.toFixed(2)}% today)`;
}
function buildCountryBrief(data: unknown): string {
if (!data || typeof data !== 'object') return '';
const d = data as Record<string, unknown>;
@@ -417,6 +440,8 @@ const SOURCE_LABELS: Array<[keyof Omit<AnalystContext, 'timestamp' | 'degraded'
['forecasts', 'Forecasts'],
['marketData', 'Markets'],
['energyExposure', 'EnergyMix'],
['coalSpotPrice', 'CoalSpot'],
['gasSpotTtf', 'GasTTF'],
['macroSignals', 'Macro'],
['predictionMarkets', 'Prediction'],
['countryBrief', 'Country'],
@@ -452,6 +477,9 @@ export async function assembleAnalystContext(
const ENERGY_EXPOSURE_DOMAINS = new Set(['geo', 'economic', 'all']);
const needsEnergyExposure = ENERGY_EXPOSURE_DOMAINS.has(resolvedDomain);
const SPOT_ENERGY_DOMAINS = new Set(['economic', 'geo', 'all']);
const needsSpotEnergy = SPOT_ENERGY_DOMAINS.has(resolvedDomain);
const [
insightsResult,
riskResult,
@@ -503,6 +531,8 @@ export async function assembleAnalystContext(
marketData: buildMarketData(get(stocksResult), get(commoditiesResult)),
macroSignals: buildMacroSignals(get(macroResult)),
energyExposure: buildEnergyExposure(get(energyExposureResult)),
coalSpotPrice: needsSpotEnergy ? buildSpotCommodityLine(get(commoditiesResult), 'MTF=F', 'Newcastle coal', '$', '/t') : '',
gasSpotTtf: needsSpotEnergy ? buildSpotCommodityLine(get(commoditiesResult), 'TTF=F', 'TTF gas', '€') : '',
predictionMarkets: buildPredictionMarkets(get(predResult)),
countryBrief: buildCountryBrief(get(countryResult)),
liveHeadlines: getStr(headlinesResult),

View File

@@ -10,9 +10,9 @@ const DOMAIN_EMPHASIS: Record<string, string> = {
/** Context fields included per domain. 'all' includes everything. */
const DOMAIN_SECTIONS: Record<string, Set<string>> = {
market: new Set(['relevantArticles', 'marketData', 'macroSignals', 'marketImplications', 'predictionMarkets', 'forecasts', 'liveHeadlines']),
geo: new Set(['relevantArticles', 'worldBrief', 'riskScores', 'forecasts', 'predictionMarkets', 'countryBrief', 'energyExposure', 'liveHeadlines']),
geo: new Set(['relevantArticles', 'worldBrief', 'riskScores', 'forecasts', 'predictionMarkets', 'countryBrief', 'energyExposure', 'coalSpotPrice', 'gasSpotTtf', 'liveHeadlines']),
military: new Set(['relevantArticles', 'worldBrief', 'riskScores', 'forecasts', 'countryBrief', 'liveHeadlines']),
economic: new Set(['relevantArticles', 'marketData', 'macroSignals', 'marketImplications', 'riskScores', 'energyExposure', 'liveHeadlines']),
economic: new Set(['relevantArticles', 'marketData', 'macroSignals', 'marketImplications', 'riskScores', 'energyExposure', 'coalSpotPrice', 'gasSpotTtf', 'liveHeadlines']),
};
export function buildAnalystSystemPrompt(ctx: AnalystContext, domainFocus?: string): string {
@@ -44,6 +44,10 @@ export function buildAnalystSystemPrompt(ctx: AnalystContext, domainFocus?: stri
contextSections.push(`## ${ctx.macroSignals}`);
if (ctx.energyExposure && include('energyExposure'))
contextSections.push(`## Energy Exposure\n${ctx.energyExposure}`);
if (ctx.coalSpotPrice && include('coalSpotPrice'))
contextSections.push(`## Coal Spot Price\n${ctx.coalSpotPrice}`);
if (ctx.gasSpotTtf && include('gasSpotTtf'))
contextSections.push(`## TTF Gas Price\n${ctx.gasSpotTtf}`);
if (ctx.predictionMarkets && include('predictionMarkets'))
contextSections.push(`## ${ctx.predictionMarkets}`);
if (ctx.countryBrief && include('countryBrief'))

View File

@@ -10,6 +10,7 @@
{ "symbol": "CL=F", "name": "Crude Oil WTI", "display": "OIL" },
{ "symbol": "BZ=F", "name": "Brent Crude", "display": "BRENT" },
{ "symbol": "NG=F", "name": "Natural Gas", "display": "NATGAS" },
{ "symbol": "TTF=F", "name": "TTF Natural Gas", "display": "TTF GAS" },
{ "symbol": "RB=F", "name": "Gasoline RBOB", "display": "GASOLINE" },
{ "symbol": "HO=F", "name": "Heating Oil", "display": "HEATING OIL" },
{ "symbol": "URA", "name": "Uranium (Global X)", "display": "URANIUM" },