market-data-analyzer 2.1.0 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/dist/index.js +0 -1
  2. package/dist/license.d.ts +6 -13
  3. package/dist/license.d.ts.map +1 -1
  4. package/dist/license.js +19 -36
  5. package/dist/tools/analyze_portfolio.js +0 -1
  6. package/dist/tools/analyze_stock.js +0 -1
  7. package/dist/tools/compare_assets.js +0 -1
  8. package/dist/tools/crypto_analysis.js +0 -1
  9. package/dist/tools/market_overview.js +0 -1
  10. package/dist/tools/screen_stocks.js +0 -1
  11. package/dist/types.js +0 -1
  12. package/dist/utils/api.js +0 -1
  13. package/dist/utils/cache.js +0 -1
  14. package/dist/utils/math.js +0 -1
  15. package/package.json +4 -1
  16. package/dist/index.js.map +0 -1
  17. package/dist/license.js.map +0 -1
  18. package/dist/tools/analyze_portfolio.js.map +0 -1
  19. package/dist/tools/analyze_stock.js.map +0 -1
  20. package/dist/tools/compare_assets.js.map +0 -1
  21. package/dist/tools/crypto_analysis.js.map +0 -1
  22. package/dist/tools/market_overview.js.map +0 -1
  23. package/dist/tools/screen_stocks.js.map +0 -1
  24. package/dist/types.js.map +0 -1
  25. package/dist/utils/api.js.map +0 -1
  26. package/dist/utils/cache.js.map +0 -1
  27. package/dist/utils/math.js.map +0 -1
  28. package/src/index.ts +0 -393
  29. package/src/license.ts +0 -143
  30. package/src/tools/analyze_portfolio.ts +0 -207
  31. package/src/tools/analyze_stock.ts +0 -204
  32. package/src/tools/compare_assets.ts +0 -183
  33. package/src/tools/crypto_analysis.ts +0 -221
  34. package/src/tools/market_overview.ts +0 -236
  35. package/src/tools/screen_stocks.ts +0 -156
  36. package/src/types.ts +0 -175
  37. package/src/utils/api.ts +0 -396
  38. package/src/utils/cache.ts +0 -65
  39. package/src/utils/math.ts +0 -342
  40. package/tsconfig.json +0 -19
