market-feed 0.8.0 → 0.9.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 +61 -0
- package/dist/screener.cjs +76 -0
- package/dist/screener.cjs.map +1 -0
- package/dist/screener.d.cts +108 -0
- package/dist/screener.d.ts +108 -0
- package/dist/screener.js +74 -0
- package/dist/screener.js.map +1 -0
- package/package.json +6 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,66 @@
|
|
|
1
1
|
# market-feed Changelog
|
|
2
2
|
|
|
3
|
+
## 0.9.0 — 2026-03-12
|
|
4
|
+
|
|
5
|
+
### New module
|
|
6
|
+
|
|
7
|
+
**`market-feed/screener`** — Filter a list of symbols against a set of criteria using live quote data.
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { screen } from "market-feed/screener";
|
|
11
|
+
import { MarketFeed } from "market-feed";
|
|
12
|
+
|
|
13
|
+
const feed = new MarketFeed();
|
|
14
|
+
|
|
15
|
+
const results = await screen(feed, ["AAPL", "MSFT", "GOOGL", "TSLA", "NVDA"], {
|
|
16
|
+
criteria: [
|
|
17
|
+
{ type: "price_above", value: 100 },
|
|
18
|
+
{ type: "change_pct_above", value: 1.5 },
|
|
19
|
+
{ type: "volume_above", value: 10_000_000 },
|
|
20
|
+
{ type: "market_cap_above", value: 100_000_000_000 },
|
|
21
|
+
],
|
|
22
|
+
limit: 10,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
console.log(results.map((r) => `${r.symbol} @ ${r.quote.price}`));
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
#### Criterion types
|
|
29
|
+
|
|
30
|
+
| Type | Description |
|
|
31
|
+
|------|-------------|
|
|
32
|
+
| `price_above` / `price_below` | Filter by current price |
|
|
33
|
+
| `change_pct_above` / `change_pct_below` | Filter by daily % change |
|
|
34
|
+
| `volume_above` / `volume_below` | Filter by trading volume |
|
|
35
|
+
| `market_cap_above` / `market_cap_below` | Filter by market cap |
|
|
36
|
+
| `52w_high_pct_below` | Price is within N% of the 52-week high |
|
|
37
|
+
| `52w_low_pct_above` | Price is at least N% above the 52-week low |
|
|
38
|
+
| `custom` | Arbitrary predicate: `{ type: "custom", fn: (quote) => boolean }` |
|
|
39
|
+
|
|
40
|
+
All criteria are evaluated with **AND logic** — a symbol must pass every criterion to be included.
|
|
41
|
+
|
|
42
|
+
#### Options
|
|
43
|
+
|
|
44
|
+
| Option | Description |
|
|
45
|
+
|--------|-------------|
|
|
46
|
+
| `criteria` | Array of `ScreenerCriterion` (required) |
|
|
47
|
+
| `batchSize` | Max symbols per quote fetch call (default: all at once) |
|
|
48
|
+
| `limit` | Max number of results to return |
|
|
49
|
+
|
|
50
|
+
#### Result shape
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
interface ScreenerResult {
|
|
54
|
+
symbol: string;
|
|
55
|
+
quote: Quote;
|
|
56
|
+
matchedCriteria: number; // always === criteria.length
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
`screen()` accepts any object with a `quote(symbols[]) → Quote[]` method — works with `MarketFeed`, individual providers, or your own mock.
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
3
64
|
## 0.8.0 — 2026-03-12
|
|
4
65
|
|
|
5
66
|
### New module
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// market-feed — Unified financial market data client
|
|
4
|
+
// https://github.com/piyushgupta344/market-feed
|
|
5
|
+
|
|
6
|
+
// src/screener/index.ts
|
|
7
|
+
async function screen(source, symbols, options) {
|
|
8
|
+
if (symbols.length === 0 || options.criteria.length === 0) return [];
|
|
9
|
+
const { criteria, batchSize, limit } = options;
|
|
10
|
+
const quotes = await fetchInBatches(source, symbols, batchSize);
|
|
11
|
+
const results = [];
|
|
12
|
+
for (const quote of quotes) {
|
|
13
|
+
if (matchesAll(quote, criteria)) {
|
|
14
|
+
results.push({ symbol: quote.symbol, quote, matchedCriteria: criteria.length });
|
|
15
|
+
if (limit !== void 0 && results.length >= limit) break;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return results;
|
|
19
|
+
}
|
|
20
|
+
function matchesAll(quote, criteria) {
|
|
21
|
+
for (const c of criteria) {
|
|
22
|
+
if (!matchesCriterion(quote, c)) return false;
|
|
23
|
+
}
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
function matchesCriterion(quote, criterion) {
|
|
27
|
+
switch (criterion.type) {
|
|
28
|
+
case "price_above":
|
|
29
|
+
return quote.price > criterion.value;
|
|
30
|
+
case "price_below":
|
|
31
|
+
return quote.price < criterion.value;
|
|
32
|
+
case "change_pct_above":
|
|
33
|
+
return quote.changePercent > criterion.value;
|
|
34
|
+
case "change_pct_below":
|
|
35
|
+
return quote.changePercent < criterion.value;
|
|
36
|
+
case "volume_above":
|
|
37
|
+
return quote.volume > criterion.value;
|
|
38
|
+
case "volume_below":
|
|
39
|
+
return quote.volume < criterion.value;
|
|
40
|
+
case "market_cap_above":
|
|
41
|
+
return quote.marketCap !== void 0 && quote.marketCap > criterion.value;
|
|
42
|
+
case "market_cap_below":
|
|
43
|
+
return quote.marketCap !== void 0 && quote.marketCap < criterion.value;
|
|
44
|
+
case "pe_above":
|
|
45
|
+
return true;
|
|
46
|
+
case "pe_below":
|
|
47
|
+
return true;
|
|
48
|
+
case "52w_high_pct_below": {
|
|
49
|
+
if (quote.fiftyTwoWeekHigh === void 0) return true;
|
|
50
|
+
const pctBelow = (quote.fiftyTwoWeekHigh - quote.price) / quote.fiftyTwoWeekHigh * 100;
|
|
51
|
+
return pctBelow < criterion.value;
|
|
52
|
+
}
|
|
53
|
+
case "52w_low_pct_above": {
|
|
54
|
+
if (quote.fiftyTwoWeekLow === void 0) return true;
|
|
55
|
+
const pctAbove = (quote.price - quote.fiftyTwoWeekLow) / quote.fiftyTwoWeekLow * 100;
|
|
56
|
+
return pctAbove > criterion.value;
|
|
57
|
+
}
|
|
58
|
+
case "custom":
|
|
59
|
+
return criterion.fn(quote);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
async function fetchInBatches(source, symbols, batchSize) {
|
|
63
|
+
if (!batchSize || batchSize >= symbols.length) {
|
|
64
|
+
return source.quote(symbols);
|
|
65
|
+
}
|
|
66
|
+
const batches = [];
|
|
67
|
+
for (let i = 0; i < symbols.length; i += batchSize) {
|
|
68
|
+
batches.push(symbols.slice(i, i + batchSize));
|
|
69
|
+
}
|
|
70
|
+
const results = await Promise.all(batches.map((batch) => source.quote(batch)));
|
|
71
|
+
return results.flat();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
exports.screen = screen;
|
|
75
|
+
//# sourceMappingURL=screener.cjs.map
|
|
76
|
+
//# sourceMappingURL=screener.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/screener/index.ts"],"names":[],"mappings":"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","file":"screener.cjs","sourcesContent":["/**\n * market-feed/screener\n *\n * Filter a list of symbols against a set of criteria using live quote data,\n * company profile data, or any custom condition function.\n *\n * @example\n * ```ts\n * import { screen } from \"market-feed/screener\";\n * import { MarketFeed } from \"market-feed\";\n *\n * const feed = new MarketFeed();\n *\n * const results = await screen(feed, [\"AAPL\", \"MSFT\", \"GOOGL\", \"TSLA\"], {\n * criteria: [\n * { type: \"price_above\", value: 100 },\n * { type: \"change_pct_above\", value: -5 },\n * { type: \"volume_above\", value: 1_000_000 },\n * ],\n * });\n *\n * console.log(results.map((r) => r.symbol));\n * ```\n */\n\nimport type { Quote } from \"../types/quote.js\";\n\n// ---------------------------------------------------------------------------\n// Criterion types\n// ---------------------------------------------------------------------------\n\nexport type QuoteCriterion =\n | { type: \"price_above\"; value: number }\n | { type: \"price_below\"; value: number }\n | { type: \"change_pct_above\"; value: number }\n | { type: \"change_pct_below\"; value: number }\n | { type: \"volume_above\"; value: number }\n | { type: \"volume_below\"; value: number }\n | { type: \"market_cap_above\"; value: number }\n | { type: \"market_cap_below\"; value: number }\n | { type: \"pe_above\"; value: number }\n | { type: \"pe_below\"; value: number }\n | { type: \"52w_high_pct_below\"; value: number }\n | { type: \"52w_low_pct_above\"; value: number }\n /** Custom predicate — return true to include, false to exclude */\n | { type: \"custom\"; fn: (quote: Quote) => boolean };\n\nexport type ScreenerCriterion = QuoteCriterion;\n\nexport interface ScreenerOptions {\n /** List of criteria — ALL must pass (logical AND) */\n criteria: ScreenerCriterion[];\n /**\n * Maximum number of symbols to fetch quotes for per batch.\n * Defaults to all symbols in one call.\n */\n batchSize?: number;\n /**\n * Maximum number of results to return.\n * Defaults to all matching symbols.\n */\n limit?: number;\n}\n\nexport interface ScreenerResult {\n symbol: string;\n quote: Quote;\n /** Which criteria matched (all of them, by design) */\n matchedCriteria: number;\n}\n\n// ---------------------------------------------------------------------------\n// Quote source interface (duck-typed for testability)\n// ---------------------------------------------------------------------------\n\ninterface QuoteSource {\n quote(symbols: string[], options?: { raw?: boolean }): Promise<Quote[]>;\n}\n\n// ---------------------------------------------------------------------------\n// Core screener function\n// ---------------------------------------------------------------------------\n\n/**\n * Screen a list of symbols against a set of criteria.\n *\n * Fetches quotes for all symbols (in optional batches) and returns those\n * that satisfy ALL of the provided criteria.\n *\n * @param source - Any object with a `quote(symbols[]) → Quote[]` method (e.g. `MarketFeed`, a single provider)\n * @param symbols - Symbols to evaluate\n * @param options - Screener configuration\n */\nexport async function screen(\n source: QuoteSource,\n symbols: string[],\n options: ScreenerOptions,\n): Promise<ScreenerResult[]> {\n if (symbols.length === 0 || options.criteria.length === 0) return [];\n\n const { criteria, batchSize, limit } = options;\n\n // Fetch quotes in batches (or all at once if no batchSize)\n const quotes = await fetchInBatches(source, symbols, batchSize);\n\n const results: ScreenerResult[] = [];\n for (const quote of quotes) {\n if (matchesAll(quote, criteria)) {\n results.push({ symbol: quote.symbol, quote, matchedCriteria: criteria.length });\n if (limit !== undefined && results.length >= limit) break;\n }\n }\n\n return results;\n}\n\n// ---------------------------------------------------------------------------\n// Criterion evaluator\n// ---------------------------------------------------------------------------\n\nfunction matchesAll(quote: Quote, criteria: ScreenerCriterion[]): boolean {\n for (const c of criteria) {\n if (!matchesCriterion(quote, c)) return false;\n }\n return true;\n}\n\nfunction matchesCriterion(quote: Quote, criterion: ScreenerCriterion): boolean {\n switch (criterion.type) {\n case \"price_above\":\n return quote.price > criterion.value;\n case \"price_below\":\n return quote.price < criterion.value;\n case \"change_pct_above\":\n return quote.changePercent > criterion.value;\n case \"change_pct_below\":\n return quote.changePercent < criterion.value;\n case \"volume_above\":\n return quote.volume > criterion.value;\n case \"volume_below\":\n return quote.volume < criterion.value;\n case \"market_cap_above\":\n return quote.marketCap !== undefined && quote.marketCap > criterion.value;\n case \"market_cap_below\":\n return quote.marketCap !== undefined && quote.marketCap < criterion.value;\n case \"pe_above\":\n // CompanyProfile-level data is not on Quote — skip (return true to not block)\n return true;\n case \"pe_below\":\n return true;\n case \"52w_high_pct_below\": {\n if (quote.fiftyTwoWeekHigh === undefined) return true;\n const pctBelow = ((quote.fiftyTwoWeekHigh - quote.price) / quote.fiftyTwoWeekHigh) * 100;\n return pctBelow < criterion.value;\n }\n case \"52w_low_pct_above\": {\n if (quote.fiftyTwoWeekLow === undefined) return true;\n const pctAbove = ((quote.price - quote.fiftyTwoWeekLow) / quote.fiftyTwoWeekLow) * 100;\n return pctAbove > criterion.value;\n }\n case \"custom\":\n return criterion.fn(quote);\n }\n}\n\n// ---------------------------------------------------------------------------\n// Batch fetching helper\n// ---------------------------------------------------------------------------\n\nasync function fetchInBatches(\n source: QuoteSource,\n symbols: string[],\n batchSize?: number,\n): Promise<Quote[]> {\n if (!batchSize || batchSize >= symbols.length) {\n return source.quote(symbols);\n }\n\n const batches: string[][] = [];\n for (let i = 0; i < symbols.length; i += batchSize) {\n batches.push(symbols.slice(i, i + batchSize));\n }\n\n const results = await Promise.all(batches.map((batch) => source.quote(batch)));\n return results.flat();\n}\n"]}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { a as Quote } from './quote-Cfh_7Cgg.cjs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* market-feed/screener
|
|
5
|
+
*
|
|
6
|
+
* Filter a list of symbols against a set of criteria using live quote data,
|
|
7
|
+
* company profile data, or any custom condition function.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* import { screen } from "market-feed/screener";
|
|
12
|
+
* import { MarketFeed } from "market-feed";
|
|
13
|
+
*
|
|
14
|
+
* const feed = new MarketFeed();
|
|
15
|
+
*
|
|
16
|
+
* const results = await screen(feed, ["AAPL", "MSFT", "GOOGL", "TSLA"], {
|
|
17
|
+
* criteria: [
|
|
18
|
+
* { type: "price_above", value: 100 },
|
|
19
|
+
* { type: "change_pct_above", value: -5 },
|
|
20
|
+
* { type: "volume_above", value: 1_000_000 },
|
|
21
|
+
* ],
|
|
22
|
+
* });
|
|
23
|
+
*
|
|
24
|
+
* console.log(results.map((r) => r.symbol));
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
type QuoteCriterion = {
|
|
29
|
+
type: "price_above";
|
|
30
|
+
value: number;
|
|
31
|
+
} | {
|
|
32
|
+
type: "price_below";
|
|
33
|
+
value: number;
|
|
34
|
+
} | {
|
|
35
|
+
type: "change_pct_above";
|
|
36
|
+
value: number;
|
|
37
|
+
} | {
|
|
38
|
+
type: "change_pct_below";
|
|
39
|
+
value: number;
|
|
40
|
+
} | {
|
|
41
|
+
type: "volume_above";
|
|
42
|
+
value: number;
|
|
43
|
+
} | {
|
|
44
|
+
type: "volume_below";
|
|
45
|
+
value: number;
|
|
46
|
+
} | {
|
|
47
|
+
type: "market_cap_above";
|
|
48
|
+
value: number;
|
|
49
|
+
} | {
|
|
50
|
+
type: "market_cap_below";
|
|
51
|
+
value: number;
|
|
52
|
+
} | {
|
|
53
|
+
type: "pe_above";
|
|
54
|
+
value: number;
|
|
55
|
+
} | {
|
|
56
|
+
type: "pe_below";
|
|
57
|
+
value: number;
|
|
58
|
+
} | {
|
|
59
|
+
type: "52w_high_pct_below";
|
|
60
|
+
value: number;
|
|
61
|
+
} | {
|
|
62
|
+
type: "52w_low_pct_above";
|
|
63
|
+
value: number;
|
|
64
|
+
}
|
|
65
|
+
/** Custom predicate — return true to include, false to exclude */
|
|
66
|
+
| {
|
|
67
|
+
type: "custom";
|
|
68
|
+
fn: (quote: Quote) => boolean;
|
|
69
|
+
};
|
|
70
|
+
type ScreenerCriterion = QuoteCriterion;
|
|
71
|
+
interface ScreenerOptions {
|
|
72
|
+
/** List of criteria — ALL must pass (logical AND) */
|
|
73
|
+
criteria: ScreenerCriterion[];
|
|
74
|
+
/**
|
|
75
|
+
* Maximum number of symbols to fetch quotes for per batch.
|
|
76
|
+
* Defaults to all symbols in one call.
|
|
77
|
+
*/
|
|
78
|
+
batchSize?: number;
|
|
79
|
+
/**
|
|
80
|
+
* Maximum number of results to return.
|
|
81
|
+
* Defaults to all matching symbols.
|
|
82
|
+
*/
|
|
83
|
+
limit?: number;
|
|
84
|
+
}
|
|
85
|
+
interface ScreenerResult {
|
|
86
|
+
symbol: string;
|
|
87
|
+
quote: Quote;
|
|
88
|
+
/** Which criteria matched (all of them, by design) */
|
|
89
|
+
matchedCriteria: number;
|
|
90
|
+
}
|
|
91
|
+
interface QuoteSource {
|
|
92
|
+
quote(symbols: string[], options?: {
|
|
93
|
+
raw?: boolean;
|
|
94
|
+
}): Promise<Quote[]>;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Screen a list of symbols against a set of criteria.
|
|
98
|
+
*
|
|
99
|
+
* Fetches quotes for all symbols (in optional batches) and returns those
|
|
100
|
+
* that satisfy ALL of the provided criteria.
|
|
101
|
+
*
|
|
102
|
+
* @param source - Any object with a `quote(symbols[]) → Quote[]` method (e.g. `MarketFeed`, a single provider)
|
|
103
|
+
* @param symbols - Symbols to evaluate
|
|
104
|
+
* @param options - Screener configuration
|
|
105
|
+
*/
|
|
106
|
+
declare function screen(source: QuoteSource, symbols: string[], options: ScreenerOptions): Promise<ScreenerResult[]>;
|
|
107
|
+
|
|
108
|
+
export { type QuoteCriterion, type ScreenerCriterion, type ScreenerOptions, type ScreenerResult, screen };
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { a as Quote } from './quote-Cfh_7Cgg.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* market-feed/screener
|
|
5
|
+
*
|
|
6
|
+
* Filter a list of symbols against a set of criteria using live quote data,
|
|
7
|
+
* company profile data, or any custom condition function.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* import { screen } from "market-feed/screener";
|
|
12
|
+
* import { MarketFeed } from "market-feed";
|
|
13
|
+
*
|
|
14
|
+
* const feed = new MarketFeed();
|
|
15
|
+
*
|
|
16
|
+
* const results = await screen(feed, ["AAPL", "MSFT", "GOOGL", "TSLA"], {
|
|
17
|
+
* criteria: [
|
|
18
|
+
* { type: "price_above", value: 100 },
|
|
19
|
+
* { type: "change_pct_above", value: -5 },
|
|
20
|
+
* { type: "volume_above", value: 1_000_000 },
|
|
21
|
+
* ],
|
|
22
|
+
* });
|
|
23
|
+
*
|
|
24
|
+
* console.log(results.map((r) => r.symbol));
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
type QuoteCriterion = {
|
|
29
|
+
type: "price_above";
|
|
30
|
+
value: number;
|
|
31
|
+
} | {
|
|
32
|
+
type: "price_below";
|
|
33
|
+
value: number;
|
|
34
|
+
} | {
|
|
35
|
+
type: "change_pct_above";
|
|
36
|
+
value: number;
|
|
37
|
+
} | {
|
|
38
|
+
type: "change_pct_below";
|
|
39
|
+
value: number;
|
|
40
|
+
} | {
|
|
41
|
+
type: "volume_above";
|
|
42
|
+
value: number;
|
|
43
|
+
} | {
|
|
44
|
+
type: "volume_below";
|
|
45
|
+
value: number;
|
|
46
|
+
} | {
|
|
47
|
+
type: "market_cap_above";
|
|
48
|
+
value: number;
|
|
49
|
+
} | {
|
|
50
|
+
type: "market_cap_below";
|
|
51
|
+
value: number;
|
|
52
|
+
} | {
|
|
53
|
+
type: "pe_above";
|
|
54
|
+
value: number;
|
|
55
|
+
} | {
|
|
56
|
+
type: "pe_below";
|
|
57
|
+
value: number;
|
|
58
|
+
} | {
|
|
59
|
+
type: "52w_high_pct_below";
|
|
60
|
+
value: number;
|
|
61
|
+
} | {
|
|
62
|
+
type: "52w_low_pct_above";
|
|
63
|
+
value: number;
|
|
64
|
+
}
|
|
65
|
+
/** Custom predicate — return true to include, false to exclude */
|
|
66
|
+
| {
|
|
67
|
+
type: "custom";
|
|
68
|
+
fn: (quote: Quote) => boolean;
|
|
69
|
+
};
|
|
70
|
+
type ScreenerCriterion = QuoteCriterion;
|
|
71
|
+
interface ScreenerOptions {
|
|
72
|
+
/** List of criteria — ALL must pass (logical AND) */
|
|
73
|
+
criteria: ScreenerCriterion[];
|
|
74
|
+
/**
|
|
75
|
+
* Maximum number of symbols to fetch quotes for per batch.
|
|
76
|
+
* Defaults to all symbols in one call.
|
|
77
|
+
*/
|
|
78
|
+
batchSize?: number;
|
|
79
|
+
/**
|
|
80
|
+
* Maximum number of results to return.
|
|
81
|
+
* Defaults to all matching symbols.
|
|
82
|
+
*/
|
|
83
|
+
limit?: number;
|
|
84
|
+
}
|
|
85
|
+
interface ScreenerResult {
|
|
86
|
+
symbol: string;
|
|
87
|
+
quote: Quote;
|
|
88
|
+
/** Which criteria matched (all of them, by design) */
|
|
89
|
+
matchedCriteria: number;
|
|
90
|
+
}
|
|
91
|
+
interface QuoteSource {
|
|
92
|
+
quote(symbols: string[], options?: {
|
|
93
|
+
raw?: boolean;
|
|
94
|
+
}): Promise<Quote[]>;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Screen a list of symbols against a set of criteria.
|
|
98
|
+
*
|
|
99
|
+
* Fetches quotes for all symbols (in optional batches) and returns those
|
|
100
|
+
* that satisfy ALL of the provided criteria.
|
|
101
|
+
*
|
|
102
|
+
* @param source - Any object with a `quote(symbols[]) → Quote[]` method (e.g. `MarketFeed`, a single provider)
|
|
103
|
+
* @param symbols - Symbols to evaluate
|
|
104
|
+
* @param options - Screener configuration
|
|
105
|
+
*/
|
|
106
|
+
declare function screen(source: QuoteSource, symbols: string[], options: ScreenerOptions): Promise<ScreenerResult[]>;
|
|
107
|
+
|
|
108
|
+
export { type QuoteCriterion, type ScreenerCriterion, type ScreenerOptions, type ScreenerResult, screen };
|
package/dist/screener.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// market-feed — Unified financial market data client
|
|
2
|
+
// https://github.com/piyushgupta344/market-feed
|
|
3
|
+
|
|
4
|
+
// src/screener/index.ts
|
|
5
|
+
async function screen(source, symbols, options) {
|
|
6
|
+
if (symbols.length === 0 || options.criteria.length === 0) return [];
|
|
7
|
+
const { criteria, batchSize, limit } = options;
|
|
8
|
+
const quotes = await fetchInBatches(source, symbols, batchSize);
|
|
9
|
+
const results = [];
|
|
10
|
+
for (const quote of quotes) {
|
|
11
|
+
if (matchesAll(quote, criteria)) {
|
|
12
|
+
results.push({ symbol: quote.symbol, quote, matchedCriteria: criteria.length });
|
|
13
|
+
if (limit !== void 0 && results.length >= limit) break;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return results;
|
|
17
|
+
}
|
|
18
|
+
function matchesAll(quote, criteria) {
|
|
19
|
+
for (const c of criteria) {
|
|
20
|
+
if (!matchesCriterion(quote, c)) return false;
|
|
21
|
+
}
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
function matchesCriterion(quote, criterion) {
|
|
25
|
+
switch (criterion.type) {
|
|
26
|
+
case "price_above":
|
|
27
|
+
return quote.price > criterion.value;
|
|
28
|
+
case "price_below":
|
|
29
|
+
return quote.price < criterion.value;
|
|
30
|
+
case "change_pct_above":
|
|
31
|
+
return quote.changePercent > criterion.value;
|
|
32
|
+
case "change_pct_below":
|
|
33
|
+
return quote.changePercent < criterion.value;
|
|
34
|
+
case "volume_above":
|
|
35
|
+
return quote.volume > criterion.value;
|
|
36
|
+
case "volume_below":
|
|
37
|
+
return quote.volume < criterion.value;
|
|
38
|
+
case "market_cap_above":
|
|
39
|
+
return quote.marketCap !== void 0 && quote.marketCap > criterion.value;
|
|
40
|
+
case "market_cap_below":
|
|
41
|
+
return quote.marketCap !== void 0 && quote.marketCap < criterion.value;
|
|
42
|
+
case "pe_above":
|
|
43
|
+
return true;
|
|
44
|
+
case "pe_below":
|
|
45
|
+
return true;
|
|
46
|
+
case "52w_high_pct_below": {
|
|
47
|
+
if (quote.fiftyTwoWeekHigh === void 0) return true;
|
|
48
|
+
const pctBelow = (quote.fiftyTwoWeekHigh - quote.price) / quote.fiftyTwoWeekHigh * 100;
|
|
49
|
+
return pctBelow < criterion.value;
|
|
50
|
+
}
|
|
51
|
+
case "52w_low_pct_above": {
|
|
52
|
+
if (quote.fiftyTwoWeekLow === void 0) return true;
|
|
53
|
+
const pctAbove = (quote.price - quote.fiftyTwoWeekLow) / quote.fiftyTwoWeekLow * 100;
|
|
54
|
+
return pctAbove > criterion.value;
|
|
55
|
+
}
|
|
56
|
+
case "custom":
|
|
57
|
+
return criterion.fn(quote);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
async function fetchInBatches(source, symbols, batchSize) {
|
|
61
|
+
if (!batchSize || batchSize >= symbols.length) {
|
|
62
|
+
return source.quote(symbols);
|
|
63
|
+
}
|
|
64
|
+
const batches = [];
|
|
65
|
+
for (let i = 0; i < symbols.length; i += batchSize) {
|
|
66
|
+
batches.push(symbols.slice(i, i + batchSize));
|
|
67
|
+
}
|
|
68
|
+
const results = await Promise.all(batches.map((batch) => source.quote(batch)));
|
|
69
|
+
return results.flat();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export { screen };
|
|
73
|
+
//# sourceMappingURL=screener.js.map
|
|
74
|
+
//# sourceMappingURL=screener.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/screener/index.ts"],"names":[],"mappings":"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","file":"screener.js","sourcesContent":["/**\n * market-feed/screener\n *\n * Filter a list of symbols against a set of criteria using live quote data,\n * company profile data, or any custom condition function.\n *\n * @example\n * ```ts\n * import { screen } from \"market-feed/screener\";\n * import { MarketFeed } from \"market-feed\";\n *\n * const feed = new MarketFeed();\n *\n * const results = await screen(feed, [\"AAPL\", \"MSFT\", \"GOOGL\", \"TSLA\"], {\n * criteria: [\n * { type: \"price_above\", value: 100 },\n * { type: \"change_pct_above\", value: -5 },\n * { type: \"volume_above\", value: 1_000_000 },\n * ],\n * });\n *\n * console.log(results.map((r) => r.symbol));\n * ```\n */\n\nimport type { Quote } from \"../types/quote.js\";\n\n// ---------------------------------------------------------------------------\n// Criterion types\n// ---------------------------------------------------------------------------\n\nexport type QuoteCriterion =\n | { type: \"price_above\"; value: number }\n | { type: \"price_below\"; value: number }\n | { type: \"change_pct_above\"; value: number }\n | { type: \"change_pct_below\"; value: number }\n | { type: \"volume_above\"; value: number }\n | { type: \"volume_below\"; value: number }\n | { type: \"market_cap_above\"; value: number }\n | { type: \"market_cap_below\"; value: number }\n | { type: \"pe_above\"; value: number }\n | { type: \"pe_below\"; value: number }\n | { type: \"52w_high_pct_below\"; value: number }\n | { type: \"52w_low_pct_above\"; value: number }\n /** Custom predicate — return true to include, false to exclude */\n | { type: \"custom\"; fn: (quote: Quote) => boolean };\n\nexport type ScreenerCriterion = QuoteCriterion;\n\nexport interface ScreenerOptions {\n /** List of criteria — ALL must pass (logical AND) */\n criteria: ScreenerCriterion[];\n /**\n * Maximum number of symbols to fetch quotes for per batch.\n * Defaults to all symbols in one call.\n */\n batchSize?: number;\n /**\n * Maximum number of results to return.\n * Defaults to all matching symbols.\n */\n limit?: number;\n}\n\nexport interface ScreenerResult {\n symbol: string;\n quote: Quote;\n /** Which criteria matched (all of them, by design) */\n matchedCriteria: number;\n}\n\n// ---------------------------------------------------------------------------\n// Quote source interface (duck-typed for testability)\n// ---------------------------------------------------------------------------\n\ninterface QuoteSource {\n quote(symbols: string[], options?: { raw?: boolean }): Promise<Quote[]>;\n}\n\n// ---------------------------------------------------------------------------\n// Core screener function\n// ---------------------------------------------------------------------------\n\n/**\n * Screen a list of symbols against a set of criteria.\n *\n * Fetches quotes for all symbols (in optional batches) and returns those\n * that satisfy ALL of the provided criteria.\n *\n * @param source - Any object with a `quote(symbols[]) → Quote[]` method (e.g. `MarketFeed`, a single provider)\n * @param symbols - Symbols to evaluate\n * @param options - Screener configuration\n */\nexport async function screen(\n source: QuoteSource,\n symbols: string[],\n options: ScreenerOptions,\n): Promise<ScreenerResult[]> {\n if (symbols.length === 0 || options.criteria.length === 0) return [];\n\n const { criteria, batchSize, limit } = options;\n\n // Fetch quotes in batches (or all at once if no batchSize)\n const quotes = await fetchInBatches(source, symbols, batchSize);\n\n const results: ScreenerResult[] = [];\n for (const quote of quotes) {\n if (matchesAll(quote, criteria)) {\n results.push({ symbol: quote.symbol, quote, matchedCriteria: criteria.length });\n if (limit !== undefined && results.length >= limit) break;\n }\n }\n\n return results;\n}\n\n// ---------------------------------------------------------------------------\n// Criterion evaluator\n// ---------------------------------------------------------------------------\n\nfunction matchesAll(quote: Quote, criteria: ScreenerCriterion[]): boolean {\n for (const c of criteria) {\n if (!matchesCriterion(quote, c)) return false;\n }\n return true;\n}\n\nfunction matchesCriterion(quote: Quote, criterion: ScreenerCriterion): boolean {\n switch (criterion.type) {\n case \"price_above\":\n return quote.price > criterion.value;\n case \"price_below\":\n return quote.price < criterion.value;\n case \"change_pct_above\":\n return quote.changePercent > criterion.value;\n case \"change_pct_below\":\n return quote.changePercent < criterion.value;\n case \"volume_above\":\n return quote.volume > criterion.value;\n case \"volume_below\":\n return quote.volume < criterion.value;\n case \"market_cap_above\":\n return quote.marketCap !== undefined && quote.marketCap > criterion.value;\n case \"market_cap_below\":\n return quote.marketCap !== undefined && quote.marketCap < criterion.value;\n case \"pe_above\":\n // CompanyProfile-level data is not on Quote — skip (return true to not block)\n return true;\n case \"pe_below\":\n return true;\n case \"52w_high_pct_below\": {\n if (quote.fiftyTwoWeekHigh === undefined) return true;\n const pctBelow = ((quote.fiftyTwoWeekHigh - quote.price) / quote.fiftyTwoWeekHigh) * 100;\n return pctBelow < criterion.value;\n }\n case \"52w_low_pct_above\": {\n if (quote.fiftyTwoWeekLow === undefined) return true;\n const pctAbove = ((quote.price - quote.fiftyTwoWeekLow) / quote.fiftyTwoWeekLow) * 100;\n return pctAbove > criterion.value;\n }\n case \"custom\":\n return criterion.fn(quote);\n }\n}\n\n// ---------------------------------------------------------------------------\n// Batch fetching helper\n// ---------------------------------------------------------------------------\n\nasync function fetchInBatches(\n source: QuoteSource,\n symbols: string[],\n batchSize?: number,\n): Promise<Quote[]> {\n if (!batchSize || batchSize >= symbols.length) {\n return source.quote(symbols);\n }\n\n const batches: string[][] = [];\n for (let i = 0; i < symbols.length; i += batchSize) {\n batches.push(symbols.slice(i, i + batchSize));\n }\n\n const results = await Promise.all(batches.map((batch) => source.quote(batch)));\n return results.flat();\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "market-feed",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Unified TypeScript client for financial market data — wraps Yahoo Finance, Alpha Vantage, Polygon.io, and Finnhub under one consistent interface with caching, fallback, and real-time WebSocket streaming.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"finance",
|
|
@@ -83,6 +83,11 @@
|
|
|
83
83
|
"types": "./dist/fundamentals.d.ts",
|
|
84
84
|
"import": "./dist/fundamentals.js",
|
|
85
85
|
"require": "./dist/fundamentals.cjs"
|
|
86
|
+
},
|
|
87
|
+
"./screener": {
|
|
88
|
+
"types": "./dist/screener.d.ts",
|
|
89
|
+
"import": "./dist/screener.js",
|
|
90
|
+
"require": "./dist/screener.cjs"
|
|
86
91
|
}
|
|
87
92
|
},
|
|
88
93
|
"bin": {
|