market-data-analyzer 2.0.0

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.

Potentially problematic release.


This version of market-data-analyzer might be problematic. Click here for more details.

Files changed (58) hide show
  1. package/README.md +159 -0
  2. package/dist/index.d.ts +12 -0
  3. package/dist/index.d.ts.map +1 -0
  4. package/dist/index.js +267 -0
  5. package/dist/index.js.map +1 -0
  6. package/dist/tools/analyze_portfolio.d.ts +14 -0
  7. package/dist/tools/analyze_portfolio.d.ts.map +1 -0
  8. package/dist/tools/analyze_portfolio.js +155 -0
  9. package/dist/tools/analyze_portfolio.js.map +1 -0
  10. package/dist/tools/analyze_stock.d.ts +8 -0
  11. package/dist/tools/analyze_stock.d.ts.map +1 -0
  12. package/dist/tools/analyze_stock.js +211 -0
  13. package/dist/tools/analyze_stock.js.map +1 -0
  14. package/dist/tools/compare_assets.d.ts +8 -0
  15. package/dist/tools/compare_assets.d.ts.map +1 -0
  16. package/dist/tools/compare_assets.js +138 -0
  17. package/dist/tools/compare_assets.js.map +1 -0
  18. package/dist/tools/crypto_analysis.d.ts +8 -0
  19. package/dist/tools/crypto_analysis.d.ts.map +1 -0
  20. package/dist/tools/crypto_analysis.js +192 -0
  21. package/dist/tools/crypto_analysis.js.map +1 -0
  22. package/dist/tools/market_overview.d.ts +8 -0
  23. package/dist/tools/market_overview.d.ts.map +1 -0
  24. package/dist/tools/market_overview.js +223 -0
  25. package/dist/tools/market_overview.js.map +1 -0
  26. package/dist/tools/screen_stocks.d.ts +19 -0
  27. package/dist/tools/screen_stocks.d.ts.map +1 -0
  28. package/dist/tools/screen_stocks.js +122 -0
  29. package/dist/tools/screen_stocks.js.map +1 -0
  30. package/dist/types.d.ts +158 -0
  31. package/dist/types.d.ts.map +1 -0
  32. package/dist/types.js +5 -0
  33. package/dist/types.js.map +1 -0
  34. package/dist/utils/api.d.ts +37 -0
  35. package/dist/utils/api.d.ts.map +1 -0
  36. package/dist/utils/api.js +228 -0
  37. package/dist/utils/api.js.map +1 -0
  38. package/dist/utils/cache.d.ts +20 -0
  39. package/dist/utils/cache.d.ts.map +1 -0
  40. package/dist/utils/cache.js +52 -0
  41. package/dist/utils/cache.js.map +1 -0
  42. package/dist/utils/math.d.ts +30 -0
  43. package/dist/utils/math.d.ts.map +1 -0
  44. package/dist/utils/math.js +300 -0
  45. package/dist/utils/math.js.map +1 -0
  46. package/package.json +30 -0
  47. package/src/index.ts +329 -0
  48. package/src/tools/analyze_portfolio.ts +204 -0
  49. package/src/tools/analyze_stock.ts +204 -0
  50. package/src/tools/compare_assets.ts +181 -0
  51. package/src/tools/crypto_analysis.ts +221 -0
  52. package/src/tools/market_overview.ts +236 -0
  53. package/src/tools/screen_stocks.ts +154 -0
  54. package/src/types.ts +175 -0
  55. package/src/utils/api.ts +262 -0
  56. package/src/utils/cache.ts +65 -0
  57. package/src/utils/math.ts +332 -0
  58. package/tsconfig.json +19 -0
