market-feed 0.4.0 → 0.5.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/CHANGELOG.md CHANGED
@@ -1,5 +1,128 @@
1
1
  # market-feed Changelog
2
2
 
3
+ ## 0.5.0 — 2026-03-11
4
+
5
+ ### New modules
6
+
7
+ **`market-feed/backtest`** — Pure-function backtesting engine over `HistoricalBar[]`.
8
+
9
+ ```ts
10
+ import { backtest } from "market-feed/backtest";
11
+ import type { EntrySignal, ExitSignal } from "market-feed/backtest";
12
+
13
+ const entry: EntrySignal = (bars, i) => i > 0 && bars[i]!.close > bars[i - 1]!.close;
14
+ const exit: ExitSignal = (bars, i) => i > 0 && bars[i]!.close < bars[i - 1]!.close;
15
+
16
+ const result = backtest("AAPL", bars, entry, exit, { initialCapital: 10_000 });
17
+ console.log(`Total return: ${(result.totalReturn * 100).toFixed(2)}%`);
18
+ console.log(`Sharpe ratio: ${result.sharpeRatio.toFixed(2)}`);
19
+ console.log(`Max drawdown: ${(result.maxDrawdown * 100).toFixed(2)}%`);
20
+ ```
21
+
22
+ | Field | Description |
23
+ |-------|-------------|
24
+ | `totalReturn` | Fraction, e.g. 0.25 = 25% |
25
+ | `annualizedReturn` | CAGR as a fraction |
26
+ | `sharpeRatio` | Annualised Sharpe (risk-free rate = 0) |
27
+ | `maxDrawdown` | Peak-to-trough as a positive fraction |
28
+ | `winRate` | Fraction of profitable trades |
29
+ | `profitFactor` | Gross profit / gross loss (`Infinity` when no losses) |
30
+ | `totalTrades` | Number of completed round-trip trades |
31
+ | `trades` | Full `BacktestTrade[]` ledger |
32
+
33
+ **`market-feed/alerts`** — Poll a feed and yield `AlertEvent` when conditions are met.
34
+
35
+ ```ts
36
+ import { watchAlerts } from "market-feed/alerts";
37
+ import { MarketFeed } from "market-feed";
38
+
39
+ const feed = new MarketFeed();
40
+ const controller = new AbortController();
41
+
42
+ for await (const event of watchAlerts(feed, [
43
+ { symbol: "AAPL", condition: { type: "price_above", threshold: 200 }, once: true },
44
+ { symbol: "TSLA", condition: { type: "change_pct_below", threshold: -5 }, debounceMs: 60_000 },
45
+ ], { signal: controller.signal })) {
46
+ console.log(`${event.alert.symbol} triggered: $${event.quote.price}`);
47
+ }
48
+ ```
49
+
50
+ | Condition type | Description |
51
+ |----------------|-------------|
52
+ | `price_above` | `quote.price > threshold` |
53
+ | `price_below` | `quote.price < threshold` |
54
+ | `change_pct_above` | Daily `%` change exceeds threshold |
55
+ | `change_pct_below` | Daily `%` change falls below threshold |
56
+ | `volume_above` | `quote.volume > threshold` |
57
+
58
+ `AlertConfig` options: `once` (fire at most once), `debounceMs` (suppress re-fires within window).
59
+
60
+ ### New data: earnings, dividends, splits
61
+
62
+ Three new methods on `MarketFeed` (and on individual providers):
63
+
64
+ ```ts
65
+ const feed = new MarketFeed([new PolygonProvider({ apiKey: "..." })]);
66
+
67
+ const earnings = await feed.earnings("AAPL", { limit: 8 });
68
+ const dividends = await feed.dividends("AAPL");
69
+ const splits = await feed.splits("AAPL");
70
+ ```
71
+
72
+ #### Provider support
73
+
74
+ | Method | `YahooProvider` | `PolygonProvider` | `FinnhubProvider` | `AlphaVantageProvider` |
75
+ |--------|-----------------|-------------------|-------------------|------------------------|
76
+ | `earnings` | ✓ quoteSummary `earningsHistory` | — | ✓ `/stock/earnings` | — |
77
+ | `dividends` | ✓ chart `events=div` | ✓ `/v3/reference/dividends` | — | — |
78
+ | `splits` | ✓ chart `events=split` | ✓ `/v3/reference/splits` | — | — |
79
+
80
+ #### `EarningsEvent`
81
+
82
+ ```ts
83
+ interface EarningsEvent {
84
+ symbol: string; date: Date; period?: string;
85
+ epsActual?: number; epsEstimate?: number; epsSurprisePct?: number;
86
+ revenueActual?: number; revenueEstimate?: number;
87
+ provider: string; raw?: unknown;
88
+ }
89
+ ```
90
+
91
+ #### `DividendEvent`
92
+
93
+ ```ts
94
+ interface DividendEvent {
95
+ symbol: string; exDate: Date; payDate?: Date; declaredDate?: Date;
96
+ amount: number; currency: string;
97
+ frequency?: "annual" | "semi-annual" | "quarterly" | "monthly" | "irregular";
98
+ provider: string; raw?: unknown;
99
+ }
100
+ ```
101
+
102
+ #### `SplitEvent`
103
+
104
+ ```ts
105
+ interface SplitEvent {
106
+ symbol: string; date: Date;
107
+ ratio: number; // 4-for-1 forward split → 4; 1-for-10 reverse → 0.1
108
+ description?: string;
109
+ provider: string; raw?: unknown;
110
+ }
111
+ ```
112
+
113
+ ### Other changes
114
+
115
+ - `CacheMethod` extended with `"earnings" | "dividends" | "splits"` (TTLs: earnings 1 h, dividends/splits 24 h)
116
+ - All new types exported from main `market-feed` entry point
117
+ - 51 new unit tests (392 total across 21 test files)
118
+ - 9 tsup library entry points + 1 CLI binary: `index`, `calendar`, `stream`, `consensus`, `indicators`, `portfolio`, `ws`, `backtest`, `alerts`, `cli`
119
+
120
+ ### Breaking changes
121
+
122
+ None. All v0.4.0 imports continue to work unchanged.
123
+
124
+ ---
125
+
3
126
  ## 0.4.0 — 2026-03-11
