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,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
|
-
}
|