Spaces:
Running
Running
| import { useState } from 'react'; | |
| import { Tooltip, ResponsiveContainer, RadarChart, Radar, PolarGrid, PolarAngleAxis, PolarRadiusAxis, BarChart, Bar, XAxis, YAxis, CartesianGrid, Cell } from 'recharts'; | |
| import { quantAPI, mlAPI } from '../api/client'; | |
| import { TrendingUp, TrendingDown, Activity, Target, Brain, Eye, BarChart3, Clock, ArrowUpRight, ArrowDownRight } from 'lucide-react'; | |
| import TickerSearch from '../components/TickerSearch'; | |
| export default function FactorAnalysis() { | |
| const [tickers, setTickers] = useState('AAPL, MSFT, GOOGL, AMZN, NVDA'); | |
| const factors = ['momentum', 'value', 'size', 'quality', 'volatility']; | |
| const [results, setResults] = useState<any>(null); | |
| const [signals, setSignals] = useState<any>(null); | |
| const [loading, setLoading] = useState(false); | |
| const [activeTab, setActiveTab] = useState('factors'); | |
| // ML state | |
| const [mlTicker, setMlTicker] = useState('AAPL'); | |
| const [prediction, setPrediction] = useState<any>(null); | |
| const [regime, setRegime] = useState<any>(null); | |
| const [mlLoading, setMlLoading] = useState(false); | |
| const analyze = async () => { | |
| setLoading(true); | |
| const tickerList = tickers.split(',').map(t => t.trim()).filter(Boolean); | |
| try { | |
| const [factorRes, signalRes] = await Promise.all([ | |
| quantAPI.analyzeFactors({ tickers: tickerList, factors }), | |
| quantAPI.generateSignals({ tickers: tickerList }), | |
| ]); | |
| setResults(factorRes.data); | |
| setSignals(signalRes.data); | |
| } catch (e) { console.error(e); } finally { setLoading(false); } | |
| }; | |
| const runML = async () => { | |
| if (!mlTicker.trim()) return; | |
| setMlLoading(true); | |
| setPrediction(null); | |
| setRegime(null); | |
| try { | |
| // Sequential calls to avoid overwhelming Yahoo Finance with parallel requests | |
| const regimeRes = await mlAPI.regime(mlTicker.trim().toUpperCase()); | |
| setRegime(regimeRes.data); | |
| const predRes = await mlAPI.predict(mlTicker.trim().toUpperCase()); | |
| setPrediction(predRes.data); | |
| } catch (e: any) { | |
| console.error(e); | |
| alert(e?.response?.data?.detail || 'ML prediction failed'); | |
| } finally { setMlLoading(false); } | |
| }; | |
| const getRadarData = () => { | |
| if (!results?.exposures) return []; | |
| const factorNames = [...new Set(results.exposures.map((e: any) => e.factor_name))]; | |
| return factorNames.map(f => { | |
| const entry: any = { factor: f }; | |
| results.exposures.filter((e: any) => e.factor_name === f).forEach((e: any) => { | |
| entry[e.ticker] = (e.percentile_rank * 100).toFixed(0); | |
| }); | |
| return entry; | |
| }); | |
| }; | |
| const regimeColor = (r: string) => | |
| r === 'bull' ? '#0a8f5c' : r === 'bear' ? '#c23030' : '#b8860b'; | |
| const RegimeIcon = ({ regime: r, size = 14 }: { regime: string; size?: number }) => { | |
| if (r === 'bull') return <TrendingUp size={size} color="#0a8f5c" />; | |
| if (r === 'bear') return <TrendingDown size={size} color="#c23030" />; | |
| return <Activity size={size} color="#b8860b" />; | |
| }; | |
| return ( | |
| <div className="page animate-fade-in"> | |
| <div className="page-header"> | |
| <h1 style={{fontSize:'1.75rem',fontFamily:'var(--font-serif)'}}>Factor Analysis</h1> | |
| <p style={{color:'var(--text-muted)',fontSize:'0.85rem'}}>Multi-factor model exposures, quantitative signals, and ML-powered predictions</p> | |
| </div> | |
| {/* Input */} | |
| <div className="card" style={{marginBottom:'1.5rem'}}> | |
| <div className="factor-input-bar" style={{display:'flex',gap:'0.75rem',alignItems:'flex-end',flexWrap:'wrap'}}> | |
| <div style={{flex:1,minWidth:300}}> | |
| <label style={{fontSize:'0.75rem',fontWeight:600,color:'var(--text-muted)',textTransform:'uppercase',letterSpacing:'0.05em',display:'block',marginBottom:'0.375rem'}}>Ticker Universe (comma-separated)</label> | |
| <input className="input" value={tickers} onChange={e => setTickers(e.target.value)} placeholder="AAPL, MSFT, GOOGL..." /> | |
| </div> | |
| <button className="btn btn-primary" onClick={analyze} disabled={loading}> | |
| {loading ? <div className="spinner" /> : 'Analyze Factors'} | |
| </button> | |
| </div> | |
| </div> | |
| {/* Tabs */} | |
| <div className="tabs"> | |
| <button className={`tab ${activeTab === 'factors' ? 'active' : ''}`} onClick={() => setActiveTab('factors')}> | |
| <span style={{display:'flex',alignItems:'center',gap:'0.375rem'}}><BarChart3 size={13}/> Factor Exposures</span> | |
| </button> | |
| <button className={`tab ${activeTab === 'signals' ? 'active' : ''}`} onClick={() => setActiveTab('signals')}> | |
| <span style={{display:'flex',alignItems:'center',gap:'0.375rem'}}><Activity size={13}/> Signals</span> | |
| </button> | |
| <button className={`tab ${activeTab === 'ml' ? 'active' : ''}`} onClick={() => setActiveTab('ml')}> | |
| <span style={{display:'flex',alignItems:'center',gap:'0.375rem'}}><Brain size={13}/> ML Insights</span> | |
| </button> | |
| </div> | |
| {/* Factor Results */} | |
| {activeTab === 'factors' && results && ( | |
| <div className="grid-2"> | |
| <div className="card"> | |
| <div className="card-header"><h3>Factor Exposure Heatmap</h3></div> | |
| <div className="table-container"> | |
| <table> | |
| <thead> | |
| <tr> | |
| <th>Ticker</th> | |
| {factors.map(f => <th key={f}>{f.charAt(0).toUpperCase() + f.slice(1)}</th>)} | |
| </tr> | |
| </thead> | |
| <tbody> | |
| {results.tickers?.map((ticker: string) => ( | |
| <tr key={ticker}> | |
| <td style={{fontWeight:600}}>{ticker}</td> | |
| {factors.map(f => { | |
| const exp = results.exposures?.find((e: any) => e.ticker === ticker && e.factor_name === f); | |
| const val = exp?.z_score || 0; | |
| const bg = val > 1 ? 'rgba(16,185,129,0.15)' : val < -1 ? 'rgba(244,63,94,0.15)' : 'transparent'; | |
| return <td key={f} style={{background:bg,color:val > 0 ? 'var(--accent-emerald)' : val < 0 ? 'var(--accent-rose)' : 'var(--text-secondary)'}}>{val.toFixed(2)}</td>; | |
| })} | |
| </tr> | |
| ))} | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| <div className="card"> | |
| <div className="card-header"><h3>Factor Radar</h3></div> | |
| <div style={{height:300}}> | |
| <ResponsiveContainer> | |
| <RadarChart data={getRadarData()}> | |
| <PolarGrid stroke="var(--chart-grid)" /> | |
| <PolarAngleAxis dataKey="factor" tick={{fontSize:11,fill:'var(--chart-axis)'}} /> | |
| <PolarRadiusAxis tick={{fontSize:9,fill:'var(--chart-axis)'}} /> | |
| {results.tickers?.slice(0, 4).map((t: string, i: number) => ( | |
| <Radar key={t} name={t} dataKey={t} stroke={['#6366f1','#10b981','#f59e0b','#f43f5e'][i]} fill={['#6366f1','#10b981','#f59e0b','#f43f5e'][i]} fillOpacity={0.1} strokeWidth={2} /> | |
| ))} | |
| <Tooltip contentStyle={{background:'var(--chart-tooltip-bg)',border:'1px solid var(--chart-tooltip-border)',borderRadius:8,fontSize:'0.8rem',boxShadow:'var(--shadow-md)'}} /> | |
| </RadarChart> | |
| </ResponsiveContainer> | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| {/* Signals */} | |
| {activeTab === 'signals' && signals && ( | |
| <div className="card"> | |
| <div className="card-header"><h3>Generated Signals ({signals.total_signals || 0})</h3></div> | |
| <div className="table-container"> | |
| <table> | |
| <thead><tr><th>Ticker</th><th>Signal</th><th>Type</th><th>Direction</th><th>Strength</th><th>Value</th></tr></thead> | |
| <tbody> | |
| {signals.results?.flatMap((r: any) => r.signals?.map((s: any, i: number) => ( | |
| <tr key={`${r.ticker}-${i}`}> | |
| <td style={{fontWeight:600}}>{s.ticker}</td> | |
| <td>{s.name}</td> | |
| <td><span className="badge badge-primary">{s.signal_type}</span></td> | |
| <td><span className={`badge ${s.direction === 'long' ? 'badge-emerald' : s.direction === 'short' ? 'badge-rose' : 'badge-amber'}`}>{s.direction?.toUpperCase()}</span></td> | |
| <td>{(s.strength * 100).toFixed(0)}%</td> | |
| <td>{s.value?.toFixed(4)}</td> | |
| </tr> | |
| )) || [])} | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| )} | |
| {/* ML Insights */} | |
| {activeTab === 'ml' && ( | |
| <div> | |
| {/* ML Search */} | |
| <div className="card" style={{marginBottom:'1.5rem'}}> | |
| <div style={{display:'flex',gap:'0.75rem',alignItems:'flex-end',flexWrap:'wrap'}}> | |
| <div style={{flex:1,minWidth:200}}> | |
| <label style={{fontSize:'0.75rem',fontWeight:600,color:'var(--text-muted)',textTransform:'uppercase',letterSpacing:'0.05em',display:'block',marginBottom:'0.375rem'}}>Ticker for ML Analysis</label> | |
| <TickerSearch value={mlTicker} onChange={v => setMlTicker(v)} onSelect={t => setMlTicker(t.symbol)} placeholder="Search any ticker..." /> | |
| </div> | |
| <button className="btn btn-primary" onClick={runML} disabled={mlLoading}> | |
| {mlLoading ? <div className="spinner" /> : <><Brain size={14}/> Run ML Models</>} | |
| </button> | |
| </div> | |
| <p style={{fontSize:'0.75rem',color:'var(--text-muted)',marginTop:'0.5rem'}}> | |
| Models train on 2 years of live data with 30+ technical features. Auto-retrains every 6 hours. | |
| </p> | |
| </div> | |
| {mlLoading && ( | |
| <div className="card" style={{textAlign:'center',padding:'3rem'}}> | |
| <div className="spinner" style={{margin:'0 auto 1rem'}} /> | |
| <p style={{color:'var(--text-muted)'}}>Training models on live data... This may take a few seconds.</p> | |
| </div> | |
| )} | |
| {prediction && regime && ( | |
| <div className="grid-2"> | |
| {/* XGBoost Prediction */} | |
| <div className="card"> | |
| <div className="card-header"> | |
| <h3 style={{display:'flex',alignItems:'center',gap:'0.5rem'}}><Target size={16} color="var(--accent)"/> XGBoost Return Prediction</h3> | |
| <span className="badge badge-primary">{prediction.horizon_days}-day forecast</span> | |
| </div> | |
| {/* Direction badge */} | |
| <div style={{display:'flex',alignItems:'center',gap:'1rem',margin:'1.5rem 0',padding:'1.25rem',borderRadius:12,background: prediction.prediction === 'up' ? 'rgba(34,197,94,0.08)' : 'rgba(239,68,68,0.08)',border: `1px solid ${prediction.prediction === 'up' ? 'rgba(34,197,94,0.2)' : 'rgba(239,68,68,0.2)'}`}}> | |
| <div style={{width:44,height:44,borderRadius:'var(--radius-md)',background: prediction.prediction === 'up' ? 'rgba(10,143,92,0.1)' : 'rgba(194,48,48,0.1)',display:'flex',alignItems:'center',justifyContent:'center'}}>{prediction.prediction === 'up' ? <ArrowUpRight size={24} color="#0a8f5c"/> : <ArrowDownRight size={24} color="#c23030"/>}</div> | |
| <div> | |
| <div style={{fontSize:'1.5rem',fontWeight:700,color: prediction.prediction === 'up' ? '#0a8f5c' : '#c23030'}}> | |
| {prediction.prediction.toUpperCase()} | |
| </div> | |
| <div style={{fontSize:'0.85rem',color:'var(--text-muted)'}}> | |
| Probability: <strong>{(prediction.probability * 100).toFixed(1)}%</strong> | |
| </div> | |
| </div> | |
| <div style={{marginLeft:'auto',textAlign:'right'}}> | |
| <div style={{fontSize:'0.7rem',textTransform:'uppercase',color:'var(--text-muted)',letterSpacing:'0.05em'}}>Expected Return</div> | |
| <div style={{fontSize:'1.25rem',fontWeight:700,fontFamily:'var(--font-mono)',color: prediction.expected_return_pct > 0 ? '#0a8f5c' : '#c23030'}}> | |
| {prediction.expected_return_pct > 0 ? '+' : ''}{prediction.expected_return_pct}% | |
| </div> | |
| </div> | |
| </div> | |
| {/* Confidence + Price */} | |
| <div style={{display:'flex',gap:'1rem',marginBottom:'1.25rem',flexWrap:'wrap'}}> | |
| <div className="metric"> | |
| <div className="metric-label">Confidence</div> | |
| <div className="metric-value" style={{color: prediction.confidence_level === 'high' ? '#0a8f5c' : prediction.confidence_level === 'medium' ? '#b8860b' : '#c23030'}}> | |
| {prediction.confidence_level?.toUpperCase()} | |
| </div> | |
| </div> | |
| <div className="metric"> | |
| <div className="metric-label">Current Price</div> | |
| <div className="metric-value">${prediction.current_price}</div> | |
| </div> | |
| <div className="metric"> | |
| <div className="metric-label">Model Accuracy</div> | |
| <div className="metric-value">{(prediction.model_metrics?.accuracy * 100).toFixed(1)}%</div> | |
| </div> | |
| <div className="metric"> | |
| <div className="metric-label">F1 Score</div> | |
| <div className="metric-value">{(prediction.model_metrics?.f1 * 100).toFixed(1)}%</div> | |
| </div> | |
| </div> | |
| {/* Feature Importance */} | |
| <h4 style={{fontSize:'0.85rem',fontWeight:600,marginBottom:'0.75rem',color:'var(--text-muted)'}}>Top Feature Importance</h4> | |
| <div style={{height:220}}> | |
| <ResponsiveContainer> | |
| <BarChart data={prediction.top_features} layout="vertical" margin={{left:100,right:20,top:5,bottom:5}}> | |
| <CartesianGrid strokeDasharray="3 3" stroke="var(--chart-grid)" /> | |
| <XAxis type="number" tick={{fontSize:10,fill:'var(--chart-axis)'}} /> | |
| <YAxis type="category" dataKey="name" tick={{fontSize:10,fill:'var(--chart-axis)'}} width={95} /> | |
| <Tooltip contentStyle={{background:'var(--chart-tooltip-bg)',border:'1px solid var(--chart-tooltip-border)',borderRadius:8,fontSize:'0.8rem',boxShadow:'var(--shadow-md)'}} /> | |
| <Bar dataKey="importance" radius={[0,4,4,0]}> | |
| {prediction.top_features?.map((_: any, i: number) => ( | |
| <Cell key={i} fill={['#6366f1','#818cf8','#a5b4fc','#c7d2fe','#e0e7ff','#eef2ff','#f0f0ff','#f5f5ff','#fafafe','#fdfdfd'][i] || '#c7d2fe'} /> | |
| ))} | |
| </Bar> | |
| </BarChart> | |
| </ResponsiveContainer> | |
| </div> | |
| {prediction.from_cache && ( | |
| <p style={{fontSize:'0.7rem',color:'var(--text-muted)',marginTop:'0.5rem',display:'flex',alignItems:'center',gap:'0.25rem'}}><Clock size={11}/> Used cached model (retrains every 6h)</p> | |
| )} | |
| </div> | |
| {/* HMM Regime */} | |
| <div className="card"> | |
| <div className="card-header"> | |
| <h3 style={{display:'flex',alignItems:'center',gap:'0.5rem'}}><Eye size={16} color="var(--accent)"/> Market Regime Detection</h3> | |
| <span className="badge" style={{background:regimeColor(regime.current_regime)+'20',color:regimeColor(regime.current_regime),fontWeight:600,display:'inline-flex',alignItems:'center',gap:'0.25rem'}}> | |
| <RegimeIcon regime={regime.current_regime} size={12}/> {regime.current_regime?.replace('_',' ').toUpperCase()} | |
| </span> | |
| </div> | |
| {/* Current regime banner */} | |
| <div style={{padding:'1.25rem',borderRadius:12,background:regimeColor(regime.current_regime)+'10',border:`1px solid ${regimeColor(regime.current_regime)}30`,margin:'1rem 0'}}> | |
| <div style={{display:'flex',alignItems:'center',gap:'0.75rem'}}> | |
| <div style={{width:40,height:40,borderRadius:'var(--radius-md)',background:regimeColor(regime.current_regime)+'15',display:'flex',alignItems:'center',justifyContent:'center'}}><RegimeIcon regime={regime.current_regime} size={22}/></div> | |
| <div> | |
| <div style={{fontWeight:700,fontSize:'1.1rem',color:regimeColor(regime.current_regime)}}> | |
| {regime.current_regime?.replace('_',' ').replace(/^\w/, (c: string) => c.toUpperCase())} Regime | |
| </div> | |
| <div style={{fontSize:'0.8rem',color:'var(--text-muted)'}}> | |
| {regime.regime_info?.description} | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| {/* Regime probabilities */} | |
| <h4 style={{fontSize:'0.85rem',fontWeight:600,marginBottom:'0.75rem',color:'var(--text-muted)'}}>Regime Probabilities</h4> | |
| <div style={{display:'flex',flexDirection:'column',gap:'0.5rem',marginBottom:'1.25rem'}}> | |
| {regime.regime_probabilities && Object.entries(regime.regime_probabilities).map(([label, prob]: [string, any]) => ( | |
| <div key={label} style={{display:'flex',alignItems:'center',gap:'0.75rem'}}> | |
| <span style={{width:120,fontSize:'0.8rem',fontWeight:500,display:'flex',alignItems:'center',gap:'0.35rem'}}><RegimeIcon regime={label} size={12}/> {label.replace('_',' ')}</span> | |
| <div style={{flex:1,height:20,borderRadius:10,background:'var(--chart-bar-bg)',overflow:'hidden'}}> | |
| <div style={{width:`${prob*100}%`,height:'100%',borderRadius:10,background:regimeColor(label),transition:'width 0.5s ease'}} /> | |
| </div> | |
| <span style={{fontSize:'0.8rem',fontWeight:600,width:45,textAlign:'right'}}>{(prob*100).toFixed(1)}%</span> | |
| </div> | |
| ))} | |
| </div> | |
| {/* Regime stats */} | |
| {regime.regime_stats && ( | |
| <> | |
| <h4 style={{fontSize:'0.85rem',fontWeight:600,marginBottom:'0.75rem',color:'var(--text-muted)'}}>Regime Statistics</h4> | |
| <div className="table-container" style={{marginBottom:'1.25rem'}}> | |
| <table> | |
| <thead><tr><th>Regime</th><th>Avg Daily Return</th><th>Daily Volatility</th><th>Days</th><th>% of Time</th></tr></thead> | |
| <tbody> | |
| {Object.entries(regime.regime_stats).map(([label, s]: [string, any]) => ( | |
| <tr key={label}> | |
| <td style={{fontWeight:600,fontSize:'0.8rem'}}><span style={{display:'inline-flex',alignItems:'center',gap:'0.3rem'}}><RegimeIcon regime={label} size={12}/> {label.replace('_',' ')}</span></td> | |
| <td style={{color: s.mean_daily_return_pct > 0 ? '#0a8f5c' : '#c23030',fontFamily:'var(--font-mono)',fontSize:'0.8rem'}}>{s.mean_daily_return_pct > 0 ? '+' : ''}{s.mean_daily_return_pct}%</td> | |
| <td>{s.daily_volatility_pct}%</td> | |
| <td>{s.days_in_regime}</td> | |
| <td>{s.pct_of_time}%</td> | |
| </tr> | |
| ))} | |
| </tbody> | |
| </table> | |
| </div> | |
| </> | |
| )} | |
| {/* Regime History Timeline */} | |
| <h4 style={{fontSize:'0.85rem',fontWeight:600,marginBottom:'0.75rem',color:'var(--text-muted)'}}>Regime History (Last 60 Days)</h4> | |
| <div style={{display:'flex',gap:1,height:24,borderRadius:6,overflow:'hidden',marginBottom:'1.25rem'}}> | |
| {regime.regime_history?.map((d: any, i: number) => ( | |
| <div key={i} style={{flex:1,background:regimeColor(d.regime),opacity:0.8,cursor:'pointer'}} title={`${d.date}: ${d.regime}`} /> | |
| ))} | |
| </div> | |
| <div style={{display:'flex',gap:'1rem',fontSize:'0.7rem',color:'var(--text-muted)'}}> | |
| <span style={{display:'inline-flex',alignItems:'center',gap:'0.2rem'}}><span style={{width:8,height:8,borderRadius:'50%',background:'#0a8f5c',display:'inline-block'}}/>Bull</span> | |
| <span style={{display:'inline-flex',alignItems:'center',gap:'0.2rem'}}><span style={{width:8,height:8,borderRadius:'50%',background:'#c23030',display:'inline-block'}}/>Bear</span> | |
| <span style={{display:'inline-flex',alignItems:'center',gap:'0.2rem'}}><span style={{width:8,height:8,borderRadius:'50%',background:'#b8860b',display:'inline-block'}}/>High Vol</span> | |
| <span style={{marginLeft:'auto'}}>{regime.regime_history?.[0]?.date} → {regime.regime_history?.[regime.regime_history.length-1]?.date}</span> | |
| </div> | |
| {/* Transition Matrix */} | |
| {regime.transition_matrix && ( | |
| <> | |
| <h4 style={{fontSize:'0.85rem',fontWeight:600,margin:'1.25rem 0 0.75rem',color:'var(--text-muted)'}}>Transition Matrix</h4> | |
| <div className="table-container"> | |
| <table> | |
| <thead><tr><th style={{fontSize:'0.7rem'}}>From / To</th><th style={{fontSize:'0.7rem'}}>Bull</th><th style={{fontSize:'0.7rem'}}>Bear</th><th style={{fontSize:'0.7rem'}}>High Vol</th></tr></thead> | |
| <tbody> | |
| {['bull','bear','high_volatility'].map(from => ( | |
| <tr key={from}> | |
| <td style={{fontWeight:600,fontSize:'0.8rem'}}><span style={{display:'inline-flex',alignItems:'center',gap:'0.3rem'}}><RegimeIcon regime={from} size={12}/> {from.replace('_',' ')}</span></td> | |
| {['bull','bear','high_volatility'].map(to => { | |
| const p = regime.transition_matrix[from]?.[to] || 0; | |
| return ( | |
| <td key={to} style={{ | |
| background: from === to ? `${regimeColor(to)}15` : p > 0.3 ? 'rgba(99,102,241,0.08)' : 'transparent', | |
| fontWeight: from === to ? 700 : 400, | |
| }}> | |
| {(p*100).toFixed(1)}% | |
| </td> | |
| ); | |
| })} | |
| </tr> | |
| ))} | |
| </tbody> | |
| </table> | |
| </div> | |
| <p style={{fontSize:'0.7rem',color:'var(--text-muted)',marginTop:'0.5rem'}}> | |
| Diagonal = probability of staying in same regime. Off-diagonal = probability of switching. | |
| </p> | |
| </> | |
| )} | |
| </div> | |
| </div> | |
| )} | |
| {!prediction && !mlLoading && ( | |
| <div className="empty-state card"><h3>Enter a ticker to run ML analysis</h3><p>XGBoost predicts 5-day return direction. HMM detects bull/bear/high-volatility regimes. Models auto-retrain on live data every 6 hours.</p></div> | |
| )} | |
| </div> | |
| )} | |
| {activeTab !== 'ml' && !results && !loading && ( | |
| <div className="empty-state card"><h3>Enter tickers and run analysis</h3><p>The factor model will compute momentum, value, size, quality, and volatility exposures.</p></div> | |
| )} | |
| </div> | |
| ); | |
| } | |