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.
- package/dist/index.js +0 -1
- package/dist/license.d.ts +6 -13
- package/dist/license.d.ts.map +1 -1
- package/dist/license.js +19 -36
- package/dist/tools/analyze_portfolio.js +0 -1
- package/dist/tools/analyze_stock.js +0 -1
- package/dist/tools/compare_assets.js +0 -1
- package/dist/tools/crypto_analysis.js +0 -1
- package/dist/tools/market_overview.js +0 -1
- package/dist/tools/screen_stocks.js +0 -1
- package/dist/types.js +0 -1
- package/dist/utils/api.js +0 -1
- package/dist/utils/cache.js +0 -1
- package/dist/utils/math.js +0 -1
- package/package.json +4 -1
- package/dist/index.js.map +0 -1
- package/dist/license.js.map +0 -1
- package/dist/tools/analyze_portfolio.js.map +0 -1
- package/dist/tools/analyze_stock.js.map +0 -1
- package/dist/tools/compare_assets.js.map +0 -1
- package/dist/tools/crypto_analysis.js.map +0 -1
- package/dist/tools/market_overview.js.map +0 -1
- package/dist/tools/screen_stocks.js.map +0 -1
- package/dist/types.js.map +0 -1
- package/dist/utils/api.js.map +0 -1
- package/dist/utils/cache.js.map +0 -1
- package/dist/utils/math.js.map +0 -1
- package/src/index.ts +0 -393
- package/src/license.ts +0 -143
- package/src/tools/analyze_portfolio.ts +0 -207
- package/src/tools/analyze_stock.ts +0 -204
- package/src/tools/compare_assets.ts +0 -183
- package/src/tools/crypto_analysis.ts +0 -221
- package/src/tools/market_overview.ts +0 -236
- package/src/tools/screen_stocks.ts +0 -156
- package/src/types.ts +0 -175
- package/src/utils/api.ts +0 -396
- package/src/utils/cache.ts +0 -65
- package/src/utils/math.ts +0 -342
- 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
|
-
}
|