fed-policy-cli
Version:
Macro trading intelligence from Fed policy analysis. Transform economic data into actionable trading insights by analyzing historical Fed policy analogues.
389 lines (333 loc) • 13.4 kB
text/typescript
// /src/services/crossAssetAnalysis.ts
import { HistoricalAnalogue, EconomicDataPoint } from '../types';
import { CrossAssetDataPoint, getAllCrossAssetData, getAllETFData, getAllETFFundamentals } from './database';
import { ETFDataPoint, ETFFundamentals, TARGET_ETFS } from './etfDataService';
import { CROSS_ASSET_SERIES } from '../constants';
export interface AssetPerformance {
symbol: string;
name: string;
assetClass: string;
startPrice: number;
endPrice: number;
totalReturn: number; // Percentage
annualizedReturn: number; // Percentage
volatility: number; // Standard deviation of daily returns
sharpeRatio: number; // Risk-adjusted return
maxDrawdown: number; // Maximum peak-to-trough decline
}
export interface CrossAssetAnalogue extends HistoricalAnalogue {
assetPerformance: AssetPerformance[];
crossAssetData: {
commodities: AssetPerformance[];
currencies: AssetPerformance[];
etfs: AssetPerformance[];
};
}
export interface CrossAssetSummary {
period: string;
bestPerformers: AssetPerformance[];
worstPerformers: AssetPerformance[];
sectorRotation: {
intoSectors: string[];
outOfSectors: string[];
};
commodityTrends: {
rising: string[];
falling: string[];
};
currencyStrength: {
dollarDirection: 'strengthening' | 'weakening' | 'neutral';
magnitude: number;
};
}
/**
* Calculate asset performance metrics for a given time period
*/
export const calculateAssetPerformance = (
data: any[],
startDate: string,
endDate: string,
symbol: string,
name: string,
assetClass: string,
priceField: string = 'close'
): AssetPerformance | null => {
// Filter data for the period
const periodData = data.filter(d => d.date >= startDate && d.date <= endDate);
if (periodData.length < 2) {
return null;
}
// Sort by date
periodData.sort((a, b) => a.date.localeCompare(b.date));
const startPrice = periodData[0][priceField];
const endPrice = periodData[periodData.length - 1][priceField];
if (!startPrice || !endPrice || startPrice <= 0 || endPrice <= 0) {
return null;
}
// Calculate total return
const totalReturn = ((endPrice - startPrice) / startPrice) * 100;
// Calculate time period in years
const startTime = new Date(startDate).getTime();
const endTime = new Date(endDate).getTime();
const yearsElapsed = (endTime - startTime) / (1000 * 60 * 60 * 24 * 365.25);
// Calculate annualized return
const annualizedReturn = yearsElapsed > 0 ? (Math.pow(endPrice / startPrice, 1 / yearsElapsed) - 1) * 100 : totalReturn;
// Calculate daily returns for volatility and other metrics
const dailyReturns: number[] = [];
for (let i = 1; i < periodData.length; i++) {
const prevPrice = periodData[i - 1][priceField];
const currPrice = periodData[i][priceField];
if (prevPrice > 0 && currPrice > 0) {
dailyReturns.push((currPrice - prevPrice) / prevPrice);
}
}
// Calculate volatility (standard deviation of daily returns, annualized)
const avgReturn = dailyReturns.reduce((sum, r) => sum + r, 0) / dailyReturns.length;
const variance = dailyReturns.reduce((sum, r) => sum + Math.pow(r - avgReturn, 2), 0) / dailyReturns.length;
const volatility = Math.sqrt(variance) * Math.sqrt(252) * 100; // Annualized volatility
// Calculate Sharpe ratio (assuming 0% risk-free rate for simplicity)
const sharpeRatio = volatility > 0 ? annualizedReturn / volatility : 0;
// Calculate maximum drawdown
let maxDrawdown = 0;
let peak = startPrice;
for (const point of periodData) {
const price = point[priceField];
if (price > peak) {
peak = price;
}
const drawdown = (peak - price) / peak;
if (drawdown > maxDrawdown) {
maxDrawdown = drawdown;
}
}
maxDrawdown *= 100; // Convert to percentage
return {
symbol,
name,
assetClass,
startPrice,
endPrice,
totalReturn: parseFloat(totalReturn.toFixed(2)),
annualizedReturn: parseFloat(annualizedReturn.toFixed(2)),
volatility: parseFloat(volatility.toFixed(2)),
sharpeRatio: parseFloat(sharpeRatio.toFixed(3)),
maxDrawdown: parseFloat(maxDrawdown.toFixed(2))
};
};
/**
* Analyze cross-asset performance during historical analogues
*/
export const analyzeCrossAssetPerformance = async (
analogues: HistoricalAnalogue[]
): Promise<CrossAssetAnalogue[]> => {
// Load all cross-asset and ETF data
const [crossAssetData, etfData, etfFundamentals] = await Promise.all([
getAllCrossAssetData(),
getAllETFData(),
getAllETFFundamentals()
]);
const crossAssetAnalogues: CrossAssetAnalogue[] = [];
for (const analogue of analogues) {
const startDate = analogue.startDate;
const endDate = analogue.endDate;
const assetPerformances: AssetPerformance[] = [];
// Analyze FRED cross-asset series (commodities, currencies)
for (const [seriesId, seriesInfo] of Object.entries(CROSS_ASSET_SERIES)) {
const performance = calculateAssetPerformance(
crossAssetData.map(d => ({ date: d.date, close: d[seriesId] })),
startDate,
endDate,
seriesId,
seriesInfo.name,
getCrossAssetClass(seriesId),
'close'
);
if (performance) {
assetPerformances.push(performance);
}
}
// Analyze ETF performance
const etfPerformances: AssetPerformance[] = [];
for (const [symbol, etfInfo] of Object.entries(TARGET_ETFS)) {
const etfDataForSymbol = etfData.filter(d => d.symbol === symbol);
const performance = calculateAssetPerformance(
etfDataForSymbol,
startDate,
endDate,
symbol,
etfInfo.name,
etfInfo.assetClass,
'close'
);
if (performance) {
etfPerformances.push(performance);
assetPerformances.push(performance);
}
}
// Categorize performance by asset class
const commodities = assetPerformances.filter(p => p.assetClass === 'Commodities');
const currencies = assetPerformances.filter(p => p.assetClass === 'Currency');
const etfs = etfPerformances;
crossAssetAnalogues.push({
...analogue,
assetPerformance: assetPerformances,
crossAssetData: {
commodities,
currencies,
etfs
}
});
}
return crossAssetAnalogues;
};
/**
* Get asset class for cross-asset FRED series
*/
const getCrossAssetClass = (seriesId: string): string => {
if (seriesId.includes('OIL') || seriesId.includes('COMMODITY') || seriesId.includes('ENERGY') ||
seriesId.includes('COPPER') || seriesId.includes('WHEAT') || seriesId.includes('ALUMINUM')) {
return 'Commodities';
}
if (seriesId.includes('GOLD') || seriesId.includes('PCU') || seriesId.includes('WPU')) {
return 'Precious Metals';
}
if (seriesId.includes('USD') || seriesId.includes('DTWEX') || seriesId.includes('RBUS')) {
return 'Currency';
}
return 'Other';
};
/**
* Generate cross-asset summary for a set of analogues
*/
export const generateCrossAssetSummary = (
crossAssetAnalogues: CrossAssetAnalogue[]
): CrossAssetSummary => {
if (crossAssetAnalogues.length === 0) {
return {
period: 'No data',
bestPerformers: [],
worstPerformers: [],
sectorRotation: { intoSectors: [], outOfSectors: [] },
commodityTrends: { rising: [], falling: [] },
currencyStrength: { dollarDirection: 'neutral', magnitude: 0 }
};
}
// Aggregate performance across all analogues
const allPerformances = crossAssetAnalogues.flatMap(a => a.assetPerformance);
// Sort by total return
allPerformances.sort((a, b) => b.totalReturn - a.totalReturn);
const bestPerformers = allPerformances.slice(0, 5);
const worstPerformers = allPerformances.slice(-5).reverse();
// Analyze sector rotation (ETFs only)
const etfPerformances = allPerformances.filter(p => p.symbol.length <= 4); // ETF symbols are typically 3-4 characters
const etfAvgReturn = etfPerformances.reduce((sum, p) => sum + p.totalReturn, 0) / etfPerformances.length;
const intoSectors = etfPerformances.filter(p => p.totalReturn > etfAvgReturn).map(p => p.assetClass);
const outOfSectors = etfPerformances.filter(p => p.totalReturn < etfAvgReturn).map(p => p.assetClass);
// Analyze commodity trends
const commodityPerformances = allPerformances.filter(p => p.assetClass === 'Commodities');
const commodityAvgReturn = commodityPerformances.reduce((sum, p) => sum + p.totalReturn, 0) / commodityPerformances.length;
const rising = commodityPerformances.filter(p => p.totalReturn > commodityAvgReturn).map(p => p.name);
const falling = commodityPerformances.filter(p => p.totalReturn < commodityAvgReturn).map(p => p.name);
// Analyze USD strength
const usdPerformances = allPerformances.filter(p => p.assetClass === 'Currency');
const avgUsdReturn = usdPerformances.reduce((sum, p) => sum + p.totalReturn, 0) / usdPerformances.length;
let dollarDirection: 'strengthening' | 'weakening' | 'neutral' = 'neutral';
if (avgUsdReturn > 2) dollarDirection = 'strengthening';
else if (avgUsdReturn < -2) dollarDirection = 'weakening';
const startDate = crossAssetAnalogues[0]?.startDate || '';
const endDate = crossAssetAnalogues[0]?.endDate || '';
return {
period: `${startDate} to ${endDate}`,
bestPerformers,
worstPerformers,
sectorRotation: {
intoSectors: [...new Set(intoSectors)],
outOfSectors: [...new Set(outOfSectors)]
},
commodityTrends: {
rising: [...new Set(rising)],
falling: [...new Set(falling)]
},
currencyStrength: {
dollarDirection,
magnitude: Math.abs(avgUsdReturn)
}
};
};
/**
* Generate cross-asset trading signals based on historical analogues
*/
export interface CrossAssetTradingSignal {
type: 'BUY' | 'SELL' | 'HOLD';
asset: string;
assetClass: string;
confidence: number; // 0-100
reasoning: string;
expectedReturn: number; // Percentage
timeframe: string;
}
export const generateCrossAssetTradingSignals = (
crossAssetAnalogues: CrossAssetAnalogue[]
): CrossAssetTradingSignal[] => {
if (crossAssetAnalogues.length === 0) return [];
const signals: CrossAssetTradingSignal[] = [];
// Aggregate performance data across analogues
const assetAggregates = new Map<string, { totalReturns: number[]; sharpeRatios: number[]; name: string; assetClass: string; }>();
for (const analogue of crossAssetAnalogues) {
for (const performance of analogue.assetPerformance) {
if (!assetAggregates.has(performance.symbol)) {
assetAggregates.set(performance.symbol, {
totalReturns: [],
sharpeRatios: [],
name: performance.name,
assetClass: performance.assetClass
});
}
const aggregate = assetAggregates.get(performance.symbol)!;
aggregate.totalReturns.push(performance.totalReturn);
aggregate.sharpeRatios.push(performance.sharpeRatio);
}
}
// Generate signals based on historical performance patterns
for (const [symbol, data] of assetAggregates.entries()) {
const avgReturn = data.totalReturns.reduce((sum, r) => sum + r, 0) / data.totalReturns.length;
const avgSharpe = data.sharpeRatios.reduce((sum, r) => sum + r, 0) / data.sharpeRatios.length;
// Calculate consistency (lower standard deviation = higher consistency)
const returnStdDev = Math.sqrt(
data.totalReturns.reduce((sum, r) => sum + Math.pow(r - avgReturn, 2), 0) / data.totalReturns.length
);
const consistency = Math.max(0, 100 - returnStdDev); // Convert to 0-100 scale
// Generate signal based on performance and consistency
let signalType: 'BUY' | 'SELL' | 'HOLD' = 'HOLD';
let confidence = 50;
let reasoning = 'Neutral performance in historical analogues';
if (avgReturn > 5 && avgSharpe > 0.5) {
signalType = 'BUY';
confidence = Math.min(95, 60 + consistency * 0.35);
reasoning = `Strong historical performance: ${avgReturn.toFixed(1)}% avg return, Sharpe ratio ${avgSharpe.toFixed(2)}`;
} else if (avgReturn < -5 && avgSharpe < -0.2) {
signalType = 'SELL';
confidence = Math.min(95, 60 + consistency * 0.35);
reasoning = `Poor historical performance: ${avgReturn.toFixed(1)}% avg return, negative risk-adjusted returns`;
} else if (Math.abs(avgReturn) > 10) {
signalType = avgReturn > 0 ? 'BUY' : 'SELL';
confidence = Math.min(85, 50 + Math.abs(avgReturn) * 2);
reasoning = `Significant directional bias: ${avgReturn.toFixed(1)}% average return in similar periods`;
}
signals.push({
type: signalType,
asset: symbol,
assetClass: data.assetClass,
confidence: Math.round(confidence),
reasoning,
expectedReturn: Math.round(avgReturn * 100) / 100,
timeframe: `${crossAssetAnalogues[0].windowMonths} months (based on ${data.totalReturns.length} historical periods)`
});
}
// Sort by confidence and expected return
signals.sort((a, b) => {
if (a.confidence !== b.confidence) return b.confidence - a.confidence;
return Math.abs(b.expectedReturn) - Math.abs(a.expectedReturn);
});
return signals.slice(0, 10); // Return top 10 signals
};