Add widget for transformed data

This commit is contained in:
Jenit Jain 2025-08-10 02:14:20 -07:00
parent 4c6564b5d1
commit 0530d0c840
2 changed files with 122 additions and 68 deletions

View File

@ -308,7 +308,7 @@ const TransformedDataAdapter: React.FC<TransformedDataAdapterProps> = ({ analysi
widgets_config: { widgets_config: {
charts_needed: [ charts_needed: [
{ type: 'price_chart', data_source: 'financial_data.current_price', timeframe: '30_days' }, { type: 'price_chart', data_source: 'financial_data.current_price', timeframe: '30_days' },
{ type: 'technical_indicators', data_source: 'technical_indicators' } { type: 'technical_indicators', data_source: 'technical_indicators', timeframe: '30_days' }
], ],
text_widgets: [ text_widgets: [
{ type: 'expandable_report', title: 'Technical Analysis', content_source: 'text_content.market_report' } { type: 'expandable_report', title: 'Technical Analysis', content_source: 'text_content.market_report' }
@ -322,64 +322,124 @@ const TransformedDataAdapter: React.FC<TransformedDataAdapterProps> = ({ analysi
? analysisData ? analysisData
: convertLegacyToTransformed(analysisData); : convertLegacyToTransformed(analysisData);
// Convert transformed data to the format expected by AnalysisWidgets // Builds a default widgets_config for transformed analyses when missing
const convertToWidgetFormat = (data: TransformedAnalysisData) => { const buildTransformedWidgetsConfig = (data: any) => {
const confidenceMap: Record<string, number> = { LOW: 33, MEDIUM: 66, HIGH: 90 };
return { return {
symbol: data.metadata.company_ticker, layout: [
final_decision: { ["kpi_recommendation", "kpi_confidence", "kpi_price_change", "kpi_marketcap", "kpi_ev"],
decision: data.metadata.final_recommendation, ["ownership_donut", "range_bar", "volume_chip"],
reasoning: data.debate_summary.final_decision_rationale ["rsi_gauge", "macd_mini", "ma_trends", "atr_chip"],
}, ["strategy_ladder", "position_sizing", "monitoring_points"],
technical_analysis: { ["analyst_target_vs_price"],
current_price: data.financial_data.current_price, ["report_technical", "report_sentiment"],
rsi: data.technical_indicators.rsi, ["report_fundamentals", "report_investment_plan"],
macd: data.technical_indicators.macd, ["debate_summary"]
moving_averages: { ],
ma_50: data.technical_indicators.sma_50, widgets: [
ma_200: data.technical_indicators.sma_200 { id: "kpi_recommendation", type: "kpi_badge", title: "Recommendation", dataPath: "metadata.final_recommendation", colorMapping: { BUY: "green", HOLD: "yellow", SELL: "red" } },
} { id: "kpi_confidence", type: "radial_gauge", title: "Confidence", value: confidenceMap[(data?.metadata?.confidence_level || "MEDIUM").toUpperCase()] || 66 },
}, { id: "kpi_price_change", type: "price_kpi", title: "Current Price", valuePath: "financial_data.current_price", deltaPath: "financial_data.price_change_percent", format: { value: "currency", delta: "percent" } },
fundamental_analysis: { { id: "kpi_marketcap", type: "kpi_text", title: "Market Cap", dataPath: "financial_data.market_cap" },
market_cap: data.financial_data.market_cap, { id: "kpi_ev", type: "kpi_text", title: "Enterprise Value", dataPath: "financial_data.enterprise_value" },
ps_ratio: data.financial_data.valuation_ratios.current_ps_ratio, { id: "ownership_donut", type: "donut", title: "Ownership", series: [
forward_pe: data.financial_data.valuation_ratios.forward_pe, { label: "Insider %", valuePath: "financial_data.ownership.insider_percent" },
analyst_target: data.financial_data.analyst_data.price_target { label: "Institutional %", valuePath: "financial_data.ownership.institutional_percent" }
}, ], valueFormat: "percent" },
bull_arguments: data.debate_summary.bull_key_points, { id: "range_bar", type: "range_bar", title: "Trading Range", lowPath: "financial_data.trading_range.low", highPath: "financial_data.trading_range.high", openPath: "financial_data.trading_range.open", currentPath: "financial_data.current_price" },
bear_arguments: data.debate_summary.bear_key_points, { id: "volume_chip", type: "kpi_text", title: "Volume", dataPath: "financial_data.volume", format: { value: "number_compact" } },
neutral_perspective: data.debate_summary.neutral_perspective, { id: "rsi_gauge", type: "linear_gauge", title: "RSI", dataPath: "technical_indicators.rsi", min: 0, max: 100, zones: [ { to: 30, color: "blue", label: "Oversold" }, { from: 70, to: 100, color: "red", label: "Overbought" } ] },
risk_assessment: { { id: "macd_mini", type: "macd_snapshot", title: "MACD", macdPath: "technical_indicators.macd", signalPath: "technical_indicators.macd_signal", histogramCompute: "macd - macd_signal" },
overall_risk: data.investment_strategy.risk_management.stop_loss_percent { id: "ma_trends", type: "badges", title: "MA & Trend", badges: [
}, { label: "SMA 50", valuePath: "technical_indicators.sma_50", trendPath: "technical_indicators.trend_directions.sma_50" },
sentiment_analysis: { { label: "SMA 200", valuePath: "technical_indicators.sma_200", trendPath: "technical_indicators.trend_directions.sma_200" },
overall_score: data.technical_indicators.rsi / 100 // Approximate sentiment from RSI { label: "EMA 10", valuePath: "technical_indicators.ema_10", trendPath: "technical_indicators.trend_directions.ema_10" }
}, ], trendColors: { BULLISH: "green", BEARISH: "red", NEUTRAL: "gray" } },
ownership_structure: { { id: "atr_chip", type: "kpi_text", title: "ATR (Volatility)", dataPath: "technical_indicators.atr" },
insider_ownership: data.financial_data.ownership.insider_percent, { id: "strategy_ladder", type: "price_ladder", title: "Entry, Stop, Targets", entryStrategyPath: "investment_strategy.position_sizing.entry_strategy", stopPricePath: "investment_strategy.risk_management.initial_stop_loss", stopPercentPath: "investment_strategy.risk_management.stop_loss_percent", targetsPath: "investment_strategy.profit_targets", targetMapping: { price: "target_price", label: "action" }, currentPricePath: "financial_data.current_price" },
institutional_ownership: data.financial_data.ownership.institutional_percent, { id: "position_sizing", type: "chips", title: "Position Sizing", items: [
retail_ownership: Math.max(0, 100 - data.financial_data.ownership.insider_percent - data.financial_data.ownership.institutional_percent) { label: "Total Allocation", valuePath: "investment_strategy.position_sizing.total_allocation_percent" },
}, { label: "Tranche 1", valuePath: "investment_strategy.position_sizing.tranche_1_percent" },
investment_plan: { { label: "Tranche 2", valuePath: "investment_strategy.position_sizing.tranche_2_percent" }
stop_loss: data.investment_strategy.risk_management.initial_stop_loss, ] },
profit_targets: data.investment_strategy.profit_targets.map(target => target.target_price) { id: "monitoring_points", type: "bullet_list", title: "Monitoring Points", itemsPath: "investment_strategy.monitoring_points" },
}, { id: "analyst_target_vs_price", type: "bar_compare", title: "Analyst Target vs Current", series: [ { label: "Current", valuePath: "financial_data.current_price" }, { label: "Target", valuePath: "financial_data.analyst_data.price_target" } ], yFormat: "currency" },
earnings_date: data.metadata.analysis_date, { id: "report_technical", type: "expandable_report", titlePath: "text_content.market_report.title", contentPath: "text_content.market_report.content", bulletsPath: "text_content.market_report.key_takeaways" },
{ id: "report_sentiment", type: "expandable_report", titlePath: "text_content.sentiment_report.title", contentPath: "text_content.sentiment_report.content", bulletsPath: "text_content.sentiment_report.recent_developments" },
// Extended data from new format { id: "report_fundamentals", type: "expandable_report", titlePath: "text_content.fundamentals_report.title", contentPath: "text_content.fundamentals_report.content", bulletsPath: "text_content.fundamentals_report.financial_highlights" },
extended_data: { { id: "report_investment_plan", type: "expandable_report", titlePath: "text_content.investment_plan_full.title", contentPath: "text_content.investment_plan_full.content" },
metadata: data.metadata, { id: "debate_summary", type: "debate_viewer", bullPath: "debate_summary.bull_key_points", bearPath: "debate_summary.bear_key_points", neutralPath: "debate_summary.neutral_perspective", finalPath: "debate_summary.final_decision_rationale" }
financial_data: data.financial_data, ]
technical_indicators: data.technical_indicators,
investment_strategy: data.investment_strategy,
text_content: data.text_content,
widgets_config: data.widgets_config
}
}; };
}; };
const widgetData = convertToWidgetFormat(transformedData); // Ensure we have a widgets_config; if missing, build a sensible default
const transformedWithConfig = {
...transformedData,
widgets_config: transformedData?.widgets_config || buildTransformedWidgetsConfig(transformedData)
};
return <AnalysisWidgets tradingResult={widgetData} />; // Minimalist Technical Indicators widget (single-card dashboard)
const TechnicalIndicatorsOnly: React.FC<{ data: TransformedAnalysisData }> = ({ data }) => {
const ti = data?.technical_indicators || {} as any;
const trends = ti?.trend_directions || {};
const fmt = (v: any, d = 0) => {
const n = Number(v);
return Number.isFinite(n) ? n.toFixed(d) : '—';
};
const trendChip = (label: string, dir?: string) => {
const map: Record<string, string> = { BULLISH: 'bg-green-100 text-green-800', BEARISH: 'bg-red-100 text-red-800', NEUTRAL: 'bg-gray-100 text-gray-700' };
const cls = map[String(dir || 'NEUTRAL').toUpperCase()] || map.NEUTRAL;
return <span className={`px-2 py-1 rounded text-xs font-medium ${cls}`}>{label}: {dir || 'NEUTRAL'}</span>;
};
return (
<div className="min-h-[200px] p-6">
<div className="max-w-3xl mx-auto">
<div className="bg-white rounded-lg shadow-md p-6">
<h2 className="text-xl font-semibold mb-4">Technical Indicators</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<p className="text-sm text-gray-500">RSI</p>
<p className="text-lg font-semibold">{fmt(ti?.rsi, 2)}</p>
</div>
<div>
<p className="text-sm text-gray-500">MACD</p>
<p className="text-lg font-semibold">{fmt(ti?.macd, 3)}</p>
<p className="text-xs text-gray-500">Signal: {fmt(ti?.macd_signal, 3)}</p>
</div>
<div>
<p className="text-sm text-gray-500">SMA 50</p>
<p className="text-lg font-semibold">{fmt(ti?.sma_50, 2)}</p>
</div>
<div>
<p className="text-sm text-gray-500">SMA 200</p>
<p className="text-lg font-semibold">{fmt(ti?.sma_200, 2)}</p>
</div>
{ti?.ema_10 !== undefined && (
<div>
<p className="text-sm text-gray-500">EMA 10</p>
<p className="text-lg font-semibold">{fmt(ti?.ema_10, 2)}</p>
</div>
)}
<div>
<p className="text-sm text-gray-500">ATR</p>
<p className="text-lg font-semibold">{fmt(ti?.atr, 3)}</p>
</div>
</div>
<div className="mt-6 flex flex-wrap gap-2">
{trendChip('SMA 50', trends?.sma_50)}
{trendChip('SMA 200', trends?.sma_200)}
{trends?.ema_10 !== undefined && trendChip('EMA 10', trends?.ema_10)}
{trends?.price_action !== undefined && trendChip('Price', trends?.price_action)}
</div>
</div>
</div>
</div>
);
};
// Only render the minimalist Technical Indicators widget for transformed data
return <TechnicalIndicatorsOnly data={transformedWithConfig} />;
}; };
export default TransformedDataAdapter; export default TransformedDataAdapter;

View File

@ -79,7 +79,7 @@ export interface TransformedData {
risk_factors: string[]; risk_factors: string[];
catalysts: string[]; catalysts: string[];
}; };
widget_config: { widgets_config: {
charts_enabled: string[]; charts_enabled: string[];
priority_widgets: string[]; priority_widgets: string[];
display_preferences: Record<string, any>; display_preferences: Record<string, any>;
@ -213,31 +213,25 @@ class TransformedDataService {
} }
private validateTransformedData(data: any): void { private validateTransformedData(data: any): void {
// Core sections that must exist
const requiredSections = [ const requiredSections = [
'metadata', 'metadata',
'financial_data', 'financial_data',
'technical_indicators', 'technical_indicators',
'investment_strategy', 'investment_strategy'
'debate_summary',
'text_content',
'widget_config'
]; ];
for (const section of requiredSections) { for (const section of requiredSections) {
if (!data[section]) { if (!(section in data)) {
throw new Error(`Missing required section: ${section}`); throw new Error(`Missing required section: ${section}`);
} }
} }
const metadata = data.metadata; // Normalize legacy singular widget_config to widgets_config, if present
if (!metadata.company_ticker || !metadata.analysis_date) { if (!('widgets_config' in data) && ('widget_config' in data)) {
throw new Error('Invalid metadata: missing company_ticker or analysis_date'); data.widgets_config = data.widget_config;
} }
const dateRegex = /^\d{4}-\d{2}-\d{2}$/; // widgets_config is optional; UI will supply a sensible default if absent
if (!dateRegex.test(metadata.analysis_date)) {
throw new Error('Invalid date format in metadata.analysis_date');
}
} }
clearCache(): void { clearCache(): void {