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,221 +0,0 @@
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.market_cap > 0 ? ((coin.total_volume / coin.market_cap) * 100).toFixed(2) : "N/A"}% |`);
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 && coin.max_supply > 0) {
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.market_cap > 0 ? coin.total_volume / coin.market_cap : 0;
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
- }
@@ -1,236 +0,0 @@
1
- /**
2
- * market_overview -- Current market snapshot.
3
- *
4
- * Fetches live data for major indices, sector ETFs, and VIX
5
- * from Yahoo Finance to build a comprehensive market summary.
6
- */
7
-
8
- import { yahooQuote } from "../utils/api.js";
9
- import { formatCurrency, formatVolume } from "../utils/math.js";
10
-
11
- /** Index/ETF symbols to track */
12
- const INDEX_SYMBOLS = [
13
- "^GSPC", // S&P 500
14
- "^IXIC", // NASDAQ Composite
15
- "^DJI", // Dow Jones
16
- "^RUT", // Russell 2000
17
- "^VIX", // VIX
18
- "^TNX", // 10-Year Treasury Yield
19
- ];
20
-
21
- /** Sector ETFs (SPDR Select Sector) */
22
- const SECTOR_ETFS = [
23
- { symbol: "XLK", name: "Technology" },
24
- { symbol: "XLV", name: "Healthcare" },
25
- { symbol: "XLF", name: "Financials" },
26
- { symbol: "XLY", name: "Consumer Discretionary" },
27
- { symbol: "XLP", name: "Consumer Staples" },
28
- { symbol: "XLE", name: "Energy" },
29
- { symbol: "XLI", name: "Industrials" },
30
- { symbol: "XLU", name: "Utilities" },
31
- { symbol: "XLRE", name: "Real Estate" },
32
- { symbol: "XLB", name: "Materials" },
33
- { symbol: "XLC", name: "Communication Services" },
34
- ];
35
-
36
- /** Other reference symbols */
37
- const OTHER_SYMBOLS = [
38
- "BTC-USD", // Bitcoin
39
- "ETH-USD", // Ethereum
40
- "GC=F", // Gold futures
41
- "CL=F", // Crude oil WTI
42
- ];
43
-
44
- export async function handleMarketOverview(): Promise<string> {
45
- // Fetch all data in parallel
46
- const allSymbols = [
47
- ...INDEX_SYMBOLS,
48
- ...SECTOR_ETFS.map((s) => s.symbol),
49
- ...OTHER_SYMBOLS,
50
- ];
51
-
52
- const quotes = await yahooQuote(allSymbols);
53
- const quoteMap = new Map(quotes.map((q) => [q.symbol, q]));
54
-
55
- const lines: string[] = [];
56
-
57
- lines.push("# Market Overview");
58
- lines.push("");
59
- lines.push(`*Live data as of ${new Date().toISOString().slice(0, 16)} UTC*`);
60
- lines.push("");
61
-
62
- // Major Indices
63
- lines.push("## Major Indices");
64
- lines.push("");
65
- lines.push("| Index | Value | Change | Change % |");
66
- lines.push("|-------|-------|--------|----------|");
67
-
68
- const indexNames: Record<string, string> = {
69
- "^GSPC": "S&P 500",
70
- "^IXIC": "NASDAQ Composite",
71
- "^DJI": "Dow Jones Industrial",
72
- "^RUT": "Russell 2000",
73
- "^VIX": "CBOE Volatility (VIX)",
74
- "^TNX": "10-Year Treasury Yield",
75
- };
76
-
77
- for (const sym of INDEX_SYMBOLS) {
78
- const q = quoteMap.get(sym);
79
- if (!q || q.regularMarketPrice == null) continue;
80
- const name = indexNames[sym] ?? q.shortName ?? sym;
81
- const price = q.regularMarketPrice;
82
- const change = q.regularMarketChange ?? 0;
83
- const changePct = q.regularMarketChangePercent ?? 0;
84
- const sign = change >= 0 ? "+" : "";
85
- const fmtPrice = price > 100
86
- ? price.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })
87
- : price.toFixed(2);
88
- lines.push(`| ${name} | ${fmtPrice} | ${sign}${change.toFixed(2)} | ${sign}${changePct.toFixed(2)}% |`);
89
- }
90
- lines.push("");
91
-
92
- // VIX interpretation
93
- const vix = quoteMap.get("^VIX");
94
- if (vix?.regularMarketPrice != null) {
95
- const vixVal = vix.regularMarketPrice;
96
- lines.push("### VIX Interpretation");
97
- lines.push("");
98
- if (vixVal < 15) {
99
- lines.push(`VIX at ${vixVal.toFixed(2)} -- **Low volatility.** Market complacency; conditions are calm.`);
100
- } else if (vixVal < 20) {
101
- lines.push(`VIX at ${vixVal.toFixed(2)} -- **Normal.** Typical market conditions.`);
102
- } else if (vixVal < 25) {
103
- lines.push(`VIX at ${vixVal.toFixed(2)} -- **Elevated.** Increasing uncertainty and hedging activity.`);
104
- } else if (vixVal < 30) {
105
- lines.push(`VIX at ${vixVal.toFixed(2)} -- **High.** Significant fear in the market.`);
106
- } else {
107
- lines.push(`VIX at ${vixVal.toFixed(2)} -- **Very high.** Extreme fear; possible market stress event.`);
108
- }
109
- lines.push("");
110
- }
111
-
112
- // Sector Performance
113
- lines.push("## Sector Performance");
114
- lines.push("");
115
- lines.push("| Sector | ETF | Price | Change % |");
116
- lines.push("|--------|-----|-------|----------|");
117
-
118
- const sectorData: Array<{ name: string; symbol: string; changePct: number }> = [];
119
- for (const etf of SECTOR_ETFS) {
120
- const q = quoteMap.get(etf.symbol);
121
- if (!q || q.regularMarketPrice == null) continue;
122
- const changePct = q.regularMarketChangePercent ?? 0;
123
- const sign = changePct >= 0 ? "+" : "";
124
- lines.push(`| ${etf.name} | ${etf.symbol} | $${q.regularMarketPrice.toFixed(2)} | ${sign}${changePct.toFixed(2)}% |`);
125
- sectorData.push({ name: etf.name, symbol: etf.symbol, changePct });
126
- }
127
- lines.push("");
128
-
129
- // Sector breadth
130
- if (sectorData.length > 0) {
131
- const positive = sectorData.filter((s) => s.changePct > 0).length;
132
- const negative = sectorData.filter((s) => s.changePct < 0).length;
133
- const bestSector = [...sectorData].sort((a, b) => b.changePct - a.changePct)[0]!;
134
- const worstSector = [...sectorData].sort((a, b) => a.changePct - b.changePct)[0]!;
135
-
136
- lines.push("### Sector Breadth");
137
- lines.push("");
138
- lines.push(`- Sectors advancing: ${positive}/${sectorData.length}`);
139
- lines.push(`- Sectors declining: ${negative}/${sectorData.length}`);
140
- lines.push(`- Best: **${bestSector.name}** (${bestSector.changePct >= 0 ? "+" : ""}${bestSector.changePct.toFixed(2)}%)`);
141
- lines.push(`- Worst: **${worstSector.name}** (${worstSector.changePct >= 0 ? "+" : ""}${worstSector.changePct.toFixed(2)}%)`);
142
- lines.push("");
143
- }
144
-
145
- // Crypto & Commodities
146
- lines.push("## Crypto & Commodities");
147
- lines.push("");
148
- lines.push("| Asset | Price | Change % |");
149
- lines.push("|-------|-------|----------|");
150
-
151
- const otherNames: Record<string, string> = {
152
- "BTC-USD": "Bitcoin",
153
- "ETH-USD": "Ethereum",
154
- "GC=F": "Gold",
155
- "CL=F": "Crude Oil WTI",
156
- };
157
-
158
- for (const sym of OTHER_SYMBOLS) {
159
- const q = quoteMap.get(sym);
160
- if (!q || q.regularMarketPrice == null) continue;
161
- const name = otherNames[sym] ?? q.shortName ?? sym;
162
- const changePct = q.regularMarketChangePercent ?? 0;
163
- const sign = changePct >= 0 ? "+" : "";
164
- lines.push(`| ${name} | $${q.regularMarketPrice.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })} | ${sign}${changePct.toFixed(2)}% |`);
165
- }
166
- lines.push("");
167
-
168
- // Market Breadth / Sentiment estimate
169
- lines.push("## Market Sentiment");
170
- lines.push("");
171
-
172
- const spx = quoteMap.get("^GSPC");
173
- const spxChange = spx?.regularMarketChangePercent ?? 0;
174
- const vixVal = vix?.regularMarketPrice ?? 20;
175
-
176
- let sentimentScore = 50;
177
-
178
- // SPX contribution
179
- if (spxChange > 1) sentimentScore += 15;
180
- else if (spxChange > 0.5) sentimentScore += 10;
181
- else if (spxChange > 0) sentimentScore += 5;
182
- else if (spxChange < -1) sentimentScore -= 15;
183
- else if (spxChange < -0.5) sentimentScore -= 10;
184
- else if (spxChange < 0) sentimentScore -= 5;
185
-
186
- // VIX contribution
187
- if (vixVal < 15) sentimentScore += 10;
188
- else if (vixVal < 20) sentimentScore += 5;
189
- else if (vixVal > 30) sentimentScore -= 15;
190
- else if (vixVal > 25) sentimentScore -= 10;
191
- else if (vixVal > 20) sentimentScore -= 5;
192
-
193
- // Sector breadth
194
- if (sectorData.length > 0) {
195
- const posRatio = sectorData.filter((s) => s.changePct > 0).length / sectorData.length;
196
- if (posRatio > 0.7) sentimentScore += 10;
197
- else if (posRatio > 0.5) sentimentScore += 5;
198
- else if (posRatio < 0.3) sentimentScore -= 10;
199
- }
200
-
201
- sentimentScore = Math.max(0, Math.min(100, sentimentScore));
202
- const sentiment = sentimentScore >= 60 ? "BULLISH" : sentimentScore <= 40 ? "BEARISH" : "NEUTRAL";
203
-
204
- lines.push(`**Sentiment: ${sentiment}** (Score: ${sentimentScore}/100)`);
205
- lines.push("");
206
-
207
- // Key observations
208
- lines.push("## Key Observations");
209
- lines.push("");
210
-
211
- if (spx) {
212
- const val = spx.regularMarketPrice?.toLocaleString("en-US", { minimumFractionDigits: 2 }) ?? "N/A";
213
- lines.push(`- S&P 500 at ${val} (${spxChange >= 0 ? "+" : ""}${spxChange.toFixed(2)}%).`);
214
- }
215
-
216
- const btc = quoteMap.get("BTC-USD");
217
- if (btc && btc.regularMarketChangePercent != null && Math.abs(btc.regularMarketChangePercent) > 2) {
218
- lines.push(`- Crypto ${btc.regularMarketChangePercent > 0 ? "rallying" : "selling off"}: Bitcoin ${btc.regularMarketChangePercent >= 0 ? "+" : ""}${btc.regularMarketChangePercent.toFixed(1)}%.`);
219
- }
220
-
221
- const oil = quoteMap.get("CL=F");
222
- if (oil && oil.regularMarketChangePercent != null && Math.abs(oil.regularMarketChangePercent) > 1) {
223
- lines.push(`- Oil ${oil.regularMarketChangePercent > 0 ? "rising" : "falling"} (${oil.regularMarketChangePercent >= 0 ? "+" : ""}${oil.regularMarketChangePercent.toFixed(1)}%).`);
224
- }
225
-
226
- const gold = quoteMap.get("GC=F");
227
- if (gold && gold.regularMarketChangePercent != null && gold.regularMarketChangePercent > 0.5) {
228
- lines.push(`- Gold up ${gold.regularMarketChangePercent.toFixed(1)}% -- possible flight to safety.`);
229
- }
230
-
231
- if (vixVal > 25) {
232
- lines.push("- Elevated VIX suggests caution; hedging demand is high.");
233
- }
234
-
235
- return lines.join("\n");
236
- }
@@ -1,156 +0,0 @@
1
- /**
2
- * screen_stocks -- Screen stocks by criteria using Yahoo Finance data.
3
- *
4
- * Fetches live quotes for a universe of popular stocks and filters
5
- * by market cap, P/E ratio, sector, volume, etc.
6
- */
7
-
8
- import { yahooScreener, POPULAR_SYMBOLS, SYMBOL_SECTORS } from "../utils/api.js";
9
- import { formatCurrency, formatVolume } from "../utils/math.js";
10
- import type { YahooQuote } from "../types.js";
11
-
12
- export interface ScreenCriteria {
13
- min_market_cap?: number;
14
- max_market_cap?: number;
15
- min_pe?: number;
16
- max_pe?: number;
17
- sector?: string;
18
- min_volume?: number;
19
- min_price?: number;
20
- max_price?: number;
21
- limit?: number;
22
- }
23
-
24
- export async function handleScreenStocks(criteria: ScreenCriteria): Promise<string> {
25
- // Fetch quotes for the full symbol universe
26
- let quotes = await yahooScreener(POPULAR_SYMBOLS);
27
-
28
- // Apply filters
29
- if (criteria.min_market_cap !== undefined) {
30
- quotes = quotes.filter((q) => (q.marketCap ?? 0) >= criteria.min_market_cap!);
31
- }
32
- if (criteria.max_market_cap !== undefined) {
33
- quotes = quotes.filter((q) => (q.marketCap ?? Infinity) <= criteria.max_market_cap!);
34
- }
35
- if (criteria.min_pe !== undefined) {
36
- quotes = quotes.filter((q) => q.trailingPE != null && q.trailingPE >= criteria.min_pe!);
37
- }
38
- if (criteria.max_pe !== undefined) {
39
- quotes = quotes.filter((q) => q.trailingPE != null && q.trailingPE <= criteria.max_pe!);
40
- }
41
- if (criteria.sector !== undefined) {
42
- const sectorLower = criteria.sector.toLowerCase();
43
- quotes = quotes.filter((q) => {
44
- const sector = q.sector ?? SYMBOL_SECTORS[q.symbol]?.sector ?? "";
45
- const industry = q.industry ?? SYMBOL_SECTORS[q.symbol]?.industry ?? "";
46
- return sector.toLowerCase().includes(sectorLower) ||
47
- industry.toLowerCase().includes(sectorLower);
48
- });
49
- }
50
- if (criteria.min_volume !== undefined) {
51
- quotes = quotes.filter((q) => (q.regularMarketVolume ?? 0) >= criteria.min_volume!);
52
- }
53
- if (criteria.min_price !== undefined) {
54
- quotes = quotes.filter((q) => (q.regularMarketPrice ?? 0) >= criteria.min_price!);
55
- }
56
- if (criteria.max_price !== undefined) {
57
- quotes = quotes.filter((q) => (q.regularMarketPrice ?? Infinity) <= criteria.max_price!);
58
- }
59
-
60
- // Sort by market cap descending
61
- quotes.sort((a, b) => (b.marketCap ?? 0) - (a.marketCap ?? 0));
62
-
63
- // Limit results
64
- const limit = criteria.limit ?? 25;
65
- const total = quotes.length;
66
- quotes = quotes.slice(0, limit);
67
-
68
- return formatScreenResults(quotes, criteria, total);
69
- }
70
-
71
- function formatScreenResults(
72
- quotes: YahooQuote[],
73
- criteria: ScreenCriteria,
74
- totalMatches: number,
75
- ): string {
76
- const lines: string[] = [];
77
-
78
- lines.push("# Stock Screener Results");
79
- lines.push("");
80
-
81
- // Filters applied
82
- const filters: string[] = [];
83
- if (criteria.min_market_cap) filters.push(`Min Mkt Cap: ${formatCurrency(criteria.min_market_cap)}`);
84
- if (criteria.max_market_cap) filters.push(`Max Mkt Cap: ${formatCurrency(criteria.max_market_cap)}`);
85
- if (criteria.min_pe) filters.push(`Min P/E: ${criteria.min_pe}`);
86
- if (criteria.max_pe) filters.push(`Max P/E: ${criteria.max_pe}`);
87
- if (criteria.sector) filters.push(`Sector: ${criteria.sector}`);
88
- if (criteria.min_volume) filters.push(`Min Volume: ${formatVolume(criteria.min_volume)}`);
89
- if (criteria.min_price) filters.push(`Min Price: $${criteria.min_price}`);
90
- if (criteria.max_price) filters.push(`Max Price: $${criteria.max_price}`);
91
-
92
- if (filters.length > 0) {
93
- lines.push(`**Filters:** ${filters.join(" | ")}`);
94
- } else {
95
- lines.push("**Filters:** None (showing full universe)");
96
- }
97
- lines.push(`**Matches:** ${totalMatches} stocks (showing top ${quotes.length})`);
98
- lines.push("");
99
-
100
- if (quotes.length === 0) {
101
- lines.push("_No stocks match the specified criteria. Try broadening your filters._");
102
- return lines.join("\n");
103
- }
104
-
105
- // Results table
106
- lines.push("| Symbol | Name | Price | Chg% | Volume | Mkt Cap | P/E | Sector |");
107
- lines.push("|--------|------|-------|------|--------|---------|-----|--------|");
108
-
109
- for (const q of quotes) {
110
- const price = q.regularMarketPrice?.toFixed(2) ?? "N/A";
111
- const changePct = q.regularMarketChangePercent != null
112
- ? `${q.regularMarketChangePercent >= 0 ? "+" : ""}${q.regularMarketChangePercent.toFixed(2)}%`
113
- : "N/A";
114
- const vol = q.regularMarketVolume ? formatVolume(q.regularMarketVolume) : "N/A";
115
- const cap = q.marketCap ? formatCurrency(q.marketCap) : "N/A";
116
- const pe = q.trailingPE?.toFixed(1) ?? "N/A";
117
- const name = (q.shortName ?? q.longName ?? q.symbol).slice(0, 25);
118
- const sector = q.sector ?? SYMBOL_SECTORS[q.symbol]?.sector ?? "--";
119
-
120
- lines.push(`| ${q.symbol} | ${name} | $${price} | ${changePct} | ${vol} | ${cap} | ${pe} | ${sector} |`);
121
- }
122
-
123
- lines.push("");
124
-
125
- // Quick stats
126
- const withPE = quotes.filter((q) => q.trailingPE != null);
127
- if (withPE.length > 0) {
128
- const avgPE = withPE.reduce((s, q) => s + q.trailingPE!, 0) / withPE.length;
129
- const minPE = Math.min(...withPE.map((q) => q.trailingPE!));
130
- const maxPE = Math.max(...withPE.map((q) => q.trailingPE!));
131
- lines.push("## P/E Statistics");
132
- lines.push("");
133
- lines.push(`- Average P/E: ${avgPE.toFixed(1)}`);
134
- lines.push(`- P/E Range: ${minPE.toFixed(1)} - ${maxPE.toFixed(1)}`);
135
- lines.push("");
136
- }
137
-
138
- // Top gainers/losers in results
139
- const sorted = [...quotes].filter((q) => q.regularMarketChangePercent != null);
140
- if (sorted.length >= 3) {
141
- sorted.sort((a, b) => (b.regularMarketChangePercent ?? 0) - (a.regularMarketChangePercent ?? 0));
142
- const top3 = sorted.slice(0, 3);
143
- const bottom3 = sorted.slice(-3).reverse();
144
-
145
- lines.push("## Notable Movers (within results)");
146
- lines.push("");
147
- lines.push("**Top gainers:** " + top3.map((q) =>
148
- `${q.symbol} (${q.regularMarketChangePercent! >= 0 ? "+" : ""}${q.regularMarketChangePercent!.toFixed(2)}%)`
149
- ).join(", "));
150
- lines.push("**Top losers:** " + bottom3.map((q) =>
151
- `${q.symbol} (${q.regularMarketChangePercent!.toFixed(2)}%)`
152
- ).join(", "));
153
- }
154
-
155
- return lines.join("\n");
156
- }