4
127
 
5
128
  ### New module
package/README.md CHANGED
@@ -59,6 +59,9 @@ One interface. Four providers. Zero API key required for Yahoo Finance.
59
59
  - **Price consensus** — query all providers in parallel, get a weighted mean with confidence score
60
60
  - **Technical indicators** — SMA, EMA, RSI, MACD, Bollinger Bands, ATR, VWAP, Stochastic — pure functions, zero deps
61
61
  - **Portfolio tracking** — live P&L, unrealised gains, day change across all positions
62
+ - **Backtesting** — pure-function engine: total return, CAGR, Sharpe ratio, max drawdown, win rate, profit factor
63
+ - **Price alerts** — async generator that fires `AlertEvent` on price/volume/change conditions, with debounce
64
+ - **Earnings, dividends, splits** — structured historical corporate action data from Yahoo, Polygon, and Finnhub
62
65
  - **CLI** — `npx market-feed quote AAPL` — no install required
63
66
  - **Crypto & Forex** — `isCrypto()` / `isForex()` helpers, CRYPTO calendar exchange (always open)
64
67
 
@@ -66,7 +69,7 @@ One interface. Four providers. Zero API key required for Yahoo Finance.
66
69
 
67
70
  ## Subpath modules
68
71
 
69
- `market-feed` ships six optional subpath modules alongside the core client.
72
+ `market-feed` ships eight optional subpath modules alongside the core client.
70
73
 
71
74
  ### `market-feed/ws`
72
75
 
@@ -404,6 +407,117 @@ portfolio.snapshot(feed) // Promise<PortfolioSnapshot>
404
407
 
405
408
  ---
406
409
 
