market-data-analyzer 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of market-data-analyzer might be problematic. Click here for more details.
- package/README.md +159 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +267 -0
- package/dist/index.js.map +1 -0
- package/dist/tools/analyze_portfolio.d.ts +14 -0
- package/dist/tools/analyze_portfolio.d.ts.map +1 -0
- package/dist/tools/analyze_portfolio.js +155 -0
- package/dist/tools/analyze_portfolio.js.map +1 -0
- package/dist/tools/analyze_stock.d.ts +8 -0
- package/dist/tools/analyze_stock.d.ts.map +1 -0
- package/dist/tools/analyze_stock.js +211 -0
- package/dist/tools/analyze_stock.js.map +1 -0
- package/dist/tools/compare_assets.d.ts +8 -0
- package/dist/tools/compare_assets.d.ts.map +1 -0
- package/dist/tools/compare_assets.js +138 -0
- package/dist/tools/compare_assets.js.map +1 -0
- package/dist/tools/crypto_analysis.d.ts +8 -0
- package/dist/tools/crypto_analysis.d.ts.map +1 -0
- package/dist/tools/crypto_analysis.js +192 -0
- package/dist/tools/crypto_analysis.js.map +1 -0
- package/dist/tools/market_overview.d.ts +8 -0
- package/dist/tools/market_overview.d.ts.map +1 -0
- package/dist/tools/market_overview.js +223 -0
- package/dist/tools/market_overview.js.map +1 -0
- package/dist/tools/screen_stocks.d.ts +19 -0
- package/dist/tools/screen_stocks.d.ts.map +1 -0
- package/dist/tools/screen_stocks.js +122 -0
- package/dist/tools/screen_stocks.js.map +1 -0
- package/dist/types.d.ts +158 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/api.d.ts +37 -0
- package/dist/utils/api.d.ts.map +1 -0
- package/dist/utils/api.js +228 -0
- package/dist/utils/api.js.map +1 -0
- package/dist/utils/cache.d.ts +20 -0
- package/dist/utils/cache.d.ts.map +1 -0
- package/dist/utils/cache.js +52 -0
- package/dist/utils/cache.js.map +1 -0
- package/dist/utils/math.d.ts +30 -0
- package/dist/utils/math.d.ts.map +1 -0
- package/dist/utils/math.js +300 -0
- package/dist/utils/math.js.map +1 -0
- package/package.json +30 -0
- package/src/index.ts +329 -0
- package/src/tools/analyze_portfolio.ts +204 -0
- package/src/tools/analyze_stock.ts +204 -0
- package/src/tools/compare_assets.ts +181 -0
- package/src/tools/crypto_analysis.ts +221 -0
- package/src/tools/market_overview.ts +236 -0
- package/src/tools/screen_stocks.ts +154 -0
- package/src/types.ts +175 -0
- package/src/utils/api.ts +262 -0
- package/src/utils/cache.ts +65 -0
- package/src/utils/math.ts +332 -0
- package/tsconfig.json +19 -0
package/src/utils/api.ts
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API helpers for Yahoo Finance and CoinGecko free endpoints.
|
|
3
|
+
*
|
|
4
|
+
* Yahoo Finance: Uses the v8 finance quote/chart endpoints (publicly accessible).
|
|
5
|
+
* CoinGecko: Uses the free /api/v3 endpoints (no API key needed, rate-limited).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { priceCache } from "./cache.js";
|
|
9
|
+
import type {
|
|
10
|
+
YahooQuote,
|
|
11
|
+
YahooChartPoint,
|
|
12
|
+
CoinGeckoMarketData,
|
|
13
|
+
CoinGeckoGlobalData,
|
|
14
|
+
} from "../types.js";
|
|
15
|
+
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// User-Agent to avoid blocks
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
const UA =
|
|
21
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
|
|
22
|
+
|
|
23
|
+
async function fetchJSON<T>(url: string): Promise<T> {
|
|
24
|
+
const res = await fetch(url, {
|
|
25
|
+
headers: {
|
|
26
|
+
"User-Agent": UA,
|
|
27
|
+
Accept: "application/json",
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
if (!res.ok) {
|
|
31
|
+
throw new Error(`HTTP ${res.status} from ${url}: ${res.statusText}`);
|
|
32
|
+
}
|
|
33
|
+
return res.json() as Promise<T>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// Yahoo Finance helpers
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Fetch a quote for one or more symbols via Yahoo Finance v6 quote endpoint.
|
|
42
|
+
*/
|
|
43
|
+
export async function yahooQuote(symbols: string[]): Promise<YahooQuote[]> {
|
|
44
|
+
const cacheKey = `yq:${symbols.sort().join(",")}`;
|
|
45
|
+
const cached = priceCache.get<YahooQuote[]>(cacheKey);
|
|
46
|
+
if (cached) return cached;
|
|
47
|
+
|
|
48
|
+
const joined = symbols.join(",");
|
|
49
|
+
const url = `https://query1.finance.yahoo.com/v6/finance/quote?symbols=${encodeURIComponent(joined)}`;
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
const data = await fetchJSON<any>(url);
|
|
53
|
+
const quotes: YahooQuote[] =
|
|
54
|
+
data?.finance?.result?.[0]?.quotes ??
|
|
55
|
+
data?.quoteResponse?.result ??
|
|
56
|
+
[];
|
|
57
|
+
priceCache.set(cacheKey, quotes);
|
|
58
|
+
return quotes;
|
|
59
|
+
} catch {
|
|
60
|
+
// Fallback: try v7 endpoint
|
|
61
|
+
const url7 = `https://query1.finance.yahoo.com/v7/finance/quote?symbols=${encodeURIComponent(joined)}`;
|
|
62
|
+
try {
|
|
63
|
+
const data = await fetchJSON<any>(url7);
|
|
64
|
+
const quotes: YahooQuote[] = data?.quoteResponse?.result ?? [];
|
|
65
|
+
priceCache.set(cacheKey, quotes);
|
|
66
|
+
return quotes;
|
|
67
|
+
} catch {
|
|
68
|
+
// Final fallback: try v8 quote summary for single symbols
|
|
69
|
+
if (symbols.length === 1) {
|
|
70
|
+
return [await yahooQuoteSummary(symbols[0])];
|
|
71
|
+
}
|
|
72
|
+
throw new Error(`Failed to fetch Yahoo Finance quotes for: ${joined}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Fallback: v10 quoteSummary for a single symbol.
|
|
79
|
+
*/
|
|
80
|
+
async function yahooQuoteSummary(symbol: string): Promise<YahooQuote> {
|
|
81
|
+
const url = `https://query1.finance.yahoo.com/v10/finance/quoteSummary/${encodeURIComponent(symbol)}?modules=price,summaryDetail,defaultKeyStatistics`;
|
|
82
|
+
const data = await fetchJSON<any>(url);
|
|
83
|
+
const result = data?.quoteSummary?.result?.[0];
|
|
84
|
+
const price = result?.price ?? {};
|
|
85
|
+
const summary = result?.summaryDetail ?? {};
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
symbol: price.symbol ?? symbol,
|
|
89
|
+
shortName: price.shortName,
|
|
90
|
+
longName: price.longName,
|
|
91
|
+
regularMarketPrice: price.regularMarketPrice?.raw,
|
|
92
|
+
regularMarketChange: price.regularMarketChange?.raw,
|
|
93
|
+
regularMarketChangePercent: price.regularMarketChangePercent?.raw
|
|
94
|
+
? price.regularMarketChangePercent.raw * 100
|
|
95
|
+
: undefined,
|
|
96
|
+
regularMarketVolume: price.regularMarketVolume?.raw,
|
|
97
|
+
regularMarketDayHigh: price.regularMarketDayHigh?.raw,
|
|
98
|
+
regularMarketDayLow: price.regularMarketDayLow?.raw,
|
|
99
|
+
regularMarketOpen: price.regularMarketOpen?.raw,
|
|
100
|
+
regularMarketPreviousClose: price.regularMarketPreviousClose?.raw,
|
|
101
|
+
fiftyTwoWeekHigh: summary.fiftyTwoWeekHigh?.raw,
|
|
102
|
+
fiftyTwoWeekLow: summary.fiftyTwoWeekLow?.raw,
|
|
103
|
+
fiftyDayAverage: summary.fiftyDayAverage?.raw,
|
|
104
|
+
twoHundredDayAverage: summary.twoHundredDayAverage?.raw,
|
|
105
|
+
marketCap: price.marketCap?.raw,
|
|
106
|
+
trailingPE: summary.trailingPE?.raw,
|
|
107
|
+
forwardPE: summary.forwardPE?.raw,
|
|
108
|
+
trailingAnnualDividendYield: summary.trailingAnnualDividendYield?.raw,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Fetch historical chart data from Yahoo Finance.
|
|
114
|
+
* @param symbol Ticker symbol
|
|
115
|
+
* @param range "1mo", "3mo", "6mo", "1y", "5y"
|
|
116
|
+
* @param interval "1d", "1wk", "1mo"
|
|
117
|
+
*/
|
|
118
|
+
export async function yahooChart(
|
|
119
|
+
symbol: string,
|
|
120
|
+
range: string = "6mo",
|
|
121
|
+
interval: string = "1d",
|
|
122
|
+
): Promise<YahooChartPoint[]> {
|
|
123
|
+
const cacheKey = `yc:${symbol}:${range}:${interval}`;
|
|
124
|
+
const cached = priceCache.get<YahooChartPoint[]>(cacheKey);
|
|
125
|
+
if (cached) return cached;
|
|
126
|
+
|
|
127
|
+
const url = `https://query1.finance.yahoo.com/v8/finance/chart/${encodeURIComponent(symbol)}?range=${range}&interval=${interval}&includePrePost=false`;
|
|
128
|
+
const data = await fetchJSON<any>(url);
|
|
129
|
+
const result = data?.chart?.result?.[0];
|
|
130
|
+
if (!result) {
|
|
131
|
+
throw new Error(`No chart data returned for ${symbol}`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const timestamps: number[] = result.timestamp ?? [];
|
|
135
|
+
const ohlcv = result.indicators?.quote?.[0] ?? {};
|
|
136
|
+
const adjClose = result.indicators?.adjclose?.[0]?.adjclose;
|
|
137
|
+
|
|
138
|
+
const points: YahooChartPoint[] = [];
|
|
139
|
+
for (let i = 0; i < timestamps.length; i++) {
|
|
140
|
+
const close = adjClose?.[i] ?? ohlcv.close?.[i];
|
|
141
|
+
if (close == null || ohlcv.open?.[i] == null) continue;
|
|
142
|
+
points.push({
|
|
143
|
+
date: timestamps[i],
|
|
144
|
+
open: ohlcv.open[i],
|
|
145
|
+
high: ohlcv.high?.[i] ?? ohlcv.open[i],
|
|
146
|
+
low: ohlcv.low?.[i] ?? ohlcv.open[i],
|
|
147
|
+
close,
|
|
148
|
+
volume: ohlcv.volume?.[i] ?? 0,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
priceCache.set(cacheKey, points);
|
|
153
|
+
return points;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Screen stocks using Yahoo Finance's screener or quote list.
|
|
158
|
+
* Fetches quotes for a predefined set of popular tickers and filters them.
|
|
159
|
+
*/
|
|
160
|
+
export async function yahooScreener(
|
|
161
|
+
symbols: string[],
|
|
162
|
+
): Promise<YahooQuote[]> {
|
|
163
|
+
// Batch into groups of 20 to stay within URL limits
|
|
164
|
+
const results: YahooQuote[] = [];
|
|
165
|
+
for (let i = 0; i < symbols.length; i += 20) {
|
|
166
|
+
const batch = symbols.slice(i, i + 20);
|
|
167
|
+
const quotes = await yahooQuote(batch);
|
|
168
|
+
results.push(...quotes);
|
|
169
|
+
}
|
|
170
|
+
return results;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ---------------------------------------------------------------------------
|
|
174
|
+
// CoinGecko helpers
|
|
175
|
+
// ---------------------------------------------------------------------------
|
|
176
|
+
|
|
177
|
+
const CG_BASE = "https://api.coingecko.com/api/v3";
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Fetch market data for one or more coins by ID.
|
|
181
|
+
*/
|
|
182
|
+
export async function coingeckoMarkets(
|
|
183
|
+
ids: string[],
|
|
184
|
+
vsCurrency: string = "usd",
|
|
185
|
+
): Promise<CoinGeckoMarketData[]> {
|
|
186
|
+
const cacheKey = `cg:${ids.sort().join(",")}:${vsCurrency}`;
|
|
187
|
+
const cached = priceCache.get<CoinGeckoMarketData[]>(cacheKey);
|
|
188
|
+
if (cached) return cached;
|
|
189
|
+
|
|
190
|
+
const url = `${CG_BASE}/coins/markets?vs_currency=${vsCurrency}&ids=${ids.join(",")}&order=market_cap_desc&per_page=100&page=1&sparkline=false&price_change_percentage=7d,30d`;
|
|
191
|
+
const data = await fetchJSON<CoinGeckoMarketData[]>(url);
|
|
192
|
+
priceCache.set(cacheKey, data);
|
|
193
|
+
return data;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Fetch global crypto market data.
|
|
198
|
+
*/
|
|
199
|
+
export async function coingeckoGlobal(): Promise<CoinGeckoGlobalData> {
|
|
200
|
+
const cacheKey = "cg:global";
|
|
201
|
+
const cached = priceCache.get<CoinGeckoGlobalData>(cacheKey);
|
|
202
|
+
if (cached) return cached;
|
|
203
|
+
|
|
204
|
+
const data = await fetchJSON<{ data: CoinGeckoGlobalData }>(`${CG_BASE}/global`);
|
|
205
|
+
priceCache.set(cacheKey, data.data);
|
|
206
|
+
return data.data;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Map common symbols to CoinGecko IDs.
|
|
211
|
+
*/
|
|
212
|
+
export function symbolToCoinGeckoId(symbol: string): string {
|
|
213
|
+
const map: Record<string, string> = {
|
|
214
|
+
BTC: "bitcoin",
|
|
215
|
+
ETH: "ethereum",
|
|
216
|
+
SOL: "solana",
|
|
217
|
+
BNB: "binancecoin",
|
|
218
|
+
XRP: "ripple",
|
|
219
|
+
ADA: "cardano",
|
|
220
|
+
DOGE: "dogecoin",
|
|
221
|
+
DOT: "polkadot",
|
|
222
|
+
AVAX: "avalanche-2",
|
|
223
|
+
MATIC: "matic-network",
|
|
224
|
+
LINK: "chainlink",
|
|
225
|
+
UNI: "uniswap",
|
|
226
|
+
ATOM: "cosmos",
|
|
227
|
+
LTC: "litecoin",
|
|
228
|
+
NEAR: "near",
|
|
229
|
+
APT: "aptos",
|
|
230
|
+
ARB: "arbitrum",
|
|
231
|
+
OP: "optimism",
|
|
232
|
+
SUI: "sui",
|
|
233
|
+
TRX: "tron",
|
|
234
|
+
SHIB: "shiba-inu",
|
|
235
|
+
PEPE: "pepe",
|
|
236
|
+
};
|
|
237
|
+
const upper = symbol.toUpperCase().replace("-USD", "").replace("USD", "");
|
|
238
|
+
return map[upper] ?? symbol.toLowerCase();
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ---------------------------------------------------------------------------
|
|
242
|
+
// Popular stock symbols for screening
|
|
243
|
+
// ---------------------------------------------------------------------------
|
|
244
|
+
|
|
245
|
+
export const POPULAR_SYMBOLS = [
|
|
246
|
+
// Mega-cap tech
|
|
247
|
+
"AAPL", "MSFT", "NVDA", "GOOGL", "AMZN", "META", "TSLA", "AVGO", "TSM", "ORCL",
|
|
248
|
+
// More tech
|
|
249
|
+
"CRM", "AMD", "INTC", "ADBE", "NFLX", "CSCO", "QCOM", "TXN", "AMAT", "NOW",
|
|
250
|
+
// Financials
|
|
251
|
+
"JPM", "V", "MA", "BAC", "GS", "MS", "BRK-B", "AXP", "BLK", "SCHW",
|
|
252
|
+
// Healthcare
|
|
253
|
+
"UNH", "JNJ", "LLY", "PFE", "ABBV", "MRK", "TMO", "ABT", "DHR", "BMY",
|
|
254
|
+
// Consumer
|
|
255
|
+
"WMT", "PG", "KO", "PEP", "COST", "MCD", "NKE", "HD", "LOW", "SBUX",
|
|
256
|
+
// Energy
|
|
257
|
+
"XOM", "CVX", "COP", "SLB", "EOG",
|
|
258
|
+
// Industrials
|
|
259
|
+
"CAT", "GE", "UNP", "HON", "BA", "RTX", "DE", "LMT",
|
|
260
|
+
// Other
|
|
261
|
+
"DIS", "NEE", "AMT", "PLD", "LIN", "SPGI",
|
|
262
|
+
];
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory cache with configurable TTL.
|
|
3
|
+
* Default TTL is 5 minutes (300_000 ms) for price data.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
interface CacheEntry<T> {
|
|
7
|
+
value: T;
|
|
8
|
+
expiresAt: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class MemoryCache {
|
|
12
|
+
private store = new Map<string, CacheEntry<unknown>>();
|
|
13
|
+
private defaultTTL: number;
|
|
14
|
+
|
|
15
|
+
constructor(defaultTTLMs: number = 300_000) {
|
|
16
|
+
this.defaultTTL = defaultTTLMs;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
get<T>(key: string): T | undefined {
|
|
20
|
+
const entry = this.store.get(key);
|
|
21
|
+
if (!entry) return undefined;
|
|
22
|
+
if (Date.now() > entry.expiresAt) {
|
|
23
|
+
this.store.delete(key);
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
return entry.value as T;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
set<T>(key: string, value: T, ttlMs?: number): void {
|
|
30
|
+
this.store.set(key, {
|
|
31
|
+
value,
|
|
32
|
+
expiresAt: Date.now() + (ttlMs ?? this.defaultTTL),
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
has(key: string): boolean {
|
|
37
|
+
return this.get(key) !== undefined;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
delete(key: string): void {
|
|
41
|
+
this.store.delete(key);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
clear(): void {
|
|
45
|
+
this.store.clear();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Remove all expired entries. */
|
|
49
|
+
prune(): void {
|
|
50
|
+
const now = Date.now();
|
|
51
|
+
for (const [key, entry] of this.store) {
|
|
52
|
+
if (now > entry.expiresAt) {
|
|
53
|
+
this.store.delete(key);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
get size(): number {
|
|
59
|
+
this.prune();
|
|
60
|
+
return this.store.size;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Shared price cache instance -- 5 minute TTL */
|
|
65
|
+
export const priceCache = new MemoryCache(300_000);
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Financial math utilities -- technical indicators, risk metrics, etc.
|
|
3
|
+
* Pure functions with no external dependencies.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// Basic statistics
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
|
|
10
|
+
export function mean(values: number[]): number {
|
|
11
|
+
if (values.length === 0) return 0;
|
|
12
|
+
return values.reduce((s, v) => s + v, 0) / values.length;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function stdDev(values: number[]): number {
|
|
16
|
+
if (values.length < 2) return 0;
|
|
17
|
+
const m = mean(values);
|
|
18
|
+
const variance = values.reduce((s, v) => s + (v - m) ** 2, 0) / values.length;
|
|
19
|
+
return Math.sqrt(variance);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function percentile(sorted: number[], p: number): number {
|
|
23
|
+
if (sorted.length === 0) return 0;
|
|
24
|
+
const idx = (p / 100) * (sorted.length - 1);
|
|
25
|
+
const lower = Math.floor(idx);
|
|
26
|
+
const upper = Math.ceil(idx);
|
|
27
|
+
if (lower === upper) return sorted[lower];
|
|
28
|
+
const frac = idx - lower;
|
|
29
|
+
return sorted[lower] * (1 - frac) + sorted[upper] * frac;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function pricesToReturns(prices: number[]): number[] {
|
|
33
|
+
const returns: number[] = [];
|
|
34
|
+
for (let i = 1; i < prices.length; i++) {
|
|
35
|
+
if (prices[i - 1] !== 0) {
|
|
36
|
+
returns.push((prices[i] - prices[i - 1]) / prices[i - 1]);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return returns;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function correlation(a: number[], b: number[]): number {
|
|
43
|
+
const n = Math.min(a.length, b.length);
|
|
44
|
+
if (n < 2) return 0;
|
|
45
|
+
const ma = mean(a.slice(0, n));
|
|
46
|
+
const mb = mean(b.slice(0, n));
|
|
47
|
+
let sumAB = 0, sumA2 = 0, sumB2 = 0;
|
|
48
|
+
for (let i = 0; i < n; i++) {
|
|
49
|
+
const da = a[i] - ma;
|
|
50
|
+
const db = b[i] - mb;
|
|
51
|
+
sumAB += da * db;
|
|
52
|
+
sumA2 += da * da;
|
|
53
|
+
sumB2 += db * db;
|
|
54
|
+
}
|
|
55
|
+
const denom = Math.sqrt(sumA2 * sumB2);
|
|
56
|
+
return denom > 0 ? sumAB / denom : 0;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
// Moving averages
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
export function sma(values: number[], period: number): (number | null)[] {
|
|
64
|
+
const result: (number | null)[] = [];
|
|
65
|
+
for (let i = 0; i < values.length; i++) {
|
|
66
|
+
if (i < period - 1) {
|
|
67
|
+
result.push(null);
|
|
68
|
+
} else {
|
|
69
|
+
let sum = 0;
|
|
70
|
+
for (let j = i - period + 1; j <= i; j++) sum += values[j];
|
|
71
|
+
result.push(sum / period);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function ema(values: number[], period: number): (number | null)[] {
|
|
78
|
+
const result: (number | null)[] = [];
|
|
79
|
+
const multiplier = 2 / (period + 1);
|
|
80
|
+
let prev: number | null = null;
|
|
81
|
+
|
|
82
|
+
for (let i = 0; i < values.length; i++) {
|
|
83
|
+
if (i < period - 1) {
|
|
84
|
+
result.push(null);
|
|
85
|
+
} else if (prev === null) {
|
|
86
|
+
let sum = 0;
|
|
87
|
+
for (let j = i - period + 1; j <= i; j++) sum += values[j];
|
|
88
|
+
prev = sum / period;
|
|
89
|
+
result.push(prev);
|
|
90
|
+
} else {
|
|
91
|
+
prev = (values[i] - prev) * multiplier + prev;
|
|
92
|
+
result.push(prev);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return result;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Get the latest non-null value from an indicator series. */
|
|
99
|
+
export function lastValue(series: (number | null)[]): number | null {
|
|
100
|
+
for (let i = series.length - 1; i >= 0; i--) {
|
|
101
|
+
if (series[i] !== null) return series[i];
|
|
102
|
+
}
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
// RSI (Relative Strength Index)
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
|
|
110
|
+
export function rsi(closes: number[], period: number = 14): (number | null)[] {
|
|
111
|
+
if (closes.length < period + 1) return closes.map(() => null);
|
|
112
|
+
|
|
113
|
+
const result: (number | null)[] = [null];
|
|
114
|
+
const gains: number[] = [];
|
|
115
|
+
const losses: number[] = [];
|
|
116
|
+
|
|
117
|
+
for (let i = 1; i < closes.length; i++) {
|
|
118
|
+
const change = closes[i] - closes[i - 1];
|
|
119
|
+
gains.push(change > 0 ? change : 0);
|
|
120
|
+
losses.push(change < 0 ? -change : 0);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
let avgGain = 0;
|
|
124
|
+
let avgLoss = 0;
|
|
125
|
+
|
|
126
|
+
// Initial averages
|
|
127
|
+
for (let i = 0; i < period; i++) {
|
|
128
|
+
avgGain += gains[i];
|
|
129
|
+
avgLoss += losses[i];
|
|
130
|
+
}
|
|
131
|
+
avgGain /= period;
|
|
132
|
+
avgLoss /= period;
|
|
133
|
+
|
|
134
|
+
for (let i = 0; i < gains.length; i++) {
|
|
135
|
+
if (i < period - 1) {
|
|
136
|
+
result.push(null);
|
|
137
|
+
} else if (i === period - 1) {
|
|
138
|
+
if (avgLoss === 0) {
|
|
139
|
+
result.push(100);
|
|
140
|
+
} else {
|
|
141
|
+
result.push(100 - 100 / (1 + avgGain / avgLoss));
|
|
142
|
+
}
|
|
143
|
+
} else {
|
|
144
|
+
// Wilder smoothing
|
|
145
|
+
avgGain = (avgGain * (period - 1) + gains[i]) / period;
|
|
146
|
+
avgLoss = (avgLoss * (period - 1) + losses[i]) / period;
|
|
147
|
+
if (avgLoss === 0) {
|
|
148
|
+
result.push(100);
|
|
149
|
+
} else {
|
|
150
|
+
result.push(100 - 100 / (1 + avgGain / avgLoss));
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return result;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
// MACD
|
|
160
|
+
// ---------------------------------------------------------------------------
|
|
161
|
+
|
|
162
|
+
export interface MACDResult {
|
|
163
|
+
line: (number | null)[];
|
|
164
|
+
signal: (number | null)[];
|
|
165
|
+
histogram: (number | null)[];
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function macd(
|
|
169
|
+
closes: number[],
|
|
170
|
+
fastPeriod = 12,
|
|
171
|
+
slowPeriod = 26,
|
|
172
|
+
signalPeriod = 9,
|
|
173
|
+
): MACDResult {
|
|
174
|
+
const fastEma = ema(closes, fastPeriod);
|
|
175
|
+
const slowEma = ema(closes, slowPeriod);
|
|
176
|
+
|
|
177
|
+
const macdLine: (number | null)[] = [];
|
|
178
|
+
for (let i = 0; i < closes.length; i++) {
|
|
179
|
+
if (fastEma[i] !== null && slowEma[i] !== null) {
|
|
180
|
+
macdLine.push(fastEma[i]! - slowEma[i]!);
|
|
181
|
+
} else {
|
|
182
|
+
macdLine.push(null);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Signal line = EMA of MACD line
|
|
187
|
+
const nonNull: number[] = [];
|
|
188
|
+
const nonNullIdx: number[] = [];
|
|
189
|
+
for (let i = 0; i < macdLine.length; i++) {
|
|
190
|
+
if (macdLine[i] !== null) {
|
|
191
|
+
nonNull.push(macdLine[i]!);
|
|
192
|
+
nonNullIdx.push(i);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const sigEma = ema(nonNull, signalPeriod);
|
|
197
|
+
const signalLine: (number | null)[] = new Array(closes.length).fill(null);
|
|
198
|
+
for (let i = 0; i < nonNullIdx.length; i++) {
|
|
199
|
+
signalLine[nonNullIdx[i]] = sigEma[i];
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const histogram: (number | null)[] = [];
|
|
203
|
+
for (let i = 0; i < closes.length; i++) {
|
|
204
|
+
if (macdLine[i] !== null && signalLine[i] !== null) {
|
|
205
|
+
histogram.push(macdLine[i]! - signalLine[i]!);
|
|
206
|
+
} else {
|
|
207
|
+
histogram.push(null);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return { line: macdLine, signal: signalLine, histogram };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ---------------------------------------------------------------------------
|
|
215
|
+
// Support / Resistance (pivot-based)
|
|
216
|
+
// ---------------------------------------------------------------------------
|
|
217
|
+
|
|
218
|
+
export function findSupportResistance(
|
|
219
|
+
highs: number[],
|
|
220
|
+
lows: number[],
|
|
221
|
+
closes: number[],
|
|
222
|
+
lookback: number = 20,
|
|
223
|
+
): { support: number[]; resistance: number[] } {
|
|
224
|
+
const support: number[] = [];
|
|
225
|
+
const resistance: number[] = [];
|
|
226
|
+
|
|
227
|
+
if (closes.length < lookback) {
|
|
228
|
+
return { support: [], resistance: [] };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const currentPrice = closes[closes.length - 1];
|
|
232
|
+
|
|
233
|
+
// Find local minima (support) and maxima (resistance)
|
|
234
|
+
const windowSize = Math.max(5, Math.floor(lookback / 4));
|
|
235
|
+
for (let i = windowSize; i < closes.length - windowSize; i++) {
|
|
236
|
+
let isLocalMin = true;
|
|
237
|
+
let isLocalMax = true;
|
|
238
|
+
|
|
239
|
+
for (let j = i - windowSize; j <= i + windowSize; j++) {
|
|
240
|
+
if (j === i) continue;
|
|
241
|
+
if (lows[j] < lows[i]) isLocalMin = false;
|
|
242
|
+
if (highs[j] > highs[i]) isLocalMax = false;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (isLocalMin && lows[i] < currentPrice) {
|
|
246
|
+
support.push(Math.round(lows[i] * 100) / 100);
|
|
247
|
+
}
|
|
248
|
+
if (isLocalMax && highs[i] > currentPrice) {
|
|
249
|
+
resistance.push(Math.round(highs[i] * 100) / 100);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Deduplicate: merge levels within 1% of each other
|
|
254
|
+
const dedup = (levels: number[]): number[] => {
|
|
255
|
+
if (levels.length === 0) return [];
|
|
256
|
+
levels.sort((a, b) => a - b);
|
|
257
|
+
const merged: number[] = [levels[0]];
|
|
258
|
+
for (let i = 1; i < levels.length; i++) {
|
|
259
|
+
const last = merged[merged.length - 1];
|
|
260
|
+
if (Math.abs(levels[i] - last) / last < 0.01) {
|
|
261
|
+
merged[merged.length - 1] = (last + levels[i]) / 2;
|
|
262
|
+
} else {
|
|
263
|
+
merged.push(levels[i]);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return merged;
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
// Return closest 3 levels each
|
|
270
|
+
const dedupedSupport = dedup(support).sort((a, b) => b - a).slice(0, 3);
|
|
271
|
+
const dedupedResistance = dedup(resistance).sort((a, b) => a - b).slice(0, 3);
|
|
272
|
+
|
|
273
|
+
return { support: dedupedSupport, resistance: dedupedResistance };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// ---------------------------------------------------------------------------
|
|
277
|
+
// Max drawdown
|
|
278
|
+
// ---------------------------------------------------------------------------
|
|
279
|
+
|
|
280
|
+
export function maxDrawdown(prices: number[]): number {
|
|
281
|
+
if (prices.length < 2) return 0;
|
|
282
|
+
let peak = prices[0];
|
|
283
|
+
let worst = 0;
|
|
284
|
+
for (const p of prices) {
|
|
285
|
+
if (p > peak) peak = p;
|
|
286
|
+
const dd = (peak - p) / peak;
|
|
287
|
+
if (dd > worst) worst = dd;
|
|
288
|
+
}
|
|
289
|
+
return worst;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// ---------------------------------------------------------------------------
|
|
293
|
+
// Sharpe ratio approximation
|
|
294
|
+
// ---------------------------------------------------------------------------
|
|
295
|
+
|
|
296
|
+
const TRADING_DAYS = 252;
|
|
297
|
+
const DEFAULT_RISK_FREE_RATE = 0.045; // ~4.5% annualized
|
|
298
|
+
|
|
299
|
+
export function sharpeRatio(
|
|
300
|
+
returns: number[],
|
|
301
|
+
riskFreeRate: number = DEFAULT_RISK_FREE_RATE,
|
|
302
|
+
): number {
|
|
303
|
+
if (returns.length < 2) return 0;
|
|
304
|
+
const dailyRfr = riskFreeRate / TRADING_DAYS;
|
|
305
|
+
const avgReturn = mean(returns);
|
|
306
|
+
const vol = stdDev(returns);
|
|
307
|
+
if (vol === 0) return 0;
|
|
308
|
+
return ((avgReturn - dailyRfr) / vol) * Math.sqrt(TRADING_DAYS);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// ---------------------------------------------------------------------------
|
|
312
|
+
// Formatting helpers
|
|
313
|
+
// ---------------------------------------------------------------------------
|
|
314
|
+
|
|
315
|
+
export function formatCurrency(n: number): string {
|
|
316
|
+
if (Math.abs(n) >= 1e12) return `$${(n / 1e12).toFixed(2)}T`;
|
|
317
|
+
if (Math.abs(n) >= 1e9) return `$${(n / 1e9).toFixed(2)}B`;
|
|
318
|
+
if (Math.abs(n) >= 1e6) return `$${(n / 1e6).toFixed(2)}M`;
|
|
319
|
+
return `$${n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export function formatVolume(n: number): string {
|
|
323
|
+
if (n >= 1e9) return `${(n / 1e9).toFixed(1)}B`;
|
|
324
|
+
if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
|
|
325
|
+
if (n >= 1e3) return `${(n / 1e3).toFixed(1)}K`;
|
|
326
|
+
return n.toString();
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export function formatPct(n: number, decimals: number = 2): string {
|
|
330
|
+
const sign = n >= 0 ? "+" : "";
|
|
331
|
+
return `${sign}${(n * 100).toFixed(decimals)}%`;
|
|
332
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "Node16",
|
|
5
|
+
"moduleResolution": "Node16",
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"rootDir": "./src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"declaration": true,
|
|
14
|
+
"declarationMap": true,
|
|
15
|
+
"sourceMap": true
|
|
16
|
+
},
|
|
17
|
+
"include": ["src/**/*"],
|
|
18
|
+
"exclude": ["node_modules", "dist"]
|
|
19
|
+
}
|