@@ -1,207 +0,0 @@
1
- /**
2
- * analyze_portfolio -- Portfolio analytics with live prices.
3
- *
4
- * Takes an array of { symbol, shares, avg_cost } holdings, fetches
5
- * current prices from Yahoo Finance, and returns P&L, allocation,
6
- * diversification score, and risk metrics.
7
- */
8
-
9
- import { yahooQuote, SYMBOL_SECTORS } from "../utils/api.js";
10
- import { formatCurrency } from "../utils/math.js";
11
-
12
- export interface PortfolioHolding {
13
- symbol: string;
14
- shares: number;
15
- avg_cost: number;
16
- }
17
-
18
- interface PositionResult {
19
- symbol: string;
20
- name: string;
21
- shares: number;
22
- avgCost: number;
23
- currentPrice: number;
24
- marketValue: number;
25
- costBasis: number;
26
- pnl: number;
27
- pnlPercent: number;
28
- allocationPercent: number;
29
- sector: string;
30
- }
31
-
32
- export async function handleAnalyzePortfolio(
33
- holdings: PortfolioHolding[],
34
- ): Promise<string> {
35
- if (holdings.length === 0) {
36
- throw new Error("At least one holding is required.");
37
- }
38
-
39
- // Fetch live quotes for all symbols
40
- const symbols = holdings.map((h) => h.symbol);
41
- const quotes = await yahooQuote(symbols);
42
- const quoteMap = new Map(quotes.map((q) => [q.symbol, q]));
43
-
44
- // Build position analysis
45
- let totalValue = 0;
46
- let totalCost = 0;
47
-
48
- const positions: PositionResult[] = holdings.map((h) => {
49
- const q = quoteMap.get(h.symbol);
50
- const currentPrice = q?.regularMarketPrice ?? 0;
51
- const marketValue = h.shares * currentPrice;
52
- const costBasis = h.shares * h.avg_cost;
53
- totalValue += marketValue;
54
- totalCost += costBasis;
55
-
56
- return {
57
- symbol: h.symbol,
58
- name: q?.shortName ?? q?.longName ?? h.symbol,
59
- shares: h.shares,
60
- avgCost: h.avg_cost,
61
- currentPrice,
62
- marketValue,
63
- costBasis,
64
- pnl: marketValue - costBasis,
65
- pnlPercent: costBasis > 0 ? ((marketValue - costBasis) / costBasis) * 100 : 0,
66
- allocationPercent: 0, // calculated below
67
- sector: q?.sector ?? SYMBOL_SECTORS[h.symbol]?.sector ?? "Unknown",
68
- };
69
- });
70
-
71
- // Calculate allocation percentages
72
- for (const p of positions) {
73
- p.allocationPercent = totalValue > 0 ? (p.marketValue / totalValue) * 100 : 0;
74
- }
75
-
76
- // Sort by market value descending
77
- positions.sort((a, b) => b.marketValue - a.marketValue);
78
-
79
- // Sector breakdown
80
- const sectorAlloc: Record<string, number> = {};
81
- for (const p of positions) {
82
- sectorAlloc[p.sector] = (sectorAlloc[p.sector] ?? 0) + p.allocationPercent;
83
- }
84
-
85
- // Diversification score (HHI-based, 0-100 where 100 = perfectly diversified)
86
- const weights = positions.map((p) => p.allocationPercent / 100);
87
- const hhi = weights.reduce((s, w) => s + w * w, 0);
88
- const n = weights.length;
89
- const minHHI = n > 0 ? 1 / n : 1;
90
- const denom = 1 - minHHI;
91
- const divScore = n <= 1 || denom === 0 || totalValue === 0
92
- ? 0
93
- : Math.max(0, Math.min(100, Math.round((1 - (hhi - minHHI) / denom) * 100)));
94
-
95
- // Risk warnings
96
- const warnings: string[] = [];
97
-
98
- for (const p of positions) {
99
- if (p.allocationPercent > 30) {
100
- warnings.push(`HIGH CONCENTRATION: ${p.symbol} is ${p.allocationPercent.toFixed(1)}% of portfolio.`);
101
- }
102
- }
103
-
104
- for (const [sector, pct] of Object.entries(sectorAlloc)) {
105
- if (pct > 50) {
106
- warnings.push(`SECTOR RISK: ${sector} represents ${pct.toFixed(1)}% of portfolio.`);
107
- }
108
- }
109
-
110
- for (const p of positions) {
111
- if (p.pnlPercent < -20) {
112
- warnings.push(`SIGNIFICANT LOSS: ${p.symbol} is down ${Math.abs(p.pnlPercent).toFixed(1)}%.`);
113
- }
114
- }
115
-
116
- if (n < 5) {
117
- warnings.push(`LOW DIVERSIFICATION: Only ${n} position(s). Consider adding more holdings.`);
118
- }
119
-
120
- if (Object.keys(sectorAlloc).length < 3 && n >= 3) {
121
- warnings.push("LIMITED SECTOR EXPOSURE: Portfolio spans fewer than 3 sectors.");
122
- }
123
-
124
- // Any positions where we couldn't fetch a price?
125
- const missingPrices = positions.filter((p) => p.currentPrice === 0);
126
- for (const p of missingPrices) {
127
- warnings.push(`MISSING DATA: Could not fetch price for ${p.symbol}. Values may be inaccurate.`);
128
- }
129
-
130
- // Format output
131
- const lines: string[] = [];
132
-
133
- lines.push("# Portfolio Analysis");
134
- lines.push("");
135
-
136
- // Summary
137
- lines.push("## Summary");
138
- lines.push("");
139
- lines.push("| Metric | Value |");
140
- lines.push("|--------|-------|");
141
- lines.push(`| Total Value | ${formatCurrency(totalValue)} |`);
142
- lines.push(`| Total Cost Basis | ${formatCurrency(totalCost)} |`);
143
- const totalPnl = totalValue - totalCost;
144
- const totalPnlPct = totalCost > 0 ? ((totalPnl) / totalCost) * 100 : 0;
145
- lines.push(`| Total P&L | ${totalPnl >= 0 ? "+" : ""}${formatCurrency(totalPnl)} (${totalPnlPct >= 0 ? "+" : ""}${totalPnlPct.toFixed(2)}%) |`);
146
- lines.push(`| Positions | ${n} |`);
147
- lines.push(`| Sectors | ${Object.keys(sectorAlloc).length} |`);
148
- lines.push(`| Diversification Score | ${divScore}/100 |`);
149
- lines.push("");
150
-
151
- // Positions
152
- lines.push("## Positions");
153
- lines.push("");
154
- lines.push("| Symbol | Name | Shares | Avg Cost | Price | Value | P&L | P&L% | Alloc% |");
155
- lines.push("|--------|------|--------|----------|-------|-------|-----|------|--------|");
156
-
157
- for (const p of positions) {
158
- const pnlSign = p.pnl >= 0 ? "+" : "";
159
- lines.push(
160
- `| ${p.symbol} | ${p.name.slice(0, 20)} | ${p.shares} | $${p.avgCost.toFixed(2)} | $${p.currentPrice.toFixed(2)} | ${formatCurrency(p.marketValue)} | ${pnlSign}${formatCurrency(p.pnl)} | ${pnlSign}${p.pnlPercent.toFixed(2)}% | ${p.allocationPercent.toFixed(1)}% |`,
161
- );
162
- }
163
- lines.push("");
164
-
165
- // Sector allocation
166
- lines.push("## Sector Allocation");
167
- lines.push("");
168
- lines.push("| Sector | Allocation |");
169
- lines.push("|--------|------------|");
170
- const sortedSectors = Object.entries(sectorAlloc).sort((a, b) => b[1] - a[1]);
171
- for (const [sector, pct] of sortedSectors) {
172
- const bar = "=".repeat(Math.round(pct / 2));
173
- lines.push(`| ${sector} | ${pct.toFixed(1)}% ${bar} |`);
174
- }
175
- lines.push("");
176
-
177
- // Winners and losers
178
- const winners = [...positions].sort((a, b) => b.pnlPercent - a.pnlPercent);
179
- if (winners.length > 0) {
180
- lines.push("## Top Performers");
181
- lines.push("");
182
- const top3 = winners.slice(0, Math.min(3, winners.length));
183
- for (const p of top3) {
184
- lines.push(`- **${p.symbol}**: ${p.pnlPercent >= 0 ? "+" : ""}${p.pnlPercent.toFixed(2)}% (${p.pnl >= 0 ? "+" : ""}${formatCurrency(p.pnl)})`);
185
- }
186
- lines.push("");
187
- const bottom3 = winners.slice(-Math.min(3, winners.length)).reverse();
188
- lines.push("## Underperformers");
189
- lines.push("");
190
- for (const p of bottom3) {
191
- lines.push(`- **${p.symbol}**: ${p.pnlPercent >= 0 ? "+" : ""}${p.pnlPercent.toFixed(2)}% (${p.pnl >= 0 ? "+" : ""}${formatCurrency(p.pnl)})`);
192
- }
193
- lines.push("");
194
- }
195
-
196
- // Risk warnings
197
- if (warnings.length > 0) {
198
- lines.push("## Risk Warnings");
199
- lines.push("");
200
- for (const w of warnings) {
201
- lines.push(`- ${w}`);
202
- }
203
- lines.push("");
204
- }
205
-
206
- return lines.join("\n");
207
- }
@@ -1,204 +0,0 @@
1
- /**
2
- * analyze_stock -- Deep analysis of a stock symbol.
3
- *
4
- * Fetches live data from Yahoo Finance, computes SMA 20/50/200, RSI,
5
- * MACD, and support/resistance levels.
6
- */
7
-
8
- import { yahooQuote, yahooChart } from "../utils/api.js";
9
- import {
10
- sma,
11
- rsi,
12
- macd,
13
- lastValue,
14
- findSupportResistance,
15
- formatCurrency,
16
- formatVolume,
17
- } from "../utils/math.js";
18
-
19
- export async function handleAnalyzeStock(symbol: string): Promise<string> {
20
- // Fetch quote and 6-month daily chart in parallel
21
- const [quotes, chart] = await Promise.all([
22
- yahooQuote([symbol]),
23
- yahooChart(symbol, "6mo", "1d"),
24
- ]);
25
-
26
- const quote = quotes[0];
27
- if (!quote || !quote.regularMarketPrice) {
28
- throw new Error(`No quote data found for symbol "${symbol}". Verify the ticker is correct.`);
29
- }
30
-
31
- if (chart.length < 5) {
32
- throw new Error(`Insufficient price history for "${symbol}" (got ${chart.length} data points).`);
33
- }
34
-
35
- const closes = chart.map((p) => p.close);
36
- const highs = chart.map((p) => p.high);
37
- const lows = chart.map((p) => p.low);
38
- const currentPrice = quote.regularMarketPrice;
39
-
40
- // Compute indicators
41
- const sma20 = closes.length >= 20 ? lastValue(sma(closes, 20)) : null;
42
- const sma50 = closes.length >= 50 ? lastValue(sma(closes, 50)) : null;
43
- const sma200 = closes.length >= 200 ? lastValue(sma(closes, 200)) : null;
44
- const rsi14 = closes.length >= 15 ? lastValue(rsi(closes, 14)) : null;
45
-
46
- let macdResult: { line: number; signal: number; histogram: number } | null = null;
47
- if (closes.length >= 35) {
48
- const m = macd(closes);
49
- const mLine = lastValue(m.line);
50
- const mSignal = lastValue(m.signal);
51
- const mHist = lastValue(m.histogram);
52
- if (mLine !== null && mSignal !== null && mHist !== null) {
53
- macdResult = { line: mLine, signal: mSignal, histogram: mHist };
54
- }
55
- }
56
-
57
- // Support & resistance
58
- const sr = findSupportResistance(highs, lows, closes);
59
-
60
- // Build output
61
- const lines: string[] = [];
62
- const name = quote.longName ?? quote.shortName ?? symbol;
63
- const change = quote.regularMarketChange ?? 0;
64
- const changePct = quote.regularMarketChangePercent ?? 0;
65
-
66
- lines.push(`# Stock Analysis: ${name} (${symbol})`);
67
- lines.push("");
68
-
69
- // Price overview
70
- lines.push("## Price Overview");
71
- lines.push("");
72
- lines.push("| Metric | Value |");
73
- lines.push("|--------|-------|");
74
- lines.push(`| Current Price | $${currentPrice.toFixed(2)} |`);
75
- lines.push(`| Change | ${change >= 0 ? "+" : ""}$${change.toFixed(2)} (${changePct >= 0 ? "+" : ""}${changePct.toFixed(2)}%) |`);
76
- lines.push(`| Day Range | $${(quote.regularMarketDayLow ?? 0).toFixed(2)} - $${(quote.regularMarketDayHigh ?? 0).toFixed(2)} |`);
77
- lines.push(`| 52-Week Range | $${(quote.fiftyTwoWeekLow ?? 0).toFixed(2)} - $${(quote.fiftyTwoWeekHigh ?? 0).toFixed(2)} |`);
78
- lines.push(`| Volume | ${formatVolume(quote.regularMarketVolume ?? 0)} |`);
79
- lines.push(`| Market Cap | ${formatCurrency(quote.marketCap ?? 0)} |`);
80
- if (quote.trailingPE) lines.push(`| P/E (Trailing) | ${quote.trailingPE.toFixed(2)} |`);
81
- if (quote.forwardPE) lines.push(`| P/E (Forward) | ${quote.forwardPE.toFixed(2)} |`);
82
- if (quote.trailingAnnualDividendYield) {
83
- lines.push(`| Dividend Yield | ${(quote.trailingAnnualDividendYield * 100).toFixed(2)}% |`);
84
- }
85
- lines.push("");
86
-
87
- // Moving averages
88
- lines.push("## Moving Averages");
89
- lines.push("");
90
- lines.push("| Indicator | Value | Signal |");
91
- lines.push("|-----------|-------|--------|");
92
-
93
- if (sma20 !== null) {
94
- const sig = currentPrice > sma20 ? "BULLISH (price above)" : currentPrice < sma20 ? "BEARISH (price below)" : "NEUTRAL";
95
- lines.push(`| SMA(20) | $${sma20.toFixed(2)} | ${sig} |`);
96
- }
97
- if (sma50 !== null) {
98
- const sig = currentPrice > sma50 ? "BULLISH (price above)" : currentPrice < sma50 ? "BEARISH (price below)" : "NEUTRAL";
99
- lines.push(`| SMA(50) | $${sma50.toFixed(2)} | ${sig} |`);
100
- }
101
- if (sma200 !== null) {
102
- const sig = currentPrice > sma200 ? "BULLISH (price above)" : currentPrice < sma200 ? "BEARISH (price below)" : "NEUTRAL";
103
- lines.push(`| SMA(200) | $${sma200.toFixed(2)} | ${sig} |`);
104
- }
105
- if (quote.fiftyDayAverage) {
106
- lines.push(`| Yahoo 50-Day Avg | $${quote.fiftyDayAverage.toFixed(2)} | -- |`);
107
- }
108
- if (quote.twoHundredDayAverage) {
109
- lines.push(`| Yahoo 200-Day Avg | $${quote.twoHundredDayAverage.toFixed(2)} | -- |`);
110
- }
111
-
112
- // Golden/Death cross
113
- if (sma50 !== null && sma200 !== null) {
114
- if (sma50 > sma200) {
115
- lines.push("");
116
- lines.push("**Golden Cross**: 50-day SMA is above 200-day SMA -- historically bullish signal.");
117
- } else if (sma50 < sma200) {
118
- lines.push("");
119
- lines.push("**Death Cross**: 50-day SMA is below 200-day SMA -- historically bearish signal.");
120
- }
121
- }
122
- lines.push("");
123
-
124
- // RSI
125
- if (rsi14 !== null) {
126
- lines.push("## RSI (14-period)");
127
- lines.push("");
128
- lines.push(`**RSI: ${rsi14.toFixed(1)}**`);
129
- lines.push("");
130
- if (rsi14 >= 70) {
131
- lines.push("Interpretation: **OVERBOUGHT** (RSI >= 70). The stock may be overvalued and due for a pullback.");
132
- } else if (rsi14 <= 30) {
133
- lines.push("Interpretation: **OVERSOLD** (RSI <= 30). The stock may be undervalued and due for a bounce.");
134
- } else if (rsi14 >= 60) {
135
- lines.push("Interpretation: Bullish momentum (RSI in 60-70 range).");
136
- } else if (rsi14 <= 40) {
137
- lines.push("Interpretation: Bearish momentum (RSI in 30-40 range).");
138
- } else {
139
- lines.push("Interpretation: Neutral momentum (RSI in 40-60 range).");
140
- }
141
- lines.push("");
142
- }
143
-
144
- // MACD
145
- if (macdResult) {
146
- lines.push("## MACD (12, 26, 9)");
147
- lines.push("");
148
- lines.push("| Component | Value |");
149
- lines.push("|-----------|-------|");
150
- lines.push(`| MACD Line | ${macdResult.line.toFixed(4)} |`);
151
- lines.push(`| Signal Line | ${macdResult.signal.toFixed(4)} |`);
152
- lines.push(`| Histogram | ${macdResult.histogram.toFixed(4)} |`);
153
- lines.push("");
154
- if (macdResult.histogram > 0) {
155
- lines.push("Interpretation: **BULLISH** -- MACD is above signal line, positive momentum.");
156
- } else {
157
- lines.push("Interpretation: **BEARISH** -- MACD is below signal line, negative momentum.");
158
- }
159
- lines.push("");
160
- }
161
-
162
- // Support & Resistance
163
- lines.push("## Support & Resistance Levels");
164
- lines.push("");
165
- if (sr.resistance.length > 0) {
166
- lines.push("**Resistance:**");
167
- for (const r of sr.resistance) {
168
- const distPct = ((r - currentPrice) / currentPrice * 100).toFixed(1);
169
- lines.push(`- $${r.toFixed(2)} (+${distPct}% from current)`);
170
- }
171
- } else {
172
- lines.push("**Resistance:** No clear resistance levels identified in recent data.");
173
- }
174
- lines.push("");
175
- if (sr.support.length > 0) {
176
- lines.push("**Support:**");
177
- for (const s of sr.support) {
178
- const distPct = ((currentPrice - s) / currentPrice * 100).toFixed(1);
179
- lines.push(`- $${s.toFixed(2)} (-${distPct}% from current)`);
180
- }
181
- } else {
182
- lines.push("**Support:** No clear support levels identified in recent data.");
183
- }
184
- lines.push("");
185
-
186
- // Signal summary
187
- lines.push("## Signal Summary");
188
- lines.push("");
189
- let bullishCount = 0;
190
- let bearishCount = 0;
191
-
192
- if (sma20 !== null) { if (currentPrice > sma20) bullishCount++; else bearishCount++; }
193
- if (sma50 !== null) { if (currentPrice > sma50) bullishCount++; else bearishCount++; }
194
- if (sma200 !== null) { if (currentPrice > sma200) bullishCount++; else bearishCount++; }
195
- if (rsi14 !== null) { if (rsi14 < 30) bullishCount++; else if (rsi14 > 70) bearishCount++; }
196
- if (macdResult) { if (macdResult.histogram > 0) bullishCount++; else bearishCount++; }
197
-
198
- const total = bullishCount + bearishCount;
199
- const consensus = bullishCount > bearishCount ? "BULLISH" : bearishCount > bullishCount ? "BEARISH" : "NEUTRAL";
200
- lines.push(`Bullish signals: ${bullishCount}/${total} | Bearish signals: ${bearishCount}/${total}`);
201
- lines.push(`**Overall technical consensus: ${consensus}**`);
202
-
203
- return lines.join("\n");
204
- }
@@ -1,183 +0,0 @@
1
- /**
2
- * compare_assets -- Compare 2-5 assets side by side.
3
- *
4
- * Fetches price history from Yahoo Finance, computes returns, volatility,
5
- * correlation, and Sharpe ratio approximation.
6
- */
7
-
8
- import { yahooChart, yahooQuote } from "../utils/api.js";
9
- import {
10
- pricesToReturns,
11
- mean,
12
- stdDev,
13
- correlation,
14
- maxDrawdown,
15
- sharpeRatio,
16
- formatPct,
17
- } from "../utils/math.js";
18
-
19
- export async function handleCompareAssets(
20
- symbols: string[],
21
- period: string = "6mo",
22
- ): Promise<string> {
23
- if (symbols.length < 2 || symbols.length > 5) {
24
- throw new Error("Compare requires 2-5 symbols.");
25
- }
26
-
27
- // Fetch chart data and quotes in parallel
28
- const [charts, quotes] = await Promise.all([
29
- Promise.all(symbols.map((s) => yahooChart(s, period, "1d"))),
30
- yahooQuote(symbols),
31
- ]);
32
-
33
- const quoteMap = new Map(quotes.map((q) => [q.symbol, q]));
34
-
35
- // Build per-asset metrics
36
- const TRADING_DAYS = 252;
37
- const assetMetrics: Array<{
38
- symbol: string;
39
- name: string;
40
- currentPrice: number;
41
- totalReturn: number;
42
- annualizedReturn: number;
43
- volatility: number;
44
- sharpe: number;
45
- mdd: number;
46
- returns: number[];
47
- }> = [];
48
-
49
- for (let i = 0; i < symbols.length; i++) {
50
- const sym = symbols[i]!;
51
- const chart = charts[i]!;
52
- const quote = quoteMap.get(sym);
53
-
54
- if (chart.length < 5) {
55
- throw new Error(`Insufficient data for ${sym} (got ${chart.length} points). Try a longer period.`);
56
- }
57
-
58
- const closes = chart.map((p) => p.close);
59
- const returns = pricesToReturns(closes);
60
- const avgDailyReturn = mean(returns);
61
- const dailyVol = stdDev(returns);
62
- const firstClose = closes[0]!;
63
- const lastClose = closes[closes.length - 1]!;
64
- const totalReturn = firstClose > 0 ? (lastClose - firstClose) / firstClose : 0;
65
-
66
- assetMetrics.push({
67
- symbol: sym,
68
- name: quote?.shortName ?? quote?.longName ?? sym,
69
- currentPrice: quote?.regularMarketPrice ?? lastClose,
70
- totalReturn,
71
- annualizedReturn: avgDailyReturn * TRADING_DAYS,
72
- volatility: dailyVol * Math.sqrt(TRADING_DAYS),
73
- sharpe: sharpeRatio(returns),
74
- mdd: maxDrawdown(closes),
75
- returns,
76
- });
77
- }
78
-
79
- // Correlation matrix
80
- const correlations: Array<{ a: string; b: string; corr: number }> = [];
81
- for (let i = 0; i < assetMetrics.length; i++) {
82
- for (let j = i + 1; j < assetMetrics.length; j++) {
83
- correlations.push({
84
- a: assetMetrics[i].symbol,
85
- b: assetMetrics[j].symbol,
86
- corr: correlation(assetMetrics[i].returns, assetMetrics[j].returns),
87
- });
88
- }
89
- }
90
-
91
- // Format output
92
- const lines: string[] = [];
93
-
94
- lines.push("# Asset Comparison");
95
- lines.push("");
96
- lines.push(`**Assets:** ${symbols.join(", ")}`);
97
- lines.push(`**Period:** ${period}`);
98
- lines.push("");
99
-
100
- // Performance table (transposed)
101
- lines.push("## Performance Comparison");
102
- lines.push("");
103
- lines.push("| Metric | " + assetMetrics.map((a) => a.symbol).join(" | ") + " |");
104
- lines.push("| --- | " + assetMetrics.map(() => "---").join(" | ") + " |");
105
- lines.push("| Name | " + assetMetrics.map((a) => a.name.slice(0, 20)).join(" | ") + " |");
106
- lines.push("| Current Price | " + assetMetrics.map((a) => `$${a.currentPrice.toFixed(2)}`).join(" | ") + " |");
107
- lines.push("| Total Return | " + assetMetrics.map((a) => formatPct(a.totalReturn)).join(" | ") + " |");
108
- lines.push("| Annualized Return | " + assetMetrics.map((a) => formatPct(a.annualizedReturn)).join(" | ") + " |");
109
- lines.push("| Volatility (Ann.) | " + assetMetrics.map((a) => formatPct(a.volatility)).join(" | ") + " |");
110
- lines.push("| Sharpe Ratio | " + assetMetrics.map((a) => a.sharpe.toFixed(3)).join(" | ") + " |");
111
- lines.push("| Max Drawdown | " + assetMetrics.map((a) => formatPct(-a.mdd)).join(" | ") + " |");
112
- lines.push("");
113
-
114
- // Rankings
115
- lines.push("## Rankings");
116
- lines.push("");
117
-
118
- const byReturn = [...assetMetrics].sort((a, b) => b.totalReturn - a.totalReturn);
119
- const bySharpe = [...assetMetrics].sort((a, b) => b.sharpe - a.sharpe);
120
- const byVol = [...assetMetrics].sort((a, b) => a.volatility - b.volatility);
121
- const byDD = [...assetMetrics].sort((a, b) => a.mdd - b.mdd);
122
-
123
- lines.push("| Rank | Best Return | Best Sharpe | Lowest Vol | Smallest DD |");
124
- lines.push("|------|-------------|-------------|------------|-------------|");
125
- for (let i = 0; i < assetMetrics.length; i++) {
126
- lines.push(
127
- `| ${i + 1} | ${byReturn[i]!.symbol} (${formatPct(byReturn[i]!.totalReturn)}) | ${bySharpe[i]!.symbol} (${bySharpe[i]!.sharpe.toFixed(3)}) | ${byVol[i]!.symbol} (${formatPct(byVol[i]!.volatility)}) | ${byDD[i]!.symbol} (${formatPct(-byDD[i]!.mdd)}) |`,
128
- );
129
- }
130
- lines.push("");
131
-
132
- // Correlation matrix
133
- lines.push("## Correlation Matrix");
134
- lines.push("");
135
-
136
- const syms = assetMetrics.map((a) => a.symbol);
137
- lines.push("| | " + syms.join(" | ") + " |");
138
- lines.push("| --- | " + syms.map(() => "---").join(" | ") + " |");
139
-
140
- for (let i = 0; i < syms.length; i++) {
141
- const row = [syms[i]];
142
- for (let j = 0; j < syms.length; j++) {
143
- if (i === j) {
144
- row.push("1.000");
145
- } else {
146
- const pair = correlations.find(
147
- (c) =>
148
- (c.a === syms[i] && c.b === syms[j]) ||
149
- (c.a === syms[j] && c.b === syms[i]),
150
- );
151
- row.push(pair ? pair.corr.toFixed(3) : "N/A");
152
- }
153
- }
154
- lines.push("| " + row.join(" | ") + " |");
155
- }
156
- lines.push("");
157
-
158
- // Correlation insight
159
- const avgCorr = correlations.length > 0
160
- ? mean(correlations.map((c) => c.corr))
161
- : 0;
162
- lines.push(`**Average correlation:** ${avgCorr.toFixed(3)}`);
163
- if (avgCorr > 0.7) {
164
- lines.push("These assets are highly correlated -- limited diversification benefit.");
165
- } else if (avgCorr > 0.3) {
166
- lines.push("Moderate correlation -- some diversification benefit.");
167
- } else if (avgCorr > -0.1) {
168
- lines.push("Low correlation -- good diversification potential.");
169
- } else {
170
- lines.push("Negative correlation -- excellent diversification. These assets tend to move in opposite directions.");
171
- }
172
- lines.push("");
173
-
174
- // Summary
175
- lines.push("## Summary");
176
- lines.push("");
177
- lines.push(`- **Highest return:** ${byReturn[0]!.symbol} (${formatPct(byReturn[0]!.totalReturn)})`);
178
- lines.push(`- **Best risk-adjusted:** ${bySharpe[0]!.symbol} (Sharpe: ${bySharpe[0]!.sharpe.toFixed(3)})`);
179
- lines.push(`- **Lowest volatility:** ${byVol[0]!.symbol} (${formatPct(byVol[0]!.volatility)})`);
180
- lines.push(`- **Smallest drawdown:** ${byDD[0]!.symbol} (${formatPct(-byDD[0]!.mdd)})`);
181
-
182
- return lines.join("\n");
183
- }