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 +123 -0
- package/README.md +124 -1
- package/dist/alerts.cjs +70 -0
- package/dist/alerts.cjs.map +1 -0
- package/dist/alerts.d.cts +70 -0
- package/dist/alerts.d.ts +70 -0
- package/dist/alerts.js +68 -0
- package/dist/alerts.js.map +1 -0
- package/dist/backtest.cjs +110 -0
- package/dist/backtest.cjs.map +1 -0
- package/dist/backtest.d.cts +67 -0
- package/dist/backtest.d.ts +67 -0
- package/dist/backtest.js +108 -0
- package/dist/backtest.js.map +1 -0
- package/dist/cli.js +269 -1
- package/dist/cli.js.map +1 -1
- package/dist/{client-DMChcXq5.d.cts → client-B8eMDynL.d.cts} +5 -2
- package/dist/{client-CfZS7mw6.d.ts → client-DFXMg2UE.d.ts} +5 -2
- package/dist/consensus.d.cts +1 -1
- package/dist/consensus.d.ts +1 -1
- package/dist/index.cjs +269 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +11 -5
- package/dist/index.d.ts +11 -5
- package/dist/index.js +269 -1
- package/dist/index.js.map +1 -1
- package/dist/{provider-0hpSUPR2.d.cts → provider-C6qYyVGJ.d.cts} +75 -1
- package/dist/{provider-DK6G4Nmp.d.ts → provider-CC1CWzj-.d.ts} +75 -1
- package/dist/stream.d.cts +2 -2
- package/dist/stream.d.ts +2 -2
- package/dist/ws.d.cts +1 -1
- package/dist/ws.d.ts +1 -1
- package/package.json +11 -1
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
|
|
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>
|
package/dist/alerts.cjs
ADDED
|
@@ -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 };
|
package/dist/alerts.d.ts
ADDED
|
@@ -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"]}
|