410
+ ### `market-feed/backtest`
411
+
412
+ Pure-function backtesting engine over `HistoricalBar[]`. No network, no side effects.
413
+
414
+ ```ts
415
+ import { backtest } from "market-feed/backtest";
416
+ import type { EntrySignal, ExitSignal } from "market-feed/backtest";
417
+ import { MarketFeed } from "market-feed";
418
+
419
+ const feed = new MarketFeed();
420
+ const bars = await feed.historical("AAPL", { period1: "2020-01-01", interval: "1d" });
421
+
422
+ // Buy when today's close > yesterday's close (momentum)
423
+ const entry: EntrySignal = (bars, i) => i > 0 && bars[i]!.close > bars[i - 1]!.close;
424
+
425
+ // Sell when today's close < yesterday's close
426
+ const exit: ExitSignal = (bars, i, _entryPrice) => i > 0 && bars[i]!.close < bars[i - 1]!.close;
427
+
428
+ const result = backtest("AAPL", bars, entry, exit, {
429
+ initialCapital: 10_000,
430
+ quantity: 10,
431
+ commission: 1,
432
+ });
433
+
434
+ console.log(`Total return: ${(result.totalReturn * 100).toFixed(2)}%`);
435
+ console.log(`Annualised CAGR: ${(result.annualizedReturn * 100).toFixed(2)}%`);
436
+ console.log(`Sharpe ratio: ${result.sharpeRatio.toFixed(2)}`);
437
+ console.log(`Max drawdown: ${(result.maxDrawdown * 100).toFixed(2)}%`);
438
+ console.log(`Win rate: ${(result.winRate * 100).toFixed(1)}%`);
439
+ console.log(`Trades: ${result.totalTrades}`);
440
+ ```
441
+
442
+ Signals fire at bar[i].close. Any open position is closed at the last bar. At most one position is held at a time.
443
+
444
+ #### `BacktestOptions`
445
+
446
+ | Option | Type | Default | Description |
447
+ |--------|------|---------|-------------|
448
+ | `initialCapital` | `number` | `100_000` | Starting capital |
449
+ | `quantity` | `number` | `1` | Shares per trade |
450
+ | `commission` | `number` | `0` | One-way commission per trade |
451
+
452
+ #### `BacktestResult`
453
+
454
+ | Field | Description |
455
+ |-------|-------------|
456
+ | `totalReturn` | Fraction — e.g. `0.25` = 25% |
457
+ | `annualizedReturn` | CAGR as a fraction |
458
+ | `sharpeRatio` | Annualised Sharpe (risk-free rate = 0) |
459
+ | `maxDrawdown` | Positive fraction — peak-to-trough |
460
+ | `winRate` | Fraction of profitable trades |
461
+ | `profitFactor` | Gross profit / gross loss (`Infinity` = no losses) |
462
+ | `totalTrades` | Completed round-trip trades |
463
+ | `trades` | `BacktestTrade[]` ledger |
464
+
465
+ ---
466
+
467
+ ### `market-feed/alerts`
468
+
469
+ Poll a quote feed and yield `AlertEvent` whenever a configured condition is met.
470
+
471
+ ```ts
472
+ import { watchAlerts } from "market-feed/alerts";
473
+ import type { AlertConfig } from "market-feed/alerts";
474
+ import { MarketFeed } from "market-feed";
475
+
476
+ const feed = new MarketFeed();
477
+ const controller = new AbortController();
478
+
479
+ const alerts: AlertConfig[] = [
480
+ // Fire once when AAPL crosses $200
481
+ { symbol: "AAPL", condition: { type: "price_above", threshold: 200 }, once: true },
482
+ // Alert on TSLA intraday crash; debounce 5 min to avoid spam
483
+ { symbol: "TSLA", condition: { type: "change_pct_below", threshold: -5 }, debounceMs: 300_000 },
484
+ // Unusual volume spike on MSFT
485
+ { symbol: "MSFT", condition: { type: "volume_above", threshold: 100_000_000 } },
486
+ ];
487
+
488
+ for await (const event of watchAlerts(feed, alerts, {
489
+ intervalMs: 5_000,
490
+ signal: controller.signal,
491
+ })) {
492
+ console.log(
493
+ `[${event.triggeredAt.toISOString()}] ${event.alert.symbol} triggered: $${event.quote.price}`,
494
+ );
495
+ }
496
+ ```
497
+
498
+ #### Alert conditions
499
+
500
+ | `type` | Fires when |
501
+ |--------|-----------|
502
+ | `price_above` | `quote.price > threshold` |
503
+ | `price_below` | `quote.price < threshold` |
504
+ | `change_pct_above` | `quote.changePercent > threshold` |
505
+ | `change_pct_below` | `quote.changePercent < threshold` |
506
+ | `volume_above` | `quote.volume > threshold` |
507
+
508
+ #### `AlertConfig`
509
+
510
+ | Field | Type | Description |
511
+ |-------|------|-------------|
512
+ | `symbol` | `string` | Ticker to watch |
513
+ | `condition` | `AlertCondition` | Trigger condition |
514
+ | `once` | `boolean?` | Remove after first fire. Default: `false` |
515
+ | `debounceMs` | `number?` | Suppress re-fires within this window. Default: `0` |
516
+
517
+ When all `once` alerts have fired, the generator terminates automatically. Permanent alerts (`once: false`) run until `signal.abort()` is called. Transient fetch errors are silently retried.
518
+
519
+ ---
520
+
407
521
  ### CLI (`npx market-feed`)
