market-feed 0.1.0 → 0.4.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 +213 -0
- package/README.md +393 -5
- package/dist/calendar.cjs +508 -0
- package/dist/calendar.cjs.map +1 -0
- package/dist/calendar.d.cts +42 -0
- package/dist/calendar.d.ts +42 -0
- package/dist/calendar.js +498 -0
- package/dist/calendar.js.map +1 -0
- package/dist/cli.js +1582 -0
- package/dist/cli.js.map +1 -0
- package/dist/client-CfZS7mw6.d.ts +107 -0
- package/dist/client-DMChcXq5.d.cts +107 -0
- package/dist/consensus.cjs +181 -0
- package/dist/consensus.cjs.map +1 -0
- package/dist/consensus.d.cts +112 -0
- package/dist/consensus.d.ts +112 -0
- package/dist/consensus.js +174 -0
- package/dist/consensus.js.map +1 -0
- package/dist/errors-CbrhDR4X.d.cts +39 -0
- package/dist/errors-CbrhDR4X.d.ts +39 -0
- package/dist/historical-BbCuwqyZ.d.cts +30 -0
- package/dist/historical-BbCuwqyZ.d.ts +30 -0
- package/dist/index.cjs +334 -60
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +70 -347
- package/dist/index.d.ts +70 -347
- package/dist/index.js +331 -61
- package/dist/index.js.map +1 -1
- package/dist/indicators.cjs +178 -0
- package/dist/indicators.cjs.map +1 -0
- package/dist/indicators.d.cts +98 -0
- package/dist/indicators.d.ts +98 -0
- package/dist/indicators.js +169 -0
- package/dist/indicators.js.map +1 -0
- package/dist/market-B9DVrpuM.d.cts +22 -0
- package/dist/market-B9DVrpuM.d.ts +22 -0
- package/dist/portfolio.cjs +119 -0
- package/dist/portfolio.cjs.map +1 -0
- package/dist/portfolio.d.cts +112 -0
- package/dist/portfolio.d.ts +112 -0
- package/dist/portfolio.js +117 -0
- package/dist/portfolio.js.map +1 -0
- package/dist/provider-0hpSUPR2.d.cts +118 -0
- package/dist/provider-DK6G4Nmp.d.ts +118 -0
- package/dist/quote-Cfh_7Cgg.d.cts +48 -0
- package/dist/quote-Cfh_7Cgg.d.ts +48 -0
- package/dist/stream.cjs +552 -0
- package/dist/stream.cjs.map +1 -0
- package/dist/stream.d.cts +113 -0
- package/dist/stream.d.ts +113 -0
- package/dist/stream.js +550 -0
- package/dist/stream.js.map +1 -0
- package/dist/types-DVfrmc4W.d.cts +27 -0
- package/dist/types-DVfrmc4W.d.ts +27 -0
- package/dist/ws.cjs +364 -0
- package/dist/ws.cjs.map +1 -0
- package/dist/ws.d.cts +111 -0
- package/dist/ws.d.ts +111 -0
- package/dist/ws.js +362 -0
- package/dist/ws.js.map +1 -0
- package/package.json +60 -20
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,218 @@
|
|
|
1
1
|
# market-feed Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.0 — 2026-03-11
|
|
4
|
+
|
|
5
|
+
### New module
|
|
6
|
+
|
|
7
|
+
**`market-feed/ws`** — True WebSocket streaming for tick-by-tick trade data.
|
|
8
|
+
|
|
9
|
+
Unlike `market-feed/stream` (which is HTTP polling), `market-feed/ws` opens a
|
|
10
|
+
persistent WebSocket connection and yields individual trade executions in real time.
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
import { connect } from "market-feed/ws";
|
|
14
|
+
import { FinnhubProvider } from "market-feed";
|
|
15
|
+
|
|
16
|
+
const provider = new FinnhubProvider({ apiKey: process.env.FINNHUB_KEY! });
|
|
17
|
+
const controller = new AbortController();
|
|
18
|
+
|
|
19
|
+
for await (const event of connect(provider, ["AAPL", "MSFT"], { signal: controller.signal })) {
|
|
20
|
+
switch (event.type) {
|
|
21
|
+
case "trade":
|
|
22
|
+
console.log(`${event.trade.symbol}: $${event.trade.price} × ${event.trade.size}`);
|
|
23
|
+
break;
|
|
24
|
+
case "connected":
|
|
25
|
+
console.log(`Connected to ${event.provider}`);
|
|
26
|
+
break;
|
|
27
|
+
case "disconnected":
|
|
28
|
+
console.log(`Disconnected (reconnecting: ${event.reconnecting})`);
|
|
29
|
+
break;
|
|
30
|
+
case "error":
|
|
31
|
+
if (!event.recoverable) throw event.error;
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
#### Provider support
|
|
38
|
+
|
|
39
|
+
| Provider | WebSocket | Notes |
|
|
40
|
+
|----------|-----------|-------|
|
|
41
|
+
| **PolygonProvider** | Native WS | `wss://socket.polygon.io/stocks` — auth via JSON handshake, subscribes to `T.*` trade channel |
|
|
42
|
+
| **FinnhubProvider** | Native WS | `wss://ws.finnhub.io?token=KEY` — per-symbol subscribe, batched trade messages |
|
|
43
|
+
| **YahooProvider** | Polling fallback | Polls `provider.quote()` every 5 s; emits `WsTrade` from quote data |
|
|
44
|
+
| **AlphaVantageProvider** | Polling fallback | Same as Yahoo |
|
|
45
|
+
|
|
46
|
+
The `connect()` function detects provider capability automatically — no configuration required.
|
|
47
|
+
|
|
48
|
+
#### `WsEvent` union
|
|
49
|
+
|
|
50
|
+
| `type` | Payload | When |
|
|
51
|
+
|--------|---------|------|
|
|
52
|
+
| `"connected"` | `provider: string` | WS opened (and after each reconnect) |
|
|
53
|
+
| `"trade"` | `trade: WsTrade` | Each trade tick |
|
|
54
|
+
| `"disconnected"` | `provider`, `reconnecting`, `attempt` | WS closed unexpectedly |
|
|
55
|
+
| `"error"` | `error`, `recoverable` | Protocol or network error |
|
|
56
|
+
|
|
57
|
+
#### `WsTrade`
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
interface WsTrade {
|
|
61
|
+
symbol: string;
|
|
62
|
+
price: number;
|
|
63
|
+
size: number; // shares / units
|
|
64
|
+
timestamp: Date;
|
|
65
|
+
conditions?: number[]; // provider-specific condition codes
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
#### `WsOptions`
|
|
70
|
+
|
|
71
|
+
| Option | Type | Default | Description |
|
|
72
|
+
|--------|------|---------|-------------|
|
|
73
|
+
| `wsImpl` | `typeof globalThis.WebSocket` | `globalThis.WebSocket` | Custom WS constructor for Node 18–20 |
|
|
74
|
+
| `maxReconnectAttempts` | `number` | `10` | Reconnect attempts before closing |
|
|
75
|
+
| `reconnectDelayMs` | `number` | `1000` | Base delay (doubles per attempt, max 30 s) |
|
|
76
|
+
| `signal` | `AbortSignal` | — | Stop the stream |
|
|
77
|
+
|
|
78
|
+
#### Node 18–20 compatibility
|
|
79
|
+
|
|
80
|
+
Node 21+ exposes `WebSocket` globally. For Node 18/20, install the `ws` package and inject it:
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
import WebSocket from "ws";
|
|
84
|
+
connect(provider, ["AAPL"], { wsImpl: WebSocket as unknown as typeof globalThis.WebSocket })
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Other changes
|
|
88
|
+
|
|
89
|
+
- `PolygonProvider` now exposes `get wsApiKey(): string` (used internally by `market-feed/ws`)
|
|
90
|
+
- `FinnhubProvider` now exposes `get wsApiKey(): string` (used internally by `market-feed/ws`)
|
|
91
|
+
- 27 new unit tests (341 total across 18 test files)
|
|
92
|
+
- 7 tsup library entry points + 1 CLI binary: `index`, `calendar`, `stream`, `consensus`, `indicators`, `portfolio`, `ws`, `cli`
|
|
93
|
+
|
|
94
|
+
### Breaking changes
|
|
95
|
+
|
|
96
|
+
None. All v0.3.0 imports continue to work unchanged.
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## 0.3.0 — 2026-03-11
|
|
101
|
+
|
|
102
|
+
### New provider
|
|
103
|
+
|
|
104
|
+
**`FinnhubProvider`** — Finnhub.io (free tier: real-time US stock data, 60 calls/minute).
|
|
105
|
+
- `quote`, `historical` (candles), `search`, `company`, `news`
|
|
106
|
+
- API key required — get one free at https://finnhub.io
|
|
107
|
+
- Uses `X-Finnhub-Token` header; rate-limited client-side at 60 req/min
|
|
108
|
+
- `historical` maps standard intervals to Finnhub resolutions (1m→"1", 1d→"D", 1wk→"W", 1mo→"M")
|
|
109
|
+
- `news` fetches articles from the last 30 days by default
|
|
110
|
+
|
|
111
|
+
### New modules
|
|
112
|
+
|
|
113
|
+
**`market-feed/indicators`** — Technical indicators as pure functions over `HistoricalBar[]`.
|
|
114
|
+
- `sma(bars, period)` — Simple Moving Average (O(1) sliding window)
|
|
115
|
+
- `ema(bars, period)` — Exponential Moving Average (k = 2/(period+1), SMA-seeded)
|
|
116
|
+
- `rsi(bars, period?)` — Relative Strength Index via Wilder's smoothing (default period: 14)
|
|
117
|
+
- `macd(bars, fast?, slow?, signal?)` — MACD line, signal line, histogram (default 12/26/9)
|
|
118
|
+
- `bollingerBands(bars, period?, stdDevMult?)` — upper/middle/lower bands (default 20/2)
|
|
119
|
+
- `atr(bars, period?)` — Average True Range via Wilder's smoothing (default period: 14)
|
|
120
|
+
- `vwap(bars)` — Volume-Weighted Average Price (cumulative from first bar)
|
|
121
|
+
- `stochastic(bars, kPeriod?, dPeriod?)` — %K and %D oscillator (default 14/3)
|
|
122
|
+
- Zero dependencies, no network, tree-shakeable per indicator
|
|
123
|
+
- All functions return typed result arrays (`IndicatorPoint[]`, `MACDPoint[]`, `BollingerPoint[]`, `StochasticPoint[]`)
|
|
124
|
+
|
|
125
|
+
**`market-feed/portfolio`** — Track positions and compute live P&L.
|
|
126
|
+
- `new Portfolio(positions?)` — construct with an array of `Position` objects
|
|
127
|
+
- `portfolio.add(position)` / `portfolio.remove(symbol)` — mutable, chainable
|
|
128
|
+
- `portfolio.snapshot(feed)` — fetches live quotes and returns `PortfolioSnapshot` with per-position and aggregate P&L
|
|
129
|
+
- `PositionSnapshot` includes: `marketValue`, `costBasis`, `unrealizedPnl`, `unrealizedPnlPct`, `dayChange`, `dayChangePct`, `quote`
|
|
130
|
+
- `PortfolioSnapshot` includes aggregate totals: `totalMarketValue`, `totalCostBasis`, `totalUnrealizedPnl`, `totalDayChange`
|
|
131
|
+
- Supports long and short positions (negative `quantity`)
|
|
132
|
+
- Uses `QuoteFetcher` duck-type interface — accepts any object with a `quote(symbols)` method, including `MarketFeed`
|
|
133
|
+
|
|
134
|
+
### CLI (`npx market-feed`)
|
|
135
|
+
|
|
136
|
+
A zero-install CLI that uses Yahoo Finance by default (no API key needed).
|
|
137
|
+
|
|
138
|
+
```
|
|
139
|
+
market-feed quote AAPL MSFT GOOGL
|
|
140
|
+
market-feed historical AAPL --interval 1wk --period1 2024-01-01
|
|
141
|
+
market-feed search "apple inc"
|
|
142
|
+
market-feed company AAPL
|
|
143
|
+
market-feed news AAPL --limit 5 --json
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Flags: `--av-key`, `--polygon-key`, `--finnhub-key`, `--json`, `--limit`, `--interval`, `--period1`, `--period2`
|
|
147
|
+
|
|
148
|
+
### Crypto / Forex support
|
|
149
|
+
|
|
150
|
+
- `isCrypto(symbol)` — detects `"BTC-USD"`, `"BTC/USD"`, `"X:BTCUSD"` using a known-crypto-bases set
|
|
151
|
+
- `isForex(symbol)` — detects `"EURUSD=X"`, `"C:EURUSD"`, `"OANDA:EUR_USD"`, `"EUR/USD"`
|
|
152
|
+
- `toFinnhubSymbol(symbol)` — normalises symbol for Finnhub
|
|
153
|
+
- `CRYPTO` exchange added to the calendar — `alwaysOpen: true`, no holidays, no session boundaries
|
|
154
|
+
- `isMarketOpen("CRYPTO")` → always `true`
|
|
155
|
+
- `getSession("CRYPTO")` → always `"regular"`
|
|
156
|
+
- `isHoliday("CRYPTO")` → always `false`
|
|
157
|
+
- `nextSessionOpen("CRYPTO")` → returns `from` (already open)
|
|
158
|
+
- `nextSessionClose("CRYPTO")` → returns `from + 24h` (rolling window)
|
|
159
|
+
|
|
160
|
+
### Breaking changes
|
|
161
|
+
|
|
162
|
+
None. All v0.2.0 imports continue to work unchanged.
|
|
163
|
+
|
|
164
|
+
### Other changes
|
|
165
|
+
|
|
166
|
+
- `isCrypto`, `isForex`, `toFinnhubSymbol` exported from main `market-feed` entry point
|
|
167
|
+
- `FinnhubProvider` and `FinnhubProviderOptions` exported from main `market-feed` entry point
|
|
168
|
+
- 94 new unit tests (314 total across 17 test files)
|
|
169
|
+
- 6 library tsup entry points + 1 CLI binary: `index`, `calendar`, `stream`, `consensus`, `indicators`, `portfolio`, `cli`
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## 0.2.0 — 2026-03-11
|
|
174
|
+
|
|
175
|
+
### New modules
|
|
176
|
+
|
|
177
|
+
**`market-feed/calendar`** — Synchronous exchange calendar. No network required.
|
|
178
|
+
- `isMarketOpen(exchange, at?)` — boolean, DST-correct via `Intl`
|
|
179
|
+
- `getSession(exchange, at?)` — `"pre" | "regular" | "post" | "closed"`
|
|
180
|
+
- `nextSessionOpen(exchange, from?)` / `nextSessionClose(exchange, from?)` — next UTC Date
|
|
181
|
+
- `isHoliday(exchange, date?)` / `isEarlyClose(exchange, date?)` — boolean
|
|
182
|
+
- `getHolidayDates(exchange, year)` — all holidays for a given year
|
|
183
|
+
- `getExchangeInfo(exchange)` — name, MIC, timezone, open/close times, currency
|
|
184
|
+
- Supports: NYSE, NASDAQ, LSE, TSX, ASX, XETRA, NSE, BSE
|
|
185
|
+
- Holiday rules computed from first principles (Easter via Meeus/Jones/Butcher algorithm, all US federal/NYSE-specific rules, UK bank holidays, Canadian/Australian/German/Indian holidays)
|
|
186
|
+
- Early-close days (NYSE: day before Thanksgiving, Independence Day, Christmas Eve)
|
|
187
|
+
|
|
188
|
+
**`market-feed/stream`** — Market-hours-aware observable quote stream.
|
|
189
|
+
- `watch(feed, symbols, options)` — async generator yielding typed `StreamEvent` union
|
|
190
|
+
- Polls at `interval.open` (default 5s) during regular hours, `interval.prepost` (default 30s) pre/post, pauses at `interval.closed` (default 60s) when closed — saves API quota overnight and on weekends
|
|
191
|
+
- Emits `market-open` / `market-close` events at session transitions
|
|
192
|
+
- Emits `divergence` events when multiple configured providers disagree beyond `divergenceThreshold`
|
|
193
|
+
- Graceful `AbortSignal` cancellation
|
|
194
|
+
- Configurable `maxErrors` before the generator throws
|
|
195
|
+
|
|
196
|
+
**`market-feed/consensus`** — Multi-provider parallel price consensus.
|
|
197
|
+
- `consensus(providers, symbol, options)` — queries all providers simultaneously via `Promise.allSettled`
|
|
198
|
+
- Median-based outlier detection (avoids the all-outlier edge case of mean-based approaches)
|
|
199
|
+
- Staleness detection: providers with quotes older than `stalenessThreshold` receive half weight
|
|
200
|
+
- Returns `ConsensusResult` with `price`, `confidence` (0–1), `spread`, `spreadPct`, per-provider breakdown, and `flags`
|
|
201
|
+
- Flags: `HIGH_DIVERGENCE`, `STALE_DATA`, `SINGLE_SOURCE`, `OUTLIER_EXCLUDED`
|
|
202
|
+
- Algorithm helpers exported: `normalizeWeights`, `applyStalenessPenalty`, `weightedMean`, `detectOutliers`, `computeConfidence`
|
|
203
|
+
|
|
204
|
+
### Breaking changes
|
|
205
|
+
|
|
206
|
+
None. All v0.1.0 imports continue to work unchanged.
|
|
207
|
+
|
|
208
|
+
### Other changes
|
|
209
|
+
|
|
210
|
+
- `MarketFeed` now exposes `get providers(): readonly MarketProvider[]` — read-only view of configured providers, used by `watch()` for divergence detection and `consensus()` for parallel querying
|
|
211
|
+
- 78 new unit tests (220 total across 13 test files)
|
|
212
|
+
- 4 tsup entry points: `index`, `calendar`, `stream`, `consensus` — each tree-shaken independently
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
3
216
|
## 0.1.0 — 2026-03-10
|
|
4
217
|
|
|
5
218
|
### Initial release
|
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# market-feed
|
|
2
2
|
|
|
3
3
|
> Unified TypeScript client for financial market data.
|
|
4
|
-
> Wraps Yahoo Finance, Alpha Vantage,
|
|
4
|
+
> Wraps Yahoo Finance, Alpha Vantage, Polygon.io, and Finnhub under one consistent interface — with caching and automatic fallback built in.
|
|
5
5
|
|
|
6
6
|
[](https://github.com/piyushgupta344/market-feed/actions/workflows/ci.yml)
|
|
7
7
|
[](https://www.npmjs.com/package/market-feed)
|
|
@@ -39,7 +39,7 @@ const quote = await feed.quote("AAPL");
|
|
|
39
39
|
console.log(quote.price); // always a number, always the same key
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
-
One interface.
|
|
42
|
+
One interface. Four providers. Zero API key required for Yahoo Finance.
|
|
43
43
|
|
|
44
44
|
---
|
|
45
45
|
|
|
@@ -53,6 +53,391 @@ One interface. Three providers. Zero API key required for Yahoo Finance.
|
|
|
53
53
|
- **Strict TypeScript** — no `any`, full autocomplete, compile-time safety
|
|
54
54
|
- **Multi-runtime** — Node 18+, Bun 1+, Deno 2+, Cloudflare Workers
|
|
55
55
|
- **Escape hatch** — pass `{ raw: true }` to get the original provider response
|
|
56
|
+
- **Exchange calendar** — synchronous, offline-capable holiday and session detection for 8 exchanges + crypto (24/7)
|
|
57
|
+
- **WebSocket streaming** — `market-feed/ws` opens a persistent WS connection to Polygon or Finnhub; polling fallback for Yahoo/AV
|
|
58
|
+
- **Observable stream** — market-hours-aware HTTP polling that pauses overnight and on weekends
|
|
59
|
+
- **Price consensus** — query all providers in parallel, get a weighted mean with confidence score
|
|
60
|
+
- **Technical indicators** — SMA, EMA, RSI, MACD, Bollinger Bands, ATR, VWAP, Stochastic — pure functions, zero deps
|
|
61
|
+
- **Portfolio tracking** — live P&L, unrealised gains, day change across all positions
|
|
62
|
+
- **CLI** — `npx market-feed quote AAPL` — no install required
|
|
63
|
+
- **Crypto & Forex** — `isCrypto()` / `isForex()` helpers, CRYPTO calendar exchange (always open)
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## Subpath modules
|
|
68
|
+
|
|
69
|
+
`market-feed` ships six optional subpath modules alongside the core client.
|
|
70
|
+
|
|
71
|
+
### `market-feed/ws`
|
|
72
|
+
|
|
73
|
+
True WebSocket streaming — tick-by-tick trade data from Polygon and Finnhub, with automatic polling fallback for other providers.
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
import { connect } from "market-feed/ws";
|
|
77
|
+
import { MarketFeed, FinnhubProvider, PolygonProvider } from "market-feed";
|
|
78
|
+
|
|
79
|
+
// Finnhub — real-time WebSocket
|
|
80
|
+
const provider = new FinnhubProvider({ apiKey: process.env.FINNHUB_KEY! });
|
|
81
|
+
|
|
82
|
+
// Polygon — real-time WebSocket (requires paid plan for true real-time)
|
|
83
|
+
// const provider = new PolygonProvider({ apiKey: process.env.POLYGON_KEY! });
|
|
84
|
+
|
|
85
|
+
const controller = new AbortController();
|
|
86
|
+
|
|
87
|
+
for await (const event of connect(provider, ["AAPL", "MSFT", "TSLA"], {
|
|
88
|
+
signal: controller.signal,
|
|
89
|
+
})) {
|
|
90
|
+
switch (event.type) {
|
|
91
|
+
case "trade":
|
|
92
|
+
console.log(`${event.trade.symbol}: $${event.trade.price} × ${event.trade.size} shares`);
|
|
93
|
+
break;
|
|
94
|
+
case "connected":
|
|
95
|
+
console.log(`Connected to ${event.provider}`);
|
|
96
|
+
break;
|
|
97
|
+
case "disconnected":
|
|
98
|
+
console.log(`Disconnected (attempt ${event.attempt}, reconnecting: ${event.reconnecting})`);
|
|
99
|
+
break;
|
|
100
|
+
case "error":
|
|
101
|
+
if (!event.recoverable) throw event.error;
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
controller.abort(); // stop the stream
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Unlike `market-feed/stream` (which is HTTP polling), `market-feed/ws` opens a persistent WebSocket connection and yields individual trade executions in real time.
|
|
110
|
+
|
|
111
|
+
#### Provider support
|
|
112
|
+
|
|
113
|
+
| Provider | WebSocket | Notes |
|
|
114
|
+
|----------|-----------|-------|
|
|
115
|
+
| `PolygonProvider` | Native WS | Auth via JSON handshake, subscribes to `T.*` trades |
|
|
116
|
+
| `FinnhubProvider` | Native WS | Token in URL, per-symbol subscribe |
|
|
117
|
+
| `YahooProvider` | Polling fallback | Polls `quote()` every 5 s |
|
|
118
|
+
| `AlphaVantageProvider` | Polling fallback | Same as Yahoo |
|
|
119
|
+
|
|
120
|
+
#### `WsOptions`
|
|
121
|
+
|
|
122
|
+
| Option | Type | Default | Description |
|
|
123
|
+
|--------|------|---------|-------------|
|
|
124
|
+
| `wsImpl` | `typeof WebSocket` | `globalThis.WebSocket` | Custom WS constructor for Node 18–20 |
|
|
125
|
+
| `maxReconnectAttempts` | `number` | `10` | Reconnects before closing |
|
|
126
|
+
| `reconnectDelayMs` | `number` | `1000` | Base delay (doubles per attempt, max 30 s) |
|
|
127
|
+
| `signal` | `AbortSignal` | — | Stop the stream |
|
|
128
|
+
|
|
129
|
+
#### Node 18–20
|
|
130
|
+
|
|
131
|
+
Node 21+, Bun, Deno, and Cloudflare Workers expose `WebSocket` globally. For Node 18–20, install the `ws` package and inject it:
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
import WebSocket from "ws";
|
|
135
|
+
connect(provider, ["AAPL"], { wsImpl: WebSocket as unknown as typeof globalThis.WebSocket })
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
### `market-feed/calendar`
|
|
141
|
+
|
|
142
|
+
Synchronous exchange calendar — no network, no async, works offline.
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
import {
|
|
146
|
+
isMarketOpen,
|
|
147
|
+
getSession,
|
|
148
|
+
nextSessionOpen,
|
|
149
|
+
nextSessionClose,
|
|
150
|
+
isHoliday,
|
|
151
|
+
isEarlyClose,
|
|
152
|
+
getHolidayDates,
|
|
153
|
+
getExchangeInfo,
|
|
154
|
+
} from "market-feed/calendar";
|
|
155
|
+
|
|
156
|
+
// Is NYSE open right now?
|
|
157
|
+
isMarketOpen("NYSE"); // true | false
|
|
158
|
+
|
|
159
|
+
// What session is it?
|
|
160
|
+
getSession("NYSE"); // "pre" | "regular" | "post" | "closed"
|
|
161
|
+
|
|
162
|
+
// When does it next open?
|
|
163
|
+
nextSessionOpen("NYSE"); // Date (UTC)
|
|
164
|
+
nextSessionClose("NYSE"); // Date (UTC)
|
|
165
|
+
|
|
166
|
+
// Holiday checks
|
|
167
|
+
isHoliday("LSE"); // false
|
|
168
|
+
isHoliday("NYSE", new Date("2025-04-18")); // true — Good Friday
|
|
169
|
+
isEarlyClose("NYSE"); // true on day before Thanksgiving
|
|
170
|
+
|
|
171
|
+
// All holidays for a year
|
|
172
|
+
getHolidayDates("NYSE", 2026); // Date[]
|
|
173
|
+
|
|
174
|
+
// Exchange metadata
|
|
175
|
+
getExchangeInfo("LSE");
|
|
176
|
+
// { id: "LSE", name: "London Stock Exchange", mic: "XLON",
|
|
177
|
+
// timezone: "Europe/London", openTime: "08:00", closeTime: "16:30", ... }
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Supports **NYSE, NASDAQ, LSE, TSX, ASX, XETRA, NSE, BSE**, and **CRYPTO** (always open — no sessions, no holidays). Holiday rules are computed from first principles — Easter via the Meeus/Jones/Butcher algorithm, all NYSE-specific rules (MLK Day, Presidents' Day, Juneteenth, Memorial Day, Labor Day, Thanksgiving, Good Friday), UK bank holidays, Canadian, Australian, German, and Indian exchanges. DST is handled via `Intl.DateTimeFormat` — no manual offset arithmetic.
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
### `market-feed/stream`
|
|
185
|
+
|
|
186
|
+
Market-hours-aware async generator. Polls during open hours, pauses when the market is closed.
|
|
187
|
+
|
|
188
|
+
```ts
|
|
189
|
+
import { watch } from "market-feed/stream";
|
|
190
|
+
import { MarketFeed, YahooProvider, PolygonProvider } from "market-feed";
|
|
191
|
+
|
|
192
|
+
const feed = new MarketFeed({
|
|
193
|
+
providers: [
|
|
194
|
+
new YahooProvider(),
|
|
195
|
+
new PolygonProvider({ apiKey: process.env.POLYGON_KEY }),
|
|
196
|
+
],
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
const controller = new AbortController();
|
|
200
|
+
|
|
201
|
+
for await (const event of watch(feed, ["AAPL", "MSFT"], {
|
|
202
|
+
exchange: "NYSE",
|
|
203
|
+
interval: {
|
|
204
|
+
open: 5_000, // poll every 5s during regular hours
|
|
205
|
+
prepost: 30_000, // every 30s pre/post market
|
|
206
|
+
closed: 60_000, // check every 60s for session open
|
|
207
|
+
},
|
|
208
|
+
divergenceThreshold: 0.5, // % spread between providers
|
|
209
|
+
signal: controller.signal,
|
|
210
|
+
})) {
|
|
211
|
+
switch (event.type) {
|
|
212
|
+
case "quote":
|
|
213
|
+
console.log(`${event.symbol}: $${event.quote.price}`);
|
|
214
|
+
break;
|
|
215
|
+
case "market-open":
|
|
216
|
+
console.log(`${event.exchange} session started: ${event.session}`);
|
|
217
|
+
break;
|
|
218
|
+
case "market-close":
|
|
219
|
+
console.log(`${event.exchange} session ended`);
|
|
220
|
+
break;
|
|
221
|
+
case "divergence":
|
|
222
|
+
console.log(`${event.symbol} providers disagree: ${event.spreadPct.toFixed(2)}%`);
|
|
223
|
+
break;
|
|
224
|
+
case "error":
|
|
225
|
+
if (!event.recoverable) throw event.error;
|
|
226
|
+
break;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Stop the stream
|
|
231
|
+
controller.abort();
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
When `marketHoursAware: true` (default), the stream pauses completely during closed sessions — no wasted API calls overnight or on weekends. It emits `market-open` / `market-close` events at session boundaries. When the feed has multiple providers, it detects price divergence across them and emits `divergence` events.
|
|
235
|
+
|
|
236
|
+
#### `WatchOptions`
|
|
237
|
+
|
|
238
|
+
| Option | Type | Default | Description |
|
|
239
|
+
|--------|------|---------|-------------|
|
|
240
|
+
| `exchange` | `ExchangeId` | `"NYSE"` | Calendar to use for session detection |
|
|
241
|
+
| `interval.open` | `number` | `5000` | Poll interval (ms) during regular hours |
|
|
242
|
+
| `interval.prepost` | `number` | `30000` | Poll interval (ms) during pre/post market |
|
|
243
|
+
| `interval.closed` | `number` | `60000` | Check interval (ms) when market is closed |
|
|
244
|
+
| `marketHoursAware` | `boolean` | `true` | Pause during closed sessions |
|
|
245
|
+
| `divergenceThreshold` | `number` | `0.5` | % spread that triggers a divergence event |
|
|
246
|
+
| `maxErrors` | `number` | `5` | Consecutive errors before the generator throws |
|
|
247
|
+
| `signal` | `AbortSignal` | — | Cancel the stream |
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
### `market-feed/consensus`
|
|
252
|
+
|
|
253
|
+
Queries all configured providers simultaneously and returns a statistically-weighted price consensus.
|
|
254
|
+
|
|
255
|
+
Unlike `feed.quote()` which stops at the first successful provider, `consensus()` fires all providers in parallel and combines their results.
|
|
256
|
+
|
|
257
|
+
```ts
|
|
258
|
+
import { consensus } from "market-feed/consensus";
|
|
259
|
+
import { MarketFeed, YahooProvider, AlphaVantageProvider, PolygonProvider } from "market-feed";
|
|
260
|
+
|
|
261
|
+
const feed = new MarketFeed({
|
|
262
|
+
providers: [
|
|
263
|
+
new YahooProvider(),
|
|
264
|
+
new AlphaVantageProvider({ apiKey: process.env.AV_KEY }),
|
|
265
|
+
new PolygonProvider({ apiKey: process.env.POLYGON_KEY }),
|
|
266
|
+
],
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
const result = await consensus(feed.providers, "AAPL");
|
|
270
|
+
|
|
271
|
+
console.log(result.price); // 189.82 — weighted mean
|
|
272
|
+
console.log(result.confidence); // 0.97 — 0=no agreement, 1=perfect
|
|
273
|
+
console.log(result.spread); // 0.08 — max - min across providers
|
|
274
|
+
console.log(result.spreadPct); // 0.042 — spread as % of price
|
|
275
|
+
console.log(result.flags); // [] or ["HIGH_DIVERGENCE", "STALE_DATA", ...]
|
|
276
|
+
console.log(result.providers);
|
|
277
|
+
// {
|
|
278
|
+
// yahoo: { price: 189.84, weight: 0.33, stale: false, included: true },
|
|
279
|
+
// polygon: { price: 189.80, weight: 0.33, stale: false, included: true },
|
|
280
|
+
// "alpha-vantage": { price: 189.82, weight: 0.33, stale: false, included: true },
|
|
281
|
+
// }
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
#### `ConsensusOptions`
|
|
285
|
+
|
|
286
|
+
| Option | Type | Default | Description |
|
|
287
|
+
|--------|------|---------|-------------|
|
|
288
|
+
| `stalenessThreshold` | `number` | `60` | Seconds before a quote is stale. Stale providers get half weight. |
|
|
289
|
+
| `divergenceThreshold` | `number` | `2.0` | % deviation from median that marks a provider as an outlier |
|
|
290
|
+
| `weights` | `Record<string, number>` | equal | Custom weights per provider name (normalized automatically) |
|
|
291
|
+
|
|
292
|
+
#### Flags
|
|
293
|
+
|
|
294
|
+
| Flag | Meaning |
|
|
295
|
+
|------|---------|
|
|
296
|
+
| `HIGH_DIVERGENCE` | `spreadPct` exceeds `divergenceThreshold` |
|
|
297
|
+
| `STALE_DATA` | At least one provider returned a quote older than `stalenessThreshold` |
|
|
298
|
+
| `SINGLE_SOURCE` | Only one provider responded successfully |
|
|
299
|
+
| `OUTLIER_EXCLUDED` | At least one provider was excluded as a price outlier |
|
|
300
|
+
|
|
301
|
+
---
|
|
302
|
+
|
|
303
|
+
### `market-feed/indicators`
|
|
304
|
+
|
|
305
|
+
Technical indicators as pure functions over `HistoricalBar[]`. No network, no async, tree-shakeable.
|
|
306
|
+
|
|
307
|
+
```ts
|
|
308
|
+
import {
|
|
309
|
+
sma, ema, rsi, macd, bollingerBands, atr, vwap, stochastic,
|
|
310
|
+
} from "market-feed/indicators";
|
|
311
|
+
import { MarketFeed } from "market-feed";
|
|
312
|
+
|
|
313
|
+
const feed = new MarketFeed();
|
|
314
|
+
const bars = await feed.historical("AAPL", { period1: "2024-01-01", interval: "1d" });
|
|
315
|
+
|
|
316
|
+
// Simple / Exponential Moving Average
|
|
317
|
+
const sma20 = sma(bars, 20); // IndicatorPoint[] — { date, value }[]
|
|
318
|
+
const ema12 = ema(bars, 12);
|
|
319
|
+
|
|
320
|
+
// Relative Strength Index
|
|
321
|
+
const rsi14 = rsi(bars, 14); // values in [0, 100]
|
|
322
|
+
|
|
323
|
+
// MACD
|
|
324
|
+
const macdResult = macd(bars); // MACDPoint[] — { date, macd, signal, histogram }[]
|
|
325
|
+
|
|
326
|
+
// Bollinger Bands
|
|
327
|
+
const bb = bollingerBands(bars, 20, 2); // BollingerPoint[] — { date, upper, middle, lower }[]
|
|
328
|
+
|
|
329
|
+
// Average True Range
|
|
330
|
+
const atr14 = atr(bars, 14);
|
|
331
|
+
|
|
332
|
+
// VWAP (cumulative from first bar)
|
|
333
|
+
const vwapPoints = vwap(bars);
|
|
334
|
+
|
|
335
|
+
// Stochastic Oscillator
|
|
336
|
+
const stoch = stochastic(bars, 14, 3); // StochasticPoint[] — { date, k, d }[]
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
All functions return typed arrays starting from the first bar where enough data exists. They never throw for insufficient data — they return an empty array instead.
|
|
340
|
+
|
|
341
|
+
| Function | Output type | Default params |
|
|
342
|
+
|----------|------------|----------------|
|
|
343
|
+
| `sma(bars, period)` | `IndicatorPoint[]` | — |
|
|
344
|
+
| `ema(bars, period)` | `IndicatorPoint[]` | — |
|
|
345
|
+
| `rsi(bars, period?)` | `IndicatorPoint[]` | period: 14 |
|
|
346
|
+
| `macd(bars, fast?, slow?, signal?)` | `MACDPoint[]` | 12, 26, 9 |
|
|
347
|
+
| `bollingerBands(bars, period?, stdDevMult?)` | `BollingerPoint[]` | 20, 2 |
|
|
348
|
+
| `atr(bars, period?)` | `IndicatorPoint[]` | period: 14 |
|
|
349
|
+
| `vwap(bars)` | `IndicatorPoint[]` | — |
|
|
350
|
+
| `stochastic(bars, kPeriod?, dPeriod?)` | `StochasticPoint[]` | 14, 3 |
|
|
351
|
+
|
|
352
|
+
---
|
|
353
|
+
|
|
354
|
+
### `market-feed/portfolio`
|
|
355
|
+
|
|
356
|
+
Track a collection of positions and compute live P&L against current market prices.
|
|
357
|
+
|
|
358
|
+
```ts
|
|
359
|
+
import { Portfolio } from "market-feed/portfolio";
|
|
360
|
+
import { MarketFeed } from "market-feed";
|
|
361
|
+
|
|
362
|
+
const feed = new MarketFeed();
|
|
363
|
+
|
|
364
|
+
const portfolio = new Portfolio([
|
|
365
|
+
{ symbol: "AAPL", quantity: 10, avgCost: 150.00 },
|
|
366
|
+
{ symbol: "MSFT", quantity: 5, avgCost: 280.00 },
|
|
367
|
+
{ symbol: "TSLA", quantity: -3, avgCost: 250.00 }, // short position
|
|
368
|
+
]);
|
|
369
|
+
|
|
370
|
+
// Fetch live quotes and compute P&L in one call
|
|
371
|
+
const snap = await portfolio.snapshot(feed);
|
|
372
|
+
|
|
373
|
+
console.log(`Total value: $${snap.totalMarketValue.toFixed(2)}`);
|
|
374
|
+
console.log(`Unrealised P&L: $${snap.totalUnrealizedPnl.toFixed(2)}`);
|
|
375
|
+
console.log(`Today's change: $${snap.totalDayChange.toFixed(2)}`);
|
|
376
|
+
|
|
377
|
+
for (const pos of snap.positions) {
|
|
378
|
+
const pct = (pos.unrealizedPnlPct * 100).toFixed(2);
|
|
379
|
+
console.log(`${pos.symbol}: $${pos.marketValue.toFixed(2)} (${pct}%)`);
|
|
380
|
+
}
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
#### `Position`
|
|
384
|
+
|
|
385
|
+
| Field | Type | Description |
|
|
386
|
+
|-------|------|-------------|
|
|
387
|
+
| `symbol` | `string` | Ticker symbol |
|
|
388
|
+
| `quantity` | `number` | Units held. Negative = short. |
|
|
389
|
+
| `avgCost` | `number` | Average cost per unit |
|
|
390
|
+
| `currency` | `string?` | Defaults to "USD" |
|
|
391
|
+
| `openedAt` | `Date?` | When the position was opened |
|
|
392
|
+
| `notes` | `string?` | Free-form notes |
|
|
393
|
+
|
|
394
|
+
#### `Portfolio` API
|
|
395
|
+
|
|
396
|
+
```ts
|
|
397
|
+
portfolio.add(position) // add or replace a position (chainable)
|
|
398
|
+
portfolio.remove(symbol) // remove a position (chainable)
|
|
399
|
+
portfolio.get(symbol) // Position | undefined
|
|
400
|
+
portfolio.list() // readonly Position[]
|
|
401
|
+
portfolio.size // number
|
|
402
|
+
portfolio.snapshot(feed) // Promise<PortfolioSnapshot>
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
---
|
|
406
|
+
|
|
407
|
+
### CLI (`npx market-feed`)
|
|
408
|
+
|
|
409
|
+
A zero-config CLI powered by Yahoo Finance (no API key needed). Add keys to unlock more providers.
|
|
410
|
+
|
|
411
|
+
```bash
|
|
412
|
+
# Quotes
|
|
413
|
+
npx market-feed quote AAPL MSFT GOOGL
|
|
414
|
+
|
|
415
|
+
# Historical data
|
|
416
|
+
npx market-feed historical AAPL --interval 1wk --period1 2024-01-01
|
|
417
|
+
|
|
418
|
+
# Symbol search
|
|
419
|
+
npx market-feed search "apple inc"
|
|
420
|
+
|
|
421
|
+
# Company profile
|
|
422
|
+
npx market-feed company AAPL
|
|
423
|
+
|
|
424
|
+
# News (JSON output)
|
|
425
|
+
npx market-feed news AAPL --limit 5 --json
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
#### Options
|
|
429
|
+
|
|
430
|
+
| Flag | Description |
|
|
431
|
+
|------|-------------|
|
|
432
|
+
| `--av-key <key>` | Alpha Vantage API key |
|
|
433
|
+
| `--polygon-key <key>` | Polygon.io API key |
|
|
434
|
+
| `--finnhub-key <key>` | Finnhub API key |
|
|
435
|
+
| `--json` | Output raw JSON instead of formatted tables |
|
|
436
|
+
| `--limit <n>` | Limit results (default: 10) |
|
|
437
|
+
| `--interval <i>` | Historical interval: `1m` `5m` `15m` `30m` `1h` `1d` `1wk` `1mo` (default: `1d`) |
|
|
438
|
+
| `--period1 <date>` | Historical start date (ISO 8601) |
|
|
439
|
+
| `--period2 <date>` | Historical end date (ISO 8601) |
|
|
440
|
+
| `-h`, `--help` | Show help |
|
|
56
441
|
|
|
57
442
|
---
|
|
58
443
|
|
|
@@ -107,8 +492,9 @@ console.log(profile.sector); // "Technology"
|
|
|
107
492
|
| **Yahoo Finance** | Not required | ✓ | ✓ | ✓ | ✓ | — | — |
|
|
108
493
|
| **Alpha Vantage** | Free (25/day) | ✓ | ✓ | ✓ | ✓ | — | — |
|
|
109
494
|
| **Polygon.io** | Free (delayed) | ✓ | ✓ | ✓ | ✓ | ✓ | — |
|
|
495
|
+
| **Finnhub** | Free (60/min) | ✓ | ✓ | ✓ | ✓ | ✓ | — |
|
|
110
496
|
|
|
111
|
-
Get free keys: [Alpha Vantage](https://www.alphavantage.co/support/#api-key) · [Polygon.io](https://polygon.io/)
|
|
497
|
+
Get free keys: [Alpha Vantage](https://www.alphavantage.co/support/#api-key) · [Polygon.io](https://polygon.io/) · [Finnhub](https://finnhub.io/)
|
|
112
498
|
|
|
113
499
|
### Using multiple providers
|
|
114
500
|
|
|
@@ -118,6 +504,7 @@ import {
|
|
|
118
504
|
YahooProvider,
|
|
119
505
|
AlphaVantageProvider,
|
|
120
506
|
PolygonProvider,
|
|
507
|
+
FinnhubProvider,
|
|
121
508
|
} from "market-feed";
|
|
122
509
|
|
|
123
510
|
const feed = new MarketFeed({
|
|
@@ -125,6 +512,7 @@ const feed = new MarketFeed({
|
|
|
125
512
|
new YahooProvider(),
|
|
126
513
|
new AlphaVantageProvider({ apiKey: process.env.AV_KEY }),
|
|
127
514
|
new PolygonProvider({ apiKey: process.env.POLYGON_KEY }),
|
|
515
|
+
new FinnhubProvider({ apiKey: process.env.FINNHUB_KEY }),
|
|
128
516
|
],
|
|
129
517
|
fallback: true, // auto-try next provider on failure
|
|
130
518
|
});
|
|
@@ -300,7 +688,7 @@ const feed = new MarketFeed({ providers: [new MyProvider()] });
|
|
|
300
688
|
|
|
301
689
|
| Runtime | Version | Notes |
|
|
302
690
|
|---------|---------|-------|
|
|
303
|
-
| Node.js | 18+ |
|
|
691
|
+
| Node.js | 18+ | `fetch` available since Node 18; `WebSocket` global available since Node 21. For Node 18–20, inject the `ws` package via `wsImpl` for `market-feed/ws`. |
|
|
304
692
|
| Bun | 1+ | Fully supported |
|
|
305
693
|
| Deno | 2+ | Fully supported |
|
|
306
694
|
| Cloudflare Workers | Latest | Fully supported |
|
|
@@ -323,7 +711,7 @@ pnpm test
|
|
|
323
711
|
|
|
324
712
|
## Disclaimer
|
|
325
713
|
|
|
326
|
-
This library is not affiliated with or endorsed by Yahoo Finance, Alpha Vantage,
|
|
714
|
+
This library is not affiliated with or endorsed by Yahoo Finance, Alpha Vantage, Polygon.io, or Finnhub. Data is provided for informational purposes only and should not be used as the sole basis for investment decisions.
|
|
327
715
|
|
|
328
716
|
---
|
|
329
717
|
|