market-feed 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,86 @@
1
1
  # market-feed Changelog
2
2
 
3
+ ## 0.6.0 — 2026-03-12
4
+
5
+ ### New provider
6
+
7
+ **`TwelveDataProvider`** — Twelve Data (free tier: 800 credits/day, 8 calls/minute).
8
+
9
+ ```ts
10
+ import { MarketFeed, TwelveDataProvider } from "market-feed";
11
+
12
+ const feed = new MarketFeed([
13
+ new TwelveDataProvider({ apiKey: process.env.TWELVE_DATA_KEY! }),
14
+ ]);
15
+
16
+ const quote = await feed.quote(["AAPL", "BTC/USD", "EUR/USD"]);
17
+ const bars = await feed.historical("AAPL", { interval: "1wk" });
18
+ const results = await feed.search("apple");
19
+ const profile = await feed.company("AAPL");
20
+ ```
21
+
22
+ Supports: `quote`, `historical`, `search`, `company`.
23
+
24
+ Strong coverage for global equities, forex, and crypto pairs. Symbol normalisation handles all common formats:
25
+ - US stocks: `AAPL` (unchanged)
26
+ - Crypto: `BTC-USD` / `BTC/USD` / `X:BTCUSD` → `BTC/USD`
27
+ - Forex: `EURUSD=X` / `C:EURUSD` → `EUR/USD`
28
+
29
+ Free plan sign-up: https://twelvedata.com
30
+
31
+ #### Interval mapping
32
+
33
+ | market-feed | Twelve Data |
34
+ |-------------|-------------|
35
+ | `1m` | `1min` |
36
+ | `5m` | `5min` |
37
+ | `15m` | `15min` |
38
+ | `30m` | `30min` |
39
+ | `1h` | `1h` |
40
+ | `1d` | `1day` |
41
+ | `1wk` | `1week` |
42
+ | `1mo` | `1month` |
43
+
44
+ ### New utility
45
+
46
+ **`toTwelveDataSymbol(symbol)`** — exported from main `market-feed` entry point. Converts any supported symbol format (Yahoo/Polygon/standard) to the slash-pair notation that Twelve Data expects for crypto and forex.
47
+
48
+ ### CLI
49
+
50
+ `--td-key <key>` flag adds `TwelveDataProvider` to the CLI provider chain.
51
+
52
+ ### Breaking changes
53
+
54
+ None. All v0.5.x imports continue to work unchanged.
55
+
56
+ ### Other changes
57
+
58
+ - `TwelveDataProvider` and `TwelveDataProviderOptions` exported from main `market-feed` entry point
59
+ - `toTwelveDataSymbol` exported from main `market-feed` entry point
60
+ - 30 new unit tests (456 total across 22 test files)
61
+
62
+ ---
63
+
64
+ ## 0.5.1 — 2026-03-12
65
+
66
+ ### CLI additions
67
+
68
+ Three new commands expose the v0.5.0 corporate-action data from the terminal:
69
+
70
+ ```bash
71
+ market-feed earnings AAPL --limit 8
72
+ market-feed dividends AAPL --from 2020-01-01
73
+ market-feed splits AAPL --json
74
+ ```
75
+
76
+ New flags `--from <date>` and `--to <date>` scope the dividend/split history by date range (ISO 8601). `--limit` and `--json` work on all three commands.
77
+
78
+ ### Breaking changes
79
+
80
+ None.
81
+
82
+ ---
83
+
3
84
  ## 0.5.0 — 2026-03-11
4
85
 
5
86
  ### New modules
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, Polygon.io, and Finnhub under one consistent interface — with caching and automatic fallback built in.
4
+ > Wraps Yahoo Finance, Alpha Vantage, Polygon.io, Finnhub, and Twelve Data under one consistent interface — with caching and automatic fallback built in.
5
5
 