@@ -0,0 +1,204 @@
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 = lastValue(sma(closes, Math.min(20, closes.length)));
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
+ }
@@ -0,0 +1,181 @@
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 totalReturn = (closes[closes.length - 1] - closes[0]) / closes[0];
63
+
64
+ assetMetrics.push({
65
+ symbol: sym,
66
+ name: quote?.shortName ?? quote?.longName ?? sym,
67
+ currentPrice: quote?.regularMarketPrice ?? closes[closes.length - 1],
68
+ totalReturn,
69
+ annualizedReturn: avgDailyReturn * TRADING_DAYS,
70
+ volatility: dailyVol * Math.sqrt(TRADING_DAYS),
71
+ sharpe: sharpeRatio(returns),
72
+ mdd: maxDrawdown(closes),
73
+ returns,
74
+ });
75
+ }
76
+
77
+ // Correlation matrix
78
+ const correlations: Array<{ a: string; b: string; corr: number }> = [];
79
+ for (let i = 0; i < assetMetrics.length; i++) {
80
+ for (let j = i + 1; j < assetMetrics.length; j++) {
81
+ correlations.push({
82
+ a: assetMetrics[i].symbol,
83
+ b: assetMetrics[j].symbol,
84
+ corr: correlation(assetMetrics[i].returns, assetMetrics[j].returns),
85
+ });
86
+ }
87
+ }
88
+
89
+ // Format output
90
+ const lines: string[] = [];
91
+
92
+ lines.push("# Asset Comparison");
93
+ lines.push("");
94
+ lines.push(`**Assets:** ${symbols.join(", ")}`);
95
+ lines.push(`**Period:** ${period}`);
96
+ lines.push("");
97
+
98
+ // Performance table (transposed)
99
+ lines.push("## Performance Comparison");
100
+ lines.push("");
101
+ lines.push("| Metric | " + assetMetrics.map((a) => a.symbol).join(" | ") + " |");
102
+ lines.push("| --- | " + assetMetrics.map(() => "---").join(" | ") + " |");
103
+ lines.push("| Name | " + assetMetrics.map((a) => a.name.slice(0, 20)).join(" | ") + " |");
104
+ lines.push("| Current Price | " + assetMetrics.map((a) => `$${a.currentPrice.toFixed(2)}`).join(" | ") + " |");
105
+ lines.push("| Total Return | " + assetMetrics.map((a) => formatPct(a.totalReturn)).join(" | ") + " |");
106
+ lines.push("| Annualized Return | " + assetMetrics.map((a) => formatPct(a.annualizedReturn)).join(" | ") + " |");
107
+ lines.push("| Volatility (Ann.) | " + assetMetrics.map((a) => formatPct(a.volatility)).join(" | ") + " |");
108
+ lines.push("| Sharpe Ratio | " + assetMetrics.map((a) => a.sharpe.toFixed(3)).join(" | ") + " |");
109
+ lines.push("| Max Drawdown | " + assetMetrics.map((a) => formatPct(-a.mdd)).join(" | ") + " |");
110
+ lines.push("");
111
+
112
+ // Rankings
113
+ lines.push("## Rankings");
114
+ lines.push("");
115
+
116
+ const byReturn = [...assetMetrics].sort((a, b) => b.totalReturn - a.totalReturn);
117
+ const bySharpe = [...assetMetrics].sort((a, b) => b.sharpe - a.sharpe);
118
+ const byVol = [...assetMetrics].sort((a, b) => a.volatility - b.volatility);
119
+ const byDD = [...assetMetrics].sort((a, b) => a.mdd - b.mdd);
120
+
121
+ lines.push("| Rank | Best Return | Best Sharpe | Lowest Vol | Smallest DD |");
122
+ lines.push("|------|-------------|-------------|------------|-------------|");
123
+ for (let i = 0; i < assetMetrics.length; i++) {
124
+ lines.push(
125
+ `| ${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)}) |`,
126
+ );
127
+ }
128
+ lines.push("");
129
+
130
+ // Correlation matrix
131
+ lines.push("## Correlation Matrix");
132
+ lines.push("");
133
+
134
+ const syms = assetMetrics.map((a) => a.symbol);
135
+ lines.push("| | " + syms.join(" | ") + " |");
136
+ lines.push("| --- | " + syms.map(() => "---").join(" | ") + " |");
137
+
138
+ for (let i = 0; i < syms.length; i++) {
139
+ const row = [syms[i]];
140
+ for (let j = 0; j < syms.length; j++) {
141
+ if (i === j) {
142
+ row.push("1.000");
143
+ } else {
144
+ const pair = correlations.find(
145
+ (c) =>
146
+ (c.a === syms[i] && c.b === syms[j]) ||
147
+ (c.a === syms[j] && c.b === syms[i]),
148
+ );
149
+ row.push(pair ? pair.corr.toFixed(3) : "N/A");
150
+ }
151
+ }
152
+ lines.push("| " + row.join(" | ") + " |");
153
+ }
154
+ lines.push("");
155
+
156
+ // Correlation insight
157
+ const avgCorr = correlations.length > 0
158
+ ? mean(correlations.map((c) => c.corr))
159
+ : 0;
160
+ lines.push(`**Average correlation:** ${avgCorr.toFixed(3)}`);
161
+ if (avgCorr > 0.7) {
162
+ lines.push("These assets are highly correlated -- limited diversification benefit.");
163
+ } else if (avgCorr > 0.3) {
164
+ lines.push("Moderate correlation -- some diversification benefit.");
165
+ } else if (avgCorr > -0.1) {
166
+ lines.push("Low correlation -- good diversification potential.");
167
+ } else {
168
+ lines.push("Negative correlation -- excellent diversification. These assets tend to move in opposite directions.");
169
+ }
170
+ lines.push("");
171
+
172
+ // Summary
173
+ lines.push("## Summary");
174
+ lines.push("");
175
+ lines.push(`- **Highest return:** ${byReturn[0].symbol} (${formatPct(byReturn[0].totalReturn)})`);
176
+ lines.push(`- **Best risk-adjusted:** ${bySharpe[0].symbol} (Sharpe: ${bySharpe[0].sharpe.toFixed(3)})`);
177
+ lines.push(`- **Lowest volatility:** ${byVol[0].symbol} (${formatPct(byVol[0].volatility)})`);
178
+ lines.push(`- **Smallest drawdown:** ${byDD[0].symbol} (${formatPct(-byDD[0].mdd)})`);
179
+
180
+ return lines.join("\n");
181
+ }
@@ -0,0 +1,221 @@
1
+ /**
2
+ * crypto_analysis -- Crypto-specific analysis using CoinGecko free API.
3
+ *
4
+ * Provides price, 24h volume, market dominance, fear/greed index
5
+ * approximation, and on-chain metrics where available.
6
+ */
7
+
8
+ import {
9
+ coingeckoMarkets,
10
+ coingeckoGlobal,
11
+ symbolToCoinGeckoId,
12
+ } from "../utils/api.js";
13
+ import { formatCurrency, formatVolume, formatPct } from "../utils/math.js";
14
+ import type { CoinGeckoMarketData, CoinGeckoGlobalData } from "../types.js";
15
+
16
+ export async function handleCryptoAnalysis(symbol: string): Promise<string> {
17
+ const coinId = symbolToCoinGeckoId(symbol);
18
+
19
+ // Fetch coin data and global data in parallel
20
+ const [markets, globalData] = await Promise.all([
21
+ coingeckoMarkets([coinId]),
22
+ coingeckoGlobal(),
23
+ ]);
24
+
25
+ const coin = markets[0];
26
+ if (!coin) {
27
+ throw new Error(
28
+ `No data found for "${symbol}" (CoinGecko ID: "${coinId}"). ` +
29
+ `Try using the full coin name (e.g., "bitcoin", "ethereum") or a common ticker (BTC, ETH, SOL).`,
30
+ );
31
+ }
32
+
33
+ // Fear & Greed approximation based on available market data
34
+ const fearGreed = approximateFearGreed(coin, globalData);
35
+
36
+ // Dominance
37
+ const dominance = globalData.market_cap_percentage?.[coin.symbol.toLowerCase()] ?? null;
38
+
39
+ // Format output
40
+ const lines: string[] = [];
41
+
42
+ lines.push(`# Crypto Analysis: ${coin.name} (${coin.symbol.toUpperCase()})`);
43
+ lines.push("");
44
+
45
+ // Price overview
46
+ lines.push("## Price Overview");
47
+ lines.push("");
48
+ lines.push("| Metric | Value |");
49
+ lines.push("|--------|-------|");
50
+ lines.push(`| Current Price | $${coin.current_price.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 8 })} |`);
51
+ lines.push(`| 24h Change | ${coin.price_change_24h >= 0 ? "+" : ""}$${coin.price_change_24h.toFixed(2)} (${coin.price_change_percentage_24h >= 0 ? "+" : ""}${coin.price_change_percentage_24h.toFixed(2)}%) |`);
52
+ if (coin.price_change_percentage_7d_in_currency != null) {
53
+ lines.push(`| 7d Change | ${formatPct(coin.price_change_percentage_7d_in_currency / 100)} |`);
54
+ }
55
+ if (coin.price_change_percentage_30d_in_currency != null) {
56
+ lines.push(`| 30d Change | ${formatPct(coin.price_change_percentage_30d_in_currency / 100)} |`);
57
+ }
58
+ lines.push(`| 24h High | $${coin.high_24h.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 8 })} |`);
59
+ lines.push(`| 24h Low | $${coin.low_24h.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 8 })} |`);
60
+ lines.push("");
61
+
62
+ // Market metrics
63
+ lines.push("## Market Metrics");
64
+ lines.push("");
65
+ lines.push("| Metric | Value |");
66
+ lines.push("|--------|-------|");
67
+ lines.push(`| Market Cap | ${formatCurrency(coin.market_cap)} |`);
68
+ lines.push(`| Market Cap Rank | #${coin.market_cap_rank} |`);
69
+ lines.push(`| 24h Volume | ${formatCurrency(coin.total_volume)} |`);
70
+ lines.push(`| Volume/Market Cap | ${((coin.total_volume / coin.market_cap) * 100).toFixed(2)}% |`);
71
+ if (dominance !== null) {
72
+ lines.push(`| Market Dominance | ${dominance.toFixed(2)}% |`);
73
+ }
74
+ lines.push(`| Market Cap Change (24h) | ${coin.market_cap_change_percentage_24h >= 0 ? "+" : ""}${coin.market_cap_change_percentage_24h.toFixed(2)}% |`);
75
+ lines.push("");
76
+
77
+ // Supply info
78
+ lines.push("## Supply");
79
+ lines.push("");
80
+ lines.push("| Metric | Value |");
81
+ lines.push("|--------|-------|");
82
+ lines.push(`| Circulating Supply | ${coin.circulating_supply.toLocaleString("en-US")} |`);
83
+ if (coin.total_supply != null) {
84
+ lines.push(`| Total Supply | ${coin.total_supply.toLocaleString("en-US")} |`);
85
+ }
86
+ if (coin.max_supply != null) {
87
+ lines.push(`| Max Supply | ${coin.max_supply.toLocaleString("en-US")} |`);
88
+ const pctMined = (coin.circulating_supply / coin.max_supply * 100).toFixed(1);
89
+ lines.push(`| % Mined/Released | ${pctMined}% |`);
90
+ }
91
+ lines.push("");
92
+
93
+ // All-time records
94
+ lines.push("## All-Time Records");
95
+ lines.push("");
96
+ lines.push("| Metric | Value |");
97
+ lines.push("|--------|-------|");
98
+ lines.push(`| All-Time High | $${coin.ath.toLocaleString("en-US", { minimumFractionDigits: 2 })} |`);
99
+ lines.push(`| ATH Change | ${coin.ath_change_percentage.toFixed(2)}% |`);
100
+ lines.push(`| ATH Date | ${new Date(coin.ath_date).toLocaleDateString("en-US")} |`);
101
+ lines.push(`| All-Time Low | $${coin.atl.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 8 })} |`);
102
+ lines.push(`| ATL Change | +${coin.atl_change_percentage.toFixed(2)}% |`);
103
+ lines.push(`| ATL Date | ${new Date(coin.atl_date).toLocaleDateString("en-US")} |`);
104
+ lines.push("");
105
+
106
+ // Fear & Greed approximation
107
+ lines.push("## Market Sentiment (Approximation)");
108
+ lines.push("");
109
+ lines.push(`**Fear & Greed: ${fearGreed.score}/100 -- ${fearGreed.label}**`);
110
+ lines.push("");
111
+ lines.push("Factors considered:");
112
+ for (const factor of fearGreed.factors) {
113
+ lines.push(`- ${factor}`);
114
+ }
115
+ lines.push("");
116
+ lines.push("*Note: This is an approximation based on available market data, not the official Crypto Fear & Greed Index.*");
117
+ lines.push("");
118
+
119
+ // Global crypto market context
120
+ lines.push("## Global Crypto Market");
121
+ lines.push("");
122
+ const totalMcap = globalData.total_market_cap?.usd ?? 0;
123
+ const totalVol = globalData.total_volume?.usd ?? 0;
124
+ const btcDom = globalData.market_cap_percentage?.btc ?? 0;
125
+ const ethDom = globalData.market_cap_percentage?.eth ?? 0;
126
+
127
+ lines.push("| Metric | Value |");
128
+ lines.push("|--------|-------|");
129
+ lines.push(`| Total Crypto Market Cap | ${formatCurrency(totalMcap)} |`);
130
+ lines.push(`| Total 24h Volume | ${formatCurrency(totalVol)} |`);
131
+ lines.push(`| BTC Dominance | ${btcDom.toFixed(1)}% |`);
132
+ lines.push(`| ETH Dominance | ${ethDom.toFixed(1)}% |`);
133
+ lines.push(`| Active Cryptocurrencies | ${globalData.active_cryptocurrencies?.toLocaleString() ?? "N/A"} |`);
134
+ lines.push(`| Market Cap Change (24h) | ${globalData.market_cap_change_percentage_24h_usd >= 0 ? "+" : ""}${globalData.market_cap_change_percentage_24h_usd.toFixed(2)}% |`);
135
+
136
+ return lines.join("\n");
137
+ }
138
+
139
+ // ---------------------------------------------------------------------------
140
+ // Fear & Greed approximation
141
+ // ---------------------------------------------------------------------------
142
+
143
+ interface FearGreedResult {
144
+ score: number;
145
+ label: string;
146
+ factors: string[];
147
+ }
148
+
149
+ function approximateFearGreed(
150
+ coin: CoinGeckoMarketData,
151
+ global: CoinGeckoGlobalData,
152
+ ): FearGreedResult {
153
+ let score = 50; // Start neutral
154
+ const factors: string[] = [];
155
+
156
+ // Price momentum (24h change)
157
+ const change24h = coin.price_change_percentage_24h;
158
+ if (change24h > 5) {
159
+ score += 15;
160
+ factors.push(`Strong 24h gain (${change24h.toFixed(1)}%) -- greed signal`);
161
+ } else if (change24h > 2) {
162
+ score += 8;
163
+ factors.push(`Positive 24h movement (${change24h.toFixed(1)}%) -- mild greed`);
164
+ } else if (change24h < -5) {
165
+ score -= 15;
166
+ factors.push(`Sharp 24h decline (${change24h.toFixed(1)}%) -- fear signal`);
167
+ } else if (change24h < -2) {
168
+ score -= 8;
169
+ factors.push(`Negative 24h movement (${change24h.toFixed(1)}%) -- mild fear`);
170
+ } else {
171
+ factors.push(`Flat 24h movement (${change24h.toFixed(1)}%) -- neutral`);
172
+ }
173
+
174
+ // Volume/market cap ratio (higher = more activity = more extreme sentiment)
175
+ const volMcapRatio = coin.total_volume / coin.market_cap;
176
+ if (volMcapRatio > 0.15) {
177
+ score += Math.min(10, Math.round(volMcapRatio * 30));
178
+ factors.push(`High trading volume (${(volMcapRatio * 100).toFixed(1)}% of mcap) -- elevated activity`);
179
+ } else if (volMcapRatio < 0.03) {
180
+ score -= 5;
181
+ factors.push(`Low trading volume (${(volMcapRatio * 100).toFixed(1)}% of mcap) -- low interest`);
182
+ }
183
+
184
+ // Distance from ATH
185
+ const athDist = Math.abs(coin.ath_change_percentage);
186
+ if (athDist < 10) {
187
+ score += 10;
188
+ factors.push(`Near all-time high (${athDist.toFixed(0)}% below) -- extreme greed territory`);
189
+ } else if (athDist < 25) {
190
+ score += 5;
191
+ factors.push(`Within 25% of ATH -- greed territory`);
192
+ } else if (athDist > 70) {
193
+ score -= 10;
194
+ factors.push(`Far from ATH (${athDist.toFixed(0)}% below) -- fear territory`);
195
+ } else if (athDist > 50) {
196
+ score -= 5;
197
+ factors.push(`Significantly below ATH (${athDist.toFixed(0)}% below) -- mild fear`);
198
+ }
199
+
200
+ // Overall market direction
201
+ const marketChange = global.market_cap_change_percentage_24h_usd;
202
+ if (marketChange > 3) {
203
+ score += 8;
204
+ factors.push(`Crypto market rallying (+${marketChange.toFixed(1)}%) -- broad greed`);
205
+ } else if (marketChange < -3) {
206
+ score -= 8;
207
+ factors.push(`Crypto market declining (${marketChange.toFixed(1)}%) -- broad fear`);
208
+ }
209
+
210
+ // Clamp to 0-100
211
+ score = Math.max(0, Math.min(100, score));
212
+
213
+ let label: string;
214
+ if (score >= 75) label = "Extreme Greed";
215
+ else if (score >= 55) label = "Greed";
216
+ else if (score >= 45) label = "Neutral";
217
+ else if (score >= 25) label = "Fear";
218
+ else label = "Extreme Fear";
219
+
220
+ return { score, label, factors };
221
+ }