408
522
 
409
523
  A zero-config CLI powered by Yahoo Finance (no API key needed). Add keys to unlock more providers.
@@ -612,6 +726,15 @@ feed.news(symbol: string, options?: NewsOptions): Promise<NewsItem[]>
612
726
  // Market status
613
727
  feed.marketStatus(market?: string): Promise<MarketStatus>
614
728
 
729
+ // Earnings history (EPS actuals vs. estimates)
730
+ feed.earnings(symbol: string, options?: EarningsOptions): Promise<EarningsEvent[]>
731
+
732
+ // Cash dividend history
733
+ feed.dividends(symbol: string, options?: DividendOptions): Promise<DividendEvent[]>
734
+
735
+ // Stock split history
736
+ feed.splits(symbol: string, options?: SplitOptions): Promise<SplitEvent[]>
737
+
615
738
  // Cache management
616
739
  feed.clearCache(): Promise<void>
617
740
  feed.invalidate(key: string): Promise<void>
@@ -0,0 +1,70 @@
1
+ 'use strict';
2
+
3
+ // market-feed — Unified financial market data client
4
+ // https://github.com/piyushgupta344/market-feed
5
+
6
+ // src/alerts/index.ts
7
+ function conditionMet(condition, quote) {
8
+ switch (condition.type) {
9
+ case "price_above":
10
+ return quote.price > condition.threshold;
11
+ case "price_below":
12
+ return quote.price < condition.threshold;
13
+ case "change_pct_above":
14
+ return quote.changePercent > condition.threshold;
15
+ case "change_pct_below":
16
+ return quote.changePercent < condition.threshold;
17
+ case "volume_above":
18
+ return (quote.volume ?? 0) > condition.threshold;
19
+ }
20
+ }
21
+ async function* watchAlerts(feed, alerts, options) {
22
+ if (alerts.length === 0) return;
23
+ const intervalMs = options?.intervalMs ?? 5e3;
24
+ const signal = options?.signal;
25
+ const lastFired = /* @__PURE__ */ new Map();
26
+ const active = new Set(alerts);
27
+ while (active.size > 0) {
28
+ if (signal?.aborted) return;
29
+ const symbols = [...new Set([...active].map((a) => a.symbol))];
30
+ let quotes;
31
+ try {
32
+ quotes = await feed.quote(symbols);
33
+ } catch {
34
+ await sleep(intervalMs, signal);
35
+ continue;
36
+ }
37
+ if (signal?.aborted) return;
38
+ const quoteMap = new Map(quotes.map((q) => [q.symbol.toUpperCase(), q]));
39
+ const now = Date.now();
40
+ for (const alert of [...active]) {
41
+ const quote = quoteMap.get(alert.symbol.toUpperCase());
42
+ if (!quote) continue;
43
+ if (!conditionMet(alert.condition, quote)) continue;
44
+ const last = lastFired.get(alert);
45
+ if (last !== void 0 && alert.debounceMs && now - last < alert.debounceMs) continue;
46
+ lastFired.set(alert, now);
47
+ yield { type: "triggered", alert, quote, triggeredAt: new Date(now) };
48
+ if (alert.once) active.delete(alert);
49
+ }
50
+ if (active.size === 0) break;
51
+ await sleep(intervalMs, signal);
52
+ }
53
+ }
54
+ function sleep(ms, signal) {
55
+ return new Promise((resolve) => {
56
+ if (signal?.aborted) {
57
+ resolve();
58
+ return;
59
+ }
60
+ const timer = setTimeout(resolve, ms);
61
+ signal?.addEventListener("abort", () => {
62
+ clearTimeout(timer);
63
+ resolve();
64
+ }, { once: true });
65
+ });
66
+ }
67
+
68
+ exports.watchAlerts = watchAlerts;
69
+ //# sourceMappingURL=alerts.cjs.map
70
+ //# sourceMappingURL=alerts.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/alerts/index.ts"],"names":[],"mappings":"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","file":"alerts.cjs","sourcesContent":["import type { Quote } from \"../types/quote.js\";\nimport type { AlertCondition, AlertConfig, AlertEvent, AlertsOptions } from \"./types.js\";\n\nexport type { AlertCondition, AlertConfig, AlertEvent, AlertsOptions } from \"./types.js\";\n\ninterface QuoteFetcher {\n quote(symbols: string[]): Promise<Quote[]>;\n}\n\nfunction conditionMet(condition: AlertCondition, quote: Quote): boolean {\n switch (condition.type) {\n case \"price_above\":\n return quote.price > condition.threshold;\n case \"price_below\":\n return quote.price < condition.threshold;\n case \"change_pct_above\":\n return quote.changePercent > condition.threshold;\n case \"change_pct_below\":\n return quote.changePercent < condition.threshold;\n case \"volume_above\":\n return (quote.volume ?? 0) > condition.threshold;\n }\n}\n\n/**\n * Poll a quote feed and yield `AlertEvent` whenever a configured condition is met.\n *\n * - Resolves when all `once` alerts have fired, or when `options.signal` is aborted.\n * - Permanent alerts (once = false) run until the AbortSignal fires.\n * - Debounce suppresses re-fires within `debounceMs` milliseconds.\n *\n * @example\n * ```ts\n * import { watchAlerts } from \"market-feed/alerts\";\n * import { MarketFeed } from \"market-feed\";\n *\n * const feed = new MarketFeed();\n * const controller = new AbortController();\n *\n * for await (const event of watchAlerts(feed, [\n * { symbol: \"AAPL\", condition: { type: \"price_above\", threshold: 200 }, once: true },\n * ], { signal: controller.signal })) {\n * console.log(`AAPL crossed $200: $${event.quote.price}`);\n * }\n * ```\n */\nexport async function* watchAlerts(\n feed: QuoteFetcher,\n alerts: AlertConfig[],\n options?: AlertsOptions,\n): AsyncGenerator<AlertEvent> {\n if (alerts.length === 0) return;\n\n const intervalMs = options?.intervalMs ?? 5_000;\n const signal = options?.signal;\n\n const lastFired = new Map<AlertConfig, number>();\n const active = new Set(alerts);\n\n while (active.size > 0) {\n if (signal?.aborted) return;\n\n const symbols = [...new Set([...active].map((a) => a.symbol))];\n\n let quotes: Quote[];\n try {\n quotes = await feed.quote(symbols);\n } catch {\n await sleep(intervalMs, signal);\n continue;\n }\n\n if (signal?.aborted) return;\n\n const quoteMap = new Map(quotes.map((q) => [q.symbol.toUpperCase(), q]));\n const now = Date.now();\n\n for (const alert of [...active]) {\n const quote = quoteMap.get(alert.symbol.toUpperCase());\n if (!quote) continue;\n\n if (!conditionMet(alert.condition, quote)) continue;\n\n // Debounce check\n const last = lastFired.get(alert);\n if (last !== undefined && alert.debounceMs && now - last < alert.debounceMs) continue;\n\n lastFired.set(alert, now);\n yield { type: \"triggered\", alert, quote, triggeredAt: new Date(now) };\n\n if (alert.once) active.delete(alert);\n }\n\n if (active.size === 0) break;\n await sleep(intervalMs, signal);\n }\n}\n\nfunction sleep(ms: number, signal?: AbortSignal): Promise<void> {\n return new Promise((resolve) => {\n if (signal?.aborted) {\n resolve();\n return;\n }\n const timer = setTimeout(resolve, ms);\n signal?.addEventListener(\"abort\", () => {\n clearTimeout(timer);\n resolve();\n }, { once: true });\n });\n}\n"]}
@@ -0,0 +1,70 @@
1
+ import { a as Quote } from './quote-Cfh_7Cgg.cjs';
2
+
3
+ type AlertCondition = {
4
+ type: "price_above";
5
+ threshold: number;
6
+ } | {
7
+ type: "price_below";
8
+ threshold: number;
9
+ } | {
10
+ type: "change_pct_above";
11
+ threshold: number;
12
+ } | {
13
+ type: "change_pct_below";
14
+ threshold: number;
15
+ } | {
16
+ type: "volume_above";
17
+ threshold: number;
18
+ };
19
+ interface AlertConfig {
20
+ symbol: string;
21
+ condition: AlertCondition;
22
+ /** Emit this alert at most once, then stop watching it. Defaults to false. */
23
+ once?: boolean;
24
+ /**
25
+ * Suppress re-fires within this many milliseconds after the last trigger.
26
+ * Defaults to 0 (no debounce).
27
+ */
28
+ debounceMs?: number;
29
+ }
30
+ interface AlertEvent {
31
+ type: "triggered";
32
+ alert: AlertConfig;
33
+ quote: Quote;
34
+ triggeredAt: Date;
35
+ }
36
+ interface AlertsOptions {
37
+ /** Poll interval in milliseconds. Defaults to 5 000. */
38
+ intervalMs?: number;
39
+ /** AbortSignal to stop the generator. */
40
+ signal?: AbortSignal;
41
+ }
42
+
43
+ interface QuoteFetcher {
44
+ quote(symbols: string[]): Promise<Quote[]>;
45
+ }
46
+ /**
47
+ * Poll a quote feed and yield `AlertEvent` whenever a configured condition is met.
48
+ *
49
+ * - Resolves when all `once` alerts have fired, or when `options.signal` is aborted.
50
+ * - Permanent alerts (once = false) run until the AbortSignal fires.
51
+ * - Debounce suppresses re-fires within `debounceMs` milliseconds.
52
+ *
53
+ * @example
54
+ * ```ts
55
+ * import { watchAlerts } from "market-feed/alerts";
56
+ * import { MarketFeed } from "market-feed";
57
+ *
58
+ * const feed = new MarketFeed();
59
+ * const controller = new AbortController();
60
+ *
61
+ * for await (const event of watchAlerts(feed, [
62
+ * { symbol: "AAPL", condition: { type: "price_above", threshold: 200 }, once: true },
63
+ * ], { signal: controller.signal })) {
64
+ * console.log(`AAPL crossed $200: $${event.quote.price}`);
65
+ * }
66
+ * ```
67
+ */
68
+ declare function watchAlerts(feed: QuoteFetcher, alerts: AlertConfig[], options?: AlertsOptions): AsyncGenerator<AlertEvent>;
69
+
70
+ export { type AlertCondition, type AlertConfig, type AlertEvent, type AlertsOptions, watchAlerts };
@@ -0,0 +1,70 @@
1
+ import { a as Quote } from './quote-Cfh_7Cgg.js';
2
+
3
+ type AlertCondition = {
4
+ type: "price_above";
5
+ threshold: number;
6
+ } | {
7
+ type: "price_below";
8
+ threshold: number;
9
+ } | {
10
+ type: "change_pct_above";
11
+ threshold: number;
12
+ } | {
13
+ type: "change_pct_below";
14
+ threshold: number;
15
+ } | {
16
+ type: "volume_above";
17
+ threshold: number;
18
+ };
19
+ interface AlertConfig {
20
+ symbol: string;
21
+ condition: AlertCondition;
22
+ /** Emit this alert at most once, then stop watching it. Defaults to false. */
23
+ once?: boolean;
24
+ /**
25
+ * Suppress re-fires within this many milliseconds after the last trigger.
26
+ * Defaults to 0 (no debounce).
27
+ */
28
+ debounceMs?: number;
29
+ }
30
+ interface AlertEvent {
31
+ type: "triggered";
32
+ alert: AlertConfig;
33
+ quote: Quote;
34
+ triggeredAt: Date;
35
+ }
36
+ interface AlertsOptions {
37
+ /** Poll interval in milliseconds. Defaults to 5 000. */
38
+ intervalMs?: number;
39
+ /** AbortSignal to stop the generator. */
40
+ signal?: AbortSignal;
41
+ }
42
+
43
+ interface QuoteFetcher {
44
+ quote(symbols: string[]): Promise<Quote[]>;
45
+ }
46
+ /**
47
+ * Poll a quote feed and yield `AlertEvent` whenever a configured condition is met.
48
+ *
49
+ * - Resolves when all `once` alerts have fired, or when `options.signal` is aborted.
50
+ * - Permanent alerts (once = false) run until the AbortSignal fires.
51
+ * - Debounce suppresses re-fires within `debounceMs` milliseconds.
52
+ *
53
+ * @example
54
+ * ```ts
55
+ * import { watchAlerts } from "market-feed/alerts";
56
+ * import { MarketFeed } from "market-feed";
57
+ *
58
+ * const feed = new MarketFeed();
59
+ * const controller = new AbortController();
60
+ *
61
+ * for await (const event of watchAlerts(feed, [
62
+ * { symbol: "AAPL", condition: { type: "price_above", threshold: 200 }, once: true },
63
+ * ], { signal: controller.signal })) {
64
+ * console.log(`AAPL crossed $200: $${event.quote.price}`);
65
+ * }
66
+ * ```
67
+ */
68
+ declare function watchAlerts(feed: QuoteFetcher, alerts: AlertConfig[], options?: AlertsOptions): AsyncGenerator<AlertEvent>;
69
+
70
+ export { type AlertCondition, type AlertConfig, type AlertEvent, type AlertsOptions, watchAlerts };
package/dist/alerts.js ADDED
@@ -0,0 +1,68 @@
1
+ // market-feed — Unified financial market data client
2
+ // https://github.com/piyushgupta344/market-feed
3
+
4
+ // src/alerts/index.ts
5
+ function conditionMet(condition, quote) {
6
+ switch (condition.type) {
7
+ case "price_above":
8
+ return quote.price > condition.threshold;
9
+ case "price_below":
10
+ return quote.price < condition.threshold;
11
+ case "change_pct_above":
12
+ return quote.changePercent > condition.threshold;
13
+ case "change_pct_below":
14
+ return quote.changePercent < condition.threshold;
15
+ case "volume_above":
16
+ return (quote.volume ?? 0) > condition.threshold;
17
+ }
18
+ }
19
+ async function* watchAlerts(feed, alerts, options) {
20
+ if (alerts.length === 0) return;
21
+ const intervalMs = options?.intervalMs ?? 5e3;
22
+ const signal = options?.signal;
23
+ const lastFired = /* @__PURE__ */ new Map();
24
+ const active = new Set(alerts);
25
+ while (active.size > 0) {
26
+ if (signal?.aborted) return;
27
+ const symbols = [...new Set([...active].map((a) => a.symbol))];
28
+ let quotes;
29
+ try {
30
+ quotes = await feed.quote(symbols);
31
+ } catch {
32
+ await sleep(intervalMs, signal);
33
+ continue;
34
+ }
35
+ if (signal?.aborted) return;
36
+ const quoteMap = new Map(quotes.map((q) => [q.symbol.toUpperCase(), q]));
37
+ const now = Date.now();
38
+ for (const alert of [...active]) {
39
+ const quote = quoteMap.get(alert.symbol.toUpperCase());
40
+ if (!quote) continue;
41
+ if (!conditionMet(alert.condition, quote)) continue;
42
+ const last = lastFired.get(alert);
43
+ if (last !== void 0 && alert.debounceMs && now - last < alert.debounceMs) continue;
44
+ lastFired.set(alert, now);
45
+ yield { type: "triggered", alert, quote, triggeredAt: new Date(now) };
46
+ if (alert.once) active.delete(alert);
47
+ }
48
+ if (active.size === 0) break;
49
+ await sleep(intervalMs, signal);
50
+ }
51
+ }
52
+ function sleep(ms, signal) {
53
+ return new Promise((resolve) => {
54
+ if (signal?.aborted) {
55
+ resolve();
56
+ return;
57
+ }
58
+ const timer = setTimeout(resolve, ms);
59
+ signal?.addEventListener("abort", () => {
60
+ clearTimeout(timer);
61
+ resolve();
62
+ }, { once: true });
63
+ });
64
+ }
65
+
66
+ export { watchAlerts };
67
+ //# sourceMappingURL=alerts.js.map
68
+ //# sourceMappingURL=alerts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/alerts/index.ts"],"names":[],"mappings":"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","file":"alerts.js","sourcesContent":["import type { Quote } from \"../types/quote.js\";\nimport type { AlertCondition, AlertConfig, AlertEvent, AlertsOptions } from \"./types.js\";\n\nexport type { AlertCondition, AlertConfig, AlertEvent, AlertsOptions } from \"./types.js\";\n\ninterface QuoteFetcher {\n quote(symbols: string[]): Promise<Quote[]>;\n}\n\nfunction conditionMet(condition: AlertCondition, quote: Quote): boolean {\n switch (condition.type) {\n case \"price_above\":\n return quote.price > condition.threshold;\n case \"price_below\":\n return quote.price < condition.threshold;\n case \"change_pct_above\":\n return quote.changePercent > condition.threshold;\n case \"change_pct_below\":\n return quote.changePercent < condition.threshold;\n case \"volume_above\":\n return (quote.volume ?? 0) > condition.threshold;\n }\n}\n\n/**\n * Poll a quote feed and yield `AlertEvent` whenever a configured condition is met.\n *\n * - Resolves when all `once` alerts have fired, or when `options.signal` is aborted.\n * - Permanent alerts (once = false) run until the AbortSignal fires.\n * - Debounce suppresses re-fires within `debounceMs` milliseconds.\n *\n * @example\n * ```ts\n * import { watchAlerts } from \"market-feed/alerts\";\n * import { MarketFeed } from \"market-feed\";\n *\n * const feed = new MarketFeed();\n * const controller = new AbortController();\n *\n * for await (const event of watchAlerts(feed, [\n * { symbol: \"AAPL\", condition: { type: \"price_above\", threshold: 200 }, once: true },\n * ], { signal: controller.signal })) {\n * console.log(`AAPL crossed $200: $${event.quote.price}`);\n * }\n * ```\n */\nexport async function* watchAlerts(\n feed: QuoteFetcher,\n alerts: AlertConfig[],\n options?: AlertsOptions,\n): AsyncGenerator<AlertEvent> {\n if (alerts.length === 0) return;\n\n const intervalMs = options?.intervalMs ?? 5_000;\n const signal = options?.signal;\n\n const lastFired = new Map<AlertConfig, number>();\n const active = new Set(alerts);\n\n while (active.size > 0) {\n if (signal?.aborted) return;\n\n const symbols = [...new Set([...active].map((a) => a.symbol))];\n\n let quotes: Quote[];\n try {\n quotes = await feed.quote(symbols);\n } catch {\n await sleep(intervalMs, signal);\n continue;\n }\n\n if (signal?.aborted) return;\n\n const quoteMap = new Map(quotes.map((q) => [q.symbol.toUpperCase(), q]));\n const now = Date.now();\n\n for (const alert of [...active]) {\n const quote = quoteMap.get(alert.symbol.toUpperCase());\n if (!quote) continue;\n\n if (!conditionMet(alert.condition, quote)) continue;\n\n // Debounce check\n const last = lastFired.get(alert);\n if (last !== undefined && alert.debounceMs && now - last < alert.debounceMs) continue;\n\n lastFired.set(alert, now);\n yield { type: \"triggered\", alert, quote, triggeredAt: new Date(now) };\n\n if (alert.once) active.delete(alert);\n }\n\n if (active.size === 0) break;\n await sleep(intervalMs, signal);\n }\n}\n\nfunction sleep(ms: number, signal?: AbortSignal): Promise<void> {\n return new Promise((resolve) => {\n if (signal?.aborted) {\n resolve();\n return;\n }\n const timer = setTimeout(resolve, ms);\n signal?.addEventListener(\"abort\", () => {\n clearTimeout(timer);\n resolve();\n }, { once: true });\n });\n}\n"]}