nothumanallowed 13.5.199 → 14.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.
- package/package.json +4 -2
- package/src/commands/ui.mjs +13 -6732
- package/src/constants.mjs +1 -1
- package/src/server/index.mjs +299 -0
- package/src/server/routes/agents.mjs +109 -0
- package/src/server/routes/calendar.mjs +115 -0
- package/src/server/routes/chat.mjs +350 -0
- package/src/server/routes/collab.mjs +97 -0
- package/src/server/routes/config.mjs +179 -0
- package/src/server/routes/drive.mjs +184 -0
- package/src/server/routes/email.mjs +428 -0
- package/src/server/routes/google-auth.mjs +58 -0
- package/src/server/routes/integrations.mjs +437 -0
- package/src/server/routes/studio.mjs +287 -0
- package/src/server/routes/tasks.mjs +116 -0
- package/src/server/routes/webcraft.mjs +1092 -0
- package/src/server/ws.mjs +44 -0
- package/src/services/email-db.mjs +32 -15
- package/src/services/tool-executor.mjs +702 -2
- package/src/ui-dist/3rdArmLogo.jpeg +0 -0
- package/src/ui-dist/NHALogo.jpeg +0 -0
- package/src/ui-dist/apple-touch-icon.png +0 -0
- package/src/ui-dist/assets/index-DBDRtTmR.js +423 -0
- package/src/ui-dist/assets/index-DyNVopKq.css +1 -0
- package/src/ui-dist/favicon-16x16.png +0 -0
- package/src/ui-dist/favicon-32x32.png +0 -0
- package/src/ui-dist/favicon.ico +0 -0
- package/src/ui-dist/favicon.svg +6 -0
- package/src/ui-dist/icon-192x192.png +0 -0
- package/src/ui-dist/icons.svg +24 -0
- package/src/ui-dist/index.html +17 -0
- package/src/services/web-ui.mjs +0 -10651
|
@@ -155,6 +155,11 @@ TOOLS:
|
|
|
155
155
|
13. calendar_week(startDate?: string)
|
|
156
156
|
List all events for a full week starting from startDate (YYYY-MM-DD). Defaults to current week.
|
|
157
157
|
|
|
158
|
+
13b. calendar_month(month?: string)
|
|
159
|
+
List ALL events for a full calendar month. month is YYYY-MM (e.g. "2026-05"). Defaults to current month.
|
|
160
|
+
USE THIS when the user asks for events in a month: "appuntamenti di maggio", "eventi di giugno", "show me April", "cosa ho in marzo".
|
|
161
|
+
NEVER use calendar_find with month names — use calendar_month instead.
|
|
162
|
+
|
|
158
163
|
14. calendar_create(summary: string, start: string, end: string, attendees?: string[], description?: string)
|
|
159
164
|
Create a calendar event. start/end are ISO 8601 datetime strings.
|
|
160
165
|
ALWAYS confirm with the user before creating.
|
|
@@ -549,6 +554,50 @@ TOOLS:
|
|
|
549
554
|
Download a file from Drive. For Google Docs/Sheets/Slides, exports as PDF.
|
|
550
555
|
Returns the file as base64-encoded content. Use for binary files, PDFs, images.
|
|
551
556
|
|
|
557
|
+
--- FINANCIAL MARKET DATA (Real-time) ---
|
|
558
|
+
|
|
559
|
+
82. market_price(ticker: string)
|
|
560
|
+
Get the real-time price quote for any stock, ETF, index, forex pair, or futures contract.
|
|
561
|
+
Uses Yahoo Finance — no API key needed. Returns: price, change %, day range, 52-week range, volume.
|
|
562
|
+
ticker examples: "AAPL" (Apple), "TSLA" (Tesla), "ENI.MI" (Borsa Italiana), "BMW.DE" (DAX),
|
|
563
|
+
"BTC-USD" (Bitcoin), "GC=F" (Gold futures), "EURUSD=X" (EUR/USD forex), "^GSPC" (S&P 500).
|
|
564
|
+
ALWAYS use this before any financial analysis — get real data first.
|
|
565
|
+
|
|
566
|
+
83. market_chart(ticker: string, period?: string, interval?: string)
|
|
567
|
+
Get OHLCV price history + computed technical indicators (RSI-14, MACD 12/26/9, EMA-20, EMA-50, ATR-14).
|
|
568
|
+
period: "1d" "5d" "1mo" "3mo" "6mo" "1y" "2y" "5y" "ytd" "max". Default: "3mo".
|
|
569
|
+
interval: "1m" "5m" "15m" "1h" "1d" "1wk" "1mo". Default: "1d".
|
|
570
|
+
Returns last 10 candles + full indicator breakdown + trend signal.
|
|
571
|
+
Use this for technical analysis, support/resistance, momentum assessment.
|
|
572
|
+
|
|
573
|
+
84. market_indicators(ticker: string)
|
|
574
|
+
Get comprehensive fundamental analysis: P/E, P/B, PEG, EV/EBITDA, EV/Revenue, gross/EBITDA/profit margins,
|
|
575
|
+
ROE, ROA, debt/equity, current ratio, quick ratio, revenue growth, market cap, enterprise value,
|
|
576
|
+
dividend yield, beta, short interest, analyst consensus (buy/hold/sell), and price target.
|
|
577
|
+
Use this for fundamental/value analysis, DCF inputs, and screener-style evaluation.
|
|
578
|
+
|
|
579
|
+
85. macro_data(indicator?: string)
|
|
580
|
+
Get real-time macroeconomic data. indicator: "all" | "yield" | "commodities" | "indices" | "macro".
|
|
581
|
+
Default: "all" — returns everything.
|
|
582
|
+
- yield: U.S. Treasury yield curve (3M/5Y/10Y/30Y) + inversion status (recession signal)
|
|
583
|
+
- commodities: Gold, Silver, WTI Crude, Natural Gas, EUR/USD, DXY Dollar Index
|
|
584
|
+
- indices: S&P 500, Nasdaq 100, Dow, Russell 2000, VIX, EURO STOXX 50, DAX, CAC 40, Nikkei, Shanghai
|
|
585
|
+
- macro: FRED indicators (Fed Funds Rate, CPI, Unemployment, GDP) — requires FRED_API_KEY
|
|
586
|
+
ALWAYS use this for macro regime analysis, risk-on/risk-off context, and cross-asset positioning.
|
|
587
|
+
|
|
588
|
+
86. crypto_data(coin: string, vs_currency?: string)
|
|
589
|
+
Get real-time crypto data from CoinGecko (no API key needed, 60 req/min free tier).
|
|
590
|
+
coin: CoinGecko ID like "bitcoin", "ethereum", "solana", "ripple". vs_currency default: "usd".
|
|
591
|
+
Returns: price, 24h/7d/30d/1y performance, market cap, volume, ATH, circulating supply,
|
|
592
|
+
max supply, sparkline momentum signal, and global crypto market context (BTC/ETH dominance).
|
|
593
|
+
Also returns supply scarcity % and ATH distance zone (fear/greed proxy).
|
|
594
|
+
|
|
595
|
+
87. market_news(ticker?: string, query?: string, limit?: number)
|
|
596
|
+
Get latest financial news from Yahoo Finance for a ticker or topic. limit default: 10, max: 20.
|
|
597
|
+
ticker: "AAPL", "BTC-USD", "^SPX" — returns news specific to that asset.
|
|
598
|
+
query: free-text like "Federal Reserve inflation", "AI chips", "earnings season".
|
|
599
|
+
Returns: headline, source, publish time, and URL for each article.
|
|
600
|
+
|
|
552
601
|
--- CODE EXECUTION ---
|
|
553
602
|
|
|
554
603
|
81. execute_code(language: "python"|"javascript"|"typescript", code: string, files?: [{path: string, content: string}], packages?: string[], stdin?: string, timeout?: number)
|
|
@@ -587,6 +636,9 @@ RULES:
|
|
|
587
636
|
- BROWSER TIP: Cookie/consent banners are auto-dismissed when a page loads. Do NOT waste time clicking cookie buttons — the browser handles it automatically.
|
|
588
637
|
- BROWSER TIP: When extracting data from a page, prefer browser_js with code "document.body.innerText.slice(0, 3000)" to get all visible text. This is more reliable than guessing CSS selectors.
|
|
589
638
|
- API TIP: For npm package info, use fetch_url with the registry API: fetch_url("https://registry.npmjs.org/PACKAGE/latest") for version/description, and fetch_url("https://api.npmjs.org/downloads/point/last-week/PACKAGE") for weekly downloads. These are JSON APIs, much more reliable than scraping the npm website.
|
|
639
|
+
- FINANCE TIP: For ANY financial analysis request, ALWAYS call real data tools FIRST — never fabricate prices, never use training data prices (they are stale). Workflow: market_price → market_chart → market_indicators → macro_data → analysis. For crypto: crypto_data → market_news. For macro regime: macro_data(indicator="all").
|
|
640
|
+
- FINANCE TIP: After gathering data, use canvas_render to produce a professional HTML report with Chart.js charts (price chart, indicator gauges, summary table). A visual report is ALWAYS superior to plain text for financial analysis. Use dark theme: bg #0a0a0a, green #00ff41, amber #f59e0b, red #ff4444, blue #00e5ff.
|
|
641
|
+
- FINANCE TIP: When building a trading strategy, ALWAYS cover: 1) macro regime (macro_data), 2) asset-specific technicals (market_chart), 3) fundamentals if equity (market_indicators), 4) news catalyst (market_news), 5) entry/exit/stop levels with specific prices, 6) position sizing (% of portfolio), 7) risk/reward ratio.
|
|
590
642
|
`.trim();
|
|
591
643
|
|
|
592
644
|
// ── Liara compact system prompt ───────────────────────────────────────────────
|
|
@@ -602,11 +654,12 @@ When the user's request requires an action, output one or more fenced JSON block
|
|
|
602
654
|
\`\`\`
|
|
603
655
|
Multiple blocks allowed for chaining. Include natural text before/between/after blocks.
|
|
604
656
|
Never output a JSON block as a suggestion — every block executes immediately.
|
|
657
|
+
FINANCE: For financial analysis — ALWAYS call real data tools first (market_price → market_chart → market_indicators OR crypto_data + macro_data), then canvas_render with a full Chart.js HTML dashboard (dark theme: bg #070b0f, green #00ff41, amber #f59e0b, red #ff4444, blue #00e5ff). Never invent prices.
|
|
605
658
|
|
|
606
659
|
AVAILABLE TOOLS:
|
|
607
660
|
gmail_list · gmail_read · gmail_send · gmail_draft · gmail_reply · gmail_mark_read · gmail_mark_unread · gmail_archive · gmail_delete · gmail_send_attach
|
|
608
661
|
imap_accounts · imap_list · imap_read · imap_send · imap_sync · imap_labels · imap_mark_read · imap_reply · imap_thread · imap_search · imap_mark_starred · imap_trash · imap_draft · imap_send_template · imap_bulk_send
|
|
609
|
-
calendar_today · calendar_tomorrow · calendar_date · calendar_upcoming · calendar_week · calendar_create · calendar_move · calendar_find · calendar_update · calendar_delete · schedule_meeting · schedule_draft_email
|
|
662
|
+
calendar_today · calendar_tomorrow · calendar_date · calendar_upcoming · calendar_week · calendar_month · calendar_create · calendar_move · calendar_find · calendar_update · calendar_delete · schedule_meeting · schedule_draft_email
|
|
610
663
|
task_list · task_add · task_done · task_move · task_delete · task_clear · task_edit
|
|
611
664
|
contact_search · contact_add · contact_update · contact_delete
|
|
612
665
|
gtask_list · gtask_add · gtask_complete
|
|
@@ -622,7 +675,8 @@ canvas_render · canvas_clear
|
|
|
622
675
|
collab_send · collab_read
|
|
623
676
|
file_list · file_read · file_write · file_info · file_search
|
|
624
677
|
drive_list · drive_read · drive_upload · drive_update · drive_delete · drive_info · drive_folder · drive_download
|
|
625
|
-
maps_directions · notify_remind · birthdays_upcoming · birthday_add · execute_code
|
|
678
|
+
maps_directions · notify_remind · birthdays_upcoming · birthday_add · execute_code
|
|
679
|
+
market_price · market_chart · market_indicators · macro_data · crypto_data · market_news`.trim();
|
|
626
680
|
|
|
627
681
|
// ── Action Parser ────────────────────────────────────────────────────────────
|
|
628
682
|
|
|
@@ -1298,6 +1352,46 @@ export async function executeTool(action, params, config) {
|
|
|
1298
1352
|
return lines.join('\n');
|
|
1299
1353
|
}
|
|
1300
1354
|
|
|
1355
|
+
case 'calendar_month': {
|
|
1356
|
+
// Determine target month: params.month = 'YYYY-MM' or defaults to current month
|
|
1357
|
+
const now = new Date();
|
|
1358
|
+
let year = now.getFullYear();
|
|
1359
|
+
let month = now.getMonth(); // 0-based
|
|
1360
|
+
|
|
1361
|
+
if (params.month && /^\d{4}-\d{2}$/.test(params.month)) {
|
|
1362
|
+
const parts = params.month.split('-');
|
|
1363
|
+
year = parseInt(parts[0], 10);
|
|
1364
|
+
month = parseInt(parts[1], 10) - 1; // convert to 0-based
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
const from = new Date(year, month, 1, 0, 0, 0);
|
|
1368
|
+
const to = new Date(year, month + 1, 1, 0, 0, 0);
|
|
1369
|
+
const events = await listEvents(config, 'primary', from, to);
|
|
1370
|
+
const monthLabel = from.toLocaleDateString('it-IT', { month: 'long', year: 'numeric' });
|
|
1371
|
+
|
|
1372
|
+
if (events.length === 0) return `Nessun evento trovato per ${monthLabel}.`;
|
|
1373
|
+
|
|
1374
|
+
const byDay = new Map();
|
|
1375
|
+
for (const e of events) {
|
|
1376
|
+
const day = e.start.split('T')[0];
|
|
1377
|
+
if (!byDay.has(day)) byDay.set(day, []);
|
|
1378
|
+
byDay.get(day).push(e);
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
const lines = [`📅 ${monthLabel} — ${events.length} eventi:`];
|
|
1382
|
+
for (const [day, dayEvents] of [...byDay.entries()].sort()) {
|
|
1383
|
+
const d = new Date(day + 'T12:00:00');
|
|
1384
|
+
const dayName = d.toLocaleDateString('it-IT', { weekday: 'short', day: 'numeric' });
|
|
1385
|
+
lines.push(`\n${dayName}:`);
|
|
1386
|
+
for (const e of dayEvents) {
|
|
1387
|
+
const time = e.isAllDay ? 'Tutto il giorno' : `${formatTime(e.start)} - ${formatTime(e.end)}`;
|
|
1388
|
+
const loc = e.location ? ` @ ${e.location}` : '';
|
|
1389
|
+
lines.push(` ${time} — ${e.summary}${loc}`);
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
return lines.join('\n');
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1301
1395
|
case 'calendar_find': {
|
|
1302
1396
|
const query = (params.query || '').toLowerCase();
|
|
1303
1397
|
const daysAhead = params.daysAhead || 30;
|
|
@@ -2625,6 +2719,612 @@ export async function executeTool(action, params, config) {
|
|
|
2625
2719
|
}
|
|
2626
2720
|
}
|
|
2627
2721
|
|
|
2722
|
+
// ── Financial Market Data ────────────────────────────────────────────
|
|
2723
|
+
// All endpoints are free-tier, no API key required for Yahoo Finance, CoinGecko.
|
|
2724
|
+
// FRED macro data requires a free key (optional — falls back gracefully).
|
|
2725
|
+
|
|
2726
|
+
case 'market_price': {
|
|
2727
|
+
const ticker = (params.ticker || '').trim().toUpperCase();
|
|
2728
|
+
if (!ticker) return 'market_price: ticker is required (e.g. "AAPL", "BTC-USD", "EURUSD=X")';
|
|
2729
|
+
|
|
2730
|
+
try {
|
|
2731
|
+
const url = `https://query1.finance.yahoo.com/v8/finance/chart/${encodeURIComponent(ticker)}?interval=1m&range=1d&includePrePost=true&events=div%2Csplits`;
|
|
2732
|
+
const res = await fetch(url, {
|
|
2733
|
+
headers: {
|
|
2734
|
+
'User-Agent': 'Mozilla/5.0 (compatible; NHA/1.0)',
|
|
2735
|
+
'Accept': 'application/json',
|
|
2736
|
+
},
|
|
2737
|
+
});
|
|
2738
|
+
if (!res.ok) return `market_price: Yahoo Finance returned HTTP ${res.status} for ${ticker}. Check the ticker symbol.`;
|
|
2739
|
+
const data = await res.json();
|
|
2740
|
+
const meta = data?.chart?.result?.[0]?.meta;
|
|
2741
|
+
if (!meta) return `market_price: No data found for ticker "${ticker}". Try adding the exchange suffix (e.g. "ENI.MI" for Borsa Italiana, "BMW.DE" for XETRA).`;
|
|
2742
|
+
|
|
2743
|
+
const price = meta.regularMarketPrice;
|
|
2744
|
+
const prev = meta.previousClose || meta.chartPreviousClose;
|
|
2745
|
+
const change = prev ? price - prev : null;
|
|
2746
|
+
const changePct = prev ? ((price - prev) / prev) * 100 : null;
|
|
2747
|
+
const currency = meta.currency || 'USD';
|
|
2748
|
+
const exchange = meta.exchangeName || meta.fullExchangeName || '';
|
|
2749
|
+
const marketState = meta.marketState || 'UNKNOWN';
|
|
2750
|
+
const dayHigh = meta.regularMarketDayHigh;
|
|
2751
|
+
const dayLow = meta.regularMarketDayLow;
|
|
2752
|
+
const volume = meta.regularMarketVolume;
|
|
2753
|
+
const fiftyTwoHigh = meta.fiftyTwoWeekHigh;
|
|
2754
|
+
const fiftyTwoLow = meta.fiftyTwoWeekLow;
|
|
2755
|
+
|
|
2756
|
+
const fmtNum = (n, d=2) => n != null ? n.toFixed(d) : 'N/A';
|
|
2757
|
+
const fmtVol = (n) => {
|
|
2758
|
+
if (n == null) return 'N/A';
|
|
2759
|
+
if (n >= 1e9) return (n/1e9).toFixed(2) + 'B';
|
|
2760
|
+
if (n >= 1e6) return (n/1e6).toFixed(2) + 'M';
|
|
2761
|
+
if (n >= 1e3) return (n/1e3).toFixed(1) + 'K';
|
|
2762
|
+
return n.toString();
|
|
2763
|
+
};
|
|
2764
|
+
|
|
2765
|
+
const arrow = change == null ? '' : change >= 0 ? '+' : '';
|
|
2766
|
+
const lines = [
|
|
2767
|
+
`${ticker} — ${exchange} [${marketState}]`,
|
|
2768
|
+
`Price: ${fmtNum(price)} ${currency} ${arrow}${fmtNum(change)} (${arrow}${fmtNum(changePct)}%)`,
|
|
2769
|
+
`Day Range: ${fmtNum(dayLow)} – ${fmtNum(dayHigh)}`,
|
|
2770
|
+
`52-Week Range: ${fmtNum(fiftyTwoLow)} – ${fmtNum(fiftyTwoHigh)}`,
|
|
2771
|
+
`Volume: ${fmtVol(volume)}`,
|
|
2772
|
+
`As of: ${new Date(meta.regularMarketTime * 1000).toISOString()}`,
|
|
2773
|
+
];
|
|
2774
|
+
return lines.join('\n');
|
|
2775
|
+
} catch (e) {
|
|
2776
|
+
return `market_price error: ${e.message}`;
|
|
2777
|
+
}
|
|
2778
|
+
}
|
|
2779
|
+
|
|
2780
|
+
case 'market_chart': {
|
|
2781
|
+
const ticker = (params.ticker || '').trim().toUpperCase();
|
|
2782
|
+
const period = params.period || '3mo'; // 1d 5d 1mo 3mo 6mo 1y 2y 5y 10y ytd max
|
|
2783
|
+
const interval = params.interval || '1d'; // 1m 2m 5m 15m 30m 60m 90m 1h 1d 5d 1wk 1mo 3mo
|
|
2784
|
+
if (!ticker) return 'market_chart: ticker is required';
|
|
2785
|
+
|
|
2786
|
+
try {
|
|
2787
|
+
const url = `https://query1.finance.yahoo.com/v8/finance/chart/${encodeURIComponent(ticker)}?interval=${interval}&range=${period}&events=div%2Csplits`;
|
|
2788
|
+
const res = await fetch(url, { headers: { 'User-Agent': 'Mozilla/5.0 (compatible; NHA/1.0)' } });
|
|
2789
|
+
if (!res.ok) return `market_chart: HTTP ${res.status} for ${ticker}`;
|
|
2790
|
+
const data = await res.json();
|
|
2791
|
+
const result = data?.chart?.result?.[0];
|
|
2792
|
+
if (!result) return `market_chart: No data for "${ticker}"`;
|
|
2793
|
+
|
|
2794
|
+
const meta = result.meta;
|
|
2795
|
+
const timestamps = result.timestamp || [];
|
|
2796
|
+
const ohlcv = result.indicators?.quote?.[0] || {};
|
|
2797
|
+
const closes = ohlcv.close || [];
|
|
2798
|
+
const opens = ohlcv.open || [];
|
|
2799
|
+
const highs = ohlcv.high || [];
|
|
2800
|
+
const lows = ohlcv.low || [];
|
|
2801
|
+
const volumes = ohlcv.volume || [];
|
|
2802
|
+
|
|
2803
|
+
// Filter valid candles
|
|
2804
|
+
const candles = timestamps.map((t, i) => ({
|
|
2805
|
+
date: new Date(t * 1000).toISOString().slice(0, 10),
|
|
2806
|
+
open: opens[i], high: highs[i], low: lows[i],
|
|
2807
|
+
close: closes[i], volume: volumes[i],
|
|
2808
|
+
})).filter(c => c.close != null);
|
|
2809
|
+
|
|
2810
|
+
if (candles.length < 2) return `market_chart: insufficient data for ${ticker}`;
|
|
2811
|
+
|
|
2812
|
+
// ── Technical indicators (computed in pure JS — no dependencies) ──
|
|
2813
|
+
|
|
2814
|
+
// EMA helper
|
|
2815
|
+
const ema = (arr, n) => {
|
|
2816
|
+
const k = 2 / (n + 1);
|
|
2817
|
+
const out = [];
|
|
2818
|
+
let prev = null;
|
|
2819
|
+
for (const v of arr) {
|
|
2820
|
+
if (v == null) { out.push(null); continue; }
|
|
2821
|
+
if (prev == null) { out.push(v); prev = v; continue; }
|
|
2822
|
+
const e = v * k + prev * (1 - k);
|
|
2823
|
+
out.push(e);
|
|
2824
|
+
prev = e;
|
|
2825
|
+
}
|
|
2826
|
+
return out;
|
|
2827
|
+
};
|
|
2828
|
+
|
|
2829
|
+
const priceArr = candles.map(c => c.close);
|
|
2830
|
+
|
|
2831
|
+
// EMA 20 & 50
|
|
2832
|
+
const ema20arr = ema(priceArr, 20);
|
|
2833
|
+
const ema50arr = ema(priceArr, 50);
|
|
2834
|
+
const ema20 = ema20arr[ema20arr.length - 1];
|
|
2835
|
+
const ema50 = ema50arr[ema50arr.length - 1];
|
|
2836
|
+
|
|
2837
|
+
// RSI 14
|
|
2838
|
+
let gains = 0, losses = 0;
|
|
2839
|
+
const rsiBars = priceArr.slice(-15);
|
|
2840
|
+
for (let i = 1; i < rsiBars.length; i++) {
|
|
2841
|
+
const d = rsiBars[i] - rsiBars[i - 1];
|
|
2842
|
+
if (d > 0) gains += d; else losses += Math.abs(d);
|
|
2843
|
+
}
|
|
2844
|
+
const avgGain = gains / 14;
|
|
2845
|
+
const avgLoss = losses / 14;
|
|
2846
|
+
const rsi = avgLoss === 0 ? 100 : 100 - (100 / (1 + avgGain / avgLoss));
|
|
2847
|
+
|
|
2848
|
+
// MACD (12/26/9)
|
|
2849
|
+
const ema12arr = ema(priceArr, 12);
|
|
2850
|
+
const ema26arr = ema(priceArr, 26);
|
|
2851
|
+
const macdLine = ema12arr.map((v, i) => (v != null && ema26arr[i] != null) ? v - ema26arr[i] : null);
|
|
2852
|
+
const macdSignal = ema(macdLine.filter(v => v != null), 9);
|
|
2853
|
+
const macdVal = macdLine[macdLine.length - 1];
|
|
2854
|
+
const signalVal = macdSignal[macdSignal.length - 1];
|
|
2855
|
+
const macdHist = macdVal != null && signalVal != null ? macdVal - signalVal : null;
|
|
2856
|
+
|
|
2857
|
+
// ATR 14
|
|
2858
|
+
const atrPeriod = 14;
|
|
2859
|
+
const trs = candles.slice(-(atrPeriod + 1)).map((c, i, arr) => {
|
|
2860
|
+
if (i === 0) return c.high - c.low;
|
|
2861
|
+
const prev = arr[i - 1].close;
|
|
2862
|
+
return Math.max(c.high - c.low, Math.abs(c.high - prev), Math.abs(c.low - prev));
|
|
2863
|
+
});
|
|
2864
|
+
const atr = trs.reduce((a, b) => a + b, 0) / trs.length;
|
|
2865
|
+
|
|
2866
|
+
// Volume SMA 20
|
|
2867
|
+
const vols = volumes.filter(v => v != null);
|
|
2868
|
+
const volSma20 = vols.slice(-20).reduce((a, b) => a + b, 0) / Math.min(20, vols.length);
|
|
2869
|
+
|
|
2870
|
+
const lastCandle = candles[candles.length - 1];
|
|
2871
|
+
const firstCandle = candles[0];
|
|
2872
|
+
const totalReturn = ((lastCandle.close / firstCandle.close) - 1) * 100;
|
|
2873
|
+
|
|
2874
|
+
const maxHigh = Math.max(...candles.map(c => c.high).filter(Boolean));
|
|
2875
|
+
const minLow = Math.min(...candles.map(c => c.low).filter(Boolean));
|
|
2876
|
+
|
|
2877
|
+
const fmt2 = (n) => n != null ? n.toFixed(2) : 'N/A';
|
|
2878
|
+
const fmtV = (n) => {
|
|
2879
|
+
if (n == null) return 'N/A';
|
|
2880
|
+
if (n >= 1e9) return (n/1e9).toFixed(1) + 'B';
|
|
2881
|
+
if (n >= 1e6) return (n/1e6).toFixed(1) + 'M';
|
|
2882
|
+
return (n/1e3).toFixed(0) + 'K';
|
|
2883
|
+
};
|
|
2884
|
+
|
|
2885
|
+
const rsiSignal = rsi < 30 ? 'OVERSOLD' : rsi > 70 ? 'OVERBOUGHT' : 'NEUTRAL';
|
|
2886
|
+
const macdSignalStr = macdHist != null ? (macdHist > 0 ? 'BULLISH' : 'BEARISH') : 'N/A';
|
|
2887
|
+
const trend = ema20 && ema50 ? (ema20 > ema50 ? 'BULLISH (EMA20 > EMA50)' : 'BEARISH (EMA20 < EMA50)') : 'N/A';
|
|
2888
|
+
|
|
2889
|
+
const lines = [
|
|
2890
|
+
`${ticker} Chart — ${period} / ${interval} bars (${candles.length} candles)`,
|
|
2891
|
+
`Currency: ${meta.currency || 'USD'} | Exchange: ${meta.exchangeName || ''}`,
|
|
2892
|
+
'',
|
|
2893
|
+
`── Price Summary ──`,
|
|
2894
|
+
`Current: ${fmt2(lastCandle.close)} (${totalReturn >= 0 ? '+' : ''}${totalReturn.toFixed(2)}% period return)`,
|
|
2895
|
+
`Period High: ${fmt2(maxHigh)} | Period Low: ${fmt2(minLow)}`,
|
|
2896
|
+
`Last Candle: O ${fmt2(lastCandle.open)} H ${fmt2(lastCandle.high)} L ${fmt2(lastCandle.low)} C ${fmt2(lastCandle.close)} V ${fmtV(lastCandle.volume)}`,
|
|
2897
|
+
'',
|
|
2898
|
+
`── Technical Indicators ──`,
|
|
2899
|
+
`RSI(14): ${fmt2(rsi)} → ${rsiSignal}`,
|
|
2900
|
+
`MACD(12,26,9): Line ${fmt2(macdVal)} / Signal ${fmt2(signalVal)} / Hist ${fmt2(macdHist)} → ${macdSignalStr}`,
|
|
2901
|
+
`EMA(20): ${fmt2(ema20)}`,
|
|
2902
|
+
`EMA(50): ${fmt2(ema50)}`,
|
|
2903
|
+
`Trend: ${trend}`,
|
|
2904
|
+
`ATR(14): ${fmt2(atr)} (volatility proxy)`,
|
|
2905
|
+
`Vol SMA(20): ${fmtV(volSma20)} | Last Volume: ${fmtV(lastCandle.volume)}`,
|
|
2906
|
+
'',
|
|
2907
|
+
`── Last 10 Candles (OHLCV) ──`,
|
|
2908
|
+
candles.slice(-10).map(c =>
|
|
2909
|
+
`${c.date} O ${fmt2(c.open)} H ${fmt2(c.high)} L ${fmt2(c.low)} C ${fmt2(c.close)} V ${fmtV(c.volume)}`
|
|
2910
|
+
).join('\n'),
|
|
2911
|
+
];
|
|
2912
|
+
|
|
2913
|
+
return lines.join('\n');
|
|
2914
|
+
} catch (e) {
|
|
2915
|
+
return `market_chart error: ${e.message}`;
|
|
2916
|
+
}
|
|
2917
|
+
}
|
|
2918
|
+
|
|
2919
|
+
case 'market_indicators': {
|
|
2920
|
+
const ticker = (params.ticker || '').trim().toUpperCase();
|
|
2921
|
+
if (!ticker) return 'market_indicators: ticker is required';
|
|
2922
|
+
|
|
2923
|
+
try {
|
|
2924
|
+
// Fetch quote summary — fundamentals, key stats, analyst data
|
|
2925
|
+
const url = `https://query1.finance.yahoo.com/v11/finance/quoteSummary/${encodeURIComponent(ticker)}?modules=summaryDetail%2CdefaultKeyStatistics%2CfinancialData%2CassetProfile%2CearningsTrend%2CrecommendationTrend`;
|
|
2926
|
+
const res = await fetch(url, { headers: { 'User-Agent': 'Mozilla/5.0 (compatible; NHA/1.0)' } });
|
|
2927
|
+
if (!res.ok) return `market_indicators: HTTP ${res.status} for ${ticker}`;
|
|
2928
|
+
const data = await res.json();
|
|
2929
|
+
const r = data?.quoteSummary?.result?.[0];
|
|
2930
|
+
if (!r) return `market_indicators: No fundamental data for "${ticker}"`;
|
|
2931
|
+
|
|
2932
|
+
const sd = r.summaryDetail || {};
|
|
2933
|
+
const ks = r.defaultKeyStatistics || {};
|
|
2934
|
+
const fd = r.financialData || {};
|
|
2935
|
+
const ap = r.assetProfile || {};
|
|
2936
|
+
const rt = r.recommendationTrend?.trend?.[0] || {};
|
|
2937
|
+
|
|
2938
|
+
const fmtN = (v) => v?.raw != null ? v.raw.toFixed(2) : (v?.fmt || 'N/A');
|
|
2939
|
+
const fmtPct = (v) => v?.raw != null ? (v.raw * 100).toFixed(2) + '%' : (v?.fmt || 'N/A');
|
|
2940
|
+
const fmtLg = (v) => {
|
|
2941
|
+
const n = v?.raw ?? v;
|
|
2942
|
+
if (n == null) return 'N/A';
|
|
2943
|
+
if (n >= 1e12) return (n/1e12).toFixed(2) + 'T';
|
|
2944
|
+
if (n >= 1e9) return (n/1e9).toFixed(2) + 'B';
|
|
2945
|
+
if (n >= 1e6) return (n/1e6).toFixed(2) + 'M';
|
|
2946
|
+
return n.toFixed(0);
|
|
2947
|
+
};
|
|
2948
|
+
|
|
2949
|
+
const totalBuy = (rt.strongBuy?.raw || 0) + (rt.buy?.raw || 0);
|
|
2950
|
+
const totalSell = (rt.strongSell?.raw || 0) + (rt.sell?.raw || 0);
|
|
2951
|
+
const totalHold = rt.hold?.raw || 0;
|
|
2952
|
+
const analystSum = totalBuy + totalSell + totalHold;
|
|
2953
|
+
const analystRec = analystSum > 0
|
|
2954
|
+
? `Buy: ${totalBuy} | Hold: ${totalHold} | Sell: ${totalSell} (${analystSum} analysts)`
|
|
2955
|
+
: 'N/A';
|
|
2956
|
+
|
|
2957
|
+
const lines = [
|
|
2958
|
+
`${ticker} — Fundamental & Key Indicators`,
|
|
2959
|
+
`Sector: ${ap.sector || 'N/A'} | Industry: ${ap.industry || 'N/A'}`,
|
|
2960
|
+
'',
|
|
2961
|
+
`── Valuation ──`,
|
|
2962
|
+
`Market Cap: ${fmtLg(sd.marketCap)}`,
|
|
2963
|
+
`Enterprise Value: ${fmtLg(ks.enterpriseValue)}`,
|
|
2964
|
+
`P/E (trailing): ${fmtN(sd.trailingPE)}`,
|
|
2965
|
+
`P/E (forward): ${fmtN(sd.forwardPE)}`,
|
|
2966
|
+
`PEG Ratio: ${fmtN(ks.pegRatio)}`,
|
|
2967
|
+
`P/B Ratio: ${fmtN(ks.priceToBook)}`,
|
|
2968
|
+
`EV/EBITDA: ${fmtN(ks.enterpriseToEbitda)}`,
|
|
2969
|
+
`EV/Revenue: ${fmtN(ks.enterpriseToRevenue)}`,
|
|
2970
|
+
'',
|
|
2971
|
+
`── Profitability ──`,
|
|
2972
|
+
`Revenue (TTM): ${fmtLg(fd.totalRevenue)}`,
|
|
2973
|
+
`Revenue Growth (YoY): ${fmtPct(fd.revenueGrowth)}`,
|
|
2974
|
+
`Gross Margin: ${fmtPct(fd.grossMargins)}`,
|
|
2975
|
+
`EBITDA Margin: ${fmtPct(fd.ebitdaMargins)}`,
|
|
2976
|
+
`Operating Margin: ${fmtPct(fd.operatingMargins)}`,
|
|
2977
|
+
`Profit Margin: ${fmtPct(fd.profitMargins)}`,
|
|
2978
|
+
`ROE: ${fmtPct(fd.returnOnEquity)}`,
|
|
2979
|
+
`ROA: ${fmtPct(fd.returnOnAssets)}`,
|
|
2980
|
+
'',
|
|
2981
|
+
`── Balance Sheet ──`,
|
|
2982
|
+
`Total Cash: ${fmtLg(fd.totalCash)}`,
|
|
2983
|
+
`Total Debt: ${fmtLg(fd.totalDebt)}`,
|
|
2984
|
+
`Debt/Equity: ${fmtN(fd.debtToEquity)}`,
|
|
2985
|
+
`Current Ratio: ${fmtN(fd.currentRatio)}`,
|
|
2986
|
+
`Quick Ratio: ${fmtN(fd.quickRatio)}`,
|
|
2987
|
+
'',
|
|
2988
|
+
`── Dividends & Shares ──`,
|
|
2989
|
+
`Dividend Yield: ${fmtPct(sd.dividendYield)}`,
|
|
2990
|
+
`Payout Ratio: ${fmtPct(sd.payoutRatio)}`,
|
|
2991
|
+
`Shares Out: ${fmtLg(ks.sharesOutstanding)}`,
|
|
2992
|
+
`Float: ${fmtLg(ks.floatShares)}`,
|
|
2993
|
+
`Short Interest: ${fmtPct(ks.shortPercentOfFloat)}`,
|
|
2994
|
+
`Beta: ${fmtN(sd.beta)}`,
|
|
2995
|
+
'',
|
|
2996
|
+
`── Analyst Consensus ──`,
|
|
2997
|
+
`Recommendation: ${fd.recommendationKey?.toUpperCase() || 'N/A'}`,
|
|
2998
|
+
`Target Price: ${fmtN(fd.targetMeanPrice)} (low ${fmtN(fd.targetLowPrice)} / high ${fmtN(fd.targetHighPrice)})`,
|
|
2999
|
+
`Analysts: ${analystRec}`,
|
|
3000
|
+
];
|
|
3001
|
+
|
|
3002
|
+
return lines.join('\n');
|
|
3003
|
+
} catch (e) {
|
|
3004
|
+
return `market_indicators error: ${e.message}`;
|
|
3005
|
+
}
|
|
3006
|
+
}
|
|
3007
|
+
|
|
3008
|
+
case 'macro_data': {
|
|
3009
|
+
// Returns key macro indicators from multiple free sources
|
|
3010
|
+
// FRED requires a free API key (optional — config.fredApiKey or env FRED_API_KEY)
|
|
3011
|
+
const indicator = (params.indicator || 'all').toLowerCase();
|
|
3012
|
+
const fredKey = config?.fredApiKey || process.env.FRED_API_KEY || null;
|
|
3013
|
+
|
|
3014
|
+
const results = [];
|
|
3015
|
+
|
|
3016
|
+
// ── 1. Treasury yields from Yahoo Finance (always available) ──
|
|
3017
|
+
const yieldTickers = [
|
|
3018
|
+
['^IRX', '13-Week T-Bill'],
|
|
3019
|
+
['^FVX', '5-Year T-Note'],
|
|
3020
|
+
['^TNX', '10-Year T-Note'],
|
|
3021
|
+
['^TYX', '30-Year T-Bond'],
|
|
3022
|
+
];
|
|
3023
|
+
|
|
3024
|
+
if (indicator === 'all' || indicator === 'yield' || indicator === 'yields' || indicator === 'curve') {
|
|
3025
|
+
const yieldResults = await Promise.all(yieldTickers.map(async ([sym, name]) => {
|
|
3026
|
+
try {
|
|
3027
|
+
const url = `https://query1.finance.yahoo.com/v8/finance/chart/${encodeURIComponent(sym)}?interval=1d&range=5d`;
|
|
3028
|
+
const res = await fetch(url, { headers: { 'User-Agent': 'Mozilla/5.0 (compatible; NHA/1.0)' } });
|
|
3029
|
+
const d = await res.json();
|
|
3030
|
+
const meta = d?.chart?.result?.[0]?.meta;
|
|
3031
|
+
if (meta?.regularMarketPrice) return ` ${name.padEnd(20)} ${meta.regularMarketPrice.toFixed(3)}%`;
|
|
3032
|
+
return ` ${name.padEnd(20)} N/A`;
|
|
3033
|
+
} catch { return ` ${name.padEnd(20)} N/A`; }
|
|
3034
|
+
}));
|
|
3035
|
+
results.push('── U.S. Treasury Yield Curve ──');
|
|
3036
|
+
results.push(...yieldResults);
|
|
3037
|
+
// Inversion check
|
|
3038
|
+
try {
|
|
3039
|
+
const r2 = await fetch('https://query1.finance.yahoo.com/v8/finance/chart/%5EIRX?interval=1d&range=5d', { headers: { 'User-Agent': 'Mozilla/5.0 (compatible; NHA/1.0)' } });
|
|
3040
|
+
const r10 = await fetch('https://query1.finance.yahoo.com/v8/finance/chart/%5ETNX?interval=1d&range=5d', { headers: { 'User-Agent': 'Mozilla/5.0 (compatible; NHA/1.0)' } });
|
|
3041
|
+
const d2 = (await r2.json())?.chart?.result?.[0]?.meta;
|
|
3042
|
+
const d10 = (await r10.json())?.chart?.result?.[0]?.meta;
|
|
3043
|
+
if (d2 && d10) {
|
|
3044
|
+
const spread = d10.regularMarketPrice - d2.regularMarketPrice;
|
|
3045
|
+
const inv = spread < 0 ? ' *** INVERTED — recession signal ***' : '';
|
|
3046
|
+
results.push(` 10Y-3M Spread: ${spread.toFixed(3)}%${inv}`);
|
|
3047
|
+
}
|
|
3048
|
+
} catch {}
|
|
3049
|
+
results.push('');
|
|
3050
|
+
}
|
|
3051
|
+
|
|
3052
|
+
// ── 2. Key commodities & FX ──
|
|
3053
|
+
const commodTickers = [
|
|
3054
|
+
['GC=F', 'Gold ($/oz)'],
|
|
3055
|
+
['SI=F', 'Silver ($/oz)'],
|
|
3056
|
+
['CL=F', 'WTI Crude Oil ($/bbl)'],
|
|
3057
|
+
['NG=F', 'Nat. Gas ($/MMBtu)'],
|
|
3058
|
+
['EURUSD=X', 'EUR/USD'],
|
|
3059
|
+
['DX-Y.NYB', 'DXY (Dollar Index)'],
|
|
3060
|
+
];
|
|
3061
|
+
|
|
3062
|
+
if (indicator === 'all' || indicator === 'commodities' || indicator === 'fx' || indicator === 'commodity') {
|
|
3063
|
+
const commodResults = await Promise.all(commodTickers.map(async ([sym, name]) => {
|
|
3064
|
+
try {
|
|
3065
|
+
const url = `https://query1.finance.yahoo.com/v8/finance/chart/${encodeURIComponent(sym)}?interval=1d&range=5d`;
|
|
3066
|
+
const res = await fetch(url, { headers: { 'User-Agent': 'Mozilla/5.0 (compatible; NHA/1.0)' } });
|
|
3067
|
+
const d = await res.json();
|
|
3068
|
+
const meta = d?.chart?.result?.[0]?.meta;
|
|
3069
|
+
if (!meta?.regularMarketPrice) return ` ${name.padEnd(26)} N/A`;
|
|
3070
|
+
const prev = meta.previousClose || meta.chartPreviousClose;
|
|
3071
|
+
const chg = prev ? ((meta.regularMarketPrice - prev) / prev * 100).toFixed(2) : null;
|
|
3072
|
+
const chgStr = chg != null ? ` (${parseFloat(chg) >= 0 ? '+' : ''}${chg}%)` : '';
|
|
3073
|
+
return ` ${name.padEnd(26)} ${meta.regularMarketPrice.toFixed(3)}${chgStr}`;
|
|
3074
|
+
} catch { return ` ${name.padEnd(26)} N/A`; }
|
|
3075
|
+
}));
|
|
3076
|
+
results.push('── Commodities & FX ──');
|
|
3077
|
+
results.push(...commodResults);
|
|
3078
|
+
results.push('');
|
|
3079
|
+
}
|
|
3080
|
+
|
|
3081
|
+
// ── 3. Major equity indices ──
|
|
3082
|
+
const indexTickers = [
|
|
3083
|
+
['^GSPC', 'S&P 500'],
|
|
3084
|
+
['^NDX', 'Nasdaq 100'],
|
|
3085
|
+
['^DJI', 'Dow Jones'],
|
|
3086
|
+
['^RUT', 'Russell 2000'],
|
|
3087
|
+
['^VIX', 'VIX (Fear Index)'],
|
|
3088
|
+
['^STOXX50E', 'EURO STOXX 50'],
|
|
3089
|
+
['^FCHI', 'CAC 40'],
|
|
3090
|
+
['^GDAXI', 'DAX'],
|
|
3091
|
+
['^N225', 'Nikkei 225'],
|
|
3092
|
+
['000001.SS', 'Shanghai Comp.'],
|
|
3093
|
+
];
|
|
3094
|
+
|
|
3095
|
+
if (indicator === 'all' || indicator === 'indices' || indicator === 'index' || indicator === 'equity') {
|
|
3096
|
+
const idxResults = await Promise.all(indexTickers.map(async ([sym, name]) => {
|
|
3097
|
+
try {
|
|
3098
|
+
const url = `https://query1.finance.yahoo.com/v8/finance/chart/${encodeURIComponent(sym)}?interval=1d&range=5d`;
|
|
3099
|
+
const res = await fetch(url, { headers: { 'User-Agent': 'Mozilla/5.0 (compatible; NHA/1.0)' } });
|
|
3100
|
+
const d = await res.json();
|
|
3101
|
+
const meta = d?.chart?.result?.[0]?.meta;
|
|
3102
|
+
if (!meta?.regularMarketPrice) return ` ${name.padEnd(20)} N/A`;
|
|
3103
|
+
const prev = meta.previousClose || meta.chartPreviousClose;
|
|
3104
|
+
const chg = prev ? ((meta.regularMarketPrice - prev) / prev * 100).toFixed(2) : null;
|
|
3105
|
+
const chgStr = chg != null ? ` (${parseFloat(chg) >= 0 ? '+' : ''}${chg}%)` : '';
|
|
3106
|
+
return ` ${name.padEnd(20)} ${meta.regularMarketPrice.toFixed(2)}${chgStr}`;
|
|
3107
|
+
} catch { return ` ${name.padEnd(20)} N/A`; }
|
|
3108
|
+
}));
|
|
3109
|
+
results.push('── Global Equity Indices ──');
|
|
3110
|
+
results.push(...idxResults);
|
|
3111
|
+
results.push('');
|
|
3112
|
+
}
|
|
3113
|
+
|
|
3114
|
+
// ── 4. FRED macro indicators (requires free key) ──
|
|
3115
|
+
if (fredKey && (indicator === 'all' || indicator === 'macro' || indicator === 'fred')) {
|
|
3116
|
+
const fredSeries = [
|
|
3117
|
+
['FEDFUNDS', 'Fed Funds Rate'],
|
|
3118
|
+
['CPIAUCSL', 'CPI (YoY)'],
|
|
3119
|
+
['UNRATE', 'Unemployment Rate'],
|
|
3120
|
+
['GDP', 'US GDP (Annualized)'],
|
|
3121
|
+
['T10YIE', '10Y Breakeven Inflation'],
|
|
3122
|
+
['IORB', 'Interest on Reserve Balances'],
|
|
3123
|
+
];
|
|
3124
|
+
const fredResults = await Promise.all(fredSeries.map(async ([id, name]) => {
|
|
3125
|
+
try {
|
|
3126
|
+
const url = `https://api.stlouisfed.org/fred/series/observations?series_id=${id}&api_key=${fredKey}&file_type=json&sort_order=desc&limit=2`;
|
|
3127
|
+
const res = await fetch(url);
|
|
3128
|
+
const d = await res.json();
|
|
3129
|
+
const obs = d?.observations?.filter(o => o.value !== '.') || [];
|
|
3130
|
+
const latest = obs[0];
|
|
3131
|
+
if (!latest) return ` ${name.padEnd(30)} N/A`;
|
|
3132
|
+
return ` ${name.padEnd(30)} ${latest.value} (${latest.date})`;
|
|
3133
|
+
} catch { return ` ${name.padEnd(30)} N/A`; }
|
|
3134
|
+
}));
|
|
3135
|
+
results.push('── FRED Macro Indicators ──');
|
|
3136
|
+
results.push(...fredResults);
|
|
3137
|
+
results.push('');
|
|
3138
|
+
} else if (!fredKey && (indicator === 'macro' || indicator === 'fred')) {
|
|
3139
|
+
results.push('── FRED Macro Indicators ──');
|
|
3140
|
+
results.push(' FRED API key not configured. Set config.fredApiKey or FRED_API_KEY env var.');
|
|
3141
|
+
results.push(' Free key at: https://fred.stlouisfed.org/docs/api/api_key.html');
|
|
3142
|
+
results.push('');
|
|
3143
|
+
}
|
|
3144
|
+
|
|
3145
|
+
if (results.length === 0) return `macro_data: unknown indicator "${params.indicator}". Use: all | yield | commodities | indices | macro`;
|
|
3146
|
+
return [`Macro Overview — ${new Date().toUTCString()}`, '', ...results].join('\n');
|
|
3147
|
+
}
|
|
3148
|
+
|
|
3149
|
+
case 'crypto_data': {
|
|
3150
|
+
const coin = (params.coin || 'bitcoin').toLowerCase().replace(/\s+/g, '-');
|
|
3151
|
+
const vsCurrency = (params.vs_currency || 'usd').toLowerCase();
|
|
3152
|
+
|
|
3153
|
+
try {
|
|
3154
|
+
// CoinGecko free tier — no API key needed (60 req/min)
|
|
3155
|
+
const [marketRes, globalRes] = await Promise.all([
|
|
3156
|
+
fetch(`https://api.coingecko.com/api/v3/coins/${encodeURIComponent(coin)}?localization=false&tickers=true&market_data=true&community_data=false&developer_data=false&sparkline=true`, {
|
|
3157
|
+
headers: { 'Accept': 'application/json' },
|
|
3158
|
+
}),
|
|
3159
|
+
fetch('https://api.coingecko.com/api/v3/global', { headers: { 'Accept': 'application/json' } }),
|
|
3160
|
+
]);
|
|
3161
|
+
|
|
3162
|
+
if (!marketRes.ok) {
|
|
3163
|
+
// Try searching by symbol
|
|
3164
|
+
const searchRes = await fetch(`https://api.coingecko.com/api/v3/search?query=${encodeURIComponent(coin)}`, { headers: { 'Accept': 'application/json' } });
|
|
3165
|
+
const searchData = await searchRes.json();
|
|
3166
|
+
const found = searchData?.coins?.[0];
|
|
3167
|
+
if (found) return `crypto_data: "${coin}" not found. Did you mean "${found.id}" (${found.symbol.toUpperCase()})? Retry with that id.`;
|
|
3168
|
+
return `crypto_data: "${coin}" not found on CoinGecko.`;
|
|
3169
|
+
}
|
|
3170
|
+
|
|
3171
|
+
const d = await marketRes.json();
|
|
3172
|
+
const gl = marketRes.ok && globalRes.ok ? (await globalRes.json())?.data : null;
|
|
3173
|
+
const md = d.market_data;
|
|
3174
|
+
const p = md?.current_price?.[vsCurrency];
|
|
3175
|
+
const mc = md?.market_cap?.[vsCurrency];
|
|
3176
|
+
const vol = md?.total_volume?.[vsCurrency];
|
|
3177
|
+
const h24 = md?.high_24h?.[vsCurrency];
|
|
3178
|
+
const l24 = md?.low_24h?.[vsCurrency];
|
|
3179
|
+
const chg1h = md?.price_change_percentage_1h_in_currency?.[vsCurrency];
|
|
3180
|
+
const chg24 = md?.price_change_percentage_24h_in_currency?.[vsCurrency];
|
|
3181
|
+
const chg7d = md?.price_change_percentage_7d_in_currency?.[vsCurrency];
|
|
3182
|
+
const chg30d = md?.price_change_percentage_30d_in_currency?.[vsCurrency];
|
|
3183
|
+
const chgYtd = md?.price_change_percentage_1y_in_currency?.[vsCurrency];
|
|
3184
|
+
const ath = md?.ath?.[vsCurrency];
|
|
3185
|
+
const athDate = md?.ath_date?.[vsCurrency]?.slice(0, 10);
|
|
3186
|
+
const athPct = md?.ath_change_percentage?.[vsCurrency];
|
|
3187
|
+
const supply = md?.circulating_supply;
|
|
3188
|
+
const maxSupply = md?.max_supply;
|
|
3189
|
+
const mcRank = d.market_cap_rank;
|
|
3190
|
+
|
|
3191
|
+
const fmtP = (n) => n != null ? n.toLocaleString('en-US', { maximumFractionDigits: 6 }) : 'N/A';
|
|
3192
|
+
const fmtPct = (n) => n != null ? `${n >= 0 ? '+' : ''}${n.toFixed(2)}%` : 'N/A';
|
|
3193
|
+
const fmtLg = (n) => {
|
|
3194
|
+
if (n == null) return 'N/A';
|
|
3195
|
+
if (n >= 1e12) return (n/1e12).toFixed(2) + 'T';
|
|
3196
|
+
if (n >= 1e9) return (n/1e9).toFixed(2) + 'B';
|
|
3197
|
+
if (n >= 1e6) return (n/1e6).toFixed(2) + 'M';
|
|
3198
|
+
return n.toFixed(0);
|
|
3199
|
+
};
|
|
3200
|
+
|
|
3201
|
+
// Sparkline momentum signal
|
|
3202
|
+
const sparkline = md?.sparkline_in_7d?.price || [];
|
|
3203
|
+
const sparkStart = sparkline[0];
|
|
3204
|
+
const sparkEnd = sparkline[sparkline.length - 1];
|
|
3205
|
+
const sparkMomentum = sparkStart && sparkEnd ? ((sparkEnd - sparkStart) / sparkStart * 100).toFixed(2) : null;
|
|
3206
|
+
|
|
3207
|
+
// Supply inflation
|
|
3208
|
+
const supplyPct = supply && maxSupply ? ((supply / maxSupply) * 100).toFixed(1) + '%' : 'uncapped';
|
|
3209
|
+
|
|
3210
|
+
// Fear/Greed proxy via ATH distance
|
|
3211
|
+
const fearGreedy = athPct != null ? (athPct > -20 ? 'GREED ZONE' : athPct > -60 ? 'NEUTRAL' : 'FEAR ZONE') : 'N/A';
|
|
3212
|
+
|
|
3213
|
+
const lines = [
|
|
3214
|
+
`${d.name} (${d.symbol?.toUpperCase()}) — CoinGecko | Rank #${mcRank || 'N/A'}`,
|
|
3215
|
+
`Category: ${(d.categories || []).slice(0, 3).join(', ') || 'N/A'}`,
|
|
3216
|
+
'',
|
|
3217
|
+
`── Price & Market Data ──`,
|
|
3218
|
+
`Price: ${fmtP(p)} ${vsCurrency.toUpperCase()}`,
|
|
3219
|
+
`24h Range: ${fmtP(l24)} – ${fmtP(h24)}`,
|
|
3220
|
+
`Market Cap: ${fmtLg(mc)} ${vsCurrency.toUpperCase()}`,
|
|
3221
|
+
`24h Volume: ${fmtLg(vol)} ${vsCurrency.toUpperCase()}`,
|
|
3222
|
+
`Vol/MC: ${mc && vol ? (vol / mc * 100).toFixed(2) + '%' : 'N/A'}`,
|
|
3223
|
+
'',
|
|
3224
|
+
`── Performance ──`,
|
|
3225
|
+
`1h: ${fmtPct(chg1h)}`,
|
|
3226
|
+
`24h: ${fmtPct(chg24)}`,
|
|
3227
|
+
`7d: ${fmtPct(chg7d)}`,
|
|
3228
|
+
`30d: ${fmtPct(chg30d)}`,
|
|
3229
|
+
`1y: ${fmtPct(chgYtd)}`,
|
|
3230
|
+
`7d Sparkline Momentum: ${sparkMomentum != null ? fmtPct(parseFloat(sparkMomentum)) : 'N/A'}`,
|
|
3231
|
+
'',
|
|
3232
|
+
`── On-Chain / Supply ──`,
|
|
3233
|
+
`ATH: ${fmtP(ath)} ${vsCurrency.toUpperCase()} (${athDate}) — ${fmtPct(athPct)} from ATH`,
|
|
3234
|
+
`Circulating: ${fmtLg(supply)} ${d.symbol?.toUpperCase() || ''}`,
|
|
3235
|
+
`Max Supply: ${maxSupply ? fmtLg(maxSupply) : 'None (uncapped)'}`,
|
|
3236
|
+
`Supply %: ${supplyPct}`,
|
|
3237
|
+
`ATH Distance Zone: ${fearGreedy}`,
|
|
3238
|
+
];
|
|
3239
|
+
|
|
3240
|
+
// Global market context
|
|
3241
|
+
if (gl) {
|
|
3242
|
+
const glMcPct = gl.market_cap_percentage;
|
|
3243
|
+
const btcDom = glMcPct?.btc ? glMcPct.btc.toFixed(1) + '%' : 'N/A';
|
|
3244
|
+
const ethDom = glMcPct?.eth ? glMcPct.eth.toFixed(1) + '%' : 'N/A';
|
|
3245
|
+
const totalMc = gl.total_market_cap?.[vsCurrency];
|
|
3246
|
+
lines.push('');
|
|
3247
|
+
lines.push('── Crypto Market Context ──');
|
|
3248
|
+
lines.push(`Total Crypto Market Cap: ${fmtLg(totalMc)} ${vsCurrency.toUpperCase()}`);
|
|
3249
|
+
lines.push(`BTC Dominance: ${btcDom} | ETH Dominance: ${ethDom}`);
|
|
3250
|
+
lines.push(`Active Coins: ${(gl.active_cryptocurrencies || 0).toLocaleString()}`);
|
|
3251
|
+
}
|
|
3252
|
+
|
|
3253
|
+
return lines.join('\n');
|
|
3254
|
+
} catch (e) {
|
|
3255
|
+
return `crypto_data error: ${e.message}`;
|
|
3256
|
+
}
|
|
3257
|
+
}
|
|
3258
|
+
|
|
3259
|
+
case 'market_news': {
|
|
3260
|
+
const query = params.query || params.ticker || '';
|
|
3261
|
+
const ticker = params.ticker || '';
|
|
3262
|
+
const limit = Math.min(params.limit || 10, 20);
|
|
3263
|
+
|
|
3264
|
+
try {
|
|
3265
|
+
let articles = [];
|
|
3266
|
+
|
|
3267
|
+
// Source 1: Yahoo Finance news for a specific ticker
|
|
3268
|
+
if (ticker) {
|
|
3269
|
+
const url = `https://query1.finance.yahoo.com/v1/finance/search?q=${encodeURIComponent(ticker)}&newsCount=${limit}"esCount=0&enableFuzzyQuery=false`;
|
|
3270
|
+
const res = await fetch(url, { headers: { 'User-Agent': 'Mozilla/5.0 (compatible; NHA/1.0)', 'Accept': 'application/json' } });
|
|
3271
|
+
if (res.ok) {
|
|
3272
|
+
const d = await res.json();
|
|
3273
|
+
const news = d?.news || [];
|
|
3274
|
+
articles = news.map(n => ({
|
|
3275
|
+
title: n.title,
|
|
3276
|
+
source: n.publisher,
|
|
3277
|
+
published: n.providerPublishTime ? new Date(n.providerPublishTime * 1000).toISOString().slice(0, 16).replace('T', ' ') : 'N/A',
|
|
3278
|
+
url: n.link,
|
|
3279
|
+
}));
|
|
3280
|
+
}
|
|
3281
|
+
}
|
|
3282
|
+
|
|
3283
|
+
// Source 2: Yahoo Finance search for general query
|
|
3284
|
+
if (articles.length === 0 && query) {
|
|
3285
|
+
const url = `https://query1.finance.yahoo.com/v1/finance/search?q=${encodeURIComponent(query)}&newsCount=${limit}"esCount=0`;
|
|
3286
|
+
const res = await fetch(url, { headers: { 'User-Agent': 'Mozilla/5.0 (compatible; NHA/1.0)', 'Accept': 'application/json' } });
|
|
3287
|
+
if (res.ok) {
|
|
3288
|
+
const d = await res.json();
|
|
3289
|
+
articles = (d?.news || []).map(n => ({
|
|
3290
|
+
title: n.title,
|
|
3291
|
+
source: n.publisher,
|
|
3292
|
+
published: n.providerPublishTime ? new Date(n.providerPublishTime * 1000).toISOString().slice(0, 16).replace('T', ' ') : 'N/A',
|
|
3293
|
+
url: n.link,
|
|
3294
|
+
}));
|
|
3295
|
+
}
|
|
3296
|
+
}
|
|
3297
|
+
|
|
3298
|
+
// Source 3: Fallback to web_search via fetch for financial news
|
|
3299
|
+
if (articles.length === 0) {
|
|
3300
|
+
const wt = await import('./web-tools.mjs');
|
|
3301
|
+
const searchQ = query || ticker ? `${query || ticker} stock market news today` : 'financial markets news today';
|
|
3302
|
+
const sr = await wt.webSearch(searchQ);
|
|
3303
|
+
if (sr.results?.length > 0) {
|
|
3304
|
+
articles = sr.results.slice(0, limit).map(r => ({
|
|
3305
|
+
title: r.title,
|
|
3306
|
+
source: r.url ? new URL(r.url).hostname.replace('www.', '') : 'Web',
|
|
3307
|
+
published: 'recent',
|
|
3308
|
+
url: r.url,
|
|
3309
|
+
snippet: r.snippet,
|
|
3310
|
+
}));
|
|
3311
|
+
}
|
|
3312
|
+
}
|
|
3313
|
+
|
|
3314
|
+
if (articles.length === 0) return `market_news: no news found for "${query || ticker}"`;
|
|
3315
|
+
|
|
3316
|
+
const label = ticker ? `${ticker} News` : query ? `News: "${query}"` : 'Financial News';
|
|
3317
|
+
const header = `${label} — ${new Date().toUTCString()} (${articles.length} articles)`;
|
|
3318
|
+
const body = articles.map((a, i) =>
|
|
3319
|
+
`${i + 1}. ${a.title}\n Source: ${a.source} | ${a.published}\n ${a.url || ''}${a.snippet ? '\n ' + a.snippet.slice(0, 200) : ''}`
|
|
3320
|
+
).join('\n\n');
|
|
3321
|
+
|
|
3322
|
+
return `${header}\n\n${body}`;
|
|
3323
|
+
} catch (e) {
|
|
3324
|
+
return `market_news error: ${e.message}`;
|
|
3325
|
+
}
|
|
3326
|
+
}
|
|
3327
|
+
|
|
2628
3328
|
default:
|
|
2629
3329
|
return `Unknown action: ${action}`;
|
|
2630
3330
|
}
|