import { SIGNAL } from '../config/constants.js'; import { YahooClient } from '../market/YahooClient.js'; export class PortfolioAdvisor { constructor() { this.client = new YahooClient(); } async advise(holdings, screenedResults) { // Build result map keyed by both the Yahoo ticker (BRK-B) and the // dot-notation variant (BRK.B) so lookups work regardless of format. const resultMap = {}; for (const r of [ ...(screenedResults.STOCK ?? []), ...(screenedResults.ETF ?? []), ...(screenedResults.BOND ?? []), ]) { const t = r.asset.ticker; resultMap[t] = r; resultMap[t.replace(/-/g, '.')] = r; // BRK-B → BRK.B resultMap[t.replace(/\./g, '-')] = r; // BRK.B → BRK-B } const cryptoPrices = await this._cryptoPrices(holdings.filter((h) => h.type === 'crypto')); return holdings.map((holding) => { const type = (holding.type ?? 'stock').toLowerCase(); const source = holding.source ?? '—'; const price = type === 'crypto' ? cryptoPrices[holding.ticker.toUpperCase()] : (resultMap[holding.ticker.toUpperCase()]?.asset?.currentPrice ?? null); return type === 'crypto' ? this._row(holding, price, source, '—', '—', '—', this._cryptoAdvice(holding, price)) : this._stockRow(holding, price, source, resultMap[holding.ticker.toUpperCase()]); }); } _stockRow(holding, price, source, result) { if (!result) { return this._row(holding, price, source, '—', '—', '—', { action: '⚪ Not screened', reason: 'No screener data available — Yahoo Finance may not support this ticker.', }); } return this._row( holding, price, source, result.signal, result.inflated.label, result.fundamental.label, this._advice(result.signal, holding, price), ); } _row(holding, currentPrice, source, signal, inflated, fundamental, { action, reason }) { const { marketValue, totalCost, gainLossPct } = this._position(holding, currentPrice); return { ticker: holding.ticker, type: holding.type ?? 'stock', source, shares: holding.shares, costBasis: holding.costBasis, currentPrice, marketValue, totalCost, gainLossPct, signal, inflated, fundamental, advice: action, reason, }; } _position(holding, currentPrice) { const totalCost = (holding.costBasis * holding.shares).toFixed(2); const marketValue = currentPrice != null ? (currentPrice * holding.shares).toFixed(2) : null; const gainLossPct = currentPrice != null && holding.costBasis > 0 ? (((currentPrice - holding.costBasis) / holding.costBasis) * 100).toFixed(1) : null; return { totalCost, marketValue, gainLossPct }; } _cryptoAdvice(holding, price) { const { gainLossPct } = this._position(holding, price); const g = parseFloat(gainLossPct); if (gainLossPct == null) return { action: '⚪ No price data', reason: 'Crypto — track price and manage risk manually.', }; if (g > 100) return { action: '🟠 Consider taking profits', reason: 'Up significantly — no fundamental analysis for crypto.', }; if (g < -30) return { action: '🔴 Review position', reason: 'Down significantly — no fundamental analysis for crypto.', }; return { action: '🟡 Hold', reason: 'Crypto — no fundamental analysis. Track price and manage risk manually.', }; } _advice(signal, holding, price) { const { gainLossPct } = this._position(holding, price); const gain = parseFloat(gainLossPct); switch (signal) { case SIGNAL.STRONG_BUY: return { action: '🟢 Hold & Add', reason: 'Passes both analyses. Strong conviction.', }; case SIGNAL.MOMENTUM: return { action: '🟡 Hold', reason: gain > 30 ? 'Up on momentum — consider partial profit-taking.' : 'Set a stop-loss — not fundamentally justified.', }; case SIGNAL.SPECULATION: return { action: gain > 20 ? '🟠 Reduce Position' : '🟡 Hold (small size)', reason: gain > 20 ? 'In profit on speculation — take partial profits.' : 'Overvalued fundamentally. Keep position small.', }; case SIGNAL.NEUTRAL: return { action: '🟡 Hold', reason: 'No clear edge. Review on any catalyst.', }; case SIGNAL.AVOID: return { action: gain > 0 ? '🔴 Sell (Take Profits)' : '🔴 Sell (Cut Loss)', reason: gain > 0 ? "Fails both analyses — you're in profit, take it." : 'Fails both analyses — stop the loss from growing.', }; default: return { action: '⚪ Review', reason: 'Signal unclear.' }; } } async _cryptoPrices(cryptoHoldings) { const prices = {}; for (const h of cryptoHoldings) { try { const summary = await this.client.fetchSummary(h.ticker); prices[h.ticker.toUpperCase()] = summary.price?.regularMarketPrice ?? null; } catch { prices[h.ticker.toUpperCase()] = null; } } return prices; } }