6
6
  [![CI](https://github.com/piyushgupta344/market-feed/actions/workflows/ci.yml/badge.svg)](https://github.com/piyushgupta344/market-feed/actions/workflows/ci.yml)
7
7
  [![npm version](https://badge.fury.io/js/market-feed.svg)](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. Four providers. Zero API key required for Yahoo Finance.
42
+ One interface. Five providers. Zero API key required for Yahoo Finance.
43
43
 
44
44
  ---
45
45
 
package/dist/cli.js CHANGED
@@ -1585,6 +1585,206 @@ function subtractOneYear3() {
1585
1585
  return d;
1586
1586
  }
1587
1587
 
1588
+ // src/providers/twelve-data/transform.ts
1589
+ var PROVIDER5 = "twelve-data";
1590
+ function transformQuote5(data, raw) {
1591
+ return {
1592
+ symbol: data.symbol,
1593
+ name: data.name,
1594
+ price: Number(data.close),
1595
+ change: Number(data.change),
1596
+ changePercent: Number(data.percent_change),
1597
+ open: Number(data.open),
1598
+ high: Number(data.high),
1599
+ low: Number(data.low),
1600
+ close: Number(data.close),
1601
+ previousClose: Number(data.previous_close),
1602
+ volume: Number(data.volume),
1603
+ ...data.average_volume !== void 0 ? { avgVolume: Number(data.average_volume) } : {},
1604
+ ...data.fifty_two_week !== void 0 ? {
1605
+ fiftyTwoWeekHigh: Number(data.fifty_two_week.high),
1606
+ fiftyTwoWeekLow: Number(data.fifty_two_week.low)
1607
+ } : {},
1608
+ currency: data.currency,
1609
+ exchange: data.exchange,
1610
+ timestamp: new Date(data.timestamp * 1e3),
1611
+ provider: PROVIDER5,
1612
+ ...raw !== void 0 ? { raw } : {}
1613
+ };
1614
+ }
1615
+ function transformHistoricalBar2(bar, raw) {
1616
+ return {
1617
+ date: new Date(bar.datetime),
1618
+ open: Number(bar.open),
1619
+ high: Number(bar.high),
1620
+ low: Number(bar.low),
1621
+ close: Number(bar.close),
1622
+ volume: Number(bar.volume),
1623
+ ...raw !== void 0 ? { raw } : {}
1624
+ };
1625
+ }
1626
+ function transformSearch5(result, raw) {
1627
+ return {
1628
+ symbol: result.symbol,
1629
+ name: result.instrument_name,
1630
+ type: mapInstrumentType(result.instrument_type),
1631
+ exchange: result.exchange || void 0,
1632
+ provider: PROVIDER5,
1633
+ ...raw !== void 0 ? { raw } : {}
1634
+ };
1635
+ }
1636
+ function transformProfile(data, raw) {
1637
+ return {
1638
+ symbol: data.symbol,
1639
+ name: data.name,
1640
+ description: data.description || void 0,
1641
+ sector: data.sector || void 0,
1642
+ industry: data.industry || void 0,
1643
+ country: data.country || void 0,
1644
+ employees: data.employees || void 0,
1645
+ website: data.website || void 0,
1646
+ ceo: data.CEO || void 0,
1647
+ exchange: data.exchange || void 0,
1648
+ provider: PROVIDER5,
1649
+ ...raw !== void 0 ? { raw } : {}
1650
+ };
1651
+ }
1652
+ function mapInstrumentType(type) {
1653
+ const t = type.toLowerCase();
1654
+ if (t.includes("common stock") || t === "cs") return "stock";
1655
+ if (t.includes("etf") || t.includes("exchange traded fund")) return "etf";
1656
+ if (t.includes("crypto")) return "crypto";
1657
+ if (t.includes("forex") || t.includes("currency")) return "forex";
1658
+ if (t.includes("mutual fund") || t.includes("fund")) return "mutual-fund";
1659
+ if (t.includes("future")) return "future";
1660
+ if (t.includes("index")) return "index";
1661
+ return "unknown";
1662
+ }
1663
+
1664
+ // src/providers/twelve-data/index.ts
1665
+ var TwelveDataProvider = class {
1666
+ constructor(options) {
1667
+ this.options = options;
1668
+ this.apiKey = options.apiKey;
1669
+ this.http = new HttpClient("twelve-data", {
1670
+ baseUrl: "https://api.twelvedata.com",
1671
+ ...options.timeoutMs !== void 0 ? { timeoutMs: options.timeoutMs } : {},
1672
+ ...options.retries !== void 0 ? { retries: options.retries } : {}
1673
+ });
1674
+ this.limiter = options.rateLimiter ?? new RateLimiter("twelve-data", 8, 8 / 60);
1675
+ }
1676
+ name = "twelve-data";
1677
+ http;
1678
+ limiter;
1679
+ apiKey;
1680
+ // ---------------------------------------------------------------------------
1681
+ // Quote
1682
+ // ---------------------------------------------------------------------------
1683
+ async quote(symbols, options) {
1684
+ return Promise.all(symbols.map((s) => this.fetchSingleQuote(s, options)));
1685
+ }
1686
+ async fetchSingleQuote(symbol, options) {
1687
+ this.limiter.consume();
1688
+ const s = normalise(symbol);
1689
+ const data = await this.http.get("/quote", {
1690
+ params: { symbol: s, apikey: this.apiKey }
1691
+ });
1692
+ if (data.code !== void 0) {
1693
+ throw new ProviderError(
1694
+ data.message ?? `No quote data for symbol "${s}"`,
1695
+ this.name,
1696
+ data.code
1697
+ );
1698
+ }
1699
+ return transformQuote5(data, options?.raw ? data : void 0);
1700
+ }
1701
+ // ---------------------------------------------------------------------------
1702
+ // Historical (time series)
1703
+ // ---------------------------------------------------------------------------
1704
+ async historical(symbol, options) {
1705
+ this.limiter.consume();
1706
+ const s = normalise(symbol);
1707
+ const interval = toInterval(options?.interval ?? "1d");
1708
+ const params = {
1709
+ symbol: s,
1710
+ interval,
1711
+ outputsize: 5e3,
1712
+ apikey: this.apiKey
1713
+ };
1714
+ if (options?.period1 !== void 0) {
1715
+ params["start_date"] = typeof options.period1 === "string" ? options.period1 : options.period1.toISOString().slice(0, 10);
1716
+ }
1717
+ if (options?.period2 !== void 0) {
1718
+ params["end_date"] = typeof options.period2 === "string" ? options.period2 : options.period2.toISOString().slice(0, 10);
1719
+ }
1720
+ const data = await this.http.get("/time_series", { params });
1721
+ if (data.code !== void 0 || data.status === "error") {
1722
+ throw new ProviderError(
1723
+ data.message ?? `No historical data for symbol "${s}"`,
1724
+ this.name,
1725
+ data.code
1726
+ );
1727
+ }
1728
+ if (!Array.isArray(data.values) || data.values.length === 0) {
1729
+ throw new ProviderError(`No historical data for symbol "${s}"`, this.name);
1730
+ }
1731
+ return [...data.values].reverse().map((bar) => transformHistoricalBar2(bar, options?.raw ? bar : void 0));
1732
+ }
1733
+ // ---------------------------------------------------------------------------
1734
+ // Search
1735
+ // ---------------------------------------------------------------------------
1736
+ async search(query, options) {
1737
+ this.limiter.consume();
1738
+ const limit = options?.limit ?? 10;
1739
+ const data = await this.http.get("/symbol_search", {
1740
+ params: { symbol: query, apikey: this.apiKey }
1741
+ });
1742
+ if (data.code !== void 0 || data.status === "error") {
1743
+ return [];
1744
+ }
1745
+ return (data.data ?? []).slice(0, limit).map((r) => transformSearch5(r, options?.raw ? r : void 0));
1746
+ }
1747
+ // ---------------------------------------------------------------------------
1748
+ // Company (profile)
1749
+ // ---------------------------------------------------------------------------
1750
+ async company(symbol, options) {
1751
+ this.limiter.consume();
1752
+ const s = normalise(symbol);
1753
+ const data = await this.http.get("/profile", {
1754
+ params: { symbol: s, apikey: this.apiKey }
1755
+ });
1756
+ if (data.code !== void 0 || !data.name) {
1757
+ throw new ProviderError(
1758
+ data.message ?? `No company data for symbol "${s}"`,
1759
+ this.name,
1760
+ data.code
1761
+ );
1762
+ }
1763
+ return transformProfile(data, options?.raw ? data : void 0);
1764
+ }
1765
+ };
1766
+ function toInterval(interval) {
1767
+ const map = {
1768
+ "1m": "1min",
1769
+ "2m": "2min",
1770
+ "5m": "5min",
1771
+ "15m": "15min",
1772
+ "30m": "30min",
1773
+ "45m": "45min",
1774
+ "60m": "1h",
1775
+ "90m": "90min",
1776
+ "1h": "1h",
1777
+ "2h": "2h",
1778
+ "4h": "4h",
1779
+ "1d": "1day",
1780
+ "5d": "1week",
1781
+ "1wk": "1week",
1782
+ "1mo": "1month",
1783
+ "3mo": "3month"
1784
+ };
1785
+ return map[interval] ?? "1day";
1786
+ }
1787
+
1588
1788
  // src/cli/index.ts
1589
1789
  var HELP = `
1590
1790
  Usage: market-feed <command> [options]
@@ -1595,16 +1795,22 @@ Commands:
1595
1795
  search <query> Search for symbols
1596
1796
  company <symbol> Fetch company profile
1597
1797
  news <symbol> Fetch recent news
1798
+ earnings <symbol> Fetch EPS history (actuals vs. estimates)
1799
+ dividends <symbol> Fetch cash dividend history
1800
+ splits <symbol> Fetch stock split history
1598
1801
 
1599
1802
  Options:
1600
1803
  --av-key <key> Alpha Vantage API key
1601
1804
  --polygon-key <key> Polygon.io API key
1602
1805
  --finnhub-key <key> Finnhub API key
1806
+ --td-key <key> Twelve Data API key
1603
1807
  --json Output raw JSON
1604
1808
  --limit <n> Limit results (default: 10)
1605
1809
  --interval <i> Historical interval: 1m 5m 15m 30m 1h 1d 1wk 1mo (default: 1d)
1606
1810
  --period1 <date> Historical start date (ISO 8601)
1607
1811
  --period2 <date> Historical end date (ISO 8601)
1812
+ --from <date> Dividends/splits start date (ISO 8601)
1813
+ --to <date> Dividends/splits end date (ISO 8601)
1608
1814
  -h, --help Show this help message
1609
1815
 
1610
1816
  Examples:
@@ -1613,6 +1819,9 @@ Examples:
1613
1819
  market-feed search "apple inc"
1614
1820
  market-feed company AAPL --json
1615
1821
  market-feed news AAPL --limit 5
1822
+ market-feed earnings AAPL --limit 8
1823
+ market-feed dividends AAPL --from 2020-01-01
1824
+ market-feed splits AAPL --json
1616
1825
  `.trim();
1617
1826
  function parseArgs(argv) {
1618
1827
  const args = argv.slice(2);
@@ -1641,6 +1850,9 @@ function parseArgs(argv) {
1641
1850
  } else if (arg === "--finnhub-key") {
1642
1851
  result.finnhubKey = args[++i];
1643
1852
  i++;
1853
+ } else if (arg === "--td-key") {
1854
+ result.tdKey = args[++i];
1855
+ i++;
1644
1856
  } else if (arg === "--limit") {
1645
1857
  result.limit = Number(args[++i]) || 10;
1646
1858
  i++;
@@ -1653,6 +1865,12 @@ function parseArgs(argv) {
1653
1865
  } else if (arg === "--period2") {
1654
1866
  result.period2 = args[++i];
1655
1867
  i++;
1868
+ } else if (arg === "--from") {
1869
+ result.from = args[++i];
1870
+ i++;
1871
+ } else if (arg === "--to") {
1872
+ result.to = args[++i];
1873
+ i++;
1656
1874
  } else if (!arg.startsWith("-") && !result.command) {
1657
1875
  result.command = arg;
1658
1876
  i++;
@@ -1666,12 +1884,11 @@ function parseArgs(argv) {
1666
1884
  return result;
1667
1885
  }
1668
1886
  function buildFeed(args) {
1669
- const providers = [
1670
- new YahooProvider()
1671
- ];
1887
+ const providers = [new YahooProvider()];
1672
1888
  if (args.avKey) providers.push(new AlphaVantageProvider({ apiKey: args.avKey }));
1673
1889
  if (args.polygonKey) providers.push(new PolygonProvider({ apiKey: args.polygonKey }));
1674
1890
  if (args.finnhubKey) providers.push(new FinnhubProvider({ apiKey: args.finnhubKey }));
1891
+ if (args.tdKey) providers.push(new TwelveDataProvider({ apiKey: args.tdKey }));
1675
1892
  return new MarketFeed({ providers });
1676
1893
  }
1677
1894
  function pad(s, n) {
@@ -1771,6 +1988,82 @@ async function runNews(feed, symbol, args) {
1771
1988
  console.log("");
1772
1989
  }
1773
1990
  }
1991
+ async function runEarnings(feed, symbol, args) {
1992
+ const events = await feed.earnings(symbol, { limit: args.limit });
1993
+ if (args.json) {
1994
+ console.log(JSON.stringify(events, null, 2));
1995
+ return;
1996
+ }
1997
+ if (events.length === 0) {
1998
+ console.log("No earnings data found.");
1999
+ return;
2000
+ }
2001
+ console.log(
2002
+ `${pad("Date", 12)} ${pad("Period", 12)} ${pad("Actual", 9)} ${pad("Estimate", 9)} ${pad("Surprise%", 10)} Provider`
2003
+ );
2004
+ console.log("-".repeat(70));
2005
+ for (const e of events) {
2006
+ const date = e.date.toISOString().slice(0, 10);
2007
+ const period = e.period ?? "-";
2008
+ const actual = e.epsActual !== void 0 ? fmtNum(e.epsActual, 2) : "-";
2009
+ const estimate = e.epsEstimate !== void 0 ? fmtNum(e.epsEstimate, 2) : "-";
2010
+ const surprise = e.epsSurprisePct !== void 0 ? `${e.epsSurprisePct >= 0 ? "+" : ""}${fmtNum(e.epsSurprisePct, 2)}%` : "-";
2011
+ console.log(
2012
+ `${pad(date, 12)} ${pad(period, 12)} ${pad(actual, 9)} ${pad(estimate, 9)} ${pad(surprise, 10)} ${e.provider}`
2013
+ );
2014
+ }
2015
+ }
2016
+ async function runDividends(feed, symbol, args) {
2017
+ const events = await feed.dividends(symbol, {
2018
+ limit: args.limit,
2019
+ ...args.from ? { from: args.from } : {},
2020
+ ...args.to ? { to: args.to } : {}
2021
+ });
2022
+ if (args.json) {
2023
+ console.log(JSON.stringify(events, null, 2));
2024
+ return;
2025
+ }
2026
+ if (events.length === 0) {
2027
+ console.log("No dividend data found.");
2028
+ return;
2029
+ }
2030
+ console.log(
2031
+ `${pad("Ex Date", 12)} ${pad("Amount", 9)} ${pad("Frequency", 14)} ${pad("Pay Date", 12)} Provider`
2032
+ );
2033
+ console.log("-".repeat(66));
2034
+ for (const e of events) {
2035
+ const exDate = e.exDate.toISOString().slice(0, 10);
2036
+ const amount = `$${fmtNum(e.amount, 4)}`;
2037
+ const freq = e.frequency ?? "-";
2038
+ const payDate = e.payDate ? e.payDate.toISOString().slice(0, 10) : "-";
2039
+ console.log(
2040
+ `${pad(exDate, 12)} ${pad(amount, 9)} ${pad(freq, 14)} ${pad(payDate, 12)} ${e.provider}`
2041
+ );
2042
+ }
2043
+ }
2044
+ async function runSplits(feed, symbol, args) {
2045
+ const events = await feed.splits(symbol, {
2046
+ limit: args.limit,
2047
+ ...args.from ? { from: args.from } : {},
2048
+ ...args.to ? { to: args.to } : {}
2049
+ });
2050
+ if (args.json) {
2051
+ console.log(JSON.stringify(events, null, 2));
2052
+ return;
2053
+ }
2054
+ if (events.length === 0) {
2055
+ console.log("No split data found.");
2056
+ return;
2057
+ }
2058
+ console.log(`${pad("Date", 12)} ${pad("Ratio", 8)} ${pad("Description", 14)} Provider`);
2059
+ console.log("-".repeat(50));
2060
+ for (const e of events) {
2061
+ const date = e.date.toISOString().slice(0, 10);
2062
+ const ratio = fmtNum(e.ratio, 4);
2063
+ const desc = e.description ?? "-";
2064
+ console.log(`${pad(date, 12)} ${pad(ratio, 8)} ${pad(desc, 14)} ${e.provider}`);
2065
+ }
2066
+ }
1774
2067
  async function main() {
1775
2068
  const args = parseArgs(process.argv);
1776
2069
  if (!args.command || args.command === "help") {
@@ -1825,6 +2118,33 @@ async function main() {
1825
2118
  await runNews(feed, symbol, args);
1826
2119
  break;
1827
2120
  }
2121
+ case "earnings": {
2122
+ const [symbol] = args.positionals;
2123
+ if (!symbol) {
2124
+ console.error("Error: earnings requires a symbol\n");
2125
+ process.exit(1);
2126
+ }
2127
+ await runEarnings(feed, symbol, args);
2128
+ break;
2129
+ }
2130
+ case "dividends": {
2131
+ const [symbol] = args.positionals;
2132
+ if (!symbol) {
2133
+ console.error("Error: dividends requires a symbol\n");
2134
+ process.exit(1);
2135
+ }
2136
+ await runDividends(feed, symbol, args);
2137
+ break;
2138
+ }
2139
+ case "splits": {
2140
+ const [symbol] = args.positionals;
2141
+ if (!symbol) {
2142
+ console.error("Error: splits requires a symbol\n");
2143
+ process.exit(1);
2144
+ }
2145
+ await runSplits(feed, symbol, args);
2146
+ break;
2147
+ }
1828
2148
  default: {
1829
2149
  console.error(`Unknown command: "${args.command}"
1830
2150
  `);