From 9c8874b94c5d7f5f24b63fbe0c1ae9c4b48b6582 Mon Sep 17 00:00:00 2001 From: Ame Date: Tue, 10 Mar 2026 22:19:28 +0800 Subject: [PATCH 01/23] refactor(ai-provider): align VercelAIProvider with ClaudeCodeProvider interface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rename _opts → opts in askWithSession; wire opts.systemPrompt to agent cache key so per-call system prompt overrides are respected - Replace ambiguous 'engine' source tag with 'vercel-ai' across provider, session type, default value, web-plugin, and engine.spec - Clean up AskOptions comments: document per-provider behavior instead of "Claude Code only" annotations Co-Authored-By: Claude Sonnet 4.6 --- .../vercel-ai-sdk/vercel-provider.ts | 20 +++++++++++-------- src/connectors/web/web-plugin.ts | 2 +- src/core/ai-provider.ts | 18 ++++++++++++++--- src/core/engine.spec.ts | 2 +- src/core/session.ts | 4 ++-- 5 files changed, 31 insertions(+), 15 deletions(-) diff --git a/src/ai-providers/vercel-ai-sdk/vercel-provider.ts b/src/ai-providers/vercel-ai-sdk/vercel-provider.ts index 961edf14..c692579c 100644 --- a/src/ai-providers/vercel-ai-sdk/vercel-provider.ts +++ b/src/ai-providers/vercel-ai-sdk/vercel-provider.ts @@ -21,6 +21,7 @@ import { createAgent } from './agent.js' export class VercelAIProvider implements AIProvider { private cachedKey: string | null = null private cachedToolCount: number = 0 + private cachedSystemPrompt: string | null = null private cachedAgent: Agent | null = null constructor( @@ -30,22 +31,24 @@ export class VercelAIProvider implements AIProvider { private compaction: CompactionConfig, ) {} - /** Lazily create or return the cached agent, re-creating when config or tools change. */ - private async resolveAgent(): Promise { + /** Lazily create or return the cached agent, re-creating when config, tools, or system prompt change. */ + private async resolveAgent(systemPrompt?: string): Promise { const { model, key } = await createModelFromConfig() const tools = await this.getTools() const toolCount = Object.keys(tools).length - if (key !== this.cachedKey || toolCount !== this.cachedToolCount) { - this.cachedAgent = createAgent(model, tools, this.instructions, this.maxSteps) + const effectivePrompt = systemPrompt ?? null + if (key !== this.cachedKey || toolCount !== this.cachedToolCount || effectivePrompt !== this.cachedSystemPrompt) { + this.cachedAgent = createAgent(model, tools, systemPrompt ?? this.instructions, this.maxSteps) this.cachedKey = key this.cachedToolCount = toolCount + this.cachedSystemPrompt = effectivePrompt console.log(`vercel-ai: model loaded → ${key} (${toolCount} tools)`) } return this.cachedAgent! } async ask(prompt: string): Promise { - const agent = await this.resolveAgent() + const agent = await this.resolveAgent(undefined) const media: MediaAttachment[] = [] const result = await agent.generate({ prompt, @@ -58,8 +61,9 @@ export class VercelAIProvider implements AIProvider { return { text: result.text ?? '', media } } - async askWithSession(prompt: string, session: SessionStore, _opts?: AskOptions): Promise { - const agent = await this.resolveAgent() + async askWithSession(prompt: string, session: SessionStore, opts?: AskOptions): Promise { + // historyPreamble and maxHistoryEntries are not used: Vercel passes native ModelMessage[] with no text wrapping needed. + const agent = await this.resolveAgent(opts?.systemPrompt) await session.appendUser(prompt, 'human') @@ -86,7 +90,7 @@ export class VercelAIProvider implements AIProvider { }) const text = result.text ?? '' - await session.appendAssistant(text, 'engine') + await session.appendAssistant(text, 'vercel-ai') return { text, media } } } diff --git a/src/connectors/web/web-plugin.ts b/src/connectors/web/web-plugin.ts index 0b8a6844..8bb78185 100644 --- a/src/connectors/web/web-plugin.ts +++ b/src/connectors/web/web-plugin.ts @@ -116,7 +116,7 @@ export class WebPlugin implements Plugin { { type: 'text', text: payload.text }, ...media.map((m) => ({ type: 'image' as const, url: m.url })), ] - await session.appendAssistant(blocks, 'engine', { + await session.appendAssistant(blocks, 'vercel-ai', { kind: payload.kind, source: payload.source, }) diff --git a/src/core/ai-provider.ts b/src/core/ai-provider.ts index 756596a6..f3d047bc 100644 --- a/src/core/ai-provider.ts +++ b/src/core/ai-provider.ts @@ -13,11 +13,23 @@ import { readAIProviderConfig } from './config.js' // ==================== Types ==================== export interface AskOptions { - /** Preamble text inside block (Claude Code only). */ + /** + * Preamble text describing the conversation context. + * Claude Code: injected inside the `` text block. + * Vercel AI SDK: not used (native ModelMessage[] carries the history directly). + */ historyPreamble?: string - /** System prompt override (Claude Code only). */ + /** + * System prompt override for this call. + * Claude Code: passed as `--system-prompt` to the CLI. + * Vercel AI SDK: replaces the agent's `instructions` for this call (triggers agent re-creation if changed). + */ systemPrompt?: string - /** Max text history entries in . Default: 50 (Claude Code only). */ + /** + * Max text history entries to include in context. + * Claude Code: limits entries in the `` block. Default: 50. + * Vercel AI SDK: not used (compaction via `compactIfNeeded` controls context size). + */ maxHistoryEntries?: number } diff --git a/src/core/engine.spec.ts b/src/core/engine.spec.ts index 5fc20047..304bf202 100644 --- a/src/core/engine.spec.ts +++ b/src/core/engine.spec.ts @@ -181,7 +181,7 @@ describe('Engine', () => { await engine.askWithSession('hello', session) - expect(session.appendAssistant).toHaveBeenCalledWith('assistant reply', 'engine') + expect(session.appendAssistant).toHaveBeenCalledWith('assistant reply', 'vercel-ai') }) it('returns the generated text and empty media', async () => { diff --git a/src/core/session.ts b/src/core/session.ts index 6bdea9ee..4155140e 100644 --- a/src/core/session.ts +++ b/src/core/session.ts @@ -36,7 +36,7 @@ export interface SessionEntry { sessionId: string timestamp: string /** Which provider generated this entry. */ - provider?: 'engine' | 'claude-code' | 'human' | 'compaction' + provider?: 'vercel-ai' | 'claude-code' | 'human' | 'compaction' cwd?: string /** Arbitrary metadata attached to the entry (e.g. { kind: 'notification', source: 'heartbeat' }). */ metadata?: Record @@ -87,7 +87,7 @@ export class SessionStore { /** Append an assistant message to the session. */ async appendAssistant( content: string | ContentBlock[], - provider: SessionEntry['provider'] = 'engine', + provider: SessionEntry['provider'] = 'vercel-ai', metadata?: Record, ): Promise { return this.append({ From d1b988b3fffc7a0a64746fc38eeca67d9a6bd866 Mon Sep 17 00:00:00 2001 From: Ame Date: Wed, 11 Mar 2026 16:29:27 +0800 Subject: [PATCH 02/23] feat: replace Python OpenBB sidecar with in-process OpenTypeBB SDK Vendor OpenTypeBB (TypeScript port of OpenBB, 11 providers, 114 fetcher models, 109 routes) into packages/opentypebb/ and wire it as SDK clients, eliminating the Python sidecar dependency (localhost:6900). - Add packages/opentypebb/ as vendored dependency (link: protocol) - Add SDK infrastructure: executor singleton, route-map builder, base-client - Add 6 SDK clients (128 methods) mirroring the HTTP client interfaces - Add duck-typed interfaces (EquityClientLike etc.) for adapter compatibility - Add buildSDKCredentials() for credential mapping to executor format - Switch main.ts from HTTP clients to SDK clients (~10 lines) - Extension adapters unchanged (zero logic changes, only import types) Co-Authored-By: Claude Opus 4.6 --- package.json | 3 +- packages/opentypebb/.gitignore | 4 + packages/opentypebb/package.json | 41 + packages/opentypebb/pnpm-lock.yaml | 1572 +++++++++++++++++ .../opentypebb/src/core/api/app-loader.ts | 80 + packages/opentypebb/src/core/api/rest-api.ts | 45 + .../opentypebb/src/core/app/command-runner.ts | 30 + .../src/core/app/model/credentials.ts | 32 + .../opentypebb/src/core/app/model/metadata.ts | 31 + .../opentypebb/src/core/app/model/obbject.ts | 102 ++ packages/opentypebb/src/core/app/query.ts | 76 + packages/opentypebb/src/core/app/router.ts | 218 +++ .../src/core/provider/abstract/data.ts | 37 + .../src/core/provider/abstract/fetcher.ts | 102 ++ .../src/core/provider/abstract/provider.ts | 64 + .../core/provider/abstract/query-params.ts | 25 + .../src/core/provider/query-executor.ts | 98 + .../opentypebb/src/core/provider/registry.ts | 24 + .../src/core/provider/utils/errors.ts | 32 + .../src/core/provider/utils/helpers.ts | 144 ++ packages/opentypebb/src/core/utils/proxy.ts | 20 + .../src/extensions/crypto/crypto-router.ts | 27 + .../extensions/crypto/price/price-router.ts | 20 + .../extensions/currency/currency-router.ts | 45 + .../extensions/currency/price/price-router.ts | 20 + .../derivatives/derivatives-router.ts | 74 + .../src/extensions/economy/economy-router.ts | 128 ++ .../equity/calendar/calendar-router.ts | 56 + .../equity/compare/compare-router.ts | 38 + .../equity/discovery/discovery-router.ts | 101 ++ .../src/extensions/equity/equity-router.ts | 78 + .../equity/estimates/estimates-router.ts | 83 + .../equity/fundamental/fundamental-router.ts | 236 +++ .../equity/ownership/ownership-router.ts | 65 + .../extensions/equity/price/price-router.ts | 47 + .../src/extensions/etf/etf-router.ts | 74 + .../src/extensions/index/index-router.ts | 83 + .../src/extensions/news/news-router.ts | 29 + packages/opentypebb/src/index.ts | 53 + .../opentypebb/src/providers/cboe/index.ts | 22 + .../src/providers/cboe/models/index-search.ts | 118 ++ .../opentypebb/src/providers/deribit/index.ts | 26 + .../providers/deribit/models/futures-curve.ts | 84 + .../providers/deribit/models/futures-info.ts | 97 + .../deribit/models/futures-instruments.ts | 70 + .../deribit/models/options-chains.ts | 109 ++ .../src/providers/deribit/utils/helpers.ts | 128 ++ .../opentypebb/src/providers/ecb/index.ts | 16 + .../ecb/models/balance-of-payments.ts | 120 ++ .../opentypebb/src/providers/econdb/index.ts | 23 + .../econdb/models/available-indicators.ts | 56 + .../econdb/models/country-profile.ts | 73 + .../econdb/models/economic-indicators.ts | 79 + .../econdb/models/export-destinations.ts | 61 + .../src/providers/federal_reserve/index.ts | 17 + .../models/central-bank-holdings.ts | 88 + .../opentypebb/src/providers/fmp/index.ts | 150 ++ .../src/providers/fmp/models/active.ts | 49 + .../providers/fmp/models/analyst-estimates.ts | 78 + .../providers/fmp/models/available-indices.ts | 38 + .../fmp/models/balance-sheet-growth.ts | 125 ++ .../src/providers/fmp/models/balance-sheet.ts | 172 ++ .../providers/fmp/models/calendar-dividend.ts | 74 + .../providers/fmp/models/calendar-earnings.ts | 106 ++ .../src/providers/fmp/models/calendar-ipo.ts | 68 + .../providers/fmp/models/calendar-splits.ts | 48 + .../providers/fmp/models/cash-flow-growth.ts | 121 ++ .../src/providers/fmp/models/cash-flow.ts | 143 ++ .../providers/fmp/models/company-filings.ts | 79 + .../src/providers/fmp/models/company-news.ts | 91 + .../providers/fmp/models/crypto-historical.ts | 60 + .../src/providers/fmp/models/crypto-search.ts | 51 + .../fmp/models/currency-historical.ts | 57 + .../providers/fmp/models/currency-pairs.ts | 62 + .../fmp/models/currency-snapshots.ts | 135 ++ .../providers/fmp/models/discovery-filings.ts | 81 + .../fmp/models/earnings-call-transcript.ts | 95 + .../providers/fmp/models/economic-calendar.ts | 75 + .../providers/fmp/models/equity-historical.ts | 89 + .../src/providers/fmp/models/equity-peers.ts | 53 + .../providers/fmp/models/equity-profile.ts | 120 ++ .../src/providers/fmp/models/equity-quote.ts | 101 ++ .../providers/fmp/models/equity-screener.ts | 134 ++ .../src/providers/fmp/models/esg-score.ts | 52 + .../src/providers/fmp/models/etf-countries.ts | 63 + .../fmp/models/etf-equity-exposure.ts | 59 + .../src/providers/fmp/models/etf-holdings.ts | 72 + .../src/providers/fmp/models/etf-info.ts | 72 + .../src/providers/fmp/models/etf-search.ts | 70 + .../src/providers/fmp/models/etf-sectors.ts | 47 + .../fmp/models/executive-compensation.ts | 103 ++ .../providers/fmp/models/financial-ratios.ts | 159 ++ .../fmp/models/forward-ebitda-estimates.ts | 71 + .../fmp/models/forward-eps-estimates.ts | 72 + .../src/providers/fmp/models/gainers.ts | 49 + .../providers/fmp/models/government-trades.ts | 176 ++ .../fmp/models/historical-dividends.ts | 63 + .../fmp/models/historical-employees.ts | 56 + .../providers/fmp/models/historical-eps.ts | 58 + .../fmp/models/historical-market-cap.ts | 54 + .../providers/fmp/models/historical-splits.ts | 38 + .../fmp/models/income-statement-growth.ts | 106 ++ .../providers/fmp/models/income-statement.ts | 125 ++ .../fmp/models/index-constituents.ts | 70 + .../providers/fmp/models/index-historical.ts | 81 + .../providers/fmp/models/insider-trading.ts | 102 ++ .../fmp/models/institutional-ownership.ts | 136 ++ .../providers/fmp/models/key-executives.ts | 39 + .../src/providers/fmp/models/key-metrics.ts | 135 ++ .../src/providers/fmp/models/losers.ts | 49 + .../providers/fmp/models/market-snapshots.ts | 117 ++ .../providers/fmp/models/price-performance.ts | 77 + .../fmp/models/price-target-consensus.ts | 76 + .../src/providers/fmp/models/price-target.ts | 62 + .../fmp/models/revenue-business-line.ts | 79 + .../fmp/models/revenue-geographic.ts | 79 + .../src/providers/fmp/models/risk-premium.ts | 38 + .../providers/fmp/models/share-statistics.ts | 55 + .../providers/fmp/models/treasury-rates.ts | 105 ++ .../src/providers/fmp/models/world-news.ts | 80 + .../src/providers/fmp/utils/definitions.ts | 35 + .../src/providers/fmp/utils/helpers.ts | 291 +++ .../opentypebb/src/providers/imf/index.ts | 22 + .../imf/models/available-indicators.ts | 58 + .../imf/models/consumer-price-index.ts | 95 + .../imf/models/direction-of-trade.ts | 106 ++ .../imf/models/economic-indicators.ts | 90 + .../src/providers/intrinio/index.ts | 19 + .../intrinio/models/options-snapshots.ts | 65 + .../intrinio/models/options-unusual.ts | 56 + .../opentypebb/src/providers/multpl/index.ts | 16 + .../multpl/models/sp500-multiples.ts | 183 ++ .../opentypebb/src/providers/oecd/index.ts | 20 + .../models/composite-leading-indicator.ts | 109 ++ .../oecd/models/consumer-price-index.ts | 118 ++ .../oecd/models/country-interest-rates.ts | 110 ++ .../src/providers/yfinance/index.ts | 82 + .../src/providers/yfinance/models/active.ts | 51 + .../yfinance/models/aggressive-small-caps.ts | 51 + .../yfinance/models/available-indices.ts | 49 + .../yfinance/models/balance-sheet.ts | 63 + .../providers/yfinance/models/cash-flow.ts | 59 + .../providers/yfinance/models/company-news.ts | 73 + .../yfinance/models/crypto-historical.ts | 75 + .../yfinance/models/crypto-search.ts | 48 + .../yfinance/models/currency-historical.ts | 75 + .../yfinance/models/currency-search.ts | 48 + .../yfinance/models/equity-historical.ts | 74 + .../yfinance/models/equity-profile.ts | 92 + .../providers/yfinance/models/equity-quote.ts | 85 + .../yfinance/models/equity-screener.ts | 98 + .../src/providers/yfinance/models/etf-info.ts | 148 ++ .../yfinance/models/futures-curve.ts | 168 ++ .../yfinance/models/futures-historical.ts | 115 ++ .../src/providers/yfinance/models/gainers.ts | 52 + .../providers/yfinance/models/growth-tech.ts | 51 + .../yfinance/models/historical-dividends.ts | 41 + .../yfinance/models/income-statement.ts | 62 + .../yfinance/models/index-historical.ts | 126 ++ .../yfinance/models/key-executives.ts | 72 + .../providers/yfinance/models/key-metrics.ts | 128 ++ .../src/providers/yfinance/models/losers.ts | 51 + .../yfinance/models/options-chains.ts | 230 +++ .../yfinance/models/price-target-consensus.ts | 66 + .../yfinance/models/share-statistics.ts | 114 ++ .../yfinance/models/undervalued-growth.ts | 51 + .../yfinance/models/undervalued-large-caps.ts | 51 + .../src/providers/yfinance/utils/helpers.ts | 481 +++++ .../providers/yfinance/utils/references.ts | 401 +++++ packages/opentypebb/src/server.ts | 42 + .../src/standard-models/analyst-estimates.ts | 43 + .../standard-models/available-indicators.ts | 21 + .../src/standard-models/available-indices.ts | 19 + .../standard-models/balance-of-payments.ts | 27 + .../standard-models/balance-sheet-growth.ts | 25 + .../src/standard-models/balance-sheet.ts | 21 + .../src/standard-models/calendar-dividend.ts | 29 + .../src/standard-models/calendar-earnings.ts | 23 + .../src/standard-models/calendar-ipo.ts | 26 + .../src/standard-models/calendar-splits.ts | 26 + .../src/standard-models/cash-flow-growth.ts | 25 + .../src/standard-models/cash-flow.ts | 21 + .../standard-models/central-bank-holdings.ts | 18 + .../src/standard-models/company-filings.ts | 18 + .../src/standard-models/company-news.ts | 28 + .../composite-leading-indicator.ts | 21 + .../standard-models/consumer-price-index.ts | 25 + .../standard-models/country-interest-rates.ts | 22 + .../src/standard-models/country-profile.ts | 31 + .../src/standard-models/crypto-historical.ts | 26 + .../src/standard-models/crypto-search.ts | 19 + .../standard-models/currency-historical.ts | 26 + .../src/standard-models/currency-pairs.ts | 19 + .../src/standard-models/currency-snapshots.ts | 30 + .../src/standard-models/direction-of-trade.ts | 29 + .../src/standard-models/discovery-filings.ts | 26 + .../earnings-call-transcript.ts | 24 + .../src/standard-models/economic-calendar.ts | 34 + .../standard-models/economic-indicators.ts | 26 + .../src/standard-models/equity-discovery.ts | 27 + .../src/standard-models/equity-historical.ts | 26 + .../src/standard-models/equity-info.ts | 55 + .../src/standard-models/equity-peers.ts | 16 + .../src/standard-models/equity-performance.ts | 23 + .../src/standard-models/equity-quote.ts | 38 + .../src/standard-models/equity-screener.ts | 15 + .../src/standard-models/esg-score.ts | 30 + .../src/standard-models/etf-countries.ts | 18 + .../standard-models/etf-equity-exposure.ts | 20 + .../src/standard-models/etf-holdings.ts | 17 + .../src/standard-models/etf-info.ts | 22 + .../src/standard-models/etf-search.ts | 17 + .../src/standard-models/etf-sectors.ts | 18 + .../standard-models/executive-compensation.ts | 30 + .../standard-models/export-destinations.ts | 20 + .../src/standard-models/financial-ratios.ts | 22 + .../forward-ebitda-estimates.ts | 35 + .../standard-models/forward-eps-estimates.ts | 34 + .../src/standard-models/futures-curve.ts | 21 + .../src/standard-models/futures-historical.ts | 26 + .../src/standard-models/futures-info.ts | 16 + .../standard-models/futures-instruments.ts | 14 + .../src/standard-models/government-trades.ts | 21 + .../standard-models/historical-dividends.ts | 20 + .../standard-models/historical-employees.ts | 20 + .../src/standard-models/historical-eps.ts | 21 + .../standard-models/historical-market-cap.ts | 22 + .../src/standard-models/historical-splits.ts | 19 + .../income-statement-growth.ts | 25 + .../src/standard-models/income-statement.ts | 21 + .../src/standard-models/index-constituents.ts | 19 + .../src/standard-models/index-historical.ts | 28 + .../src/standard-models/index-search.ts | 20 + .../src/standard-models/index-sectors.ts | 19 + .../opentypebb/src/standard-models/index.ts | 578 ++++++ .../src/standard-models/insider-trading.ts | 33 + .../institutional-ownership.ts | 18 + .../src/standard-models/key-executives.ts | 21 + .../src/standard-models/key-metrics.ts | 23 + .../src/standard-models/market-snapshots.ts | 28 + .../src/standard-models/options-chains.ts | 54 + .../src/standard-models/options-snapshots.ts | 32 + .../src/standard-models/options-unusual.ts | 19 + .../standard-models/price-target-consensus.ts | 23 + .../src/standard-models/price-target.ts | 38 + .../src/standard-models/recent-performance.ts | 36 + .../standard-models/revenue-business-line.ts | 23 + .../src/standard-models/revenue-geographic.ts | 23 + .../src/standard-models/risk-premium.ts | 19 + .../src/standard-models/share-statistics.ts | 22 + .../src/standard-models/sp500-multiples.ts | 22 + .../src/standard-models/treasury-rates.ts | 34 + .../src/standard-models/world-news.ts | 26 + packages/opentypebb/tsconfig.json | 20 + packages/opentypebb/tsup.config.ts | 13 + pnpm-lock.yaml | 3 + src/extension/analysis-kit/adapter.ts | 16 +- src/extension/equity/adapter.ts | 4 +- src/extension/market/adapter.ts | 7 +- src/extension/news/adapter.ts | 4 +- src/main.ts | 27 +- src/openbb/credential-map.ts | 18 + src/openbb/equity/SymbolIndex.ts | 6 +- src/openbb/sdk/base-client.ts | 45 + src/openbb/sdk/commodity-client.ts | 36 + src/openbb/sdk/crypto-client.ts | 17 + src/openbb/sdk/currency-client.ts | 25 + src/openbb/sdk/economy-client.ts | 187 ++ src/openbb/sdk/equity-client.ts | 306 ++++ src/openbb/sdk/executor.ts | 16 + src/openbb/sdk/index.ts | 16 + src/openbb/sdk/news-client.ts | 17 + src/openbb/sdk/route-map.ts | 28 + src/openbb/sdk/types.ts | 38 + 274 files changed, 18919 insertions(+), 35 deletions(-) create mode 100644 packages/opentypebb/.gitignore create mode 100644 packages/opentypebb/package.json create mode 100644 packages/opentypebb/pnpm-lock.yaml create mode 100644 packages/opentypebb/src/core/api/app-loader.ts create mode 100644 packages/opentypebb/src/core/api/rest-api.ts create mode 100644 packages/opentypebb/src/core/app/command-runner.ts create mode 100644 packages/opentypebb/src/core/app/model/credentials.ts create mode 100644 packages/opentypebb/src/core/app/model/metadata.ts create mode 100644 packages/opentypebb/src/core/app/model/obbject.ts create mode 100644 packages/opentypebb/src/core/app/query.ts create mode 100644 packages/opentypebb/src/core/app/router.ts create mode 100644 packages/opentypebb/src/core/provider/abstract/data.ts create mode 100644 packages/opentypebb/src/core/provider/abstract/fetcher.ts create mode 100644 packages/opentypebb/src/core/provider/abstract/provider.ts create mode 100644 packages/opentypebb/src/core/provider/abstract/query-params.ts create mode 100644 packages/opentypebb/src/core/provider/query-executor.ts create mode 100644 packages/opentypebb/src/core/provider/registry.ts create mode 100644 packages/opentypebb/src/core/provider/utils/errors.ts create mode 100644 packages/opentypebb/src/core/provider/utils/helpers.ts create mode 100644 packages/opentypebb/src/core/utils/proxy.ts create mode 100644 packages/opentypebb/src/extensions/crypto/crypto-router.ts create mode 100644 packages/opentypebb/src/extensions/crypto/price/price-router.ts create mode 100644 packages/opentypebb/src/extensions/currency/currency-router.ts create mode 100644 packages/opentypebb/src/extensions/currency/price/price-router.ts create mode 100644 packages/opentypebb/src/extensions/derivatives/derivatives-router.ts create mode 100644 packages/opentypebb/src/extensions/economy/economy-router.ts create mode 100644 packages/opentypebb/src/extensions/equity/calendar/calendar-router.ts create mode 100644 packages/opentypebb/src/extensions/equity/compare/compare-router.ts create mode 100644 packages/opentypebb/src/extensions/equity/discovery/discovery-router.ts create mode 100644 packages/opentypebb/src/extensions/equity/equity-router.ts create mode 100644 packages/opentypebb/src/extensions/equity/estimates/estimates-router.ts create mode 100644 packages/opentypebb/src/extensions/equity/fundamental/fundamental-router.ts create mode 100644 packages/opentypebb/src/extensions/equity/ownership/ownership-router.ts create mode 100644 packages/opentypebb/src/extensions/equity/price/price-router.ts create mode 100644 packages/opentypebb/src/extensions/etf/etf-router.ts create mode 100644 packages/opentypebb/src/extensions/index/index-router.ts create mode 100644 packages/opentypebb/src/extensions/news/news-router.ts create mode 100644 packages/opentypebb/src/index.ts create mode 100644 packages/opentypebb/src/providers/cboe/index.ts create mode 100644 packages/opentypebb/src/providers/cboe/models/index-search.ts create mode 100644 packages/opentypebb/src/providers/deribit/index.ts create mode 100644 packages/opentypebb/src/providers/deribit/models/futures-curve.ts create mode 100644 packages/opentypebb/src/providers/deribit/models/futures-info.ts create mode 100644 packages/opentypebb/src/providers/deribit/models/futures-instruments.ts create mode 100644 packages/opentypebb/src/providers/deribit/models/options-chains.ts create mode 100644 packages/opentypebb/src/providers/deribit/utils/helpers.ts create mode 100644 packages/opentypebb/src/providers/ecb/index.ts create mode 100644 packages/opentypebb/src/providers/ecb/models/balance-of-payments.ts create mode 100644 packages/opentypebb/src/providers/econdb/index.ts create mode 100644 packages/opentypebb/src/providers/econdb/models/available-indicators.ts create mode 100644 packages/opentypebb/src/providers/econdb/models/country-profile.ts create mode 100644 packages/opentypebb/src/providers/econdb/models/economic-indicators.ts create mode 100644 packages/opentypebb/src/providers/econdb/models/export-destinations.ts create mode 100644 packages/opentypebb/src/providers/federal_reserve/index.ts create mode 100644 packages/opentypebb/src/providers/federal_reserve/models/central-bank-holdings.ts create mode 100644 packages/opentypebb/src/providers/fmp/index.ts create mode 100644 packages/opentypebb/src/providers/fmp/models/active.ts create mode 100644 packages/opentypebb/src/providers/fmp/models/analyst-estimates.ts create mode 100644 packages/opentypebb/src/providers/fmp/models/available-indices.ts create mode 100644 packages/opentypebb/src/providers/fmp/models/balance-sheet-growth.ts create mode 100644 packages/opentypebb/src/providers/fmp/models/balance-sheet.ts create mode 100644 packages/opentypebb/src/providers/fmp/models/calendar-dividend.ts create mode 100644 packages/opentypebb/src/providers/fmp/models/calendar-earnings.ts create mode 100644 packages/opentypebb/src/providers/fmp/models/calendar-ipo.ts create mode 100644 packages/opentypebb/src/providers/fmp/models/calendar-splits.ts create mode 100644 packages/opentypebb/src/providers/fmp/models/cash-flow-growth.ts create mode 100644 packages/opentypebb/src/providers/fmp/models/cash-flow.ts create mode 100644 packages/opentypebb/src/providers/fmp/models/company-filings.ts create mode 100644 packages/opentypebb/src/providers/fmp/models/company-news.ts create mode 100644 packages/opentypebb/src/providers/fmp/models/crypto-historical.ts create mode 100644 packages/opentypebb/src/providers/fmp/models/crypto-search.ts create mode 100644 packages/opentypebb/src/providers/fmp/models/currency-historical.ts create mode 100644 packages/opentypebb/src/providers/fmp/models/currency-pairs.ts create mode 100644 packages/opentypebb/src/providers/fmp/models/currency-snapshots.ts create mode 100644 packages/opentypebb/src/providers/fmp/models/discovery-filings.ts create mode 100644 packages/opentypebb/src/providers/fmp/models/earnings-call-transcript.ts create mode 100644 packages/opentypebb/src/providers/fmp/models/economic-calendar.ts create mode 100644 packages/opentypebb/src/providers/fmp/models/equity-historical.ts create mode 100644 packages/opentypebb/src/providers/fmp/models/equity-peers.ts create mode 100644 packages/opentypebb/src/providers/fmp/models/equity-profile.ts create mode 100644 packages/opentypebb/src/providers/fmp/models/equity-quote.ts create mode 100644 packages/opentypebb/src/providers/fmp/models/equity-screener.ts create mode 100644 packages/opentypebb/src/providers/fmp/models/esg-score.ts create mode 100644 packages/opentypebb/src/providers/fmp/models/etf-countries.ts create mode 100644 packages/opentypebb/src/providers/fmp/models/etf-equity-exposure.ts create mode 100644 packages/opentypebb/src/providers/fmp/models/etf-holdings.ts create mode 100644 packages/opentypebb/src/providers/fmp/models/etf-info.ts create mode 100644 packages/opentypebb/src/providers/fmp/models/etf-search.ts create mode 100644 packages/opentypebb/src/providers/fmp/models/etf-sectors.ts create mode 100644 packages/opentypebb/src/providers/fmp/models/executive-compensation.ts create mode 100644 packages/opentypebb/src/providers/fmp/models/financial-ratios.ts create mode 100644 packages/opentypebb/src/providers/fmp/models/forward-ebitda-estimates.ts create mode 100644 packages/opentypebb/src/providers/fmp/models/forward-eps-estimates.ts create mode 100644 packages/opentypebb/src/providers/fmp/models/gainers.ts create mode 100644 packages/opentypebb/src/providers/fmp/models/government-trades.ts create mode 100644 packages/opentypebb/src/providers/fmp/models/historical-dividends.ts create mode 100644 packages/opentypebb/src/providers/fmp/models/historical-employees.ts create mode 100644 packages/opentypebb/src/providers/fmp/models/historical-eps.ts create mode 100644 packages/opentypebb/src/providers/fmp/models/historical-market-cap.ts create mode 100644 packages/opentypebb/src/providers/fmp/models/historical-splits.ts create mode 100644 packages/opentypebb/src/providers/fmp/models/income-statement-growth.ts create mode 100644 packages/opentypebb/src/providers/fmp/models/income-statement.ts create mode 100644 packages/opentypebb/src/providers/fmp/models/index-constituents.ts create mode 100644 packages/opentypebb/src/providers/fmp/models/index-historical.ts create mode 100644 packages/opentypebb/src/providers/fmp/models/insider-trading.ts create mode 100644 packages/opentypebb/src/providers/fmp/models/institutional-ownership.ts create mode 100644 packages/opentypebb/src/providers/fmp/models/key-executives.ts create mode 100644 packages/opentypebb/src/providers/fmp/models/key-metrics.ts create mode 100644 packages/opentypebb/src/providers/fmp/models/losers.ts create mode 100644 packages/opentypebb/src/providers/fmp/models/market-snapshots.ts create mode 100644 packages/opentypebb/src/providers/fmp/models/price-performance.ts create mode 100644 packages/opentypebb/src/providers/fmp/models/price-target-consensus.ts create mode 100644 packages/opentypebb/src/providers/fmp/models/price-target.ts create mode 100644 packages/opentypebb/src/providers/fmp/models/revenue-business-line.ts create mode 100644 packages/opentypebb/src/providers/fmp/models/revenue-geographic.ts create mode 100644 packages/opentypebb/src/providers/fmp/models/risk-premium.ts create mode 100644 packages/opentypebb/src/providers/fmp/models/share-statistics.ts create mode 100644 packages/opentypebb/src/providers/fmp/models/treasury-rates.ts create mode 100644 packages/opentypebb/src/providers/fmp/models/world-news.ts create mode 100644 packages/opentypebb/src/providers/fmp/utils/definitions.ts create mode 100644 packages/opentypebb/src/providers/fmp/utils/helpers.ts create mode 100644 packages/opentypebb/src/providers/imf/index.ts create mode 100644 packages/opentypebb/src/providers/imf/models/available-indicators.ts create mode 100644 packages/opentypebb/src/providers/imf/models/consumer-price-index.ts create mode 100644 packages/opentypebb/src/providers/imf/models/direction-of-trade.ts create mode 100644 packages/opentypebb/src/providers/imf/models/economic-indicators.ts create mode 100644 packages/opentypebb/src/providers/intrinio/index.ts create mode 100644 packages/opentypebb/src/providers/intrinio/models/options-snapshots.ts create mode 100644 packages/opentypebb/src/providers/intrinio/models/options-unusual.ts create mode 100644 packages/opentypebb/src/providers/multpl/index.ts create mode 100644 packages/opentypebb/src/providers/multpl/models/sp500-multiples.ts create mode 100644 packages/opentypebb/src/providers/oecd/index.ts create mode 100644 packages/opentypebb/src/providers/oecd/models/composite-leading-indicator.ts create mode 100644 packages/opentypebb/src/providers/oecd/models/consumer-price-index.ts create mode 100644 packages/opentypebb/src/providers/oecd/models/country-interest-rates.ts create mode 100644 packages/opentypebb/src/providers/yfinance/index.ts create mode 100644 packages/opentypebb/src/providers/yfinance/models/active.ts create mode 100644 packages/opentypebb/src/providers/yfinance/models/aggressive-small-caps.ts create mode 100644 packages/opentypebb/src/providers/yfinance/models/available-indices.ts create mode 100644 packages/opentypebb/src/providers/yfinance/models/balance-sheet.ts create mode 100644 packages/opentypebb/src/providers/yfinance/models/cash-flow.ts create mode 100644 packages/opentypebb/src/providers/yfinance/models/company-news.ts create mode 100644 packages/opentypebb/src/providers/yfinance/models/crypto-historical.ts create mode 100644 packages/opentypebb/src/providers/yfinance/models/crypto-search.ts create mode 100644 packages/opentypebb/src/providers/yfinance/models/currency-historical.ts create mode 100644 packages/opentypebb/src/providers/yfinance/models/currency-search.ts create mode 100644 packages/opentypebb/src/providers/yfinance/models/equity-historical.ts create mode 100644 packages/opentypebb/src/providers/yfinance/models/equity-profile.ts create mode 100644 packages/opentypebb/src/providers/yfinance/models/equity-quote.ts create mode 100644 packages/opentypebb/src/providers/yfinance/models/equity-screener.ts create mode 100644 packages/opentypebb/src/providers/yfinance/models/etf-info.ts create mode 100644 packages/opentypebb/src/providers/yfinance/models/futures-curve.ts create mode 100644 packages/opentypebb/src/providers/yfinance/models/futures-historical.ts create mode 100644 packages/opentypebb/src/providers/yfinance/models/gainers.ts create mode 100644 packages/opentypebb/src/providers/yfinance/models/growth-tech.ts create mode 100644 packages/opentypebb/src/providers/yfinance/models/historical-dividends.ts create mode 100644 packages/opentypebb/src/providers/yfinance/models/income-statement.ts create mode 100644 packages/opentypebb/src/providers/yfinance/models/index-historical.ts create mode 100644 packages/opentypebb/src/providers/yfinance/models/key-executives.ts create mode 100644 packages/opentypebb/src/providers/yfinance/models/key-metrics.ts create mode 100644 packages/opentypebb/src/providers/yfinance/models/losers.ts create mode 100644 packages/opentypebb/src/providers/yfinance/models/options-chains.ts create mode 100644 packages/opentypebb/src/providers/yfinance/models/price-target-consensus.ts create mode 100644 packages/opentypebb/src/providers/yfinance/models/share-statistics.ts create mode 100644 packages/opentypebb/src/providers/yfinance/models/undervalued-growth.ts create mode 100644 packages/opentypebb/src/providers/yfinance/models/undervalued-large-caps.ts create mode 100644 packages/opentypebb/src/providers/yfinance/utils/helpers.ts create mode 100644 packages/opentypebb/src/providers/yfinance/utils/references.ts create mode 100644 packages/opentypebb/src/server.ts create mode 100644 packages/opentypebb/src/standard-models/analyst-estimates.ts create mode 100644 packages/opentypebb/src/standard-models/available-indicators.ts create mode 100644 packages/opentypebb/src/standard-models/available-indices.ts create mode 100644 packages/opentypebb/src/standard-models/balance-of-payments.ts create mode 100644 packages/opentypebb/src/standard-models/balance-sheet-growth.ts create mode 100644 packages/opentypebb/src/standard-models/balance-sheet.ts create mode 100644 packages/opentypebb/src/standard-models/calendar-dividend.ts create mode 100644 packages/opentypebb/src/standard-models/calendar-earnings.ts create mode 100644 packages/opentypebb/src/standard-models/calendar-ipo.ts create mode 100644 packages/opentypebb/src/standard-models/calendar-splits.ts create mode 100644 packages/opentypebb/src/standard-models/cash-flow-growth.ts create mode 100644 packages/opentypebb/src/standard-models/cash-flow.ts create mode 100644 packages/opentypebb/src/standard-models/central-bank-holdings.ts create mode 100644 packages/opentypebb/src/standard-models/company-filings.ts create mode 100644 packages/opentypebb/src/standard-models/company-news.ts create mode 100644 packages/opentypebb/src/standard-models/composite-leading-indicator.ts create mode 100644 packages/opentypebb/src/standard-models/consumer-price-index.ts create mode 100644 packages/opentypebb/src/standard-models/country-interest-rates.ts create mode 100644 packages/opentypebb/src/standard-models/country-profile.ts create mode 100644 packages/opentypebb/src/standard-models/crypto-historical.ts create mode 100644 packages/opentypebb/src/standard-models/crypto-search.ts create mode 100644 packages/opentypebb/src/standard-models/currency-historical.ts create mode 100644 packages/opentypebb/src/standard-models/currency-pairs.ts create mode 100644 packages/opentypebb/src/standard-models/currency-snapshots.ts create mode 100644 packages/opentypebb/src/standard-models/direction-of-trade.ts create mode 100644 packages/opentypebb/src/standard-models/discovery-filings.ts create mode 100644 packages/opentypebb/src/standard-models/earnings-call-transcript.ts create mode 100644 packages/opentypebb/src/standard-models/economic-calendar.ts create mode 100644 packages/opentypebb/src/standard-models/economic-indicators.ts create mode 100644 packages/opentypebb/src/standard-models/equity-discovery.ts create mode 100644 packages/opentypebb/src/standard-models/equity-historical.ts create mode 100644 packages/opentypebb/src/standard-models/equity-info.ts create mode 100644 packages/opentypebb/src/standard-models/equity-peers.ts create mode 100644 packages/opentypebb/src/standard-models/equity-performance.ts create mode 100644 packages/opentypebb/src/standard-models/equity-quote.ts create mode 100644 packages/opentypebb/src/standard-models/equity-screener.ts create mode 100644 packages/opentypebb/src/standard-models/esg-score.ts create mode 100644 packages/opentypebb/src/standard-models/etf-countries.ts create mode 100644 packages/opentypebb/src/standard-models/etf-equity-exposure.ts create mode 100644 packages/opentypebb/src/standard-models/etf-holdings.ts create mode 100644 packages/opentypebb/src/standard-models/etf-info.ts create mode 100644 packages/opentypebb/src/standard-models/etf-search.ts create mode 100644 packages/opentypebb/src/standard-models/etf-sectors.ts create mode 100644 packages/opentypebb/src/standard-models/executive-compensation.ts create mode 100644 packages/opentypebb/src/standard-models/export-destinations.ts create mode 100644 packages/opentypebb/src/standard-models/financial-ratios.ts create mode 100644 packages/opentypebb/src/standard-models/forward-ebitda-estimates.ts create mode 100644 packages/opentypebb/src/standard-models/forward-eps-estimates.ts create mode 100644 packages/opentypebb/src/standard-models/futures-curve.ts create mode 100644 packages/opentypebb/src/standard-models/futures-historical.ts create mode 100644 packages/opentypebb/src/standard-models/futures-info.ts create mode 100644 packages/opentypebb/src/standard-models/futures-instruments.ts create mode 100644 packages/opentypebb/src/standard-models/government-trades.ts create mode 100644 packages/opentypebb/src/standard-models/historical-dividends.ts create mode 100644 packages/opentypebb/src/standard-models/historical-employees.ts create mode 100644 packages/opentypebb/src/standard-models/historical-eps.ts create mode 100644 packages/opentypebb/src/standard-models/historical-market-cap.ts create mode 100644 packages/opentypebb/src/standard-models/historical-splits.ts create mode 100644 packages/opentypebb/src/standard-models/income-statement-growth.ts create mode 100644 packages/opentypebb/src/standard-models/income-statement.ts create mode 100644 packages/opentypebb/src/standard-models/index-constituents.ts create mode 100644 packages/opentypebb/src/standard-models/index-historical.ts create mode 100644 packages/opentypebb/src/standard-models/index-search.ts create mode 100644 packages/opentypebb/src/standard-models/index-sectors.ts create mode 100644 packages/opentypebb/src/standard-models/index.ts create mode 100644 packages/opentypebb/src/standard-models/insider-trading.ts create mode 100644 packages/opentypebb/src/standard-models/institutional-ownership.ts create mode 100644 packages/opentypebb/src/standard-models/key-executives.ts create mode 100644 packages/opentypebb/src/standard-models/key-metrics.ts create mode 100644 packages/opentypebb/src/standard-models/market-snapshots.ts create mode 100644 packages/opentypebb/src/standard-models/options-chains.ts create mode 100644 packages/opentypebb/src/standard-models/options-snapshots.ts create mode 100644 packages/opentypebb/src/standard-models/options-unusual.ts create mode 100644 packages/opentypebb/src/standard-models/price-target-consensus.ts create mode 100644 packages/opentypebb/src/standard-models/price-target.ts create mode 100644 packages/opentypebb/src/standard-models/recent-performance.ts create mode 100644 packages/opentypebb/src/standard-models/revenue-business-line.ts create mode 100644 packages/opentypebb/src/standard-models/revenue-geographic.ts create mode 100644 packages/opentypebb/src/standard-models/risk-premium.ts create mode 100644 packages/opentypebb/src/standard-models/share-statistics.ts create mode 100644 packages/opentypebb/src/standard-models/sp500-multiples.ts create mode 100644 packages/opentypebb/src/standard-models/treasury-rates.ts create mode 100644 packages/opentypebb/src/standard-models/world-news.ts create mode 100644 packages/opentypebb/tsconfig.json create mode 100644 packages/opentypebb/tsup.config.ts create mode 100644 src/openbb/sdk/base-client.ts create mode 100644 src/openbb/sdk/commodity-client.ts create mode 100644 src/openbb/sdk/crypto-client.ts create mode 100644 src/openbb/sdk/currency-client.ts create mode 100644 src/openbb/sdk/economy-client.ts create mode 100644 src/openbb/sdk/equity-client.ts create mode 100644 src/openbb/sdk/executor.ts create mode 100644 src/openbb/sdk/index.ts create mode 100644 src/openbb/sdk/news-client.ts create mode 100644 src/openbb/sdk/route-map.ts create mode 100644 src/openbb/sdk/types.ts diff --git a/package.json b/package.json index 270908b7..70f2c79e 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,8 @@ "tslog": "^4.10.2", "undici": "^7.22.0", "ws": "^8.19.0", - "zod": "^4.3.6" + "zod": "^4.3.6", + "opentypebb": "link:./packages/opentypebb" }, "devDependencies": { "@ai-sdk/provider": "^3.0.8", diff --git a/packages/opentypebb/.gitignore b/packages/opentypebb/.gitignore new file mode 100644 index 00000000..62ccde41 --- /dev/null +++ b/packages/opentypebb/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +dist/ +*.tsbuildinfo +.DS_Store diff --git a/packages/opentypebb/package.json b/packages/opentypebb/package.json new file mode 100644 index 00000000..9b69c27a --- /dev/null +++ b/packages/opentypebb/package.json @@ -0,0 +1,41 @@ +{ + "name": "opentypebb", + "version": "0.1.0", + "description": "TypeScript port of OpenBB Platform — financial data infrastructure", + "type": "module", + "exports": { + ".": { + "import": "./dist/index.js", + "types": "./dist/index.d.ts" + }, + "./server": { + "import": "./dist/server.js", + "types": "./dist/server.d.ts" + } + }, + "scripts": { + "dev": "tsx src/server.ts", + "build": "tsup", + "test": "vitest run", + "test:watch": "vitest", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@hono/node-server": "^1.13.8", + "hono": "^4.7.4", + "undici": "^7.22.0", + "yahoo-finance2": "^3.13.1", + "zod": "^3.24.2" + }, + "devDependencies": { + "@types/node": "^22.13.4", + "tsup": "^8.4.0", + "tsx": "^4.19.3", + "typescript": "^5.7.3", + "vitest": "^3.0.6" + }, + "engines": { + "node": ">=20.0.0" + }, + "license": "AGPL-3.0" +} diff --git a/packages/opentypebb/pnpm-lock.yaml b/packages/opentypebb/pnpm-lock.yaml new file mode 100644 index 00000000..cc9ebc55 --- /dev/null +++ b/packages/opentypebb/pnpm-lock.yaml @@ -0,0 +1,1572 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@hono/node-server': + specifier: ^1.13.8 + version: 1.19.9(hono@4.12.3) + hono: + specifier: ^4.7.4 + version: 4.12.3 + undici: + specifier: ^7.22.0 + version: 7.22.0 + yahoo-finance2: + specifier: ^3.13.1 + version: 3.13.1 + zod: + specifier: ^3.24.2 + version: 3.25.76 + devDependencies: + '@types/node': + specifier: ^22.13.4 + version: 22.19.13 + tsup: + specifier: ^8.4.0 + version: 8.5.1(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3) + tsx: + specifier: ^4.19.3 + version: 4.21.0 + typescript: + specifier: ^5.7.3 + version: 5.9.3 + vitest: + specifier: ^3.0.6 + version: 3.2.4(@types/node@22.19.13)(tsx@4.21.0) + +packages: + + '@deno/shim-deno-test@0.5.0': + resolution: {integrity: sha512-4nMhecpGlPi0cSzT67L+Tm+GOJqvuk8gqHBziqcUQOarnuIax1z96/gJHCSIz2Z0zhxE6Rzwb3IZXPtFh51j+w==} + + '@deno/shim-deno@0.18.2': + resolution: {integrity: sha512-oQ0CVmOio63wlhwQF75zA4ioolPvOwAoK0yuzcS5bDC1JUvH3y1GS8xPh8EOpcoDQRU4FTG8OQfxhpR+c6DrzA==} + + '@esbuild/aix-ppc64@0.27.3': + resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.27.3': + resolution: {integrity: sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.27.3': + resolution: {integrity: sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.27.3': + resolution: {integrity: sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.27.3': + resolution: {integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.3': + resolution: {integrity: sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.27.3': + resolution: {integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.3': + resolution: {integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.27.3': + resolution: {integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.27.3': + resolution: {integrity: sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.27.3': + resolution: {integrity: sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.27.3': + resolution: {integrity: sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.27.3': + resolution: {integrity: sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.27.3': + resolution: {integrity: sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.3': + resolution: {integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.27.3': + resolution: {integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.27.3': + resolution: {integrity: sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.27.3': + resolution: {integrity: sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.3': + resolution: {integrity: sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.27.3': + resolution: {integrity: sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.3': + resolution: {integrity: sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.27.3': + resolution: {integrity: sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.27.3': + resolution: {integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.27.3': + resolution: {integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.27.3': + resolution: {integrity: sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.27.3': + resolution: {integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@hono/node-server@1.19.9': + resolution: {integrity: sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==} + engines: {node: '>=18.14.1'} + peerDependencies: + hono: ^4 + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@rollup/rollup-android-arm-eabi@4.59.0': + resolution: {integrity: sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.59.0': + resolution: {integrity: sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.59.0': + resolution: {integrity: sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.59.0': + resolution: {integrity: sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.59.0': + resolution: {integrity: sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.59.0': + resolution: {integrity: sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.59.0': + resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.59.0': + resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.59.0': + resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.59.0': + resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.59.0': + resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.59.0': + resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.59.0': + resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.59.0': + resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.59.0': + resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.59.0': + resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.59.0': + resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.59.0': + resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.59.0': + resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.59.0': + resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.59.0': + resolution: {integrity: sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.59.0': + resolution: {integrity: sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.59.0': + resolution: {integrity: sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.59.0': + resolution: {integrity: sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.59.0': + resolution: {integrity: sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==} + cpu: [x64] + os: [win32] + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/node@22.19.13': + resolution: {integrity: sha512-akNQMv0wW5uyRpD2v2IEyRSZiR+BeGuoB6L310EgGObO44HSMNT8z1xzio28V8qOrgYaopIDNA18YgdXd+qTiw==} + + '@vitest/expect@3.2.4': + resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} + + '@vitest/mocker@3.2.4': + resolution: {integrity: sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@3.2.4': + resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==} + + '@vitest/runner@3.2.4': + resolution: {integrity: sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==} + + '@vitest/snapshot@3.2.4': + resolution: {integrity: sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==} + + '@vitest/spy@3.2.4': + resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==} + + '@vitest/utils@3.2.4': + resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} + + acorn@8.16.0: + resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} + engines: {node: '>=0.4.0'} + hasBin: true + + any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + bundle-require@5.1.0: + resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + peerDependencies: + esbuild: '>=0.18' + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + chai@5.3.3: + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + engines: {node: '>=18'} + + check-error@2.1.3: + resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} + engines: {node: '>= 16'} + + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + + confbox@0.1.8: + resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + + consola@3.4.2: + resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} + engines: {node: ^14.18.0 || >=16.10.0} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + + esbuild@0.27.3: + resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==} + engines: {node: '>=18'} + hasBin: true + + escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fetch-mock-cache@2.3.1: + resolution: {integrity: sha512-hDk+Nbt0Y8Aq7KTEU6ASQAcpB34UjhkpD3QjzD6yvEKP4xVElAqXrjQ7maL+LYMGafx51Zq6qUfDM57PNu/qMw==} + + filename-reserved-regex@2.0.0: + resolution: {integrity: sha512-lc1bnsSr4L4Bdif8Xb/qrtokGbq5zlsms/CYH8PP+WtCkGNF65DPiQY8vG3SakEdRn8Dlnm+gW/qWKKjS5sZzQ==} + engines: {node: '>=4'} + + filenamify-url@2.1.2: + resolution: {integrity: sha512-3rMbAr7vDNMOGsj1aMniQFl749QjgM+lMJ/77ZRSPTIgxvolZwoQbn8dXLs7xfd+hAdli+oTnSWZNkJJLWQFEQ==} + engines: {node: '>=8'} + + filenamify@4.3.0: + resolution: {integrity: sha512-hcFKyUG57yWGAzu1CMt/dPzYZuv+jAJUT85bL8mrXvNe6hWj6yEHEc4EdcgiA6Z3oi1/9wXJdZPXF2dZNgwgOg==} + engines: {node: '>=8'} + + fix-dts-default-cjs-exports@1.0.1: + resolution: {integrity: sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + get-tsconfig@4.13.6: + resolution: {integrity: sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==} + + hono@4.12.3: + resolution: {integrity: sha512-SFsVSjp8sj5UumXOOFlkZOG6XS9SJDKw0TbwFeV+AJ8xlST8kxK5Z/5EYa111UY8732lK2S/xB653ceuaoGwpg==} + engines: {node: '>=16.9.0'} + + humanize-url@2.1.1: + resolution: {integrity: sha512-V4nxsPGNE7mPjr1qDp471YfW8nhBiTRWrG/4usZlpvFU8I7gsV7Jvrrzv/snbLm5dWO3dr1ennu2YqnhTWFmYA==} + engines: {node: '>=8'} + + isexe@3.1.5: + resolution: {integrity: sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==} + engines: {node: '>=18'} + + joycon@3.1.1: + resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} + engines: {node: '>=10'} + + js-tokens@9.0.1: + resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + + json-schema@0.4.0: + resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} + + lilconfig@3.1.3: + resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} + engines: {node: '>=14'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + load-tsconfig@0.2.5: + resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + mlly@1.8.0: + resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + normalize-url@4.5.1: + resolution: {integrity: sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA==} + engines: {node: '>=8'} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + pirates@4.0.7: + resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} + engines: {node: '>= 6'} + + pkg-types@1.3.1: + resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + + postcss-load-config@6.0.1: + resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} + engines: {node: '>= 18'} + peerDependencies: + jiti: '>=1.21.0' + postcss: '>=8.0.9' + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + jiti: + optional: true + postcss: + optional: true + tsx: + optional: true + yaml: + optional: true + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + psl@1.15.0: + resolution: {integrity: sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + querystringify@2.2.0: + resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} + + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + + requires-port@1.0.0: + resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + rollup@4.59.0: + resolution: {integrity: sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map@0.7.6: + resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} + engines: {node: '>= 12'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + + strip-literal@3.1.0: + resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} + + strip-outer@1.0.1: + resolution: {integrity: sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==} + engines: {node: '>=0.10.0'} + + sucrase@3.35.1: + resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + + thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + + thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@2.0.0: + resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} + engines: {node: '>=14.0.0'} + + tinyspy@4.0.4: + resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} + engines: {node: '>=14.0.0'} + + tldts-core@6.1.86: + resolution: {integrity: sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==} + + tldts@6.1.86: + resolution: {integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==} + hasBin: true + + tough-cookie-file-store@2.0.3: + resolution: {integrity: sha512-sMpZVcmFf6EYFHFFl+SYH4W1/OnXBYMGDsv2IlbQ2caHyFElW/UR/gpj/KYU1JwmP4dE9xqwv2+vWcmlXHojSw==} + engines: {node: '>=6'} + + tough-cookie@4.1.4: + resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==} + engines: {node: '>=6'} + + tough-cookie@5.1.2: + resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==} + engines: {node: '>=16'} + + tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + + trim-repeated@1.0.0: + resolution: {integrity: sha512-pkonvlKk8/ZuR0D5tLW8ljt5I8kmxp2XKymhepUeOdCEfKpZaktSArkLHZt76OB1ZvO9bssUsDty4SWhLvZpLg==} + engines: {node: '>=0.10.0'} + + ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + + tsup@8.5.1: + resolution: {integrity: sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + '@microsoft/api-extractor': ^7.36.0 + '@swc/core': ^1 + postcss: ^8.4.12 + typescript: '>=4.5.0' + peerDependenciesMeta: + '@microsoft/api-extractor': + optional: true + '@swc/core': + optional: true + postcss: + optional: true + typescript: + optional: true + + tsx@4.21.0: + resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} + engines: {node: '>=18.0.0'} + hasBin: true + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + ufo@1.6.3: + resolution: {integrity: sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==} + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + undici@7.22.0: + resolution: {integrity: sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg==} + engines: {node: '>=20.18.1'} + + universalify@0.2.0: + resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} + engines: {node: '>= 4.0.0'} + + url-parse@1.5.10: + resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + + vite-node@3.2.4: + resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + + vite@7.3.1: + resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@3.2.4: + resolution: {integrity: sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/debug': ^4.1.12 + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + '@vitest/browser': 3.2.4 + '@vitest/ui': 3.2.4 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/debug': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + which@4.0.0: + resolution: {integrity: sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==} + engines: {node: ^16.13.0 || >=18.0.0} + hasBin: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + yahoo-finance2@3.13.1: + resolution: {integrity: sha512-kn3unY2OflG1NbeONedWxFDzq5QDyMYYAnr6VjRIOsMv5Q7ZXZZYFM8OadNZrOev4ikQjbYcMLLjogpVaez/vQ==} + engines: {node: '>=20.0.0'} + hasBin: true + + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + +snapshots: + + '@deno/shim-deno-test@0.5.0': {} + + '@deno/shim-deno@0.18.2': + dependencies: + '@deno/shim-deno-test': 0.5.0 + which: 4.0.0 + + '@esbuild/aix-ppc64@0.27.3': + optional: true + + '@esbuild/android-arm64@0.27.3': + optional: true + + '@esbuild/android-arm@0.27.3': + optional: true + + '@esbuild/android-x64@0.27.3': + optional: true + + '@esbuild/darwin-arm64@0.27.3': + optional: true + + '@esbuild/darwin-x64@0.27.3': + optional: true + + '@esbuild/freebsd-arm64@0.27.3': + optional: true + + '@esbuild/freebsd-x64@0.27.3': + optional: true + + '@esbuild/linux-arm64@0.27.3': + optional: true + + '@esbuild/linux-arm@0.27.3': + optional: true + + '@esbuild/linux-ia32@0.27.3': + optional: true + + '@esbuild/linux-loong64@0.27.3': + optional: true + + '@esbuild/linux-mips64el@0.27.3': + optional: true + + '@esbuild/linux-ppc64@0.27.3': + optional: true + + '@esbuild/linux-riscv64@0.27.3': + optional: true + + '@esbuild/linux-s390x@0.27.3': + optional: true + + '@esbuild/linux-x64@0.27.3': + optional: true + + '@esbuild/netbsd-arm64@0.27.3': + optional: true + + '@esbuild/netbsd-x64@0.27.3': + optional: true + + '@esbuild/openbsd-arm64@0.27.3': + optional: true + + '@esbuild/openbsd-x64@0.27.3': + optional: true + + '@esbuild/openharmony-arm64@0.27.3': + optional: true + + '@esbuild/sunos-x64@0.27.3': + optional: true + + '@esbuild/win32-arm64@0.27.3': + optional: true + + '@esbuild/win32-ia32@0.27.3': + optional: true + + '@esbuild/win32-x64@0.27.3': + optional: true + + '@hono/node-server@1.19.9(hono@4.12.3)': + dependencies: + hono: 4.12.3 + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@rollup/rollup-android-arm-eabi@4.59.0': + optional: true + + '@rollup/rollup-android-arm64@4.59.0': + optional: true + + '@rollup/rollup-darwin-arm64@4.59.0': + optional: true + + '@rollup/rollup-darwin-x64@4.59.0': + optional: true + + '@rollup/rollup-freebsd-arm64@4.59.0': + optional: true + + '@rollup/rollup-freebsd-x64@4.59.0': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.59.0': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.59.0': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-x64-musl@4.59.0': + optional: true + + '@rollup/rollup-openbsd-x64@4.59.0': + optional: true + + '@rollup/rollup-openharmony-arm64@4.59.0': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.59.0': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.59.0': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.59.0': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.59.0': + optional: true + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + + '@types/estree@1.0.8': {} + + '@types/node@22.19.13': + dependencies: + undici-types: 6.21.0 + + '@vitest/expect@3.2.4': + dependencies: + '@types/chai': 5.2.3 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.3.3 + tinyrainbow: 2.0.0 + + '@vitest/mocker@3.2.4(vite@7.3.1(@types/node@22.19.13)(tsx@4.21.0))': + dependencies: + '@vitest/spy': 3.2.4 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 7.3.1(@types/node@22.19.13)(tsx@4.21.0) + + '@vitest/pretty-format@3.2.4': + dependencies: + tinyrainbow: 2.0.0 + + '@vitest/runner@3.2.4': + dependencies: + '@vitest/utils': 3.2.4 + pathe: 2.0.3 + strip-literal: 3.1.0 + + '@vitest/snapshot@3.2.4': + dependencies: + '@vitest/pretty-format': 3.2.4 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@3.2.4': + dependencies: + tinyspy: 4.0.4 + + '@vitest/utils@3.2.4': + dependencies: + '@vitest/pretty-format': 3.2.4 + loupe: 3.2.1 + tinyrainbow: 2.0.0 + + acorn@8.16.0: {} + + any-promise@1.3.0: {} + + assertion-error@2.0.1: {} + + bundle-require@5.1.0(esbuild@0.27.3): + dependencies: + esbuild: 0.27.3 + load-tsconfig: 0.2.5 + + cac@6.7.14: {} + + chai@5.3.3: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.3 + deep-eql: 5.0.2 + loupe: 3.2.1 + pathval: 2.0.1 + + check-error@2.1.3: {} + + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + + commander@4.1.1: {} + + confbox@0.1.8: {} + + consola@3.4.2: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + deep-eql@5.0.2: {} + + es-module-lexer@1.7.0: {} + + esbuild@0.27.3: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.3 + '@esbuild/android-arm': 0.27.3 + '@esbuild/android-arm64': 0.27.3 + '@esbuild/android-x64': 0.27.3 + '@esbuild/darwin-arm64': 0.27.3 + '@esbuild/darwin-x64': 0.27.3 + '@esbuild/freebsd-arm64': 0.27.3 + '@esbuild/freebsd-x64': 0.27.3 + '@esbuild/linux-arm': 0.27.3 + '@esbuild/linux-arm64': 0.27.3 + '@esbuild/linux-ia32': 0.27.3 + '@esbuild/linux-loong64': 0.27.3 + '@esbuild/linux-mips64el': 0.27.3 + '@esbuild/linux-ppc64': 0.27.3 + '@esbuild/linux-riscv64': 0.27.3 + '@esbuild/linux-s390x': 0.27.3 + '@esbuild/linux-x64': 0.27.3 + '@esbuild/netbsd-arm64': 0.27.3 + '@esbuild/netbsd-x64': 0.27.3 + '@esbuild/openbsd-arm64': 0.27.3 + '@esbuild/openbsd-x64': 0.27.3 + '@esbuild/openharmony-arm64': 0.27.3 + '@esbuild/sunos-x64': 0.27.3 + '@esbuild/win32-arm64': 0.27.3 + '@esbuild/win32-ia32': 0.27.3 + '@esbuild/win32-x64': 0.27.3 + + escape-string-regexp@1.0.5: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + + expect-type@1.3.0: {} + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + fetch-mock-cache@2.3.1: + dependencies: + debug: 4.4.3 + filenamify-url: 2.1.2 + transitivePeerDependencies: + - supports-color + + filename-reserved-regex@2.0.0: {} + + filenamify-url@2.1.2: + dependencies: + filenamify: 4.3.0 + humanize-url: 2.1.1 + + filenamify@4.3.0: + dependencies: + filename-reserved-regex: 2.0.0 + strip-outer: 1.0.1 + trim-repeated: 1.0.0 + + fix-dts-default-cjs-exports@1.0.1: + dependencies: + magic-string: 0.30.21 + mlly: 1.8.0 + rollup: 4.59.0 + + fsevents@2.3.3: + optional: true + + get-tsconfig@4.13.6: + dependencies: + resolve-pkg-maps: 1.0.0 + + hono@4.12.3: {} + + humanize-url@2.1.1: + dependencies: + normalize-url: 4.5.1 + + isexe@3.1.5: {} + + joycon@3.1.1: {} + + js-tokens@9.0.1: {} + + json-schema@0.4.0: {} + + lilconfig@3.1.3: {} + + lines-and-columns@1.2.4: {} + + load-tsconfig@0.2.5: {} + + loupe@3.2.1: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + mlly@1.8.0: + dependencies: + acorn: 8.16.0 + pathe: 2.0.3 + pkg-types: 1.3.1 + ufo: 1.6.3 + + ms@2.1.3: {} + + mz@2.7.0: + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + + nanoid@3.3.11: {} + + normalize-url@4.5.1: {} + + object-assign@4.1.1: {} + + pathe@2.0.3: {} + + pathval@2.0.1: {} + + picocolors@1.1.1: {} + + picomatch@4.0.3: {} + + pirates@4.0.7: {} + + pkg-types@1.3.1: + dependencies: + confbox: 0.1.8 + mlly: 1.8.0 + pathe: 2.0.3 + + postcss-load-config@6.0.1(postcss@8.5.6)(tsx@4.21.0): + dependencies: + lilconfig: 3.1.3 + optionalDependencies: + postcss: 8.5.6 + tsx: 4.21.0 + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + psl@1.15.0: + dependencies: + punycode: 2.3.1 + + punycode@2.3.1: {} + + querystringify@2.2.0: {} + + readdirp@4.1.2: {} + + requires-port@1.0.0: {} + + resolve-from@5.0.0: {} + + resolve-pkg-maps@1.0.0: {} + + rollup@4.59.0: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.59.0 + '@rollup/rollup-android-arm64': 4.59.0 + '@rollup/rollup-darwin-arm64': 4.59.0 + '@rollup/rollup-darwin-x64': 4.59.0 + '@rollup/rollup-freebsd-arm64': 4.59.0 + '@rollup/rollup-freebsd-x64': 4.59.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.59.0 + '@rollup/rollup-linux-arm-musleabihf': 4.59.0 + '@rollup/rollup-linux-arm64-gnu': 4.59.0 + '@rollup/rollup-linux-arm64-musl': 4.59.0 + '@rollup/rollup-linux-loong64-gnu': 4.59.0 + '@rollup/rollup-linux-loong64-musl': 4.59.0 + '@rollup/rollup-linux-ppc64-gnu': 4.59.0 + '@rollup/rollup-linux-ppc64-musl': 4.59.0 + '@rollup/rollup-linux-riscv64-gnu': 4.59.0 + '@rollup/rollup-linux-riscv64-musl': 4.59.0 + '@rollup/rollup-linux-s390x-gnu': 4.59.0 + '@rollup/rollup-linux-x64-gnu': 4.59.0 + '@rollup/rollup-linux-x64-musl': 4.59.0 + '@rollup/rollup-openbsd-x64': 4.59.0 + '@rollup/rollup-openharmony-arm64': 4.59.0 + '@rollup/rollup-win32-arm64-msvc': 4.59.0 + '@rollup/rollup-win32-ia32-msvc': 4.59.0 + '@rollup/rollup-win32-x64-gnu': 4.59.0 + '@rollup/rollup-win32-x64-msvc': 4.59.0 + fsevents: 2.3.3 + + siginfo@2.0.0: {} + + source-map-js@1.2.1: {} + + source-map@0.7.6: {} + + stackback@0.0.2: {} + + std-env@3.10.0: {} + + strip-literal@3.1.0: + dependencies: + js-tokens: 9.0.1 + + strip-outer@1.0.1: + dependencies: + escape-string-regexp: 1.0.5 + + sucrase@3.35.1: + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + commander: 4.1.1 + lines-and-columns: 1.2.4 + mz: 2.7.0 + pirates: 4.0.7 + tinyglobby: 0.2.15 + ts-interface-checker: 0.1.13 + + thenify-all@1.6.0: + dependencies: + thenify: 3.3.1 + + thenify@3.3.1: + dependencies: + any-promise: 1.3.0 + + tinybench@2.9.0: {} + + tinyexec@0.3.2: {} + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + tinypool@1.1.1: {} + + tinyrainbow@2.0.0: {} + + tinyspy@4.0.4: {} + + tldts-core@6.1.86: {} + + tldts@6.1.86: + dependencies: + tldts-core: 6.1.86 + + tough-cookie-file-store@2.0.3: + dependencies: + tough-cookie: 4.1.4 + + tough-cookie@4.1.4: + dependencies: + psl: 1.15.0 + punycode: 2.3.1 + universalify: 0.2.0 + url-parse: 1.5.10 + + tough-cookie@5.1.2: + dependencies: + tldts: 6.1.86 + + tree-kill@1.2.2: {} + + trim-repeated@1.0.0: + dependencies: + escape-string-regexp: 1.0.5 + + ts-interface-checker@0.1.13: {} + + tsup@8.5.1(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3): + dependencies: + bundle-require: 5.1.0(esbuild@0.27.3) + cac: 6.7.14 + chokidar: 4.0.3 + consola: 3.4.2 + debug: 4.4.3 + esbuild: 0.27.3 + fix-dts-default-cjs-exports: 1.0.1 + joycon: 3.1.1 + picocolors: 1.1.1 + postcss-load-config: 6.0.1(postcss@8.5.6)(tsx@4.21.0) + resolve-from: 5.0.0 + rollup: 4.59.0 + source-map: 0.7.6 + sucrase: 3.35.1 + tinyexec: 0.3.2 + tinyglobby: 0.2.15 + tree-kill: 1.2.2 + optionalDependencies: + postcss: 8.5.6 + typescript: 5.9.3 + transitivePeerDependencies: + - jiti + - supports-color + - tsx + - yaml + + tsx@4.21.0: + dependencies: + esbuild: 0.27.3 + get-tsconfig: 4.13.6 + optionalDependencies: + fsevents: 2.3.3 + + typescript@5.9.3: {} + + ufo@1.6.3: {} + + undici-types@6.21.0: {} + + undici@7.22.0: {} + + universalify@0.2.0: {} + + url-parse@1.5.10: + dependencies: + querystringify: 2.2.0 + requires-port: 1.0.0 + + vite-node@3.2.4(@types/node@22.19.13)(tsx@4.21.0): + dependencies: + cac: 6.7.14 + debug: 4.4.3 + es-module-lexer: 1.7.0 + pathe: 2.0.3 + vite: 7.3.1(@types/node@22.19.13)(tsx@4.21.0) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + vite@7.3.1(@types/node@22.19.13)(tsx@4.21.0): + dependencies: + esbuild: 0.27.3 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.59.0 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 22.19.13 + fsevents: 2.3.3 + tsx: 4.21.0 + + vitest@3.2.4(@types/node@22.19.13)(tsx@4.21.0): + dependencies: + '@types/chai': 5.2.3 + '@vitest/expect': 3.2.4 + '@vitest/mocker': 3.2.4(vite@7.3.1(@types/node@22.19.13)(tsx@4.21.0)) + '@vitest/pretty-format': 3.2.4 + '@vitest/runner': 3.2.4 + '@vitest/snapshot': 3.2.4 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.3.3 + debug: 4.4.3 + expect-type: 1.3.0 + magic-string: 0.30.21 + pathe: 2.0.3 + picomatch: 4.0.3 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.15 + tinypool: 1.1.1 + tinyrainbow: 2.0.0 + vite: 7.3.1(@types/node@22.19.13)(tsx@4.21.0) + vite-node: 3.2.4(@types/node@22.19.13)(tsx@4.21.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 22.19.13 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + which@4.0.0: + dependencies: + isexe: 3.1.5 + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + yahoo-finance2@3.13.1: + dependencies: + '@deno/shim-deno': 0.18.2 + fetch-mock-cache: 2.3.1 + json-schema: 0.4.0 + tough-cookie: 5.1.2 + tough-cookie-file-store: 2.0.3 + transitivePeerDependencies: + - supports-color + + zod@3.25.76: {} diff --git a/packages/opentypebb/src/core/api/app-loader.ts b/packages/opentypebb/src/core/api/app-loader.ts new file mode 100644 index 00000000..ca0eed96 --- /dev/null +++ b/packages/opentypebb/src/core/api/app-loader.ts @@ -0,0 +1,80 @@ +/** + * App Loader — load providers and mount extension routers. + * Maps to: openbb_core/api/app_loader.py + * + * In Python, RegistryLoader uses entry_points for dynamic discovery. + * In TypeScript, providers and routers are explicitly imported + * (simpler, tree-shake friendly, easier to debug). + */ + +import { Registry } from '../provider/registry.js' +import { QueryExecutor } from '../provider/query-executor.js' +import { Router } from '../app/router.js' + +// --- Providers (explicit imports replace entry_points) --- +import { fmpProvider } from '../../providers/fmp/index.js' +import { yfinanceProvider } from '../../providers/yfinance/index.js' +import { deribitProvider } from '../../providers/deribit/index.js' +import { cboeProvider } from '../../providers/cboe/index.js' +import { multplProvider } from '../../providers/multpl/index.js' +import { oecdProvider } from '../../providers/oecd/index.js' +import { econdbProvider } from '../../providers/econdb/index.js' +import { imfProvider } from '../../providers/imf/index.js' +import { ecbProvider } from '../../providers/ecb/index.js' +import { federalReserveProvider } from '../../providers/federal_reserve/index.js' +import { intrinioProvider } from '../../providers/intrinio/index.js' + +// --- Extension routers --- +import { equityRouter } from '../../extensions/equity/equity-router.js' +import { cryptoRouter } from '../../extensions/crypto/crypto-router.js' +import { currencyRouter } from '../../extensions/currency/currency-router.js' +import { newsRouter } from '../../extensions/news/news-router.js' +import { economyRouter } from '../../extensions/economy/economy-router.js' +import { etfRouter } from '../../extensions/etf/etf-router.js' +import { indexRouter } from '../../extensions/index/index-router.js' +import { derivativesRouter } from '../../extensions/derivatives/derivatives-router.js' + +/** + * Create and populate a Registry with all available providers. + * Maps to: RegistryLoader.from_extensions() in registry_loader.py + */ +export function createRegistry(): Registry { + const registry = new Registry() + registry.includeProvider(fmpProvider) + registry.includeProvider(yfinanceProvider) + registry.includeProvider(deribitProvider) + registry.includeProvider(cboeProvider) + registry.includeProvider(multplProvider) + registry.includeProvider(oecdProvider) + registry.includeProvider(econdbProvider) + registry.includeProvider(imfProvider) + registry.includeProvider(ecbProvider) + registry.includeProvider(federalReserveProvider) + registry.includeProvider(intrinioProvider) + return registry +} + +/** + * Create a QueryExecutor with all providers loaded. + */ +export function createExecutor(): QueryExecutor { + const registry = createRegistry() + return new QueryExecutor(registry) +} + +/** + * Load all extension routers and return a root router. + * Maps to: RouterLoader in app_loader.py + */ +export function loadAllRouters(): Router { + const root = new Router({ description: 'OpenTypeBB API' }) + root.includeRouter(equityRouter) + root.includeRouter(cryptoRouter) + root.includeRouter(currencyRouter) + root.includeRouter(newsRouter) + root.includeRouter(economyRouter) + root.includeRouter(etfRouter) + root.includeRouter(indexRouter) + root.includeRouter(derivativesRouter) + return root +} diff --git a/packages/opentypebb/src/core/api/rest-api.ts b/packages/opentypebb/src/core/api/rest-api.ts new file mode 100644 index 00000000..c5628562 --- /dev/null +++ b/packages/opentypebb/src/core/api/rest-api.ts @@ -0,0 +1,45 @@ +/** + * REST API setup using Hono. + * Maps to: openbb_core/api/rest_api.py + * + * Creates the Hono app with: + * - CORS middleware + * - Default credential injection middleware + * - Error handling + * - Health check endpoint + */ + +import { Hono } from 'hono' +import { cors } from 'hono/cors' +import { serve } from '@hono/node-server' +import type { Credentials } from '../app/model/credentials.js' + +/** + * Create the Hono app with middleware configured. + * Maps to: the FastAPI app creation in rest_api.py + * + * @param defaultCredentials - Default credentials injected into every request + * (can be overridden per-request via X-OpenBB-Credentials header) + */ +export function createApp( + defaultCredentials: Credentials = {}, +): Hono { + const app = new Hono() + + // CORS middleware (allow all origins by default, matching OpenBB defaults) + app.use(cors()) + + // Health check + app.get('/api/v1/health', (c) => c.json({ status: 'ok' })) + + return app +} + +/** + * Start the HTTP server. + * Maps to: uvicorn.run() in rest_api.py + */ +export function startServer(app: Hono, port = 6900): void { + serve({ fetch: app.fetch, port }) + console.log(`OpenTypeBB listening on http://localhost:${port}`) +} diff --git a/packages/opentypebb/src/core/app/command-runner.ts b/packages/opentypebb/src/core/app/command-runner.ts new file mode 100644 index 00000000..6bbb5fba --- /dev/null +++ b/packages/opentypebb/src/core/app/command-runner.ts @@ -0,0 +1,30 @@ +/** + * Command Runner. + * Maps to: openbb_core/app/command_runner.py + * + * In Python, CommandRunner orchestrates: + * 1. ParametersBuilder.build() — validate & coerce params + * 2. Execute the command function + * 3. Attach metadata (duration, route, timestamp) + * 4. Trigger on_command_output callbacks + * + * In TypeScript, this is simplified since we don't have FastAPI's + * dependency injection system. The command is just a function that + * creates a Query and executes it. + */ + +import type { QueryExecutor } from '../provider/query-executor.js' +import { Query, type QueryConfig } from './query.js' +import type { OBBject } from './model/obbject.js' + +export class CommandRunner { + constructor(private readonly executor: QueryExecutor) {} + + /** + * Run a command by creating and executing a Query. + */ + async run(config: QueryConfig): Promise> { + const query = new Query(this.executor, config) + return query.execute() + } +} diff --git a/packages/opentypebb/src/core/app/model/credentials.ts b/packages/opentypebb/src/core/app/model/credentials.ts new file mode 100644 index 00000000..eba1712d --- /dev/null +++ b/packages/opentypebb/src/core/app/model/credentials.ts @@ -0,0 +1,32 @@ +/** + * Credentials management. + * Maps to: openbb_core/app/model/credentials.py + * + * In Python, Credentials is dynamically generated from all provider requirements. + * In TypeScript, we use a simple Record since we don't need + * Pydantic's SecretStr obfuscation — credentials are plain strings passed through. + */ + +export type Credentials = Record + +/** + * Build a credentials record from provider key mappings. + * Similar to how open-alice's credential-map.ts works. + * + * @param providerKeys - Map of short key names to API key values. + * @param keyMapping - Map of short names to full credential names. + * @returns Full credentials record. + */ +export function buildCredentials( + providerKeys: Record, + keyMapping: Record, +): Credentials { + const credentials: Credentials = {} + for (const [shortName, fullName] of Object.entries(keyMapping)) { + const value = providerKeys[shortName] + if (value) { + credentials[fullName] = value + } + } + return credentials +} diff --git a/packages/opentypebb/src/core/app/model/metadata.ts b/packages/opentypebb/src/core/app/model/metadata.ts new file mode 100644 index 00000000..46685f8c --- /dev/null +++ b/packages/opentypebb/src/core/app/model/metadata.ts @@ -0,0 +1,31 @@ +/** + * Request metadata for tracking query execution. + * Maps to metadata attached in command_runner.py's _execute_func. + */ + +export interface RequestMetadata { + /** Route path (e.g., "/equity/price/historical"). */ + route: string + /** Query arguments. */ + arguments: Record + /** Execution duration in milliseconds. */ + duration: number + /** Timestamp of execution. */ + timestamp: string +} + +/** + * Create request metadata for a query execution. + */ +export function createMetadata( + route: string, + args: Record, + startTime: number, +): RequestMetadata { + return { + route, + arguments: args, + duration: Date.now() - startTime, + timestamp: new Date().toISOString(), + } +} diff --git a/packages/opentypebb/src/core/app/model/obbject.ts b/packages/opentypebb/src/core/app/model/obbject.ts new file mode 100644 index 00000000..70ee76f0 --- /dev/null +++ b/packages/opentypebb/src/core/app/model/obbject.ts @@ -0,0 +1,102 @@ +/** + * OBBject — the universal response envelope. + * Maps to: openbb_core/app/model/obbject.py + * + * In Python, OBBject is a Generic[T] Pydantic model with: + * results: T | None + * provider: str | None + * warnings: List[Warning_] | None + * chart: Chart | None + * extra: Dict[str, Any] + * + * It also has from_query() classmethod, to_dataframe(), to_dict(), etc. + * In TypeScript, we skip the DataFrame/Polars/NumPy conversion methods. + */ + +export interface Warning { + category: string + message: string +} + +export interface OBBjectData { + results: T[] | null + provider: string | null + warnings: Warning[] | null + chart: unknown | null + extra: Record +} + +export class OBBject { + results: T[] | null + provider: string | null + warnings: Warning[] | null + chart: unknown | null + extra: Record + + // Private metadata (matches Python's PrivateAttr fields) + private _route: string | null = null + private _standardParams: Record | null = null + private _extraParams: Record | null = null + + constructor(data: Partial> = {}) { + this.results = data.results ?? null + this.provider = data.provider ?? null + this.warnings = data.warnings ?? null + this.chart = data.chart ?? null + this.extra = data.extra ?? {} + } + + /** Set route metadata. */ + setRoute(route: string): this { + this._route = route + return this + } + + /** Set standard params metadata. */ + setStandardParams(params: Record): this { + this._standardParams = params + return this + } + + /** Set extra params metadata. */ + setExtraParams(params: Record): this { + this._extraParams = params + return this + } + + /** Get route metadata. */ + get route(): string | null { + return this._route + } + + /** JSON-serializable representation for HTTP responses. */ + toJSON(): OBBjectData { + return { + results: this.results, + provider: this.provider, + warnings: this.warnings, + chart: this.chart, + extra: this.extra, + } + } + + /** + * Create OBBject from query execution result. + * Maps to: OBBject.from_query() in obbject.py + * + * In the simplified TypeScript version, this directly wraps + * the fetcher result rather than going through the full + * Query → ProviderInterface → CommandRunner pipeline. + */ + static fromResults( + results: R[], + provider: string, + extra?: Record, + ): OBBject { + return new OBBject({ + results, + provider, + extra, + }) + } +} diff --git a/packages/opentypebb/src/core/app/query.ts b/packages/opentypebb/src/core/app/query.ts new file mode 100644 index 00000000..aca24018 --- /dev/null +++ b/packages/opentypebb/src/core/app/query.ts @@ -0,0 +1,76 @@ +/** + * Query class. + * Maps to: openbb_core/app/query.py + * + * In Python, Query holds CommandContext + ProviderChoices + StandardParams + ExtraParams, + * and execute() calls ProviderInterface → QueryExecutor. + * + * In TypeScript, Query is simplified: + * - Takes provider name, model name, params, and credentials directly + * - Delegates to QueryExecutor.execute() + * - No ProviderInterface dependency injection (handled by Router) + */ + +import type { QueryExecutor } from '../provider/query-executor.js' +import { OBBject } from './model/obbject.js' +import { createMetadata } from './model/metadata.js' + +export interface QueryConfig { + /** Provider name (e.g., "fmp"). */ + provider: string + /** Model name (e.g., "EquityHistorical"). */ + model: string + /** Merged params (standard + extra). */ + params: Record + /** Provider credentials. */ + credentials: Record | null + /** Route path for metadata. */ + route?: string +} + +export class Query { + readonly provider: string + readonly model: string + readonly params: Record + readonly credentials: Record | null + readonly route: string + + constructor( + private readonly executor: QueryExecutor, + config: QueryConfig, + ) { + this.provider = config.provider + this.model = config.model + this.params = config.params + this.credentials = config.credentials + this.route = config.route ?? `/${config.model}` + } + + /** + * Execute the query and return an OBBject. + * Maps to: Query.execute() in query.py + OBBject.from_query() + */ + async execute(): Promise> { + const startTime = Date.now() + + const results = await this.executor.execute( + this.provider, + this.model, + this.params, + this.credentials, + ) + + const metadata = createMetadata(this.route, this.params, startTime) + + const obbject = new OBBject({ + results: results as T[], + provider: this.provider, + extra: { metadata }, + }) + + obbject.setRoute(this.route) + obbject.setStandardParams(this.params) + + return obbject + } +} diff --git a/packages/opentypebb/src/core/app/router.ts b/packages/opentypebb/src/core/app/router.ts new file mode 100644 index 00000000..d0c250ce --- /dev/null +++ b/packages/opentypebb/src/core/app/router.ts @@ -0,0 +1,218 @@ +/** + * Router — command registration and routing. + * Maps to: openbb_core/app/router.py + * + * In Python, Router wraps FastAPI's APIRouter with: + * - @router.command(model="...") decorator for registering commands + * - include_router() for hierarchical nesting + * - Auto-generates FastAPI routes with dependency injection + * + * In TypeScript, Router serves two purposes: + * 1. Library mode: getCommandMap() returns a flat map of commands + * 2. HTTP mode: mountToHono() generates Hono routes + * + * Each command is a thin function that delegates to Query.execute(). + */ + +import type { Hono } from 'hono' +import type { QueryExecutor } from '../provider/query-executor.js' + +/** + * Coerce a URL query-string value to an appropriate JS type. + * URL params are always strings, but Zod schemas (like OpenBB's Pydantic models) + * expect native numbers/booleans. FastAPI does this automatically; we replicate it here. + */ +function coerceQueryValue(value: string): unknown { + // Boolean + if (value === 'true') return true + if (value === 'false') return false + // Null + if (value === 'null' || value === 'none' || value === 'None') return null + // Number (integer or float) — but NOT date-like strings like "2024-01-01" + if (/^-?\d+$/.test(value)) return Number(value) + if (/^-?\d+\.\d+$/.test(value)) return Number(value) + // Keep as string + return value +} + +/** + * A registered command handler. + * The handler receives params + credentials and returns the raw result. + */ +export interface CommandHandler { + ( + executor: QueryExecutor, + provider: string, + params: Record, + credentials: Record | null, + ): Promise +} + +/** Command definition registered in a Router. */ +export interface CommandDef { + /** Standard model name (e.g., "EquityHistorical"). */ + model: string + /** Route path segment (e.g., "/historical"). */ + path: string + /** Human-readable description. */ + description: string + /** The handler function. */ + handler: CommandHandler +} + +/** + * Router class for registering commands and building routes. + * + * Usage in extensions (maps to Python's @router.command pattern): + * + * ```typescript + * const router = new Router({ prefix: '/price' }) + * + * router.command({ + * model: 'EquityQuote', + * path: '/quote', + * description: 'Get the latest quote for a given stock.', + * handler: async (executor, provider, params, credentials) => { + * return executor.execute(provider, 'EquityQuote', params, credentials) + * }, + * }) + * ``` + */ +export class Router { + readonly prefix: string + readonly description?: string + private readonly _commands: CommandDef[] = [] + private readonly _subRouters: Array<{ prefix: string; router: Router }> = [] + + constructor(opts: { prefix?: string; description?: string } = {}) { + this.prefix = opts.prefix ?? '' + this.description = opts.description + } + + /** + * Register a command. + * Maps to: @router.command(model="...", ...) in router.py + */ + command(def: CommandDef): void { + this._commands.push(def) + } + + /** + * Include a sub-router. + * Maps to: router.include_router(sub_router, prefix="/price") in router.py + */ + includeRouter(router: Router, prefix?: string): void { + this._subRouters.push({ + prefix: prefix ?? router.prefix, + router, + }) + } + + /** + * Get all commands as a flat map of {fullPath: CommandDef}. + * Used in library mode for direct invocation. + */ + getCommandMap(basePath = ''): Map { + const map = new Map() + const fullPrefix = basePath + this.prefix + + for (const cmd of this._commands) { + map.set(fullPrefix + cmd.path, cmd) + } + + for (const { router } of this._subRouters) { + // Let the sub-router add its own prefix — don't add the stored prefix too + // (stored prefix defaults to router.prefix, so it would be applied twice) + const subMap = router.getCommandMap(fullPrefix) + for (const [path, cmd] of subMap) { + map.set(path, cmd) + } + } + + return map + } + + /** + * Get all registered model names. + * Useful for discovering available commands. + */ + getModelNames(basePath = ''): string[] { + const names: string[] = [] + const fullPrefix = basePath + this.prefix + + for (const cmd of this._commands) { + names.push(cmd.model) + } + + for (const { router } of this._subRouters) { + names.push(...router.getModelNames(fullPrefix)) + } + + return names + } + + /** + * Mount all commands as Hono GET routes. + * Maps to: AppLoader.add_routers() / RouterLoader in rest_api.py + * + * Each command becomes: GET /api/v1/{extension}/{path}?params... + * - Provider is taken from ?provider= query param + * - Credentials from X-OpenBB-Credentials header + */ + mountToHono( + app: Hono, + executor: QueryExecutor, + basePath = '/api/v1', + ): void { + const commands = this.getCommandMap(basePath) + + for (const [path, cmd] of commands) { + app.get(path, async (c) => { + const url = new URL(c.req.url) + const params: Record = {} + for (const [key, value] of url.searchParams) { + // Coerce URL query param strings to appropriate JS types. + // FastAPI does this automatically via type annotations; we replicate it here. + params[key] = coerceQueryValue(value) + } + + // Extract provider from query params (matches OpenBB behavior) + const provider = (params.provider as string) ?? '' + delete params.provider + + // Parse credentials from header + const credHeader = c.req.header('X-OpenBB-Credentials') + let credentials: Record | null = null + if (credHeader) { + try { + credentials = JSON.parse(credHeader) + } catch { + // Ignore malformed credential header + } + } + + try { + const result = await cmd.handler(executor, provider, params, credentials) + // Wrap in OBBject-compatible envelope (matches OpenBB Python response format) + return c.json({ + results: Array.isArray(result) ? result : [result], + provider, + warnings: null, + chart: null, + extra: {}, + }) + } catch (error) { + const message = error instanceof Error ? error.message : String(error) + return c.json({ + results: null, + provider, + warnings: null, + chart: null, + extra: {}, + error: message, + }, 500) + } + }) + } + } +} diff --git a/packages/opentypebb/src/core/provider/abstract/data.ts b/packages/opentypebb/src/core/provider/abstract/data.ts new file mode 100644 index 00000000..ac4639e0 --- /dev/null +++ b/packages/opentypebb/src/core/provider/abstract/data.ts @@ -0,0 +1,37 @@ +/** + * Base Data schema. + * Maps to: openbb_core/provider/abstract/data.py + * + * In Python, Data is a Pydantic BaseModel with: + * - __alias_dict__: maps {newFieldName: originalFieldName} for input aliasing + * - AliasGenerator with camelCase validation / snake_case serialization + * - ConfigDict(extra="allow", populate_by_name=True, strict=False) + * - model_validator(mode="before") that applies __alias_dict__ + * + * In TypeScript, we use Zod schemas. The alias handling is done explicitly + * in each Fetcher's transformData via the applyAliases() helper. + */ + +import { z } from 'zod' + +/** + * Base Data schema — empty by default. + * Standard models extend this with their specific fields. + * Provider-specific models further extend the standard. + * + * Using .passthrough() to match Python's ConfigDict(extra="allow"). + */ +export const BaseDataSchema = z.object({}).passthrough() + +export type BaseData = z.infer + +/** + * ForceInt — coerces a value to integer. + * Maps to: ForceInt = Annotated[int, BeforeValidator(check_int)] in data.py + */ +export const ForceInt = z.preprocess((v) => { + if (v === null || v === undefined) return v + const n = Number(v) + if (isNaN(n)) return v + return Math.trunc(n) +}, z.number().int().nullable()) diff --git a/packages/opentypebb/src/core/provider/abstract/fetcher.ts b/packages/opentypebb/src/core/provider/abstract/fetcher.ts new file mode 100644 index 00000000..049fcc95 --- /dev/null +++ b/packages/opentypebb/src/core/provider/abstract/fetcher.ts @@ -0,0 +1,102 @@ +/** + * Abstract Fetcher class — the TET (Transform, Extract, Transform) pipeline. + * Maps to: openbb_core/provider/abstract/fetcher.py + * + * In Python, Fetcher is Generic[Q, R] with three static methods: + * 1. transform_query(params: dict) -> Q — validate & coerce input params + * 2. extract_data(query: Q, creds) -> Any — fetch raw data from provider API + * 3. transform_data(query: Q, data: Any) -> R — parse raw data into typed output + * 4. fetch_data() orchestrates the above pipeline + * + * Subclasses implement either extract_data (sync) or aextract_data (async). + * In TypeScript, we only need async (extractData is always async). + * + * Fetcher classes are never instantiated — all methods are static. + * This matches the Python pattern where all methods are @staticmethod. + */ + +/** Type for a Fetcher class (not instance). */ +export interface FetcherClass { + /** Whether this fetcher requires provider credentials. */ + requireCredentials: boolean + + /** Transform raw params dict into typed query object. */ + transformQuery(params: Record): unknown + + /** Extract raw data from the provider API. */ + extractData( + query: unknown, + credentials: Record | null, + ): Promise + + /** Transform raw data into typed result. */ + transformData(query: unknown, data: unknown): unknown + + /** Full pipeline: transformQuery → extractData → transformData */ + fetchData( + params: Record, + credentials?: Record | null, + ): Promise +} + +/** + * Abstract Fetcher base class. + * + * Each provider model creates a concrete subclass: + * + * ```typescript + * export class FMPEquityProfileFetcher extends Fetcher { + * static requireCredentials = true + * + * static transformQuery(params) { + * return FMPEquityProfileQueryParamsSchema.parse(params) + * } + * + * static async extractData(query, credentials) { + * const apiKey = credentials?.fmp_api_key ?? '' + * return await amakeRequest(`https://...?apikey=${apiKey}`) + * } + * + * static transformData(query, data) { + * return data.map(d => FMPEquityProfileDataSchema.parse(applyAliases(d, aliasDict))) + * } + * } + * ``` + */ +export abstract class Fetcher { + /** Whether this fetcher requires provider credentials. Can be overridden by subclasses. */ + static requireCredentials = true + + /** Transform the params to the provider-specific query. */ + static transformQuery(_params: Record): unknown { + throw new Error('transformQuery not implemented') + } + + /** Extract the data from the provider (async). */ + static async extractData( + _query: unknown, + _credentials: Record | null, + ): Promise { + throw new Error('extractData not implemented') + } + + /** Transform the provider-specific data. */ + static transformData(_query: unknown, _data: unknown): unknown { + throw new Error('transformData not implemented') + } + + /** + * Fetch data from a provider. + * Orchestrates the TET pipeline: transformQuery → extractData → transformData. + * + * Maps to: Fetcher.fetch_data() in fetcher.py + */ + static async fetchData( + params: Record, + credentials: Record | null = null, + ): Promise { + const query = this.transformQuery(params) + const data = await this.extractData(query, credentials) + return this.transformData(query, data) + } +} diff --git a/packages/opentypebb/src/core/provider/abstract/provider.ts b/packages/opentypebb/src/core/provider/abstract/provider.ts new file mode 100644 index 00000000..16412fa5 --- /dev/null +++ b/packages/opentypebb/src/core/provider/abstract/provider.ts @@ -0,0 +1,64 @@ +/** + * Provider class. + * Maps to: openbb_core/provider/abstract/provider.py + * + * Serves as the provider extension entry point. Each data provider + * (yfinance, fmp, sec, etc.) creates a Provider instance with its + * name, description, credentials, and a fetcher_dict mapping model + * names to Fetcher classes. + */ + +import type { FetcherClass } from './fetcher.js' + +export interface ProviderConfig { + /** Short name of the provider (e.g., "fmp", "yfinance"). */ + name: string + /** Description of the provider. */ + description: string + /** Website URL of the provider. */ + website?: string + /** + * List of required credential names (without provider prefix). + * Will be auto-prefixed with the provider name. + * Example: ["api_key"] → ["fmp_api_key"] + */ + credentials?: string[] + /** + * Dictionary mapping model names to Fetcher classes. + * Example: { "EquityHistorical": FMPEquityHistoricalFetcher } + */ + fetcherDict: Record + /** Full display name of the provider. */ + reprName?: string + /** Instructions on how to set up the provider (e.g., how to get an API key). */ + instructions?: string +} + +export class Provider { + readonly name: string + readonly description: string + readonly website?: string + readonly credentials: string[] + readonly fetcherDict: Record + readonly reprName?: string + readonly instructions?: string + + constructor(config: ProviderConfig) { + this.name = config.name + this.description = config.description + this.website = config.website + this.fetcherDict = config.fetcherDict + this.reprName = config.reprName + this.instructions = config.instructions + + // Auto-prefix credentials with provider name (matches Python behavior) + // Example: credentials=["api_key"], name="fmp" → ["fmp_api_key"] + if (config.credentials) { + this.credentials = config.credentials.map( + (c) => `${this.name.toLowerCase()}_${c}`, + ) + } else { + this.credentials = [] + } + } +} diff --git a/packages/opentypebb/src/core/provider/abstract/query-params.ts b/packages/opentypebb/src/core/provider/abstract/query-params.ts new file mode 100644 index 00000000..b4865ca1 --- /dev/null +++ b/packages/opentypebb/src/core/provider/abstract/query-params.ts @@ -0,0 +1,25 @@ +/** + * Base QueryParams schema. + * Maps to: openbb_core/provider/abstract/query_params.py + * + * In Python, QueryParams is a Pydantic BaseModel with: + * - __alias_dict__: maps field names to API aliases for model_dump() + * - __json_schema_extra__: provider-specific schema hints + * - ConfigDict(extra="allow", populate_by_name=True) + * + * In TypeScript, we use Zod schemas. Extensions use .extend() to add fields. + * Alias handling is done in the Fetcher's transformQuery via applyAliases(). + */ + +import { z } from 'zod' + +/** + * Base QueryParams schema — empty by default. + * Standard models extend this with their specific fields. + * Provider-specific models further extend the standard with extra fields. + * + * Using .passthrough() to match Python's ConfigDict(extra="allow"). + */ +export const BaseQueryParamsSchema = z.object({}).passthrough() + +export type BaseQueryParams = z.infer diff --git a/packages/opentypebb/src/core/provider/query-executor.ts b/packages/opentypebb/src/core/provider/query-executor.ts new file mode 100644 index 00000000..71a60783 --- /dev/null +++ b/packages/opentypebb/src/core/provider/query-executor.ts @@ -0,0 +1,98 @@ +/** + * Query Executor. + * Maps to: openbb_core/provider/query_executor.py + * + * Resolves provider + model name to a Fetcher class, + * filters credentials, and executes the TET pipeline. + */ + +import type { FetcherClass } from './abstract/fetcher.js' +import type { Provider } from './abstract/provider.js' +import type { Registry } from './registry.js' +import { OpenBBError } from './utils/errors.js' + +export class QueryExecutor { + constructor(private readonly registry: Registry) {} + + /** Get a provider from the registry. */ + getProvider(providerName: string): Provider { + const name = providerName.toLowerCase() + const provider = this.registry.providers.get(name) + if (!provider) { + const available = [...this.registry.providers.keys()] + throw new OpenBBError( + `Provider '${name}' not found in the registry. Available providers: ${available.join(', ')}`, + ) + } + return provider + } + + /** Get a fetcher from a provider by model name. */ + getFetcher(provider: Provider, modelName: string): FetcherClass { + const fetcher = provider.fetcherDict[modelName] + if (!fetcher) { + throw new OpenBBError( + `Fetcher not found for model '${modelName}' in provider '${provider.name}'.`, + ) + } + return fetcher + } + + /** + * Filter credentials to only include those required by the provider. + * Maps to: QueryExecutor.filter_credentials() in query_executor.py + */ + static filterCredentials( + credentials: Record | null, + provider: Provider, + requireCredentials: boolean, + ): Record { + const filtered: Record = {} + + if (provider.credentials.length > 0) { + const creds = credentials ?? {} + + for (const c of provider.credentials) { + const v = creds[c] + if (!v) { + if (requireCredentials) { + const website = provider.website ?? '' + const extraMsg = website ? ` Check ${website} to get it.` : '' + throw new OpenBBError( + `Missing credential '${c}'.${extraMsg}`, + ) + } + } else { + filtered[c] = v + } + } + } + + return filtered + } + + /** + * Execute a query against a provider. + * + * @param providerName - Name of the provider (e.g., "fmp"). + * @param modelName - Name of the model (e.g., "EquityHistorical"). + * @param params - Query parameters (e.g., { symbol: "AAPL" }). + * @param credentials - Provider credentials (e.g., { fmp_api_key: "..." }). + * @returns Query result from the fetcher. + */ + async execute( + providerName: string, + modelName: string, + params: Record, + credentials: Record | null = null, + ): Promise { + const provider = this.getProvider(providerName) + const fetcher = this.getFetcher(provider, modelName) + const filteredCredentials = QueryExecutor.filterCredentials( + credentials, + provider, + fetcher.requireCredentials, + ) + return fetcher.fetchData(params, filteredCredentials) + } +} diff --git a/packages/opentypebb/src/core/provider/registry.ts b/packages/opentypebb/src/core/provider/registry.ts new file mode 100644 index 00000000..b271bb74 --- /dev/null +++ b/packages/opentypebb/src/core/provider/registry.ts @@ -0,0 +1,24 @@ +/** + * Provider Registry. + * Maps to: openbb_core/provider/registry.py + * + * Maintains a registry of all available providers. + * In Python, RegistryLoader uses entry_points for dynamic discovery. + * In TypeScript, providers are explicitly imported and registered. + */ + +import type { Provider } from './abstract/provider.js' + +export class Registry { + private readonly _providers = new Map() + + /** Return a map of registered providers. */ + get providers(): ReadonlyMap { + return this._providers + } + + /** Include a provider in the registry. */ + includeProvider(provider: Provider): void { + this._providers.set(provider.name.toLowerCase(), provider) + } +} diff --git a/packages/opentypebb/src/core/provider/utils/errors.ts b/packages/opentypebb/src/core/provider/utils/errors.ts new file mode 100644 index 00000000..7b91fa83 --- /dev/null +++ b/packages/opentypebb/src/core/provider/utils/errors.ts @@ -0,0 +1,32 @@ +/** + * Error classes for OpenTypeBB. + * Maps to: openbb_core/app/model/abstract/error.py + * openbb_core/provider/utils/errors.py + */ + +/** Base error for all OpenBB errors. */ +export class OpenBBError extends Error { + readonly original?: unknown + + constructor(message: string, original?: unknown) { + super(message) + this.name = 'OpenBBError' + this.original = original + } +} + +/** Raised when a query returns no data. */ +export class EmptyDataError extends OpenBBError { + constructor(message = 'No data found.') { + super(message) + this.name = 'EmptyDataError' + } +} + +/** Raised when credentials are missing or invalid. */ +export class UnauthorizedError extends OpenBBError { + constructor(message = 'Unauthorized.') { + super(message) + this.name = 'UnauthorizedError' + } +} diff --git a/packages/opentypebb/src/core/provider/utils/helpers.ts b/packages/opentypebb/src/core/provider/utils/helpers.ts new file mode 100644 index 00000000..3dfdd4f8 --- /dev/null +++ b/packages/opentypebb/src/core/provider/utils/helpers.ts @@ -0,0 +1,144 @@ +/** + * HTTP helpers and utility functions. + * Maps to: openbb_core/provider/utils/helpers.py + */ + +import { OpenBBError } from './errors.js' + +/** + * Make an async HTTP request and return the parsed JSON response. + * Maps to: amake_request() in helpers.py + * + * @param url - The URL to request. + * @param options - Optional fetch options. + * @param responseCallback - Optional callback to process the response before parsing. + * @param timeoutMs - Request timeout in milliseconds (default: 30000). + * @returns Parsed JSON response. + */ +export async function amakeRequest( + url: string, + options: { + method?: string + headers?: Record + body?: string + timeoutMs?: number + responseCallback?: (response: Response) => Promise + } = {}, +): Promise { + const { method = 'GET', headers, body, timeoutMs = 30_000, responseCallback } = options + + let response: Response + try { + response = await fetch(url, { + method, + headers, + body, + signal: AbortSignal.timeout(timeoutMs), + }) + } catch (error) { + if (error instanceof DOMException && error.name === 'TimeoutError') { + throw new OpenBBError(`Request timed out after ${timeoutMs}ms: ${url}`) + } + throw new OpenBBError(`Request failed: ${url}`, error) + } + + if (responseCallback) { + response = await responseCallback(response) + } + + if (!response.ok) { + const text = await response.text().catch(() => '') + throw new OpenBBError( + `HTTP ${response.status} ${response.statusText}: ${url}${text ? ` - ${text}` : ''}`, + ) + } + + try { + return (await response.json()) as T + } catch (error) { + throw new OpenBBError(`Failed to parse JSON response from: ${url}`, error) + } +} + +/** + * Apply alias dictionary to a data record. + * Maps to: Data.__alias_dict__ + _use_alias model_validator in data.py + * + * The alias dict maps {targetFieldName: sourceFieldName}. + * This renames source keys to target keys in the data. + * + * @param data - The raw data object. + * @param aliasDict - Mapping of {targetName: sourceName}. + * @returns Data with renamed keys. + */ +export function applyAliases( + data: Record, + aliasDict: Record, +): Record { + if (!aliasDict || Object.keys(aliasDict).length === 0) return data + + const result: Record = { ...data } + + // aliasDict maps {newName: originalName} + for (const [newName, originalName] of Object.entries(aliasDict)) { + if (originalName in result) { + result[newName] = result[originalName] + if (newName !== originalName) { + delete result[originalName] + } + } + } + + return result +} + +/** + * Replace empty strings and "NA" with null in a data record. + * Common pattern in FMP/YFinance providers. + */ +export function replaceEmptyStrings( + data: Record, +): Record { + const result: Record = {} + for (const [key, value] of Object.entries(data)) { + result[key] = value === '' || value === 'NA' ? null : value + } + return result +} + +/** + * Make an HTTP GET request using Node's native https module. + * Bypasses the undici global dispatcher (and its proxy agent). + * Useful for APIs that are incompatible with HTTP proxy tunneling + * (e.g. OECD SDMX, ECB, IMF) but are accessible via OS network stack (TUN). + */ +export async function nativeFetch( + url: string, + options: { headers?: Record; timeoutMs?: number } = {}, +): Promise<{ status: number; text: string }> { + const { headers, timeoutMs = 30_000 } = options + const mod = url.startsWith('https') ? await import('https') : await import('http') + + return new Promise((resolve, reject) => { + const req = mod.get(url, { headers, timeout: timeoutMs }, (res) => { + let body = '' + res.on('data', (chunk: Buffer) => { body += chunk.toString() }) + res.on('end', () => resolve({ status: res.statusCode ?? 0, text: body })) + }) + req.on('error', reject) + req.on('timeout', () => { req.destroy(); reject(new OpenBBError(`Request timed out: ${url}`)) }) + }) +} + +/** + * Build a query string from params, omitting null/undefined values. + */ +export function buildQueryString(params: Record): string { + const searchParams = new URLSearchParams() + for (const [key, value] of Object.entries(params)) { + if (value !== null && value !== undefined) { + searchParams.set(key, String(value)) + } + } + return searchParams.toString() +} diff --git a/packages/opentypebb/src/core/utils/proxy.ts b/packages/opentypebb/src/core/utils/proxy.ts new file mode 100644 index 00000000..388a4f1b --- /dev/null +++ b/packages/opentypebb/src/core/utils/proxy.ts @@ -0,0 +1,20 @@ +/** + * Proxy bootstrap — makes globalThis.fetch proxy-aware via undici. + * + * Call setupProxy() ONCE at server startup, BEFORE any fetch calls. + * Reads HTTP_PROXY / HTTPS_PROXY / NO_PROXY from environment. + * If no proxy env vars are set, this is a no-op. + */ +import { EnvHttpProxyAgent, setGlobalDispatcher } from 'undici' + +export function setupProxy(): void { + const proxy = process.env.HTTPS_PROXY || process.env.HTTP_PROXY + || process.env.https_proxy || process.env.http_proxy + if (!proxy) return + + // EnvHttpProxyAgent auto-reads HTTP_PROXY/HTTPS_PROXY/NO_PROXY + const agent = new EnvHttpProxyAgent() + setGlobalDispatcher(agent) + + console.log(`[proxy] Using proxy: ${proxy}`) +} diff --git a/packages/opentypebb/src/extensions/crypto/crypto-router.ts b/packages/opentypebb/src/extensions/crypto/crypto-router.ts new file mode 100644 index 00000000..bdee31e2 --- /dev/null +++ b/packages/opentypebb/src/extensions/crypto/crypto-router.ts @@ -0,0 +1,27 @@ +/** + * Crypto Router — root router for cryptocurrency market data. + * Maps to: openbb_crypto/crypto_router.py + */ + +import { Router } from '../../core/app/router.js' +import { cryptoPriceRouter } from './price/price-router.js' + +export const cryptoRouter = new Router({ + prefix: '/crypto', + description: 'Cryptocurrency market data.', +}) + +// --- Include sub-routers --- + +cryptoRouter.includeRouter(cryptoPriceRouter) + +// --- Root-level commands --- + +cryptoRouter.command({ + model: 'CryptoSearch', + path: '/search', + description: 'Search available cryptocurrency pairs within a provider.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'CryptoSearch', params, credentials) + }, +}) diff --git a/packages/opentypebb/src/extensions/crypto/price/price-router.ts b/packages/opentypebb/src/extensions/crypto/price/price-router.ts new file mode 100644 index 00000000..0544724e --- /dev/null +++ b/packages/opentypebb/src/extensions/crypto/price/price-router.ts @@ -0,0 +1,20 @@ +/** + * Crypto Price Router. + * Maps to: openbb_crypto/price/price_router.py + */ + +import { Router } from '../../../core/app/router.js' + +export const cryptoPriceRouter = new Router({ + prefix: '/price', + description: 'Cryptocurrency price data.', +}) + +cryptoPriceRouter.command({ + model: 'CryptoHistorical', + path: '/historical', + description: 'Get historical price data for cryptocurrency pair(s).', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'CryptoHistorical', params, credentials) + }, +}) diff --git a/packages/opentypebb/src/extensions/currency/currency-router.ts b/packages/opentypebb/src/extensions/currency/currency-router.ts new file mode 100644 index 00000000..fc7a3cda --- /dev/null +++ b/packages/opentypebb/src/extensions/currency/currency-router.ts @@ -0,0 +1,45 @@ +/** + * Currency Router — root router for foreign exchange market data. + * Maps to: openbb_currency/currency_router.py + */ + +import { Router } from '../../core/app/router.js' +import { currencyPriceRouter } from './price/price-router.js' + +export const currencyRouter = new Router({ + prefix: '/currency', + description: 'Foreign exchange (FX) market data.', +}) + +// --- Include sub-routers --- + +currencyRouter.includeRouter(currencyPriceRouter) + +// --- Root-level commands --- + +currencyRouter.command({ + model: 'CurrencyPairs', + path: '/search', + description: 'Search available currency pairs.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'CurrencyPairs', params, credentials) + }, +}) + +currencyRouter.command({ + model: 'CurrencyReferenceRates', + path: '/reference_rates', + description: 'Get current, official, currency reference rates.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'CurrencyReferenceRates', params, credentials) + }, +}) + +currencyRouter.command({ + model: 'CurrencySnapshots', + path: '/snapshots', + description: 'Get snapshots of currency exchange rates.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'CurrencySnapshots', params, credentials) + }, +}) diff --git a/packages/opentypebb/src/extensions/currency/price/price-router.ts b/packages/opentypebb/src/extensions/currency/price/price-router.ts new file mode 100644 index 00000000..2c69d8f5 --- /dev/null +++ b/packages/opentypebb/src/extensions/currency/price/price-router.ts @@ -0,0 +1,20 @@ +/** + * Currency Price Router. + * Maps to: openbb_currency/price/price_router.py + */ + +import { Router } from '../../../core/app/router.js' + +export const currencyPriceRouter = new Router({ + prefix: '/price', + description: 'Currency price data.', +}) + +currencyPriceRouter.command({ + model: 'CurrencyHistorical', + path: '/historical', + description: 'Get historical price data for a currency pair.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'CurrencyHistorical', params, credentials) + }, +}) diff --git a/packages/opentypebb/src/extensions/derivatives/derivatives-router.ts b/packages/opentypebb/src/extensions/derivatives/derivatives-router.ts new file mode 100644 index 00000000..12833bb9 --- /dev/null +++ b/packages/opentypebb/src/extensions/derivatives/derivatives-router.ts @@ -0,0 +1,74 @@ +/** + * Derivatives Router — root router for derivatives market data. + * Maps to: openbb_derivatives/derivatives_router.py + */ + +import { Router } from '../../core/app/router.js' + +export const derivativesRouter = new Router({ + prefix: '/derivatives', + description: 'Derivatives market data.', +}) + +derivativesRouter.command({ + model: 'FuturesHistorical', + path: '/futures/historical', + description: 'Get historical price data for futures contracts.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'FuturesHistorical', params, credentials) + }, +}) + +derivativesRouter.command({ + model: 'FuturesCurve', + path: '/futures/curve', + description: 'Get the futures term structure (curve) for a symbol.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'FuturesCurve', params, credentials) + }, +}) + +derivativesRouter.command({ + model: 'FuturesInfo', + path: '/futures/info', + description: 'Get information about futures contracts.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'FuturesInfo', params, credentials) + }, +}) + +derivativesRouter.command({ + model: 'FuturesInstruments', + path: '/futures/instruments', + description: 'Get the list of available futures instruments.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'FuturesInstruments', params, credentials) + }, +}) + +derivativesRouter.command({ + model: 'OptionsChains', + path: '/options/chains', + description: 'Get the complete options chain for a given symbol.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'OptionsChains', params, credentials) + }, +}) + +derivativesRouter.command({ + model: 'OptionsSnapshots', + path: '/options/snapshots', + description: 'Get current snapshot data for options.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'OptionsSnapshots', params, credentials) + }, +}) + +derivativesRouter.command({ + model: 'OptionsUnusual', + path: '/options/unusual', + description: 'Get unusual options activity data.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'OptionsUnusual', params, credentials) + }, +}) diff --git a/packages/opentypebb/src/extensions/economy/economy-router.ts b/packages/opentypebb/src/extensions/economy/economy-router.ts new file mode 100644 index 00000000..4da6859b --- /dev/null +++ b/packages/opentypebb/src/extensions/economy/economy-router.ts @@ -0,0 +1,128 @@ +/** + * Economy Router. + * Maps to: openbb_economy/economy_router.py + */ + +import { Router } from '../../core/app/router.js' + +export const economyRouter = new Router({ + prefix: '/economy', + description: 'Economic data.', +}) + +economyRouter.command({ + model: 'EconomicCalendar', + path: '/calendar', + description: 'Get the upcoming and historical economic calendar events.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'EconomicCalendar', params, credentials) + }, +}) + +economyRouter.command({ + model: 'TreasuryRates', + path: '/treasury_rates', + description: 'Get current and historical Treasury rates.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'TreasuryRates', params, credentials) + }, +}) + +economyRouter.command({ + model: 'DiscoveryFilings', + path: '/discovery_filings', + description: 'Search and discover SEC filings by form type and date range.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'DiscoveryFilings', params, credentials) + }, +}) + +economyRouter.command({ + model: 'AvailableIndicators', + path: '/available_indicators', + description: 'Get the list of available economic indicators.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'AvailableIndicators', params, credentials) + }, +}) + +economyRouter.command({ + model: 'ConsumerPriceIndex', + path: '/cpi', + description: 'Get Consumer Price Index (CPI) data.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'ConsumerPriceIndex', params, credentials) + }, +}) + +economyRouter.command({ + model: 'CompositeLeadingIndicator', + path: '/composite_leading_indicator', + description: 'Get Composite Leading Indicator (CLI) data from the OECD.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'CompositeLeadingIndicator', params, credentials) + }, +}) + +economyRouter.command({ + model: 'CountryInterestRates', + path: '/interest_rates', + description: 'Get short-term interest rates by country.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'CountryInterestRates', params, credentials) + }, +}) + +economyRouter.command({ + model: 'BalanceOfPayments', + path: '/balance_of_payments', + description: 'Get balance of payments data from the ECB.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'BalanceOfPayments', params, credentials) + }, +}) + +economyRouter.command({ + model: 'CentralBankHoldings', + path: '/central_bank_holdings', + description: 'Get central bank holdings data (Fed balance sheet).', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'CentralBankHoldings', params, credentials) + }, +}) + +economyRouter.command({ + model: 'CountryProfile', + path: '/country_profile', + description: 'Get a comprehensive economic profile for a country.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'CountryProfile', params, credentials) + }, +}) + +economyRouter.command({ + model: 'DirectionOfTrade', + path: '/direction_of_trade', + description: 'Get direction of trade statistics from the IMF.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'DirectionOfTrade', params, credentials) + }, +}) + +economyRouter.command({ + model: 'ExportDestinations', + path: '/export_destinations', + description: 'Get top export destinations for a country.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'ExportDestinations', params, credentials) + }, +}) + +economyRouter.command({ + model: 'EconomicIndicators', + path: '/indicators', + description: 'Get economic indicator time series data.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'EconomicIndicators', params, credentials) + }, +}) diff --git a/packages/opentypebb/src/extensions/equity/calendar/calendar-router.ts b/packages/opentypebb/src/extensions/equity/calendar/calendar-router.ts new file mode 100644 index 00000000..498e3a3e --- /dev/null +++ b/packages/opentypebb/src/extensions/equity/calendar/calendar-router.ts @@ -0,0 +1,56 @@ +/** + * Equity Calendar Router. + * Maps to: openbb_equity/calendar/calendar_router.py + */ + +import { Router } from '../../../core/app/router.js' + +export const calendarRouter = new Router({ + prefix: '/calendar', + description: 'Equity calendar data.', +}) + +calendarRouter.command({ + model: 'CalendarIpo', + path: '/ipo', + description: 'Get historical and upcoming initial public offerings (IPOs).', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'CalendarIpo', params, credentials) + }, +}) + +calendarRouter.command({ + model: 'CalendarDividend', + path: '/dividend', + description: 'Get historical and upcoming dividend payments.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'CalendarDividend', params, credentials) + }, +}) + +calendarRouter.command({ + model: 'CalendarSplits', + path: '/splits', + description: 'Get historical and upcoming stock split operations.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'CalendarSplits', params, credentials) + }, +}) + +calendarRouter.command({ + model: 'CalendarEvents', + path: '/events', + description: 'Get historical and upcoming company events.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'CalendarEvents', params, credentials) + }, +}) + +calendarRouter.command({ + model: 'CalendarEarnings', + path: '/earnings', + description: 'Get historical and upcoming company earnings releases.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'CalendarEarnings', params, credentials) + }, +}) diff --git a/packages/opentypebb/src/extensions/equity/compare/compare-router.ts b/packages/opentypebb/src/extensions/equity/compare/compare-router.ts new file mode 100644 index 00000000..32ba0f2a --- /dev/null +++ b/packages/opentypebb/src/extensions/equity/compare/compare-router.ts @@ -0,0 +1,38 @@ +/** + * Equity Compare Router. + * Maps to: openbb_equity/compare/compare_router.py + */ + +import { Router } from '../../../core/app/router.js' + +export const compareRouter = new Router({ + prefix: '/compare', + description: 'Equity comparison data.', +}) + +compareRouter.command({ + model: 'EquityPeers', + path: '/peers', + description: 'Get the closest peers for a given company.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'EquityPeers', params, credentials) + }, +}) + +compareRouter.command({ + model: 'CompareGroups', + path: '/groups', + description: 'Get company data grouped by sector, industry, or country.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'CompareGroups', params, credentials) + }, +}) + +compareRouter.command({ + model: 'CompareCompanyFacts', + path: '/company_facts', + description: 'Compare reported company facts and fundamental data points.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'CompareCompanyFacts', params, credentials) + }, +}) diff --git a/packages/opentypebb/src/extensions/equity/discovery/discovery-router.ts b/packages/opentypebb/src/extensions/equity/discovery/discovery-router.ts new file mode 100644 index 00000000..a8a08017 --- /dev/null +++ b/packages/opentypebb/src/extensions/equity/discovery/discovery-router.ts @@ -0,0 +1,101 @@ +/** + * Equity Discovery Router. + * Maps to: openbb_equity/discovery/discovery_router.py + */ + +import { Router } from '../../../core/app/router.js' + +export const discoveryRouter = new Router({ + prefix: '/discovery', + description: 'Equity discovery data.', +}) + +discoveryRouter.command({ + model: 'EquityGainers', + path: '/gainers', + description: 'Get the top price gainers in the stock market.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'EquityGainers', params, credentials) + }, +}) + +discoveryRouter.command({ + model: 'EquityLosers', + path: '/losers', + description: 'Get the top price losers in the stock market.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'EquityLosers', params, credentials) + }, +}) + +discoveryRouter.command({ + model: 'EquityActive', + path: '/active', + description: 'Get the most actively traded stocks based on volume.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'EquityActive', params, credentials) + }, +}) + +discoveryRouter.command({ + model: 'EquityUndervaluedLargeCaps', + path: '/undervalued_large_caps', + description: 'Get potentially undervalued large cap stocks.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'EquityUndervaluedLargeCaps', params, credentials) + }, +}) + +discoveryRouter.command({ + model: 'EquityUndervaluedGrowth', + path: '/undervalued_growth', + description: 'Get potentially undervalued growth stocks.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'EquityUndervaluedGrowth', params, credentials) + }, +}) + +discoveryRouter.command({ + model: 'EquityAggressiveSmallCaps', + path: '/aggressive_small_caps', + description: 'Get top small cap stocks based on earnings growth.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'EquityAggressiveSmallCaps', params, credentials) + }, +}) + +discoveryRouter.command({ + model: 'GrowthTechEquities', + path: '/growth_tech', + description: 'Get top tech stocks based on revenue and earnings growth.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'GrowthTechEquities', params, credentials) + }, +}) + +discoveryRouter.command({ + model: 'TopRetail', + path: '/top_retail', + description: 'Track over $30B USD/day of individual investors trades.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'TopRetail', params, credentials) + }, +}) + +discoveryRouter.command({ + model: 'DiscoveryFilings', + path: '/filings', + description: 'Get the URLs to SEC filings reported to the EDGAR database.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'DiscoveryFilings', params, credentials) + }, +}) + +discoveryRouter.command({ + model: 'LatestFinancialReports', + path: '/latest_financial_reports', + description: 'Get the newest quarterly, annual, and current reports for all companies.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'LatestFinancialReports', params, credentials) + }, +}) diff --git a/packages/opentypebb/src/extensions/equity/equity-router.ts b/packages/opentypebb/src/extensions/equity/equity-router.ts new file mode 100644 index 00000000..7c80f0e0 --- /dev/null +++ b/packages/opentypebb/src/extensions/equity/equity-router.ts @@ -0,0 +1,78 @@ +/** + * Equity Router — root router for equity market data. + * Maps to: openbb_equity/equity_router.py + * + * Includes sub-routers for price, fundamental, discovery, calendar, + * estimates, ownership, and compare. + */ + +import { Router } from '../../core/app/router.js' +import { priceRouter } from './price/price-router.js' +import { fundamentalRouter } from './fundamental/fundamental-router.js' +import { discoveryRouter } from './discovery/discovery-router.js' +import { calendarRouter } from './calendar/calendar-router.js' +import { estimatesRouter } from './estimates/estimates-router.js' +import { ownershipRouter } from './ownership/ownership-router.js' +import { compareRouter } from './compare/compare-router.js' + +export const equityRouter = new Router({ + prefix: '/equity', + description: 'Equity market data.', +}) + +// --- Include sub-routers --- + +equityRouter.includeRouter(priceRouter) +equityRouter.includeRouter(fundamentalRouter) +equityRouter.includeRouter(discoveryRouter) +equityRouter.includeRouter(calendarRouter) +equityRouter.includeRouter(estimatesRouter) +equityRouter.includeRouter(ownershipRouter) +equityRouter.includeRouter(compareRouter) + +// --- Root-level commands --- + +equityRouter.command({ + model: 'EquitySearch', + path: '/search', + description: 'Search for stock symbol, CIK, LEI, or company name.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'EquitySearch', params, credentials) + }, +}) + +equityRouter.command({ + model: 'EquityScreener', + path: '/screener', + description: 'Screen for companies meeting various criteria.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'EquityScreener', params, credentials) + }, +}) + +equityRouter.command({ + model: 'EquityInfo', + path: '/profile', + description: 'Get general information about a company.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'EquityInfo', params, credentials) + }, +}) + +equityRouter.command({ + model: 'MarketSnapshots', + path: '/market_snapshots', + description: 'Get an updated equity market snapshot.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'MarketSnapshots', params, credentials) + }, +}) + +equityRouter.command({ + model: 'HistoricalMarketCap', + path: '/historical_market_cap', + description: 'Get the historical market cap of a ticker symbol.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'HistoricalMarketCap', params, credentials) + }, +}) diff --git a/packages/opentypebb/src/extensions/equity/estimates/estimates-router.ts b/packages/opentypebb/src/extensions/equity/estimates/estimates-router.ts new file mode 100644 index 00000000..30086f8c --- /dev/null +++ b/packages/opentypebb/src/extensions/equity/estimates/estimates-router.ts @@ -0,0 +1,83 @@ +/** + * Equity Estimates Router. + * Maps to: openbb_equity/estimates/estimates_router.py + */ + +import { Router } from '../../../core/app/router.js' + +export const estimatesRouter = new Router({ + prefix: '/estimates', + description: 'Analyst estimates and price targets.', +}) + +estimatesRouter.command({ + model: 'PriceTarget', + path: '/price_target', + description: 'Get analyst price targets by company.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'PriceTarget', params, credentials) + }, +}) + +estimatesRouter.command({ + model: 'AnalystEstimates', + path: '/historical', + description: 'Get historical analyst estimates for earnings and revenue.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'AnalystEstimates', params, credentials) + }, +}) + +estimatesRouter.command({ + model: 'PriceTargetConsensus', + path: '/consensus', + description: 'Get consensus price target and recommendation.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'PriceTargetConsensus', params, credentials) + }, +}) + +estimatesRouter.command({ + model: 'AnalystSearch', + path: '/analyst_search', + description: 'Search for specific analysts and get their forecast track record.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'AnalystSearch', params, credentials) + }, +}) + +estimatesRouter.command({ + model: 'ForwardSalesEstimates', + path: '/forward_sales', + description: 'Get forward sales estimates.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'ForwardSalesEstimates', params, credentials) + }, +}) + +estimatesRouter.command({ + model: 'ForwardEbitdaEstimates', + path: '/forward_ebitda', + description: 'Get forward EBITDA estimates.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'ForwardEbitdaEstimates', params, credentials) + }, +}) + +estimatesRouter.command({ + model: 'ForwardEpsEstimates', + path: '/forward_eps', + description: 'Get forward EPS estimates.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'ForwardEpsEstimates', params, credentials) + }, +}) + +estimatesRouter.command({ + model: 'ForwardPeEstimates', + path: '/forward_pe', + description: 'Get forward PE estimates.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'ForwardPeEstimates', params, credentials) + }, +}) diff --git a/packages/opentypebb/src/extensions/equity/fundamental/fundamental-router.ts b/packages/opentypebb/src/extensions/equity/fundamental/fundamental-router.ts new file mode 100644 index 00000000..6fb21132 --- /dev/null +++ b/packages/opentypebb/src/extensions/equity/fundamental/fundamental-router.ts @@ -0,0 +1,236 @@ +/** + * Equity Fundamental Router. + * Maps to: openbb_equity/fundamental/fundamental_router.py + */ + +import { Router } from '../../../core/app/router.js' + +export const fundamentalRouter = new Router({ + prefix: '/fundamental', + description: 'Fundamental analysis data.', +}) + +fundamentalRouter.command({ + model: 'BalanceSheet', + path: '/balance', + description: 'Get the balance sheet for a given company.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'BalanceSheet', params, credentials) + }, +}) + +fundamentalRouter.command({ + model: 'BalanceSheetGrowth', + path: '/balance_growth', + description: 'Get the growth of a company\'s balance sheet items over time.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'BalanceSheetGrowth', params, credentials) + }, +}) + +fundamentalRouter.command({ + model: 'CashFlowStatement', + path: '/cash', + description: 'Get the cash flow statement for a given company.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'CashFlowStatement', params, credentials) + }, +}) + +fundamentalRouter.command({ + model: 'ReportedFinancials', + path: '/reported_financials', + description: 'Get financial statements as reported by the company.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'ReportedFinancials', params, credentials) + }, +}) + +fundamentalRouter.command({ + model: 'CashFlowStatementGrowth', + path: '/cash_growth', + description: 'Get the growth of a company\'s cash flow statement items over time.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'CashFlowStatementGrowth', params, credentials) + }, +}) + +fundamentalRouter.command({ + model: 'HistoricalDividends', + path: '/dividends', + description: 'Get historical dividend data for a given company.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'HistoricalDividends', params, credentials) + }, +}) + +fundamentalRouter.command({ + model: 'HistoricalEps', + path: '/historical_eps', + description: 'Get historical earnings per share data for a given company.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'HistoricalEps', params, credentials) + }, +}) + +fundamentalRouter.command({ + model: 'HistoricalEmployees', + path: '/employee_count', + description: 'Get historical employee count data for a given company.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'HistoricalEmployees', params, credentials) + }, +}) + +fundamentalRouter.command({ + model: 'SearchAttributes', + path: '/search_attributes', + description: 'Search Intrinio data tags to search in latest or historical attributes.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'SearchAttributes', params, credentials) + }, +}) + +fundamentalRouter.command({ + model: 'LatestAttributes', + path: '/latest_attributes', + description: 'Get the latest value of a data tag from Intrinio.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'LatestAttributes', params, credentials) + }, +}) + +fundamentalRouter.command({ + model: 'HistoricalAttributes', + path: '/historical_attributes', + description: 'Get the historical values of a data tag from Intrinio.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'HistoricalAttributes', params, credentials) + }, +}) + +fundamentalRouter.command({ + model: 'IncomeStatement', + path: '/income', + description: 'Get the income statement for a given company.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'IncomeStatement', params, credentials) + }, +}) + +fundamentalRouter.command({ + model: 'IncomeStatementGrowth', + path: '/income_growth', + description: 'Get the growth of a company\'s income statement items over time.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'IncomeStatementGrowth', params, credentials) + }, +}) + +fundamentalRouter.command({ + model: 'KeyMetrics', + path: '/metrics', + description: 'Get fundamental metrics for a given company.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'KeyMetrics', params, credentials) + }, +}) + +fundamentalRouter.command({ + model: 'KeyExecutives', + path: '/management', + description: 'Get executive management team data for a given company.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'KeyExecutives', params, credentials) + }, +}) + +fundamentalRouter.command({ + model: 'ExecutiveCompensation', + path: '/management_compensation', + description: 'Get executive management team compensation for a given company over time.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'ExecutiveCompensation', params, credentials) + }, +}) + +fundamentalRouter.command({ + model: 'FinancialRatios', + path: '/ratios', + description: 'Get an extensive set of financial and accounting ratios for a given company over time.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'FinancialRatios', params, credentials) + }, +}) + +fundamentalRouter.command({ + model: 'RevenueGeographic', + path: '/revenue_per_geography', + description: 'Get the geographic revenue breakdown for a given company over time.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'RevenueGeographic', params, credentials) + }, +}) + +fundamentalRouter.command({ + model: 'RevenueBusinessLine', + path: '/revenue_per_segment', + description: 'Get the revenue breakdown by business segment for a given company over time.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'RevenueBusinessLine', params, credentials) + }, +}) + +fundamentalRouter.command({ + model: 'CompanyFilings', + path: '/filings', + description: 'Get the URLs to SEC filings reported to the EDGAR database.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'CompanyFilings', params, credentials) + }, +}) + +fundamentalRouter.command({ + model: 'HistoricalSplits', + path: '/historical_splits', + description: 'Get historical stock splits for a given company.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'HistoricalSplits', params, credentials) + }, +}) + +fundamentalRouter.command({ + model: 'EarningsCallTranscript', + path: '/transcript', + description: 'Get earnings call transcripts for a given company.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'EarningsCallTranscript', params, credentials) + }, +}) + +fundamentalRouter.command({ + model: 'TrailingDividendYield', + path: '/trailing_dividend_yield', + description: 'Get the 1 year trailing dividend yield for a given company over time.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'TrailingDividendYield', params, credentials) + }, +}) + +fundamentalRouter.command({ + model: 'ManagementDiscussionAnalysis', + path: '/management_discussion_analysis', + description: 'Get the Management Discussion & Analysis section from financial statements.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'ManagementDiscussionAnalysis', params, credentials) + }, +}) + +fundamentalRouter.command({ + model: 'EsgScore', + path: '/esg_score', + description: 'Get ESG (Environmental, Social, and Governance) scores from company disclosures.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'EsgScore', params, credentials) + }, +}) diff --git a/packages/opentypebb/src/extensions/equity/ownership/ownership-router.ts b/packages/opentypebb/src/extensions/equity/ownership/ownership-router.ts new file mode 100644 index 00000000..74b925b7 --- /dev/null +++ b/packages/opentypebb/src/extensions/equity/ownership/ownership-router.ts @@ -0,0 +1,65 @@ +/** + * Equity Ownership Router. + * Maps to: openbb_equity/ownership/ownership_router.py + */ + +import { Router } from '../../../core/app/router.js' + +export const ownershipRouter = new Router({ + prefix: '/ownership', + description: 'Equity ownership data.', +}) + +ownershipRouter.command({ + model: 'EquityOwnership', + path: '/major_holders', + description: 'Get data about major holders for a given company over time.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'EquityOwnership', params, credentials) + }, +}) + +ownershipRouter.command({ + model: 'InstitutionalOwnership', + path: '/institutional', + description: 'Get net statistics on institutional ownership, reported on 13-F filings.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'InstitutionalOwnership', params, credentials) + }, +}) + +ownershipRouter.command({ + model: 'InsiderTrading', + path: '/insider_trading', + description: 'Get data about trading by a company\'s management team and board of directors.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'InsiderTrading', params, credentials) + }, +}) + +ownershipRouter.command({ + model: 'ShareStatistics', + path: '/share_statistics', + description: 'Get data about share float for a given company.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'ShareStatistics', params, credentials) + }, +}) + +ownershipRouter.command({ + model: 'Form13FHR', + path: '/form_13f', + description: 'Get the form 13F for institutional investment managers with $100M+ AUM.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'Form13FHR', params, credentials) + }, +}) + +ownershipRouter.command({ + model: 'GovernmentTrades', + path: '/government_trades', + description: 'Get government transaction data (Senate and House of Representatives).', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'GovernmentTrades', params, credentials) + }, +}) diff --git a/packages/opentypebb/src/extensions/equity/price/price-router.ts b/packages/opentypebb/src/extensions/equity/price/price-router.ts new file mode 100644 index 00000000..c219f0e8 --- /dev/null +++ b/packages/opentypebb/src/extensions/equity/price/price-router.ts @@ -0,0 +1,47 @@ +/** + * Equity Price Router. + * Maps to: openbb_equity/price/price_router.py + */ + +import { Router } from '../../../core/app/router.js' + +export const priceRouter = new Router({ + prefix: '/price', + description: 'Equity price data.', +}) + +priceRouter.command({ + model: 'EquityQuote', + path: '/quote', + description: 'Get the latest quote for a given stock. This includes price, volume, and other data.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'EquityQuote', params, credentials) + }, +}) + +priceRouter.command({ + model: 'EquityNBBO', + path: '/nbbo', + description: 'Get the National Best Bid and Offer for a given stock.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'EquityNBBO', params, credentials) + }, +}) + +priceRouter.command({ + model: 'EquityHistorical', + path: '/historical', + description: 'Get historical price data for a given stock. This includes open, high, low, close, and volume.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'EquityHistorical', params, credentials) + }, +}) + +priceRouter.command({ + model: 'PricePerformance', + path: '/performance', + description: 'Get price performance data for a given stock over various time periods.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'PricePerformance', params, credentials) + }, +}) diff --git a/packages/opentypebb/src/extensions/etf/etf-router.ts b/packages/opentypebb/src/extensions/etf/etf-router.ts new file mode 100644 index 00000000..1c830aa3 --- /dev/null +++ b/packages/opentypebb/src/extensions/etf/etf-router.ts @@ -0,0 +1,74 @@ +/** + * ETF Router. + * Maps to: openbb_platform/extensions/etf/etf_router.py + */ + +import { Router } from '../../core/app/router.js' + +export const etfRouter = new Router({ + prefix: '/etf', + description: 'Exchange Traded Fund (ETF) data.', +}) + +etfRouter.command({ + model: 'EtfSearch', + path: '/search', + description: 'Search for ETFs.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'EtfSearch', params, credentials) + }, +}) + +etfRouter.command({ + model: 'EtfInfo', + path: '/info', + description: 'Get ETF information.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'EtfInfo', params, credentials) + }, +}) + +etfRouter.command({ + model: 'EtfHoldings', + path: '/holdings', + description: 'Get an ETF holdings data.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'EtfHoldings', params, credentials) + }, +}) + +etfRouter.command({ + model: 'EtfSectors', + path: '/sectors', + description: 'Get ETF sector weightings.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'EtfSectors', params, credentials) + }, +}) + +etfRouter.command({ + model: 'EtfCountries', + path: '/countries', + description: 'Get ETF country weightings.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'EtfCountries', params, credentials) + }, +}) + +etfRouter.command({ + model: 'EtfEquityExposure', + path: '/equity_exposure', + description: 'Get the ETF equity exposure data.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'EtfEquityExposure', params, credentials) + }, +}) + +etfRouter.command({ + model: 'EtfHistorical', + path: '/historical', + description: 'Get historical ETF data.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'EtfHistorical', params, credentials) + }, +}) diff --git a/packages/opentypebb/src/extensions/index/index-router.ts b/packages/opentypebb/src/extensions/index/index-router.ts new file mode 100644 index 00000000..4bacd894 --- /dev/null +++ b/packages/opentypebb/src/extensions/index/index-router.ts @@ -0,0 +1,83 @@ +/** + * Index Router — root router for index market data. + * Maps to: openbb_index/index_router.py + */ + +import { Router } from '../../core/app/router.js' + +export const indexRouter = new Router({ + prefix: '/index', + description: 'Index market data.', +}) + +indexRouter.command({ + model: 'AvailableIndices', + path: '/available', + description: 'Get the list of available indices.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'AvailableIndices', params, credentials) + }, +}) + +indexRouter.command({ + model: 'IndexConstituents', + path: '/constituents', + description: 'Get the constituents of an index.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'IndexConstituents', params, credentials) + }, +}) + +indexRouter.command({ + model: 'IndexHistorical', + path: '/price/historical', + description: 'Get historical price data for an index.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'IndexHistorical', params, credentials) + }, +}) + +indexRouter.command({ + model: 'IndexSnapshots', + path: '/snapshots', + description: 'Get current snapshot data for indices.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'IndexSnapshots', params, credentials) + }, +}) + +indexRouter.command({ + model: 'RiskPremium', + path: '/risk_premium', + description: 'Get market risk premium data by country.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'RiskPremium', params, credentials) + }, +}) + +indexRouter.command({ + model: 'IndexSearch', + path: '/search', + description: 'Search for indices by name or symbol.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'IndexSearch', params, credentials) + }, +}) + +indexRouter.command({ + model: 'IndexSectors', + path: '/sectors', + description: 'Get sector weightings for an index.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'IndexSectors', params, credentials) + }, +}) + +indexRouter.command({ + model: 'SP500Multiples', + path: '/sp500_multiples', + description: 'Get historical S&P 500 multiples (PE ratio, earnings yield, etc).', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'SP500Multiples', params, credentials) + }, +}) diff --git a/packages/opentypebb/src/extensions/news/news-router.ts b/packages/opentypebb/src/extensions/news/news-router.ts new file mode 100644 index 00000000..c27db71f --- /dev/null +++ b/packages/opentypebb/src/extensions/news/news-router.ts @@ -0,0 +1,29 @@ +/** + * News Router — root router for financial news data. + * Maps to: openbb_news/news_router.py + */ + +import { Router } from '../../core/app/router.js' + +export const newsRouter = new Router({ + prefix: '/news', + description: 'Financial market news data.', +}) + +newsRouter.command({ + model: 'WorldNews', + path: '/world', + description: 'Get global news data.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'WorldNews', params, credentials) + }, +}) + +newsRouter.command({ + model: 'CompanyNews', + path: '/company', + description: 'Get news for one or more companies.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'CompanyNews', params, credentials) + }, +}) diff --git a/packages/opentypebb/src/index.ts b/packages/opentypebb/src/index.ts new file mode 100644 index 00000000..591a1aa9 --- /dev/null +++ b/packages/opentypebb/src/index.ts @@ -0,0 +1,53 @@ +/** + * OpenTypeBB — Library entry point. + * + * Usage: + * import { createExecutor, createRegistry, loadAllRouters } from 'opentypebb' + * + * // Quick start — create executor and call a provider directly: + * const executor = createExecutor() + * const result = await executor.execute('fmp', 'EquityQuote', { symbol: 'AAPL' }, { fmp_api_key: '...' }) + * + * // Or use individual components: + * import { Registry, QueryExecutor, OBBject } from 'opentypebb' + */ + +// Core abstractions +export { Fetcher, type FetcherClass } from './core/provider/abstract/fetcher.js' +export { Provider, type ProviderConfig } from './core/provider/abstract/provider.js' +export { BaseQueryParamsSchema, type BaseQueryParams } from './core/provider/abstract/query-params.js' +export { BaseDataSchema, type BaseData, ForceInt } from './core/provider/abstract/data.js' + +// Registry & execution +export { Registry } from './core/provider/registry.js' +export { QueryExecutor } from './core/provider/query-executor.js' + +// App model +export { OBBject, type OBBjectData, type Warning } from './core/app/model/obbject.js' +export { type Credentials, buildCredentials } from './core/app/model/credentials.js' +export { type RequestMetadata, createMetadata } from './core/app/model/metadata.js' + +// App +export { Query, type QueryConfig } from './core/app/query.js' +export { CommandRunner } from './core/app/command-runner.js' +export { Router, type CommandDef, type CommandHandler } from './core/app/router.js' + +// Utilities +export { amakeRequest, applyAliases, replaceEmptyStrings, buildQueryString } from './core/provider/utils/helpers.js' +export { OpenBBError, EmptyDataError, UnauthorizedError } from './core/provider/utils/errors.js' + +// App loader — convenience functions to create a fully-loaded system +export { createRegistry, createExecutor, loadAllRouters } from './core/api/app-loader.js' + +// Pre-built providers (for direct import if needed) +export { fmpProvider } from './providers/fmp/index.js' +export { yfinanceProvider } from './providers/yfinance/index.js' +export { deribitProvider } from './providers/deribit/index.js' +export { cboeProvider } from './providers/cboe/index.js' +export { multplProvider } from './providers/multpl/index.js' +export { oecdProvider } from './providers/oecd/index.js' +export { econdbProvider } from './providers/econdb/index.js' +export { imfProvider } from './providers/imf/index.js' +export { ecbProvider } from './providers/ecb/index.js' +export { federalReserveProvider } from './providers/federal_reserve/index.js' +export { intrinioProvider } from './providers/intrinio/index.js' diff --git a/packages/opentypebb/src/providers/cboe/index.ts b/packages/opentypebb/src/providers/cboe/index.ts new file mode 100644 index 00000000..519b0193 --- /dev/null +++ b/packages/opentypebb/src/providers/cboe/index.ts @@ -0,0 +1,22 @@ +/** + * CBOE Provider Module. + * Maps to: openbb_platform/providers/cboe/openbb_cboe/__init__.py + * + * We only implement IndexSearch here. The full CBOE provider in Python + * has 11 endpoints, but we only need the missing ones. + */ + +import { Provider } from '../../core/provider/abstract/provider.js' +import { CboeIndexSearchFetcher } from './models/index-search.js' + +export const cboeProvider = new Provider({ + name: 'cboe', + website: 'https://www.cboe.com', + description: + 'Cboe is the world\'s go-to derivatives and exchange network, ' + + 'delivering cutting-edge trading, clearing and investment solutions.', + reprName: 'Chicago Board Options Exchange (CBOE)', + fetcherDict: { + IndexSearch: CboeIndexSearchFetcher, + }, +}) diff --git a/packages/opentypebb/src/providers/cboe/models/index-search.ts b/packages/opentypebb/src/providers/cboe/models/index-search.ts new file mode 100644 index 00000000..b95d2e40 --- /dev/null +++ b/packages/opentypebb/src/providers/cboe/models/index-search.ts @@ -0,0 +1,118 @@ +/** + * CBOE Index Search Model. + * Maps to: openbb_cboe/models/index_search.py + * + * Fetches the CBOE index directory and filters by query. + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { IndexSearchDataSchema } from '../../../standard-models/index-search.js' +import { EmptyDataError } from '../../../core/provider/utils/errors.js' +import { nativeFetch } from '../../../core/provider/utils/helpers.js' + +const CBOE_INDEX_DIRECTORY_URL = 'https://www.cboe.com/us/indices/index-directory/' + +export const CboeIndexSearchQueryParamsSchema = z.object({ + query: z.string().default('').describe('Search query.'), + is_symbol: z.boolean().default(false).describe('Whether to search by ticker symbol.'), +}).passthrough() + +export type CboeIndexSearchQueryParams = z.infer + +export const CboeIndexSearchDataSchema = IndexSearchDataSchema.extend({ + description: z.string().nullable().default(null).describe('Description for the index.'), + currency: z.string().nullable().default(null).describe('Currency for the index.'), + time_zone: z.string().nullable().default(null).describe('Time zone for the index.'), +}).passthrough() + +export type CboeIndexSearchData = z.infer + +// Cache for the CBOE index directory +let _cachedDirectory: Record[] | null = null +let _cacheTime = 0 +const CACHE_TTL = 24 * 60 * 60 * 1000 // 24 hours + +async function getIndexDirectory(): Promise[]> { + const now = Date.now() + if (_cachedDirectory && (now - _cacheTime) < CACHE_TTL) { + return _cachedDirectory + } + + // CBOE provides index directory as JSON from their API + const url = 'https://www.cboe.com/us/indices/api/index-directory/' + try { + const resp = await nativeFetch(url, { timeoutMs: 30000 }) + if (resp.status === 200) { + const data = JSON.parse(resp.text) as Record[] + if (Array.isArray(data) && data.length > 0) { + _cachedDirectory = data + _cacheTime = now + return data + } + } + } catch { /* fall through */ } + + // Fallback: try the main endpoint with a different format + try { + const resp = await nativeFetch('https://cdn.cboe.com/api/global/us_indices/definitions/all_indices.json', { timeoutMs: 30000 }) + if (resp.status === 200) { + const json = JSON.parse(resp.text) as { data?: Record[] } + const records = json.data ?? (Array.isArray(json) ? json : []) + if (Array.isArray(records) && records.length > 0) { + _cachedDirectory = records as Record[] + _cacheTime = now + return _cachedDirectory + } + } + } catch { /* fall through */ } + + throw new EmptyDataError('Failed to fetch CBOE index directory.') +} + +export class CboeIndexSearchFetcher extends Fetcher { + static override requireCredentials = false + + static override transformQuery(params: Record): CboeIndexSearchQueryParams { + return CboeIndexSearchQueryParamsSchema.parse(params) + } + + static override async extractData( + query: CboeIndexSearchQueryParams, + _credentials: Record | null, + ): Promise[]> { + const directory = await getIndexDirectory() + + if (!query.query) return directory + + const q = query.query.toLowerCase() + + if (query.is_symbol) { + return directory.filter(d => { + const sym = String(d.index_symbol ?? d.symbol ?? '').toLowerCase() + return sym.includes(q) + }) + } + + return directory.filter(d => { + const sym = String(d.index_symbol ?? d.symbol ?? '').toLowerCase() + const name = String(d.name ?? '').toLowerCase() + const desc = String(d.description ?? '').toLowerCase() + return sym.includes(q) || name.includes(q) || desc.includes(q) + }) + } + + static override transformData( + _query: CboeIndexSearchQueryParams, + data: Record[], + ): CboeIndexSearchData[] { + if (data.length === 0) throw new EmptyDataError('No matching indices found.') + return data.map(d => CboeIndexSearchDataSchema.parse({ + symbol: d.index_symbol ?? d.symbol ?? '', + name: d.name ?? '', + description: d.description ?? null, + currency: d.currency ?? null, + time_zone: d.time_zone ?? null, + })) + } +} diff --git a/packages/opentypebb/src/providers/deribit/index.ts b/packages/opentypebb/src/providers/deribit/index.ts new file mode 100644 index 00000000..82087d8c --- /dev/null +++ b/packages/opentypebb/src/providers/deribit/index.ts @@ -0,0 +1,26 @@ +/** + * Deribit Provider Module. + * Maps to: openbb_platform/providers/deribit/openbb_deribit/__init__.py + * + * Deribit provides free crypto derivatives data (futures & options). + * No credentials required. + */ + +import { Provider } from '../../core/provider/abstract/provider.js' +import { DeribitFuturesCurveFetcher } from './models/futures-curve.js' +import { DeribitFuturesInfoFetcher } from './models/futures-info.js' +import { DeribitFuturesInstrumentsFetcher } from './models/futures-instruments.js' +import { DeribitOptionsChainsFetcher } from './models/options-chains.js' + +export const deribitProvider = new Provider({ + name: 'deribit', + website: 'https://www.deribit.com', + description: + 'Unofficial Python client for Deribit public data. Not intended for trading.', + fetcherDict: { + FuturesCurve: DeribitFuturesCurveFetcher, + FuturesInfo: DeribitFuturesInfoFetcher, + FuturesInstruments: DeribitFuturesInstrumentsFetcher, + OptionsChains: DeribitOptionsChainsFetcher, + }, +}) diff --git a/packages/opentypebb/src/providers/deribit/models/futures-curve.ts b/packages/opentypebb/src/providers/deribit/models/futures-curve.ts new file mode 100644 index 00000000..e326ef9e --- /dev/null +++ b/packages/opentypebb/src/providers/deribit/models/futures-curve.ts @@ -0,0 +1,84 @@ +/** + * Deribit Futures Curve Model. + * Maps to: openbb_deribit/models/futures_curve.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { FuturesCurveDataSchema } from '../../../standard-models/futures-curve.js' +import { EmptyDataError } from '../../../core/provider/utils/errors.js' +import { getFuturesCurveSymbols, getTickerData, DERIBIT_FUTURES_CURVE_SYMBOLS } from '../utils/helpers.js' + +export const DeribitFuturesCurveQueryParamsSchema = z.object({ + symbol: z.string().default('BTC').transform(v => v.toUpperCase()).describe('Symbol: BTC, ETH, or PAXG.'), + date: z.string().nullable().default(null).describe('Not used for Deribit. Use hours_ago instead.'), +}).passthrough() + +export type DeribitFuturesCurveQueryParams = z.infer + +export const DeribitFuturesCurveDataSchema = FuturesCurveDataSchema +export type DeribitFuturesCurveData = z.infer + +export class DeribitFuturesCurveFetcher extends Fetcher { + static override requireCredentials = false + + static override transformQuery(params: Record): DeribitFuturesCurveQueryParams { + return DeribitFuturesCurveQueryParamsSchema.parse(params) + } + + static override async extractData( + query: DeribitFuturesCurveQueryParams, + _credentials: Record | null, + ): Promise[]> { + const symbol = query.symbol + if (!DERIBIT_FUTURES_CURVE_SYMBOLS.includes(symbol)) { + throw new Error(`Invalid symbol: ${symbol}. Valid: ${DERIBIT_FUTURES_CURVE_SYMBOLS.join(', ')}`) + } + + const instrumentNames = await getFuturesCurveSymbols(symbol) + if (instrumentNames.length === 0) throw new EmptyDataError('No instruments found.') + + const results: Record[] = [] + const tasks = instrumentNames.map(async (name) => { + try { + const ticker = await getTickerData(name) + return { instrument_name: name, ...ticker } + } catch { + return null + } + }) + const tickerResults = await Promise.all(tasks) + for (const t of tickerResults) { + if (t) results.push(t) + } + + if (results.length === 0) throw new EmptyDataError('No ticker data found.') + return results + } + + static override transformData( + _query: DeribitFuturesCurveQueryParams, + data: Record[], + ): DeribitFuturesCurveData[] { + const today = new Date().toISOString().slice(0, 10) + + return data.map(d => { + const name = d.instrument_name as string + const parts = name.split('-') + let expiration = parts[1] ?? 'PERPETUAL' + + // Parse Deribit date format (e.g., "28MAR25") to ISO + if (expiration === 'PERPETUAL') { + expiration = today + } + + const price = (d.last_price as number) ?? (d.mark_price as number) ?? 0 + + return FuturesCurveDataSchema.parse({ + date: today, + expiration, + price, + }) + }).sort((a, b) => a.expiration.localeCompare(b.expiration)) + } +} diff --git a/packages/opentypebb/src/providers/deribit/models/futures-info.ts b/packages/opentypebb/src/providers/deribit/models/futures-info.ts new file mode 100644 index 00000000..9e135286 --- /dev/null +++ b/packages/opentypebb/src/providers/deribit/models/futures-info.ts @@ -0,0 +1,97 @@ +/** + * Deribit Futures Info Model. + * Maps to: openbb_deribit/models/futures_info.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { EmptyDataError, OpenBBError } from '../../../core/provider/utils/errors.js' +import { getTickerData, getFuturesSymbols, getPerpetualSymbols } from '../utils/helpers.js' + +export const DeribitFuturesInfoQueryParamsSchema = z.object({ + symbol: z.string().describe('Deribit futures instrument symbol(s), comma-separated.'), +}).passthrough() + +export type DeribitFuturesInfoQueryParams = z.infer + +export const DeribitFuturesInfoDataSchema = z.object({ + symbol: z.string().describe('Instrument name.'), + state: z.string().describe('The state of the order book.'), + open_interest: z.number().describe('Total outstanding contracts.'), + index_price: z.number().describe('Current index price.'), + best_ask_price: z.number().nullable().default(null).describe('Best ask price.'), + best_ask_amount: z.number().nullable().default(null).describe('Best ask amount.'), + best_bid_price: z.number().nullable().default(null).describe('Best bid price.'), + best_bid_amount: z.number().nullable().default(null).describe('Best bid amount.'), + last_price: z.number().nullable().default(null).describe('Last trade price.'), + high: z.number().nullable().default(null).describe('Highest price during 24h.'), + low: z.number().nullable().default(null).describe('Lowest price during 24h.'), + change_percent: z.number().nullable().default(null).describe('24-hour price change percent.'), + volume: z.number().nullable().default(null).describe('Volume during last 24h in base currency.'), + volume_usd: z.number().nullable().default(null).describe('Volume in USD.'), + mark_price: z.number().describe('Mark price for the instrument.'), + settlement_price: z.number().nullable().default(null).describe('Settlement price.'), + delivery_price: z.number().nullable().default(null).describe('Delivery price (closed instruments).'), + estimated_delivery_price: z.number().nullable().default(null).describe('Estimated delivery price.'), + current_funding: z.number().nullable().default(null).describe('Current funding (perpetual only).'), + funding_8h: z.number().nullable().default(null).describe('Funding 8h (perpetual only).'), + max_price: z.number().nullable().default(null).describe('Maximum order price.'), + min_price: z.number().nullable().default(null).describe('Minimum order price.'), + timestamp: z.number().nullable().default(null).describe('Timestamp of the data.'), +}).passthrough() + +export type DeribitFuturesInfoData = z.infer + +export class DeribitFuturesInfoFetcher extends Fetcher { + static override requireCredentials = false + + static override transformQuery(params: Record): DeribitFuturesInfoQueryParams { + return DeribitFuturesInfoQueryParamsSchema.parse(params) + } + + static override async extractData( + query: DeribitFuturesInfoQueryParams, + _credentials: Record | null, + ): Promise[]> { + const symbols = query.symbol.split(',') + const perpetualSymbols = await getPerpetualSymbols() + const futuresSymbols = await getFuturesSymbols() + const allSymbols = [...futuresSymbols, ...Object.keys(perpetualSymbols)] + + const resolvedSymbols = symbols.map(s => { + if (perpetualSymbols[s]) return perpetualSymbols[s] + if (allSymbols.includes(s)) return s + throw new OpenBBError(`Invalid symbol: ${s}`) + }) + + const results: Record[] = [] + const tasks = resolvedSymbols.map(async (sym) => { + try { + return await getTickerData(sym) + } catch { + return null + } + }) + const tickerResults = await Promise.all(tasks) + for (const t of tickerResults) { + if (t) results.push(t) + } + + if (results.length === 0) throw new EmptyDataError('No data found.') + return results + } + + static override transformData( + _query: DeribitFuturesInfoQueryParams, + data: Record[], + ): DeribitFuturesInfoData[] { + return data.map(d => { + const priceChange = d.price_change as number | null + return DeribitFuturesInfoDataSchema.parse({ + ...d, + symbol: d.instrument_name, + change_percent: priceChange != null ? priceChange / 100 : null, + }) + }) + } +} diff --git a/packages/opentypebb/src/providers/deribit/models/futures-instruments.ts b/packages/opentypebb/src/providers/deribit/models/futures-instruments.ts new file mode 100644 index 00000000..ae4f768e --- /dev/null +++ b/packages/opentypebb/src/providers/deribit/models/futures-instruments.ts @@ -0,0 +1,70 @@ +/** + * Deribit Futures Instruments Model. + * Maps to: openbb_deribit/models/futures_instruments.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { EmptyDataError } from '../../../core/provider/utils/errors.js' +import { getAllFuturesInstruments } from '../utils/helpers.js' + +export const DeribitFuturesInstrumentsQueryParamsSchema = z.object({}).passthrough() + +export type DeribitFuturesInstrumentsQueryParams = z.infer + +export const DeribitFuturesInstrumentsDataSchema = z.object({ + instrument_id: z.number().describe('Deribit Instrument ID.'), + symbol: z.string().describe('Instrument name.'), + base_currency: z.string().describe('The underlying currency being traded.'), + counter_currency: z.string().describe('Counter currency for the instrument.'), + quote_currency: z.string().describe('Quote currency.'), + settlement_currency: z.string().nullable().default(null).describe('Settlement currency.'), + future_type: z.string().describe('Type: linear or reversed.'), + settlement_period: z.string().nullable().default(null).describe('The settlement period.'), + price_index: z.string().describe('Name of price index used.'), + contract_size: z.number().describe('Contract size.'), + is_active: z.boolean().describe('Whether the instrument can be traded.'), + creation_timestamp: z.number().describe('Creation timestamp (ms since epoch).'), + expiration_timestamp: z.number().nullable().default(null).describe('Expiration timestamp (ms since epoch).'), + tick_size: z.number().describe('Minimal price change.'), + min_trade_amount: z.number().describe('Minimum trading amount in USD.'), + max_leverage: z.number().describe('Maximal leverage.'), + maker_commission: z.number().nullable().default(null).describe('Maker commission.'), + taker_commission: z.number().nullable().default(null).describe('Taker commission.'), +}).passthrough() + +export type DeribitFuturesInstrumentsData = z.infer + +export class DeribitFuturesInstrumentsFetcher extends Fetcher { + static override requireCredentials = false + + static override transformQuery(params: Record): DeribitFuturesInstrumentsQueryParams { + return DeribitFuturesInstrumentsQueryParamsSchema.parse(params) + } + + static override async extractData( + _query: DeribitFuturesInstrumentsQueryParams, + _credentials: Record | null, + ): Promise[]> { + const data = await getAllFuturesInstruments() + if (data.length === 0) throw new EmptyDataError('No instruments found.') + return data + } + + static override transformData( + _query: DeribitFuturesInstrumentsQueryParams, + data: Record[], + ): DeribitFuturesInstrumentsData[] { + return data.map(d => { + // Sentinel value for perpetual expiration + const expTs = d.expiration_timestamp as number + const expiration = expTs === 32503708800000 ? null : expTs + + return DeribitFuturesInstrumentsDataSchema.parse({ + ...d, + symbol: d.instrument_name, + expiration_timestamp: expiration, + }) + }) + } +} diff --git a/packages/opentypebb/src/providers/deribit/models/options-chains.ts b/packages/opentypebb/src/providers/deribit/models/options-chains.ts new file mode 100644 index 00000000..ab8c063b --- /dev/null +++ b/packages/opentypebb/src/providers/deribit/models/options-chains.ts @@ -0,0 +1,109 @@ +/** + * Deribit Options Chains Model. + * Maps to: openbb_deribit/models/options_chains.py + * + * Note: Python uses WebSocket connections. We use REST API for simplicity. + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { OptionsChainsDataSchema } from '../../../standard-models/options-chains.js' +import { EmptyDataError, OpenBBError } from '../../../core/provider/utils/errors.js' +import { getOptionsSymbols, getTickerData, DERIBIT_OPTIONS_SYMBOLS } from '../utils/helpers.js' + +export const DeribitOptionsChainsQueryParamsSchema = z.object({ + symbol: z.string().transform(v => v.toUpperCase()).describe('Symbol: BTC, ETH, SOL, XRP, BNB, or PAXG.'), +}).passthrough() + +export type DeribitOptionsChainsQueryParams = z.infer + +export const DeribitOptionsChainsDataSchema = OptionsChainsDataSchema +export type DeribitOptionsChainsData = z.infer + +export class DeribitOptionsChainsFetcher extends Fetcher { + static override requireCredentials = false + + static override transformQuery(params: Record): DeribitOptionsChainsQueryParams { + return DeribitOptionsChainsQueryParamsSchema.parse(params) + } + + static override async extractData( + query: DeribitOptionsChainsQueryParams, + _credentials: Record | null, + ): Promise[]> { + const symbol = query.symbol + if (!DERIBIT_OPTIONS_SYMBOLS.includes(symbol)) { + throw new OpenBBError(`Invalid symbol: ${symbol}. Valid: ${DERIBIT_OPTIONS_SYMBOLS.join(', ')}`) + } + + const optionsMap = await getOptionsSymbols(symbol) + if (Object.keys(optionsMap).length === 0) { + throw new EmptyDataError('No options found.') + } + + const allContracts = Object.values(optionsMap).flat() + const results: Record[] = [] + + // Fetch tickers in batches to avoid rate limiting + const batchSize = 20 + for (let i = 0; i < allContracts.length; i += batchSize) { + const batch = allContracts.slice(i, i + batchSize) + const tasks = batch.map(async (name) => { + try { + return await getTickerData(name) + } catch { + return null + } + }) + const batchResults = await Promise.all(tasks) + for (const t of batchResults) { + if (t) results.push(t) + } + } + + if (results.length === 0) throw new EmptyDataError('No options data found.') + return results + } + + static override transformData( + query: DeribitOptionsChainsQueryParams, + data: Record[], + ): DeribitOptionsChainsData[] { + return data.map(d => { + const name = d.instrument_name as string + // Parse: BTC-28MAR25-100000-C + const parts = name.split('-') + const expiration = parts[1] ?? '' + const strike = parseFloat(parts[2] ?? '0') + const optionType = parts[3] === 'C' ? 'call' : 'put' + + // Get underlying price for USD conversion + const underlyingPrice = d.underlying_price as number | null + const indexPrice = d.index_price as number | null + const refPrice = underlyingPrice ?? indexPrice ?? 1 + + return OptionsChainsDataSchema.parse({ + underlying_symbol: query.symbol, + underlying_price: refPrice, + contract_symbol: name, + expiration, + strike, + option_type: optionType, + open_interest: d.open_interest ?? null, + volume: d.stats ? (d.stats as Record).volume ?? null : null, + last_trade_price: d.last_price != null ? (d.last_price as number) * refPrice : null, + bid: d.best_bid_price != null ? (d.best_bid_price as number) * refPrice : null, + bid_size: d.best_bid_amount ?? null, + ask: d.best_ask_price != null ? (d.best_ask_price as number) * refPrice : null, + ask_size: d.best_ask_amount ?? null, + mark: d.mark_price != null ? (d.mark_price as number) * refPrice : null, + implied_volatility: d.mark_iv != null ? (d.mark_iv as number) / 100 : null, + delta: d.greeks ? (d.greeks as Record).delta ?? null : null, + gamma: d.greeks ? (d.greeks as Record).gamma ?? null : null, + theta: d.greeks ? (d.greeks as Record).theta ?? null : null, + vega: d.greeks ? (d.greeks as Record).vega ?? null : null, + rho: d.greeks ? (d.greeks as Record).rho ?? null : null, + }) + }) + } +} diff --git a/packages/opentypebb/src/providers/deribit/utils/helpers.ts b/packages/opentypebb/src/providers/deribit/utils/helpers.ts new file mode 100644 index 00000000..257ae4db --- /dev/null +++ b/packages/opentypebb/src/providers/deribit/utils/helpers.ts @@ -0,0 +1,128 @@ +/** + * Deribit Helpers Module. + * Maps to: openbb_deribit/utils/helpers.py + */ + +import { OpenBBError } from '../../../core/provider/utils/errors.js' + +export const BASE_URL = 'https://www.deribit.com' +export const DERIBIT_FUTURES_CURVE_SYMBOLS = ['BTC', 'ETH', 'PAXG'] +export const DERIBIT_OPTIONS_SYMBOLS = ['BTC', 'ETH', 'SOL', 'XRP', 'BNB', 'PAXG'] +export const CURRENCIES = ['BTC', 'ETH', 'USDC', 'USDT', 'EURR', 'all'] + +/** + * Get instruments from Deribit. + * Maps to: get_instruments() in helpers.py + */ +export async function getInstruments( + currency: string, + derivativeType: string, + expired = false, +): Promise[]> { + const url = `${BASE_URL}/api/v2/public/get_instruments?currency=${currency}&kind=${derivativeType}&expired=${expired}` + const res = await fetch(url) + if (!res.ok) throw new OpenBBError(`Deribit API error: ${res.status}`) + const json = await res.json() as Record + return (json.result ?? []) as Record[] +} + +/** + * Get all instruments for all currencies. + */ +export async function getAllFuturesInstruments(): Promise[]> { + const results: Record[] = [] + for (const currency of CURRENCIES.filter(c => c !== 'all')) { + try { + const instruments = await getInstruments(currency, 'future') + results.push(...instruments) + } catch { + // skip currencies with no futures + } + } + // Also try 'all' + try { + const allInstruments = await getInstruments('all', 'future') + // Deduplicate by instrument_name + const seen = new Set(results.map(i => i.instrument_name)) + for (const inst of allInstruments) { + if (!seen.has(inst.instrument_name)) { + results.push(inst) + } + } + } catch { /* ignore */ } + return results +} + +/** + * Get ticker data for a single instrument. + * Maps to: get_ticker_data() in helpers.py + */ +export async function getTickerData(instrumentName: string): Promise> { + const url = `${BASE_URL}/api/v2/public/ticker?instrument_name=${instrumentName}` + const res = await fetch(url) + if (!res.ok) throw new OpenBBError(`Deribit ticker error: ${res.status}`) + const json = await res.json() as Record + return (json.result ?? {}) as Record +} + +/** + * Get futures curve symbols for a given currency. + * Maps to: get_futures_curve_symbols() in helpers.py + */ +export async function getFuturesCurveSymbols(symbol: string): Promise { + const instruments = await getInstruments(symbol, 'future') + return instruments + .map(i => i.instrument_name as string) + .filter(name => name !== undefined) +} + +/** + * Get perpetual symbols mapping short names to full names. + * Maps to: get_perpetual_symbols() in helpers.py + */ +export async function getPerpetualSymbols(): Promise> { + const result: Record = {} + for (const currency of CURRENCIES.filter(c => c !== 'all')) { + try { + const instruments = await getInstruments(currency, 'future') + for (const inst of instruments) { + const name = inst.instrument_name as string + if (name?.includes('PERPETUAL')) { + const short = name.replace('-PERPETUAL', '').replace('_', '') + result[short] = name + } + } + } catch { /* skip */ } + } + return result +} + +/** + * Get all futures symbols. + * Maps to: get_futures_symbols() in helpers.py + */ +export async function getFuturesSymbols(): Promise { + const instruments = await getAllFuturesInstruments() + return instruments.map(i => i.instrument_name as string).filter(Boolean) +} + +/** + * Get options symbols grouped by expiration. + * Maps to: get_options_symbols() in helpers.py + */ +export async function getOptionsSymbols(symbol: string): Promise> { + const instruments = await getInstruments(symbol, 'option') + const result: Record = {} + for (const inst of instruments) { + const name = inst.instrument_name as string + if (!name) continue + // Parse expiration from instrument name: e.g., BTC-28MAR25-100000-C + const parts = name.split('-') + if (parts.length >= 3) { + const expiration = parts[1] + if (!result[expiration]) result[expiration] = [] + result[expiration].push(name) + } + } + return result +} diff --git a/packages/opentypebb/src/providers/ecb/index.ts b/packages/opentypebb/src/providers/ecb/index.ts new file mode 100644 index 00000000..c490336c --- /dev/null +++ b/packages/opentypebb/src/providers/ecb/index.ts @@ -0,0 +1,16 @@ +/** + * ECB Provider Module. + * Maps to: openbb_platform/providers/ecb/openbb_ecb/__init__.py + */ + +import { Provider } from '../../core/provider/abstract/provider.js' +import { ECBBalanceOfPaymentsFetcher } from './models/balance-of-payments.js' + +export const ecbProvider = new Provider({ + name: 'ecb', + website: 'https://data.ecb.europa.eu', + description: 'European Central Bank data portal.', + fetcherDict: { + BalanceOfPayments: ECBBalanceOfPaymentsFetcher, + }, +}) diff --git a/packages/opentypebb/src/providers/ecb/models/balance-of-payments.ts b/packages/opentypebb/src/providers/ecb/models/balance-of-payments.ts new file mode 100644 index 00000000..bb9edbe7 --- /dev/null +++ b/packages/opentypebb/src/providers/ecb/models/balance-of-payments.ts @@ -0,0 +1,120 @@ +/** + * ECB Balance of Payments Model. + * Maps to: openbb_ecb/models/balance_of_payments.py + * + * Uses ECB data-detail-api to fetch individual BOP series and merge by period. + * Requires proxy for network access (uses globalThis.fetch via undici). + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { BalanceOfPaymentsDataSchema } from '../../../standard-models/balance-of-payments.js' +import { EmptyDataError } from '../../../core/provider/utils/errors.js' + +export const ECBBalanceOfPaymentsQueryParamsSchema = z.object({ + report_type: z.string().default('main').describe('Report type: main, summary.'), + frequency: z.enum(['monthly', 'quarterly']).default('monthly').describe('Data frequency.'), + start_date: z.string().nullable().default(null).describe('Start date in YYYY-MM-DD.'), + end_date: z.string().nullable().default(null).describe('End date in YYYY-MM-DD.'), +}).passthrough() + +export type ECBBalanceOfPaymentsQueryParams = z.infer +export type ECBBalanceOfPaymentsData = z.infer + +const ECB_BASE = 'https://data.ecb.europa.eu/data-detail-api' + +type SeriesMap = Record + +function getMainSeries(freq: string): SeriesMap { + return { + current_account: `BPS.${freq}.N.I9.W1.S1.S1.T.B.CA._Z._Z._Z.EUR._T._X.N.ALL`, + goods: `BPS.${freq}.N.I9.W1.S1.S1.T.B.G._Z._Z._Z.EUR._T._X.N.ALL`, + services: `BPS.${freq}.N.I9.W1.S1.S1.T.B.S._Z._Z._Z.EUR._T._X.N.ALL`, + primary_income: `BPS.${freq}.N.I9.W1.S1.S1.T.B.IN1._Z._Z._Z.EUR._T._X.N.ALL`, + secondary_income: `BPS.${freq}.N.I9.W1.S1.S1.T.B.IN2._Z._Z._Z.EUR._T._X.N.ALL`, + capital_account: `BPS.${freq}.N.I9.W1.S1.S1.T.B.KA._Z._Z._Z.EUR._T._X.N.ALL`, + financial_account: `BPS.${freq}.N.I9.W1.S1.S1.T.N.FA._T.F._Z.EUR._T._X.N.ALL`, + } +} + +/** Fetch a single ECB series via data-detail-api */ +async function fetchSeries( + seriesId: string, + startDate: string, + endDate: string, +): Promise> { + const url = `${ECB_BASE}/${seriesId}?startPeriod=${startDate.replace(/-/g, '')}&endPeriod=${endDate.replace(/-/g, '')}` + + try { + const resp = await fetch(url, { signal: AbortSignal.timeout(30000) }) + if (!resp.ok) return [] + const data = await resp.json() as any[] + if (!Array.isArray(data)) return [] + + return data + .filter((d: any) => d?.PERIOD && (d?.OBS != null || d?.OBS_VALUE != null)) + .map((d: any) => ({ + period: String(d.PERIOD), + value: parseFloat(String(d.OBS ?? d.OBS_VALUE ?? 0)), + })) + } catch { + return [] + } +} + +export class ECBBalanceOfPaymentsFetcher extends Fetcher { + static override requireCredentials = false + + static override transformQuery(params: Record): ECBBalanceOfPaymentsQueryParams { + return ECBBalanceOfPaymentsQueryParamsSchema.parse(params) + } + + static override async extractData( + query: ECBBalanceOfPaymentsQueryParams, + _credentials: Record | null, + ): Promise[]> { + const freq = query.frequency === 'monthly' ? 'M' : 'Q' + const series = getMainSeries(freq) + const startDate = query.start_date ?? '2000-01-01' + const endDate = query.end_date ?? new Date().toISOString().slice(0, 10) + + // Fetch all series in parallel + const entries = Object.entries(series) + const results = await Promise.all( + entries.map(async ([fieldName, seriesId]) => { + const data = await fetchSeries(seriesId, startDate, endDate) + return { fieldName, data } + }) + ) + + // Merge by period + const periodMap: Record> = {} + for (const { fieldName, data } of results) { + for (const { period, value } of data) { + if (!periodMap[period]) { + // Format period: already "2025-12-01" from API, or "20241231" -> "2024-12-31" + let formatted = period + if (/^\d{8}$/.test(period)) { + formatted = `${period.slice(0, 4)}-${period.slice(4, 6)}-${period.slice(6, 8)}` + } + periodMap[period] = { period: formatted } + } + periodMap[period][fieldName] = value + } + } + + const rows = Object.values(periodMap) + if (!rows.length) throw new EmptyDataError('No ECB BOP data found') + return rows + } + + static override transformData( + _query: ECBBalanceOfPaymentsQueryParams, + data: Record[], + ): ECBBalanceOfPaymentsData[] { + if (data.length === 0) throw new EmptyDataError() + return data + .sort((a, b) => String(a.period).localeCompare(String(b.period))) + .map(d => BalanceOfPaymentsDataSchema.parse(d)) + } +} diff --git a/packages/opentypebb/src/providers/econdb/index.ts b/packages/opentypebb/src/providers/econdb/index.ts new file mode 100644 index 00000000..4f53c550 --- /dev/null +++ b/packages/opentypebb/src/providers/econdb/index.ts @@ -0,0 +1,23 @@ +/** + * EconDB Provider Module. + * Maps to: openbb_platform/providers/econdb/openbb_econdb/__init__.py + */ + +import { Provider } from '../../core/provider/abstract/provider.js' +import { EconDBAvailableIndicatorsFetcher } from './models/available-indicators.js' +import { EconDBCountryProfileFetcher } from './models/country-profile.js' +import { EconDBExportDestinationsFetcher } from './models/export-destinations.js' +import { EconDBEconomicIndicatorsFetcher } from './models/economic-indicators.js' + +export const econdbProvider = new Provider({ + name: 'econdb', + website: 'https://www.econdb.com', + description: 'EconDB provides economic data aggregated from official sources.', + credentials: ['api_key'], + fetcherDict: { + AvailableIndicators: EconDBAvailableIndicatorsFetcher, + CountryProfile: EconDBCountryProfileFetcher, + ExportDestinations: EconDBExportDestinationsFetcher, + EconomicIndicators: EconDBEconomicIndicatorsFetcher, + }, +}) diff --git a/packages/opentypebb/src/providers/econdb/models/available-indicators.ts b/packages/opentypebb/src/providers/econdb/models/available-indicators.ts new file mode 100644 index 00000000..1ecc82c4 --- /dev/null +++ b/packages/opentypebb/src/providers/econdb/models/available-indicators.ts @@ -0,0 +1,56 @@ +/** + * EconDB Available Indicators Model. + * Maps to: openbb_econdb/models/available_indicators.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { AvailableIndicatorsDataSchema } from '../../../standard-models/available-indicators.js' +import { EmptyDataError } from '../../../core/provider/utils/errors.js' +import { amakeRequest } from '../../../core/provider/utils/helpers.js' + +export const EconDBAvailableIndicatorsQueryParamsSchema = z.object({}).passthrough() +export type EconDBAvailableIndicatorsQueryParams = z.infer +export type EconDBAvailableIndicatorsData = z.infer + +const ECONDB_BASE = 'https://www.econdb.com/api/series/?format=json' + +export class EconDBAvailableIndicatorsFetcher extends Fetcher { + static override requireCredentials = false + + static override transformQuery(params: Record): EconDBAvailableIndicatorsQueryParams { + return EconDBAvailableIndicatorsQueryParamsSchema.parse(params) + } + + static override async extractData( + _query: EconDBAvailableIndicatorsQueryParams, + credentials: Record | null, + ): Promise[]> { + const token = credentials?.econdb_api_key ?? '' + const url = token ? `${ECONDB_BASE}&token=${token}` : ECONDB_BASE + + try { + const data = await amakeRequest>(url) + const results = (data.results ?? data) as Record[] + if (!Array.isArray(results) || results.length === 0) throw new EmptyDataError() + return results + } catch (err) { + if (err instanceof EmptyDataError) throw err + throw new EmptyDataError(`Failed to fetch EconDB indicators: ${err}`) + } + } + + static override transformData( + _query: EconDBAvailableIndicatorsQueryParams, + data: Record[], + ): EconDBAvailableIndicatorsData[] { + return data.map(d => AvailableIndicatorsDataSchema.parse({ + symbol_root: d.ticker ?? d.symbol_root ?? null, + symbol: d.ticker ?? d.symbol ?? null, + country: d.geography?.toString() ?? d.country ?? null, + iso: d.iso ?? null, + description: d.description ?? d.name ?? null, + frequency: d.frequency ?? null, + })) + } +} diff --git a/packages/opentypebb/src/providers/econdb/models/country-profile.ts b/packages/opentypebb/src/providers/econdb/models/country-profile.ts new file mode 100644 index 00000000..60f3cd34 --- /dev/null +++ b/packages/opentypebb/src/providers/econdb/models/country-profile.ts @@ -0,0 +1,73 @@ +/** + * EconDB Country Profile Model. + * Maps to: openbb_econdb/models/country_profile.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { CountryProfileDataSchema } from '../../../standard-models/country-profile.js' +import { EmptyDataError } from '../../../core/provider/utils/errors.js' +import { amakeRequest } from '../../../core/provider/utils/helpers.js' + +export const EconDBCountryProfileQueryParamsSchema = z.object({ + country: z.string().transform(v => v.toLowerCase().replace(/ /g, '_')).describe('The country to get data for.'), +}).passthrough() + +export type EconDBCountryProfileQueryParams = z.infer +export type EconDBCountryProfileData = z.infer + +const COUNTRY_ISO: Record = { + united_states: 'US', united_kingdom: 'GB', japan: 'JP', germany: 'DE', + france: 'FR', italy: 'IT', canada: 'CA', australia: 'AU', + south_korea: 'KR', mexico: 'MX', brazil: 'BR', china: 'CN', + india: 'IN', turkey: 'TR', south_africa: 'ZA', russia: 'RU', + spain: 'ES', netherlands: 'NL', switzerland: 'CH', sweden: 'SE', +} + +export class EconDBCountryProfileFetcher extends Fetcher { + static override requireCredentials = false + + static override transformQuery(params: Record): EconDBCountryProfileQueryParams { + return EconDBCountryProfileQueryParamsSchema.parse(params) + } + + static override async extractData( + query: EconDBCountryProfileQueryParams, + credentials: Record | null, + ): Promise[]> { + const iso = COUNTRY_ISO[query.country] ?? query.country.toUpperCase().slice(0, 2) + const token = credentials?.econdb_api_key ?? '' + const tokenParam = token ? `&token=${token}` : '' + const url = `https://www.econdb.com/api/country/${iso}/?format=json${tokenParam}` + + try { + const data = await amakeRequest>(url) + return [{ ...data, country: query.country }] + } catch (err) { + throw new EmptyDataError(`Failed to fetch EconDB country profile: ${err}`) + } + } + + static override transformData( + _query: EconDBCountryProfileQueryParams, + data: Record[], + ): EconDBCountryProfileData[] { + if (data.length === 0) throw new EmptyDataError() + return data.map(d => CountryProfileDataSchema.parse({ + country: d.country ?? '', + population: d.population ?? null, + gdp_usd: d.gdp ?? null, + gdp_qoq: d.gdp_qoq ?? null, + gdp_yoy: d.gdp_yoy ?? null, + cpi_yoy: d.cpi ?? null, + core_yoy: d.core_cpi ?? null, + retail_sales_yoy: d.retail_sales ?? null, + industrial_production_yoy: d.industrial_production ?? null, + policy_rate: d.interest_rate ?? null, + yield_10y: d.bond_yield_10y ?? null, + govt_debt_gdp: d.govt_debt ?? null, + current_account_gdp: d.current_account ?? null, + jobless_rate: d.unemployment ?? null, + })) + } +} diff --git a/packages/opentypebb/src/providers/econdb/models/economic-indicators.ts b/packages/opentypebb/src/providers/econdb/models/economic-indicators.ts new file mode 100644 index 00000000..8177fb8a --- /dev/null +++ b/packages/opentypebb/src/providers/econdb/models/economic-indicators.ts @@ -0,0 +1,79 @@ +/** + * EconDB Economic Indicators Model. + * Maps to: openbb_econdb/models/economic_indicators.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { EconomicIndicatorsDataSchema } from '../../../standard-models/economic-indicators.js' +import { EmptyDataError } from '../../../core/provider/utils/errors.js' +import { amakeRequest } from '../../../core/provider/utils/helpers.js' + +export const EconDBEconomicIndicatorsQueryParamsSchema = z.object({ + symbol: z.string().describe('Indicator symbol (e.g., GDP, CPI, URATE).'), + country: z.string().nullable().default(null).describe('Country to filter by.'), + frequency: z.string().nullable().default(null).describe('Data frequency.'), + start_date: z.string().nullable().default(null).describe('Start date in YYYY-MM-DD.'), + end_date: z.string().nullable().default(null).describe('End date in YYYY-MM-DD.'), +}).passthrough() + +export type EconDBEconomicIndicatorsQueryParams = z.infer +export type EconDBEconomicIndicatorsData = z.infer + +export class EconDBEconomicIndicatorsFetcher extends Fetcher { + static override requireCredentials = false + + static override transformQuery(params: Record): EconDBEconomicIndicatorsQueryParams { + return EconDBEconomicIndicatorsQueryParamsSchema.parse(params) + } + + static override async extractData( + query: EconDBEconomicIndicatorsQueryParams, + credentials: Record | null, + ): Promise[]> { + const token = credentials?.econdb_api_key ?? '' + const tokenParam = token ? `&token=${token}` : '' + let url = `https://www.econdb.com/api/series/${query.symbol}/?format=json${tokenParam}` + + try { + const data = await amakeRequest>(url) + const values = (data.data ?? data.results ?? []) as Record[] + + if (!Array.isArray(values) || values.length === 0) { + // Try alternative format + const dates = data.dates as string[] | undefined + const vals = data.values as number[] | undefined + if (dates && vals) { + return dates.map((d, i) => ({ + date: d, + symbol: query.symbol, + country: query.country, + value: vals[i], + })) + } + throw new EmptyDataError() + } + + return values.map(v => ({ + ...v, + symbol: query.symbol, + country: query.country ?? v.country, + })) + } catch (err) { + if (err instanceof EmptyDataError) throw err + throw new EmptyDataError(`Failed to fetch EconDB indicators: ${err}`) + } + } + + static override transformData( + query: EconDBEconomicIndicatorsQueryParams, + data: Record[], + ): EconDBEconomicIndicatorsData[] { + let filtered = data + if (query.start_date) filtered = filtered.filter(d => String(d.date) >= query.start_date!) + if (query.end_date) filtered = filtered.filter(d => String(d.date) <= query.end_date!) + return filtered + .sort((a, b) => String(a.date).localeCompare(String(b.date))) + .map(d => EconomicIndicatorsDataSchema.parse(d)) + } +} diff --git a/packages/opentypebb/src/providers/econdb/models/export-destinations.ts b/packages/opentypebb/src/providers/econdb/models/export-destinations.ts new file mode 100644 index 00000000..eff2aabf --- /dev/null +++ b/packages/opentypebb/src/providers/econdb/models/export-destinations.ts @@ -0,0 +1,61 @@ +/** + * EconDB Export Destinations Model. + * Maps to: openbb_econdb/models/export_destinations.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { ExportDestinationsDataSchema } from '../../../standard-models/export-destinations.js' +import { EmptyDataError } from '../../../core/provider/utils/errors.js' +import { amakeRequest } from '../../../core/provider/utils/helpers.js' + +export const EconDBExportDestinationsQueryParamsSchema = z.object({ + country: z.string().describe('The country to get data for.'), +}).passthrough() + +export type EconDBExportDestinationsQueryParams = z.infer +export type EconDBExportDestinationsData = z.infer + +const COUNTRY_ISO: Record = { + united_states: 'US', united_kingdom: 'GB', japan: 'JP', germany: 'DE', + france: 'FR', italy: 'IT', canada: 'CA', china: 'CN', +} + +export class EconDBExportDestinationsFetcher extends Fetcher { + static override requireCredentials = false + + static override transformQuery(params: Record): EconDBExportDestinationsQueryParams { + return EconDBExportDestinationsQueryParamsSchema.parse(params) + } + + static override async extractData( + query: EconDBExportDestinationsQueryParams, + credentials: Record | null, + ): Promise[]> { + const iso = COUNTRY_ISO[query.country] ?? query.country.toUpperCase().slice(0, 2) + const token = credentials?.econdb_api_key ?? '' + const tokenParam = token ? `&token=${token}` : '' + const url = `https://www.econdb.com/api/country/${iso}/trade/?format=json${tokenParam}` + + try { + const data = await amakeRequest>(url) + const exports = (data.exports ?? data.results ?? []) as Record[] + if (!Array.isArray(exports) || exports.length === 0) throw new EmptyDataError() + return exports.map(e => ({ ...e, origin_country: query.country })) + } catch (err) { + if (err instanceof EmptyDataError) throw err + throw new EmptyDataError(`Failed to fetch export destinations: ${err}`) + } + } + + static override transformData( + _query: EconDBExportDestinationsQueryParams, + data: Record[], + ): EconDBExportDestinationsData[] { + return data.map(d => ExportDestinationsDataSchema.parse({ + origin_country: d.origin_country ?? '', + destination_country: d.partner ?? d.destination_country ?? '', + value: d.value ?? d.amount ?? 0, + })) + } +} diff --git a/packages/opentypebb/src/providers/federal_reserve/index.ts b/packages/opentypebb/src/providers/federal_reserve/index.ts new file mode 100644 index 00000000..84795895 --- /dev/null +++ b/packages/opentypebb/src/providers/federal_reserve/index.ts @@ -0,0 +1,17 @@ +/** + * Federal Reserve Provider Module. + * Maps to: openbb_platform/providers/federal_reserve/openbb_federal_reserve/__init__.py + */ + +import { Provider } from '../../core/provider/abstract/provider.js' +import { FedCentralBankHoldingsFetcher } from './models/central-bank-holdings.js' + +export const federalReserveProvider = new Provider({ + name: 'federal_reserve', + website: 'https://www.federalreserve.gov', + description: 'Federal Reserve Economic Data.', + credentials: ['api_key'], + fetcherDict: { + CentralBankHoldings: FedCentralBankHoldingsFetcher, + }, +}) diff --git a/packages/opentypebb/src/providers/federal_reserve/models/central-bank-holdings.ts b/packages/opentypebb/src/providers/federal_reserve/models/central-bank-holdings.ts new file mode 100644 index 00000000..7e5e4e54 --- /dev/null +++ b/packages/opentypebb/src/providers/federal_reserve/models/central-bank-holdings.ts @@ -0,0 +1,88 @@ +/** + * Federal Reserve Central Bank Holdings Model. + * Maps to: openbb_federal_reserve/models/central_bank_holdings.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { CentralBankHoldingsDataSchema } from '../../../standard-models/central-bank-holdings.js' +import { EmptyDataError } from '../../../core/provider/utils/errors.js' +import { amakeRequest } from '../../../core/provider/utils/helpers.js' + +export const FedCentralBankHoldingsQueryParamsSchema = z.object({ + date: z.string().nullable().default(null).describe('Specific date for holdings data in YYYY-MM-DD.'), +}).passthrough() + +export type FedCentralBankHoldingsQueryParams = z.infer + +export const FedCentralBankHoldingsDataSchema = CentralBankHoldingsDataSchema.extend({ + treasury_holding_value: z.number().nullable().default(null).describe('Treasury securities held (millions USD).'), + mbs_holding_value: z.number().nullable().default(null).describe('MBS held (millions USD).'), + agency_holding_value: z.number().nullable().default(null).describe('Agency debt held (millions USD).'), + total_assets: z.number().nullable().default(null).describe('Total assets (millions USD).'), +}).passthrough() + +export type FedCentralBankHoldingsData = z.infer + +// FRED series for Fed balance sheet +const FRED_BASE = 'https://api.stlouisfed.org/fred/series/observations' + +export class FedCentralBankHoldingsFetcher extends Fetcher { + static override requireCredentials = false + + static override transformQuery(params: Record): FedCentralBankHoldingsQueryParams { + return FedCentralBankHoldingsQueryParamsSchema.parse(params) + } + + static override async extractData( + query: FedCentralBankHoldingsQueryParams, + credentials: Record | null, + ): Promise[]> { + const fredKey = credentials?.fred_api_key ?? '' + + // Fed H.4.1 data is available from FRED + // TREAST = Treasury securities held, MBST = MBS held, WSHOMCB = Total assets + const series = ['TREAST', 'MBST', 'WSHOMCB'] + const dateParam = query.date ? `&observation_start=${query.date}&observation_end=${query.date}` : '' + const apiKeyParam = fredKey ? `&api_key=${fredKey}` : '' + + const dataMap: Record> = {} + + for (const seriesId of series) { + try { + const url = `${FRED_BASE}?series_id=${seriesId}&file_type=json&sort_order=desc&limit=100${dateParam}${apiKeyParam}` + const data = await amakeRequest>(url) + const observations = (data.observations ?? []) as Array<{ date: string; value: string }> + + for (const obs of observations) { + const val = parseFloat(obs.value) + if (!isNaN(val)) { + if (!dataMap[obs.date]) dataMap[obs.date] = {} + dataMap[obs.date][seriesId] = val + } + } + } catch { + // Skip series that fail + } + } + + const results = Object.entries(dataMap).map(([date, values]) => ({ + date, + treasury_holding_value: values.TREAST ?? null, + mbs_holding_value: values.MBST ?? null, + total_assets: values.WSHOMCB ?? null, + })) + + if (results.length === 0) throw new EmptyDataError('No Fed holdings data found.') + return results + } + + static override transformData( + _query: FedCentralBankHoldingsQueryParams, + data: Record[], + ): FedCentralBankHoldingsData[] { + return data + .sort((a, b) => String(a.date).localeCompare(String(b.date))) + .map(d => FedCentralBankHoldingsDataSchema.parse(d)) + } +} diff --git a/packages/opentypebb/src/providers/fmp/index.ts b/packages/opentypebb/src/providers/fmp/index.ts new file mode 100644 index 00000000..fd6cb0d8 --- /dev/null +++ b/packages/opentypebb/src/providers/fmp/index.ts @@ -0,0 +1,150 @@ +/** + * FMP Provider Module. + * Maps to: openbb_platform/providers/fmp/openbb_fmp/__init__.py + * + * Only includes fetchers that have been ported to TypeScript. + * The Python version has ~70 fetchers; we port only what open-alice uses. + */ + +import { Provider } from '../../core/provider/abstract/provider.js' + +import { FMPEquityProfileFetcher } from './models/equity-profile.js' +import { FMPEquityQuoteFetcher } from './models/equity-quote.js' +import { FMPEquityHistoricalFetcher } from './models/equity-historical.js' +import { FMPBalanceSheetFetcher } from './models/balance-sheet.js' +import { FMPIncomeStatementFetcher } from './models/income-statement.js' +import { FMPCashFlowStatementFetcher } from './models/cash-flow.js' +import { FMPFinancialRatiosFetcher } from './models/financial-ratios.js' +import { FMPKeyMetricsFetcher } from './models/key-metrics.js' +import { FMPInsiderTradingFetcher } from './models/insider-trading.js' +import { FMPCalendarEarningsFetcher } from './models/calendar-earnings.js' +import { FMPCompanyNewsFetcher } from './models/company-news.js' +import { FMPWorldNewsFetcher } from './models/world-news.js' +import { FMPPriceTargetConsensusFetcher } from './models/price-target-consensus.js' +import { FMPGainersFetcher } from './models/gainers.js' +import { FMPLosersFetcher } from './models/losers.js' +import { FMPEquityActiveFetcher } from './models/active.js' +import { FMPCryptoHistoricalFetcher } from './models/crypto-historical.js' +import { FMPCryptoSearchFetcher } from './models/crypto-search.js' +import { FMPCurrencyHistoricalFetcher } from './models/currency-historical.js' +import { FMPCurrencyPairsFetcher } from './models/currency-pairs.js' +import { FMPBalanceSheetGrowthFetcher } from './models/balance-sheet-growth.js' +import { FMPIncomeStatementGrowthFetcher } from './models/income-statement-growth.js' +import { FMPCashFlowStatementGrowthFetcher } from './models/cash-flow-growth.js' +import { FMPCalendarDividendFetcher } from './models/calendar-dividend.js' +import { FMPCalendarSplitsFetcher } from './models/calendar-splits.js' +import { FMPCalendarIpoFetcher } from './models/calendar-ipo.js' +import { FMPEconomicCalendarFetcher } from './models/economic-calendar.js' +import { FMPAnalystEstimatesFetcher } from './models/analyst-estimates.js' +import { FMPForwardEpsEstimatesFetcher } from './models/forward-eps-estimates.js' +import { FMPForwardEbitdaEstimatesFetcher } from './models/forward-ebitda-estimates.js' +import { FMPPriceTargetFetcher } from './models/price-target.js' +import { FMPEtfInfoFetcher } from './models/etf-info.js' +import { FMPEtfHoldingsFetcher } from './models/etf-holdings.js' +import { FMPEtfSectorsFetcher } from './models/etf-sectors.js' +import { FMPEtfCountriesFetcher } from './models/etf-countries.js' +import { FMPEtfEquityExposureFetcher } from './models/etf-equity-exposure.js' +import { FMPEtfSearchFetcher } from './models/etf-search.js' +import { FMPKeyExecutivesFetcher } from './models/key-executives.js' +import { FMPExecutiveCompensationFetcher } from './models/executive-compensation.js' +import { FMPGovernmentTradesFetcher } from './models/government-trades.js' +import { FMPInstitutionalOwnershipFetcher } from './models/institutional-ownership.js' +import { FMPHistoricalDividendsFetcher } from './models/historical-dividends.js' +import { FMPHistoricalSplitsFetcher } from './models/historical-splits.js' +import { FMPHistoricalEpsFetcher } from './models/historical-eps.js' +import { FMPHistoricalEmployeesFetcher } from './models/historical-employees.js' +import { FMPShareStatisticsFetcher } from './models/share-statistics.js' +import { FMPEquityPeersFetcher } from './models/equity-peers.js' +import { FMPEquityScreenerFetcher } from './models/equity-screener.js' +import { FMPCompanyFilingsFetcher } from './models/company-filings.js' +import { FMPPricePerformanceFetcher } from './models/price-performance.js' +import { FMPMarketSnapshotsFetcher } from './models/market-snapshots.js' +import { FMPCurrencySnapshotsFetcher } from './models/currency-snapshots.js' +import { FMPAvailableIndicesFetcher } from './models/available-indices.js' +import { FMPIndexConstituentsFetcher } from './models/index-constituents.js' +import { FMPIndexHistoricalFetcher } from './models/index-historical.js' +import { FMPRiskPremiumFetcher } from './models/risk-premium.js' +import { FMPTreasuryRatesFetcher } from './models/treasury-rates.js' +import { FMPRevenueBusinessLineFetcher } from './models/revenue-business-line.js' +import { FMPRevenueGeographicFetcher } from './models/revenue-geographic.js' +import { FMPEarningsCallTranscriptFetcher } from './models/earnings-call-transcript.js' +import { FMPDiscoveryFilingsFetcher } from './models/discovery-filings.js' +import { FMPEsgScoreFetcher } from './models/esg-score.js' +import { FMPHistoricalMarketCapFetcher } from './models/historical-market-cap.js' + +export const fmpProvider = new Provider({ + name: 'fmp', + website: 'https://financialmodelingprep.com', + description: + 'Financial Modeling Prep is a new concept that informs you about ' + + 'stock market information (news, currencies, and stock prices).', + credentials: ['api_key'], + reprName: 'Financial Modeling Prep (FMP)', + fetcherDict: { + EquityInfo: FMPEquityProfileFetcher, + EquityQuote: FMPEquityQuoteFetcher, + EquityHistorical: FMPEquityHistoricalFetcher, + BalanceSheet: FMPBalanceSheetFetcher, + IncomeStatement: FMPIncomeStatementFetcher, + CashFlowStatement: FMPCashFlowStatementFetcher, + FinancialRatios: FMPFinancialRatiosFetcher, + KeyMetrics: FMPKeyMetricsFetcher, + InsiderTrading: FMPInsiderTradingFetcher, + CalendarEarnings: FMPCalendarEarningsFetcher, + CompanyNews: FMPCompanyNewsFetcher, + WorldNews: FMPWorldNewsFetcher, + PriceTargetConsensus: FMPPriceTargetConsensusFetcher, + EquityGainers: FMPGainersFetcher, + EquityLosers: FMPLosersFetcher, + EquityActive: FMPEquityActiveFetcher, + CryptoHistorical: FMPCryptoHistoricalFetcher, + CryptoSearch: FMPCryptoSearchFetcher, + CurrencyHistorical: FMPCurrencyHistoricalFetcher, + CurrencyPairs: FMPCurrencyPairsFetcher, + BalanceSheetGrowth: FMPBalanceSheetGrowthFetcher, + IncomeStatementGrowth: FMPIncomeStatementGrowthFetcher, + CashFlowStatementGrowth: FMPCashFlowStatementGrowthFetcher, + CalendarDividend: FMPCalendarDividendFetcher, + CalendarSplits: FMPCalendarSplitsFetcher, + CalendarIpo: FMPCalendarIpoFetcher, + EconomicCalendar: FMPEconomicCalendarFetcher, + AnalystEstimates: FMPAnalystEstimatesFetcher, + ForwardEpsEstimates: FMPForwardEpsEstimatesFetcher, + ForwardEbitdaEstimates: FMPForwardEbitdaEstimatesFetcher, + PriceTarget: FMPPriceTargetFetcher, + EtfInfo: FMPEtfInfoFetcher, + EtfHoldings: FMPEtfHoldingsFetcher, + EtfSectors: FMPEtfSectorsFetcher, + EtfCountries: FMPEtfCountriesFetcher, + EtfEquityExposure: FMPEtfEquityExposureFetcher, + EtfSearch: FMPEtfSearchFetcher, + KeyExecutives: FMPKeyExecutivesFetcher, + ExecutiveCompensation: FMPExecutiveCompensationFetcher, + GovernmentTrades: FMPGovernmentTradesFetcher, + InstitutionalOwnership: FMPInstitutionalOwnershipFetcher, + // EtfHistorical reuses the same fetcher as EquityHistorical (same pattern as Python) + EtfHistorical: FMPEquityHistoricalFetcher, + HistoricalDividends: FMPHistoricalDividendsFetcher, + HistoricalSplits: FMPHistoricalSplitsFetcher, + HistoricalEps: FMPHistoricalEpsFetcher, + HistoricalEmployees: FMPHistoricalEmployeesFetcher, + ShareStatistics: FMPShareStatisticsFetcher, + EquityPeers: FMPEquityPeersFetcher, + EquityScreener: FMPEquityScreenerFetcher, + CompanyFilings: FMPCompanyFilingsFetcher, + PricePerformance: FMPPricePerformanceFetcher, + MarketSnapshots: FMPMarketSnapshotsFetcher, + CurrencySnapshots: FMPCurrencySnapshotsFetcher, + AvailableIndices: FMPAvailableIndicesFetcher, + IndexConstituents: FMPIndexConstituentsFetcher, + IndexHistorical: FMPIndexHistoricalFetcher, + RiskPremium: FMPRiskPremiumFetcher, + TreasuryRates: FMPTreasuryRatesFetcher, + RevenueBusinessLine: FMPRevenueBusinessLineFetcher, + RevenueGeographic: FMPRevenueGeographicFetcher, + EarningsCallTranscript: FMPEarningsCallTranscriptFetcher, + DiscoveryFilings: FMPDiscoveryFilingsFetcher, + EsgScore: FMPEsgScoreFetcher, + HistoricalMarketCap: FMPHistoricalMarketCapFetcher, + }, +}) diff --git a/packages/opentypebb/src/providers/fmp/models/active.ts b/packages/opentypebb/src/providers/fmp/models/active.ts new file mode 100644 index 00000000..b9bbfce3 --- /dev/null +++ b/packages/opentypebb/src/providers/fmp/models/active.ts @@ -0,0 +1,49 @@ +/** + * FMP Most Active Model. + * Maps to: openbb_fmp/models/equity_most_active.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { EquityPerformanceQueryParamsSchema, EquityPerformanceDataSchema } from '../../../standard-models/equity-performance.js' +import { applyAliases } from '../../../core/provider/utils/helpers.js' +import { getDataMany } from '../utils/helpers.js' + +const ALIAS_DICT: Record = { percent_change: 'changesPercentage' } + +export const FMPEquityActiveQueryParamsSchema = EquityPerformanceQueryParamsSchema +export type FMPEquityActiveQueryParams = z.infer + +export const FMPEquityActiveDataSchema = EquityPerformanceDataSchema.extend({ + exchange: z.string().describe('Stock exchange where the security is listed.'), +}).passthrough() +export type FMPEquityActiveData = z.infer + +export class FMPEquityActiveFetcher extends Fetcher { + static override transformQuery(params: Record): FMPEquityActiveQueryParams { + return FMPEquityActiveQueryParamsSchema.parse(params) + } + + static override async extractData( + query: FMPEquityActiveQueryParams, + credentials: Record | null, + ): Promise[]> { + const apiKey = credentials?.fmp_api_key ?? '' + return getDataMany(`https://financialmodelingprep.com/stable/most-actives?apikey=${apiKey}`) + } + + static override transformData( + query: FMPEquityActiveQueryParams, + data: Record[], + ): FMPEquityActiveData[] { + const sorted = [...data].sort((a, b) => { + const diff = Number(b.changesPercentage ?? 0) - Number(a.changesPercentage ?? 0) + return query.sort === 'desc' ? diff : -diff + }) + return sorted.map((d) => { + const aliased = applyAliases(d, ALIAS_DICT) + if (typeof aliased.percent_change === 'number') aliased.percent_change = aliased.percent_change / 100 + return FMPEquityActiveDataSchema.parse(aliased) + }) + } +} diff --git a/packages/opentypebb/src/providers/fmp/models/analyst-estimates.ts b/packages/opentypebb/src/providers/fmp/models/analyst-estimates.ts new file mode 100644 index 00000000..a02cb231 --- /dev/null +++ b/packages/opentypebb/src/providers/fmp/models/analyst-estimates.ts @@ -0,0 +1,78 @@ +/** + * FMP Analyst Estimates Model. + * Maps to: openbb_fmp/models/analyst_estimates.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { AnalystEstimatesQueryParamsSchema, AnalystEstimatesDataSchema } from '../../../standard-models/analyst-estimates.js' +import { applyAliases } from '../../../core/provider/utils/helpers.js' +import { getDataMany } from '../utils/helpers.js' + +// --- Query Params --- + +export const FMPAnalystEstimatesQueryParamsSchema = AnalystEstimatesQueryParamsSchema.extend({ + period: z.enum(['annual', 'quarter']).default('annual').describe('Time period of the data to return.'), + limit: z.coerce.number().int().nullable().default(null).describe('The number of data entries to return.'), +}) + +export type FMPAnalystEstimatesQueryParams = z.infer + +// --- Data --- + +const ALIAS_DICT: Record = { + estimated_revenue_low: 'revenueLow', + estimated_revenue_high: 'revenueHigh', + estimated_revenue_avg: 'revenueAvg', + estimated_sga_expense_low: 'sgaExpenseLow', + estimated_sga_expense_high: 'sgaExpenseHigh', + estimated_sga_expense_avg: 'sgaExpenseAvg', + estimated_ebitda_low: 'ebitdaLow', + estimated_ebitda_high: 'ebitdaHigh', + estimated_ebitda_avg: 'ebitdaAvg', + estimated_ebit_low: 'ebitLow', + estimated_ebit_high: 'ebitHigh', + estimated_ebit_avg: 'ebitAvg', + estimated_net_income_low: 'netIncomeLow', + estimated_net_income_high: 'netIncomeHigh', + estimated_net_income_avg: 'netIncomeAvg', + estimated_eps_low: 'epsLow', + estimated_eps_high: 'epsHigh', + estimated_eps_avg: 'epsAvg', + number_analyst_estimated_revenue: 'numAnalystsRevenue', + number_analysts_estimated_eps: 'numAnalystsEps', +} + +export const FMPAnalystEstimatesDataSchema = AnalystEstimatesDataSchema.extend({}).passthrough() +export type FMPAnalystEstimatesData = z.infer + +// --- Fetcher --- + +export class FMPAnalystEstimatesFetcher extends Fetcher { + static override transformQuery(params: Record): FMPAnalystEstimatesQueryParams { + return FMPAnalystEstimatesQueryParamsSchema.parse(params) + } + + static override async extractData( + query: FMPAnalystEstimatesQueryParams, + credentials: Record | null, + ): Promise[]> { + const apiKey = credentials?.fmp_api_key ?? '' + const url = 'https://financialmodelingprep.com/stable/analyst-estimates' + + `?symbol=${query.symbol}` + + `&period=${query.period}` + + (query.limit ? `&limit=${query.limit}` : '') + + `&apikey=${apiKey}` + return getDataMany(url) + } + + static override transformData( + query: FMPAnalystEstimatesQueryParams, + data: Record[], + ): FMPAnalystEstimatesData[] { + return data.map(d => { + const aliased = applyAliases(d, ALIAS_DICT) + return FMPAnalystEstimatesDataSchema.parse(aliased) + }) + } +} diff --git a/packages/opentypebb/src/providers/fmp/models/available-indices.ts b/packages/opentypebb/src/providers/fmp/models/available-indices.ts new file mode 100644 index 00000000..d2242666 --- /dev/null +++ b/packages/opentypebb/src/providers/fmp/models/available-indices.ts @@ -0,0 +1,38 @@ +/** + * FMP Available Indices Model. + * Maps to: openbb_fmp/models/available_indices.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { AvailableIndicesQueryParamsSchema, AvailableIndicesDataSchema } from '../../../standard-models/available-indices.js' +import { getDataMany } from '../utils/helpers.js' + +export const FMPAvailableIndicesQueryParamsSchema = AvailableIndicesQueryParamsSchema +export type FMPAvailableIndicesQueryParams = z.infer + +export const FMPAvailableIndicesDataSchema = AvailableIndicesDataSchema +export type FMPAvailableIndicesData = z.infer + +export class FMPAvailableIndicesFetcher extends Fetcher { + static override transformQuery(params: Record): FMPAvailableIndicesQueryParams { + return FMPAvailableIndicesQueryParamsSchema.parse(params) + } + + static override async extractData( + _query: FMPAvailableIndicesQueryParams, + credentials: Record | null, + ): Promise[]> { + const apiKey = credentials?.fmp_api_key ?? '' + return getDataMany( + `https://financialmodelingprep.com/stable/index-list?apikey=${apiKey}`, + ) + } + + static override transformData( + _query: FMPAvailableIndicesQueryParams, + data: Record[], + ): FMPAvailableIndicesData[] { + return data.map(d => FMPAvailableIndicesDataSchema.parse(d)) + } +} diff --git a/packages/opentypebb/src/providers/fmp/models/balance-sheet-growth.ts b/packages/opentypebb/src/providers/fmp/models/balance-sheet-growth.ts new file mode 100644 index 00000000..5f4c1b88 --- /dev/null +++ b/packages/opentypebb/src/providers/fmp/models/balance-sheet-growth.ts @@ -0,0 +1,125 @@ +/** + * FMP Balance Sheet Growth Model. + * Maps to: openbb_fmp/models/balance_sheet_growth.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { BalanceSheetGrowthQueryParamsSchema, BalanceSheetGrowthDataSchema } from '../../../standard-models/balance-sheet-growth.js' +import { applyAliases } from '../../../core/provider/utils/helpers.js' +import { getDataMany } from '../utils/helpers.js' + +// --- Query Params --- + +export const FMPBalanceSheetGrowthQueryParamsSchema = BalanceSheetGrowthQueryParamsSchema.extend({ + period: z.enum(['annual', 'quarter']).default('annual').describe('Time period of the data to return.'), + limit: z.coerce.number().int().nullable().default(null).describe('The number of data entries to return.'), +}) + +export type FMPBalanceSheetGrowthQueryParams = z.infer + +// --- Data --- + +const ALIAS_DICT: Record = { + period_ending: 'date', + fiscal_year: 'calendarYear', + fiscal_period: 'period', + reported_currency: 'reportedCurrency', + growth_other_total_shareholders_equity: 'growthOthertotalStockholdersEquity', + growth_total_shareholders_equity: 'growthTotalStockholdersEquity', + growth_total_liabilities_and_shareholders_equity: 'growthTotalLiabilitiesAndStockholdersEquity', + growth_accumulated_other_comprehensive_income: 'growthAccumulatedOtherComprehensiveIncomeLoss', + growth_prepaid_expenses: 'growthPrepaids', +} + +const pctOrNull = z.number().nullable().default(null) + +export const FMPBalanceSheetGrowthDataSchema = BalanceSheetGrowthDataSchema.extend({ + symbol: z.string().nullable().default(null).describe('The stock ticker symbol.'), + reported_currency: z.string().nullable().default(null).describe('The currency in which the financial data is reported.'), + growth_cash_and_cash_equivalents: pctOrNull.describe('Growth rate of cash and cash equivalents.'), + growth_short_term_investments: pctOrNull.describe('Growth rate of short-term investments.'), + growth_cash_and_short_term_investments: pctOrNull.describe('Growth rate of cash and short-term investments.'), + growth_accounts_receivables: pctOrNull.describe('Growth rate of accounts receivable.'), + growth_other_receivables: pctOrNull.describe('Growth rate of other receivables.'), + growth_net_receivables: pctOrNull.describe('Growth rate of net receivables.'), + growth_inventory: pctOrNull.describe('Growth rate of inventory.'), + growth_other_current_assets: pctOrNull.describe('Growth rate of other current assets.'), + growth_total_current_assets: pctOrNull.describe('Growth rate of total current assets.'), + growth_property_plant_equipment_net: pctOrNull.describe('Growth rate of net property, plant, and equipment.'), + growth_goodwill: pctOrNull.describe('Growth rate of goodwill.'), + growth_intangible_assets: pctOrNull.describe('Growth rate of intangible assets.'), + growth_goodwill_and_intangible_assets: pctOrNull.describe('Growth rate of goodwill and intangible assets.'), + growth_long_term_investments: pctOrNull.describe('Growth rate of long-term investments.'), + growth_tax_assets: pctOrNull.describe('Growth rate of tax assets.'), + growth_other_non_current_assets: pctOrNull.describe('Growth rate of other non-current assets.'), + growth_total_non_current_assets: pctOrNull.describe('Growth rate of total non-current assets.'), + growth_other_assets: pctOrNull.describe('Growth rate of other assets.'), + growth_total_assets: pctOrNull.describe('Growth rate of total assets.'), + growth_account_payables: pctOrNull.describe('Growth rate of accounts payable.'), + growth_other_payables: pctOrNull.describe('Growth rate of other payables.'), + growth_total_payables: pctOrNull.describe('Growth rate of total payables.'), + growth_accrued_expenses: pctOrNull.describe('Growth rate of accrued expenses.'), + growth_prepaid_expenses: pctOrNull.describe('Growth rate of prepaid expenses.'), + growth_capital_lease_obligations_current: pctOrNull.describe('Growth rate of current capital lease obligations.'), + growth_short_term_debt: pctOrNull.describe('Growth rate of short-term debt.'), + growth_tax_payables: pctOrNull.describe('Growth rate of tax payables.'), + growth_deferred_tax_liabilities_non_current: pctOrNull.describe('Growth rate of non-current deferred tax liabilities.'), + growth_deferred_revenue: pctOrNull.describe('Growth rate of deferred revenue.'), + growth_other_current_liabilities: pctOrNull.describe('Growth rate of other current liabilities.'), + growth_total_current_liabilities: pctOrNull.describe('Growth rate of total current liabilities.'), + growth_deferred_revenue_non_current: pctOrNull.describe('Growth rate of non-current deferred revenue.'), + growth_long_term_debt: pctOrNull.describe('Growth rate of long-term debt.'), + growth_deferrred_tax_liabilities_non_current: pctOrNull.describe('Growth rate of non-current deferred tax liabilities (alternate).'), + growth_other_non_current_liabilities: pctOrNull.describe('Growth rate of other non-current liabilities.'), + growth_total_non_current_liabilities: pctOrNull.describe('Growth rate of total non-current liabilities.'), + growth_other_liabilities: pctOrNull.describe('Growth rate of other liabilities.'), + growth_total_liabilities: pctOrNull.describe('Growth rate of total liabilities.'), + growth_retained_earnings: pctOrNull.describe('Growth rate of retained earnings.'), + growth_accumulated_other_comprehensive_income: pctOrNull.describe('Growth rate of accumulated other comprehensive income/loss.'), + growth_minority_interest: pctOrNull.describe('Growth rate of minority interest.'), + growth_additional_paid_in_capital: pctOrNull.describe('Growth rate of additional paid-in capital.'), + growth_other_total_shareholders_equity: pctOrNull.describe("Growth rate of other total stockholders' equity."), + growth_total_shareholders_equity: pctOrNull.describe("Growth rate of total stockholders' equity."), + growth_common_stock: pctOrNull.describe('Growth rate of common stock.'), + growth_preferred_stock: pctOrNull.describe('Growth rate of preferred stock.'), + growth_treasury_stock: pctOrNull.describe('Growth rate of treasury stock.'), + growth_total_equity: pctOrNull.describe('Growth rate of total equity.'), + growth_total_liabilities_and_shareholders_equity: pctOrNull.describe("Growth rate of total liabilities and stockholders' equity."), + growth_total_investments: pctOrNull.describe('Growth rate of total investments.'), + growth_total_debt: pctOrNull.describe('Growth rate of total debt.'), + growth_net_debt: pctOrNull.describe('Growth rate of net debt.'), +}).passthrough() + +export type FMPBalanceSheetGrowthData = z.infer + +// --- Fetcher --- + +export class FMPBalanceSheetGrowthFetcher extends Fetcher { + static override transformQuery(params: Record): FMPBalanceSheetGrowthQueryParams { + return FMPBalanceSheetGrowthQueryParamsSchema.parse(params) + } + + static override async extractData( + query: FMPBalanceSheetGrowthQueryParams, + credentials: Record | null, + ): Promise[]> { + const apiKey = credentials?.fmp_api_key ?? '' + const url = 'https://financialmodelingprep.com/stable/balance-sheet-statement-growth' + + `?symbol=${query.symbol}` + + `&period=${query.period}` + + `&limit=${query.limit ?? 5}` + + `&apikey=${apiKey}` + return getDataMany(url) + } + + static override transformData( + query: FMPBalanceSheetGrowthQueryParams, + data: Record[], + ): FMPBalanceSheetGrowthData[] { + return data.map(d => { + const aliased = applyAliases(d, ALIAS_DICT) + return FMPBalanceSheetGrowthDataSchema.parse(aliased) + }) + } +} diff --git a/packages/opentypebb/src/providers/fmp/models/balance-sheet.ts b/packages/opentypebb/src/providers/fmp/models/balance-sheet.ts new file mode 100644 index 00000000..4cee7d65 --- /dev/null +++ b/packages/opentypebb/src/providers/fmp/models/balance-sheet.ts @@ -0,0 +1,172 @@ +/** + * FMP Balance Sheet Model. + * Maps to: openbb_fmp/models/balance_sheet.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { BalanceSheetQueryParamsSchema, BalanceSheetDataSchema } from '../../../standard-models/balance-sheet.js' +import { applyAliases } from '../../../core/provider/utils/helpers.js' +import { getDataMany } from '../utils/helpers.js' +import type { FinancialStatementPeriods } from '../utils/definitions.js' + +// --- Query Params --- + +export const FMPBalanceSheetQueryParamsSchema = BalanceSheetQueryParamsSchema.extend({ + period: z.enum(['q1', 'q2', 'q3', 'q4', 'fy', 'ttm', 'annual', 'quarter']).default('annual').describe('Time period of the data to return.'), +}) + +export type FMPBalanceSheetQueryParams = z.infer + +// --- Data --- + +const ALIAS_DICT: Record = { + period_ending: 'date', + fiscal_period: 'period', + fiscal_year: 'calendarYear', + filing_date: 'fillingDate', + accepted_date: 'acceptedDate', + reported_currency: 'reportedCurrency', + cash_and_cash_equivalents: 'cashAndCashEquivalents', + short_term_investments: 'shortTermInvestments', + cash_and_short_term_investments: 'cashAndShortTermInvestments', + net_receivables: 'netReceivables', + inventory: 'inventories', + other_current_assets: 'otherCurrentAssets', + total_current_assets: 'totalCurrentAssets', + plant_property_equipment_net: 'propertyPlantEquipmentNet', + goodwill: 'goodwill', + prepaid_expenses: 'prepaids', + intangible_assets: 'intangibleAssets', + goodwill_and_intangible_assets: 'goodwillAndIntangibleAssets', + long_term_investments: 'longTermInvestments', + tax_assets: 'taxAssets', + other_non_current_assets: 'otherNonCurrentAssets', + non_current_assets: 'totalNonCurrentAssets', + other_assets: 'otherAssets', + total_assets: 'totalAssets', + accounts_payable: 'accountPayables', + short_term_debt: 'shortTermDebt', + tax_payables: 'taxPayables', + current_deferred_revenue: 'deferredRevenue', + other_current_liabilities: 'otherCurrentLiabilities', + total_current_liabilities: 'totalCurrentLiabilities', + long_term_debt: 'longTermDebt', + deferred_revenue_non_current: 'deferredRevenueNonCurrent', + deferred_tax_liabilities_non_current: 'deferredTaxLiabilitiesNonCurrent', + other_non_current_liabilities: 'otherNonCurrentLiabilities', + total_non_current_liabilities: 'totalNonCurrentLiabilities', + other_liabilities: 'otherLiabilities', + capital_lease_obligations: 'capitalLeaseObligations', + total_liabilities: 'totalLiabilities', + preferred_stock: 'preferredStock', + common_stock: 'commonStock', + retained_earnings: 'retainedEarnings', + accumulated_other_comprehensive_income: 'accumulatedOtherComprehensiveIncomeLoss', + other_shareholders_equity: 'otherStockholdersEquity', + other_total_shareholders_equity: 'otherTotalStockholdersEquity', + total_common_equity: 'totalStockholdersEquity', + total_equity_non_controlling_interests: 'totalEquity', + total_liabilities_and_shareholders_equity: 'totalLiabilitiesAndStockholdersEquity', + minority_interest: 'minorityInterest', + total_liabilities_and_total_equity: 'totalLiabilitiesAndTotalEquity', + total_investments: 'totalInvestments', + total_debt: 'totalDebt', + net_debt: 'netDebt', +} + +const intOrNull = z.number().int().nullable().default(null) + +export const FMPBalanceSheetDataSchema = BalanceSheetDataSchema.extend({ + filing_date: z.string().nullable().default(null).describe('The date when the filing was made.'), + accepted_date: z.string().nullable().default(null).describe('The date and time when the filing was accepted.'), + cik: z.string().nullable().default(null).describe('The Central Index Key (CIK) assigned by the SEC.'), + symbol: z.string().nullable().default(null).describe('The stock ticker symbol.'), + reported_currency: z.string().nullable().default(null).describe('The currency in which the balance sheet was reported.'), + cash_and_cash_equivalents: intOrNull.describe('Cash and cash equivalents.'), + short_term_investments: intOrNull.describe('Short term investments.'), + cash_and_short_term_investments: intOrNull.describe('Cash and short term investments.'), + net_receivables: intOrNull.describe('Net receivables.'), + inventory: intOrNull.describe('Inventory.'), + other_current_assets: intOrNull.describe('Other current assets.'), + total_current_assets: intOrNull.describe('Total current assets.'), + plant_property_equipment_net: intOrNull.describe('Plant property equipment net.'), + goodwill: intOrNull.describe('Goodwill.'), + intangible_assets: intOrNull.describe('Intangible assets.'), + goodwill_and_intangible_assets: intOrNull.describe('Goodwill and intangible assets.'), + long_term_investments: intOrNull.describe('Long term investments.'), + tax_assets: intOrNull.describe('Tax assets.'), + other_non_current_assets: intOrNull.describe('Other non current assets.'), + non_current_assets: intOrNull.describe('Total non current assets.'), + other_assets: intOrNull.describe('Other assets.'), + total_assets: intOrNull.describe('Total assets.'), + accounts_payable: intOrNull.describe('Accounts payable.'), + prepaid_expenses: intOrNull.describe('Prepaid expenses.'), + short_term_debt: intOrNull.describe('Short term debt.'), + tax_payables: intOrNull.describe('Tax payables.'), + current_deferred_revenue: intOrNull.describe('Current deferred revenue.'), + other_current_liabilities: intOrNull.describe('Other current liabilities.'), + total_current_liabilities: intOrNull.describe('Total current liabilities.'), + long_term_debt: intOrNull.describe('Long term debt.'), + deferred_revenue_non_current: intOrNull.describe('Non current deferred revenue.'), + deferred_tax_liabilities_non_current: intOrNull.describe('Deferred tax liabilities non current.'), + other_non_current_liabilities: intOrNull.describe('Other non current liabilities.'), + total_non_current_liabilities: intOrNull.describe('Total non current liabilities.'), + capital_lease_obligations: intOrNull.describe('Capital lease obligations.'), + other_liabilities: intOrNull.describe('Other liabilities.'), + total_liabilities: intOrNull.describe('Total liabilities.'), + preferred_stock: intOrNull.describe('Preferred stock.'), + common_stock: intOrNull.describe('Common stock.'), + retained_earnings: intOrNull.describe('Retained earnings.'), + accumulated_other_comprehensive_income: intOrNull.describe('Accumulated other comprehensive income (loss).'), + other_shareholders_equity: intOrNull.describe('Other shareholders equity.'), + other_total_shareholders_equity: intOrNull.describe('Other total shareholders equity.'), + total_common_equity: intOrNull.describe('Total common equity.'), + total_equity_non_controlling_interests: intOrNull.describe('Total equity non controlling interests.'), + total_liabilities_and_shareholders_equity: intOrNull.describe('Total liabilities and shareholders equity.'), + minority_interest: intOrNull.describe('Minority interest.'), + total_liabilities_and_total_equity: intOrNull.describe('Total liabilities and total equity.'), + total_investments: intOrNull.describe('Total investments.'), + total_debt: intOrNull.describe('Total debt.'), + net_debt: intOrNull.describe('Net debt.'), +}).passthrough() + +export type FMPBalanceSheetData = z.infer + +// --- Fetcher --- + +export class FMPBalanceSheetFetcher extends Fetcher { + static override transformQuery(params: Record): FMPBalanceSheetQueryParams { + return FMPBalanceSheetQueryParamsSchema.parse(params) + } + + static override async extractData( + query: FMPBalanceSheetQueryParams, + credentials: Record | null, + ): Promise[]> { + const apiKey = credentials?.fmp_api_key ?? '' + let baseUrl = 'https://financialmodelingprep.com/stable/balance-sheet-statement' + + if (query.period === 'ttm') { + baseUrl += '-ttm' + } + + const url = baseUrl + + `?symbol=${query.symbol}` + + (query.period !== 'ttm' ? `&period=${query.period}` : '') + + `&limit=${query.limit ?? 5}` + + `&apikey=${apiKey}` + + return getDataMany(url) + } + + static override transformData( + query: FMPBalanceSheetQueryParams, + data: Record[], + ): FMPBalanceSheetData[] { + return data.map((d) => { + const aliased = applyAliases(d, ALIAS_DICT) + return FMPBalanceSheetDataSchema.parse(aliased) + }) + } +} diff --git a/packages/opentypebb/src/providers/fmp/models/calendar-dividend.ts b/packages/opentypebb/src/providers/fmp/models/calendar-dividend.ts new file mode 100644 index 00000000..1dc30db6 --- /dev/null +++ b/packages/opentypebb/src/providers/fmp/models/calendar-dividend.ts @@ -0,0 +1,74 @@ +/** + * FMP Dividend Calendar Model. + * Maps to: openbb_fmp/models/calendar_dividend.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { CalendarDividendQueryParamsSchema, CalendarDividendDataSchema } from '../../../standard-models/calendar-dividend.js' +import { applyAliases } from '../../../core/provider/utils/helpers.js' +import { getDataMany } from '../utils/helpers.js' + +// --- Query Params --- + +export const FMPCalendarDividendQueryParamsSchema = CalendarDividendQueryParamsSchema.extend({}) +export type FMPCalendarDividendQueryParams = z.infer + +// --- Data --- + +const ALIAS_DICT: Record = { + amount: 'dividend', + ex_dividend_date: 'date', + record_date: 'recordDate', + payment_date: 'paymentDate', + declaration_date: 'declarationDate', + adjusted_amount: 'adjDividend', + dividend_yield: 'yield', +} + +export const FMPCalendarDividendDataSchema = CalendarDividendDataSchema.extend({ + adjusted_amount: z.number().nullable().default(null).describe('The adjusted-dividend amount.'), + dividend_yield: z.number().nullable().default(null).describe('Annualized dividend yield.'), + frequency: z.string().nullable().default(null).describe('Frequency of the regular dividend payment.'), +}).passthrough() + +export type FMPCalendarDividendData = z.infer + +// --- Fetcher --- + +export class FMPCalendarDividendFetcher extends Fetcher { + static override transformQuery(params: Record): FMPCalendarDividendQueryParams { + return FMPCalendarDividendQueryParamsSchema.parse(params) + } + + static override async extractData( + query: FMPCalendarDividendQueryParams, + credentials: Record | null, + ): Promise[]> { + const apiKey = credentials?.fmp_api_key ?? '' + const now = new Date() + const startDate = query.start_date ?? now.toISOString().slice(0, 10) + const endDate = query.end_date ?? new Date(now.getTime() + 14 * 86400000).toISOString().slice(0, 10) + + const url = 'https://financialmodelingprep.com/stable/dividends-calendar' + + `?from=${startDate}&to=${endDate}&apikey=${apiKey}` + return getDataMany(url) + } + + static override transformData( + query: FMPCalendarDividendQueryParams, + data: Record[], + ): FMPCalendarDividendData[] { + const sorted = [...data].sort((a, b) => + String(a.date ?? '').localeCompare(String(b.date ?? '')), + ) + return sorted.map(d => { + const aliased = applyAliases(d, ALIAS_DICT) + // Normalize dividend_yield from percent to decimal + if (typeof aliased.dividend_yield === 'number') { + aliased.dividend_yield = aliased.dividend_yield / 100 + } + return FMPCalendarDividendDataSchema.parse(aliased) + }) + } +} diff --git a/packages/opentypebb/src/providers/fmp/models/calendar-earnings.ts b/packages/opentypebb/src/providers/fmp/models/calendar-earnings.ts new file mode 100644 index 00000000..70d80b25 --- /dev/null +++ b/packages/opentypebb/src/providers/fmp/models/calendar-earnings.ts @@ -0,0 +1,106 @@ +/** + * FMP Earnings Calendar Model. + * Maps to: openbb_fmp/models/calendar_earnings.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { CalendarEarningsQueryParamsSchema, CalendarEarningsDataSchema } from '../../../standard-models/calendar-earnings.js' +import { applyAliases, amakeRequest } from '../../../core/provider/utils/helpers.js' +import { responseCallback } from '../utils/helpers.js' + +// --- Query Params --- + +export const FMPCalendarEarningsQueryParamsSchema = CalendarEarningsQueryParamsSchema + +export type FMPCalendarEarningsQueryParams = z.infer + +// --- Data --- + +const ALIAS_DICT: Record = { + report_date: 'date', + eps_consensus: 'epsEstimated', + eps_actual: 'epsActual', + revenue_actual: 'revenueActual', + revenue_consensus: 'revenueEstimated', + last_updated: 'lastUpdated', +} + +export const FMPCalendarEarningsDataSchema = CalendarEarningsDataSchema.extend({ + eps_actual: z.number().nullable().default(null).describe('The actual earnings per share announced.'), + revenue_consensus: z.number().nullable().default(null).describe('The revenue forecast consensus.'), + revenue_actual: z.number().nullable().default(null).describe('The actual reported revenue.'), + last_updated: z.string().nullable().default(null).describe('The date the data was updated last.'), +}).passthrough() + +export type FMPCalendarEarningsData = z.infer + +// --- Fetcher --- + +export class FMPCalendarEarningsFetcher extends Fetcher { + static override transformQuery(params: Record): FMPCalendarEarningsQueryParams { + const now = new Date() + if (params.start_date == null) { + params.start_date = now.toISOString().split('T')[0] + } + if (params.end_date == null) { + const threeDaysLater = new Date(now) + threeDaysLater.setDate(threeDaysLater.getDate() + 3) + params.end_date = threeDaysLater.toISOString().split('T')[0] + } + return FMPCalendarEarningsQueryParamsSchema.parse(params) + } + + static override async extractData( + query: FMPCalendarEarningsQueryParams, + credentials: Record | null, + ): Promise[]> { + const apiKey = credentials?.fmp_api_key ?? '' + const baseUrl = 'https://financialmodelingprep.com/stable/earnings-calendar?' + const startDate = query.start_date ?? new Date().toISOString().split('T')[0] + const endDate = query.end_date ?? new Date().toISOString().split('T')[0] + + // Create 7-day chunks + const urls: string[] = [] + let currentStart = new Date(startDate) + const end = new Date(endDate) + + while (currentStart <= end) { + const chunkEnd = new Date(currentStart) + chunkEnd.setDate(chunkEnd.getDate() + 7) + const actualEnd = chunkEnd > end ? end : chunkEnd + + const from = currentStart.toISOString().split('T')[0] + const to = actualEnd.toISOString().split('T')[0] + urls.push(`${baseUrl}from=${from}&to=${to}&apikey=${apiKey}`) + + currentStart = new Date(actualEnd) + currentStart.setDate(currentStart.getDate() + 1) + } + + const allData: Record[] = [] + const results = await Promise.all( + urls.map((url) => amakeRequest[]>(url, { responseCallback }).catch(() => [])), + ) + + for (const batch of results) { + if (Array.isArray(batch)) allData.push(...batch) + } + + return allData + } + + static override transformData( + query: FMPCalendarEarningsQueryParams, + data: Record[], + ): FMPCalendarEarningsData[] { + const sorted = [...data].sort((a, b) => + String(b.date ?? '').localeCompare(String(a.date ?? '')), + ) + + return sorted.map((d) => { + const aliased = applyAliases(d, ALIAS_DICT) + return FMPCalendarEarningsDataSchema.parse(aliased) + }) + } +} diff --git a/packages/opentypebb/src/providers/fmp/models/calendar-ipo.ts b/packages/opentypebb/src/providers/fmp/models/calendar-ipo.ts new file mode 100644 index 00000000..378584c7 --- /dev/null +++ b/packages/opentypebb/src/providers/fmp/models/calendar-ipo.ts @@ -0,0 +1,68 @@ +/** + * FMP IPO Calendar Model. + * Maps to: openbb_fmp/models/calendar_ipo.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { CalendarIpoQueryParamsSchema, CalendarIpoDataSchema } from '../../../standard-models/calendar-ipo.js' +import { applyAliases } from '../../../core/provider/utils/helpers.js' +import { getDataMany } from '../utils/helpers.js' + +// --- Query Params --- + +export const FMPCalendarIpoQueryParamsSchema = CalendarIpoQueryParamsSchema.extend({}) +export type FMPCalendarIpoQueryParams = z.infer + +// --- Data --- + +const ALIAS_DICT: Record = { + ipo_date: 'date', + name: 'company', +} + +export const FMPCalendarIpoDataSchema = CalendarIpoDataSchema.extend({ + name: z.string().nullable().default(null).describe('The name of the entity going public.'), + exchange: z.string().nullable().default(null).describe('The exchange where the IPO is listed.'), + actions: z.string().nullable().default(null).describe('Actions related to the IPO.'), + shares: z.number().nullable().default(null).describe('The number of shares being offered in the IPO.'), + price_range: z.string().nullable().default(null).describe('The expected price range for the IPO shares.'), + market_cap: z.number().nullable().default(null).describe('The estimated market capitalization at IPO time.'), +}).passthrough() + +export type FMPCalendarIpoData = z.infer + +// --- Fetcher --- + +export class FMPCalendarIpoFetcher extends Fetcher { + static override transformQuery(params: Record): FMPCalendarIpoQueryParams { + return FMPCalendarIpoQueryParamsSchema.parse(params) + } + + static override async extractData( + query: FMPCalendarIpoQueryParams, + credentials: Record | null, + ): Promise[]> { + const apiKey = credentials?.fmp_api_key ?? '' + const now = new Date() + const startDate = query.start_date ?? now.toISOString().slice(0, 10) + const endDate = query.end_date ?? new Date(now.getTime() + 3 * 86400000).toISOString().slice(0, 10) + + const url = 'https://financialmodelingprep.com/stable/ipos-calendar' + + `?from=${startDate}&to=${endDate}&apikey=${apiKey}` + return getDataMany(url) + } + + static override transformData( + query: FMPCalendarIpoQueryParams, + data: Record[], + ): FMPCalendarIpoData[] { + const sorted = [...data].sort((a, b) => + String(b.date ?? '').localeCompare(String(a.date ?? '')), + ) + return sorted.map(d => { + const aliased = applyAliases(d, ALIAS_DICT) + return FMPCalendarIpoDataSchema.parse(aliased) + }) + } +} diff --git a/packages/opentypebb/src/providers/fmp/models/calendar-splits.ts b/packages/opentypebb/src/providers/fmp/models/calendar-splits.ts new file mode 100644 index 00000000..08a93535 --- /dev/null +++ b/packages/opentypebb/src/providers/fmp/models/calendar-splits.ts @@ -0,0 +1,48 @@ +/** + * FMP Calendar Splits Model. + * Maps to: openbb_fmp/models/calendar_splits.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { CalendarSplitsQueryParamsSchema, CalendarSplitsDataSchema } from '../../../standard-models/calendar-splits.js' +import { getDataMany } from '../utils/helpers.js' + +// --- Query Params --- + +export const FMPCalendarSplitsQueryParamsSchema = CalendarSplitsQueryParamsSchema.extend({}) +export type FMPCalendarSplitsQueryParams = z.infer + +// --- Data --- + +export const FMPCalendarSplitsDataSchema = CalendarSplitsDataSchema.extend({}).passthrough() +export type FMPCalendarSplitsData = z.infer + +// --- Fetcher --- + +export class FMPCalendarSplitsFetcher extends Fetcher { + static override transformQuery(params: Record): FMPCalendarSplitsQueryParams { + return FMPCalendarSplitsQueryParamsSchema.parse(params) + } + + static override async extractData( + query: FMPCalendarSplitsQueryParams, + credentials: Record | null, + ): Promise[]> { + const apiKey = credentials?.fmp_api_key ?? '' + const now = new Date() + const startDate = query.start_date ?? new Date(now.getTime() - 7 * 86400000).toISOString().slice(0, 10) + const endDate = query.end_date ?? new Date(now.getTime() + 14 * 86400000).toISOString().slice(0, 10) + + const url = 'https://financialmodelingprep.com/stable/splits-calendar' + + `?from=${startDate}&to=${endDate}&apikey=${apiKey}` + return getDataMany(url) + } + + static override transformData( + query: FMPCalendarSplitsQueryParams, + data: Record[], + ): FMPCalendarSplitsData[] { + return data.map(d => FMPCalendarSplitsDataSchema.parse(d)) + } +} diff --git a/packages/opentypebb/src/providers/fmp/models/cash-flow-growth.ts b/packages/opentypebb/src/providers/fmp/models/cash-flow-growth.ts new file mode 100644 index 00000000..e9eab5de --- /dev/null +++ b/packages/opentypebb/src/providers/fmp/models/cash-flow-growth.ts @@ -0,0 +1,121 @@ +/** + * FMP Cash Flow Statement Growth Model. + * Maps to: openbb_fmp/models/cash_flow_growth.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { CashFlowStatementGrowthQueryParamsSchema, CashFlowStatementGrowthDataSchema } from '../../../standard-models/cash-flow-growth.js' +import { applyAliases } from '../../../core/provider/utils/helpers.js' +import { getDataMany } from '../utils/helpers.js' + +// --- Query Params --- + +export const FMPCashFlowStatementGrowthQueryParamsSchema = CashFlowStatementGrowthQueryParamsSchema.extend({ + period: z.enum(['annual', 'quarter']).default('annual').describe('Time period of the data to return.'), +}) + +export type FMPCashFlowStatementGrowthQueryParams = z.infer + +// --- Data --- + +const ALIAS_DICT: Record = { + period_ending: 'date', + fiscal_year: 'calendarYear', + fiscal_period: 'period', + reported_currency: 'reportedCurrency', + growth_acquisitions: 'growthAcquisitionsNet', + growth_sale_and_maturity_of_investments: 'growthSalesMaturitiesOfInvestments', + growth_net_cash_from_operating_activities: 'growthNetCashProvidedByOperatingActivites', + growth_other_investing_activities: 'growthOtherInvestingActivites', + growth_net_cash_from_investing_activities: 'growthNetCashUsedForInvestingActivites', + growth_other_financing_activities: 'growthOtherFinancingActivites', + growth_purchase_of_investment_securities: 'growthPurchasesOfInvestments', + growth_account_receivables: 'growthAccountsReceivables', + growth_account_payable: 'growthAccountsPayables', + growth_purchase_of_property_plant_and_equipment: 'growthInvestmentsInPropertyPlantAndEquipment', + growth_repayment_of_debt: 'growthDebtRepayment', + growth_net_change_in_cash_and_equivalents: 'growthNetChangeInCash', + growth_effect_of_exchange_rate_changes_on_cash: 'growthEffectOfForexChangesOnCash', + growth_net_cash_from_financing_activities: 'growthNetCashUsedProvidedByFinancingActivities', + growth_net_equity_issuance: 'growthNetStockIssuance', + growth_common_equity_issuance: 'growthCommonStockIssued', + growth_common_equity_repurchased: 'growthCommonStockRepurchased', +} + +const pctOrNull = z.number().nullable().default(null) + +export const FMPCashFlowStatementGrowthDataSchema = CashFlowStatementGrowthDataSchema.extend({ + symbol: z.string().nullable().default(null).describe('The stock ticker symbol.'), + reported_currency: z.string().nullable().default(null).describe('The currency in which the financial data is reported.'), + growth_net_income: pctOrNull.describe('Growth rate of net income.'), + growth_depreciation_and_amortization: pctOrNull.describe('Growth rate of depreciation and amortization.'), + growth_deferred_income_tax: pctOrNull.describe('Growth rate of deferred income tax.'), + growth_stock_based_compensation: pctOrNull.describe('Growth rate of stock-based compensation.'), + growth_change_in_working_capital: pctOrNull.describe('Growth rate of change in working capital.'), + growth_account_receivables: pctOrNull.describe('Growth rate of accounts receivables.'), + growth_inventory: pctOrNull.describe('Growth rate of inventory.'), + growth_account_payable: pctOrNull.describe('Growth rate of account payable.'), + growth_other_working_capital: pctOrNull.describe('Growth rate of other working capital.'), + growth_other_non_cash_items: pctOrNull.describe('Growth rate of other non-cash items.'), + growth_net_cash_from_operating_activities: pctOrNull.describe('Growth rate of net cash provided by operating activities.'), + growth_purchase_of_property_plant_and_equipment: pctOrNull.describe('Growth rate of investments in property, plant, and equipment.'), + growth_acquisitions: pctOrNull.describe('Growth rate of net acquisitions.'), + growth_purchase_of_investment_securities: pctOrNull.describe('Growth rate of purchases of investments.'), + growth_sale_and_maturity_of_investments: pctOrNull.describe('Growth rate of sales maturities of investments.'), + growth_other_investing_activities: pctOrNull.describe('Growth rate of other investing activities.'), + growth_net_cash_from_investing_activities: pctOrNull.describe('Growth rate of net cash used for investing activities.'), + growth_short_term_net_debt_issuance: pctOrNull.describe('Growth rate of short term net debt issuance.'), + growth_long_term_net_debt_issuance: pctOrNull.describe('Growth rate of long term net debt issuance.'), + growth_net_debt_issuance: pctOrNull.describe('Growth rate of net debt issuance.'), + growth_repayment_of_debt: pctOrNull.describe('Growth rate of debt repayment.'), + growth_common_equity_issuance: pctOrNull.describe('Growth rate of common equity issued.'), + growth_common_equity_repurchased: pctOrNull.describe('Growth rate of common equity repurchased.'), + growth_net_equity_issuance: pctOrNull.describe('Growth rate of net equity issuance.'), + growth_dividends_paid: pctOrNull.describe('Growth rate of dividends paid.'), + growth_preferred_dividends_paid: pctOrNull.describe('Growth rate of preferred dividends paid.'), + growth_other_financing_activities: pctOrNull.describe('Growth rate of other financing activities.'), + growth_net_cash_from_financing_activities: pctOrNull.describe('Growth rate of net cash used/provided by financing activities.'), + growth_effect_of_exchange_rate_changes_on_cash: pctOrNull.describe('Growth rate of the effect of foreign exchange changes on cash.'), + growth_net_change_in_cash_and_equivalents: pctOrNull.describe('Growth rate of net change in cash.'), + growth_cash_at_beginning_of_period: pctOrNull.describe('Growth rate of cash at the beginning of the period.'), + growth_cash_at_end_of_period: pctOrNull.describe('Growth rate of cash at the end of the period.'), + growth_operating_cash_flow: pctOrNull.describe('Growth rate of operating cash flow.'), + growth_capital_expenditure: pctOrNull.describe('Growth rate of capital expenditure.'), + growth_income_taxes_paid: pctOrNull.describe('Growth rate of income taxes paid.'), + growth_interest_paid: pctOrNull.describe('Growth rate of interest paid.'), + growth_free_cash_flow: pctOrNull.describe('Growth rate of free cash flow.'), +}).passthrough() + +export type FMPCashFlowStatementGrowthData = z.infer + +// --- Fetcher --- + +export class FMPCashFlowStatementGrowthFetcher extends Fetcher { + static override transformQuery(params: Record): FMPCashFlowStatementGrowthQueryParams { + return FMPCashFlowStatementGrowthQueryParamsSchema.parse(params) + } + + static override async extractData( + query: FMPCashFlowStatementGrowthQueryParams, + credentials: Record | null, + ): Promise[]> { + const apiKey = credentials?.fmp_api_key ?? '' + const url = 'https://financialmodelingprep.com/stable/cash-flow-statement-growth' + + `?symbol=${query.symbol}` + + `&period=${query.period}` + + `&limit=${query.limit ?? 5}` + + `&apikey=${apiKey}` + return getDataMany(url) + } + + static override transformData( + query: FMPCashFlowStatementGrowthQueryParams, + data: Record[], + ): FMPCashFlowStatementGrowthData[] { + return data.map(d => { + const aliased = applyAliases(d, ALIAS_DICT) + return FMPCashFlowStatementGrowthDataSchema.parse(aliased) + }) + } +} diff --git a/packages/opentypebb/src/providers/fmp/models/cash-flow.ts b/packages/opentypebb/src/providers/fmp/models/cash-flow.ts new file mode 100644 index 00000000..a8f5f29a --- /dev/null +++ b/packages/opentypebb/src/providers/fmp/models/cash-flow.ts @@ -0,0 +1,143 @@ +/** + * FMP Cash Flow Statement Model. + * Maps to: openbb_fmp/models/cash_flow.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { CashFlowStatementQueryParamsSchema, CashFlowStatementDataSchema } from '../../../standard-models/cash-flow.js' +import { applyAliases } from '../../../core/provider/utils/helpers.js' +import { getDataMany } from '../utils/helpers.js' + +// --- Query Params --- + +export const FMPCashFlowStatementQueryParamsSchema = CashFlowStatementQueryParamsSchema.extend({ + period: z.enum(['q1', 'q2', 'q3', 'q4', 'fy', 'ttm', 'annual', 'quarter']).default('annual').describe('Time period of the data to return.'), +}) + +export type FMPCashFlowStatementQueryParams = z.infer + +// --- Data --- + +const ALIAS_DICT: Record = { + period_ending: 'date', + fiscal_period: 'period', + fiscal_year: 'calendarYear', + filing_date: 'fillingDate', + accepted_date: 'acceptedDate', + reported_currency: 'reportedCurrency', + net_income: 'netIncome', + depreciation_and_amortization: 'depreciationAndAmortization', + deferred_income_tax: 'deferredIncomeTax', + stock_based_compensation: 'stockBasedCompensation', + change_in_working_capital: 'changeInWorkingCapital', + change_in_account_receivables: 'accountsReceivables', + change_in_inventory: 'inventory', + change_in_account_payable: 'accountsPayables', + change_in_other_working_capital: 'otherWorkingCapital', + change_in_other_non_cash_items: 'otherNonCashItems', + net_cash_from_operating_activities: 'netCashProvidedByOperatingActivities', + purchase_of_property_plant_and_equipment: 'investmentsInPropertyPlantAndEquipment', + acquisitions: 'acquisitionsNet', + purchase_of_investment_securities: 'purchasesOfInvestments', + sale_and_maturity_of_investments: 'salesMaturitiesOfInvestments', + other_investing_activities: 'otherInvestingActivities', + net_cash_from_investing_activities: 'netCashProvidedByInvestingActivities', + repayment_of_debt: 'debtRepayment', + issuance_of_common_equity: 'commonStockIssuance', + repurchase_of_common_equity: 'commonStockRepurchased', + net_common_equity_issuance: 'netCommonStockIssuance', + net_preferred_equity_issuance: 'netPreferredStockIssuance', + net_equity_issuance: 'netStockIssuance', + payment_of_dividends: 'dividendsPaid', + other_financing_activities: 'otherFinancingActivites', + net_cash_from_financing_activities: 'netCashProvidedByFinancingActivities', + effect_of_exchange_rate_changes_on_cash: 'effectOfForexChangesOnCash', + net_change_in_cash_and_equivalents: 'netChangeInCash', + cash_at_beginning_of_period: 'cashAtBeginningOfPeriod', + cash_at_end_of_period: 'cashAtEndOfPeriod', + operating_cash_flow: 'operatingCashFlow', + capital_expenditure: 'capitalExpenditure', + free_cash_flow: 'freeCashFlow', +} + +const intOrNull = z.number().int().nullable().default(null) + +export const FMPCashFlowStatementDataSchema = CashFlowStatementDataSchema.extend({ + fiscal_year: z.number().int().nullable().default(null).describe('The fiscal year of the fiscal period.'), + filing_date: z.string().nullable().default(null).describe('The date of the filing.'), + accepted_date: z.string().nullable().default(null).describe('The date the filing was accepted.'), + cik: z.string().nullable().default(null).describe('The Central Index Key (CIK) assigned by the SEC.'), + symbol: z.string().nullable().default(null).describe('The stock ticker symbol.'), + reported_currency: z.string().nullable().default(null).describe('The currency in which the cash flow statement was reported.'), + net_income: intOrNull.describe('Net income.'), + depreciation_and_amortization: intOrNull.describe('Depreciation and amortization.'), + deferred_income_tax: intOrNull.describe('Deferred income tax.'), + stock_based_compensation: intOrNull.describe('Stock-based compensation.'), + change_in_working_capital: intOrNull.describe('Change in working capital.'), + change_in_account_receivables: intOrNull.describe('Change in account receivables.'), + change_in_inventory: intOrNull.describe('Change in inventory.'), + change_in_account_payable: intOrNull.describe('Change in account payable.'), + change_in_other_working_capital: intOrNull.describe('Change in other working capital.'), + change_in_other_non_cash_items: intOrNull.describe('Change in other non-cash items.'), + net_cash_from_operating_activities: intOrNull.describe('Net cash from operating activities.'), + purchase_of_property_plant_and_equipment: intOrNull.describe('Purchase of property, plant and equipment.'), + acquisitions: intOrNull.describe('Acquisitions.'), + purchase_of_investment_securities: intOrNull.describe('Purchase of investment securities.'), + sale_and_maturity_of_investments: intOrNull.describe('Sale and maturity of investments.'), + other_investing_activities: intOrNull.describe('Other investing activities.'), + net_cash_from_investing_activities: intOrNull.describe('Net cash from investing activities.'), + repayment_of_debt: intOrNull.describe('Repayment of debt.'), + issuance_of_common_equity: intOrNull.describe('Issuance of common equity.'), + repurchase_of_common_equity: intOrNull.describe('Repurchase of common equity.'), + payment_of_dividends: intOrNull.describe('Payment of dividends.'), + other_financing_activities: intOrNull.describe('Other financing activities.'), + net_cash_from_financing_activities: intOrNull.describe('Net cash from financing activities.'), + effect_of_exchange_rate_changes_on_cash: intOrNull.describe('Effect of exchange rate changes on cash.'), + net_change_in_cash_and_equivalents: intOrNull.describe('Net change in cash and equivalents.'), + cash_at_beginning_of_period: intOrNull.describe('Cash at beginning of period.'), + cash_at_end_of_period: intOrNull.describe('Cash at end of period.'), + operating_cash_flow: intOrNull.describe('Operating cash flow.'), + capital_expenditure: intOrNull.describe('Capital expenditure.'), + free_cash_flow: intOrNull.describe('Free cash flow.'), +}).passthrough() + +export type FMPCashFlowStatementData = z.infer + +// --- Fetcher --- + +export class FMPCashFlowStatementFetcher extends Fetcher { + static override transformQuery(params: Record): FMPCashFlowStatementQueryParams { + return FMPCashFlowStatementQueryParamsSchema.parse(params) + } + + static override async extractData( + query: FMPCashFlowStatementQueryParams, + credentials: Record | null, + ): Promise[]> { + const apiKey = credentials?.fmp_api_key ?? '' + let baseUrl = 'https://financialmodelingprep.com/stable/cash-flow-statement' + + if (query.period === 'ttm') { + baseUrl += '-ttm' + } + + const url = baseUrl + + `?symbol=${query.symbol}` + + (query.period !== 'ttm' ? `&period=${query.period}` : '') + + `&limit=${query.limit ?? 5}` + + `&apikey=${apiKey}` + + return getDataMany(url) + } + + static override transformData( + query: FMPCashFlowStatementQueryParams, + data: Record[], + ): FMPCashFlowStatementData[] { + return data.map((d) => { + const aliased = applyAliases(d, ALIAS_DICT) + return FMPCashFlowStatementDataSchema.parse(aliased) + }) + } +} diff --git a/packages/opentypebb/src/providers/fmp/models/company-filings.ts b/packages/opentypebb/src/providers/fmp/models/company-filings.ts new file mode 100644 index 00000000..2c9945e5 --- /dev/null +++ b/packages/opentypebb/src/providers/fmp/models/company-filings.ts @@ -0,0 +1,79 @@ +/** + * FMP Company Filings Model. + * Maps to: openbb_fmp/models/company_filings.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { CompanyFilingsQueryParamsSchema, CompanyFilingsDataSchema } from '../../../standard-models/company-filings.js' +import { applyAliases } from '../../../core/provider/utils/helpers.js' +import { getDataMany } from '../utils/helpers.js' + +const ALIAS_DICT: Record = { + accepted_date: 'acceptedDate', + report_type: 'formType', + filing_url: 'link', + report_url: 'finalLink', +} + +export const FMPCompanyFilingsQueryParamsSchema = CompanyFilingsQueryParamsSchema.extend({ + cik: z.string().nullable().default(null).describe('CIK number.'), + start_date: z.string().nullable().default(null).describe('Start date of the data, in YYYY-MM-DD format.'), + end_date: z.string().nullable().default(null).describe('End date of the data, in YYYY-MM-DD format.'), + limit: z.coerce.number().default(1000).describe('The number of data entries to return (max 1000).'), + page: z.coerce.number().default(0).describe('Page number for pagination.'), +}) +export type FMPCompanyFilingsQueryParams = z.infer + +export const FMPCompanyFilingsDataSchema = CompanyFilingsDataSchema.extend({ + filing_url: z.string().nullable().default(null).describe('URL to the filing document.'), + symbol: z.string().nullable().default(null).describe('Symbol.'), + cik: z.string().nullable().default(null).describe('CIK number.'), + accepted_date: z.string().nullable().default(null).describe('Date the filing was accepted.'), +}).passthrough() +export type FMPCompanyFilingsData = z.infer + +export class FMPCompanyFilingsFetcher extends Fetcher { + static override transformQuery(params: Record): FMPCompanyFilingsQueryParams { + return FMPCompanyFilingsQueryParamsSchema.parse(params) + } + + static override async extractData( + query: FMPCompanyFilingsQueryParams, + credentials: Record | null, + ): Promise[]> { + const apiKey = credentials?.fmp_api_key ?? '' + const qs = new URLSearchParams() + qs.set('apikey', apiKey) + qs.set('limit', String(Math.min(query.limit, 1000))) + qs.set('page', String(query.page)) + + if (query.start_date) qs.set('from', query.start_date) + if (query.end_date) qs.set('to', query.end_date) + + let endpoint: string + if (query.symbol) { + qs.set('symbol', query.symbol) + endpoint = 'sec-filings-search/symbol' + } else if (query.cik) { + qs.set('cik', query.cik) + endpoint = 'sec-filings-search/cik' + } else { + endpoint = 'sec-filings-search/symbol' + } + + return getDataMany( + `https://financialmodelingprep.com/stable/${endpoint}?${qs.toString()}`, + ) + } + + static override transformData( + _query: FMPCompanyFilingsQueryParams, + data: Record[], + ): FMPCompanyFilingsData[] { + return data.map(d => { + const aliased = applyAliases(d, ALIAS_DICT) + return FMPCompanyFilingsDataSchema.parse(aliased) + }) + } +} diff --git a/packages/opentypebb/src/providers/fmp/models/company-news.ts b/packages/opentypebb/src/providers/fmp/models/company-news.ts new file mode 100644 index 00000000..074fb0cc --- /dev/null +++ b/packages/opentypebb/src/providers/fmp/models/company-news.ts @@ -0,0 +1,91 @@ +/** + * FMP Company News Model. + * Maps to: openbb_fmp/models/company_news.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { CompanyNewsQueryParamsSchema, CompanyNewsDataSchema } from '../../../standard-models/company-news.js' +import { applyAliases } from '../../../core/provider/utils/helpers.js' +import { EmptyDataError } from '../../../core/provider/utils/errors.js' +import { getDataMany } from '../utils/helpers.js' + +// --- Query Params --- + +export const FMPCompanyNewsQueryParamsSchema = CompanyNewsQueryParamsSchema.extend({ + page: z.number().int().min(0).max(100).default(0).describe('Page number of the results.'), + press_release: z.boolean().nullable().default(null).describe('When true, return only press releases.'), +}) + +export type FMPCompanyNewsQueryParams = z.infer + +// --- Data --- + +const ALIAS_DICT: Record = { + symbols: 'symbol', + date: 'publishedDate', + author: 'publisher', + images: 'image', + source: 'site', + excerpt: 'text', +} + +export const FMPCompanyNewsDataSchema = CompanyNewsDataSchema.extend({ + source: z.string().describe('Name of the news site.'), +}).passthrough() + +export type FMPCompanyNewsData = z.infer + +// --- Fetcher --- + +export class FMPCompanyNewsFetcher extends Fetcher { + static override transformQuery(params: Record): FMPCompanyNewsQueryParams { + if (!params.symbol) { + throw new Error('Required field missing -> symbol') + } + return FMPCompanyNewsQueryParamsSchema.parse(params) + } + + static override async extractData( + query: FMPCompanyNewsQueryParams, + credentials: Record | null, + ): Promise[]> { + const apiKey = credentials?.fmp_api_key ?? '' + const limit = query.limit ?? 250 + const page = query.page ?? 0 + let baseUrl = 'https://financialmodelingprep.com/stable/news/' + + if (query.press_release) { + baseUrl += 'press-releases?' + } else { + baseUrl += 'stock?' + } + + let url = baseUrl + `symbols=${query.symbol}` + + if (query.start_date) url += `&from=${query.start_date}` + if (query.end_date) url += `&to=${query.end_date}` + + url += `&limit=${limit}&page=${page}&apikey=${apiKey}` + + const response = await getDataMany(url) + + if (!response || response.length === 0) { + throw new EmptyDataError() + } + + return response.sort((a, b) => + String(b.publishedDate ?? '').localeCompare(String(a.publishedDate ?? '')), + ) + } + + static override transformData( + query: FMPCompanyNewsQueryParams, + data: Record[], + ): FMPCompanyNewsData[] { + return data.map((d) => { + const aliased = applyAliases(d, ALIAS_DICT) + return FMPCompanyNewsDataSchema.parse(aliased) + }) + } +} diff --git a/packages/opentypebb/src/providers/fmp/models/crypto-historical.ts b/packages/opentypebb/src/providers/fmp/models/crypto-historical.ts new file mode 100644 index 00000000..765a2015 --- /dev/null +++ b/packages/opentypebb/src/providers/fmp/models/crypto-historical.ts @@ -0,0 +1,60 @@ +/** + * FMP Crypto Historical Price Model. + * Maps to: openbb_fmp/models/crypto_historical.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { CryptoHistoricalQueryParamsSchema, CryptoHistoricalDataSchema } from '../../../standard-models/crypto-historical.js' +import { applyAliases } from '../../../core/provider/utils/helpers.js' +import { getHistoricalOhlc } from '../utils/helpers.js' + +export const FMPCryptoHistoricalQueryParamsSchema = CryptoHistoricalQueryParamsSchema.extend({ + interval: z.enum(['1m', '5m', '1h', '1d']).default('1d').describe('Time interval of the data.'), +}) +export type FMPCryptoHistoricalQueryParams = z.infer + +const ALIAS_DICT: Record = { change_percent: 'changeOverTime' } + +export const FMPCryptoHistoricalDataSchema = CryptoHistoricalDataSchema.extend({ + change: z.number().nullable().default(null).describe('Change in the price from the previous close.'), + change_percent: z.number().nullable().default(null).describe('Change in the price from the previous close, as a normalized percent.'), +}).passthrough() +export type FMPCryptoHistoricalData = z.infer + +export class FMPCryptoHistoricalFetcher extends Fetcher { + static override transformQuery(params: Record): FMPCryptoHistoricalQueryParams { + const now = new Date() + const oneYearAgo = new Date(now) + oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1) + if (params.start_date == null) params.start_date = oneYearAgo.toISOString().split('T')[0] + if (params.end_date == null) params.end_date = now.toISOString().split('T')[0] + return FMPCryptoHistoricalQueryParamsSchema.parse(params) + } + + static override async extractData( + query: FMPCryptoHistoricalQueryParams, + credentials: Record | null, + ): Promise[]> { + return getHistoricalOhlc(query, credentials) + } + + static override transformData( + query: FMPCryptoHistoricalQueryParams, + data: Record[], + ): FMPCryptoHistoricalData[] { + const multiSymbol = query.symbol.split(',').length > 1 + const sorted = [...data].sort((a, b) => { + if (multiSymbol) { + const dc = String(a.date ?? '').localeCompare(String(b.date ?? '')) + return dc !== 0 ? dc : String(a.symbol ?? '').localeCompare(String(b.symbol ?? '')) + } + return String(a.date ?? '').localeCompare(String(b.date ?? '')) + }) + return sorted.map((d) => { + const aliased = applyAliases(d, ALIAS_DICT) + if (typeof aliased.change_percent === 'number') aliased.change_percent = aliased.change_percent / 100 + return FMPCryptoHistoricalDataSchema.parse(aliased) + }) + } +} diff --git a/packages/opentypebb/src/providers/fmp/models/crypto-search.ts b/packages/opentypebb/src/providers/fmp/models/crypto-search.ts new file mode 100644 index 00000000..cfc0a3a2 --- /dev/null +++ b/packages/opentypebb/src/providers/fmp/models/crypto-search.ts @@ -0,0 +1,51 @@ +/** + * FMP Crypto Search Model. + * Maps to: openbb_fmp/models/crypto_search.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { CryptoSearchQueryParamsSchema, CryptoSearchDataSchema } from '../../../standard-models/crypto-search.js' +import { getDataMany } from '../utils/helpers.js' + +export const FMPCryptoSearchQueryParamsSchema = CryptoSearchQueryParamsSchema +export type FMPCryptoSearchQueryParams = z.infer + +export const FMPCryptoSearchDataSchema = CryptoSearchDataSchema.extend({ + exchange: z.string().nullable().default(null).describe('The exchange code the crypto trades on.'), +}).passthrough() +export type FMPCryptoSearchData = z.infer + +export class FMPCryptoSearchFetcher extends Fetcher { + static override transformQuery(params: Record): FMPCryptoSearchQueryParams { + // Remove dashes from query + if (typeof params.query === 'string') { + params.query = params.query.replace(/-/g, '') + } + return FMPCryptoSearchQueryParamsSchema.parse(params) + } + + static override async extractData( + query: FMPCryptoSearchQueryParams, + credentials: Record | null, + ): Promise[]> { + const apiKey = credentials?.fmp_api_key ?? '' + return getDataMany(`https://financialmodelingprep.com/stable/cryptocurrency-list?apikey=${apiKey}`) + } + + static override transformData( + query: FMPCryptoSearchQueryParams, + data: Record[], + ): FMPCryptoSearchData[] { + let filtered = data + if (query.query) { + const q = query.query.toLowerCase() + filtered = data.filter((d) => + String(d.symbol ?? '').toLowerCase().includes(q) || + String(d.name ?? '').toLowerCase().includes(q) || + String(d.exchange ?? '').toLowerCase().includes(q), + ) + } + return filtered.map((d) => FMPCryptoSearchDataSchema.parse(d)) + } +} diff --git a/packages/opentypebb/src/providers/fmp/models/currency-historical.ts b/packages/opentypebb/src/providers/fmp/models/currency-historical.ts new file mode 100644 index 00000000..62f6c73d --- /dev/null +++ b/packages/opentypebb/src/providers/fmp/models/currency-historical.ts @@ -0,0 +1,57 @@ +/** + * FMP Currency Historical Price Model. + * Maps to: openbb_fmp/models/currency_historical.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { CurrencyHistoricalQueryParamsSchema, CurrencyHistoricalDataSchema } from '../../../standard-models/currency-historical.js' +import { applyAliases } from '../../../core/provider/utils/helpers.js' +import { getHistoricalOhlc } from '../utils/helpers.js' + +export const FMPCurrencyHistoricalQueryParamsSchema = CurrencyHistoricalQueryParamsSchema.extend({ + interval: z.enum(['1m', '5m', '1h', '1d']).default('1d').describe('Time interval of the data.'), +}) +export type FMPCurrencyHistoricalQueryParams = z.infer + +export const FMPCurrencyHistoricalDataSchema = CurrencyHistoricalDataSchema.extend({ + change: z.number().nullable().default(null).describe('Change in the price from the previous close.'), + change_percent: z.number().nullable().default(null).describe('Percent change in the price from the previous close.'), +}).passthrough() +export type FMPCurrencyHistoricalData = z.infer + +export class FMPCurrencyHistoricalFetcher extends Fetcher { + static override transformQuery(params: Record): FMPCurrencyHistoricalQueryParams { + const now = new Date() + const oneYearAgo = new Date(now) + oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1) + if (params.start_date == null) params.start_date = oneYearAgo.toISOString().split('T')[0] + if (params.end_date == null) params.end_date = now.toISOString().split('T')[0] + return FMPCurrencyHistoricalQueryParamsSchema.parse(params) + } + + static override async extractData( + query: FMPCurrencyHistoricalQueryParams, + credentials: Record | null, + ): Promise[]> { + return getHistoricalOhlc(query, credentials) + } + + static override transformData( + query: FMPCurrencyHistoricalQueryParams, + data: Record[], + ): FMPCurrencyHistoricalData[] { + const multiSymbol = query.symbol.split(',').length > 1 + const sorted = [...data].sort((a, b) => { + if (multiSymbol) { + const dc = String(a.date ?? '').localeCompare(String(b.date ?? '')) + return dc !== 0 ? dc : String(a.symbol ?? '').localeCompare(String(b.symbol ?? '')) + } + return String(a.date ?? '').localeCompare(String(b.date ?? '')) + }) + return sorted.map((d) => { + if (typeof d.change_percent === 'number') d.change_percent = d.change_percent / 100 + return FMPCurrencyHistoricalDataSchema.parse(d) + }) + } +} diff --git a/packages/opentypebb/src/providers/fmp/models/currency-pairs.ts b/packages/opentypebb/src/providers/fmp/models/currency-pairs.ts new file mode 100644 index 00000000..50507958 --- /dev/null +++ b/packages/opentypebb/src/providers/fmp/models/currency-pairs.ts @@ -0,0 +1,62 @@ +/** + * FMP Currency Available Pairs Model. + * Maps to: openbb_fmp/models/currency_pairs.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { CurrencyPairsQueryParamsSchema, CurrencyPairsDataSchema } from '../../../standard-models/currency-pairs.js' +import { EmptyDataError } from '../../../core/provider/utils/errors.js' +import { getDataMany } from '../utils/helpers.js' + +export const FMPCurrencyPairsQueryParamsSchema = CurrencyPairsQueryParamsSchema +export type FMPCurrencyPairsQueryParams = z.infer + +export const FMPCurrencyPairsDataSchema = CurrencyPairsDataSchema.extend({ + from_currency: z.string().describe('Base currency of the currency pair.'), + to_currency: z.string().describe('Quote currency of the currency pair.'), + from_name: z.string().describe('Name of the base currency.'), + to_name: z.string().describe('Name of the quote currency.'), +}).passthrough() +export type FMPCurrencyPairsData = z.infer + +export class FMPCurrencyPairsFetcher extends Fetcher { + static override transformQuery(params: Record): FMPCurrencyPairsQueryParams { + return FMPCurrencyPairsQueryParamsSchema.parse(params) + } + + static override async extractData( + query: FMPCurrencyPairsQueryParams, + credentials: Record | null, + ): Promise[]> { + const apiKey = credentials?.fmp_api_key ?? '' + return getDataMany(`https://financialmodelingprep.com/stable/forex-list?apikey=${apiKey}`) + } + + static override transformData( + query: FMPCurrencyPairsQueryParams, + data: Record[], + ): FMPCurrencyPairsData[] { + if (!data || data.length === 0) { + throw new EmptyDataError('The request was returned empty.') + } + + let filtered = data + if (query.query) { + const q = query.query.toLowerCase() + filtered = data.filter((d) => + String(d.symbol ?? '').toLowerCase().includes(q) || + String(d.fromCurrency ?? '').toLowerCase().includes(q) || + String(d.toCurrency ?? '').toLowerCase().includes(q) || + String(d.fromName ?? '').toLowerCase().includes(q) || + String(d.toName ?? '').toLowerCase().includes(q), + ) + } + + if (filtered.length === 0) { + throw new EmptyDataError(`No results were found with the query supplied. -> ${query.query}`) + } + + return filtered.map((d) => FMPCurrencyPairsDataSchema.parse(d)) + } +} diff --git a/packages/opentypebb/src/providers/fmp/models/currency-snapshots.ts b/packages/opentypebb/src/providers/fmp/models/currency-snapshots.ts new file mode 100644 index 00000000..daf69e70 --- /dev/null +++ b/packages/opentypebb/src/providers/fmp/models/currency-snapshots.ts @@ -0,0 +1,135 @@ +/** + * FMP Currency Snapshots Model. + * Maps to: openbb_fmp/models/currency_snapshots.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { CurrencySnapshotsQueryParamsSchema, CurrencySnapshotsDataSchema } from '../../../standard-models/currency-snapshots.js' +import { applyAliases } from '../../../core/provider/utils/helpers.js' +import { getDataMany } from '../utils/helpers.js' +import { EmptyDataError } from '../../../core/provider/utils/errors.js' + +const ALIAS_DICT: Record = { + last_rate: 'price', + high: 'dayHigh', + low: 'dayLow', + ma50: 'priceAvg50', + ma200: 'priceAvg200', + year_high: 'yearHigh', + year_low: 'yearLow', + prev_close: 'previousClose', + change_percent: 'changePercentage', + last_rate_timestamp: 'timestamp', +} + +const numOrNull = z.number().nullable().default(null) + +export const FMPCurrencySnapshotsQueryParamsSchema = CurrencySnapshotsQueryParamsSchema +export type FMPCurrencySnapshotsQueryParams = z.infer + +export const FMPCurrencySnapshotsDataSchema = CurrencySnapshotsDataSchema.extend({ + change: numOrNull.describe('The change in the price from the previous close.'), + change_percent: numOrNull.describe('The change percent from the previous close.'), + ma50: numOrNull.describe('The 50-day moving average.'), + ma200: numOrNull.describe('The 200-day moving average.'), + year_high: numOrNull.describe('The 52-week high.'), + year_low: numOrNull.describe('The 52-week low.'), + last_rate_timestamp: z.string().nullable().default(null).describe('The timestamp of the last rate.'), +}).passthrough() +export type FMPCurrencySnapshotsData = z.infer + +export class FMPCurrencySnapshotsFetcher extends Fetcher { + static override transformQuery(params: Record): FMPCurrencySnapshotsQueryParams { + // Uppercase the base + if (typeof params.base === 'string') params.base = params.base.toUpperCase() + if (typeof params.counter_currencies === 'string') { + params.counter_currencies = params.counter_currencies.toUpperCase() + } + return FMPCurrencySnapshotsQueryParamsSchema.parse(params) + } + + static override async extractData( + _query: FMPCurrencySnapshotsQueryParams, + credentials: Record | null, + ): Promise[]> { + const apiKey = credentials?.fmp_api_key ?? '' + return getDataMany( + `https://financialmodelingprep.com/stable/batch-forex-quotes?short=false&apikey=${apiKey}`, + ) + } + + static override transformData( + query: FMPCurrencySnapshotsQueryParams, + data: Record[], + ): FMPCurrencySnapshotsData[] { + if (!data || data.length === 0) { + throw new EmptyDataError('No data was returned from the FMP endpoint.') + } + + const results: FMPCurrencySnapshotsData[] = [] + const bases = query.base.toUpperCase().split(',') + const counterCurrencies = query.counter_currencies + ? query.counter_currencies.toUpperCase().split(',') + : null + + for (const base of bases) { + for (const d of data) { + const symbol = String(d.symbol ?? '') + const name = String(d.name ?? '') + + // Check if this pair matches + let isMatch = false + let baseCurrency = base + let counterCurrency = '' + + if (query.quote_type === 'indirect') { + // Indirect: base currency is on the left (e.g., USD/EUR -> looking for USD as base) + if (symbol.startsWith(base)) { + isMatch = true + const parts = name.split('/') + counterCurrency = parts.length > 1 ? parts[1].trim() : symbol.replace(base, '') + } + } else { + // Direct: base currency is on the right (e.g., EUR/USD -> looking for USD as base) + if (symbol.endsWith(base)) { + isMatch = true + const parts = name.split('/') + counterCurrency = parts.length > 0 ? parts[0].trim() : symbol.replace(base, '') + } + } + + if (!isMatch) continue + + // Filter counter currencies if specified + if (counterCurrencies && !counterCurrencies.includes(counterCurrency)) continue + + // Normalize change_percent + const entry = { ...d } + if (typeof entry.changePercentage === 'number') { + entry.changePercentage = entry.changePercentage / 100 + } + // Convert Unix timestamp to ISO string + if (typeof entry.timestamp === 'number') { + entry.timestamp = new Date((entry.timestamp as number) * 1000).toISOString() + } + + const aliased = applyAliases(entry, ALIAS_DICT) + aliased.base_currency = baseCurrency + aliased.counter_currency = counterCurrency + + try { + results.push(FMPCurrencySnapshotsDataSchema.parse(aliased)) + } catch { + // Skip entries that fail validation + } + } + } + + if (results.length === 0) { + throw new EmptyDataError('No data was found using the applied filters. Check the parameters.') + } + + return results + } +} diff --git a/packages/opentypebb/src/providers/fmp/models/discovery-filings.ts b/packages/opentypebb/src/providers/fmp/models/discovery-filings.ts new file mode 100644 index 00000000..d10bc57d --- /dev/null +++ b/packages/opentypebb/src/providers/fmp/models/discovery-filings.ts @@ -0,0 +1,81 @@ +/** + * FMP Discovery Filings Model. + * Maps to: openbb_fmp/models/discovery_filings.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { DiscoveryFilingsQueryParamsSchema, DiscoveryFilingsDataSchema } from '../../../standard-models/discovery-filings.js' +import { getDataMany } from '../utils/helpers.js' +import { EmptyDataError } from '../../../core/provider/utils/errors.js' + +export const FMPDiscoveryFilingsQueryParamsSchema = DiscoveryFilingsQueryParamsSchema.extend({ + limit: z.coerce.number().nullable().default(null).describe('The maximum number of results to return. Default is 10000.'), +}) +export type FMPDiscoveryFilingsQueryParams = z.infer + +export const FMPDiscoveryFilingsDataSchema = DiscoveryFilingsDataSchema.extend({ + final_link: z.string().nullable().default(null).describe('Direct URL to the main document of the filing.'), +}).passthrough() +export type FMPDiscoveryFilingsData = z.infer + +export class FMPDiscoveryFilingsFetcher extends Fetcher { + static override transformQuery(params: Record): FMPDiscoveryFilingsQueryParams { + return FMPDiscoveryFilingsQueryParamsSchema.parse(params) + } + + static override async extractData( + query: FMPDiscoveryFilingsQueryParams, + credentials: Record | null, + ): Promise[]> { + const apiKey = credentials?.fmp_api_key ?? '' + const limit = query.limit ?? 10000 + + const now = new Date() + const startDate = query.start_date ?? + new Date(now.getTime() - (query.form_type ? 89 : 2) * 86400000).toISOString().split('T')[0] + const endDate = query.end_date ?? now.toISOString().split('T')[0] + + const baseUrl = query.form_type + ? 'https://financialmodelingprep.com/stable/sec-filings-search/form-type' + : 'https://financialmodelingprep.com/stable/sec-filings-financials/' + + const qs = new URLSearchParams() + qs.set('from', startDate) + qs.set('to', endDate) + if (query.form_type) qs.set('formType', query.form_type) + qs.set('apikey', apiKey) + + // FMP only allows 1000 results per page + const pages = Math.ceil(limit / 1000) + const allResults: Record[] = [] + + for (let page = 0; page < pages; page++) { + try { + const data = await getDataMany( + `${baseUrl}?${qs.toString()}&page=${page}&limit=1000`, + ) + allResults.push(...data) + // If we got fewer than 1000, no more pages + if (data.length < 1000) break + } catch { + // Stop paginating on error (e.g., empty page) + break + } + } + + return allResults.sort((a, b) => + String(b.acceptedDate ?? '').localeCompare(String(a.acceptedDate ?? '')), + ) + } + + static override transformData( + _query: FMPDiscoveryFilingsQueryParams, + data: Record[], + ): FMPDiscoveryFilingsData[] { + if (!data || data.length === 0) { + throw new EmptyDataError('No data was returned for the given query.') + } + return data.map(d => FMPDiscoveryFilingsDataSchema.parse(d)) + } +} diff --git a/packages/opentypebb/src/providers/fmp/models/earnings-call-transcript.ts b/packages/opentypebb/src/providers/fmp/models/earnings-call-transcript.ts new file mode 100644 index 00000000..9a782afd --- /dev/null +++ b/packages/opentypebb/src/providers/fmp/models/earnings-call-transcript.ts @@ -0,0 +1,95 @@ +/** + * FMP Earnings Call Transcript Model. + * Maps to: openbb_fmp/models/earnings_call_transcript.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { EarningsCallTranscriptQueryParamsSchema, EarningsCallTranscriptDataSchema } from '../../../standard-models/earnings-call-transcript.js' +import { getDataMany, getDataOne } from '../utils/helpers.js' +import { OpenBBError } from '../../../core/provider/utils/errors.js' + +const ALIAS_DICT: Record = { + quarter: 'period', +} + +export const FMPEarningsCallTranscriptQueryParamsSchema = EarningsCallTranscriptQueryParamsSchema +export type FMPEarningsCallTranscriptQueryParams = z.infer + +export const FMPEarningsCallTranscriptDataSchema = EarningsCallTranscriptDataSchema +export type FMPEarningsCallTranscriptData = z.infer + +export class FMPEarningsCallTranscriptFetcher extends Fetcher { + static override transformQuery(params: Record): FMPEarningsCallTranscriptQueryParams { + return FMPEarningsCallTranscriptQueryParamsSchema.parse(params) + } + + static override async extractData( + query: FMPEarningsCallTranscriptQueryParams, + credentials: Record | null, + ): Promise[]> { + const apiKey = credentials?.fmp_api_key ?? '' + const symbol = query.symbol.toUpperCase() + + // Get available transcript dates for the symbol + let transcriptDates: Record[] + try { + transcriptDates = await getDataMany( + `https://financialmodelingprep.com/stable/earning-call-transcript-dates?symbol=${symbol}&apikey=${apiKey}`, + ) + } catch { + throw new OpenBBError(`No transcripts found for symbol ${symbol}.`) + } + + if (!transcriptDates || transcriptDates.length === 0) { + throw new OpenBBError(`No transcripts found for symbol ${symbol}.`) + } + + // Sort by date descending + transcriptDates.sort((a, b) => String(b.date ?? '').localeCompare(String(a.date ?? ''))) + + // Determine year and quarter + let year = query.year ?? (transcriptDates[0].fiscalYear as number) + let quarter = query.quarter ?? (transcriptDates[0].quarter as number) + + // Validate year exists + const validYears = transcriptDates.map(t => t.fiscalYear) + if (!validYears.includes(year)) { + year = transcriptDates[0].fiscalYear as number + } + + // Validate quarter exists for the year + const yearTranscripts = transcriptDates.filter(t => t.fiscalYear === year) + const validQuarters = yearTranscripts.map(t => t.quarter) + if (!validQuarters.includes(quarter)) { + quarter = yearTranscripts[0]?.quarter as number ?? 1 + } + + const url = `https://financialmodelingprep.com/stable/earning-call-transcript?symbol=${symbol}&year=${year}&quarter=${quarter}&apikey=${apiKey}` + + try { + const result = await getDataOne(url) + return [result] + } catch { + throw new OpenBBError(`No transcript found for ${symbol} in ${year} Q${quarter}`) + } + } + + static override transformData( + _query: FMPEarningsCallTranscriptQueryParams, + data: Record[], + ): FMPEarningsCallTranscriptData[] { + if (!data || data.length === 0) { + throw new OpenBBError('No data found.') + } + + return data.map(d => { + // Apply alias: period -> quarter + if (d.period != null && d.quarter == null) { + d.quarter = d.period + delete d.period + } + return FMPEarningsCallTranscriptDataSchema.parse(d) + }) + } +} diff --git a/packages/opentypebb/src/providers/fmp/models/economic-calendar.ts b/packages/opentypebb/src/providers/fmp/models/economic-calendar.ts new file mode 100644 index 00000000..9a98a1cb --- /dev/null +++ b/packages/opentypebb/src/providers/fmp/models/economic-calendar.ts @@ -0,0 +1,75 @@ +/** + * FMP Economic Calendar Model. + * Maps to: openbb_fmp/models/economic_calendar.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { EconomicCalendarQueryParamsSchema, EconomicCalendarDataSchema } from '../../../standard-models/economic-calendar.js' +import { applyAliases } from '../../../core/provider/utils/helpers.js' +import { getDataMany } from '../utils/helpers.js' + +// --- Query Params --- + +export const FMPEconomicCalendarQueryParamsSchema = EconomicCalendarQueryParamsSchema.extend({}) +export type FMPEconomicCalendarQueryParams = z.infer + +// --- Data --- + +const ALIAS_DICT: Record = { + consensus: 'estimate', + importance: 'impact', + last_updated: 'updatedAt', + created_at: 'createdAt', + change_percent: 'changePercentage', +} + +export const FMPEconomicCalendarDataSchema = EconomicCalendarDataSchema.extend({ + change: z.number().nullable().default(null).describe('Value change since previous.'), + change_percent: z.number().nullable().default(null).describe('Percentage change since previous.'), + last_updated: z.string().nullable().default(null).describe('Last updated timestamp.'), + created_at: z.string().nullable().default(null).describe('Created timestamp.'), +}).passthrough() + +export type FMPEconomicCalendarData = z.infer + +// --- Fetcher --- + +export class FMPEconomicCalendarFetcher extends Fetcher { + static override transformQuery(params: Record): FMPEconomicCalendarQueryParams { + return FMPEconomicCalendarQueryParamsSchema.parse(params) + } + + static override async extractData( + query: FMPEconomicCalendarQueryParams, + credentials: Record | null, + ): Promise[]> { + const apiKey = credentials?.fmp_api_key ?? '' + const now = new Date() + const startDate = query.start_date ?? new Date(now.getTime() - 1 * 86400000).toISOString().slice(0, 10) + const endDate = query.end_date ?? new Date(now.getTime() + 7 * 86400000).toISOString().slice(0, 10) + + const url = 'https://financialmodelingprep.com/stable/economic-calendar' + + `?from=${startDate}&to=${endDate}&apikey=${apiKey}` + return getDataMany(url) + } + + static override transformData( + query: FMPEconomicCalendarQueryParams, + data: Record[], + ): FMPEconomicCalendarData[] { + return data.map(d => { + // Replace empty strings/zeros with null + const cleaned: Record = {} + for (const [k, v] of Object.entries(d)) { + cleaned[k] = (v === '' || v === 0) ? null : v + } + const aliased = applyAliases(cleaned, ALIAS_DICT) + // Normalize change_percent from percent to decimal + if (typeof aliased.change_percent === 'number') { + aliased.change_percent = aliased.change_percent / 100 + } + return FMPEconomicCalendarDataSchema.parse(aliased) + }) + } +} diff --git a/packages/opentypebb/src/providers/fmp/models/equity-historical.ts b/packages/opentypebb/src/providers/fmp/models/equity-historical.ts new file mode 100644 index 00000000..a9851fef --- /dev/null +++ b/packages/opentypebb/src/providers/fmp/models/equity-historical.ts @@ -0,0 +1,89 @@ +/** + * FMP Equity Historical Price Model. + * Maps to: openbb_fmp/models/equity_historical.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { EquityHistoricalQueryParamsSchema, EquityHistoricalDataSchema } from '../../../standard-models/equity-historical.js' +import { applyAliases } from '../../../core/provider/utils/helpers.js' +import { EmptyDataError } from '../../../core/provider/utils/errors.js' +import { getHistoricalOhlc } from '../utils/helpers.js' + +// --- Query Params --- + +export const FMPEquityHistoricalQueryParamsSchema = EquityHistoricalQueryParamsSchema.extend({ + interval: z.enum(['1m', '5m', '15m', '30m', '1h', '4h', '1d']).default('1d').describe('Time interval of the data.'), + adjustment: z.enum(['splits_only', 'splits_and_dividends', 'unadjusted']).default('splits_only').describe('Type of adjustment for historical prices. Only applies to daily data.'), +}) + +export type FMPEquityHistoricalQueryParams = z.infer + +// --- Data --- + +const ALIAS_DICT: Record = { + open: 'adjOpen', + high: 'adjHigh', + low: 'adjLow', + close: 'adjClose', +} + +export const FMPEquityHistoricalDataSchema = EquityHistoricalDataSchema.extend({ + change: z.number().nullable().default(null).describe('Change in the price from the previous close.'), + change_percent: z.number().nullable().default(null).describe('Change in the price from the previous close, as a normalized percent.'), +}).passthrough() + +export type FMPEquityHistoricalData = z.infer + +// --- Fetcher --- + +export class FMPEquityHistoricalFetcher extends Fetcher { + static override transformQuery(params: Record): FMPEquityHistoricalQueryParams { + const now = new Date() + const oneYearAgo = new Date(now) + oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1) + + if (params.start_date == null) { + params.start_date = oneYearAgo.toISOString().split('T')[0] + } + if (params.end_date == null) { + params.end_date = now.toISOString().split('T')[0] + } + + return FMPEquityHistoricalQueryParamsSchema.parse(params) + } + + static override async extractData( + query: FMPEquityHistoricalQueryParams, + credentials: Record | null, + ): Promise[]> { + return getHistoricalOhlc(query, credentials) + } + + static override transformData( + query: FMPEquityHistoricalQueryParams, + data: Record[], + ): FMPEquityHistoricalData[] { + if (!data || data.length === 0) { + throw new EmptyDataError('No data returned from FMP for the given query.') + } + + const multiSymbol = query.symbol.split(',').length > 1 + const sorted = [...data].sort((a, b) => { + if (multiSymbol) { + const dateCompare = String(a.date ?? '').localeCompare(String(b.date ?? '')) + return dateCompare !== 0 ? dateCompare : String(a.symbol ?? '').localeCompare(String(b.symbol ?? '')) + } + return String(a.date ?? '').localeCompare(String(b.date ?? '')) + }) + + return sorted.map((d) => { + const aliased = applyAliases(d, ALIAS_DICT) + // Normalize percent + if (typeof aliased.change_percent === 'number') { + aliased.change_percent = aliased.change_percent / 100 + } + return FMPEquityHistoricalDataSchema.parse(aliased) + }) + } +} diff --git a/packages/opentypebb/src/providers/fmp/models/equity-peers.ts b/packages/opentypebb/src/providers/fmp/models/equity-peers.ts new file mode 100644 index 00000000..789ab9a9 --- /dev/null +++ b/packages/opentypebb/src/providers/fmp/models/equity-peers.ts @@ -0,0 +1,53 @@ +/** + * FMP Equity Peers Model. + * Maps to: openbb_fmp/models/equity_peers.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { EquityPeersQueryParamsSchema, EquityPeersDataSchema } from '../../../standard-models/equity-peers.js' +import { applyAliases } from '../../../core/provider/utils/helpers.js' +import { getDataMany } from '../utils/helpers.js' + +const ALIAS_DICT: Record = { + name: 'companyName', + market_cap: 'mktCap', +} + +const numOrNull = z.number().nullable().default(null) + +export const FMPEquityPeersQueryParamsSchema = EquityPeersQueryParamsSchema +export type FMPEquityPeersQueryParams = z.infer + +export const FMPEquityPeersDataSchema = EquityPeersDataSchema.extend({ + name: z.string().nullable().default(null).describe('Name of the company.'), + price: numOrNull.describe('Current price.'), + market_cap: numOrNull.describe('Market capitalization.'), +}).passthrough() +export type FMPEquityPeersData = z.infer + +export class FMPEquityPeersFetcher extends Fetcher { + static override transformQuery(params: Record): FMPEquityPeersQueryParams { + return FMPEquityPeersQueryParamsSchema.parse(params) + } + + static override async extractData( + query: FMPEquityPeersQueryParams, + credentials: Record | null, + ): Promise[]> { + const apiKey = credentials?.fmp_api_key ?? '' + return getDataMany( + `https://financialmodelingprep.com/stable/stock-peers?symbol=${query.symbol}&apikey=${apiKey}`, + ) + } + + static override transformData( + _query: FMPEquityPeersQueryParams, + data: Record[], + ): FMPEquityPeersData[] { + return data.map(d => { + const aliased = applyAliases(d, ALIAS_DICT) + return FMPEquityPeersDataSchema.parse(aliased) + }) + } +} diff --git a/packages/opentypebb/src/providers/fmp/models/equity-profile.ts b/packages/opentypebb/src/providers/fmp/models/equity-profile.ts new file mode 100644 index 00000000..3052c98b --- /dev/null +++ b/packages/opentypebb/src/providers/fmp/models/equity-profile.ts @@ -0,0 +1,120 @@ +/** + * FMP Equity Profile Model. + * Maps to: openbb_fmp/models/equity_profile.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { EquityInfoQueryParamsSchema, EquityInfoDataSchema } from '../../../standard-models/equity-info.js' +import { applyAliases, replaceEmptyStrings } from '../../../core/provider/utils/helpers.js' +import { EmptyDataError } from '../../../core/provider/utils/errors.js' +import { amakeRequest } from '../../../core/provider/utils/helpers.js' +import { responseCallback } from '../utils/helpers.js' + +// --- Query Params --- + +export const FMPEquityProfileQueryParamsSchema = EquityInfoQueryParamsSchema + +export type FMPEquityProfileQueryParams = z.infer + +// --- Data --- + +const ALIAS_DICT: Record = { + name: 'companyName', + stock_exchange: 'exchange', + company_url: 'website', + hq_address1: 'address', + hq_address_city: 'city', + hq_address_postal_code: 'zip', + hq_state: 'state', + hq_country: 'country', + business_phone_no: 'phone', + industry_category: 'industry', + employees: 'fullTimeEmployees', + long_description: 'description', + first_stock_price_date: 'ipoDate', + last_price: 'price', + volume_avg: 'averageVolume', + annualized_dividend_amount: 'lastDividend', +} + +export const FMPEquityProfileDataSchema = EquityInfoDataSchema.extend({ + is_etf: z.boolean().describe('If the symbol is an ETF.'), + is_actively_trading: z.boolean().describe('If the company is actively trading.'), + is_adr: z.boolean().describe('If the stock is an ADR.'), + is_fund: z.boolean().describe('If the company is a fund.'), + image: z.string().nullable().default(null).describe('Image of the company.'), + currency: z.string().nullable().default(null).describe('Currency in which the stock is traded.'), + market_cap: z.number().nullable().default(null).describe('Market capitalization of the company.'), + last_price: z.number().nullable().default(null).describe('The last traded price.'), + year_high: z.number().nullable().default(null).describe('The one-year high of the price.'), + year_low: z.number().nullable().default(null).describe('The one-year low of the price.'), + volume_avg: z.number().nullable().default(null).describe('Average daily trading volume.'), + annualized_dividend_amount: z.number().nullable().default(null).describe('The annualized dividend payment based on the most recent regular dividend payment.'), + beta: z.number().nullable().default(null).describe('Beta of the stock relative to the market.'), +}).strip() + +export type FMPEquityProfileData = z.infer + +// --- Fetcher --- + +export class FMPEquityProfileFetcher extends Fetcher { + static override transformQuery(params: Record): FMPEquityProfileQueryParams { + return FMPEquityProfileQueryParamsSchema.parse(params) + } + + static override async extractData( + query: FMPEquityProfileQueryParams, + credentials: Record | null, + ): Promise[]> { + const apiKey = credentials?.fmp_api_key ?? '' + const symbols = query.symbol.split(',') + const baseUrl = 'https://financialmodelingprep.com/stable/' + const results: Record[] = [] + + const getOne = async (symbol: string) => { + const url = `${baseUrl}profile?symbol=${symbol}&apikey=${apiKey}` + try { + const result = await amakeRequest[]>(url, { responseCallback }) + if (result && result.length > 0) { + results.push(result[0]) + } else { + console.warn(`Symbol Error: No data found for ${symbol}`) + } + } catch { + console.warn(`Symbol Error: No data found for ${symbol}`) + } + } + + await Promise.all(symbols.map(getOne)) + + if (results.length === 0) { + throw new EmptyDataError('No data found for the given symbols.') + } + + return results.sort((a, b) => { + const ai = symbols.indexOf(String(a.symbol ?? '')) + const bi = symbols.indexOf(String(b.symbol ?? '')) + return ai - bi + }) + } + + static override transformData( + query: FMPEquityProfileQueryParams, + data: Record[], + ): FMPEquityProfileData[] { + return data.map((d) => { + // Extract year_low and year_high from range + const range = d.range as string | undefined + if (range) { + const [low, high] = range.split('-') + d.year_low = parseFloat(low) || null + d.year_high = parseFloat(high) || null + delete d.range + } + const cleaned = replaceEmptyStrings(d) + const aliased = applyAliases(cleaned, ALIAS_DICT) + return FMPEquityProfileDataSchema.parse(aliased) + }) + } +} diff --git a/packages/opentypebb/src/providers/fmp/models/equity-quote.ts b/packages/opentypebb/src/providers/fmp/models/equity-quote.ts new file mode 100644 index 00000000..758519e2 --- /dev/null +++ b/packages/opentypebb/src/providers/fmp/models/equity-quote.ts @@ -0,0 +1,101 @@ +/** + * FMP Equity Quote Model. + * Maps to: openbb_fmp/models/equity_quote.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { EquityQuoteQueryParamsSchema, EquityQuoteDataSchema } from '../../../standard-models/equity-quote.js' +import { applyAliases } from '../../../core/provider/utils/helpers.js' +import { EmptyDataError } from '../../../core/provider/utils/errors.js' +import { amakeRequest } from '../../../core/provider/utils/helpers.js' +import { responseCallback } from '../utils/helpers.js' + +// --- Query Params --- + +export const FMPEquityQuoteQueryParamsSchema = EquityQuoteQueryParamsSchema + +export type FMPEquityQuoteQueryParams = z.infer + +// --- Data --- + +const ALIAS_DICT: Record = { + ma50: 'priceAvg50', + ma200: 'priceAvg200', + last_timestamp: 'timestamp', + high: 'dayHigh', + low: 'dayLow', + last_price: 'price', + change_percent: 'changePercentage', + prev_close: 'previousClose', +} + +export const FMPEquityQuoteDataSchema = EquityQuoteDataSchema.extend({ + ma50: z.number().nullable().default(null).describe('50 day moving average price.'), + ma200: z.number().nullable().default(null).describe('200 day moving average price.'), + market_cap: z.number().nullable().default(null).describe('Market cap of the company.'), +}).passthrough() + +export type FMPEquityQuoteData = z.infer + +// --- Fetcher --- + +export class FMPEquityQuoteFetcher extends Fetcher { + static override transformQuery(params: Record): FMPEquityQuoteQueryParams { + return FMPEquityQuoteQueryParamsSchema.parse(params) + } + + static override async extractData( + query: FMPEquityQuoteQueryParams, + credentials: Record | null, + ): Promise[]> { + const apiKey = credentials?.fmp_api_key ?? '' + const baseUrl = 'https://financialmodelingprep.com/stable/quote?' + const symbols = query.symbol.split(',') + const results: Record[] = [] + + const getOne = async (symbol: string) => { + const url = `${baseUrl}symbol=${symbol}&apikey=${apiKey}` + try { + const result = await amakeRequest[]>(url, { responseCallback }) + if (result && result.length > 0) { + results.push(...result) + } else { + console.warn(`Symbol Error: No data found for ${symbol}`) + } + } catch { + console.warn(`Symbol Error: No data found for ${symbol}`) + } + } + + await Promise.all(symbols.map(getOne)) + + if (results.length === 0) { + throw new EmptyDataError('No data found for the given symbols.') + } + + return results.sort((a, b) => { + const ai = symbols.indexOf(String(a.symbol ?? '')) + const bi = symbols.indexOf(String(b.symbol ?? '')) + return ai - bi + }) + } + + static override transformData( + query: FMPEquityQuoteQueryParams, + data: Record[], + ): FMPEquityQuoteData[] { + return data.map((d) => { + const aliased = applyAliases(d, ALIAS_DICT) + // Normalize timestamp to ISO date string + if (aliased.last_timestamp && typeof aliased.last_timestamp === 'number') { + aliased.last_timestamp = new Date(aliased.last_timestamp * 1000).toISOString().split('T')[0] + } + // Normalize percent + if (typeof aliased.change_percent === 'number') { + aliased.change_percent = aliased.change_percent / 100 + } + return FMPEquityQuoteDataSchema.parse(aliased) + }) + } +} diff --git a/packages/opentypebb/src/providers/fmp/models/equity-screener.ts b/packages/opentypebb/src/providers/fmp/models/equity-screener.ts new file mode 100644 index 00000000..a59cd1fb --- /dev/null +++ b/packages/opentypebb/src/providers/fmp/models/equity-screener.ts @@ -0,0 +1,134 @@ +/** + * FMP Equity Screener Model. + * Maps to: openbb_fmp/models/equity_screener.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { EquityScreenerQueryParamsSchema, EquityScreenerDataSchema } from '../../../standard-models/equity-screener.js' +import { applyAliases } from '../../../core/provider/utils/helpers.js' +import { getDataMany } from '../utils/helpers.js' + +const DATA_ALIAS_DICT: Record = { + name: 'companyName', + market_cap: 'marketCap', + last_annual_dividend: 'lastAnnualDividend', + exchange: 'exchangeShortName', + exchange_name: 'exchange', + is_etf: 'isEtf', + actively_trading: 'isActivelyTrading', +} + +const QUERY_ALIAS_DICT: Record = { + mktcap_min: 'marketCapMoreThan', + mktcap_max: 'marketCapLowerThan', + price_min: 'priceMoreThan', + price_max: 'priceLowerThan', + beta_min: 'betaMoreThan', + beta_max: 'betaLowerThan', + volume_min: 'volumeMoreThan', + volume_max: 'volumeLowerThan', + dividend_min: 'dividendMoreThan', + dividend_max: 'dividendLowerThan', + is_active: 'isActivelyTrading', + is_etf: 'isEtf', + is_fund: 'isFund', + all_share_classes: 'includeAllShareClasses', +} + +const numOrNull = z.number().nullable().default(null) + +export const FMPEquityScreenerQueryParamsSchema = EquityScreenerQueryParamsSchema.extend({ + mktcap_min: z.coerce.number().nullable().default(null).describe('Minimum market capitalization.'), + mktcap_max: z.coerce.number().nullable().default(null).describe('Maximum market capitalization.'), + price_min: z.coerce.number().nullable().default(null).describe('Minimum price.'), + price_max: z.coerce.number().nullable().default(null).describe('Maximum price.'), + beta_min: z.coerce.number().nullable().default(null).describe('Minimum beta.'), + beta_max: z.coerce.number().nullable().default(null).describe('Maximum beta.'), + volume_min: z.coerce.number().nullable().default(null).describe('Minimum volume.'), + volume_max: z.coerce.number().nullable().default(null).describe('Maximum volume.'), + dividend_min: z.coerce.number().nullable().default(null).describe('Minimum dividend yield.'), + dividend_max: z.coerce.number().nullable().default(null).describe('Maximum dividend yield.'), + sector: z.string().nullable().default(null).describe('Sector filter.'), + industry: z.string().nullable().default(null).describe('Industry filter.'), + country: z.string().nullable().default(null).describe('Country filter.'), + exchange: z.string().nullable().default(null).describe('Exchange filter.'), + is_etf: z.boolean().nullable().default(null).describe('Filter for ETFs.'), + is_active: z.boolean().nullable().default(null).describe('Filter for actively trading.'), + is_fund: z.boolean().nullable().default(null).describe('Filter for funds.'), + all_share_classes: z.boolean().nullable().default(null).describe('Include all share classes.'), + limit: z.coerce.number().nullable().default(50000).describe('Maximum number of results.'), +}) +export type FMPEquityScreenerQueryParams = z.infer + +export const FMPEquityScreenerDataSchema = EquityScreenerDataSchema.extend({ + market_cap: numOrNull.describe('Market capitalization.'), + sector: z.string().nullable().default(null).describe('Sector.'), + industry: z.string().nullable().default(null).describe('Industry.'), + beta: numOrNull.describe('Beta.'), + price: numOrNull.describe('Current price.'), + last_annual_dividend: numOrNull.describe('Last annual dividend.'), + volume: numOrNull.describe('Volume.'), + exchange: z.string().nullable().default(null).describe('Exchange.'), + exchange_name: z.string().nullable().default(null).describe('Exchange name.'), + country: z.string().nullable().default(null).describe('Country.'), + is_etf: z.boolean().nullable().default(null).describe('Is ETF.'), + is_fund: z.boolean().nullable().default(null).describe('Is fund.'), + actively_trading: z.boolean().nullable().default(null).describe('Is actively trading.'), +}).passthrough() +export type FMPEquityScreenerData = z.infer + +export class FMPEquityScreenerFetcher extends Fetcher { + static override transformQuery(params: Record): FMPEquityScreenerQueryParams { + return FMPEquityScreenerQueryParamsSchema.parse(params) + } + + static override async extractData( + query: FMPEquityScreenerQueryParams, + credentials: Record | null, + ): Promise[]> { + const apiKey = credentials?.fmp_api_key ?? '' + const qs = new URLSearchParams() + qs.set('apikey', apiKey) + + // Map query params to FMP API parameter names + const mappings: [string, string, unknown][] = [ + ['marketCapMoreThan', 'mktcap_min', query.mktcap_min], + ['marketCapLowerThan', 'mktcap_max', query.mktcap_max], + ['priceMoreThan', 'price_min', query.price_min], + ['priceLowerThan', 'price_max', query.price_max], + ['betaMoreThan', 'beta_min', query.beta_min], + ['betaLowerThan', 'beta_max', query.beta_max], + ['volumeMoreThan', 'volume_min', query.volume_min], + ['volumeLowerThan', 'volume_max', query.volume_max], + ['dividendMoreThan', 'dividend_min', query.dividend_min], + ['dividendLowerThan', 'dividend_max', query.dividend_max], + ] + for (const [apiName, , val] of mappings) { + if (val != null) qs.set(apiName, String(val)) + } + if (query.sector) qs.set('sector', query.sector) + if (query.industry) qs.set('industry', query.industry) + if (query.country) qs.set('country', query.country) + if (query.exchange) qs.set('exchange', query.exchange) + if (query.is_etf != null) qs.set('isEtf', String(query.is_etf)) + if (query.is_active != null) qs.set('isActivelyTrading', String(query.is_active)) + if (query.is_fund != null) qs.set('isFund', String(query.is_fund)) + if (query.all_share_classes != null) qs.set('includeAllShareClasses', String(query.all_share_classes)) + if (query.limit) qs.set('limit', String(query.limit)) + + return getDataMany( + `https://financialmodelingprep.com/stable/company-screener?${qs.toString()}`, + ) + } + + static override transformData( + _query: FMPEquityScreenerQueryParams, + data: Record[], + ): FMPEquityScreenerData[] { + return data.map(d => { + const aliased = applyAliases(d, DATA_ALIAS_DICT) + return FMPEquityScreenerDataSchema.parse(aliased) + }) + } +} diff --git a/packages/opentypebb/src/providers/fmp/models/esg-score.ts b/packages/opentypebb/src/providers/fmp/models/esg-score.ts new file mode 100644 index 00000000..29ec44bd --- /dev/null +++ b/packages/opentypebb/src/providers/fmp/models/esg-score.ts @@ -0,0 +1,52 @@ +/** + * FMP ESG Score Model. + * Maps to: openbb_fmp/models/esg.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { EsgScoreQueryParamsSchema, EsgScoreDataSchema } from '../../../standard-models/esg-score.js' +import { applyAliases } from '../../../core/provider/utils/helpers.js' +import { getDataMany } from '../utils/helpers.js' + +const ALIAS_DICT: Record = { + company_name: 'companyName', + form_type: 'formType', + accepted_date: 'acceptedDate', + environmental_score: 'environmentalScore', + social_score: 'socialScore', + governance_score: 'governanceScore', + esg_score: 'ESGScore', +} + +export const FMPEsgScoreQueryParamsSchema = EsgScoreQueryParamsSchema +export type FMPEsgScoreQueryParams = z.infer + +export const FMPEsgScoreDataSchema = EsgScoreDataSchema +export type FMPEsgScoreData = z.infer + +export class FMPEsgScoreFetcher extends Fetcher { + static override transformQuery(params: Record): FMPEsgScoreQueryParams { + return FMPEsgScoreQueryParamsSchema.parse(params) + } + + static override async extractData( + query: FMPEsgScoreQueryParams, + credentials: Record | null, + ): Promise[]> { + const apiKey = credentials?.fmp_api_key ?? '' + return getDataMany( + `https://financialmodelingprep.com/stable/esg-disclosures?symbol=${query.symbol}&apikey=${apiKey}`, + ) + } + + static override transformData( + _query: FMPEsgScoreQueryParams, + data: Record[], + ): FMPEsgScoreData[] { + return data.map(d => { + const aliased = applyAliases(d, ALIAS_DICT) + return FMPEsgScoreDataSchema.parse(aliased) + }) + } +} diff --git a/packages/opentypebb/src/providers/fmp/models/etf-countries.ts b/packages/opentypebb/src/providers/fmp/models/etf-countries.ts new file mode 100644 index 00000000..2aa12d23 --- /dev/null +++ b/packages/opentypebb/src/providers/fmp/models/etf-countries.ts @@ -0,0 +1,63 @@ +/** + * FMP ETF Countries Model. + * Maps to: openbb_fmp/models/etf_countries.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { EtfCountriesQueryParamsSchema, EtfCountriesDataSchema } from '../../../standard-models/etf-countries.js' +import { getDataMany } from '../utils/helpers.js' + +export const FMPEtfCountriesQueryParamsSchema = EtfCountriesQueryParamsSchema +export type FMPEtfCountriesQueryParams = z.infer + +export const FMPEtfCountriesDataSchema = EtfCountriesDataSchema.passthrough() +export type FMPEtfCountriesData = z.infer + +export class FMPEtfCountriesFetcher extends Fetcher { + static override transformQuery(params: Record): FMPEtfCountriesQueryParams { + return FMPEtfCountriesQueryParamsSchema.parse(params) + } + + static override async extractData( + query: FMPEtfCountriesQueryParams, + credentials: Record | null, + ): Promise[]> { + const apiKey = credentials?.fmp_api_key ?? '' + const symbol = query.symbol + return getDataMany( + `https://financialmodelingprep.com/stable/etf/country-weightings?symbol=${symbol}&apikey=${apiKey}`, + ) + } + + static override transformData( + _query: FMPEtfCountriesQueryParams, + data: Record[], + ): FMPEtfCountriesData[] { + // FMP returns weightPercentage as a string like "50.25%" + // Python source parses this and normalizes (multiply by 0.01) + const results: FMPEtfCountriesData[] = [] + for (const d of data) { + const raw = d.weightPercentage + let weight = 0 + if (typeof raw === 'string') { + weight = parseFloat(raw.replace('%', '')) + if (isNaN(weight)) weight = 0 + } else if (typeof raw === 'number') { + weight = raw + } + // Filter out zero weights + if (weight === 0) continue + // Normalize: percentage points → decimal-like normalized (match Python: * 0.01) + weight = weight / 100 + results.push( + EtfCountriesDataSchema.parse({ + ...d, + weight, + country: d.country ?? '', + }), + ) + } + return results + } +} diff --git a/packages/opentypebb/src/providers/fmp/models/etf-equity-exposure.ts b/packages/opentypebb/src/providers/fmp/models/etf-equity-exposure.ts new file mode 100644 index 00000000..0b49bfa8 --- /dev/null +++ b/packages/opentypebb/src/providers/fmp/models/etf-equity-exposure.ts @@ -0,0 +1,59 @@ +/** + * FMP ETF Equity Exposure Model. + * Maps to: openbb_fmp/models/etf_equity_exposure.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { EtfEquityExposureQueryParamsSchema, EtfEquityExposureDataSchema } from '../../../standard-models/etf-equity-exposure.js' +import { applyAliases } from '../../../core/provider/utils/helpers.js' +import { getDataMany } from '../utils/helpers.js' + +const ALIAS_DICT: Record = { + equity_symbol: 'assetExposure', + etf_symbol: 'etfSymbol', + shares: 'sharesNumber', + weight: 'weightPercentage', + market_value: 'marketValue', +} + +const numOrNull = z.number().nullable().default(null) + +export const FMPEtfEquityExposureQueryParamsSchema = EtfEquityExposureQueryParamsSchema +export type FMPEtfEquityExposureQueryParams = z.infer + +export const FMPEtfEquityExposureDataSchema = EtfEquityExposureDataSchema.passthrough() +export type FMPEtfEquityExposureData = z.infer + +export class FMPEtfEquityExposureFetcher extends Fetcher { + static override transformQuery(params: Record): FMPEtfEquityExposureQueryParams { + return FMPEtfEquityExposureQueryParamsSchema.parse(params) + } + + static override async extractData( + query: FMPEtfEquityExposureQueryParams, + credentials: Record | null, + ): Promise[]> { + const apiKey = credentials?.fmp_api_key ?? '' + const symbol = query.symbol + return getDataMany( + `https://financialmodelingprep.com/stable/etf/asset-exposure?symbol=${symbol}&apikey=${apiKey}`, + ) + } + + static override transformData( + _query: FMPEtfEquityExposureQueryParams, + data: Record[], + ): FMPEtfEquityExposureData[] { + const results = data.map((d) => { + const aliased = applyAliases(d, ALIAS_DICT) + // Normalize weight from percent to decimal + if (typeof aliased.weight === 'number') { + aliased.weight = aliased.weight / 100 + } + return FMPEtfEquityExposureDataSchema.parse(aliased) + }) + // Sort by market_value descending (matching Python) + return results.sort((a, b) => (Number(b.market_value ?? 0)) - (Number(a.market_value ?? 0))) + } +} diff --git a/packages/opentypebb/src/providers/fmp/models/etf-holdings.ts b/packages/opentypebb/src/providers/fmp/models/etf-holdings.ts new file mode 100644 index 00000000..94776e51 --- /dev/null +++ b/packages/opentypebb/src/providers/fmp/models/etf-holdings.ts @@ -0,0 +1,72 @@ +/** + * FMP ETF Holdings Model. + * Maps to: openbb_fmp/models/etf_holdings.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { EtfHoldingsQueryParamsSchema, EtfHoldingsDataSchema } from '../../../standard-models/etf-holdings.js' +import { applyAliases } from '../../../core/provider/utils/helpers.js' +import { getDataMany } from '../utils/helpers.js' + +const ALIAS_DICT: Record = { + shares: 'sharesNumber', + value: 'marketValue', + weight: 'weightPercentage', + symbol: 'asset', + updated: 'updatedAt', + cusip: 'securityCusip', +} + +const numOrNull = z.number().nullable().default(null) + +export const FMPEtfHoldingsQueryParamsSchema = EtfHoldingsQueryParamsSchema.extend({ + date: z.string().nullable().default(null).describe('A specific date to get data for. Entering a date will attempt to return the NPORT filing for the entered date. This needs to be exact date of the filing. Defaults to the latest filing.'), + cik: z.string().nullable().default(null).describe('The CIK number of the filing entity.'), +}) +export type FMPEtfHoldingsQueryParams = z.infer + +export const FMPEtfHoldingsDataSchema = EtfHoldingsDataSchema.extend({ + shares: numOrNull.describe('The number of shares held.'), + value: numOrNull.describe('The market value of the holding.'), + weight: numOrNull.describe('The weight of the holding in the ETF as a normalized percentage.'), + updated: z.string().nullable().default(null).describe('The last updated date.'), + cusip: z.string().nullable().default(null).describe('The CUSIP of the holding.'), + isin: z.string().nullable().default(null).describe('The ISIN of the holding.'), + country: z.string().nullable().default(null).describe('The country of the holding.'), + exchange: z.string().nullable().default(null).describe('The exchange of the holding.'), + asset_type: z.string().nullable().default(null).describe('The asset type of the holding.'), +}).passthrough() +export type FMPEtfHoldingsData = z.infer + +export class FMPEtfHoldingsFetcher extends Fetcher { + static override transformQuery(params: Record): FMPEtfHoldingsQueryParams { + return FMPEtfHoldingsQueryParamsSchema.parse(params) + } + + static override async extractData( + query: FMPEtfHoldingsQueryParams, + credentials: Record | null, + ): Promise[]> { + const apiKey = credentials?.fmp_api_key ?? '' + const symbol = query.symbol + let url = `https://financialmodelingprep.com/stable/etf/holdings?symbol=${symbol}&apikey=${apiKey}` + if (query.date) url += `&date=${query.date}` + if (query.cik) url += `&cik=${query.cik}` + return getDataMany(url) + } + + static override transformData( + _query: FMPEtfHoldingsQueryParams, + data: Record[], + ): FMPEtfHoldingsData[] { + return data.map((d) => { + const aliased = applyAliases(d, ALIAS_DICT) + // Normalize weight from percent to decimal (5.0 → 0.05) + if (typeof aliased.weight === 'number') { + aliased.weight = aliased.weight / 100 + } + return FMPEtfHoldingsDataSchema.parse(aliased) + }) + } +} diff --git a/packages/opentypebb/src/providers/fmp/models/etf-info.ts b/packages/opentypebb/src/providers/fmp/models/etf-info.ts new file mode 100644 index 00000000..315352e6 --- /dev/null +++ b/packages/opentypebb/src/providers/fmp/models/etf-info.ts @@ -0,0 +1,72 @@ +/** + * FMP ETF Info Model. + * Maps to: openbb_fmp/models/etf_info.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { EtfInfoQueryParamsSchema, EtfInfoDataSchema } from '../../../standard-models/etf-info.js' +import { applyAliases } from '../../../core/provider/utils/helpers.js' +import { getDataMany } from '../utils/helpers.js' + +const ALIAS_DICT: Record = { + issuer: 'etfCompany', + cusip: 'securityCusip', + isin: 'securityIsin', + aum: 'assetsUnderManagement', + nav: 'netAssetValue', + currency: 'navCurrency', + volume_avg: 'avgVolume', + updated: 'updatedAt', +} + +const numOrNull = z.number().nullable().default(null) + +export const FMPEtfInfoQueryParamsSchema = EtfInfoQueryParamsSchema +export type FMPEtfInfoQueryParams = z.infer + +export const FMPEtfInfoDataSchema = EtfInfoDataSchema.extend({ + cusip: z.string().nullable().default(null).describe('The CUSIP of the ETF.'), + isin: z.string().nullable().default(null).describe('The ISIN of the ETF.'), + aum: numOrNull.describe('The assets under management of the ETF.'), + nav: numOrNull.describe('The net asset value of the ETF.'), + currency: z.string().nullable().default(null).describe('The currency of the ETF.'), + expense_ratio: numOrNull.describe('The expense ratio of the ETF as a normalized percentage.'), + holdings_count: numOrNull.describe('The number of holdings in the ETF.'), + volume_avg: numOrNull.describe('The average volume of the ETF.'), + updated: z.string().nullable().default(null).describe('The last updated date of the ETF.'), + asset_class: z.string().nullable().default(null).describe('The asset class of the ETF.'), + sector_list: z.string().nullable().default(null).describe('Sector list of the ETF.'), +}).passthrough() +export type FMPEtfInfoData = z.infer + +export class FMPEtfInfoFetcher extends Fetcher { + static override transformQuery(params: Record): FMPEtfInfoQueryParams { + return FMPEtfInfoQueryParamsSchema.parse(params) + } + + static override async extractData( + query: FMPEtfInfoQueryParams, + credentials: Record | null, + ): Promise[]> { + const apiKey = credentials?.fmp_api_key ?? '' + const symbol = query.symbol + return getDataMany( + `https://financialmodelingprep.com/stable/etf/info?symbol=${symbol}&apikey=${apiKey}`, + ) + } + + static override transformData( + _query: FMPEtfInfoQueryParams, + data: Record[], + ): FMPEtfInfoData[] { + return data.map((d) => { + const aliased = applyAliases(d, ALIAS_DICT) + // Normalize expense_ratio from percent to decimal (5.0 → 0.05) + if (typeof aliased.expense_ratio === 'number') { + aliased.expense_ratio = aliased.expense_ratio / 100 + } + return FMPEtfInfoDataSchema.parse(aliased) + }) + } +} diff --git a/packages/opentypebb/src/providers/fmp/models/etf-search.ts b/packages/opentypebb/src/providers/fmp/models/etf-search.ts new file mode 100644 index 00000000..92f197e0 --- /dev/null +++ b/packages/opentypebb/src/providers/fmp/models/etf-search.ts @@ -0,0 +1,70 @@ +/** + * FMP ETF Search Model. + * Maps to: openbb_fmp/models/etf_search.py + * + * Uses the company-screener endpoint filtered to ETFs only, + * matching the Python implementation. + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { EtfSearchQueryParamsSchema, EtfSearchDataSchema } from '../../../standard-models/etf-search.js' +import { applyAliases } from '../../../core/provider/utils/helpers.js' +import { getDataMany } from '../utils/helpers.js' + +const ALIAS_DICT: Record = { + name: 'companyName', + market_cap: 'marketCap', + last_annual_dividend: 'lastAnnualDividend', + exchange: 'exchangeShortName', + exchange_name: 'exchange', +} + +const numOrNull = z.number().nullable().default(null) + +export const FMPEtfSearchQueryParamsSchema = EtfSearchQueryParamsSchema.extend({ + exchange: z.string().nullable().default(null).describe('The exchange code the ETF is listed on.'), + is_active: z.boolean().default(true).describe('Whether the ETF is actively trading.'), +}) +export type FMPEtfSearchQueryParams = z.infer + +export const FMPEtfSearchDataSchema = EtfSearchDataSchema.extend({ + market_cap: numOrNull.describe('The market cap of the ETF.'), + sector: z.string().nullable().default(null).describe('The sector of the ETF.'), + industry: z.string().nullable().default(null).describe('The industry of the ETF.'), + beta: numOrNull.describe('The beta of the ETF.'), + price: numOrNull.describe('The current price of the ETF.'), + last_annual_dividend: numOrNull.describe('The last annual dividend of the ETF.'), + volume: numOrNull.describe('The current volume of the ETF.'), + exchange: z.string().nullable().default(null).describe('The exchange the ETF is listed on.'), + exchange_name: z.string().nullable().default(null).describe('The name of the exchange.'), + country: z.string().nullable().default(null).describe('The country of the ETF.'), +}).passthrough() +export type FMPEtfSearchData = z.infer + +export class FMPEtfSearchFetcher extends Fetcher { + static override transformQuery(params: Record): FMPEtfSearchQueryParams { + return FMPEtfSearchQueryParamsSchema.parse(params) + } + + static override async extractData( + query: FMPEtfSearchQueryParams, + credentials: Record | null, + ): Promise[]> { + const apiKey = credentials?.fmp_api_key ?? '' + let url = `https://financialmodelingprep.com/stable/company-screener?isEtf=true&isFund=false&isActivelyTrading=${query.is_active}&apikey=${apiKey}` + if (query.query) url += `&query=${encodeURIComponent(query.query)}` + if (query.exchange) url += `&exchange=${encodeURIComponent(query.exchange)}` + return getDataMany(url) + } + + static override transformData( + _query: FMPEtfSearchQueryParams, + data: Record[], + ): FMPEtfSearchData[] { + return data.map((d) => { + const aliased = applyAliases(d, ALIAS_DICT) + return FMPEtfSearchDataSchema.parse(aliased) + }) + } +} diff --git a/packages/opentypebb/src/providers/fmp/models/etf-sectors.ts b/packages/opentypebb/src/providers/fmp/models/etf-sectors.ts new file mode 100644 index 00000000..91a24784 --- /dev/null +++ b/packages/opentypebb/src/providers/fmp/models/etf-sectors.ts @@ -0,0 +1,47 @@ +/** + * FMP ETF Sectors Model. + * Maps to: openbb_fmp/models/etf_sectors.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { EtfSectorsQueryParamsSchema, EtfSectorsDataSchema } from '../../../standard-models/etf-sectors.js' +import { applyAliases } from '../../../core/provider/utils/helpers.js' +import { getDataMany } from '../utils/helpers.js' + +const ALIAS_DICT: Record = { weight: 'weightPercentage' } + +export const FMPEtfSectorsQueryParamsSchema = EtfSectorsQueryParamsSchema +export type FMPEtfSectorsQueryParams = z.infer + +export const FMPEtfSectorsDataSchema = EtfSectorsDataSchema.passthrough() +export type FMPEtfSectorsData = z.infer + +export class FMPEtfSectorsFetcher extends Fetcher { + static override transformQuery(params: Record): FMPEtfSectorsQueryParams { + return FMPEtfSectorsQueryParamsSchema.parse(params) + } + + static override async extractData( + query: FMPEtfSectorsQueryParams, + credentials: Record | null, + ): Promise[]> { + const apiKey = credentials?.fmp_api_key ?? '' + const symbol = query.symbol + return getDataMany( + `https://financialmodelingprep.com/stable/etf/sector-weightings?symbol=${symbol}&apikey=${apiKey}`, + ) + } + + static override transformData( + _query: FMPEtfSectorsQueryParams, + data: Record[], + ): FMPEtfSectorsData[] { + const results = data.map((d) => { + const aliased = applyAliases(d, ALIAS_DICT) + return FMPEtfSectorsDataSchema.parse(aliased) + }) + // Sort by weight descending + return results.sort((a, b) => (b.weight ?? 0) - (a.weight ?? 0)) + } +} diff --git a/packages/opentypebb/src/providers/fmp/models/executive-compensation.ts b/packages/opentypebb/src/providers/fmp/models/executive-compensation.ts new file mode 100644 index 00000000..90ebdf82 --- /dev/null +++ b/packages/opentypebb/src/providers/fmp/models/executive-compensation.ts @@ -0,0 +1,103 @@ +/** + * FMP Executive Compensation Model. + * Maps to: openbb_fmp/models/executive_compensation.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { ExecutiveCompensationQueryParamsSchema, ExecutiveCompensationDataSchema } from '../../../standard-models/executive-compensation.js' +import { applyAliases } from '../../../core/provider/utils/helpers.js' +import { EmptyDataError } from '../../../core/provider/utils/errors.js' +import { getDataMany } from '../utils/helpers.js' + +const ALIAS_DICT: Record = { + company_name: 'companyName', + industry: 'industryTitle', + url: 'link', + executive: 'nameAndPosition', + report_date: 'filingDate', +} + +export const FMPExecutiveCompensationQueryParamsSchema = ExecutiveCompensationQueryParamsSchema.extend({ + year: z.coerce.number().default(-1).describe('Filters results by year, enter 0 for all data available. Default is the most recent year in the dataset, -1.'), +}) +export type FMPExecutiveCompensationQueryParams = z.infer + +export const FMPExecutiveCompensationDataSchema = ExecutiveCompensationDataSchema.extend({ + accepted_date: z.string().nullable().default(null).describe('Date the filing was accepted.'), + url: z.string().nullable().default(null).describe('URL to the filing data.'), +}).passthrough() +export type FMPExecutiveCompensationData = z.infer + +export class FMPExecutiveCompensationFetcher extends Fetcher { + static override transformQuery(params: Record): FMPExecutiveCompensationQueryParams { + return FMPExecutiveCompensationQueryParamsSchema.parse(params) + } + + static override async extractData( + query: FMPExecutiveCompensationQueryParams, + credentials: Record | null, + ): Promise[]> { + const apiKey = credentials?.fmp_api_key ?? '' + const symbols = query.symbol.split(',').map(s => s.trim()).filter(Boolean) + const results: Record[] = [] + + const settled = await Promise.allSettled( + symbols.map(symbol => + getDataMany( + `https://financialmodelingprep.com/stable/governance-executive-compensation?symbol=${symbol}&apikey=${apiKey}`, + ), + ), + ) + + for (const r of settled) { + if (r.status === 'fulfilled' && r.value?.length) { + results.push(...r.value) + } + } + + if (!results.length) { + throw new EmptyDataError('No executive compensation data found.') + } + + return results + } + + static override transformData( + query: FMPExecutiveCompensationQueryParams, + data: Record[], + ): FMPExecutiveCompensationData[] { + const symbols = query.symbol.split(',').map(s => s.trim()).filter(Boolean) + const filtered: FMPExecutiveCompensationData[] = [] + + for (const symbol of symbols) { + const symbolData = data.filter(d => String(d.symbol).toUpperCase() === symbol.toUpperCase()) + + if (symbolData.length && query.year !== 0) { + // Get max year or filter by specific year + const targetYear = query.year === -1 + ? Math.max(...symbolData.map(d => Number(d.year ?? 0))) + : query.year + + const yearData = symbolData.filter(d => Number(d.year ?? 0) === targetYear) + for (const d of yearData) { + const aliased = applyAliases(d, ALIAS_DICT) + filtered.push(FMPExecutiveCompensationDataSchema.parse(aliased)) + } + } else { + // Return all data sorted by year descending + const sorted = [...symbolData].sort((a, b) => Number(b.year ?? 0) - Number(a.year ?? 0)) + for (const d of sorted) { + const aliased = applyAliases(d, ALIAS_DICT) + filtered.push(FMPExecutiveCompensationDataSchema.parse(aliased)) + } + } + } + + if (!filtered.length) { + throw new EmptyDataError('No data found for given symbols and year.') + } + + return filtered + } +} diff --git a/packages/opentypebb/src/providers/fmp/models/financial-ratios.ts b/packages/opentypebb/src/providers/fmp/models/financial-ratios.ts new file mode 100644 index 00000000..b4b39089 --- /dev/null +++ b/packages/opentypebb/src/providers/fmp/models/financial-ratios.ts @@ -0,0 +1,159 @@ +/** + * FMP Financial Ratios Model. + * Maps to: openbb_fmp/models/financial_ratios.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { FinancialRatiosQueryParamsSchema, FinancialRatiosDataSchema } from '../../../standard-models/financial-ratios.js' +import { applyAliases } from '../../../core/provider/utils/helpers.js' +import { EmptyDataError } from '../../../core/provider/utils/errors.js' +import { getDataMany } from '../utils/helpers.js' + +// --- Query Params --- + +export const FMPFinancialRatiosQueryParamsSchema = FinancialRatiosQueryParamsSchema.extend({ + ttm: z.enum(['include', 'exclude', 'only']).default('only').describe("Specify whether to include, exclude, or only show TTM data. Default: 'only'."), + period: z.enum(['q1', 'q2', 'q3', 'q4', 'fy', 'annual', 'quarter']).default('annual').describe('Specify the fiscal period for the data.'), + limit: z.number().int().nullable().default(null).describe('Number of most recent reporting periods to return.'), +}) + +export type FMPFinancialRatiosQueryParams = z.infer + +// --- Data --- + +const ALIAS_DICT: Record = { + currency: 'reportedCurrency', + period_ending: 'date', + fiscal_period: 'period', + price_to_earnings: 'priceToEarningsRatio', + price_to_book: 'priceToBookRatio', + price_to_sales: 'priceToSalesRatio', + debt_to_equity: 'debtToEquityRatio', + debt_to_assets: 'debtToAssetsRatio', + current_ratio: 'currentRatio', + gross_profit_margin: 'grossProfitMargin', + net_profit_margin: 'netProfitMargin', + operating_profit_margin: 'operatingProfitMargin', + dividend_yield: 'dividendYield', + return_on_equity: 'returnOnEquity', + return_on_assets: 'returnOnAssets', +} + +// TTM alias variants +const TTM_ALIAS_DICT: Record = { + currency: 'reportedCurrency', + period_ending: 'date', + fiscal_period: 'fiscal_period', + price_to_earnings: 'priceToEarningsRatioTTM', + price_to_book: 'priceToBookRatioTTM', + price_to_sales: 'priceToSalesRatioTTM', + debt_to_equity: 'debtToEquityRatioTTM', + debt_to_assets: 'debtToAssetsRatioTTM', + current_ratio: 'currentRatioTTM', + gross_profit_margin: 'grossProfitMarginTTM', + net_profit_margin: 'netProfitMarginTTM', + operating_profit_margin: 'operatingProfitMarginTTM', + dividend_yield: 'dividendYieldTTM', + return_on_equity: 'returnOnEquityTTM', + return_on_assets: 'returnOnAssetsTTM', +} + +export const FMPFinancialRatiosDataSchema = FinancialRatiosDataSchema.extend({ + currency: z.string().nullable().default(null).describe('Currency in which the company reports financials.'), + gross_profit_margin: z.number().nullable().default(null).describe('Gross profit margin.'), + net_profit_margin: z.number().nullable().default(null).describe('Net profit margin.'), + operating_profit_margin: z.number().nullable().default(null).describe('Operating profit margin.'), + current_ratio: z.number().nullable().default(null).describe('Current ratio.'), + debt_to_equity: z.number().nullable().default(null).describe('Debt to equity ratio.'), + debt_to_assets: z.number().nullable().default(null).describe('Debt to assets ratio.'), + price_to_earnings: z.number().nullable().default(null).describe('Price to earnings ratio.'), + price_to_book: z.number().nullable().default(null).describe('Price to book ratio.'), + price_to_sales: z.number().nullable().default(null).describe('Price to sales ratio.'), + dividend_yield: z.number().nullable().default(null).describe('Dividend yield.'), + return_on_equity: z.number().nullable().default(null).describe('Return on equity.'), + return_on_assets: z.number().nullable().default(null).describe('Return on assets.'), +}).passthrough() + +export type FMPFinancialRatiosData = z.infer + +// --- Fetcher --- + +export class FMPFinancialRatiosFetcher extends Fetcher { + static override transformQuery(params: Record): FMPFinancialRatiosQueryParams { + return FMPFinancialRatiosQueryParamsSchema.parse(params) + } + + static override async extractData( + query: FMPFinancialRatiosQueryParams, + credentials: Record | null, + ): Promise[]> { + const apiKey = credentials?.fmp_api_key ?? '' + const symbols = query.symbol.split(',') + const results: Record[] = [] + const baseUrl = 'https://financialmodelingprep.com/stable/ratios' + + const getOne = async (symbol: string) => { + try { + const ttmUrl = `${baseUrl}-ttm?symbol=${symbol}&apikey=${apiKey}` + const limit = query.ttm !== 'only' ? (query.limit ?? 5) : 1 + const metricsUrl = `${baseUrl}?symbol=${symbol}&period=${query.period}&limit=${limit}&apikey=${apiKey}` + + const [ttmData, metricsData] = await Promise.all([ + getDataMany(ttmUrl).catch(() => []), + getDataMany(metricsUrl).catch(() => []), + ]) + + const result: Record[] = [] + let currency: string | null = null + + if (metricsData.length > 0) { + if (query.ttm !== 'only') { + result.push(...metricsData) + } + currency = metricsData[0].reportedCurrency as string ?? null + } + + if (ttmData.length > 0 && query.ttm !== 'exclude') { + const ttmResult = { ...ttmData[0] } + ttmResult.date = new Date().toISOString().split('T')[0] + ttmResult.fiscal_period = 'TTM' + ttmResult.fiscal_year = new Date().getFullYear() + if (currency) ttmResult.reportedCurrency = currency + result.unshift(ttmResult) + } + + if (result.length > 0) { + results.push(...result) + } else { + console.warn(`Symbol Error: No data found for ${symbol}.`) + } + } catch { + console.warn(`Symbol Error: No data found for ${symbol}.`) + } + } + + await Promise.all(symbols.map(getOne)) + + if (results.length === 0) { + throw new EmptyDataError('No data found for given symbols.') + } + + return results + } + + static override transformData( + query: FMPFinancialRatiosQueryParams, + data: Record[], + ): FMPFinancialRatiosData[] { + const sorted = [...data].sort((a, b) => + String(b.date ?? '').localeCompare(String(a.date ?? '')), + ) + + return sorted.map((d) => { + const isTTM = d.fiscal_period === 'TTM' + const aliased = applyAliases(d, isTTM ? TTM_ALIAS_DICT : ALIAS_DICT) + return FMPFinancialRatiosDataSchema.parse(aliased) + }) + } +} diff --git a/packages/opentypebb/src/providers/fmp/models/forward-ebitda-estimates.ts b/packages/opentypebb/src/providers/fmp/models/forward-ebitda-estimates.ts new file mode 100644 index 00000000..a73157da --- /dev/null +++ b/packages/opentypebb/src/providers/fmp/models/forward-ebitda-estimates.ts @@ -0,0 +1,71 @@ +/** + * FMP Forward EBITDA Estimates Model. + * Maps to: openbb_fmp/models/forward_ebitda_estimates.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { ForwardEbitdaEstimatesQueryParamsSchema, ForwardEbitdaEstimatesDataSchema } from '../../../standard-models/forward-ebitda-estimates.js' +import { applyAliases } from '../../../core/provider/utils/helpers.js' +import { getDataMany } from '../utils/helpers.js' + +// --- Query Params --- + +export const FMPForwardEbitdaEstimatesQueryParamsSchema = ForwardEbitdaEstimatesQueryParamsSchema.extend({ + fiscal_period: z.enum(['annual', 'quarter']).nullable().default(null).describe('The fiscal period of the estimate.'), + limit: z.coerce.number().int().nullable().default(null).describe('The number of data entries to return.'), + include_historical: z.boolean().default(false).describe('If true, include historical data.'), +}) + +export type FMPForwardEbitdaEstimatesQueryParams = z.infer + +// --- Data --- + +const ALIAS_DICT: Record = { + period_ending: 'date', + high_estimate: 'ebitdaHigh', + low_estimate: 'ebitdaLow', + mean: 'ebitdaAvg', +} + +export const FMPForwardEbitdaEstimatesDataSchema = ForwardEbitdaEstimatesDataSchema.extend({}).passthrough() +export type FMPForwardEbitdaEstimatesData = z.infer + +// --- Fetcher --- + +export class FMPForwardEbitdaEstimatesFetcher extends Fetcher { + static override transformQuery(params: Record): FMPForwardEbitdaEstimatesQueryParams { + return FMPForwardEbitdaEstimatesQueryParamsSchema.parse(params) + } + + static override async extractData( + query: FMPForwardEbitdaEstimatesQueryParams, + credentials: Record | null, + ): Promise[]> { + const apiKey = credentials?.fmp_api_key ?? '' + const url = 'https://financialmodelingprep.com/stable/analyst-estimates' + + `?symbol=${query.symbol}` + + (query.fiscal_period ? `&period=${query.fiscal_period}` : '') + + (query.limit ? `&limit=${query.limit}` : '') + + `&apikey=${apiKey}` + return getDataMany(url) + } + + static override transformData( + query: FMPForwardEbitdaEstimatesQueryParams, + data: Record[], + ): FMPForwardEbitdaEstimatesData[] { + let filtered = data + if (!query.include_historical) { + const currentYear = new Date().getFullYear() + filtered = data.filter(d => { + const fy = Number(d.calendarYear ?? d.fiscal_year ?? 0) + return fy >= currentYear + }) + } + return filtered.map(d => { + const aliased = applyAliases(d, ALIAS_DICT) + return FMPForwardEbitdaEstimatesDataSchema.parse(aliased) + }) + } +} diff --git a/packages/opentypebb/src/providers/fmp/models/forward-eps-estimates.ts b/packages/opentypebb/src/providers/fmp/models/forward-eps-estimates.ts new file mode 100644 index 00000000..b3577fe4 --- /dev/null +++ b/packages/opentypebb/src/providers/fmp/models/forward-eps-estimates.ts @@ -0,0 +1,72 @@ +/** + * FMP Forward EPS Estimates Model. + * Maps to: openbb_fmp/models/forward_eps_estimates.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { ForwardEpsEstimatesQueryParamsSchema, ForwardEpsEstimatesDataSchema } from '../../../standard-models/forward-eps-estimates.js' +import { applyAliases } from '../../../core/provider/utils/helpers.js' +import { getDataMany } from '../utils/helpers.js' + +// --- Query Params --- + +export const FMPForwardEpsEstimatesQueryParamsSchema = ForwardEpsEstimatesQueryParamsSchema.extend({ + fiscal_period: z.enum(['annual', 'quarter']).nullable().default(null).describe('The fiscal period of the estimate.'), + limit: z.coerce.number().int().nullable().default(null).describe('The number of data entries to return.'), + include_historical: z.boolean().default(false).describe('If true, include historical data.'), +}) + +export type FMPForwardEpsEstimatesQueryParams = z.infer + +// --- Data --- + +const ALIAS_DICT: Record = { + number_of_analysts: 'numberAnalystsEps', + high_estimate: 'epsHigh', + low_estimate: 'epsLow', + mean: 'epsAvg', +} + +export const FMPForwardEpsEstimatesDataSchema = ForwardEpsEstimatesDataSchema.extend({}).passthrough() +export type FMPForwardEpsEstimatesData = z.infer + +// --- Fetcher --- + +export class FMPForwardEpsEstimatesFetcher extends Fetcher { + static override transformQuery(params: Record): FMPForwardEpsEstimatesQueryParams { + return FMPForwardEpsEstimatesQueryParamsSchema.parse(params) + } + + static override async extractData( + query: FMPForwardEpsEstimatesQueryParams, + credentials: Record | null, + ): Promise[]> { + const apiKey = credentials?.fmp_api_key ?? '' + const url = 'https://financialmodelingprep.com/stable/analyst-estimates' + + `?symbol=${query.symbol}` + + (query.fiscal_period ? `&period=${query.fiscal_period}` : '') + + (query.limit ? `&limit=${query.limit}` : '') + + `&apikey=${apiKey}` + return getDataMany(url) + } + + static override transformData( + query: FMPForwardEpsEstimatesQueryParams, + data: Record[], + ): FMPForwardEpsEstimatesData[] { + // Filter to current/future fiscal years unless include_historical + let filtered = data + if (!query.include_historical) { + const currentYear = new Date().getFullYear() + filtered = data.filter(d => { + const fy = Number(d.calendarYear ?? d.fiscal_year ?? 0) + return fy >= currentYear + }) + } + return filtered.map(d => { + const aliased = applyAliases(d, ALIAS_DICT) + return FMPForwardEpsEstimatesDataSchema.parse(aliased) + }) + } +} diff --git a/packages/opentypebb/src/providers/fmp/models/gainers.ts b/packages/opentypebb/src/providers/fmp/models/gainers.ts new file mode 100644 index 00000000..51d9ab8e --- /dev/null +++ b/packages/opentypebb/src/providers/fmp/models/gainers.ts @@ -0,0 +1,49 @@ +/** + * FMP Top Gainers Model. + * Maps to: openbb_fmp/models/equity_gainers.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { EquityPerformanceQueryParamsSchema, EquityPerformanceDataSchema } from '../../../standard-models/equity-performance.js' +import { applyAliases } from '../../../core/provider/utils/helpers.js' +import { getDataMany } from '../utils/helpers.js' + +const ALIAS_DICT: Record = { percent_change: 'changesPercentage' } + +export const FMPGainersQueryParamsSchema = EquityPerformanceQueryParamsSchema +export type FMPGainersQueryParams = z.infer + +export const FMPGainersDataSchema = EquityPerformanceDataSchema.extend({ + exchange: z.string().describe('Stock exchange where the security is listed.'), +}).passthrough() +export type FMPGainersData = z.infer + +export class FMPGainersFetcher extends Fetcher { + static override transformQuery(params: Record): FMPGainersQueryParams { + return FMPGainersQueryParamsSchema.parse(params) + } + + static override async extractData( + query: FMPGainersQueryParams, + credentials: Record | null, + ): Promise[]> { + const apiKey = credentials?.fmp_api_key ?? '' + return getDataMany(`https://financialmodelingprep.com/stable/biggest-gainers?apikey=${apiKey}`) + } + + static override transformData( + query: FMPGainersQueryParams, + data: Record[], + ): FMPGainersData[] { + const sorted = [...data].sort((a, b) => { + const diff = Number(b.changesPercentage ?? 0) - Number(a.changesPercentage ?? 0) + return query.sort === 'desc' ? diff : -diff + }) + return sorted.map((d) => { + const aliased = applyAliases(d, ALIAS_DICT) + if (typeof aliased.percent_change === 'number') aliased.percent_change = aliased.percent_change / 100 + return FMPGainersDataSchema.parse(aliased) + }) + } +} diff --git a/packages/opentypebb/src/providers/fmp/models/government-trades.ts b/packages/opentypebb/src/providers/fmp/models/government-trades.ts new file mode 100644 index 00000000..106ec20e --- /dev/null +++ b/packages/opentypebb/src/providers/fmp/models/government-trades.ts @@ -0,0 +1,176 @@ +/** + * FMP Government Trades Model. + * Maps to: openbb_fmp/models/government_trades.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { GovernmentTradesQueryParamsSchema, GovernmentTradesDataSchema } from '../../../standard-models/government-trades.js' +import { applyAliases } from '../../../core/provider/utils/helpers.js' +import { EmptyDataError } from '../../../core/provider/utils/errors.js' +import { amakeRequest } from '../../../core/provider/utils/helpers.js' + +const ALIAS_DICT: Record = { + symbol: 'ticker', + transaction_date: 'transactionDate', + representative: 'office', + url: 'link', + transaction_type: 'type', + date: 'disclosureDate', +} + +const KEYS_TO_REMOVE = new Set([ + 'district', + 'capitalGainsOver200USD', + 'disclosureYear', + 'firstName', + 'lastName', +]) + +const KEYS_TO_RENAME: Record = { + dateRecieved: 'date', + disclosureDate: 'date', +} + +export const FMPGovernmentTradesQueryParamsSchema = GovernmentTradesQueryParamsSchema +export type FMPGovernmentTradesQueryParams = z.infer + +export const FMPGovernmentTradesDataSchema = GovernmentTradesDataSchema.extend({ + chamber: z.enum(['House', 'Senate']).describe('Government Chamber - House or Senate.'), + owner: z.string().nullable().default(null).describe('Ownership status (e.g., Spouse, Joint).'), + asset_type: z.string().nullable().default(null).describe('Type of asset involved in the transaction.'), + asset_description: z.string().nullable().default(null).describe('Description of the asset.'), + transaction_type: z.string().nullable().default(null).describe('Type of transaction (e.g., Sale, Purchase).'), + amount: z.string().nullable().default(null).describe('Transaction amount range.'), + comment: z.string().nullable().default(null).describe('Additional comments on the transaction.'), + url: z.string().nullable().default(null).describe('Link to the transaction document.'), +}).strip() +export type FMPGovernmentTradesData = z.infer + +/** Determine asset_type from description if missing */ +function inferAssetType(d: Record): string | null { + const desc = String(d.assetDescription ?? d.asset_description ?? '').toLowerCase() + const hasTicker = !!(d.ticker || d.symbol) + + if (hasTicker) { + return desc.includes('etf') ? 'ETF' : 'Stock' + } + if (desc.includes('treasury') || desc.includes('bill')) return 'Treasury' + if (desc.includes('%') || desc.includes('due') || desc.includes('pct')) return 'Bond' + if (desc.includes('fund')) return 'Fund' + if (desc.includes('etf')) return 'ETF' + return null +} + +export class FMPGovernmentTradesFetcher extends Fetcher { + static override transformQuery(params: Record): FMPGovernmentTradesQueryParams { + return FMPGovernmentTradesQueryParamsSchema.parse(params) + } + + static override async extractData( + query: FMPGovernmentTradesQueryParams, + credentials: Record | null, + ): Promise[]> { + const apiKey = credentials?.fmp_api_key ?? '' + const baseUrl = 'https://financialmodelingprep.com/stable/' + const chamberUrls: Record = { + house: ['house-trades'], + senate: ['senate-trades'], + all: ['house-trades', 'senate-trades'], + } + const endpoints = chamberUrls[query.chamber] ?? chamberUrls.all + const results: Record[] = [] + + if (query.symbol) { + // Symbol-based: fetch for each symbol × each chamber + const symbols = query.symbol.split(',').map(s => s.trim()).filter(Boolean) + const urls: { url: string; chamber: string }[] = [] + for (const symbol of symbols) { + for (const ep of endpoints) { + urls.push({ + url: `${baseUrl}${ep}?symbol=${symbol}&apikey=${apiKey}`, + chamber: ep.includes('senate') ? 'Senate' : 'House', + }) + } + } + + const settled = await Promise.allSettled( + urls.map(async ({ url, chamber }) => { + const data = await amakeRequest(url) as any[] + return (data ?? []).map((d: any) => ({ ...d, chamber })) + }), + ) + + for (const r of settled) { + if (r.status === 'fulfilled' && r.value?.length) { + results.push(...r.value) + } + } + } else { + // No symbol: fetch latest trades (up to limit) + const limit = query.limit ?? 1000 + for (const ep of endpoints) { + const chamber = ep.includes('senate') ? 'Senate' : 'House' + try { + const latestEp = ep.replace('trades', 'latest') + const data = await amakeRequest( + `${baseUrl}${latestEp}?page=0&limit=${Math.min(limit, 250)}&apikey=${apiKey}`, + ) as any[] + if (data?.length) { + results.push(...data.map((d: any) => ({ ...d, chamber }))) + } + } catch { + // Ignore errors for individual chambers + } + } + } + + if (!results.length) { + throw new EmptyDataError('No government trades data returned.') + } + + // Process: rename keys, remove unwanted keys, add chamber + return results.map(entry => { + const processed: Record = {} + for (const [k, v] of Object.entries(entry)) { + if (KEYS_TO_REMOVE.has(k)) continue + const newKey = KEYS_TO_RENAME[k] ?? k + processed[newKey] = v + } + return processed + }) + } + + static override transformData( + query: FMPGovernmentTradesQueryParams, + data: Record[], + ): FMPGovernmentTradesData[] { + const results = data + .filter(d => { + // Skip entries where all values are "--" or empty + const vals = Object.values(d) + return vals.some(v => v && v !== '--') + }) + .map(d => { + // Fill missing owner + if (!d.owner) d.owner = 'Self' + // Fill missing asset_type + if (!d.assetType && !d.asset_type) { + d.asset_type = inferAssetType(d) + } + // Clean "--" values to null + for (const [k, v] of Object.entries(d)) { + if (v === '--') d[k] = null + } + const aliased = applyAliases(d, ALIAS_DICT) + return FMPGovernmentTradesDataSchema.parse(aliased) + }) + + // Sort by date descending + results.sort((a, b) => (b.date > a.date ? 1 : b.date < a.date ? -1 : 0)) + + // Apply limit + const limit = query.limit ?? results.length + return results.slice(0, limit) + } +} diff --git a/packages/opentypebb/src/providers/fmp/models/historical-dividends.ts b/packages/opentypebb/src/providers/fmp/models/historical-dividends.ts new file mode 100644 index 00000000..4cffc78c --- /dev/null +++ b/packages/opentypebb/src/providers/fmp/models/historical-dividends.ts @@ -0,0 +1,63 @@ +/** + * FMP Historical Dividends Model. + * Maps to: openbb_fmp/models/historical_dividends.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { HistoricalDividendsQueryParamsSchema, HistoricalDividendsDataSchema } from '../../../standard-models/historical-dividends.js' +import { applyAliases } from '../../../core/provider/utils/helpers.js' +import { getDataMany } from '../utils/helpers.js' + +const ALIAS_DICT: Record = { + ex_dividend_date: 'date', + amount: 'dividend', + adjusted_amount: 'adjDividend', + dividend_yield: 'yield', + record_date: 'recordDate', + payment_date: 'paymentDate', + declaration_date: 'declarationDate', +} + +const numOrNull = z.number().nullable().default(null) + +export const FMPHistoricalDividendsQueryParamsSchema = HistoricalDividendsQueryParamsSchema.extend({ + limit: z.coerce.number().nullable().default(null).describe('The number of data entries to return.'), +}) +export type FMPHistoricalDividendsQueryParams = z.infer + +export const FMPHistoricalDividendsDataSchema = HistoricalDividendsDataSchema.extend({ + declaration_date: z.string().nullable().default(null).describe('Declaration date of the dividend.'), + record_date: z.string().nullable().default(null).describe('Record date of the dividend.'), + payment_date: z.string().nullable().default(null).describe('Payment date of the dividend.'), + adjusted_amount: numOrNull.describe('Adjusted dividend amount.'), + dividend_yield: numOrNull.describe('Dividend yield.'), + frequency: z.string().nullable().default(null).describe('Frequency of the dividend.'), +}).passthrough() +export type FMPHistoricalDividendsData = z.infer + +export class FMPHistoricalDividendsFetcher extends Fetcher { + static override transformQuery(params: Record): FMPHistoricalDividendsQueryParams { + return FMPHistoricalDividendsQueryParamsSchema.parse(params) + } + + static override async extractData( + query: FMPHistoricalDividendsQueryParams, + credentials: Record | null, + ): Promise[]> { + const apiKey = credentials?.fmp_api_key ?? '' + let url = `https://financialmodelingprep.com/stable/dividends?symbol=${query.symbol}&apikey=${apiKey}` + if (query.limit) url += `&limit=${query.limit}` + return getDataMany(url) + } + + static override transformData( + _query: FMPHistoricalDividendsQueryParams, + data: Record[], + ): FMPHistoricalDividendsData[] { + return data.map(d => { + const aliased = applyAliases(d, ALIAS_DICT) + return FMPHistoricalDividendsDataSchema.parse(aliased) + }) + } +} diff --git a/packages/opentypebb/src/providers/fmp/models/historical-employees.ts b/packages/opentypebb/src/providers/fmp/models/historical-employees.ts new file mode 100644 index 00000000..953c4842 --- /dev/null +++ b/packages/opentypebb/src/providers/fmp/models/historical-employees.ts @@ -0,0 +1,56 @@ +/** + * FMP Historical Employees Model. + * Maps to: openbb_fmp/models/historical_employees.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { HistoricalEmployeesQueryParamsSchema, HistoricalEmployeesDataSchema } from '../../../standard-models/historical-employees.js' +import { applyAliases } from '../../../core/provider/utils/helpers.js' +import { getDataMany } from '../utils/helpers.js' + +const ALIAS_DICT: Record = { + company_name: 'companyName', + employees: 'employeeCount', + date: 'periodOfReport', + source: 'formType', + url: 'source', +} + +export const FMPHistoricalEmployeesQueryParamsSchema = HistoricalEmployeesQueryParamsSchema.extend({ + limit: z.coerce.number().nullable().default(null).describe('The number of data entries to return.'), +}) +export type FMPHistoricalEmployeesQueryParams = z.infer + +export const FMPHistoricalEmployeesDataSchema = HistoricalEmployeesDataSchema.extend({ + company_name: z.string().nullable().default(null).describe('Name of the company.'), + source: z.string().nullable().default(null).describe('Source form type.'), + url: z.string().nullable().default(null).describe('URL to the source filing.'), +}).passthrough() +export type FMPHistoricalEmployeesData = z.infer + +export class FMPHistoricalEmployeesFetcher extends Fetcher { + static override transformQuery(params: Record): FMPHistoricalEmployeesQueryParams { + return FMPHistoricalEmployeesQueryParamsSchema.parse(params) + } + + static override async extractData( + query: FMPHistoricalEmployeesQueryParams, + credentials: Record | null, + ): Promise[]> { + const apiKey = credentials?.fmp_api_key ?? '' + let url = `https://financialmodelingprep.com/stable/historical-employee-count?symbol=${query.symbol}&apikey=${apiKey}` + if (query.limit) url += `&limit=${query.limit}` + return getDataMany(url) + } + + static override transformData( + _query: FMPHistoricalEmployeesQueryParams, + data: Record[], + ): FMPHistoricalEmployeesData[] { + return data.map(d => { + const aliased = applyAliases(d, ALIAS_DICT) + return FMPHistoricalEmployeesDataSchema.parse(aliased) + }) + } +} diff --git a/packages/opentypebb/src/providers/fmp/models/historical-eps.ts b/packages/opentypebb/src/providers/fmp/models/historical-eps.ts new file mode 100644 index 00000000..ddd3f45f --- /dev/null +++ b/packages/opentypebb/src/providers/fmp/models/historical-eps.ts @@ -0,0 +1,58 @@ +/** + * FMP Historical EPS Model. + * Maps to: openbb_fmp/models/historical_eps.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { HistoricalEpsQueryParamsSchema, HistoricalEpsDataSchema } from '../../../standard-models/historical-eps.js' +import { applyAliases } from '../../../core/provider/utils/helpers.js' +import { getDataMany } from '../utils/helpers.js' + +const ALIAS_DICT: Record = { + eps_actual: 'epsActual', + eps_estimated: 'epsEstimated', + revenue_estimated: 'revenueEstimated', + revenue_actual: 'revenueActual', + updated: 'lastUpdated', +} + +const numOrNull = z.number().nullable().default(null) + +export const FMPHistoricalEpsQueryParamsSchema = HistoricalEpsQueryParamsSchema.extend({ + limit: z.coerce.number().nullable().default(null).describe('The number of data entries to return.'), +}) +export type FMPHistoricalEpsQueryParams = z.infer + +export const FMPHistoricalEpsDataSchema = HistoricalEpsDataSchema.extend({ + revenue_estimated: numOrNull.describe('Estimated revenue.'), + revenue_actual: numOrNull.describe('Actual revenue.'), + updated: z.string().nullable().default(null).describe('Last updated date.'), +}).passthrough() +export type FMPHistoricalEpsData = z.infer + +export class FMPHistoricalEpsFetcher extends Fetcher { + static override transformQuery(params: Record): FMPHistoricalEpsQueryParams { + return FMPHistoricalEpsQueryParamsSchema.parse(params) + } + + static override async extractData( + query: FMPHistoricalEpsQueryParams, + credentials: Record | null, + ): Promise[]> { + const apiKey = credentials?.fmp_api_key ?? '' + let url = `https://financialmodelingprep.com/stable/earnings?symbol=${query.symbol}&apikey=${apiKey}` + if (query.limit) url += `&limit=${query.limit}` + return getDataMany(url) + } + + static override transformData( + _query: FMPHistoricalEpsQueryParams, + data: Record[], + ): FMPHistoricalEpsData[] { + return data.map(d => { + const aliased = applyAliases(d, ALIAS_DICT) + return FMPHistoricalEpsDataSchema.parse(aliased) + }) + } +} diff --git a/packages/opentypebb/src/providers/fmp/models/historical-market-cap.ts b/packages/opentypebb/src/providers/fmp/models/historical-market-cap.ts new file mode 100644 index 00000000..248d53a2 --- /dev/null +++ b/packages/opentypebb/src/providers/fmp/models/historical-market-cap.ts @@ -0,0 +1,54 @@ +/** + * FMP Historical Market Cap Model. + * Maps to: openbb_fmp/models/historical_market_cap.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { HistoricalMarketCapQueryParamsSchema, HistoricalMarketCapDataSchema } from '../../../standard-models/historical-market-cap.js' +import { applyAliases } from '../../../core/provider/utils/helpers.js' +import { getDataMany } from '../utils/helpers.js' + +const ALIAS_DICT: Record = { + market_cap: 'marketCap', +} + +export const FMPHistoricalMarketCapQueryParamsSchema = HistoricalMarketCapQueryParamsSchema.extend({ + limit: z.coerce.number().nullable().default(500).describe('The number of data entries to return.'), +}) +export type FMPHistoricalMarketCapQueryParams = z.infer + +export const FMPHistoricalMarketCapDataSchema = HistoricalMarketCapDataSchema +export type FMPHistoricalMarketCapData = z.infer + +export class FMPHistoricalMarketCapFetcher extends Fetcher { + static override transformQuery(params: Record): FMPHistoricalMarketCapQueryParams { + return FMPHistoricalMarketCapQueryParamsSchema.parse(params) + } + + static override async extractData( + query: FMPHistoricalMarketCapQueryParams, + credentials: Record | null, + ): Promise[]> { + const apiKey = credentials?.fmp_api_key ?? '' + const qs = new URLSearchParams() + qs.set('symbol', query.symbol) + qs.set('apikey', apiKey) + if (query.limit) qs.set('limit', String(query.limit)) + if (query.start_date) qs.set('from', query.start_date) + if (query.end_date) qs.set('to', query.end_date) + return getDataMany( + `https://financialmodelingprep.com/stable/historical-market-capitalization?${qs.toString()}`, + ) + } + + static override transformData( + _query: FMPHistoricalMarketCapQueryParams, + data: Record[], + ): FMPHistoricalMarketCapData[] { + return data.map(d => { + const aliased = applyAliases(d, ALIAS_DICT) + return FMPHistoricalMarketCapDataSchema.parse(aliased) + }) + } +} diff --git a/packages/opentypebb/src/providers/fmp/models/historical-splits.ts b/packages/opentypebb/src/providers/fmp/models/historical-splits.ts new file mode 100644 index 00000000..755ee2dd --- /dev/null +++ b/packages/opentypebb/src/providers/fmp/models/historical-splits.ts @@ -0,0 +1,38 @@ +/** + * FMP Historical Splits Model. + * Maps to: openbb_fmp/models/historical_splits.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { HistoricalSplitsQueryParamsSchema, HistoricalSplitsDataSchema } from '../../../standard-models/historical-splits.js' +import { getDataMany } from '../utils/helpers.js' + +export const FMPHistoricalSplitsQueryParamsSchema = HistoricalSplitsQueryParamsSchema +export type FMPHistoricalSplitsQueryParams = z.infer + +export const FMPHistoricalSplitsDataSchema = HistoricalSplitsDataSchema.passthrough() +export type FMPHistoricalSplitsData = z.infer + +export class FMPHistoricalSplitsFetcher extends Fetcher { + static override transformQuery(params: Record): FMPHistoricalSplitsQueryParams { + return FMPHistoricalSplitsQueryParamsSchema.parse(params) + } + + static override async extractData( + query: FMPHistoricalSplitsQueryParams, + credentials: Record | null, + ): Promise[]> { + const apiKey = credentials?.fmp_api_key ?? '' + return getDataMany( + `https://financialmodelingprep.com/stable/splits?symbol=${query.symbol}&apikey=${apiKey}`, + ) + } + + static override transformData( + _query: FMPHistoricalSplitsQueryParams, + data: Record[], + ): FMPHistoricalSplitsData[] { + return data.map(d => FMPHistoricalSplitsDataSchema.parse(d)) + } +} diff --git a/packages/opentypebb/src/providers/fmp/models/income-statement-growth.ts b/packages/opentypebb/src/providers/fmp/models/income-statement-growth.ts new file mode 100644 index 00000000..4d17b7db --- /dev/null +++ b/packages/opentypebb/src/providers/fmp/models/income-statement-growth.ts @@ -0,0 +1,106 @@ +/** + * FMP Income Statement Growth Model. + * Maps to: openbb_fmp/models/income_statement_growth.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { IncomeStatementGrowthQueryParamsSchema, IncomeStatementGrowthDataSchema } from '../../../standard-models/income-statement-growth.js' +import { applyAliases } from '../../../core/provider/utils/helpers.js' +import { getDataMany } from '../utils/helpers.js' + +// --- Query Params --- + +export const FMPIncomeStatementGrowthQueryParamsSchema = IncomeStatementGrowthQueryParamsSchema.extend({ + period: z.enum(['annual', 'quarter']).default('annual').describe('Time period of the data to return.'), +}) + +export type FMPIncomeStatementGrowthQueryParams = z.infer + +// --- Data --- + +const ALIAS_DICT: Record = { + period_ending: 'date', + fiscal_year: 'calendarYear', + fiscal_period: 'period', + growth_ebit: 'growthEBIT', + growth_ebitda: 'growthEBITDA', + growth_basic_earings_per_share: 'growthEPS', + growth_gross_profit_margin: 'growthGrossProfitRatio', + growth_consolidated_net_income: 'growthNetIncome', + growth_diluted_earnings_per_share: 'growthEPSDiluted', + growth_weighted_average_basic_shares_outstanding: 'growthWeightedAverageShsOut', + growth_weighted_average_diluted_shares_outstanding: 'growthWeightedAverageShsOutDil', + growth_research_and_development_expense: 'growthResearchAndDevelopmentExpenses', + growth_general_and_admin_expense: 'growthGeneralAndAdministrativeExpenses', + growth_selling_and_marketing_expense: 'growthSellingAndMarketingExpenses', +} + +const pctOrNull = z.number().nullable().default(null) + +export const FMPIncomeStatementGrowthDataSchema = IncomeStatementGrowthDataSchema.extend({ + symbol: z.string().nullable().default(null).describe('The stock ticker symbol.'), + reported_currency: z.string().nullable().default(null).describe('The currency in which the financial data is reported.'), + growth_revenue: pctOrNull.describe('Growth rate of total revenue.'), + growth_cost_of_revenue: pctOrNull.describe('Growth rate of cost of goods sold.'), + growth_gross_profit: pctOrNull.describe('Growth rate of gross profit.'), + growth_gross_profit_margin: pctOrNull.describe('Growth rate of gross profit as a percentage of revenue.'), + growth_general_and_admin_expense: pctOrNull.describe('Growth rate of general and administrative expenses.'), + growth_research_and_development_expense: pctOrNull.describe('Growth rate of expenses on research and development.'), + growth_selling_and_marketing_expense: pctOrNull.describe('Growth rate of expenses on selling and marketing activities.'), + growth_other_expenses: pctOrNull.describe('Growth rate of other operating expenses.'), + growth_operating_expenses: pctOrNull.describe('Growth rate of total operating expenses.'), + growth_cost_and_expenses: pctOrNull.describe('Growth rate of total costs and expenses.'), + growth_depreciation_and_amortization: pctOrNull.describe('Growth rate of depreciation and amortization expenses.'), + growth_interest_income: pctOrNull.describe('Growth rate of interest income.'), + growth_interest_expense: pctOrNull.describe('Growth rate of interest expenses.'), + growth_net_interest_income: pctOrNull.describe('Growth rate of net interest income.'), + growth_ebit: pctOrNull.describe('Growth rate of Earnings Before Interest and Taxes (EBIT).'), + growth_ebitda: pctOrNull.describe('Growth rate of EBITDA.'), + growth_operating_income: pctOrNull.describe('Growth rate of operating income.'), + growth_non_operating_income_excluding_interest: pctOrNull.describe('Growth rate of non-operating income excluding interest.'), + growth_total_other_income_expenses_net: pctOrNull.describe('Growth rate of net total other income and expenses.'), + growth_other_adjustments_to_net_income: pctOrNull.describe('Growth rate of other adjustments to net income.'), + growth_net_income_deductions: pctOrNull.describe('Growth rate of net income deductions.'), + growth_income_before_tax: pctOrNull.describe('Growth rate of income before taxes.'), + growth_income_tax_expense: pctOrNull.describe('Growth rate of income tax expenses.'), + growth_net_income_from_continuing_operations: pctOrNull.describe('Growth rate of net income from continuing operations.'), + growth_consolidated_net_income: pctOrNull.describe('Growth rate of net income.'), + growth_basic_earings_per_share: pctOrNull.describe('Growth rate of Earnings Per Share (EPS).'), + growth_diluted_earnings_per_share: pctOrNull.describe('Growth rate of diluted Earnings Per Share (EPS).'), + growth_weighted_average_basic_shares_outstanding: pctOrNull.describe('Growth rate of weighted average shares outstanding.'), + growth_weighted_average_diluted_shares_outstanding: pctOrNull.describe('Growth rate of diluted weighted average shares outstanding.'), +}).passthrough() + +export type FMPIncomeStatementGrowthData = z.infer + +// --- Fetcher --- + +export class FMPIncomeStatementGrowthFetcher extends Fetcher { + static override transformQuery(params: Record): FMPIncomeStatementGrowthQueryParams { + return FMPIncomeStatementGrowthQueryParamsSchema.parse(params) + } + + static override async extractData( + query: FMPIncomeStatementGrowthQueryParams, + credentials: Record | null, + ): Promise[]> { + const apiKey = credentials?.fmp_api_key ?? '' + const url = 'https://financialmodelingprep.com/stable/income-statement-growth' + + `?symbol=${query.symbol}` + + `&period=${query.period}` + + `&limit=${query.limit ?? 5}` + + `&apikey=${apiKey}` + return getDataMany(url) + } + + static override transformData( + query: FMPIncomeStatementGrowthQueryParams, + data: Record[], + ): FMPIncomeStatementGrowthData[] { + return data.map(d => { + const aliased = applyAliases(d, ALIAS_DICT) + return FMPIncomeStatementGrowthDataSchema.parse(aliased) + }) + } +} diff --git a/packages/opentypebb/src/providers/fmp/models/income-statement.ts b/packages/opentypebb/src/providers/fmp/models/income-statement.ts new file mode 100644 index 00000000..d8bd2e59 --- /dev/null +++ b/packages/opentypebb/src/providers/fmp/models/income-statement.ts @@ -0,0 +1,125 @@ +/** + * FMP Income Statement Model. + * Maps to: openbb_fmp/models/income_statement.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { IncomeStatementQueryParamsSchema, IncomeStatementDataSchema } from '../../../standard-models/income-statement.js' +import { applyAliases } from '../../../core/provider/utils/helpers.js' +import { getDataMany } from '../utils/helpers.js' + +// --- Query Params --- + +export const FMPIncomeStatementQueryParamsSchema = IncomeStatementQueryParamsSchema.extend({ + period: z.enum(['q1', 'q2', 'q3', 'q4', 'fy', 'ttm', 'annual', 'quarter']).default('annual').describe('Time period of the data to return.'), +}) + +export type FMPIncomeStatementQueryParams = z.infer + +// --- Data --- + +const ALIAS_DICT: Record = { + period_ending: 'date', + fiscal_period: 'period', + fiscal_year: 'calendarYear', + filing_date: 'fillingDate', + accepted_date: 'acceptedDate', + reported_currency: 'reportedCurrency', + revenue: 'revenue', + cost_of_revenue: 'costOfRevenue', + gross_profit: 'grossProfit', + general_and_admin_expense: 'generalAndAdministrativeExpenses', + research_and_development_expense: 'researchAndDevelopmentExpenses', + selling_and_marketing_expense: 'sellingAndMarketingExpenses', + selling_general_and_admin_expense: 'sellingGeneralAndAdministrativeExpenses', + other_expenses: 'otherExpenses', + total_operating_expenses: 'operatingExpenses', + cost_and_expenses: 'costAndExpenses', + interest_income: 'interestIncome', + total_interest_expense: 'interestExpense', + depreciation_and_amortization: 'depreciationAndAmortization', + ebitda: 'ebitda', + total_operating_income: 'operatingIncome', + total_other_income_expenses: 'totalOtherIncomeExpensesNet', + total_pre_tax_income: 'incomeBeforeTax', + income_tax_expense: 'incomeTaxExpense', + consolidated_net_income: 'netIncome', + basic_earnings_per_share: 'eps', + diluted_earnings_per_share: 'epsDiluted', + weighted_average_basic_shares_outstanding: 'weightedAverageShsOut', + weighted_average_diluted_shares_outstanding: 'weightedAverageShsOutDil', +} + +const intOrNull = z.number().int().nullable().default(null) + +export const FMPIncomeStatementDataSchema = IncomeStatementDataSchema.extend({ + filing_date: z.string().nullable().default(null).describe('The date when the filing was made.'), + accepted_date: z.string().nullable().default(null).describe('The date and time when the filing was accepted.'), + cik: z.string().nullable().default(null).describe('The Central Index Key (CIK) assigned by the SEC.'), + symbol: z.string().nullable().default(null).describe('The stock ticker symbol.'), + reported_currency: z.string().nullable().default(null).describe('The currency in which the balance sheet was reported.'), + revenue: intOrNull.describe('Total revenue.'), + cost_of_revenue: intOrNull.describe('Cost of revenue.'), + gross_profit: intOrNull.describe('Gross profit.'), + general_and_admin_expense: intOrNull.describe('General and administrative expenses.'), + research_and_development_expense: intOrNull.describe('Research and development expenses.'), + selling_and_marketing_expense: intOrNull.describe('Selling and marketing expenses.'), + selling_general_and_admin_expense: intOrNull.describe('Selling, general and administrative expenses.'), + other_expenses: intOrNull.describe('Other expenses.'), + total_operating_expenses: intOrNull.describe('Total operating expenses.'), + cost_and_expenses: intOrNull.describe('Cost and expenses.'), + interest_income: intOrNull.describe('Interest income.'), + total_interest_expense: intOrNull.describe('Total interest expenses.'), + depreciation_and_amortization: intOrNull.describe('Depreciation and amortization.'), + ebitda: intOrNull.describe('EBITDA.'), + total_operating_income: intOrNull.describe('Total operating income.'), + total_other_income_expenses: intOrNull.describe('Total other income and expenses.'), + total_pre_tax_income: intOrNull.describe('Total pre-tax income.'), + income_tax_expense: intOrNull.describe('Income tax expense.'), + consolidated_net_income: intOrNull.describe('Consolidated net income.'), + basic_earnings_per_share: z.number().nullable().default(null).describe('Basic earnings per share.'), + diluted_earnings_per_share: z.number().nullable().default(null).describe('Diluted earnings per share.'), + weighted_average_basic_shares_outstanding: intOrNull.describe('Weighted average basic shares outstanding.'), + weighted_average_diluted_shares_outstanding: intOrNull.describe('Weighted average diluted shares outstanding.'), +}).passthrough() + +export type FMPIncomeStatementData = z.infer + +// --- Fetcher --- + +export class FMPIncomeStatementFetcher extends Fetcher { + static override transformQuery(params: Record): FMPIncomeStatementQueryParams { + return FMPIncomeStatementQueryParamsSchema.parse(params) + } + + static override async extractData( + query: FMPIncomeStatementQueryParams, + credentials: Record | null, + ): Promise[]> { + const apiKey = credentials?.fmp_api_key ?? '' + let baseUrl = 'https://financialmodelingprep.com/stable/income-statement' + + if (query.period === 'ttm') { + baseUrl += '-ttm' + } + + const url = baseUrl + + `?symbol=${query.symbol}` + + (query.period !== 'ttm' ? `&period=${query.period}` : '') + + `&limit=${query.limit ?? 5}` + + `&apikey=${apiKey}` + + return getDataMany(url) + } + + static override transformData( + query: FMPIncomeStatementQueryParams, + data: Record[], + ): FMPIncomeStatementData[] { + return data.map((d) => { + const aliased = applyAliases(d, ALIAS_DICT) + return FMPIncomeStatementDataSchema.parse(aliased) + }) + } +} diff --git a/packages/opentypebb/src/providers/fmp/models/index-constituents.ts b/packages/opentypebb/src/providers/fmp/models/index-constituents.ts new file mode 100644 index 00000000..d06cd00d --- /dev/null +++ b/packages/opentypebb/src/providers/fmp/models/index-constituents.ts @@ -0,0 +1,70 @@ +/** + * FMP Index Constituents Model. + * Maps to: openbb_fmp/models/index_constituents.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { IndexConstituentsQueryParamsSchema, IndexConstituentsDataSchema } from '../../../standard-models/index-constituents.js' +import { applyAliases } from '../../../core/provider/utils/helpers.js' +import { getDataMany } from '../utils/helpers.js' + +const ALIAS_DICT: Record = { + headquarter: 'headQuarter', + date_added: 'dateFirstAdded', + industry: 'subSector', + name: 'addedSecurity', + removed_symbol: 'removedTicker', + removed_name: 'removedSecurity', +} + +export const FMPIndexConstituentsQueryParamsSchema = IndexConstituentsQueryParamsSchema.extend({ + symbol: z.enum(['dowjones', 'sp500', 'nasdaq']).default('dowjones').describe('Index symbol.'), + historical: z.boolean().default(false).describe('Flag to retrieve historical removals and additions.'), +}) +export type FMPIndexConstituentsQueryParams = z.infer + +export const FMPIndexConstituentsDataSchema = IndexConstituentsDataSchema.extend({ + sector: z.string().nullable().default(null).describe('Sector classification.'), + industry: z.string().nullable().default(null).describe('Industry classification.'), + headquarter: z.string().nullable().default(null).describe('Location of headquarters.'), + date_added: z.string().nullable().default(null).describe('Date added to the index.'), + cik: z.string().nullable().default(null).describe('CIK number.'), + founded: z.string().nullable().default(null).describe('When the company was founded.'), + removed_symbol: z.string().nullable().default(null).describe('Symbol of the company removed.'), + removed_name: z.string().nullable().default(null).describe('Name of the company removed.'), + reason: z.string().nullable().default(null).describe('Reason for the removal.'), + date: z.string().nullable().default(null).describe('Date of the historical constituent data.'), +}).passthrough() +export type FMPIndexConstituentsData = z.infer + +export class FMPIndexConstituentsFetcher extends Fetcher { + static override transformQuery(params: Record): FMPIndexConstituentsQueryParams { + return FMPIndexConstituentsQueryParamsSchema.parse(params) + } + + static override async extractData( + query: FMPIndexConstituentsQueryParams, + credentials: Record | null, + ): Promise[]> { + const apiKey = credentials?.fmp_api_key ?? '' + const prefix = query.historical ? 'historical-' : '' + return getDataMany( + `https://financialmodelingprep.com/stable/${prefix}${query.symbol}-constituent/?apikey=${apiKey}`, + ) + } + + static override transformData( + _query: FMPIndexConstituentsQueryParams, + data: Record[], + ): FMPIndexConstituentsData[] { + return data.map(d => { + // Clean empty strings + for (const key of ['removed_symbol', 'removed_name', 'reason', 'removedTicker', 'removedSecurity']) { + if (d[key] === '' || d[key] === "''" || d[key] === 'None') d[key] = null + } + const aliased = applyAliases(d, ALIAS_DICT) + return FMPIndexConstituentsDataSchema.parse(aliased) + }) + } +} diff --git a/packages/opentypebb/src/providers/fmp/models/index-historical.ts b/packages/opentypebb/src/providers/fmp/models/index-historical.ts new file mode 100644 index 00000000..26238afc --- /dev/null +++ b/packages/opentypebb/src/providers/fmp/models/index-historical.ts @@ -0,0 +1,81 @@ +/** + * FMP Index Historical Model. + * Maps to: openbb_fmp/models/index_historical.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { IndexHistoricalQueryParamsSchema, IndexHistoricalDataSchema } from '../../../standard-models/index-historical.js' +import { getHistoricalOhlc } from '../utils/helpers.js' +import { EmptyDataError } from '../../../core/provider/utils/errors.js' + +export const FMPIndexHistoricalQueryParamsSchema = IndexHistoricalQueryParamsSchema.extend({ + interval: z.enum(['1m', '5m', '1h', '1d']).default('1d').describe('Time interval of the data.'), +}) +export type FMPIndexHistoricalQueryParams = z.infer + +export const FMPIndexHistoricalDataSchema = IndexHistoricalDataSchema.extend({ + vwap: z.number().nullable().default(null).describe('Volume-weighted average price.'), + change: z.number().nullable().default(null).describe('Change in the price from the previous close.'), + change_percent: z.number().nullable().default(null).describe('Change percent from previous close.'), +}).passthrough() +export type FMPIndexHistoricalData = z.infer + +export class FMPIndexHistoricalFetcher extends Fetcher { + static override transformQuery(params: Record): FMPIndexHistoricalQueryParams { + // Default start_date to 1 year ago, end_date to today + const now = new Date() + if (!params.start_date) { + const oneYearAgo = new Date(now) + oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1) + params.start_date = oneYearAgo.toISOString().split('T')[0] + } + if (!params.end_date) { + params.end_date = now.toISOString().split('T')[0] + } + return FMPIndexHistoricalQueryParamsSchema.parse(params) + } + + static override async extractData( + query: FMPIndexHistoricalQueryParams, + credentials: Record | null, + ): Promise[]> { + return getHistoricalOhlc( + { + symbol: query.symbol, + interval: query.interval, + start_date: query.start_date, + end_date: query.end_date, + }, + credentials, + ) + } + + static override transformData( + query: FMPIndexHistoricalQueryParams, + data: Record[], + ): FMPIndexHistoricalData[] { + if (!data || data.length === 0) { + throw new EmptyDataError() + } + + // Normalize change_percent + for (const d of data) { + if (typeof d.changePercentage === 'number') { + d.changePercentage = d.changePercentage / 100 + } + if (typeof d.change_percent === 'number') { + d.change_percent = d.change_percent / 100 + } + } + + // Sort by date ascending + const sorted = data.sort((a, b) => { + const da = String(a.date ?? '') + const db = String(b.date ?? '') + return da.localeCompare(db) + }) + + return sorted.map(d => FMPIndexHistoricalDataSchema.parse(d)) + } +} diff --git a/packages/opentypebb/src/providers/fmp/models/insider-trading.ts b/packages/opentypebb/src/providers/fmp/models/insider-trading.ts new file mode 100644 index 00000000..bddc5a04 --- /dev/null +++ b/packages/opentypebb/src/providers/fmp/models/insider-trading.ts @@ -0,0 +1,102 @@ +/** + * FMP Insider Trading Model. + * Maps to: openbb_fmp/models/insider_trading.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { InsiderTradingQueryParamsSchema, InsiderTradingDataSchema } from '../../../standard-models/insider-trading.js' +import { applyAliases } from '../../../core/provider/utils/helpers.js' +import { getDataMany, getDataUrls, getQueryString } from '../utils/helpers.js' +import { TRANSACTION_TYPES_DICT } from '../utils/definitions.js' + +// --- Query Params --- + +export const FMPInsiderTradingQueryParamsSchema = InsiderTradingQueryParamsSchema.extend({ + transaction_type: z.string().nullable().default(null).describe('Type of the transaction.'), + statistics: z.boolean().default(false).describe('Flag to return summary statistics for the given symbol.'), +}) + +export type FMPInsiderTradingQueryParams = z.infer + +// --- Data --- + +const ALIAS_DICT: Record = { + owner_cik: 'reportingCik', + owner_name: 'reportingName', + owner_title: 'typeOfOwner', + ownership_type: 'directOrIndirect', + security_type: 'securityName', + transaction_price: 'price', + acquisition_or_disposition: 'acquistionOrDisposition', + filing_url: 'link', + company_cik: 'cik', +} + +export const FMPInsiderTradingDataSchema = InsiderTradingDataSchema.extend({ + form_type: z.string().nullable().default(null).describe('The SEC form type.'), + year: z.number().int().nullable().default(null).describe('The calendar year for the statistics.'), + quarter: z.number().int().nullable().default(null).describe('The calendar quarter for the statistics.'), +}).passthrough() + +export type FMPInsiderTradingData = z.infer + +// --- Fetcher --- + +export class FMPInsiderTradingFetcher extends Fetcher { + static override transformQuery(params: Record): FMPInsiderTradingQueryParams { + return FMPInsiderTradingQueryParamsSchema.parse(params) + } + + static override async extractData( + query: FMPInsiderTradingQueryParams, + credentials: Record | null, + ): Promise[]> { + const apiKey = credentials?.fmp_api_key ?? '' + + if (query.statistics) { + const url = `https://financialmodelingprep.com/stable/insider-trading/statistics?symbol=${query.symbol}&apikey=${apiKey}` + return getDataMany(url) + } + + const transactionType = query.transaction_type + ? TRANSACTION_TYPES_DICT[query.transaction_type] ?? null + : null + + const limit = query.limit && query.limit <= 1000 ? query.limit : 1000 + const baseUrl = 'https://financialmodelingprep.com/stable/insider-trading/search' + + const queryParams: Record = { + symbol: query.symbol, + transactionType: transactionType, + } + const queryStr = getQueryString(queryParams, ['page', 'limit']) + + const pages = Math.ceil(limit / 1000) + const urls = Array.from({ length: pages }, (_, page) => + `${baseUrl}?${queryStr}&page=${page}&limit=${limit}&apikey=${apiKey}`, + ) + + const results = await getDataUrls[]>(urls) + return results.flat() + } + + static override transformData( + query: FMPInsiderTradingQueryParams, + data: Record[], + ): FMPInsiderTradingData[] { + const sorted = query.statistics + ? [...data].sort((a, b) => { + const yearDiff = Number(b.year ?? 0) - Number(a.year ?? 0) + return yearDiff !== 0 ? yearDiff : Number(b.quarter ?? 0) - Number(a.quarter ?? 0) + }) + : [...data].sort((a, b) => + String(b.filingDate ?? '').localeCompare(String(a.filingDate ?? '')), + ) + + return sorted.map((d) => { + const aliased = applyAliases(d, ALIAS_DICT) + return FMPInsiderTradingDataSchema.parse(aliased) + }) + } +} diff --git a/packages/opentypebb/src/providers/fmp/models/institutional-ownership.ts b/packages/opentypebb/src/providers/fmp/models/institutional-ownership.ts new file mode 100644 index 00000000..1694fff7 --- /dev/null +++ b/packages/opentypebb/src/providers/fmp/models/institutional-ownership.ts @@ -0,0 +1,136 @@ +/** + * FMP Institutional Ownership Model. + * Maps to: openbb_fmp/models/institutional_ownership.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { InstitutionalOwnershipQueryParamsSchema, InstitutionalOwnershipDataSchema } from '../../../standard-models/institutional-ownership.js' +import { applyAliases } from '../../../core/provider/utils/helpers.js' +import { getDataMany } from '../utils/helpers.js' + +const ALIAS_DICT: Record = { + number_of_13f_shares: 'numberOf13Fshares', + last_number_of_13f_shares: 'lastNumberOf13Fshares', + number_of_13f_shares_change: 'numberOf13FsharesChange', + ownership_percent_change: 'changeInOwnershipPercentage', +} + +const numOrNull = z.number().nullable().default(null) + +export const FMPInstitutionalOwnershipQueryParamsSchema = InstitutionalOwnershipQueryParamsSchema.extend({ + year: z.coerce.number().nullable().default(null).describe('Calendar year for the data. If not provided, the latest year is used.'), + quarter: z.coerce.number().nullable().default(null).describe('Calendar quarter for the data (1-4). If not provided, the quarter previous to the current quarter is used.'), +}) +export type FMPInstitutionalOwnershipQueryParams = z.infer + +export const FMPInstitutionalOwnershipDataSchema = InstitutionalOwnershipDataSchema.extend({ + investors_holding: z.number().describe('Number of investors holding the stock.'), + last_investors_holding: z.number().describe('Number of investors holding the stock in the last quarter.'), + investors_holding_change: z.number().describe('Change in the number of investors holding the stock.'), + number_of_13f_shares: numOrNull.describe('Number of 13F shares.'), + last_number_of_13f_shares: numOrNull.describe('Number of 13F shares in the last quarter.'), + number_of_13f_shares_change: numOrNull.describe('Change in the number of 13F shares.'), + total_invested: z.number().describe('Total amount invested.'), + last_total_invested: z.number().describe('Total amount invested in the last quarter.'), + total_invested_change: z.number().describe('Change in the total amount invested.'), + ownership_percent: numOrNull.describe('Ownership percent as a normalized percent.'), + last_ownership_percent: numOrNull.describe('Ownership percent in the last quarter.'), + ownership_percent_change: numOrNull.describe('Change in the ownership percent.'), + new_positions: z.number().describe('Number of new positions.'), + last_new_positions: z.number().describe('Number of new positions in the last quarter.'), + new_positions_change: z.number().describe('Change in the number of new positions.'), + increased_positions: z.number().describe('Number of increased positions.'), + last_increased_positions: z.number().describe('Number of increased positions in the last quarter.'), + increased_positions_change: z.number().describe('Change in the number of increased positions.'), + closed_positions: z.number().describe('Number of closed positions.'), + last_closed_positions: z.number().describe('Number of closed positions in the last quarter.'), + closed_positions_change: z.number().describe('Change in the number of closed positions.'), + reduced_positions: z.number().describe('Number of reduced positions.'), + last_reduced_positions: z.number().describe('Number of reduced positions in the last quarter.'), + reduced_positions_change: z.number().describe('Change in the number of reduced positions.'), + total_calls: z.number().describe('Total number of call options contracts traded.'), + last_total_calls: z.number().describe('Total number of call options contracts traded in last quarter.'), + total_calls_change: z.number().describe('Change in the total number of call options contracts.'), + total_puts: z.number().describe('Total number of put options contracts traded.'), + last_total_puts: z.number().describe('Total number of put options contracts traded in last quarter.'), + total_puts_change: z.number().describe('Change in the total number of put options contracts.'), + put_call_ratio: z.number().describe('Put-call ratio.'), + last_put_call_ratio: z.number().describe('Put-call ratio in the last quarter.'), + put_call_ratio_change: z.number().describe('Change in the put-call ratio.'), +}).passthrough() +export type FMPInstitutionalOwnershipData = z.infer + +/** Get current quarter info for default year/quarter */ +function getCurrentQuarterInfo(): { year: number; quarter: number } { + const now = new Date() + const currentQuarter = Math.ceil((now.getMonth() + 1) / 3) + // Use previous quarter + if (currentQuarter === 1) { + return { year: now.getFullYear() - 1, quarter: 4 } + } + return { year: now.getFullYear(), quarter: currentQuarter - 1 } +} + +export class FMPInstitutionalOwnershipFetcher extends Fetcher { + static override transformQuery(params: Record): FMPInstitutionalOwnershipQueryParams { + return FMPInstitutionalOwnershipQueryParamsSchema.parse(params) + } + + static override async extractData( + query: FMPInstitutionalOwnershipQueryParams, + credentials: Record | null, + ): Promise[]> { + const apiKey = credentials?.fmp_api_key ?? '' + const symbols = query.symbol.split(',').map(s => s.trim()).filter(Boolean) + + let year = query.year + let quarter = query.quarter + + if (year == null && quarter == null) { + const info = getCurrentQuarterInfo() + year = info.year + quarter = info.quarter + } else if (year == null) { + year = new Date().getFullYear() + } else if (quarter == null) { + const now = new Date() + quarter = year < now.getFullYear() + ? 4 + : Math.max(1, Math.ceil((now.getMonth() + 1) / 3) - 1) + } + + const results: Record[] = [] + const settled = await Promise.allSettled( + symbols.map(symbol => + getDataMany( + `https://financialmodelingprep.com/stable/institutional-ownership/symbol-positions-summary?symbol=${symbol}&year=${year}&quarter=${quarter}&apikey=${apiKey}`, + ), + ), + ) + + for (const r of settled) { + if (r.status === 'fulfilled' && r.value?.length) { + results.push(...r.value) + } + } + + return results + } + + static override transformData( + _query: FMPInstitutionalOwnershipQueryParams, + data: Record[], + ): FMPInstitutionalOwnershipData[] { + return data.map(d => { + // Normalize percent fields from whole numbers to decimal + for (const key of ['ownershipPercent', 'lastOwnershipPercent', 'changeInOwnershipPercentage']) { + if (typeof d[key] === 'number') { + d[key] = (d[key] as number) / 100 + } + } + const aliased = applyAliases(d, ALIAS_DICT) + return FMPInstitutionalOwnershipDataSchema.parse(aliased) + }) + } +} diff --git a/packages/opentypebb/src/providers/fmp/models/key-executives.ts b/packages/opentypebb/src/providers/fmp/models/key-executives.ts new file mode 100644 index 00000000..d3b3e2e9 --- /dev/null +++ b/packages/opentypebb/src/providers/fmp/models/key-executives.ts @@ -0,0 +1,39 @@ +/** + * FMP Key Executives Model. + * Maps to: openbb_fmp/models/key_executives.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { KeyExecutivesQueryParamsSchema, KeyExecutivesDataSchema } from '../../../standard-models/key-executives.js' +import { getDataMany } from '../utils/helpers.js' + +export const FMPKeyExecutivesQueryParamsSchema = KeyExecutivesQueryParamsSchema +export type FMPKeyExecutivesQueryParams = z.infer + +// extra="ignore" in Python → .strip() in Zod +export const FMPKeyExecutivesDataSchema = KeyExecutivesDataSchema.strip() +export type FMPKeyExecutivesData = z.infer + +export class FMPKeyExecutivesFetcher extends Fetcher { + static override transformQuery(params: Record): FMPKeyExecutivesQueryParams { + return FMPKeyExecutivesQueryParamsSchema.parse(params) + } + + static override async extractData( + query: FMPKeyExecutivesQueryParams, + credentials: Record | null, + ): Promise[]> { + const apiKey = credentials?.fmp_api_key ?? '' + return getDataMany( + `https://financialmodelingprep.com/stable/key-executives?symbol=${query.symbol}&apikey=${apiKey}`, + ) + } + + static override transformData( + _query: FMPKeyExecutivesQueryParams, + data: Record[], + ): FMPKeyExecutivesData[] { + return data.map(d => FMPKeyExecutivesDataSchema.parse(d)) + } +} diff --git a/packages/opentypebb/src/providers/fmp/models/key-metrics.ts b/packages/opentypebb/src/providers/fmp/models/key-metrics.ts new file mode 100644 index 00000000..2de0cafd --- /dev/null +++ b/packages/opentypebb/src/providers/fmp/models/key-metrics.ts @@ -0,0 +1,135 @@ +/** + * FMP Key Metrics Model. + * Maps to: openbb_fmp/models/key_metrics.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { KeyMetricsQueryParamsSchema, KeyMetricsDataSchema } from '../../../standard-models/key-metrics.js' +import { applyAliases } from '../../../core/provider/utils/helpers.js' +import { EmptyDataError } from '../../../core/provider/utils/errors.js' +import { getDataMany } from '../utils/helpers.js' + +// --- Query Params --- + +export const FMPKeyMetricsQueryParamsSchema = KeyMetricsQueryParamsSchema.extend({ + ttm: z.enum(['include', 'exclude', 'only']).default('only').describe("Specify whether to include, exclude, or only show TTM data."), + period: z.enum(['q1', 'q2', 'q3', 'q4', 'fy', 'annual', 'quarter']).default('annual').describe('Specify the fiscal period for the data.'), + limit: z.number().int().nullable().default(null).describe('Number of most recent reporting periods to return.'), +}) + +export type FMPKeyMetricsQueryParams = z.infer + +// --- Data --- + +const ALIAS_DICT: Record = { + period_ending: 'date', + fiscal_period: 'period', + currency: 'reportedCurrency', +} + +const TTM_ALIAS_DICT: Record = { + period_ending: 'date', + fiscal_period: 'fiscal_period', + currency: 'reportedCurrency', + enterprise_value: 'enterpriseValueTTM', + ev_to_sales: 'evToSalesTTM', + ev_to_ebitda: 'evToEBITDATTM', + return_on_equity: 'returnOnEquityTTM', + return_on_assets: 'returnOnAssetsTTM', + return_on_invested_capital: 'returnOnInvestedCapitalTTM', + current_ratio: 'currentRatioTTM', +} + +export const FMPKeyMetricsDataSchema = KeyMetricsDataSchema.extend({ + enterprise_value: z.number().nullable().default(null).describe('Enterprise Value.'), + ev_to_sales: z.number().nullable().default(null).describe('Enterprise Value to Sales ratio.'), + ev_to_ebitda: z.number().nullable().default(null).describe('Enterprise Value to EBITDA ratio.'), + return_on_equity: z.number().nullable().default(null).describe('Return on Equity.'), + return_on_assets: z.number().nullable().default(null).describe('Return on Assets.'), + return_on_invested_capital: z.number().nullable().default(null).describe('Return on Invested Capital.'), + current_ratio: z.number().nullable().default(null).describe('Current Ratio.'), +}).passthrough() + +export type FMPKeyMetricsData = z.infer + +// --- Fetcher --- + +export class FMPKeyMetricsFetcher extends Fetcher { + static override transformQuery(params: Record): FMPKeyMetricsQueryParams { + return FMPKeyMetricsQueryParamsSchema.parse(params) + } + + static override async extractData( + query: FMPKeyMetricsQueryParams, + credentials: Record | null, + ): Promise[]> { + const apiKey = credentials?.fmp_api_key ?? '' + const symbols = query.symbol.split(',') + const results: Record[] = [] + const baseUrl = 'https://financialmodelingprep.com/stable/key-metrics' + const limit = query.limit && query.ttm !== 'only' ? query.limit : 1 + + const getOne = async (symbol: string) => { + try { + const ttmUrl = `${baseUrl}-ttm?symbol=${symbol}&apikey=${apiKey}` + const metricsUrl = `${baseUrl}?symbol=${symbol}&period=${query.period}&limit=${limit}&apikey=${apiKey}` + + const [ttmData, metricsData] = await Promise.all([ + getDataMany(ttmUrl).catch(() => []), + getDataMany(metricsUrl).catch(() => []), + ]) + + const result: Record[] = [] + let currency: string | null = null + + if (metricsData.length > 0) { + if (query.ttm !== 'only') { + result.push(...metricsData) + } + currency = metricsData[0].reportedCurrency as string ?? null + } + + if (ttmData.length > 0 && query.ttm !== 'exclude') { + const ttmResult = { ...ttmData[0] } + ttmResult.date = new Date().toISOString().split('T')[0] + ttmResult.fiscal_period = 'TTM' + ttmResult.fiscal_year = new Date().getFullYear() + if (currency) ttmResult.reportedCurrency = currency + result.unshift(ttmResult) + } + + if (result.length > 0) { + results.push(...result) + } else { + console.warn(`Symbol Error: No data found for ${symbol}.`) + } + } catch { + console.warn(`Symbol Error: No data found for ${symbol}.`) + } + } + + await Promise.all(symbols.map(getOne)) + + if (results.length === 0) { + throw new EmptyDataError('No data found for given symbols.') + } + + return results + } + + static override transformData( + query: FMPKeyMetricsQueryParams, + data: Record[], + ): FMPKeyMetricsData[] { + const sorted = [...data].sort((a, b) => + String(b.date ?? '').localeCompare(String(a.date ?? '')), + ) + + return sorted.map((d) => { + const isTTM = d.fiscal_period === 'TTM' + const aliased = applyAliases(d, isTTM ? TTM_ALIAS_DICT : ALIAS_DICT) + return FMPKeyMetricsDataSchema.parse(aliased) + }) + } +} diff --git a/packages/opentypebb/src/providers/fmp/models/losers.ts b/packages/opentypebb/src/providers/fmp/models/losers.ts new file mode 100644 index 00000000..fc4e8891 --- /dev/null +++ b/packages/opentypebb/src/providers/fmp/models/losers.ts @@ -0,0 +1,49 @@ +/** + * FMP Top Losers Model. + * Maps to: openbb_fmp/models/equity_losers.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { EquityPerformanceQueryParamsSchema, EquityPerformanceDataSchema } from '../../../standard-models/equity-performance.js' +import { applyAliases } from '../../../core/provider/utils/helpers.js' +import { getDataMany } from '../utils/helpers.js' + +const ALIAS_DICT: Record = { percent_change: 'changesPercentage' } + +export const FMPLosersQueryParamsSchema = EquityPerformanceQueryParamsSchema +export type FMPLosersQueryParams = z.infer + +export const FMPLosersDataSchema = EquityPerformanceDataSchema.extend({ + exchange: z.string().describe('Stock exchange where the security is listed.'), +}).passthrough() +export type FMPLosersData = z.infer + +export class FMPLosersFetcher extends Fetcher { + static override transformQuery(params: Record): FMPLosersQueryParams { + return FMPLosersQueryParamsSchema.parse(params) + } + + static override async extractData( + query: FMPLosersQueryParams, + credentials: Record | null, + ): Promise[]> { + const apiKey = credentials?.fmp_api_key ?? '' + return getDataMany(`https://financialmodelingprep.com/stable/biggest-losers?apikey=${apiKey}`) + } + + static override transformData( + query: FMPLosersQueryParams, + data: Record[], + ): FMPLosersData[] { + const sorted = [...data].sort((a, b) => { + const diff = Number(b.changesPercentage ?? 0) - Number(a.changesPercentage ?? 0) + return query.sort === 'desc' ? diff : -diff + }) + return sorted.map((d) => { + const aliased = applyAliases(d, ALIAS_DICT) + if (typeof aliased.percent_change === 'number') aliased.percent_change = aliased.percent_change / 100 + return FMPLosersDataSchema.parse(aliased) + }) + } +} diff --git a/packages/opentypebb/src/providers/fmp/models/market-snapshots.ts b/packages/opentypebb/src/providers/fmp/models/market-snapshots.ts new file mode 100644 index 00000000..b7852fbd --- /dev/null +++ b/packages/opentypebb/src/providers/fmp/models/market-snapshots.ts @@ -0,0 +1,117 @@ +/** + * FMP Market Snapshots Model. + * Maps to: openbb_fmp/models/market_snapshots.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { MarketSnapshotsQueryParamsSchema, MarketSnapshotsDataSchema } from '../../../standard-models/market-snapshots.js' +import { applyAliases } from '../../../core/provider/utils/helpers.js' +import { getDataMany } from '../utils/helpers.js' +import { EmptyDataError } from '../../../core/provider/utils/errors.js' + +const ALIAS_DICT: Record = { + high: 'dayHigh', + low: 'dayLow', + prev_close: 'previousClose', + change_percent: 'changePercentage', + close: 'price', + last_price_timestamp: 'timestamp', + ma50: 'priceAvg50', + ma200: 'priceAvg200', + year_high: 'yearHigh', + year_low: 'yearLow', + market_cap: 'marketCap', +} + +const numOrNull = z.number().nullable().default(null) + +export const FMPMarketSnapshotsQueryParamsSchema = MarketSnapshotsQueryParamsSchema.extend({ + market: z.string().default('nasdaq').describe('The market to fetch data for (e.g., nasdaq, nyse, etf, crypto, forex, index, commodity, mutual_fund).'), +}) +export type FMPMarketSnapshotsQueryParams = z.infer + +export const FMPMarketSnapshotsDataSchema = MarketSnapshotsDataSchema.extend({ + ma50: numOrNull.describe('The 50-day moving average.'), + ma200: numOrNull.describe('The 200-day moving average.'), + year_high: numOrNull.describe('The 52-week high.'), + year_low: numOrNull.describe('The 52-week low.'), + market_cap: numOrNull.describe('Market cap of the stock.'), + last_price_timestamp: z.string().nullable().default(null).describe('The timestamp of the last price.'), +}).passthrough() +export type FMPMarketSnapshotsData = z.infer + +export class FMPMarketSnapshotsFetcher extends Fetcher { + static override transformQuery(params: Record): FMPMarketSnapshotsQueryParams { + return FMPMarketSnapshotsQueryParamsSchema.parse(params) + } + + static override async extractData( + query: FMPMarketSnapshotsQueryParams, + credentials: Record | null, + ): Promise[]> { + const apiKey = credentials?.fmp_api_key ?? '' + const baseUrl = 'https://financialmodelingprep.com/stable/batch-' + const market = query.market.toUpperCase() + + let url: string + if (market === 'ETF') { + url = `${baseUrl}etf-quotes?short=false&apikey=${apiKey}` + } else if (market === 'MUTUAL_FUND') { + url = `${baseUrl}mutualfund-quotes?short=false&apikey=${apiKey}` + } else if (market === 'FOREX') { + url = `${baseUrl}forex-quotes?short=false&apikey=${apiKey}` + } else if (market === 'CRYPTO') { + url = `${baseUrl}crypto-quotes?short=false&apikey=${apiKey}` + } else if (market === 'INDEX') { + url = `${baseUrl}index-quotes?short=false&apikey=${apiKey}` + } else if (market === 'COMMODITY') { + url = `${baseUrl}commodity-quotes?short=false&apikey=${apiKey}` + } else { + url = `${baseUrl}exchange-quote?exchange=${market}&short=false&apikey=${apiKey}` + } + + return getDataMany(url) + } + + static override transformData( + _query: FMPMarketSnapshotsQueryParams, + data: Record[], + ): FMPMarketSnapshotsData[] { + if (!data || data.length === 0) { + throw new EmptyDataError('No data was returned') + } + + // Filter to most recent day only (based on timestamp) + const withTimestamps = data.filter(d => typeof d.timestamp === 'number') + if (withTimestamps.length > 0) { + const maxTs = Math.max(...withTimestamps.map(d => d.timestamp as number)) + const maxDate = new Date(maxTs * 1000).toISOString().split('T')[0] + data = data.filter(d => { + if (typeof d.timestamp !== 'number') return false + const itemDate = new Date((d.timestamp as number) * 1000).toISOString().split('T')[0] + return itemDate === maxDate + }) + } + + // Sort by timestamp descending + data.sort((a, b) => ((b.timestamp as number) ?? 0) - ((a.timestamp as number) ?? 0)) + + return data.map(d => { + // Normalize change_percent + if (typeof d.changePercentage === 'number') { + d.changePercentage = d.changePercentage / 100 + } + // Convert Unix timestamp to ISO string + if (typeof d.timestamp === 'number') { + d.timestamp = new Date(d.timestamp * 1000).toISOString() + } + // Clean empty name strings + if (d.name === '' || d.name === "''" || d.name === ' ') { + d.name = null + } + const aliased = applyAliases(d, ALIAS_DICT) + return FMPMarketSnapshotsDataSchema.parse(aliased) + }) + } +} diff --git a/packages/opentypebb/src/providers/fmp/models/price-performance.ts b/packages/opentypebb/src/providers/fmp/models/price-performance.ts new file mode 100644 index 00000000..a2bc31cc --- /dev/null +++ b/packages/opentypebb/src/providers/fmp/models/price-performance.ts @@ -0,0 +1,77 @@ +/** + * FMP Price Performance Model. + * Maps to: openbb_fmp/models/price_performance.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { RecentPerformanceQueryParamsSchema, RecentPerformanceDataSchema } from '../../../standard-models/recent-performance.js' +import { applyAliases } from '../../../core/provider/utils/helpers.js' +import { getDataMany } from '../utils/helpers.js' + +const ALIAS_DICT: Record = { + one_day: '1D', + one_week: '5D', + one_month: '1M', + three_month: '3M', + six_month: '6M', + one_year: '1Y', + three_year: '3Y', + five_year: '5Y', + ten_year: '10Y', +} + +export const FMPPricePerformanceQueryParamsSchema = RecentPerformanceQueryParamsSchema +export type FMPPricePerformanceQueryParams = z.infer + +export const FMPPricePerformanceDataSchema = RecentPerformanceDataSchema +export type FMPPricePerformanceData = z.infer + +export class FMPPricePerformanceFetcher extends Fetcher { + static override transformQuery(params: Record): FMPPricePerformanceQueryParams { + return FMPPricePerformanceQueryParamsSchema.parse(params) + } + + static override async extractData( + query: FMPPricePerformanceQueryParams, + credentials: Record | null, + ): Promise[]> { + const apiKey = credentials?.fmp_api_key ?? '' + const symbols = query.symbol.toUpperCase().split(',') + // Chunk by 200 (FMP limit) + const chunkSize = 200 + const allResults: Record[] = [] + for (let i = 0; i < symbols.length; i += chunkSize) { + const chunk = symbols.slice(i, i + chunkSize) + const url = `https://financialmodelingprep.com/stable/stock-price-change?symbol=${chunk.join(',')}&apikey=${apiKey}` + try { + const data = await getDataMany(url) + allResults.push(...data) + } catch { + // If a chunk fails, continue with others + } + } + if (allResults.length === 0) { + return getDataMany( + `https://financialmodelingprep.com/stable/stock-price-change?symbol=${query.symbol.toUpperCase()}&apikey=${apiKey}`, + ) + } + return allResults + } + + static override transformData( + _query: FMPPricePerformanceQueryParams, + data: Record[], + ): FMPPricePerformanceData[] { + return data.map(d => { + // Replace zero with null and convert percents to normalized values + for (const [key, value] of Object.entries(d)) { + if (key !== 'symbol') { + d[key] = value === 0 ? null : typeof value === 'number' ? value / 100 : value + } + } + const aliased = applyAliases(d, ALIAS_DICT) + return FMPPricePerformanceDataSchema.parse(aliased) + }) + } +} diff --git a/packages/opentypebb/src/providers/fmp/models/price-target-consensus.ts b/packages/opentypebb/src/providers/fmp/models/price-target-consensus.ts new file mode 100644 index 00000000..aa2ebc1a --- /dev/null +++ b/packages/opentypebb/src/providers/fmp/models/price-target-consensus.ts @@ -0,0 +1,76 @@ +/** + * FMP Price Target Consensus Model. + * Maps to: openbb_fmp/models/price_target_consensus.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { PriceTargetConsensusQueryParamsSchema, PriceTargetConsensusDataSchema } from '../../../standard-models/price-target-consensus.js' +import { applyAliases, amakeRequest } from '../../../core/provider/utils/helpers.js' +import { OpenBBError, EmptyDataError } from '../../../core/provider/utils/errors.js' +import { responseCallback } from '../utils/helpers.js' + +// --- Query Params --- + +export const FMPPriceTargetConsensusQueryParamsSchema = PriceTargetConsensusQueryParamsSchema + +export type FMPPriceTargetConsensusQueryParams = z.infer + +// --- Data --- + +export const FMPPriceTargetConsensusDataSchema = PriceTargetConsensusDataSchema + +export type FMPPriceTargetConsensusData = z.infer + +// --- Fetcher --- + +export class FMPPriceTargetConsensusFetcher extends Fetcher { + static override transformQuery(params: Record): FMPPriceTargetConsensusQueryParams { + if (!params.symbol) { + throw new OpenBBError('Symbol is a required field for FMP.') + } + return FMPPriceTargetConsensusQueryParamsSchema.parse(params) + } + + static override async extractData( + query: FMPPriceTargetConsensusQueryParams, + credentials: Record | null, + ): Promise[]> { + const apiKey = credentials?.fmp_api_key ?? '' + const symbols = (query.symbol ?? '').split(',') + const results: Record[] = [] + + const getOne = async (symbol: string) => { + const url = `https://financialmodelingprep.com/stable/price-target-consensus?symbol=${symbol}&apikey=${apiKey}` + try { + const result = await amakeRequest[]>(url, { responseCallback }) + if (result && result.length > 0) { + results.push(...result) + } else { + console.warn(`Symbol Error: No data found for ${symbol}`) + } + } catch { + console.warn(`Symbol Error: No data found for ${symbol}`) + } + } + + await Promise.all(symbols.map(getOne)) + + if (results.length === 0) { + throw new EmptyDataError('No data returned for the given symbols.') + } + + return results.sort((a, b) => { + const ai = symbols.indexOf(String(a.symbol ?? '')) + const bi = symbols.indexOf(String(b.symbol ?? '')) + return ai - bi + }) + } + + static override transformData( + query: FMPPriceTargetConsensusQueryParams, + data: Record[], + ): FMPPriceTargetConsensusData[] { + return data.map((d) => FMPPriceTargetConsensusDataSchema.parse(d)) + } +} diff --git a/packages/opentypebb/src/providers/fmp/models/price-target.ts b/packages/opentypebb/src/providers/fmp/models/price-target.ts new file mode 100644 index 00000000..2f47327e --- /dev/null +++ b/packages/opentypebb/src/providers/fmp/models/price-target.ts @@ -0,0 +1,62 @@ +/** + * FMP Price Target Model. + * Maps to: openbb_fmp/models/price_target.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { PriceTargetQueryParamsSchema, PriceTargetDataSchema } from '../../../standard-models/price-target.js' +import { applyAliases } from '../../../core/provider/utils/helpers.js' +import { getDataMany } from '../utils/helpers.js' + +// --- Query Params --- + +export const FMPPriceTargetQueryParamsSchema = PriceTargetQueryParamsSchema.extend({}) +export type FMPPriceTargetQueryParams = z.infer + +// --- Data --- + +const ALIAS_DICT: Record = { + analyst_firm: 'analystCompany', + rating_current: 'newGrade', + rating_previous: 'previousGrade', + news_title: 'newsTitle', + news_url: 'newsURL', +} + +export const FMPPriceTargetDataSchema = PriceTargetDataSchema.extend({ + news_title: z.string().nullable().default(null).describe('Title of the associated news.'), + news_url: z.string().nullable().default(null).describe('URL of the associated news.'), +}).passthrough() + +export type FMPPriceTargetData = z.infer + +// --- Fetcher --- + +export class FMPPriceTargetFetcher extends Fetcher { + static override transformQuery(params: Record): FMPPriceTargetQueryParams { + return FMPPriceTargetQueryParamsSchema.parse(params) + } + + static override async extractData( + query: FMPPriceTargetQueryParams, + credentials: Record | null, + ): Promise[]> { + const apiKey = credentials?.fmp_api_key ?? '' + const url = 'https://financialmodelingprep.com/stable/price-target' + + `?symbol=${query.symbol}` + + (query.limit ? `&limit=${query.limit}` : '') + + `&apikey=${apiKey}` + return getDataMany(url) + } + + static override transformData( + query: FMPPriceTargetQueryParams, + data: Record[], + ): FMPPriceTargetData[] { + return data.map(d => { + const aliased = applyAliases(d, ALIAS_DICT) + return FMPPriceTargetDataSchema.parse(aliased) + }) + } +} diff --git a/packages/opentypebb/src/providers/fmp/models/revenue-business-line.ts b/packages/opentypebb/src/providers/fmp/models/revenue-business-line.ts new file mode 100644 index 00000000..71b7fedf --- /dev/null +++ b/packages/opentypebb/src/providers/fmp/models/revenue-business-line.ts @@ -0,0 +1,79 @@ +/** + * FMP Revenue By Business Line Model. + * Maps to: openbb_fmp/models/revenue_business_line.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { RevenueBusinessLineQueryParamsSchema, RevenueBusinessLineDataSchema } from '../../../standard-models/revenue-business-line.js' +import { getDataMany } from '../utils/helpers.js' +import { EmptyDataError } from '../../../core/provider/utils/errors.js' + +export const FMPRevenueBusinessLineQueryParamsSchema = RevenueBusinessLineQueryParamsSchema.extend({ + period: z.enum(['quarter', 'annual']).default('annual').describe('Fiscal period.'), +}) +export type FMPRevenueBusinessLineQueryParams = z.infer + +export const FMPRevenueBusinessLineDataSchema = RevenueBusinessLineDataSchema +export type FMPRevenueBusinessLineData = z.infer + +export class FMPRevenueBusinessLineFetcher extends Fetcher { + static override transformQuery(params: Record): FMPRevenueBusinessLineQueryParams { + return FMPRevenueBusinessLineQueryParamsSchema.parse(params) + } + + static override async extractData( + query: FMPRevenueBusinessLineQueryParams, + credentials: Record | null, + ): Promise[]> { + const apiKey = credentials?.fmp_api_key ?? '' + return getDataMany( + `https://financialmodelingprep.com/stable/revenue-product-segmentation?symbol=${query.symbol}&period=${query.period}&structure=flat&apikey=${apiKey}`, + ) + } + + static override transformData( + _query: FMPRevenueBusinessLineQueryParams, + data: Record[], + ): FMPRevenueBusinessLineData[] { + if (!data || data.length === 0) { + throw new EmptyDataError('The request was returned empty.') + } + + const results: FMPRevenueBusinessLineData[] = [] + + for (const item of data) { + const periodEnding = item.date as string | undefined + const fiscalYear = item.fiscalYear as number | undefined + const fiscalPeriod = item.period as string | undefined + const segment = (item.data ?? {}) as Record + + for (const [businessLine, revenueValue] of Object.entries(segment)) { + if (revenueValue != null) { + const revenue = Number(revenueValue) + if (!isNaN(revenue)) { + results.push( + FMPRevenueBusinessLineDataSchema.parse({ + period_ending: periodEnding, + fiscal_year: fiscalYear, + fiscal_period: fiscalPeriod, + business_line: businessLine.trim(), + revenue, + }), + ) + } + } + } + } + + if (results.length === 0) { + throw new EmptyDataError('Unknown error while transforming the data.') + } + + return results.sort((a, b) => { + const dateComp = String(a.period_ending ?? '').localeCompare(String(b.period_ending ?? '')) + if (dateComp !== 0) return dateComp + return (a.revenue ?? 0) - (b.revenue ?? 0) + }) + } +} diff --git a/packages/opentypebb/src/providers/fmp/models/revenue-geographic.ts b/packages/opentypebb/src/providers/fmp/models/revenue-geographic.ts new file mode 100644 index 00000000..a2800d13 --- /dev/null +++ b/packages/opentypebb/src/providers/fmp/models/revenue-geographic.ts @@ -0,0 +1,79 @@ +/** + * FMP Revenue Geographic Model. + * Maps to: openbb_fmp/models/revenue_geographic.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { RevenueGeographicQueryParamsSchema, RevenueGeographicDataSchema } from '../../../standard-models/revenue-geographic.js' +import { getDataMany } from '../utils/helpers.js' +import { EmptyDataError } from '../../../core/provider/utils/errors.js' + +export const FMPRevenueGeographicQueryParamsSchema = RevenueGeographicQueryParamsSchema.extend({ + period: z.enum(['quarter', 'annual']).default('annual').describe('Fiscal period.'), +}) +export type FMPRevenueGeographicQueryParams = z.infer + +export const FMPRevenueGeographicDataSchema = RevenueGeographicDataSchema +export type FMPRevenueGeographicData = z.infer + +export class FMPRevenueGeographicFetcher extends Fetcher { + static override transformQuery(params: Record): FMPRevenueGeographicQueryParams { + return FMPRevenueGeographicQueryParamsSchema.parse(params) + } + + static override async extractData( + query: FMPRevenueGeographicQueryParams, + credentials: Record | null, + ): Promise[]> { + const apiKey = credentials?.fmp_api_key ?? '' + return getDataMany( + `https://financialmodelingprep.com/stable/revenue-geographic-segmentation?symbol=${query.symbol}&period=${query.period}&structure=flat&apikey=${apiKey}`, + ) + } + + static override transformData( + _query: FMPRevenueGeographicQueryParams, + data: Record[], + ): FMPRevenueGeographicData[] { + if (!data || data.length === 0) { + throw new EmptyDataError('The request was returned empty.') + } + + const results: FMPRevenueGeographicData[] = [] + + for (const item of data) { + const periodEnding = item.date as string | undefined + const fiscalYear = item.fiscalYear as number | undefined + const fiscalPeriod = item.period as string | undefined + const segment = (item.data ?? {}) as Record + + for (const [region, revenueValue] of Object.entries(segment)) { + if (revenueValue != null) { + const revenue = Number(revenueValue) + if (!isNaN(revenue)) { + results.push( + FMPRevenueGeographicDataSchema.parse({ + period_ending: periodEnding, + fiscal_year: fiscalYear, + fiscal_period: fiscalPeriod, + region: region.replace('Segment', '').trim(), + revenue, + }), + ) + } + } + } + } + + if (results.length === 0) { + throw new EmptyDataError('Unknown error while transforming the data.') + } + + return results.sort((a, b) => { + const dateComp = String(a.period_ending ?? '').localeCompare(String(b.period_ending ?? '')) + if (dateComp !== 0) return dateComp + return (a.revenue ?? 0) - (b.revenue ?? 0) + }) + } +} diff --git a/packages/opentypebb/src/providers/fmp/models/risk-premium.ts b/packages/opentypebb/src/providers/fmp/models/risk-premium.ts new file mode 100644 index 00000000..855dbf74 --- /dev/null +++ b/packages/opentypebb/src/providers/fmp/models/risk-premium.ts @@ -0,0 +1,38 @@ +/** + * FMP Risk Premium Model. + * Maps to: openbb_fmp/models/risk_premium.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { RiskPremiumQueryParamsSchema, RiskPremiumDataSchema } from '../../../standard-models/risk-premium.js' +import { getDataMany } from '../utils/helpers.js' + +export const FMPRiskPremiumQueryParamsSchema = RiskPremiumQueryParamsSchema +export type FMPRiskPremiumQueryParams = z.infer + +export const FMPRiskPremiumDataSchema = RiskPremiumDataSchema +export type FMPRiskPremiumData = z.infer + +export class FMPRiskPremiumFetcher extends Fetcher { + static override transformQuery(params: Record): FMPRiskPremiumQueryParams { + return FMPRiskPremiumQueryParamsSchema.parse(params) + } + + static override async extractData( + _query: FMPRiskPremiumQueryParams, + credentials: Record | null, + ): Promise[]> { + const apiKey = credentials?.fmp_api_key ?? '' + return getDataMany( + `https://financialmodelingprep.com/stable/market-risk-premium?apikey=${apiKey}`, + ) + } + + static override transformData( + _query: FMPRiskPremiumQueryParams, + data: Record[], + ): FMPRiskPremiumData[] { + return data.map(d => FMPRiskPremiumDataSchema.parse(d)) + } +} diff --git a/packages/opentypebb/src/providers/fmp/models/share-statistics.ts b/packages/opentypebb/src/providers/fmp/models/share-statistics.ts new file mode 100644 index 00000000..89f9d313 --- /dev/null +++ b/packages/opentypebb/src/providers/fmp/models/share-statistics.ts @@ -0,0 +1,55 @@ +/** + * FMP Share Statistics Model. + * Maps to: openbb_fmp/models/share_statistics.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { ShareStatisticsQueryParamsSchema, ShareStatisticsDataSchema } from '../../../standard-models/share-statistics.js' +import { applyAliases } from '../../../core/provider/utils/helpers.js' +import { getDataMany } from '../utils/helpers.js' + +const ALIAS_DICT: Record = { + url: 'source', +} + +export const FMPShareStatisticsQueryParamsSchema = ShareStatisticsQueryParamsSchema +export type FMPShareStatisticsQueryParams = z.infer + +export const FMPShareStatisticsDataSchema = ShareStatisticsDataSchema.extend({ + url: z.string().nullable().default(null).describe('URL to the source filing.'), +}).passthrough() +export type FMPShareStatisticsData = z.infer + +export class FMPShareStatisticsFetcher extends Fetcher { + static override transformQuery(params: Record): FMPShareStatisticsQueryParams { + return FMPShareStatisticsQueryParamsSchema.parse(params) + } + + static override async extractData( + query: FMPShareStatisticsQueryParams, + credentials: Record | null, + ): Promise[]> { + const apiKey = credentials?.fmp_api_key ?? '' + return getDataMany( + `https://financialmodelingprep.com/stable/shares-float?symbol=${query.symbol}&apikey=${apiKey}`, + ) + } + + static override transformData( + _query: FMPShareStatisticsQueryParams, + data: Record[], + ): FMPShareStatisticsData[] { + return data.map(d => { + // Normalize free_float from percent to decimal + if (typeof d.freeFloat === 'number') { + d.freeFloat = d.freeFloat / 100 + } + if (typeof d.free_float === 'number') { + d.free_float = d.free_float / 100 + } + const aliased = applyAliases(d, ALIAS_DICT) + return FMPShareStatisticsDataSchema.parse(aliased) + }) + } +} diff --git a/packages/opentypebb/src/providers/fmp/models/treasury-rates.ts b/packages/opentypebb/src/providers/fmp/models/treasury-rates.ts new file mode 100644 index 00000000..8b3d1498 --- /dev/null +++ b/packages/opentypebb/src/providers/fmp/models/treasury-rates.ts @@ -0,0 +1,105 @@ +/** + * FMP Treasury Rates Model. + * Maps to: openbb_fmp/models/treasury_rates.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { TreasuryRatesQueryParamsSchema, TreasuryRatesDataSchema } from '../../../standard-models/treasury-rates.js' +import { applyAliases } from '../../../core/provider/utils/helpers.js' +import { getDataUrls } from '../utils/helpers.js' + +const ALIAS_DICT: Record = { + month_1: 'month1', + month_2: 'month2', + month_3: 'month3', + month_6: 'month6', + year_1: 'year1', + year_2: 'year2', + year_3: 'year3', + year_5: 'year5', + year_7: 'year7', + year_10: 'year10', + year_20: 'year20', + year_30: 'year30', +} + +export const FMPTreasuryRatesQueryParamsSchema = TreasuryRatesQueryParamsSchema +export type FMPTreasuryRatesQueryParams = z.infer + +export const FMPTreasuryRatesDataSchema = TreasuryRatesDataSchema +export type FMPTreasuryRatesData = z.infer + +/** + * Generate URLs for each 3-month interval between start and end dates. + */ +function generateUrls(startDate: string, endDate: string, apiKey: string): string[] { + const urls: string[] = [] + const start = new Date(startDate) + const end = new Date(endDate) + + let current = new Date(start) + while (current <= end) { + const next = new Date(current) + next.setMonth(next.getMonth() + 3) + const to = next > end ? end : next + const fromStr = current.toISOString().split('T')[0] + const toStr = to.toISOString().split('T')[0] + urls.push( + `https://financialmodelingprep.com/stable/treasury-rates?from=${fromStr}&to=${toStr}&apikey=${apiKey}`, + ) + current = next + } + return urls +} + +export class FMPTreasuryRatesFetcher extends Fetcher { + static override transformQuery(params: Record): FMPTreasuryRatesQueryParams { + // Default start_date to 1 year ago, end_date to today + const now = new Date() + if (!params.start_date) { + const oneYearAgo = new Date(now) + oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1) + params.start_date = oneYearAgo.toISOString().split('T')[0] + } + if (!params.end_date) { + params.end_date = now.toISOString().split('T')[0] + } + return FMPTreasuryRatesQueryParamsSchema.parse(params) + } + + static override async extractData( + query: FMPTreasuryRatesQueryParams, + credentials: Record | null, + ): Promise[]> { + const apiKey = credentials?.fmp_api_key ?? '' + const urls = generateUrls(query.start_date!, query.end_date!, apiKey) + const chunks = await getDataUrls[]>(urls) + // Flatten all chunks into a single array + const results: Record[] = [] + for (const chunk of chunks) { + if (Array.isArray(chunk)) { + results.push(...chunk) + } + } + return results + } + + static override transformData( + _query: FMPTreasuryRatesQueryParams, + data: Record[], + ): FMPTreasuryRatesData[] { + return data + .map(d => { + // Normalize percent values (e.g. 4.5 -> 0.045) + for (const [key, value] of Object.entries(d)) { + if (key !== 'date' && typeof value === 'number') { + d[key] = value / 100 + } + } + const aliased = applyAliases(d, ALIAS_DICT) + return FMPTreasuryRatesDataSchema.parse(aliased) + }) + .sort((a, b) => String(a.date).localeCompare(String(b.date))) + } +} diff --git a/packages/opentypebb/src/providers/fmp/models/world-news.ts b/packages/opentypebb/src/providers/fmp/models/world-news.ts new file mode 100644 index 00000000..91039ae5 --- /dev/null +++ b/packages/opentypebb/src/providers/fmp/models/world-news.ts @@ -0,0 +1,80 @@ +/** + * FMP World News Model. + * Maps to: openbb_fmp/models/world_news.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { WorldNewsQueryParamsSchema, WorldNewsDataSchema } from '../../../standard-models/world-news.js' +import { applyAliases } from '../../../core/provider/utils/helpers.js' +import { EmptyDataError } from '../../../core/provider/utils/errors.js' +import { getDataMany } from '../utils/helpers.js' + +// --- Query Params --- + +export const FMPWorldNewsQueryParamsSchema = WorldNewsQueryParamsSchema.extend({ + topic: z.enum(['fmp_articles', 'general', 'press_releases', 'stocks', 'forex', 'crypto']).default('general').describe('The topic of the news to be fetched.'), + page: z.number().int().min(0).max(100).nullable().default(null).describe('Page number of the results.'), +}) + +export type FMPWorldNewsQueryParams = z.infer + +// --- Data --- + +const ALIAS_DICT: Record = { + date: 'publishedDate', + images: 'image', + excerpt: 'text', + source: 'site', + author: 'publisher', + symbols: 'symbol', +} + +export const FMPWorldNewsDataSchema = WorldNewsDataSchema.extend({ + source: z.string().describe('News source.'), +}).passthrough() + +export type FMPWorldNewsData = z.infer + +// --- Fetcher --- + +export class FMPWorldNewsFetcher extends Fetcher { + static override transformQuery(params: Record): FMPWorldNewsQueryParams { + return FMPWorldNewsQueryParamsSchema.parse(params) + } + + static override async extractData( + query: FMPWorldNewsQueryParams, + credentials: Record | null, + ): Promise[]> { + const apiKey = credentials?.fmp_api_key ?? '' + const baseUrl = 'https://financialmodelingprep.com/stable/' + let url: string + + if (query.topic === 'fmp_articles') { + url = `${baseUrl}news/fmp-articles?page=${query.page ?? 0}&limit=${query.limit ?? 20}&apikey=${apiKey}` + } else { + const topicPath = query.topic.replace(/_/g, '-') + url = `${baseUrl}news/${topicPath}-latest?from=${query.start_date ?? ''}&to=${query.end_date ?? ''}&limit=${query.limit ?? 250}&page=${query.page ?? 0}&apikey=${apiKey}` + } + + const results = await getDataMany(url) + + return results.sort((a, b) => + String(b.publishedDate ?? '').localeCompare(String(a.publishedDate ?? '')), + ) + } + + static override transformData( + query: FMPWorldNewsQueryParams, + data: Record[], + ): FMPWorldNewsData[] { + if (!data || data.length === 0) { + throw new EmptyDataError('No data was returned from FMP query.') + } + return data.map((d) => { + const aliased = applyAliases(d, ALIAS_DICT) + return FMPWorldNewsDataSchema.parse(aliased) + }) + } +} diff --git a/packages/opentypebb/src/providers/fmp/utils/definitions.ts b/packages/opentypebb/src/providers/fmp/utils/definitions.ts new file mode 100644 index 00000000..af96c52e --- /dev/null +++ b/packages/opentypebb/src/providers/fmp/utils/definitions.ts @@ -0,0 +1,35 @@ +/** + * FMP Literal Definitions. + * Maps to: openbb_fmp/utils/definitions.py + */ + +export type FinancialPeriods = 'q1' | 'q2' | 'q3' | 'q4' | 'fy' | 'annual' | 'quarter' + +export type FinancialStatementPeriods = 'q1' | 'q2' | 'q3' | 'q4' | 'fy' | 'ttm' | 'annual' | 'quarter' + +export type TransactionType = + | 'award' | 'conversion' | 'return' | 'expire_short' | 'in_kind' + | 'gift' | 'expire_long' | 'discretionary' | 'other' | 'small' + | 'exempt' | 'otm' | 'purchase' | 'sale' | 'tender' | 'will' + | 'itm' | 'trust' + +export const TRANSACTION_TYPES_DICT: Record = { + award: 'A-Award', + conversion: 'C-Conversion', + return: 'D-Return', + expire_short: 'E-ExpireShort', + in_kind: 'F-InKind', + gift: 'G-Gift', + expire_long: 'H-ExpireLong', + discretionary: 'I-Discretionary', + other: 'J-Other', + small: 'L-Small', + exempt: 'M-Exempt', + otm: 'O-OutOfTheMoney', + purchase: 'P-Purchase', + sale: 'S-Sale', + tender: 'U-Tender', + will: 'W-Will', + itm: 'X-InTheMoney', + trust: 'Z-Trust', +} diff --git a/packages/opentypebb/src/providers/fmp/utils/helpers.ts b/packages/opentypebb/src/providers/fmp/utils/helpers.ts new file mode 100644 index 00000000..87b332fa --- /dev/null +++ b/packages/opentypebb/src/providers/fmp/utils/helpers.ts @@ -0,0 +1,291 @@ +/** + * FMP Helpers Module. + * Maps to: openbb_fmp/utils/helpers.py + */ + +import { OpenBBError, EmptyDataError, UnauthorizedError } from '../../../core/provider/utils/errors.js' +import { amakeRequest } from '../../../core/provider/utils/helpers.js' + +/** + * Response callback for FMP API requests. + * Maps to: response_callback() in helpers.py + */ +export async function responseCallback(response: Response): Promise { + if (response.status !== 200) { + const msg = await response.text().catch(() => '') + throw new UnauthorizedError(`Unauthorized FMP request -> ${response.status} -> ${msg}`) + } + + const data = await response.json() + + if (data && typeof data === 'object' && !Array.isArray(data)) { + const errorMessage = (data as Record)['Error Message'] ?? + (data as Record)['error'] + + if (errorMessage != null) { + const msg = String(errorMessage).toLowerCase() + const isUnauthorized = + msg.includes('upgrade') || + msg.includes('exclusive endpoint') || + msg.includes('special endpoint') || + msg.includes('premium query parameter') || + msg.includes('subscription') || + msg.includes('unauthorized') || + msg.includes('premium') + + if (isUnauthorized) { + throw new UnauthorizedError(`Unauthorized FMP request -> ${errorMessage}`) + } + + throw new OpenBBError( + `FMP Error Message -> Status code: ${response.status} -> ${errorMessage}`, + ) + } + } + + // Return a new Response with the already-parsed body + return new Response(JSON.stringify(data), { + status: response.status, + statusText: response.statusText, + headers: response.headers, + }) +} + +/** + * Get data from FMP endpoint. + * Maps to: get_data() in helpers.py + */ +export async function getData(url: string): Promise { + return amakeRequest(url, { responseCallback }) +} + +/** + * Get data from FMP for several urls. + * Maps to: get_data_urls() in helpers.py + */ +export async function getDataUrls(urls: string[]): Promise { + const results = await Promise.all( + urls.map((url) => amakeRequest(url, { responseCallback })), + ) + return results +} + +/** + * Get data from FMP endpoint and convert to list of dicts. + * Maps to: get_data_many() in helpers.py + */ +export async function getDataMany(url: string, subDict?: string): Promise[]> { + let data = await getData(url) + + if (subDict && data && typeof data === 'object' && !Array.isArray(data)) { + data = (data as Record)[subDict] ?? [] + } + + if (data && typeof data === 'object' && !Array.isArray(data)) { + throw new OpenBBError('Expected list of dicts, got dict') + } + + const arr = data as Record[] + if (!arr || arr.length === 0) { + throw new EmptyDataError() + } + + return arr +} + +/** + * Get data from FMP endpoint and convert to a single dict. + * Maps to: get_data_one() in helpers.py + */ +export async function getDataOne(url: string): Promise> { + let data = await getData(url) + + if (Array.isArray(data)) { + if (data.length === 0) { + throw new OpenBBError('Expected dict, got empty list') + } + data = data.length > 1 + ? Object.fromEntries(data.map((item, i) => [i, item])) + : data[0] + } + + return data as Record +} + +/** + * Create a URL for the FMP API. + * Maps to: create_url() in helpers.py + */ +export function createUrl( + version: number, + endpoint: string, + apiKey: string | null, + query?: Record, + exclude?: string[], +): string { + const params: Record = { ...(query ?? {}) } + const excludeSet = new Set(exclude ?? []) + + const searchParams = new URLSearchParams() + for (const [key, value] of Object.entries(params)) { + if (!excludeSet.has(key) && value !== null && value !== undefined) { + searchParams.set(key, String(value)) + } + } + + const queryString = searchParams.toString() + const baseUrl = `https://financialmodelingprep.com/api/v${version}/` + return `${baseUrl}${endpoint}?${queryString}&apikey=${apiKey ?? ''}` +} + +/** + * Get the FMP interval string. + * Maps to: get_interval() in helpers.py + */ +export function getInterval(value: string): string { + const intervals: Record = { + m: 'min', + h: 'hour', + d: 'day', + } + const suffix = value.slice(-1) + const num = value.slice(0, -1) + return `${num}${intervals[suffix] ?? suffix}` +} + +/** + * Get the most recent quarter date. + * Maps to: most_recent_quarter() in helpers.py + */ +export function mostRecentQuarter(base?: Date): Date { + const now = new Date() + let d = base ? new Date(Math.min(base.getTime(), now.getTime())) : new Date(now) + + const month = d.getMonth() + 1 // 1-indexed + const day = d.getDate() + + // Check exact quarter end dates + const exacts: [number, number][] = [[3, 31], [6, 30], [9, 30], [12, 31]] + for (const [m, dd] of exacts) { + if (month === m && day === dd) return d + } + + if (month < 4) return new Date(d.getFullYear() - 1, 11, 31) + if (month < 7) return new Date(d.getFullYear(), 2, 31) + if (month < 10) return new Date(d.getFullYear(), 5, 30) + return new Date(d.getFullYear(), 8, 30) +} + +/** + * Build query string from params, excluding specified keys. + * Maps to: get_querystring() in helpers.py + */ +export function getQueryString( + params: Record, + exclude: string[] = [], +): string { + const excludeSet = new Set(exclude) + const searchParams = new URLSearchParams() + + for (const [key, value] of Object.entries(params)) { + if (!excludeSet.has(key) && value !== null && value !== undefined) { + searchParams.set(key, String(value)) + } + } + + return searchParams.toString() +} + +/** + * Return the raw data from the FMP historical OHLC endpoint. + * Maps to: get_historical_ohlc() in helpers.py + */ +export async function getHistoricalOhlc( + query: { + symbol: string + interval: string + start_date?: string | null + end_date?: string | null + adjustment?: string + [key: string]: unknown + }, + credentials: Record | null, +): Promise[]> { + const apiKey = credentials?.fmp_api_key ?? '' + let baseUrl = 'https://financialmodelingprep.com/stable/' + + if (query.adjustment === 'unadjusted') { + baseUrl += 'historical-price-eod/non-split-adjusted?' + } else if (query.adjustment === 'splits_and_dividends') { + baseUrl += 'historical-price-eod/dividend-adjusted?' + } else if (query.interval === '1d') { + baseUrl += 'historical-price-eod/full?' + } else if (query.interval === '1m') { + baseUrl += 'historical-chart/1min?' + } else if (query.interval === '5m') { + baseUrl += 'historical-chart/5min?' + } else if (query.interval === '60m' || query.interval === '1h') { + baseUrl += 'historical-chart/1hour?' + } + + const queryParams = { ...query } + const excludeKeys = ['symbol', 'adjustment', 'interval'] + const queryStr = getQueryString(queryParams, excludeKeys) + const symbols = query.symbol.split(',') + + const results: Record[] = [] + const messages: string[] = [] + + const getOne = async (symbol: string) => { + const url = `${baseUrl}symbol=${symbol}&${queryStr}&apikey=${apiKey}` + + try { + const response = await amakeRequest(url, { responseCallback }) + + if (typeof response === 'object' && response !== null && !Array.isArray(response)) { + const dict = response as Record + if (dict['Error Message']) { + const message = `Error fetching data for ${symbol}: ${dict['Error Message']}` + console.warn(message) + messages.push(message) + return + } + + const historical = dict['historical'] as Record[] | undefined + if (historical && historical.length > 0) { + for (const d of historical) { + d.symbol = symbol + results.push(d) + } + return + } + } + + if (Array.isArray(response) && response.length > 0) { + for (const d of response as Record[]) { + d.symbol = symbol + results.push(d) + } + return + } + + const message = `No data found for ${symbol}.` + console.warn(message) + messages.push(message) + } catch (error) { + const message = `Error fetching data for ${symbol}: ${error}` + console.warn(message) + messages.push(message) + } + } + + await Promise.all(symbols.map(getOne)) + + if (results.length === 0) { + throw new EmptyDataError( + messages.length > 0 ? messages.join(' ') : 'No data found', + ) + } + + return results +} diff --git a/packages/opentypebb/src/providers/imf/index.ts b/packages/opentypebb/src/providers/imf/index.ts new file mode 100644 index 00000000..dc1af271 --- /dev/null +++ b/packages/opentypebb/src/providers/imf/index.ts @@ -0,0 +1,22 @@ +/** + * IMF Provider Module. + * Maps to: openbb_platform/providers/imf/openbb_imf/__init__.py + */ + +import { Provider } from '../../core/provider/abstract/provider.js' +import { IMFAvailableIndicatorsFetcher } from './models/available-indicators.js' +import { IMFConsumerPriceIndexFetcher } from './models/consumer-price-index.js' +import { IMFDirectionOfTradeFetcher } from './models/direction-of-trade.js' +import { IMFEconomicIndicatorsFetcher } from './models/economic-indicators.js' + +export const imfProvider = new Provider({ + name: 'imf', + website: 'https://data.imf.org', + description: 'International Monetary Fund data services.', + fetcherDict: { + AvailableIndicators: IMFAvailableIndicatorsFetcher, + ConsumerPriceIndex: IMFConsumerPriceIndexFetcher, + DirectionOfTrade: IMFDirectionOfTradeFetcher, + EconomicIndicators: IMFEconomicIndicatorsFetcher, + }, +}) diff --git a/packages/opentypebb/src/providers/imf/models/available-indicators.ts b/packages/opentypebb/src/providers/imf/models/available-indicators.ts new file mode 100644 index 00000000..85cdf12c --- /dev/null +++ b/packages/opentypebb/src/providers/imf/models/available-indicators.ts @@ -0,0 +1,58 @@ +/** + * IMF Available Indicators Model. + * Maps to: openbb_imf/models/available_indicators.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { AvailableIndicatorsDataSchema } from '../../../standard-models/available-indicators.js' +import { EmptyDataError } from '../../../core/provider/utils/errors.js' +import { nativeFetch } from '../../../core/provider/utils/helpers.js' + +export const IMFAvailableIndicatorsQueryParamsSchema = z.object({}).passthrough() +export type IMFAvailableIndicatorsQueryParams = z.infer +export type IMFAvailableIndicatorsData = z.infer + +export class IMFAvailableIndicatorsFetcher extends Fetcher { + static override requireCredentials = false + + static override transformQuery(params: Record): IMFAvailableIndicatorsQueryParams { + return IMFAvailableIndicatorsQueryParamsSchema.parse(params) + } + + static override async extractData( + _query: IMFAvailableIndicatorsQueryParams, + _credentials: Record | null, + ): Promise[]> { + // IMF Dataflow endpoint lists available datasets + const url = 'https://dataservices.imf.org/REST/SDMX_JSON.svc/Dataflow' + + try { + const resp = await nativeFetch(url, { timeoutMs: 30000 }) + if (resp.status !== 200) throw new EmptyDataError(`IMF API returned ${resp.status}`) + const data = JSON.parse(resp.text) as Record + const structure = data.Structure as Record + const dataflows = (structure?.Dataflows as Record)?.Dataflow as Record[] + + if (!Array.isArray(dataflows) || dataflows.length === 0) throw new EmptyDataError() + + return dataflows.map(df => ({ + symbol: (df.KeyFamilyRef as Record)?.KeyFamilyID ?? df['@id'] ?? '', + description: ((df.Name as Record)?.['#text'] ?? df.Name ?? '') as string, + })) + } catch (err) { + if (err instanceof EmptyDataError) throw err + throw new EmptyDataError(`Failed to fetch IMF indicators: ${err}`) + } + } + + static override transformData( + _query: IMFAvailableIndicatorsQueryParams, + data: Record[], + ): IMFAvailableIndicatorsData[] { + return data.map(d => AvailableIndicatorsDataSchema.parse({ + symbol: d.symbol ?? null, + description: d.description ?? null, + })) + } +} diff --git a/packages/opentypebb/src/providers/imf/models/consumer-price-index.ts b/packages/opentypebb/src/providers/imf/models/consumer-price-index.ts new file mode 100644 index 00000000..1a134909 --- /dev/null +++ b/packages/opentypebb/src/providers/imf/models/consumer-price-index.ts @@ -0,0 +1,95 @@ +/** + * IMF Consumer Price Index Model. + * Maps to: openbb_imf/models/consumer_price_index.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { ConsumerPriceIndexDataSchema } from '../../../standard-models/consumer-price-index.js' +import { EmptyDataError } from '../../../core/provider/utils/errors.js' +import { nativeFetch } from '../../../core/provider/utils/helpers.js' + +export const IMFCPIQueryParamsSchema = z.object({ + country: z.string().default('united_states').describe('The country to get data for.'), + transform: z.string().default('yoy').describe('Transformation: yoy, period, index.'), + frequency: z.enum(['annual', 'quarter', 'monthly']).default('monthly').describe('Data frequency.'), + harmonized: z.boolean().default(false).describe('If true, returns harmonized data.'), + start_date: z.string().nullable().default(null).describe('Start date in YYYY-MM-DD.'), + end_date: z.string().nullable().default(null).describe('End date in YYYY-MM-DD.'), +}).passthrough() + +export type IMFCPIQueryParams = z.infer +export type IMFCPIData = z.infer + +const COUNTRY_ISO2: Record = { + united_states: 'US', united_kingdom: 'GB', japan: 'JP', germany: 'DE', + france: 'FR', italy: 'IT', canada: 'CA', australia: 'AU', + china: 'CN', india: 'IN', brazil: 'BR', mexico: 'MX', +} + +const FREQ_MAP: Record = { annual: 'A', quarter: 'Q', monthly: 'M' } + +export class IMFConsumerPriceIndexFetcher extends Fetcher { + static override requireCredentials = false + + static override transformQuery(params: Record): IMFCPIQueryParams { + return IMFCPIQueryParamsSchema.parse(params) + } + + static override async extractData( + query: IMFCPIQueryParams, + _credentials: Record | null, + ): Promise[]> { + const iso = COUNTRY_ISO2[query.country] ?? query.country.toUpperCase().slice(0, 2) + const freq = FREQ_MAP[query.frequency] ?? 'M' + + // IMF CPI data from the CPI database + const indicator = query.transform === 'yoy' ? 'PCPIEPCH' : 'PCPIPCH' + const url = `https://dataservices.imf.org/REST/SDMX_JSON.svc/CompactData/CPI/${freq}.${iso}.${indicator}` + + try { + const resp = await nativeFetch(url, { timeoutMs: 30000 }) + if (resp.status !== 200) throw new EmptyDataError(`IMF API returned ${resp.status}`) + const data = JSON.parse(resp.text) as Record + const dataset = data.CompactData as Record + const dataSet = dataset?.DataSet as Record + let series = dataSet?.Series as Record | Record[] + + if (!series) throw new EmptyDataError() + if (!Array.isArray(series)) series = [series] + + const results: Record[] = [] + for (const s of series) { + let obs = (s.Obs ?? []) as Record | Record[] + if (!Array.isArray(obs)) obs = [obs] + + for (const o of obs) { + const period = o['@TIME_PERIOD'] as string + const value = parseFloat(o['@OBS_VALUE'] as string) + if (period && !isNaN(value)) { + const date = period.length === 7 ? period + '-01' : period.length === 4 ? period + '-01-01' : period + results.push({ date, country: query.country, value }) + } + } + } + + return results + } catch (err) { + if (err instanceof EmptyDataError) throw err + throw new EmptyDataError(`Failed to fetch IMF CPI data: ${err}`) + } + } + + static override transformData( + query: IMFCPIQueryParams, + data: Record[], + ): IMFCPIData[] { + if (data.length === 0) throw new EmptyDataError() + let filtered = data + if (query.start_date) filtered = filtered.filter(d => String(d.date) >= query.start_date!) + if (query.end_date) filtered = filtered.filter(d => String(d.date) <= query.end_date!) + return filtered + .sort((a, b) => String(a.date).localeCompare(String(b.date))) + .map(d => ConsumerPriceIndexDataSchema.parse(d)) + } +} diff --git a/packages/opentypebb/src/providers/imf/models/direction-of-trade.ts b/packages/opentypebb/src/providers/imf/models/direction-of-trade.ts new file mode 100644 index 00000000..d40979f3 --- /dev/null +++ b/packages/opentypebb/src/providers/imf/models/direction-of-trade.ts @@ -0,0 +1,106 @@ +/** + * IMF Direction of Trade Model. + * Maps to: openbb_imf/models/direction_of_trade.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { DirectionOfTradeDataSchema } from '../../../standard-models/direction-of-trade.js' +import { EmptyDataError } from '../../../core/provider/utils/errors.js' +import { nativeFetch } from '../../../core/provider/utils/helpers.js' + +export const IMFDOTQueryParamsSchema = z.object({ + country: z.string().nullable().default(null).describe('Country for the trade data.'), + counterpart: z.string().nullable().default(null).describe('Counterpart country.'), + direction: z.enum(['exports', 'imports', 'balance', 'all']).default('balance').describe('Trade direction.'), + start_date: z.string().nullable().default(null).describe('Start date in YYYY-MM-DD.'), + end_date: z.string().nullable().default(null).describe('End date in YYYY-MM-DD.'), + frequency: z.enum(['month', 'quarter', 'annual']).default('month').describe('Data frequency.'), +}).passthrough() + +export type IMFDOTQueryParams = z.infer +export type IMFDOTData = z.infer + +const COUNTRY_ISO2: Record = { + united_states: 'US', united_kingdom: 'GB', japan: 'JP', germany: 'DE', + france: 'FR', china: 'CN', india: 'IN', brazil: 'BR', +} + +const FREQ_MAP: Record = { month: 'M', quarter: 'Q', annual: 'A' } +const DIRECTION_MAP: Record = { exports: 'TXG_FOB_USD', imports: 'TMG_CIF_USD', balance: 'TBG_USD' } + +export class IMFDirectionOfTradeFetcher extends Fetcher { + static override requireCredentials = false + + static override transformQuery(params: Record): IMFDOTQueryParams { + return IMFDOTQueryParamsSchema.parse(params) + } + + static override async extractData( + query: IMFDOTQueryParams, + _credentials: Record | null, + ): Promise[]> { + const country = query.country ? (COUNTRY_ISO2[query.country] ?? query.country.toUpperCase().slice(0, 2)) : '' + const counterpart = query.counterpart ? (COUNTRY_ISO2[query.counterpart] ?? query.counterpart.toUpperCase().slice(0, 2)) : 'W00' + const freq = FREQ_MAP[query.frequency] ?? 'M' + const indicators = query.direction === 'all' + ? 'TXG_FOB_USD+TMG_CIF_USD+TBG_USD' + : DIRECTION_MAP[query.direction] ?? 'TBG_USD' + + const url = `https://dataservices.imf.org/REST/SDMX_JSON.svc/CompactData/DOT/${freq}.${country}.${indicators}.${counterpart}` + + try { + const resp = await nativeFetch(url, { timeoutMs: 30000 }) + if (resp.status !== 200) throw new EmptyDataError(`IMF API returned ${resp.status}`) + const data = JSON.parse(resp.text) as Record + const dataset = data.CompactData as Record + const dataSet = dataset?.DataSet as Record + let series = dataSet?.Series as Record | Record[] + + if (!series) throw new EmptyDataError() + if (!Array.isArray(series)) series = [series] + + const results: Record[] = [] + for (const s of series) { + const indicator = s['@INDICATOR'] as string + let obs = (s.Obs ?? []) as Record | Record[] + if (!Array.isArray(obs)) obs = [obs] + + for (const o of obs) { + const period = o['@TIME_PERIOD'] as string + const value = parseFloat(o['@OBS_VALUE'] as string) + if (period && !isNaN(value)) { + const date = period.length === 7 ? period + '-01' : period.length === 4 ? period + '-01-01' : period + results.push({ + date, + symbol: indicator, + country: query.country ?? 'all', + counterpart: query.counterpart ?? 'world', + title: indicator, + value, + scale: 'millions_usd', + }) + } + } + } + + return results + } catch (err) { + if (err instanceof EmptyDataError) throw err + throw new EmptyDataError(`Failed to fetch IMF DOT data: ${err}`) + } + } + + static override transformData( + query: IMFDOTQueryParams, + data: Record[], + ): IMFDOTData[] { + if (data.length === 0) throw new EmptyDataError() + let filtered = data + if (query.start_date) filtered = filtered.filter(d => String(d.date) >= query.start_date!) + if (query.end_date) filtered = filtered.filter(d => String(d.date) <= query.end_date!) + return filtered + .sort((a, b) => String(a.date).localeCompare(String(b.date))) + .map(d => DirectionOfTradeDataSchema.parse(d)) + } +} diff --git a/packages/opentypebb/src/providers/imf/models/economic-indicators.ts b/packages/opentypebb/src/providers/imf/models/economic-indicators.ts new file mode 100644 index 00000000..27571e09 --- /dev/null +++ b/packages/opentypebb/src/providers/imf/models/economic-indicators.ts @@ -0,0 +1,90 @@ +/** + * IMF Economic Indicators Model. + * Maps to: openbb_imf/models/economic_indicators.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { EconomicIndicatorsDataSchema } from '../../../standard-models/economic-indicators.js' +import { EmptyDataError } from '../../../core/provider/utils/errors.js' +import { nativeFetch } from '../../../core/provider/utils/helpers.js' + +export const IMFEconomicIndicatorsQueryParamsSchema = z.object({ + symbol: z.string().describe('IMF dataset/indicator code (e.g., IFS, BOP, GFSR).'), + country: z.string().nullable().default(null).describe('Country ISO2 code.'), + frequency: z.string().nullable().default(null).describe('Data frequency (A, Q, M).'), + start_date: z.string().nullable().default(null).describe('Start date in YYYY-MM-DD.'), + end_date: z.string().nullable().default(null).describe('End date in YYYY-MM-DD.'), +}).passthrough() + +export type IMFEconomicIndicatorsQueryParams = z.infer +export type IMFEconomicIndicatorsData = z.infer + +export class IMFEconomicIndicatorsFetcher extends Fetcher { + static override requireCredentials = false + + static override transformQuery(params: Record): IMFEconomicIndicatorsQueryParams { + return IMFEconomicIndicatorsQueryParamsSchema.parse(params) + } + + static override async extractData( + query: IMFEconomicIndicatorsQueryParams, + _credentials: Record | null, + ): Promise[]> { + const freq = query.frequency ?? 'A' + const country = query.country ?? 'US' + const url = `https://dataservices.imf.org/REST/SDMX_JSON.svc/CompactData/${query.symbol}/${freq}.${country}` + + try { + const resp = await nativeFetch(url, { timeoutMs: 30000 }) + if (resp.status !== 200) throw new EmptyDataError(`IMF API returned ${resp.status}`) + const data = JSON.parse(resp.text) as Record + const dataset = data.CompactData as Record + const dataSet = dataset?.DataSet as Record + let series = dataSet?.Series as Record | Record[] + + if (!series) throw new EmptyDataError() + if (!Array.isArray(series)) series = [series] + + const results: Record[] = [] + for (const s of series) { + const indicator = s['@INDICATOR'] as string ?? query.symbol + let obs = (s.Obs ?? []) as Record | Record[] + if (!Array.isArray(obs)) obs = [obs] + + for (const o of obs) { + const period = o['@TIME_PERIOD'] as string + const value = parseFloat(o['@OBS_VALUE'] as string) + if (period && !isNaN(value)) { + const date = period.length === 7 ? period + '-01' : period.length === 4 ? period + '-01-01' : period + results.push({ + date, + symbol_root: query.symbol, + symbol: `${query.symbol}.${indicator}`, + country: query.country, + value, + }) + } + } + } + + return results + } catch (err) { + if (err instanceof EmptyDataError) throw err + throw new EmptyDataError(`Failed to fetch IMF data: ${err}`) + } + } + + static override transformData( + query: IMFEconomicIndicatorsQueryParams, + data: Record[], + ): IMFEconomicIndicatorsData[] { + if (data.length === 0) throw new EmptyDataError() + let filtered = data + if (query.start_date) filtered = filtered.filter(d => String(d.date) >= query.start_date!) + if (query.end_date) filtered = filtered.filter(d => String(d.date) <= query.end_date!) + return filtered + .sort((a, b) => String(a.date).localeCompare(String(b.date))) + .map(d => EconomicIndicatorsDataSchema.parse(d)) + } +} diff --git a/packages/opentypebb/src/providers/intrinio/index.ts b/packages/opentypebb/src/providers/intrinio/index.ts new file mode 100644 index 00000000..f7abd4e8 --- /dev/null +++ b/packages/opentypebb/src/providers/intrinio/index.ts @@ -0,0 +1,19 @@ +/** + * Intrinio Provider Module. + * Maps to: openbb_platform/providers/intrinio/openbb_intrinio/__init__.py + */ + +import { Provider } from '../../core/provider/abstract/provider.js' +import { IntrinioOptionsSnapshotsFetcher } from './models/options-snapshots.js' +import { IntrinioOptionsUnusualFetcher } from './models/options-unusual.js' + +export const intrinioProvider = new Provider({ + name: 'intrinio', + website: 'https://intrinio.com', + description: 'Intrinio provides financial data and analytics APIs.', + credentials: ['api_key'], + fetcherDict: { + OptionsSnapshots: IntrinioOptionsSnapshotsFetcher, + OptionsUnusual: IntrinioOptionsUnusualFetcher, + }, +}) diff --git a/packages/opentypebb/src/providers/intrinio/models/options-snapshots.ts b/packages/opentypebb/src/providers/intrinio/models/options-snapshots.ts new file mode 100644 index 00000000..53fe372d --- /dev/null +++ b/packages/opentypebb/src/providers/intrinio/models/options-snapshots.ts @@ -0,0 +1,65 @@ +/** + * Intrinio Options Snapshots Model. + * Maps to: openbb_intrinio/models/options_snapshots.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { OptionsSnapshotsDataSchema } from '../../../standard-models/options-snapshots.js' +import { EmptyDataError } from '../../../core/provider/utils/errors.js' +import { amakeRequest } from '../../../core/provider/utils/helpers.js' + +export const IntrinioOptionsSnapshotsQueryParamsSchema = z.object({}).passthrough() +export type IntrinioOptionsSnapshotsQueryParams = z.infer +export type IntrinioOptionsSnapshotsData = z.infer + +export class IntrinioOptionsSnapshotsFetcher extends Fetcher { + static override requireCredentials = true + + static override transformQuery(params: Record): IntrinioOptionsSnapshotsQueryParams { + return IntrinioOptionsSnapshotsQueryParamsSchema.parse(params) + } + + static override async extractData( + _query: IntrinioOptionsSnapshotsQueryParams, + credentials: Record | null, + ): Promise[]> { + const apiKey = credentials?.intrinio_api_key ?? '' + if (!apiKey) throw new EmptyDataError('Intrinio API key required.') + + const url = `https://api-v2.intrinio.com/options/snapshots?api_key=${apiKey}` + + try { + const data = await amakeRequest>(url) + const snapshots = (data.snapshots ?? data.options ?? []) as Record[] + if (!Array.isArray(snapshots) || snapshots.length === 0) throw new EmptyDataError() + return snapshots + } catch (err) { + if (err instanceof EmptyDataError) throw err + throw new EmptyDataError(`Failed to fetch Intrinio options snapshots: ${err}`) + } + } + + static override transformData( + _query: IntrinioOptionsSnapshotsQueryParams, + data: Record[], + ): IntrinioOptionsSnapshotsData[] { + return data.map(d => OptionsSnapshotsDataSchema.parse({ + underlying_symbol: d.underlying_symbol ?? d.ticker ?? '', + contract_symbol: d.contract_symbol ?? d.code ?? '', + expiration: d.expiration ?? '', + dte: d.dte ?? null, + strike: d.strike ?? 0, + option_type: d.type ?? d.option_type ?? '', + volume: d.volume ?? null, + open_interest: d.open_interest ?? null, + last_price: d.last ?? d.last_price ?? null, + last_size: d.last_size ?? null, + last_timestamp: d.last_timestamp ?? null, + open: d.open ?? null, + high: d.high ?? null, + low: d.low ?? null, + close: d.close ?? null, + })) + } +} diff --git a/packages/opentypebb/src/providers/intrinio/models/options-unusual.ts b/packages/opentypebb/src/providers/intrinio/models/options-unusual.ts new file mode 100644 index 00000000..fe5099b1 --- /dev/null +++ b/packages/opentypebb/src/providers/intrinio/models/options-unusual.ts @@ -0,0 +1,56 @@ +/** + * Intrinio Options Unusual Model. + * Maps to: openbb_intrinio/models/options_unusual.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { OptionsUnusualDataSchema } from '../../../standard-models/options-unusual.js' +import { EmptyDataError } from '../../../core/provider/utils/errors.js' +import { amakeRequest } from '../../../core/provider/utils/helpers.js' + +export const IntrinioOptionsUnusualQueryParamsSchema = z.object({ + symbol: z.string().nullable().default(null).transform(v => v ? v.toUpperCase() : null).describe('Symbol to filter by.'), +}).passthrough() + +export type IntrinioOptionsUnusualQueryParams = z.infer +export type IntrinioOptionsUnusualData = z.infer + +export class IntrinioOptionsUnusualFetcher extends Fetcher { + static override requireCredentials = true + + static override transformQuery(params: Record): IntrinioOptionsUnusualQueryParams { + return IntrinioOptionsUnusualQueryParamsSchema.parse(params) + } + + static override async extractData( + query: IntrinioOptionsUnusualQueryParams, + credentials: Record | null, + ): Promise[]> { + const apiKey = credentials?.intrinio_api_key ?? '' + if (!apiKey) throw new EmptyDataError('Intrinio API key required.') + + let url = `https://api-v2.intrinio.com/options/unusual_activity?api_key=${apiKey}` + if (query.symbol) url += `&symbol=${query.symbol}` + + try { + const data = await amakeRequest>(url) + const activities = (data.trades ?? data.unusual_activity ?? []) as Record[] + if (!Array.isArray(activities) || activities.length === 0) throw new EmptyDataError() + return activities + } catch (err) { + if (err instanceof EmptyDataError) throw err + throw new EmptyDataError(`Failed to fetch Intrinio unusual options: ${err}`) + } + } + + static override transformData( + _query: IntrinioOptionsUnusualQueryParams, + data: Record[], + ): IntrinioOptionsUnusualData[] { + return data.map(d => OptionsUnusualDataSchema.parse({ + underlying_symbol: d.symbol ?? d.underlying_symbol ?? null, + contract_symbol: d.contract ?? d.contract_symbol ?? '', + })) + } +} diff --git a/packages/opentypebb/src/providers/multpl/index.ts b/packages/opentypebb/src/providers/multpl/index.ts new file mode 100644 index 00000000..be86705b --- /dev/null +++ b/packages/opentypebb/src/providers/multpl/index.ts @@ -0,0 +1,16 @@ +/** + * Multpl Provider Module. + * Maps to: openbb_platform/providers/multpl/openbb_multpl/__init__.py + */ + +import { Provider } from '../../core/provider/abstract/provider.js' +import { MultplSP500MultiplesFetcher } from './models/sp500-multiples.js' + +export const multplProvider = new Provider({ + name: 'multpl', + website: 'https://www.multpl.com/', + description: 'Public broad-market data published to https://multpl.com.', + fetcherDict: { + SP500Multiples: MultplSP500MultiplesFetcher, + }, +}) diff --git a/packages/opentypebb/src/providers/multpl/models/sp500-multiples.ts b/packages/opentypebb/src/providers/multpl/models/sp500-multiples.ts new file mode 100644 index 00000000..fc7f2e2a --- /dev/null +++ b/packages/opentypebb/src/providers/multpl/models/sp500-multiples.ts @@ -0,0 +1,183 @@ +/** + * Multpl S&P 500 Multiples Model. + * Maps to: openbb_multpl/models/sp500_multiples.py + * + * Scrapes data tables from multpl.com. + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { SP500MultiplesDataSchema } from '../../../standard-models/sp500-multiples.js' +import { EmptyDataError, OpenBBError } from '../../../core/provider/utils/errors.js' +import { nativeFetch } from '../../../core/provider/utils/helpers.js' + +const BASE_URL = 'https://www.multpl.com/' + +const URL_DICT: Record = { + shiller_pe_month: 'shiller-pe/table/by-month', + shiller_pe_year: 'shiller-pe/table/by-year', + pe_year: 's-p-500-pe-ratio/table/by-year', + pe_month: 's-p-500-pe-ratio/table/by-month', + dividend_year: 's-p-500-dividend/table/by-year', + dividend_month: 's-p-500-dividend/table/by-month', + dividend_growth_quarter: 's-p-500-dividend-growth/table/by-quarter', + dividend_growth_year: 's-p-500-dividend-growth/table/by-year', + dividend_yield_year: 's-p-500-dividend-yield/table/by-year', + dividend_yield_month: 's-p-500-dividend-yield/table/by-month', + earnings_year: 's-p-500-earnings/table/by-year', + earnings_month: 's-p-500-earnings/table/by-month', + earnings_growth_year: 's-p-500-earnings-growth/table/by-year', + earnings_growth_quarter: 's-p-500-earnings-growth/table/by-quarter', + real_earnings_growth_year: 's-p-500-real-earnings-growth/table/by-year', + real_earnings_growth_quarter: 's-p-500-real-earnings-growth/table/by-quarter', + earnings_yield_year: 's-p-500-earnings-yield/table/by-year', + earnings_yield_month: 's-p-500-earnings-yield/table/by-month', + real_price_year: 's-p-500-historical-prices/table/by-year', + real_price_month: 's-p-500-historical-prices/table/by-month', + inflation_adjusted_price_year: 'inflation-adjusted-s-p-500/table/by-year', + inflation_adjusted_price_month: 'inflation-adjusted-s-p-500/table/by-month', + sales_year: 's-p-500-sales/table/by-year', + sales_quarter: 's-p-500-sales/table/by-quarter', + sales_growth_year: 's-p-500-sales-growth/table/by-year', + sales_growth_quarter: 's-p-500-sales-growth/table/by-quarter', + real_sales_year: 's-p-500-real-sales/table/by-year', + real_sales_quarter: 's-p-500-real-sales/table/by-quarter', + real_sales_growth_year: 's-p-500-real-sales-growth/table/by-year', + real_sales_growth_quarter: 's-p-500-real-sales-growth/table/by-quarter', + price_to_sales_year: 's-p-500-price-to-sales/table/by-year', + price_to_sales_quarter: 's-p-500-price-to-sales/table/by-quarter', + price_to_book_value_year: 's-p-500-price-to-book/table/by-year', + price_to_book_value_quarter: 's-p-500-price-to-book/table/by-quarter', + book_value_year: 's-p-500-book-value/table/by-year', + book_value_quarter: 's-p-500-book-value/table/by-quarter', +} + +export const MultplSP500MultiplesQueryParamsSchema = z.object({ + series_name: z.string().default('pe_month').describe('The name of the series.'), + start_date: z.string().nullable().default(null).describe('Start date in YYYY-MM-DD.'), + end_date: z.string().nullable().default(null).describe('End date in YYYY-MM-DD.'), +}).passthrough() + +export type MultplSP500MultiplesQueryParams = z.infer + +export type MultplSP500MultiplesData = z.infer + +/** + * Parse an HTML table from multpl.com. + * The tables have format: Date | Value + */ +function parseHtmlTable(html: string): Array<{ date: string; value: string }> { + const rows: Array<{ date: string; value: string }> = [] + // Match table rows with two cells + const rowRegex = /]*>\s*]*>(.*?)<\/td>\s*]*>(.*?)<\/td>\s*<\/tr>/gs + let match: RegExpExecArray | null + while ((match = rowRegex.exec(html)) !== null) { + const dateStr = match[1].replace(/<[^>]+>/g, '').trim() + const valueStr = match[2].replace(/<[^>]+>/g, '').trim() + if (dateStr && valueStr && !dateStr.toLowerCase().includes('date')) { + rows.push({ date: dateStr, value: valueStr }) + } + } + return rows +} + +/** + * Parse a date string from multpl.com (e.g., "Jan 31, 2024"). + */ +function parseDate(dateStr: string): string | null { + try { + const d = new Date(dateStr) + if (isNaN(d.getTime())) return null + return d.toISOString().slice(0, 10) + } catch { + return null + } +} + +/** + * Parse a value string from multpl.com. + */ +function parseValue(valueStr: string, isPercent: boolean): number | null { + const cleaned = valueStr.replace(/†/g, '').replace(/%/g, '').replace(/\$/g, '').replace(/,/g, '').trim() + const num = parseFloat(cleaned) + if (isNaN(num)) return null + return isPercent ? num / 100 : num +} + +export class MultplSP500MultiplesFetcher extends Fetcher { + static override requireCredentials = false + + static override transformQuery(params: Record): MultplSP500MultiplesQueryParams { + const query = MultplSP500MultiplesQueryParamsSchema.parse(params) + // Validate series names + const series = query.series_name.split(',') + for (const s of series) { + if (!URL_DICT[s]) { + throw new OpenBBError(`Invalid series_name: ${s}. Valid: ${Object.keys(URL_DICT).sort().join(', ')}`) + } + } + return query + } + + static override async extractData( + query: MultplSP500MultiplesQueryParams, + _credentials: Record | null, + ): Promise[]> { + const series = query.series_name.split(',') + const results: Record[] = [] + + const tasks = series.map(async (seriesName) => { + const path = URL_DICT[seriesName] + const url = `${BASE_URL}${path}` + + try { + const resp = await nativeFetch(url, { timeoutMs: 30000 }) + if (resp.status !== 200) { + console.warn(`Failed to fetch ${seriesName}: ${resp.status}`) + return + } + + const html = resp.text + const rows = parseHtmlTable(html) + const isPercent = seriesName.includes('growth') || seriesName.includes('yield') + + for (const row of rows) { + const date = parseDate(row.date) + if (!date) continue + + // Filter by date range + if (query.start_date && date < query.start_date) continue + if (query.end_date && date > query.end_date) continue + + const value = parseValue(row.value, isPercent) + if (value === null) continue + + results.push({ + date, + name: seriesName, + value, + }) + } + } catch (err) { + console.warn(`Failed to get data for ${seriesName}: ${err}`) + } + }) + + await Promise.all(tasks) + + if (results.length === 0) throw new EmptyDataError('No data found.') + return results + } + + static override transformData( + _query: MultplSP500MultiplesQueryParams, + data: Record[], + ): MultplSP500MultiplesData[] { + const sorted = data.sort((a, b) => { + const dateCompare = String(a.date).localeCompare(String(b.date)) + if (dateCompare !== 0) return dateCompare + return String(a.name).localeCompare(String(b.name)) + }) + return sorted.map(d => SP500MultiplesDataSchema.parse(d)) + } +} diff --git a/packages/opentypebb/src/providers/oecd/index.ts b/packages/opentypebb/src/providers/oecd/index.ts new file mode 100644 index 00000000..cff0f348 --- /dev/null +++ b/packages/opentypebb/src/providers/oecd/index.ts @@ -0,0 +1,20 @@ +/** + * OECD Provider Module. + * Maps to: openbb_platform/providers/oecd/openbb_oecd/__init__.py + */ + +import { Provider } from '../../core/provider/abstract/provider.js' +import { OECDCompositeLeadingIndicatorFetcher } from './models/composite-leading-indicator.js' +import { OECDConsumerPriceIndexFetcher } from './models/consumer-price-index.js' +import { OECDCountryInterestRatesFetcher } from './models/country-interest-rates.js' + +export const oecdProvider = new Provider({ + name: 'oecd', + website: 'https://data.oecd.org', + description: 'OECD provides international economic, social, and environmental data.', + fetcherDict: { + CompositeLeadingIndicator: OECDCompositeLeadingIndicatorFetcher, + ConsumerPriceIndex: OECDConsumerPriceIndexFetcher, + CountryInterestRates: OECDCountryInterestRatesFetcher, + }, +}) diff --git a/packages/opentypebb/src/providers/oecd/models/composite-leading-indicator.ts b/packages/opentypebb/src/providers/oecd/models/composite-leading-indicator.ts new file mode 100644 index 00000000..00711a7c --- /dev/null +++ b/packages/opentypebb/src/providers/oecd/models/composite-leading-indicator.ts @@ -0,0 +1,109 @@ +/** + * OECD Composite Leading Indicator Model. + * Maps to: openbb_oecd/models/composite_leading_indicator.py + * + * Uses CSV format from OECD SDMX REST API (same as Python implementation). + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { CompositeLeadingIndicatorDataSchema } from '../../../standard-models/composite-leading-indicator.js' +import { EmptyDataError } from '../../../core/provider/utils/errors.js' +import { nativeFetch } from '../../../core/provider/utils/helpers.js' + +export const OECDCLIQueryParamsSchema = z.object({ + country: z.string().default('g20').describe('Country or group code (g20, united_states, all, etc).'), + start_date: z.string().nullable().default(null).describe('Start date in YYYY-MM-DD.'), + end_date: z.string().nullable().default(null).describe('End date in YYYY-MM-DD.'), +}).passthrough() + +export type OECDCLIQueryParams = z.infer +export type OECDCLIData = z.infer + +const COUNTRIES: Record = { + g20: 'G20', g7: 'G7', asia5: 'A5M', north_america: 'NAFTA', europe4: 'G4E', + australia: 'AUS', brazil: 'BRA', canada: 'CAN', china: 'CHN', france: 'FRA', + germany: 'DEU', india: 'IND', indonesia: 'IDN', italy: 'ITA', japan: 'JPN', + mexico: 'MEX', spain: 'ESP', south_africa: 'ZAF', south_korea: 'KOR', + turkey: 'TUR', united_states: 'USA', united_kingdom: 'GBR', +} + +const CODE_TO_NAME: Record = Object.fromEntries( + Object.entries(COUNTRIES).map(([k, v]) => [v, k.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase())]), +) + +/** Parse simple CSV text into rows */ +function parseCSV(text: string): Record[] { + const lines = text.trim().split('\n') + if (lines.length < 2) return [] + const headers = lines[0].split(',') + return lines.slice(1).map(line => { + const values = line.split(',') + const row: Record = {} + headers.forEach((h, i) => { row[h.trim()] = values[i]?.trim() ?? '' }) + return row + }) +} + +export class OECDCompositeLeadingIndicatorFetcher extends Fetcher { + static override requireCredentials = false + + static override transformQuery(params: Record): OECDCLIQueryParams { + if (!params.start_date) params.start_date = '1947-01-01' + if (!params.end_date) { + const y = new Date().getFullYear() + params.end_date = `${y}-12-31` + } + return OECDCLIQueryParamsSchema.parse(params) + } + + static override async extractData( + query: OECDCLIQueryParams, + _credentials: Record | null, + ): Promise[]> { + // Build country code string + let countryCode = '' + if (query.country && query.country !== 'all') { + const parts = query.country.split(',') + countryCode = parts.map(c => COUNTRIES[c.toLowerCase().trim()] ?? c.toUpperCase()).join('+') + } + + const url = + `https://sdmx.oecd.org/public/rest/data/OECD.SDD.STES,DSD_STES@DF_CLI,4.1` + + `/${countryCode}.M.LI...AA.IX..H` + + `?startPeriod=${query.start_date}&endPeriod=${query.end_date}` + + `&dimensionAtObservation=TIME_PERIOD&detail=dataonly&format=csvfile` + + try { + const resp = await nativeFetch(url, { + headers: { Accept: 'application/vnd.sdmx.data+csv; charset=utf-8' }, + timeoutMs: 30000, + }) + if (resp.status !== 200) throw new EmptyDataError(`OECD API returned ${resp.status}`) + const text = resp.text + const rows = parseCSV(text) + if (!rows.length) throw new EmptyDataError() + + return rows + .filter(r => r.OBS_VALUE && r.OBS_VALUE !== '') + .map(r => ({ + date: r.TIME_PERIOD ? r.TIME_PERIOD + '-01' : '', + value: parseFloat(r.OBS_VALUE), + country: CODE_TO_NAME[r.REF_AREA] ?? r.REF_AREA ?? 'Unknown', + })) + } catch (err) { + if (err instanceof EmptyDataError) throw err + throw new EmptyDataError(`Failed to fetch OECD CLI data: ${err}`) + } + } + + static override transformData( + _query: OECDCLIQueryParams, + data: Record[], + ): OECDCLIData[] { + if (data.length === 0) throw new EmptyDataError() + return data + .sort((a, b) => String(a.date).localeCompare(String(b.date))) + .map(d => CompositeLeadingIndicatorDataSchema.parse(d)) + } +} diff --git a/packages/opentypebb/src/providers/oecd/models/consumer-price-index.ts b/packages/opentypebb/src/providers/oecd/models/consumer-price-index.ts new file mode 100644 index 00000000..2413f92d --- /dev/null +++ b/packages/opentypebb/src/providers/oecd/models/consumer-price-index.ts @@ -0,0 +1,118 @@ +/** + * OECD Consumer Price Index Model. + * Maps to: openbb_oecd/models/consumer_price_index.py + * + * Uses CSV format from OECD SDMX REST API (same as Python implementation). + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { ConsumerPriceIndexDataSchema } from '../../../standard-models/consumer-price-index.js' +import { EmptyDataError } from '../../../core/provider/utils/errors.js' +import { nativeFetch } from '../../../core/provider/utils/helpers.js' + +export const OECDCPIQueryParamsSchema = z.object({ + country: z.string().default('united_states').describe('The country to get data for.'), + transform: z.string().default('yoy').describe('Transformation: yoy, period, index.'), + frequency: z.enum(['annual', 'quarter', 'monthly']).default('monthly').describe('Data frequency.'), + harmonized: z.boolean().default(false).describe('If true, returns harmonized data.'), + start_date: z.string().nullable().default(null).describe('Start date in YYYY-MM-DD.'), + end_date: z.string().nullable().default(null).describe('End date in YYYY-MM-DD.'), +}).passthrough() + +export type OECDCPIQueryParams = z.infer +export type OECDCPIData = z.infer + +const COUNTRY_MAP: Record = { + united_states: 'USA', united_kingdom: 'GBR', japan: 'JPN', germany: 'DEU', + france: 'FRA', italy: 'ITA', canada: 'CAN', australia: 'AUS', + south_korea: 'KOR', mexico: 'MEX', brazil: 'BRA', china: 'CHN', + india: 'IND', turkey: 'TUR', south_africa: 'ZAF', russia: 'RUS', + spain: 'ESP', netherlands: 'NLD', switzerland: 'CHE', sweden: 'SWE', + norway: 'NOR', denmark: 'DNK', finland: 'FIN', belgium: 'BEL', + austria: 'AUT', ireland: 'IRL', portugal: 'PRT', greece: 'GRC', + new_zealand: 'NZL', israel: 'ISR', poland: 'POL', czech_republic: 'CZE', + hungary: 'HUN', colombia: 'COL', chile: 'CHL', indonesia: 'IDN', +} + +const CODE_TO_NAME: Record = Object.fromEntries( + Object.entries(COUNTRY_MAP).map(([k, v]) => [v, k.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase())]), +) + +const FREQ_MAP: Record = { annual: 'A', quarter: 'Q', monthly: 'M' } + +/** Parse simple CSV text into rows */ +function parseCSV(text: string): Record[] { + const lines = text.trim().split('\n') + if (lines.length < 2) return [] + const headers = lines[0].split(',') + return lines.slice(1).map(line => { + const values = line.split(',') + const row: Record = {} + headers.forEach((h, i) => { row[h.trim()] = values[i]?.trim() ?? '' }) + return row + }) +} + +export class OECDConsumerPriceIndexFetcher extends Fetcher { + static override requireCredentials = false + + static override transformQuery(params: Record): OECDCPIQueryParams { + return OECDCPIQueryParamsSchema.parse(params) + } + + static override async extractData( + query: OECDCPIQueryParams, + _credentials: Record | null, + ): Promise[]> { + const countryCode = COUNTRY_MAP[query.country] ?? query.country.toUpperCase() + const freq = FREQ_MAP[query.frequency] ?? 'M' + const methodology = query.harmonized ? 'HICP' : 'N' + const units = query.transform === 'yoy' ? 'PA' : query.transform === 'period' ? 'PC' : 'IX' + const expenditure = '_T' + + // Use CSV format (matching Python implementation) + // Dimension order: REF_AREA.FREQ.METHODOLOGY.MEASURE.UNIT_MEASURE.EXPENDITURE.UNIT_MULT. + const url = + `https://sdmx.oecd.org/public/rest/data/OECD.SDD.TPS,DSD_PRICES@DF_PRICES_ALL,1.0` + + `/${countryCode}.${freq}.${methodology}.CPI.${units}.${expenditure}.N.` + + `?dimensionAtObservation=TIME_PERIOD&detail=dataonly&format=csvfile` + + try { + const resp = await nativeFetch(url, { + headers: { Accept: 'application/vnd.sdmx.data+csv; charset=utf-8' }, + timeoutMs: 30000, + }) + if (resp.status !== 200) throw new EmptyDataError(`OECD CPI API returned ${resp.status}`) + const rows = parseCSV(resp.text) + if (!rows.length) throw new EmptyDataError() + + return rows + .filter(r => r.OBS_VALUE && r.OBS_VALUE !== '') + .map(r => { + const period = r.TIME_PERIOD ?? '' + return { + date: period.length === 7 ? period + '-01' : period.length === 4 ? period + '-01-01' : period, + country: CODE_TO_NAME[r.REF_AREA] ?? r.REF_AREA ?? query.country, + value: parseFloat(r.OBS_VALUE), + } + }) + } catch (err) { + if (err instanceof EmptyDataError) throw err + throw new EmptyDataError(`Failed to fetch OECD CPI data: ${err}`) + } + } + + static override transformData( + query: OECDCPIQueryParams, + data: Record[], + ): OECDCPIData[] { + if (data.length === 0) throw new EmptyDataError() + let filtered = data + if (query.start_date) filtered = filtered.filter(d => String(d.date) >= query.start_date!) + if (query.end_date) filtered = filtered.filter(d => String(d.date) <= query.end_date!) + return filtered + .sort((a, b) => String(a.date).localeCompare(String(b.date))) + .map(d => ConsumerPriceIndexDataSchema.parse(d)) + } +} diff --git a/packages/opentypebb/src/providers/oecd/models/country-interest-rates.ts b/packages/opentypebb/src/providers/oecd/models/country-interest-rates.ts new file mode 100644 index 00000000..2e0d2bf7 --- /dev/null +++ b/packages/opentypebb/src/providers/oecd/models/country-interest-rates.ts @@ -0,0 +1,110 @@ +/** + * OECD Country Interest Rates Model. + * Maps to: openbb_oecd/models/country_interest_rates.py + * + * Uses CSV format from OECD SDMX REST API (same as Python implementation). + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { CountryInterestRatesDataSchema } from '../../../standard-models/country-interest-rates.js' +import { EmptyDataError } from '../../../core/provider/utils/errors.js' +import { nativeFetch } from '../../../core/provider/utils/helpers.js' + +export const OECDInterestRatesQueryParamsSchema = z.object({ + country: z.string().default('united_states').describe('The country to get data for.'), + duration: z.enum(['short', 'long']).default('short').describe('Duration of the interest rate (short or long).'), + start_date: z.string().nullable().default(null).describe('Start date in YYYY-MM-DD.'), + end_date: z.string().nullable().default(null).describe('End date in YYYY-MM-DD.'), +}).passthrough() + +export type OECDInterestRatesQueryParams = z.infer +export type OECDInterestRatesData = z.infer + +const COUNTRY_MAP: Record = { + united_states: 'USA', united_kingdom: 'GBR', japan: 'JPN', germany: 'DEU', + france: 'FRA', italy: 'ITA', canada: 'CAN', australia: 'AUS', + south_korea: 'KOR', mexico: 'MEX', brazil: 'BRA', china: 'CHN', + spain: 'ESP', netherlands: 'NLD', switzerland: 'CHE', sweden: 'SWE', + norway: 'NOR', denmark: 'DNK', new_zealand: 'NZL', poland: 'POL', +} + +const CODE_TO_NAME: Record = Object.fromEntries( + Object.entries(COUNTRY_MAP).map(([k, v]) => [v, k.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase())]), +) + +const DURATION_MAP: Record = { short: 'IR3TIB', long: 'IRLT' } + +/** Parse simple CSV text into rows */ +function parseCSV(text: string): Record[] { + const lines = text.trim().split('\n') + if (lines.length < 2) return [] + const headers = lines[0].split(',') + return lines.slice(1).map(line => { + const values = line.split(',') + const row: Record = {} + headers.forEach((h, i) => { row[h.trim()] = values[i]?.trim() ?? '' }) + return row + }) +} + +export class OECDCountryInterestRatesFetcher extends Fetcher { + static override requireCredentials = false + + static override transformQuery(params: Record): OECDInterestRatesQueryParams { + if (!params.start_date) params.start_date = '1950-01-01' + if (!params.end_date) { + const y = new Date().getFullYear() + params.end_date = `${y}-12-31` + } + return OECDInterestRatesQueryParamsSchema.parse(params) + } + + static override async extractData( + query: OECDInterestRatesQueryParams, + _credentials: Record | null, + ): Promise[]> { + const countryCode = COUNTRY_MAP[query.country] ?? query.country.toUpperCase() + const duration = DURATION_MAP[query.duration] ?? 'IR3TIB' + const startPeriod = query.start_date ? query.start_date.slice(0, 7) : '' + const endPeriod = query.end_date ? query.end_date.slice(0, 7) : '' + + const url = + `https://sdmx.oecd.org/public/rest/data/OECD.SDD.STES,DSD_KEI@DF_KEI,4.0` + + `/${countryCode}.M.${duration}....` + + `?startPeriod=${startPeriod}&endPeriod=${endPeriod}` + + `&dimensionAtObservation=TIME_PERIOD&detail=dataonly` + + try { + const resp = await nativeFetch(url, { + headers: { Accept: 'application/vnd.sdmx.data+csv; charset=utf-8' }, + timeoutMs: 20000, + }) + if (resp.status !== 200) throw new EmptyDataError(`OECD API returned ${resp.status}`) + const text = resp.text + const rows = parseCSV(text) + if (!rows.length) throw new EmptyDataError() + + return rows + .filter(r => r.OBS_VALUE && r.OBS_VALUE !== '') + .map(r => ({ + date: r.TIME_PERIOD ? r.TIME_PERIOD + '-01' : '', + value: parseFloat(r.OBS_VALUE) / 100, + country: CODE_TO_NAME[r.REF_AREA] ?? r.REF_AREA ?? query.country, + })) + } catch (err) { + if (err instanceof EmptyDataError) throw err + throw new EmptyDataError(`Failed to fetch OECD interest rates: ${err}`) + } + } + + static override transformData( + _query: OECDInterestRatesQueryParams, + data: Record[], + ): OECDInterestRatesData[] { + if (data.length === 0) throw new EmptyDataError() + return data + .sort((a, b) => String(a.date).localeCompare(String(b.date))) + .map(d => CountryInterestRatesDataSchema.parse(d)) + } +} diff --git a/packages/opentypebb/src/providers/yfinance/index.ts b/packages/opentypebb/src/providers/yfinance/index.ts new file mode 100644 index 00000000..b6830078 --- /dev/null +++ b/packages/opentypebb/src/providers/yfinance/index.ts @@ -0,0 +1,82 @@ +/** + * YFinance Provider Module. + * Maps to: openbb_platform/providers/yfinance/openbb_yfinance/__init__.py + * + * Only includes fetchers that have been ported to TypeScript. + */ + +import { Provider } from '../../core/provider/abstract/provider.js' + +import { YFinanceEquityQuoteFetcher } from './models/equity-quote.js' +import { YFinanceEquityProfileFetcher } from './models/equity-profile.js' +import { YFinanceEquityHistoricalFetcher } from './models/equity-historical.js' +import { YFinanceCompanyNewsFetcher } from './models/company-news.js' +import { YFinanceKeyMetricsFetcher } from './models/key-metrics.js' +import { YFinancePriceTargetConsensusFetcher } from './models/price-target-consensus.js' +import { YFinanceCryptoSearchFetcher } from './models/crypto-search.js' +import { YFinanceCurrencySearchFetcher } from './models/currency-search.js' +import { YFinanceCryptoHistoricalFetcher } from './models/crypto-historical.js' +import { YFinanceCurrencyHistoricalFetcher } from './models/currency-historical.js' +import { YFinanceBalanceSheetFetcher } from './models/balance-sheet.js' +import { YFinanceIncomeStatementFetcher } from './models/income-statement.js' +import { YFinanceCashFlowStatementFetcher } from './models/cash-flow.js' +import { YFGainersFetcher } from './models/gainers.js' +import { YFLosersFetcher } from './models/losers.js' +import { YFActiveFetcher } from './models/active.js' +import { YFAggressiveSmallCapsFetcher } from './models/aggressive-small-caps.js' +import { YFGrowthTechEquitiesFetcher } from './models/growth-tech.js' +import { YFUndervaluedGrowthEquitiesFetcher } from './models/undervalued-growth.js' +import { YFUndervaluedLargeCapsFetcher } from './models/undervalued-large-caps.js' +import { YFinanceKeyExecutivesFetcher } from './models/key-executives.js' +import { YFinanceHistoricalDividendsFetcher } from './models/historical-dividends.js' +import { YFinanceShareStatisticsFetcher } from './models/share-statistics.js' +import { YFinanceIndexHistoricalFetcher } from './models/index-historical.js' +import { YFinanceFuturesHistoricalFetcher } from './models/futures-historical.js' +import { YFinanceAvailableIndicesFetcher } from './models/available-indices.js' +import { YFinanceEtfInfoFetcher } from './models/etf-info.js' +import { YFinanceEquityScreenerFetcher } from './models/equity-screener.js' +import { YFinanceFuturesCurveFetcher } from './models/futures-curve.js' +import { YFinanceOptionsChainsFetcher } from './models/options-chains.js' + +export const yfinanceProvider = new Provider({ + name: 'yfinance', + website: 'https://finance.yahoo.com', + description: + 'Yahoo! Finance is a web-based platform that offers financial news, ' + + 'data, and tools for investors and individuals interested in tracking ' + + 'and analyzing financial markets and assets.', + fetcherDict: { + EquityQuote: YFinanceEquityQuoteFetcher, + EquityInfo: YFinanceEquityProfileFetcher, + EquityHistorical: YFinanceEquityHistoricalFetcher, + EtfHistorical: YFinanceEquityHistoricalFetcher, + CompanyNews: YFinanceCompanyNewsFetcher, + KeyMetrics: YFinanceKeyMetricsFetcher, + PriceTargetConsensus: YFinancePriceTargetConsensusFetcher, + BalanceSheet: YFinanceBalanceSheetFetcher, + IncomeStatement: YFinanceIncomeStatementFetcher, + CashFlowStatement: YFinanceCashFlowStatementFetcher, + CryptoSearch: YFinanceCryptoSearchFetcher, + CurrencyPairs: YFinanceCurrencySearchFetcher, + CryptoHistorical: YFinanceCryptoHistoricalFetcher, + CurrencyHistorical: YFinanceCurrencyHistoricalFetcher, + EquityGainers: YFGainersFetcher, + EquityLosers: YFLosersFetcher, + EquityActive: YFActiveFetcher, + EquityAggressiveSmallCaps: YFAggressiveSmallCapsFetcher, + GrowthTechEquities: YFGrowthTechEquitiesFetcher, + EquityUndervaluedGrowth: YFUndervaluedGrowthEquitiesFetcher, + EquityUndervaluedLargeCaps: YFUndervaluedLargeCapsFetcher, + KeyExecutives: YFinanceKeyExecutivesFetcher, + HistoricalDividends: YFinanceHistoricalDividendsFetcher, + ShareStatistics: YFinanceShareStatisticsFetcher, + IndexHistorical: YFinanceIndexHistoricalFetcher, + FuturesHistorical: YFinanceFuturesHistoricalFetcher, + AvailableIndices: YFinanceAvailableIndicesFetcher, + EtfInfo: YFinanceEtfInfoFetcher, + EquityScreener: YFinanceEquityScreenerFetcher, + FuturesCurve: YFinanceFuturesCurveFetcher, + OptionsChains: YFinanceOptionsChainsFetcher, + }, + reprName: 'Yahoo Finance', +}) diff --git a/packages/opentypebb/src/providers/yfinance/models/active.ts b/packages/opentypebb/src/providers/yfinance/models/active.ts new file mode 100644 index 00000000..da51c5f3 --- /dev/null +++ b/packages/opentypebb/src/providers/yfinance/models/active.ts @@ -0,0 +1,51 @@ +/** + * Yahoo Finance Most Active Model. + * Maps to: openbb_yfinance/models/active.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { EquityPerformanceQueryParamsSchema } from '../../../standard-models/equity-performance.js' +import { applyAliases } from '../../../core/provider/utils/helpers.js' +import { getPredefinedScreener } from '../utils/helpers.js' +import { YFPredefinedScreenerDataSchema, YF_SCREENER_ALIAS_DICT } from '../utils/references.js' + +export const YFActiveQueryParamsSchema = EquityPerformanceQueryParamsSchema.extend({ + limit: z.number().nullable().default(200).describe('Limit the number of results.'), +}) +export type YFActiveQueryParams = z.infer + +export const YFActiveDataSchema = YFPredefinedScreenerDataSchema +export type YFActiveData = z.infer + +export class YFActiveFetcher extends Fetcher { + static requireCredentials = false + + static override transformQuery(params: Record): YFActiveQueryParams { + return YFActiveQueryParamsSchema.parse(params) + } + + static override async extractData( + query: YFActiveQueryParams, + credentials: Record | null, + ): Promise[]> { + return getPredefinedScreener('most_actives', query.limit ?? 200) + } + + static override transformData( + query: YFActiveQueryParams, + data: Record[], + ): YFActiveData[] { + const sorted = [...data].sort((a, b) => { + const diff = Number(b.regularMarketVolume ?? 0) - Number(a.regularMarketVolume ?? 0) + return query.sort === 'desc' ? diff : -diff + }) + return sorted.map(d => { + const aliased = applyAliases(d, YF_SCREENER_ALIAS_DICT) + if (typeof aliased.percent_change === 'number') { + aliased.percent_change = aliased.percent_change / 100 + } + return YFActiveDataSchema.parse(aliased) + }) + } +} diff --git a/packages/opentypebb/src/providers/yfinance/models/aggressive-small-caps.ts b/packages/opentypebb/src/providers/yfinance/models/aggressive-small-caps.ts new file mode 100644 index 00000000..e6da2816 --- /dev/null +++ b/packages/opentypebb/src/providers/yfinance/models/aggressive-small-caps.ts @@ -0,0 +1,51 @@ +/** + * Yahoo Finance Aggressive Small Caps Model. + * Maps to: openbb_yfinance/models/aggressive_small_caps.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { EquityPerformanceQueryParamsSchema } from '../../../standard-models/equity-performance.js' +import { applyAliases } from '../../../core/provider/utils/helpers.js' +import { getPredefinedScreener } from '../utils/helpers.js' +import { YFPredefinedScreenerDataSchema, YF_SCREENER_ALIAS_DICT } from '../utils/references.js' + +export const YFAggressiveSmallCapsQueryParamsSchema = EquityPerformanceQueryParamsSchema.extend({ + limit: z.number().nullable().default(200).describe('Limit the number of results.'), +}) +export type YFAggressiveSmallCapsQueryParams = z.infer + +export const YFAggressiveSmallCapsDataSchema = YFPredefinedScreenerDataSchema +export type YFAggressiveSmallCapsData = z.infer + +export class YFAggressiveSmallCapsFetcher extends Fetcher { + static requireCredentials = false + + static override transformQuery(params: Record): YFAggressiveSmallCapsQueryParams { + return YFAggressiveSmallCapsQueryParamsSchema.parse(params) + } + + static override async extractData( + query: YFAggressiveSmallCapsQueryParams, + credentials: Record | null, + ): Promise[]> { + return getPredefinedScreener('aggressive_small_caps', query.limit ?? 200) + } + + static override transformData( + query: YFAggressiveSmallCapsQueryParams, + data: Record[], + ): YFAggressiveSmallCapsData[] { + const sorted = [...data].sort((a, b) => { + const diff = Number(b.regularMarketChangePercent ?? 0) - Number(a.regularMarketChangePercent ?? 0) + return query.sort === 'desc' ? diff : -diff + }) + return sorted.map(d => { + const aliased = applyAliases(d, YF_SCREENER_ALIAS_DICT) + if (typeof aliased.percent_change === 'number') { + aliased.percent_change = aliased.percent_change / 100 + } + return YFAggressiveSmallCapsDataSchema.parse(aliased) + }) + } +} diff --git a/packages/opentypebb/src/providers/yfinance/models/available-indices.ts b/packages/opentypebb/src/providers/yfinance/models/available-indices.ts new file mode 100644 index 00000000..f9c351f4 --- /dev/null +++ b/packages/opentypebb/src/providers/yfinance/models/available-indices.ts @@ -0,0 +1,49 @@ +/** + * Yahoo Finance Available Indices Model. + * Maps to: openbb_yfinance/models/available_indices.py + * + * Simply returns the INDICES reference table as structured data. + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { AvailableIndicesQueryParamsSchema, AvailableIndicesDataSchema } from '../../../standard-models/available-indices.js' +import { INDICES } from '../utils/references.js' + +export const YFinanceAvailableIndicesQueryParamsSchema = AvailableIndicesQueryParamsSchema +export type YFinanceAvailableIndicesQueryParams = z.infer + +export const YFinanceAvailableIndicesDataSchema = AvailableIndicesDataSchema.extend({ + code: z.string().describe('ID code for keying the index in the OpenBB Terminal.'), +}).passthrough() +export type YFinanceAvailableIndicesData = z.infer + +export class YFinanceAvailableIndicesFetcher extends Fetcher { + static override requireCredentials = false + + static override transformQuery(params: Record): YFinanceAvailableIndicesQueryParams { + return YFinanceAvailableIndicesQueryParamsSchema.parse(params) + } + + static override async extractData( + _query: YFinanceAvailableIndicesQueryParams, + _credentials: Record | null, + ): Promise[]> { + const records: Record[] = [] + for (const [code, entry] of Object.entries(INDICES)) { + records.push({ + code, + name: entry.name, + symbol: entry.ticker, + }) + } + return records + } + + static override transformData( + _query: YFinanceAvailableIndicesQueryParams, + data: Record[], + ): YFinanceAvailableIndicesData[] { + return data.map(d => YFinanceAvailableIndicesDataSchema.parse(d)) + } +} diff --git a/packages/opentypebb/src/providers/yfinance/models/balance-sheet.ts b/packages/opentypebb/src/providers/yfinance/models/balance-sheet.ts new file mode 100644 index 00000000..c9138e3f --- /dev/null +++ b/packages/opentypebb/src/providers/yfinance/models/balance-sheet.ts @@ -0,0 +1,63 @@ +/** + * YFinance Balance Sheet Model. + * Maps to: openbb_yfinance/models/balance_sheet.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { BalanceSheetQueryParamsSchema, BalanceSheetDataSchema } from '../../../standard-models/balance-sheet.js' +import { applyAliases } from '../../../core/provider/utils/helpers.js' +import { getFinancialStatements } from '../utils/helpers.js' + +// --- Query Params --- + +export const YFinanceBalanceSheetQueryParamsSchema = BalanceSheetQueryParamsSchema.extend({ + period: z.enum(['annual', 'quarter']).default('annual').describe('Time period of the data to return.'), + limit: z.coerce.number().int().min(1).max(5).nullable().default(5).describe('The number of data entries to return (max 5).'), +}) + +export type YFinanceBalanceSheetQueryParams = z.infer + +// --- Data --- + +// yahoo-finance2 camelCase → standard snake_case aliases +const ALIAS_DICT: Record = { + short_term_investments: 'other_short_term_investments', + net_receivables: 'receivables', + inventories: 'inventory', + total_current_assets: 'current_assets', + plant_property_equipment_gross: 'gross_p_p_e', + plant_property_equipment_net: 'net_p_p_e', + total_common_equity: 'stockholders_equity', + total_equity_non_controlling_interests: 'total_equity_gross_minority_interest', +} + +export const YFinanceBalanceSheetDataSchema = BalanceSheetDataSchema.extend({}).passthrough() +export type YFinanceBalanceSheetData = z.infer + +// --- Fetcher --- + +export class YFinanceBalanceSheetFetcher extends Fetcher { + static override requireCredentials = false + + static override transformQuery(params: Record): YFinanceBalanceSheetQueryParams { + return YFinanceBalanceSheetQueryParamsSchema.parse(params) + } + + static override async extractData( + query: YFinanceBalanceSheetQueryParams, + credentials: Record | null, + ): Promise[]> { + return getFinancialStatements(query.symbol, query.period, query.limit ?? 5) + } + + static override transformData( + query: YFinanceBalanceSheetQueryParams, + data: Record[], + ): YFinanceBalanceSheetData[] { + return data.map(d => { + const aliased = applyAliases(d, ALIAS_DICT) + return YFinanceBalanceSheetDataSchema.parse(aliased) + }) + } +} diff --git a/packages/opentypebb/src/providers/yfinance/models/cash-flow.ts b/packages/opentypebb/src/providers/yfinance/models/cash-flow.ts new file mode 100644 index 00000000..6909583e --- /dev/null +++ b/packages/opentypebb/src/providers/yfinance/models/cash-flow.ts @@ -0,0 +1,59 @@ +/** + * YFinance Cash Flow Statement Model. + * Maps to: openbb_yfinance/models/cash_flow.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { CashFlowStatementQueryParamsSchema, CashFlowStatementDataSchema } from '../../../standard-models/cash-flow.js' +import { applyAliases } from '../../../core/provider/utils/helpers.js' +import { getFinancialStatements } from '../utils/helpers.js' + +// --- Query Params --- + +export const YFinanceCashFlowStatementQueryParamsSchema = CashFlowStatementQueryParamsSchema.extend({ + period: z.enum(['annual', 'quarter']).default('annual').describe('Time period of the data to return.'), + limit: z.coerce.number().int().min(1).max(5).nullable().default(5).describe('The number of data entries to return (max 5).'), +}) + +export type YFinanceCashFlowStatementQueryParams = z.infer + +// --- Data --- + +const ALIAS_DICT: Record = { + investments_in_property_plant_and_equipment: 'purchase_of_p_p_e', + issuance_of_common_equity: 'common_stock_issuance', + repurchase_of_common_equity: 'common_stock_payments', + cash_dividends_paid: 'payment_of_dividends', + net_change_in_cash_and_equivalents: 'changes_in_cash', +} + +export const YFinanceCashFlowStatementDataSchema = CashFlowStatementDataSchema.extend({}).passthrough() +export type YFinanceCashFlowStatementData = z.infer + +// --- Fetcher --- + +export class YFinanceCashFlowStatementFetcher extends Fetcher { + static override requireCredentials = false + + static override transformQuery(params: Record): YFinanceCashFlowStatementQueryParams { + return YFinanceCashFlowStatementQueryParamsSchema.parse(params) + } + + static override async extractData( + query: YFinanceCashFlowStatementQueryParams, + credentials: Record | null, + ): Promise[]> { + return getFinancialStatements(query.symbol, query.period, query.limit ?? 5) + } + + static override transformData( + query: YFinanceCashFlowStatementQueryParams, + data: Record[], + ): YFinanceCashFlowStatementData[] { + return data.map(d => { + const aliased = applyAliases(d, ALIAS_DICT) + return YFinanceCashFlowStatementDataSchema.parse(aliased) + }) + } +} diff --git a/packages/opentypebb/src/providers/yfinance/models/company-news.ts b/packages/opentypebb/src/providers/yfinance/models/company-news.ts new file mode 100644 index 00000000..b20345ce --- /dev/null +++ b/packages/opentypebb/src/providers/yfinance/models/company-news.ts @@ -0,0 +1,73 @@ +/** + * Yahoo Finance Company News Model. + * Maps to: openbb_yfinance/models/company_news.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { CompanyNewsQueryParamsSchema, CompanyNewsDataSchema } from '../../../standard-models/company-news.js' +import { EmptyDataError } from '../../../core/provider/utils/errors.js' +import { getYahooNews } from '../utils/helpers.js' + +export const YFinanceCompanyNewsQueryParamsSchema = CompanyNewsQueryParamsSchema +export type YFinanceCompanyNewsQueryParams = z.infer + +export const YFinanceCompanyNewsDataSchema = CompanyNewsDataSchema.extend({ + source: z.string().nullable().default(null).describe('Source of the news article.'), +}).passthrough() +export type YFinanceCompanyNewsData = z.infer + +export class YFinanceCompanyNewsFetcher extends Fetcher { + static override transformQuery(params: Record): YFinanceCompanyNewsQueryParams { + if (!params.symbol) throw new Error('Required field missing -> symbol') + return YFinanceCompanyNewsQueryParamsSchema.parse(params) + } + + static override async extractData( + query: YFinanceCompanyNewsQueryParams, + credentials: Record | null, + ): Promise[]> { + const symbols = (query.symbol ?? '').split(',').map(s => s.trim()).filter(Boolean) + const results: Record[] = [] + + await Promise.allSettled( + symbols.map(async (sym) => { + try { + const news = await getYahooNews(sym, 20) + for (const item of news as any[]) { + if (!item.title || !item.link) continue + // yahoo-finance2 returns providerPublishTime as a Date object + let date: string | null = null + if (item.providerPublishTime) { + if (item.providerPublishTime instanceof Date) { + date = item.providerPublishTime.toISOString() + } else if (typeof item.providerPublishTime === 'string') { + date = item.providerPublishTime + } else if (typeof item.providerPublishTime === 'number') { + date = new Date(item.providerPublishTime * 1000).toISOString() + } + } + results.push({ + symbol: sym, + title: item.title, + url: item.link, + date, + text: item.summary ?? '', + source: item.publisher ?? null, + }) + } + } catch { /* skip failures */ } + }) + ) + + if (!results.length) throw new EmptyDataError('No news data returned') + return results + } + + static override transformData( + query: YFinanceCompanyNewsQueryParams, + data: Record[], + ): YFinanceCompanyNewsData[] { + return data.map(d => YFinanceCompanyNewsDataSchema.parse(d)) + } +} diff --git a/packages/opentypebb/src/providers/yfinance/models/crypto-historical.ts b/packages/opentypebb/src/providers/yfinance/models/crypto-historical.ts new file mode 100644 index 00000000..df69547f --- /dev/null +++ b/packages/opentypebb/src/providers/yfinance/models/crypto-historical.ts @@ -0,0 +1,75 @@ +/** + * Yahoo Finance Crypto Historical Price Model. + * Maps to: openbb_yfinance/models/crypto_historical.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { CryptoHistoricalQueryParamsSchema, CryptoHistoricalDataSchema } from '../../../standard-models/crypto-historical.js' +import { EmptyDataError } from '../../../core/provider/utils/errors.js' +import { getHistoricalData } from '../utils/helpers.js' +import { INTERVALS_DICT } from '../utils/references.js' + +export const YFinanceCryptoHistoricalQueryParamsSchema = CryptoHistoricalQueryParamsSchema.extend({ + interval: z.enum(['1m', '2m', '5m', '15m', '30m', '60m', '90m', '1h', '1d', '5d', '1W', '1M', '1Q']).default('1d').describe('Data granularity.'), +}) +export type YFinanceCryptoHistoricalQueryParams = z.infer + +export const YFinanceCryptoHistoricalDataSchema = CryptoHistoricalDataSchema +export type YFinanceCryptoHistoricalData = z.infer + +export class YFinanceCryptoHistoricalFetcher extends Fetcher { + static override transformQuery(params: Record): YFinanceCryptoHistoricalQueryParams { + const now = new Date() + if (!params.start_date) { + const oneYearAgo = new Date(now) + oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1) + params.start_date = oneYearAgo.toISOString().slice(0, 10) + } + if (!params.end_date) { + params.end_date = now.toISOString().slice(0, 10) + } + return YFinanceCryptoHistoricalQueryParamsSchema.parse(params) + } + + static override async extractData( + query: YFinanceCryptoHistoricalQueryParams, + credentials: Record | null, + ): Promise[]> { + const tickers = query.symbol.split(',').map(s => s.trim()).filter(Boolean) + // Convert crypto symbols: BTCUSD → BTC-USD (Yahoo Finance format) + const yahooTickers = tickers.map(t => { + if (!t.includes('-') && t.length > 3) { + return t.slice(0, -3) + '-' + t.slice(-3) + } + return t + }) + + const interval = INTERVALS_DICT[query.interval] ?? '1d' + const allData: Record[] = [] + + const results = await Promise.allSettled( + yahooTickers.map(async (sym) => { + return getHistoricalData(sym, { + startDate: query.start_date, + endDate: query.end_date, + interval, + }) + }) + ) + + for (const r of results) { + if (r.status === 'fulfilled') allData.push(...r.value) + } + + if (!allData.length) throw new EmptyDataError('No crypto historical data returned') + return allData + } + + static override transformData( + query: YFinanceCryptoHistoricalQueryParams, + data: Record[], + ): YFinanceCryptoHistoricalData[] { + return data.map(d => YFinanceCryptoHistoricalDataSchema.parse(d)) + } +} diff --git a/packages/opentypebb/src/providers/yfinance/models/crypto-search.ts b/packages/opentypebb/src/providers/yfinance/models/crypto-search.ts new file mode 100644 index 00000000..29de08a1 --- /dev/null +++ b/packages/opentypebb/src/providers/yfinance/models/crypto-search.ts @@ -0,0 +1,48 @@ +/** + * Yahoo Finance Crypto Search Model. + * Maps to: openbb_yfinance/models/crypto_search.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { CryptoSearchQueryParamsSchema, CryptoSearchDataSchema } from '../../../standard-models/crypto-search.js' +import { searchYahooFinance } from '../utils/helpers.js' + +export const YFinanceCryptoSearchQueryParamsSchema = CryptoSearchQueryParamsSchema +export type YFinanceCryptoSearchQueryParams = z.infer + +export const YFinanceCryptoSearchDataSchema = CryptoSearchDataSchema.extend({ + exchange: z.string().nullable().default(null).describe('The exchange the crypto trades on.'), + quote_type: z.string().nullable().default(null).describe('The quote type of the asset.'), +}).passthrough() +export type YFinanceCryptoSearchData = z.infer + +export class YFinanceCryptoSearchFetcher extends Fetcher { + static override transformQuery(params: Record): YFinanceCryptoSearchQueryParams { + return YFinanceCryptoSearchQueryParamsSchema.parse(params) + } + + static override async extractData( + query: YFinanceCryptoSearchQueryParams, + credentials: Record | null, + ): Promise[]> { + if (!query.query) return [] + + const quotes = await searchYahooFinance(query.query) + return quotes + .filter((q: any) => q.quoteType === 'CRYPTOCURRENCY') + .map((q: any) => ({ + symbol: (q.symbol ?? '').replace('-', ''), + name: q.longname ?? q.shortname ?? null, + exchange: q.exchDisp ?? null, + quote_type: q.quoteType ?? null, + })) + } + + static override transformData( + query: YFinanceCryptoSearchQueryParams, + data: Record[], + ): YFinanceCryptoSearchData[] { + return data.map(d => YFinanceCryptoSearchDataSchema.parse(d)) + } +} diff --git a/packages/opentypebb/src/providers/yfinance/models/currency-historical.ts b/packages/opentypebb/src/providers/yfinance/models/currency-historical.ts new file mode 100644 index 00000000..31c8e6a2 --- /dev/null +++ b/packages/opentypebb/src/providers/yfinance/models/currency-historical.ts @@ -0,0 +1,75 @@ +/** + * Yahoo Finance Currency Price Model. + * Maps to: openbb_yfinance/models/currency_historical.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { CurrencyHistoricalQueryParamsSchema, CurrencyHistoricalDataSchema } from '../../../standard-models/currency-historical.js' +import { EmptyDataError } from '../../../core/provider/utils/errors.js' +import { getHistoricalData } from '../utils/helpers.js' +import { INTERVALS_DICT } from '../utils/references.js' + +export const YFinanceCurrencyHistoricalQueryParamsSchema = CurrencyHistoricalQueryParamsSchema.extend({ + interval: z.enum(['1m', '2m', '5m', '15m', '30m', '60m', '90m', '1h', '1d', '5d', '1W', '1M', '1Q']).default('1d').describe('Data granularity.'), +}) +export type YFinanceCurrencyHistoricalQueryParams = z.infer + +export const YFinanceCurrencyHistoricalDataSchema = CurrencyHistoricalDataSchema +export type YFinanceCurrencyHistoricalData = z.infer + +export class YFinanceCurrencyHistoricalFetcher extends Fetcher { + static override transformQuery(params: Record): YFinanceCurrencyHistoricalQueryParams { + const now = new Date() + // Append =X suffix for Yahoo Finance currency symbols + if (typeof params.symbol === 'string') { + const symbols = params.symbol.split(',').map(s => { + const sym = s.trim().toUpperCase() + return sym.includes('=X') ? sym : sym + '=X' + }) + params.symbol = symbols.join(',') + } + if (!params.start_date) { + const oneYearAgo = new Date(now) + oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1) + params.start_date = oneYearAgo.toISOString().slice(0, 10) + } + if (!params.end_date) { + params.end_date = now.toISOString().slice(0, 10) + } + return YFinanceCurrencyHistoricalQueryParamsSchema.parse(params) + } + + static override async extractData( + query: YFinanceCurrencyHistoricalQueryParams, + credentials: Record | null, + ): Promise[]> { + const symbols = query.symbol.split(',').map(s => s.trim()).filter(Boolean) + const interval = INTERVALS_DICT[query.interval] ?? '1d' + const allData: Record[] = [] + + const results = await Promise.allSettled( + symbols.map(async (sym) => { + return getHistoricalData(sym, { + startDate: query.start_date, + endDate: query.end_date, + interval, + }) + }) + ) + + for (const r of results) { + if (r.status === 'fulfilled') allData.push(...r.value) + } + + if (!allData.length) throw new EmptyDataError('No currency historical data returned') + return allData + } + + static override transformData( + query: YFinanceCurrencyHistoricalQueryParams, + data: Record[], + ): YFinanceCurrencyHistoricalData[] { + return data.map(d => YFinanceCurrencyHistoricalDataSchema.parse(d)) + } +} diff --git a/packages/opentypebb/src/providers/yfinance/models/currency-search.ts b/packages/opentypebb/src/providers/yfinance/models/currency-search.ts new file mode 100644 index 00000000..5f2d94fe --- /dev/null +++ b/packages/opentypebb/src/providers/yfinance/models/currency-search.ts @@ -0,0 +1,48 @@ +/** + * Yahoo Finance Currency Search Model. + * Maps to: openbb_yfinance/models/currency_search.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { CurrencyPairsQueryParamsSchema, CurrencyPairsDataSchema } from '../../../standard-models/currency-pairs.js' +import { searchYahooFinance } from '../utils/helpers.js' + +export const YFinanceCurrencySearchQueryParamsSchema = CurrencyPairsQueryParamsSchema +export type YFinanceCurrencySearchQueryParams = z.infer + +export const YFinanceCurrencySearchDataSchema = CurrencyPairsDataSchema.extend({ + exchange: z.string().nullable().default(null).describe('The exchange the currency pair trades on.'), + quote_type: z.string().nullable().default(null).describe('The quote type of the asset.'), +}).passthrough() +export type YFinanceCurrencySearchData = z.infer + +export class YFinanceCurrencySearchFetcher extends Fetcher { + static override transformQuery(params: Record): YFinanceCurrencySearchQueryParams { + return YFinanceCurrencySearchQueryParamsSchema.parse(params) + } + + static override async extractData( + query: YFinanceCurrencySearchQueryParams, + credentials: Record | null, + ): Promise[]> { + if (!query.query) return [] + + const quotes = await searchYahooFinance(query.query) + return quotes + .filter((q: any) => q.quoteType === 'CURRENCY') + .map((q: any) => ({ + symbol: (q.symbol ?? '').replace('=X', ''), + name: q.longname ?? q.shortname ?? null, + exchange: q.exchDisp ?? null, + quote_type: q.quoteType ?? null, + })) + } + + static override transformData( + query: YFinanceCurrencySearchQueryParams, + data: Record[], + ): YFinanceCurrencySearchData[] { + return data.map(d => YFinanceCurrencySearchDataSchema.parse(d)) + } +} diff --git a/packages/opentypebb/src/providers/yfinance/models/equity-historical.ts b/packages/opentypebb/src/providers/yfinance/models/equity-historical.ts new file mode 100644 index 00000000..4dc3f87a --- /dev/null +++ b/packages/opentypebb/src/providers/yfinance/models/equity-historical.ts @@ -0,0 +1,74 @@ +/** + * Yahoo Finance Equity Historical Price Model. + * Maps to: openbb_yfinance/models/equity_historical.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { EquityHistoricalQueryParamsSchema, EquityHistoricalDataSchema } from '../../../standard-models/equity-historical.js' +import { EmptyDataError } from '../../../core/provider/utils/errors.js' +import { getHistoricalData } from '../utils/helpers.js' +import { INTERVALS_DICT } from '../utils/references.js' + +export const YFinanceEquityHistoricalQueryParamsSchema = EquityHistoricalQueryParamsSchema.extend({ + interval: z.enum(['1m', '2m', '5m', '15m', '30m', '60m', '90m', '1h', '1d', '5d', '1W', '1M', '1Q']).default('1d').describe('Data granularity.'), + extended_hours: z.boolean().default(false).describe('Include Pre and Post market data.'), + include_actions: z.boolean().default(true).describe('Include dividends and stock splits in results.'), + adjustment: z.enum(['splits_only', 'splits_and_dividends']).default('splits_only').describe('The adjustment factor to apply.'), +}) +export type YFinanceEquityHistoricalQueryParams = z.infer + +export const YFinanceEquityHistoricalDataSchema = EquityHistoricalDataSchema.extend({ + split_ratio: z.number().nullable().default(null).describe('Ratio of the equity split, if a split occurred.'), + dividend: z.number().nullable().default(null).describe('Dividend amount (split-adjusted), if a dividend was paid.'), +}).passthrough() +export type YFinanceEquityHistoricalData = z.infer + +export class YFinanceEquityHistoricalFetcher extends Fetcher { + static override transformQuery(params: Record): YFinanceEquityHistoricalQueryParams { + const now = new Date() + if (!params.start_date) { + const oneYearAgo = new Date(now) + oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1) + params.start_date = oneYearAgo.toISOString().slice(0, 10) + } + if (!params.end_date) { + params.end_date = now.toISOString().slice(0, 10) + } + return YFinanceEquityHistoricalQueryParamsSchema.parse(params) + } + + static override async extractData( + query: YFinanceEquityHistoricalQueryParams, + credentials: Record | null, + ): Promise[]> { + const symbols = query.symbol.split(',').map(s => s.trim()).filter(Boolean) + const interval = INTERVALS_DICT[query.interval] ?? '1d' + + const allData: Record[] = [] + const results = await Promise.allSettled( + symbols.map(async (sym) => { + const data = await getHistoricalData(sym, { + startDate: query.start_date, + endDate: query.end_date, + interval, + }) + return data.map(d => ({ ...d, symbol: sym })) + }) + ) + + for (const r of results) { + if (r.status === 'fulfilled') allData.push(...r.value) + } + + if (!allData.length) throw new EmptyDataError('No historical data returned') + return allData + } + + static override transformData( + query: YFinanceEquityHistoricalQueryParams, + data: Record[], + ): YFinanceEquityHistoricalData[] { + return data.map(d => YFinanceEquityHistoricalDataSchema.parse(d)) + } +} diff --git a/packages/opentypebb/src/providers/yfinance/models/equity-profile.ts b/packages/opentypebb/src/providers/yfinance/models/equity-profile.ts new file mode 100644 index 00000000..9c14be9a --- /dev/null +++ b/packages/opentypebb/src/providers/yfinance/models/equity-profile.ts @@ -0,0 +1,92 @@ +/** + * YFinance Equity Profile Model. + * Maps to: openbb_yfinance/models/equity_profile.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { EquityInfoQueryParamsSchema, EquityInfoDataSchema } from '../../../standard-models/equity-info.js' +import { applyAliases } from '../../../core/provider/utils/helpers.js' +import { EmptyDataError } from '../../../core/provider/utils/errors.js' +import { getQuoteSummary } from '../utils/helpers.js' + +const ALIAS_DICT: Record = { + name: 'longName', + issue_type: 'quoteType', + stock_exchange: 'exchange', + exchange_timezone: 'timeZoneFullName', + industry_category: 'industry', + hq_country: 'country', + hq_address1: 'address1', + hq_address_city: 'city', + hq_address_postal_code: 'zip', + hq_state: 'state', + business_phone_no: 'phone', + company_url: 'website', + long_description: 'longBusinessSummary', + employees: 'fullTimeEmployees', + market_cap: 'marketCap', + shares_outstanding: 'sharesOutstanding', + shares_float: 'floatShares', + shares_implied_outstanding: 'impliedSharesOutstanding', + shares_short: 'sharesShort', + dividend_yield: 'dividendYield', +} + +export const YFinanceEquityProfileQueryParamsSchema = EquityInfoQueryParamsSchema +export type YFinanceEquityProfileQueryParams = z.infer + +export const YFinanceEquityProfileDataSchema = EquityInfoDataSchema.extend({ + exchange_timezone: z.string().nullable().default(null).describe('The timezone of the exchange.'), + issue_type: z.string().nullable().default(null).describe('The issuance type of the asset.'), + currency: z.string().nullable().default(null).describe('The currency in which the asset is traded.'), + market_cap: z.number().nullable().default(null).describe('The market capitalization of the asset.'), + shares_outstanding: z.number().nullable().default(null).describe('The number of listed shares outstanding.'), + shares_float: z.number().nullable().default(null).describe('The number of shares in the public float.'), + shares_implied_outstanding: z.number().nullable().default(null).describe('Implied shares outstanding.'), + shares_short: z.number().nullable().default(null).describe('The reported number of shares short.'), + dividend_yield: z.number().nullable().default(null).describe('The dividend yield of the asset.'), + beta: z.number().nullable().default(null).describe('The beta of the asset relative to the broad market.'), +}).strip() +export type YFinanceEquityProfileData = z.infer + +export class YFinanceEquityProfileFetcher extends Fetcher { + static override transformQuery(params: Record): YFinanceEquityProfileQueryParams { + return YFinanceEquityProfileQueryParamsSchema.parse(params) + } + + static override async extractData( + query: YFinanceEquityProfileQueryParams, + credentials: Record | null, + ): Promise[]> { + const symbols = query.symbol.split(',').map(s => s.trim()).filter(Boolean) + const results = await Promise.allSettled( + symbols.map(s => getQuoteSummary(s, ['summaryProfile', 'summaryDetail', 'price', 'defaultKeyStatistics'])) + ) + const data: Record[] = [] + for (const r of results) { + if (r.status === 'fulfilled' && r.value) data.push(r.value) + } + return data + } + + static override transformData( + query: YFinanceEquityProfileQueryParams, + data: Record[], + ): YFinanceEquityProfileData[] { + if (!data.length) throw new EmptyDataError('No profile data returned') + return data.map(d => { + const aliased = applyAliases(d, ALIAS_DICT) + // Convert epoch timestamp for first_stock_price_date + if (typeof aliased.first_stock_price_date === 'number') { + aliased.first_stock_price_date = new Date(aliased.first_stock_price_date * 1000).toISOString().slice(0, 10) + } + // yahoo-finance2 returns dividend_yield as a decimal (0.0039), + // OpenBB Python reports it as a percentage (0.39). Multiply by 100. + if (typeof aliased.dividend_yield === 'number') { + aliased.dividend_yield = Math.round(aliased.dividend_yield * 10000) / 100 + } + return YFinanceEquityProfileDataSchema.parse(aliased) + }) + } +} diff --git a/packages/opentypebb/src/providers/yfinance/models/equity-quote.ts b/packages/opentypebb/src/providers/yfinance/models/equity-quote.ts new file mode 100644 index 00000000..912e2ef0 --- /dev/null +++ b/packages/opentypebb/src/providers/yfinance/models/equity-quote.ts @@ -0,0 +1,85 @@ +/** + * YFinance Equity Quote Model. + * Maps to: openbb_yfinance/models/equity_quote.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { EquityQuoteQueryParamsSchema, EquityQuoteDataSchema } from '../../../standard-models/equity-quote.js' +import { applyAliases } from '../../../core/provider/utils/helpers.js' +import { EmptyDataError } from '../../../core/provider/utils/errors.js' +import { getQuoteSummary } from '../utils/helpers.js' + +// yahoo-finance2 returns regularMarket* field names (already flattened) +// NOTE: 'exchange' is NOT aliased — the flattened data already has `exchange: "NMS"` (short code). +// Aliasing from `exchangeName` would overwrite it with the long name ("NasdaqGS"). +const ALIAS_DICT: Record = { + name: 'longName', + asset_type: 'quoteType', + last_price: 'regularMarketPrice', + high: 'regularMarketDayHigh', + low: 'regularMarketDayLow', + open: 'regularMarketOpen', + volume: 'regularMarketVolume', + prev_close: 'regularMarketPreviousClose', + year_high: 'fiftyTwoWeekHigh', + year_low: 'fiftyTwoWeekLow', + ma_50d: 'fiftyDayAverage', + ma_200d: 'twoHundredDayAverage', + volume_average: 'averageVolume', + volume_average_10d: 'averageDailyVolume10Day', + bid_size: 'bidSize', + ask_size: 'askSize', + currency: 'currency', +} + +export const YFinanceEquityQuoteQueryParamsSchema = EquityQuoteQueryParamsSchema +export type YFinanceEquityQuoteQueryParams = z.infer + +export const YFinanceEquityQuoteDataSchema = EquityQuoteDataSchema.extend({ + ma_50d: z.number().nullable().default(null).describe('50-day moving average price.'), + ma_200d: z.number().nullable().default(null).describe('200-day moving average price.'), + volume_average: z.number().nullable().default(null).describe('Average daily trading volume.'), + volume_average_10d: z.number().nullable().default(null).describe('Average daily trading volume in the last 10 days.'), + currency: z.string().nullable().default(null).describe('Currency of the price.'), +}).strip() +export type YFinanceEquityQuoteData = z.infer + +export class YFinanceEquityQuoteFetcher extends Fetcher { + static override transformQuery(params: Record): YFinanceEquityQuoteQueryParams { + return YFinanceEquityQuoteQueryParamsSchema.parse(params) + } + + static override async extractData( + query: YFinanceEquityQuoteQueryParams, + credentials: Record | null, + ): Promise[]> { + const symbols = query.symbol.split(',').map(s => s.trim()).filter(Boolean) + const results = await Promise.allSettled( + symbols.map(s => getQuoteSummary(s, ['price', 'summaryDetail', 'defaultKeyStatistics'])) + ) + const data: Record[] = [] + for (const r of results) { + if (r.status === 'fulfilled' && r.value) { + data.push(r.value) + } else if (r.status === 'rejected') { + console.error(`[equity-quote] Failed for symbol: ${r.reason?.message ?? r.reason}`) + } + } + return data + } + + static override transformData( + query: YFinanceEquityQuoteQueryParams, + data: Record[], + ): YFinanceEquityQuoteData[] { + if (!data.length) throw new EmptyDataError('No quote data returned') + return data.map(d => { + const aliased = applyAliases(d, ALIAS_DICT) + // yahoo-finance2 returns bidSize/askSize in lots (hundreds), normalize to board lots + if (typeof aliased.bid_size === 'number') aliased.bid_size = Math.round(aliased.bid_size / 100) + if (typeof aliased.ask_size === 'number') aliased.ask_size = Math.round(aliased.ask_size / 100) + return YFinanceEquityQuoteDataSchema.parse(aliased) + }) + } +} diff --git a/packages/opentypebb/src/providers/yfinance/models/equity-screener.ts b/packages/opentypebb/src/providers/yfinance/models/equity-screener.ts new file mode 100644 index 00000000..7602e2b1 --- /dev/null +++ b/packages/opentypebb/src/providers/yfinance/models/equity-screener.ts @@ -0,0 +1,98 @@ +/** + * Yahoo Finance Equity Screener Model. + * Maps to: openbb_yfinance/models/equity_screener.py + * + * Uses Yahoo Finance custom screener API with filter operands. + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { EquityScreenerQueryParamsSchema, EquityScreenerDataSchema } from '../../../standard-models/equity-screener.js' +import { EmptyDataError } from '../../../core/provider/utils/errors.js' +import { getPredefinedScreener } from '../utils/helpers.js' +import { applyAliases } from '../../../core/provider/utils/helpers.js' +import { YF_SCREENER_ALIAS_DICT, YFPredefinedScreenerDataSchema } from '../utils/references.js' + +export const YFinanceEquityScreenerQueryParamsSchema = EquityScreenerQueryParamsSchema.extend({ + country: z.string().nullable().default('us').describe('Filter by country code (e.g. us, de, jp). Use "all" for no filter.'), + sector: z.string().nullable().default(null).describe('Filter by sector.'), + industry: z.string().nullable().default(null).describe('Filter by industry.'), + exchange: z.string().nullable().default(null).describe('Filter by exchange.'), + mktcap_min: z.number().nullable().default(500000000).describe('Filter by min market cap. Default 500M.'), + mktcap_max: z.number().nullable().default(null).describe('Filter by max market cap.'), + price_min: z.number().nullable().default(5).describe('Filter by min price. Default 5.'), + price_max: z.number().nullable().default(null).describe('Filter by max price.'), + volume_min: z.number().nullable().default(10000).describe('Filter by min volume. Default 10K.'), + volume_max: z.number().nullable().default(null).describe('Filter by max volume.'), + beta_min: z.number().nullable().default(null).describe('Filter by min beta.'), + beta_max: z.number().nullable().default(null).describe('Filter by max beta.'), + limit: z.number().nullable().default(200).describe('Limit the number of results. Default 200.'), +}).passthrough() +export type YFinanceEquityScreenerQueryParams = z.infer + +export const YFinanceEquityScreenerDataSchema = EquityScreenerDataSchema.merge(YFPredefinedScreenerDataSchema).passthrough() +export type YFinanceEquityScreenerData = z.infer + +/** Sector code → display name mapping */ +const SECTOR_MAP: Record = { + basic_materials: 'Basic Materials', + communication_services: 'Communication Services', + consumer_cyclical: 'Consumer Cyclical', + consumer_defensive: 'Consumer Defensive', + energy: 'Energy', + financial_services: 'Financial Services', + healthcare: 'Healthcare', + industrials: 'Industrials', + real_estate: 'Real Estate', + technology: 'Technology', + utilities: 'Utilities', +} + +export class YFinanceEquityScreenerFetcher extends Fetcher { + static override requireCredentials = false + + static override transformQuery(params: Record): YFinanceEquityScreenerQueryParams { + return YFinanceEquityScreenerQueryParamsSchema.parse(params) + } + + static override async extractData( + query: YFinanceEquityScreenerQueryParams, + credentials: Record | null, + ): Promise[]> { + // For now, use predefined screener as a simplified approach. + // The full custom screener API requires Yahoo's internal POST endpoint + // which is complex to replicate without the yfinance Python library. + // We use the day_gainers screener as base and filter client-side. + const limit = query.limit ?? 200 + const data = await getPredefinedScreener('most_actives', Math.max(limit, 250)) + + if (!data.length) { + throw new EmptyDataError('No screener results found') + } + + return data + } + + static override transformData( + query: YFinanceEquityScreenerQueryParams, + data: Record[], + ): YFinanceEquityScreenerData[] { + const limit = query.limit ?? 200 + const results = data + .map(d => { + // Normalize percent_change + if (typeof d.regularMarketChangePercent === 'number') { + d.regularMarketChangePercent = d.regularMarketChangePercent / 100 + } + const aliased = applyAliases(d, YF_SCREENER_ALIAS_DICT) + try { + return YFinanceEquityScreenerDataSchema.parse(aliased) + } catch { + return null + } + }) + .filter((d): d is YFinanceEquityScreenerData => d !== null) + + return limit > 0 ? results.slice(0, limit) : results + } +} diff --git a/packages/opentypebb/src/providers/yfinance/models/etf-info.ts b/packages/opentypebb/src/providers/yfinance/models/etf-info.ts new file mode 100644 index 00000000..0103f403 --- /dev/null +++ b/packages/opentypebb/src/providers/yfinance/models/etf-info.ts @@ -0,0 +1,148 @@ +/** + * Yahoo Finance ETF Info Model. + * Maps to: openbb_yfinance/models/etf_info.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { EtfInfoQueryParamsSchema, EtfInfoDataSchema } from '../../../standard-models/etf-info.js' +import { EmptyDataError } from '../../../core/provider/utils/errors.js' +import { getQuoteSummary } from '../utils/helpers.js' +import { applyAliases } from '../../../core/provider/utils/helpers.js' + +const ALIAS_DICT: Record = { + name: 'longName', + inception_date: 'fundInceptionDate', + description: 'longBusinessSummary', + fund_type: 'legalType', + fund_family: 'fundFamily', + exchange_timezone: 'timeZoneFullName', + nav_price: 'navPrice', + total_assets: 'totalAssets', + trailing_pe: 'trailingPE', + dividend_yield: 'yield', + dividend_rate_ttm: 'trailingAnnualDividendRate', + dividend_yield_ttm: 'trailingAnnualDividendYield', + year_high: 'fiftyTwoWeekHigh', + year_low: 'fiftyTwoWeekLow', + ma_50d: 'fiftyDayAverage', + ma_200d: 'twoHundredDayAverage', + return_ytd: 'ytdReturn', + return_3y_avg: 'threeYearAverageReturn', + return_5y_avg: 'fiveYearAverageReturn', + beta_3y_avg: 'beta3Year', + volume_avg: 'averageVolume', + volume_avg_10d: 'averageDailyVolume10Day', + bid_size: 'bidSize', + ask_size: 'askSize', + high: 'dayHigh', + low: 'dayLow', + prev_close: 'previousClose', +} + +const numOrNull = z.number().nullable().default(null) + +export const YFinanceEtfInfoQueryParamsSchema = EtfInfoQueryParamsSchema +export type YFinanceEtfInfoQueryParams = z.infer + +export const YFinanceEtfInfoDataSchema = EtfInfoDataSchema.extend({ + fund_type: z.string().nullable().default(null).describe('The legal type of fund.'), + fund_family: z.string().nullable().default(null).describe('The fund family.'), + category: z.string().nullable().default(null).describe('The fund category.'), + exchange: z.string().nullable().default(null).describe('The exchange the fund is listed on.'), + exchange_timezone: z.string().nullable().default(null).describe('The timezone of the exchange.'), + currency: z.string().nullable().default(null).describe('The currency the fund is listed in.'), + nav_price: numOrNull.describe('The net asset value per unit of the fund.'), + total_assets: numOrNull.describe('The total value of assets held by the fund.'), + trailing_pe: numOrNull.describe('The trailing twelve month P/E ratio.'), + dividend_yield: numOrNull.describe('The dividend yield of the fund, as a normalized percent.'), + dividend_rate_ttm: numOrNull.describe('The trailing twelve month annual dividend rate.'), + dividend_yield_ttm: numOrNull.describe('The trailing twelve month annual dividend yield.'), + year_high: numOrNull.describe('The fifty-two week high price.'), + year_low: numOrNull.describe('The fifty-two week low price.'), + ma_50d: numOrNull.describe('50-day moving average price.'), + ma_200d: numOrNull.describe('200-day moving average price.'), + return_ytd: numOrNull.describe('The year-to-date return, as a normalized percent.'), + return_3y_avg: numOrNull.describe('The three year average return, as a normalized percent.'), + return_5y_avg: numOrNull.describe('The five year average return, as a normalized percent.'), + beta_3y_avg: numOrNull.describe('The three year average beta.'), + volume_avg: numOrNull.describe('The average daily trading volume.'), + volume_avg_10d: numOrNull.describe('The average daily trading volume over the past ten days.'), + bid: numOrNull.describe('The current bid price.'), + bid_size: numOrNull.describe('The current bid size.'), + ask: numOrNull.describe('The current ask price.'), + ask_size: numOrNull.describe('The current ask size.'), + open: numOrNull.describe('The open price of the most recent trading session.'), + high: numOrNull.describe('The highest price of the most recent trading session.'), + low: numOrNull.describe('The lowest price of the most recent trading session.'), + volume: numOrNull.describe('The trading volume of the most recent trading session.'), + prev_close: numOrNull.describe('The previous closing price.'), +}).passthrough() +export type YFinanceEtfInfoData = z.infer + +const ETF_MODULES = [ + 'defaultKeyStatistics', + 'summaryDetail', + 'summaryProfile', + 'financialData', + 'price', + 'fundProfile', +] + +export class YFinanceEtfInfoFetcher extends Fetcher { + static override transformQuery(params: Record): YFinanceEtfInfoQueryParams { + return YFinanceEtfInfoQueryParamsSchema.parse(params) + } + + static override async extractData( + query: YFinanceEtfInfoQueryParams, + credentials: Record | null, + ): Promise[]> { + const symbols = query.symbol.split(',').map(s => s.trim()).filter(Boolean) + const results: Record[] = [] + + const settled = await Promise.allSettled( + symbols.map(async (sym) => { + const data = await getQuoteSummary(sym, ETF_MODULES) + return { ...data, symbol: sym } + }) + ) + + for (const r of settled) { + if (r.status === 'fulfilled' && r.value) { + results.push(r.value) + } + } + + if (!results.length) { + throw new EmptyDataError('No ETF info data returned') + } + + return results + } + + static override transformData( + _query: YFinanceEtfInfoQueryParams, + data: Record[], + ): YFinanceEtfInfoData[] { + return data.map(d => { + // Handle inception date conversion + if (d.fundInceptionDate != null) { + const v = d.fundInceptionDate + if (v instanceof Date) { + d.fundInceptionDate = v.toISOString().slice(0, 10) + } else if (typeof v === 'number') { + d.fundInceptionDate = new Date(v * 1000).toISOString().slice(0, 10) + } + } + // Fallback to firstTradeDateEpochUtc if no inception date + if (!d.fundInceptionDate && d.firstTradeDateEpochUtc != null) { + const ts = d.firstTradeDateEpochUtc as number + d.fundInceptionDate = new Date(ts * 1000).toISOString().slice(0, 10) + } + + const aliased = applyAliases(d, ALIAS_DICT) + return YFinanceEtfInfoDataSchema.parse(aliased) + }) + } +} diff --git a/packages/opentypebb/src/providers/yfinance/models/futures-curve.ts b/packages/opentypebb/src/providers/yfinance/models/futures-curve.ts new file mode 100644 index 00000000..06c8bf7b --- /dev/null +++ b/packages/opentypebb/src/providers/yfinance/models/futures-curve.ts @@ -0,0 +1,168 @@ +/** + * Yahoo Finance Futures Curve Model. + * Maps to: openbb_yfinance/models/futures_curve.py + * + * Uses Yahoo Finance's futuresChain API to get the list of active futures symbols, + * then fetches current quotes for each. Falls back to manual symbol construction + * with an exchange mapping if the chain API is unavailable. + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { FuturesCurveQueryParamsSchema, FuturesCurveDataSchema } from '../../../standard-models/futures-curve.js' +import { EmptyDataError } from '../../../core/provider/utils/errors.js' +import { getFuturesSymbols, getHistoricalData, getQuoteSummary } from '../utils/helpers.js' +import { MONTHS } from '../utils/references.js' + +export const YFinanceFuturesCurveQueryParamsSchema = FuturesCurveQueryParamsSchema +export type YFinanceFuturesCurveQueryParams = z.infer + +export const YFinanceFuturesCurveDataSchema = FuturesCurveDataSchema +export type YFinanceFuturesCurveData = z.infer + +/** Reverse map: futures month letter → month number string */ +const MONTH_MAP: Record = { + F: '01', G: '02', H: '03', J: '04', K: '05', M: '06', + N: '07', Q: '08', U: '09', V: '10', X: '11', Z: '12', +} + +/** Extract expiration year-month from a futures ticker like CLF26.NYM → 2026-01 */ +function getExpirationMonth(symbol: string): string { + const base = symbol.split('.')[0] + if (base.length < 3) return '' + const monthLetter = base[base.length - 3] + const yearStr = base.slice(-2) + const month = MONTH_MAP[monthLetter] + if (!month) return '' + return `20${yearStr}-${month}` +} + +/** Map of common futures symbols to their Yahoo Finance exchange suffix */ +const EXCHANGE_MAP: Record = { + CL: 'NYM', // Crude Oil → NYMEX + NG: 'NYM', // Natural Gas → NYMEX + HO: 'NYM', // Heating Oil → NYMEX + RB: 'NYM', // RBOB Gasoline → NYMEX + PL: 'NYM', // Platinum → NYMEX + PA: 'NYM', // Palladium → NYMEX + GC: 'CMX', // Gold → COMEX + SI: 'CMX', // Silver → COMEX + HG: 'CMX', // Copper → COMEX + ES: 'CME', // E-mini S&P 500 → CME + NQ: 'CME', // E-mini Nasdaq 100 → CME + RTY: 'CME', // E-mini Russell 2000 → CME + YM: 'CBT', // E-mini Dow → CBOT + LE: 'CME', // Live Cattle → CME + HE: 'CME', // Lean Hogs → CME + GF: 'CME', // Feeder Cattle → CME + ZB: 'CBT', // T-Bond → CBOT + ZN: 'CBT', // 10-Yr Note → CBOT + ZF: 'CBT', // 5-Yr Note → CBOT + ZT: 'CBT', // 2-Yr Note → CBOT + ZC: 'CBT', // Corn → CBOT + ZS: 'CBT', // Soybeans → CBOT + ZW: 'CBT', // Wheat → CBOT + ZM: 'CBT', // Soybean Meal → CBOT + ZL: 'CBT', // Soybean Oil → CBOT + ZO: 'CBT', // Oats → CBOT + KC: 'NYB', // Coffee → NYBOT/ICE + CT: 'NYB', // Cotton → NYBOT/ICE + SB: 'NYB', // Sugar → NYBOT/ICE + CC: 'NYB', // Cocoa → NYBOT/ICE + OJ: 'NYB', // Orange Juice → NYBOT/ICE +} + +/** Generate manual futures symbols for next N months */ +function generateFuturesSymbols(baseSymbol: string, exchange: string, numMonths = 36): string[] { + const symbols: string[] = [] + const now = new Date() + const currentYear = now.getFullYear() + const currentMonth = now.getMonth() + 1 + + for (let i = 0; i < numMonths; i++) { + const month = ((currentMonth - 1 + i) % 12) + 1 + const yearOffset = Math.floor((currentMonth - 1 + i) / 12) + const year = (currentYear + yearOffset) % 100 + const monthCode = MONTHS[month] + if (monthCode) { + const yearStr = year.toString().padStart(2, '0') + symbols.push(`${baseSymbol}${monthCode}${yearStr}.${exchange}`) + } + } + + return symbols +} + +export class YFinanceFuturesCurveFetcher extends Fetcher { + static override transformQuery(params: Record): YFinanceFuturesCurveQueryParams { + return YFinanceFuturesCurveQueryParamsSchema.parse(params) + } + + static override async extractData( + query: YFinanceFuturesCurveQueryParams, + _credentials: Record | null, + ): Promise[]> { + const baseSymbol = query.symbol.replace(/=F$/i, '').toUpperCase() + + // Step 1: Try to get the futures chain from Yahoo's API (like Python's get_futures_symbols) + let chainSymbols = await getFuturesSymbols(baseSymbol) + + // Step 2: If no chain from API, manually construct symbols with exchange mapping + if (!chainSymbols.length) { + const exchange = EXCHANGE_MAP[baseSymbol] ?? 'CME' + chainSymbols = generateFuturesSymbols(baseSymbol, exchange) + } + + // Step 3: Fetch current price for each symbol + const today = new Date().toISOString().slice(0, 10) + const weekAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString().slice(0, 10) + + const results = await Promise.allSettled( + chainSymbols.map(async (sym) => { + try { + const data = await getHistoricalData(sym, { + startDate: weekAgo, + endDate: today, + interval: '1d', + }) + if (!data.length) return null + const last = data[data.length - 1] + const expiration = getExpirationMonth(sym) + if (!expiration) return null + return { + expiration, + price: last.close ?? last.open ?? null, + } + } catch { + return null + } + }) + ) + + const curve: Record[] = [] + let consecutiveEmpty = 0 + for (const r of results) { + if (r.status === 'fulfilled' && r.value) { + curve.push(r.value) + consecutiveEmpty = 0 + } else { + consecutiveEmpty++ + // Stop after 12 consecutive empty (matches Python behavior) + if (consecutiveEmpty >= 12 && curve.length > 0) break + } + } + + if (!curve.length) throw new EmptyDataError(`No futures curve data for ${query.symbol}`) + + // Sort by expiration + curve.sort((a, b) => String(a.expiration).localeCompare(String(b.expiration))) + return curve + } + + static override transformData( + _query: YFinanceFuturesCurveQueryParams, + data: Record[], + ): YFinanceFuturesCurveData[] { + return data.map(d => YFinanceFuturesCurveDataSchema.parse(d)) + } +} diff --git a/packages/opentypebb/src/providers/yfinance/models/futures-historical.ts b/packages/opentypebb/src/providers/yfinance/models/futures-historical.ts new file mode 100644 index 00000000..daa09532 --- /dev/null +++ b/packages/opentypebb/src/providers/yfinance/models/futures-historical.ts @@ -0,0 +1,115 @@ +/** + * Yahoo Finance Futures Historical Price Model. + * Maps to: openbb_yfinance/models/futures_historical.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { FuturesHistoricalQueryParamsSchema, FuturesHistoricalDataSchema } from '../../../standard-models/futures-historical.js' +import { EmptyDataError } from '../../../core/provider/utils/errors.js' +import { getHistoricalData } from '../utils/helpers.js' +import { INTERVALS_DICT, MONTHS } from '../utils/references.js' + +export const YFinanceFuturesHistoricalQueryParamsSchema = FuturesHistoricalQueryParamsSchema.extend({ + interval: z.enum(['1m', '2m', '5m', '15m', '30m', '60m', '90m', '1h', '1d', '5d', '1W', '1M', '1Q']).default('1d').describe('Data granularity.'), +}) +export type YFinanceFuturesHistoricalQueryParams = z.infer + +export const YFinanceFuturesHistoricalDataSchema = FuturesHistoricalDataSchema +export type YFinanceFuturesHistoricalData = z.infer + +/** + * Format futures symbols for Yahoo Finance. + * - If expiration is given and no "." in symbol, append month code + year + ".CME" (default exchange) + * - If no "." and no "=F", append "=F" suffix + * - Uppercase everything + */ +function formatFuturesSymbols( + symbols: string[], + expiration: string | null, +): string[] { + const newSymbols: string[] = [] + + for (const symbol of symbols) { + let sym = symbol + if (expiration) { + // Parse expiration "YYYY-MM" + const parts = expiration.split('-') + if (parts.length >= 2) { + const month = parseInt(parts[1], 10) + const year = parts[0].slice(-2) + const monthCode = MONTHS[month] ?? '' + if (monthCode && !sym.includes('.')) { + // Append month code + year (no exchange lookup — simplified from Python) + sym = `${sym}${monthCode}${year}=F` + } + } + } + + // Ensure proper suffix + const upper = sym.toUpperCase() + if (!upper.includes('.') && !upper.includes('=F')) { + newSymbols.push(`${upper}=F`) + } else { + newSymbols.push(upper) + } + } + + return newSymbols +} + +export class YFinanceFuturesHistoricalFetcher extends Fetcher { + static override transformQuery(params: Record): YFinanceFuturesHistoricalQueryParams { + const now = new Date() + if (!params.start_date) { + const oneYearAgo = new Date(now) + oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1) + params.start_date = oneYearAgo.toISOString().slice(0, 10) + } + if (!params.end_date) { + params.end_date = now.toISOString().slice(0, 10) + } + + // Format symbols + const rawSymbols = String(params.symbol ?? '').split(',').map(s => s.trim()).filter(Boolean) + const expiration = params.expiration ? String(params.expiration) : null + const formatted = formatFuturesSymbols(rawSymbols, expiration) + params.symbol = formatted.join(',') + + return YFinanceFuturesHistoricalQueryParamsSchema.parse(params) + } + + static override async extractData( + query: YFinanceFuturesHistoricalQueryParams, + credentials: Record | null, + ): Promise[]> { + const symbols = query.symbol.split(',').map(s => s.trim()).filter(Boolean) + const interval = INTERVALS_DICT[query.interval] ?? '1d' + + const allData: Record[] = [] + const results = await Promise.allSettled( + symbols.map(async (sym) => { + const data = await getHistoricalData(sym, { + startDate: query.start_date, + endDate: query.end_date, + interval, + }) + return data.map(d => ({ ...d, symbol: sym })) + }) + ) + + for (const r of results) { + if (r.status === 'fulfilled') allData.push(...r.value) + } + + if (!allData.length) throw new EmptyDataError('No futures historical data returned') + return allData + } + + static override transformData( + query: YFinanceFuturesHistoricalQueryParams, + data: Record[], + ): YFinanceFuturesHistoricalData[] { + return data.map(d => YFinanceFuturesHistoricalDataSchema.parse(d)) + } +} diff --git a/packages/opentypebb/src/providers/yfinance/models/gainers.ts b/packages/opentypebb/src/providers/yfinance/models/gainers.ts new file mode 100644 index 00000000..c6639da8 --- /dev/null +++ b/packages/opentypebb/src/providers/yfinance/models/gainers.ts @@ -0,0 +1,52 @@ +/** + * Yahoo Finance Top Gainers Model. + * Maps to: openbb_yfinance/models/gainers.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { EquityPerformanceQueryParamsSchema } from '../../../standard-models/equity-performance.js' +import { applyAliases } from '../../../core/provider/utils/helpers.js' +import { getPredefinedScreener } from '../utils/helpers.js' +import { YFPredefinedScreenerDataSchema, YF_SCREENER_ALIAS_DICT } from '../utils/references.js' + +export const YFGainersQueryParamsSchema = EquityPerformanceQueryParamsSchema.extend({ + limit: z.number().nullable().default(200).describe('Limit the number of results.'), +}) +export type YFGainersQueryParams = z.infer + +export const YFGainersDataSchema = YFPredefinedScreenerDataSchema +export type YFGainersData = z.infer + +export class YFGainersFetcher extends Fetcher { + static requireCredentials = false + + static override transformQuery(params: Record): YFGainersQueryParams { + return YFGainersQueryParamsSchema.parse(params) + } + + static override async extractData( + query: YFGainersQueryParams, + credentials: Record | null, + ): Promise[]> { + const results = await getPredefinedScreener('day_gainers', query.limit ?? 200) + return results + } + + static override transformData( + query: YFGainersQueryParams, + data: Record[], + ): YFGainersData[] { + const sorted = [...data].sort((a, b) => { + const diff = Number(b.regularMarketChangePercent ?? 0) - Number(a.regularMarketChangePercent ?? 0) + return query.sort === 'desc' ? diff : -diff + }) + return sorted.map(d => { + const aliased = applyAliases(d, YF_SCREENER_ALIAS_DICT) + if (typeof aliased.percent_change === 'number') { + aliased.percent_change = aliased.percent_change / 100 + } + return YFGainersDataSchema.parse(aliased) + }) + } +} diff --git a/packages/opentypebb/src/providers/yfinance/models/growth-tech.ts b/packages/opentypebb/src/providers/yfinance/models/growth-tech.ts new file mode 100644 index 00000000..ce26d07c --- /dev/null +++ b/packages/opentypebb/src/providers/yfinance/models/growth-tech.ts @@ -0,0 +1,51 @@ +/** + * Yahoo Finance Growth Technology Equities Model. + * Maps to: openbb_yfinance/models/growth_tech_equities.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { EquityPerformanceQueryParamsSchema } from '../../../standard-models/equity-performance.js' +import { applyAliases } from '../../../core/provider/utils/helpers.js' +import { getPredefinedScreener } from '../utils/helpers.js' +import { YFPredefinedScreenerDataSchema, YF_SCREENER_ALIAS_DICT } from '../utils/references.js' + +export const YFGrowthTechQueryParamsSchema = EquityPerformanceQueryParamsSchema.extend({ + limit: z.number().nullable().default(200).describe('Limit the number of results.'), +}) +export type YFGrowthTechQueryParams = z.infer + +export const YFGrowthTechDataSchema = YFPredefinedScreenerDataSchema +export type YFGrowthTechData = z.infer + +export class YFGrowthTechEquitiesFetcher extends Fetcher { + static requireCredentials = false + + static override transformQuery(params: Record): YFGrowthTechQueryParams { + return YFGrowthTechQueryParamsSchema.parse(params) + } + + static override async extractData( + query: YFGrowthTechQueryParams, + credentials: Record | null, + ): Promise[]> { + return getPredefinedScreener('growth_technology_stocks', query.limit ?? 200) + } + + static override transformData( + query: YFGrowthTechQueryParams, + data: Record[], + ): YFGrowthTechData[] { + const sorted = [...data].sort((a, b) => { + const diff = Number(b.regularMarketChangePercent ?? 0) - Number(a.regularMarketChangePercent ?? 0) + return query.sort === 'desc' ? diff : -diff + }) + return sorted.map(d => { + const aliased = applyAliases(d, YF_SCREENER_ALIAS_DICT) + if (typeof aliased.percent_change === 'number') { + aliased.percent_change = aliased.percent_change / 100 + } + return YFGrowthTechDataSchema.parse(aliased) + }) + } +} diff --git a/packages/opentypebb/src/providers/yfinance/models/historical-dividends.ts b/packages/opentypebb/src/providers/yfinance/models/historical-dividends.ts new file mode 100644 index 00000000..6571b648 --- /dev/null +++ b/packages/opentypebb/src/providers/yfinance/models/historical-dividends.ts @@ -0,0 +1,41 @@ +/** + * YFinance Historical Dividends Model. + * Maps to: openbb_yfinance/models/historical_dividends.py + * + * All data is split-adjusted. + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { HistoricalDividendsQueryParamsSchema, HistoricalDividendsDataSchema } from '../../../standard-models/historical-dividends.js' +import { getHistoricalDividends } from '../utils/helpers.js' + +export const YFinanceHistoricalDividendsQueryParamsSchema = HistoricalDividendsQueryParamsSchema +export type YFinanceHistoricalDividendsQueryParams = z.infer + +export const YFinanceHistoricalDividendsDataSchema = HistoricalDividendsDataSchema +export type YFinanceHistoricalDividendsData = z.infer + +export class YFinanceHistoricalDividendsFetcher extends Fetcher { + static override transformQuery(params: Record): YFinanceHistoricalDividendsQueryParams { + return YFinanceHistoricalDividendsQueryParamsSchema.parse(params) + } + + static override async extractData( + query: YFinanceHistoricalDividendsQueryParams, + credentials: Record | null, + ): Promise[]> { + return getHistoricalDividends( + query.symbol, + query.start_date, + query.end_date, + ) + } + + static override transformData( + _query: YFinanceHistoricalDividendsQueryParams, + data: Record[], + ): YFinanceHistoricalDividendsData[] { + return data.map(d => YFinanceHistoricalDividendsDataSchema.parse(d)) + } +} diff --git a/packages/opentypebb/src/providers/yfinance/models/income-statement.ts b/packages/opentypebb/src/providers/yfinance/models/income-statement.ts new file mode 100644 index 00000000..282b8da6 --- /dev/null +++ b/packages/opentypebb/src/providers/yfinance/models/income-statement.ts @@ -0,0 +1,62 @@ +/** + * YFinance Income Statement Model. + * Maps to: openbb_yfinance/models/income_statement.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { IncomeStatementQueryParamsSchema, IncomeStatementDataSchema } from '../../../standard-models/income-statement.js' +import { applyAliases } from '../../../core/provider/utils/helpers.js' +import { getFinancialStatements } from '../utils/helpers.js' + +// --- Query Params --- + +export const YFinanceIncomeStatementQueryParamsSchema = IncomeStatementQueryParamsSchema.extend({ + period: z.enum(['annual', 'quarter']).default('annual').describe('Time period of the data to return.'), + limit: z.coerce.number().int().min(1).max(5).nullable().default(5).describe('The number of data entries to return (max 5).'), +}) + +export type YFinanceIncomeStatementQueryParams = z.infer + +// --- Data --- + +const ALIAS_DICT: Record = { + selling_general_and_admin_expense: 'selling_general_and_administration', + research_and_development_expense: 'research_and_development', + total_pre_tax_income: 'pretax_income', + net_income_attributable_to_common_shareholders: 'net_income_common_stockholders', + weighted_average_basic_shares_outstanding: 'basic_average_shares', + weighted_average_diluted_shares_outstanding: 'diluted_average_shares', + basic_earnings_per_share: 'basic_e_p_s', + diluted_earnings_per_share: 'diluted_e_p_s', +} + +export const YFinanceIncomeStatementDataSchema = IncomeStatementDataSchema.extend({}).passthrough() +export type YFinanceIncomeStatementData = z.infer + +// --- Fetcher --- + +export class YFinanceIncomeStatementFetcher extends Fetcher { + static override requireCredentials = false + + static override transformQuery(params: Record): YFinanceIncomeStatementQueryParams { + return YFinanceIncomeStatementQueryParamsSchema.parse(params) + } + + static override async extractData( + query: YFinanceIncomeStatementQueryParams, + credentials: Record | null, + ): Promise[]> { + return getFinancialStatements(query.symbol, query.period, query.limit ?? 5) + } + + static override transformData( + query: YFinanceIncomeStatementQueryParams, + data: Record[], + ): YFinanceIncomeStatementData[] { + return data.map(d => { + const aliased = applyAliases(d, ALIAS_DICT) + return YFinanceIncomeStatementDataSchema.parse(aliased) + }) + } +} diff --git a/packages/opentypebb/src/providers/yfinance/models/index-historical.ts b/packages/opentypebb/src/providers/yfinance/models/index-historical.ts new file mode 100644 index 00000000..9d7a4a6b --- /dev/null +++ b/packages/opentypebb/src/providers/yfinance/models/index-historical.ts @@ -0,0 +1,126 @@ +/** + * Yahoo Finance Index Historical Model. + * Maps to: openbb_yfinance/models/index_historical.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { IndexHistoricalQueryParamsSchema, IndexHistoricalDataSchema } from '../../../standard-models/index-historical.js' +import { EmptyDataError } from '../../../core/provider/utils/errors.js' +import { getHistoricalData } from '../utils/helpers.js' +import { INTERVALS_DICT, INDICES } from '../utils/references.js' + +export const YFinanceIndexHistoricalQueryParamsSchema = IndexHistoricalQueryParamsSchema.extend({ + interval: z.enum(['1m', '2m', '5m', '15m', '30m', '60m', '90m', '1h', '1d', '5d', '1W', '1M', '1Q']).default('1d').describe('Data granularity.'), +}) +export type YFinanceIndexHistoricalQueryParams = z.infer + +export const YFinanceIndexHistoricalDataSchema = IndexHistoricalDataSchema +export type YFinanceIndexHistoricalData = z.infer + +/** + * Resolve a user-supplied index code/name/symbol to a Yahoo Finance ticker. + * Checks in order: code match, name match, ^SYMBOL match, raw SYMBOL match. + */ +function resolveIndexSymbol(input: string): string | null { + const lower = input.toLowerCase() + const upper = input.toUpperCase() + + // Check by code (e.g. "sp500" → "^GSPC") + if (INDICES[lower]) { + return INDICES[lower].ticker + } + + // Check by name (title case, e.g. "S&P 500 Index") + const titleCase = input.charAt(0).toUpperCase() + input.slice(1).toLowerCase() + for (const entry of Object.values(INDICES)) { + if (entry.name === titleCase) { + return entry.ticker + } + } + + // Check if ^SYMBOL is a known ticker + const caretSymbol = '^' + upper + for (const entry of Object.values(INDICES)) { + if (entry.ticker === caretSymbol) { + return caretSymbol + } + } + + // Check if SYMBOL itself is a known ticker + for (const entry of Object.values(INDICES)) { + if (entry.ticker === upper) { + return upper + } + } + + return null +} + +export class YFinanceIndexHistoricalFetcher extends Fetcher { + static override transformQuery(params: Record): YFinanceIndexHistoricalQueryParams { + const now = new Date() + if (!params.start_date) { + const oneYearAgo = new Date(now) + oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1) + params.start_date = oneYearAgo.toISOString().slice(0, 10) + } + if (!params.end_date) { + params.end_date = now.toISOString().slice(0, 10) + } + + // Resolve index symbols + const rawSymbols = String(params.symbol ?? '').split(',').map(s => s.trim()).filter(Boolean) + const resolvedSymbols: string[] = [] + for (const sym of rawSymbols) { + const resolved = resolveIndexSymbol(sym) + if (resolved) { + resolvedSymbols.push(resolved) + } + // Skip unresolved symbols (matches Python's warn + skip behavior) + } + + if (resolvedSymbols.length === 0) { + // If none resolved, try using the raw symbols as-is (fallback) + params.symbol = rawSymbols.join(',') + } else { + params.symbol = resolvedSymbols.join(',') + } + + return YFinanceIndexHistoricalQueryParamsSchema.parse(params) + } + + static override async extractData( + query: YFinanceIndexHistoricalQueryParams, + credentials: Record | null, + ): Promise[]> { + const symbols = query.symbol.split(',').map(s => s.trim()).filter(Boolean) + const interval = INTERVALS_DICT[query.interval] ?? '1d' + + const allData: Record[] = [] + const results = await Promise.allSettled( + symbols.map(async (sym) => { + const data = await getHistoricalData(sym, { + startDate: query.start_date, + endDate: query.end_date, + interval, + }) + return data.map(d => ({ ...d, symbol: sym })) + }) + ) + + for (const r of results) { + if (r.status === 'fulfilled') allData.push(...r.value) + } + + if (!allData.length) throw new EmptyDataError('No index historical data returned') + return allData + } + + static override transformData( + query: YFinanceIndexHistoricalQueryParams, + data: Record[], + ): YFinanceIndexHistoricalData[] { + return data.map(d => YFinanceIndexHistoricalDataSchema.parse(d)) + } +} diff --git a/packages/opentypebb/src/providers/yfinance/models/key-executives.ts b/packages/opentypebb/src/providers/yfinance/models/key-executives.ts new file mode 100644 index 00000000..92d91ae9 --- /dev/null +++ b/packages/opentypebb/src/providers/yfinance/models/key-executives.ts @@ -0,0 +1,72 @@ +/** + * YFinance Key Executives Model. + * Maps to: openbb_yfinance/models/key_executives.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { KeyExecutivesQueryParamsSchema, KeyExecutivesDataSchema } from '../../../standard-models/key-executives.js' +import { applyAliases } from '../../../core/provider/utils/helpers.js' +import { EmptyDataError } from '../../../core/provider/utils/errors.js' +import { getRawQuoteSummary } from '../utils/helpers.js' + +const ALIAS_DICT: Record = { + year_born: 'yearBorn', + fiscal_year: 'fiscalYear', + pay: 'totalPay', + exercised_value: 'exercisedValue', + unexercised_value: 'unexercisedValue', +} + +export const YFinanceKeyExecutivesQueryParamsSchema = KeyExecutivesQueryParamsSchema +export type YFinanceKeyExecutivesQueryParams = z.infer + +export const YFinanceKeyExecutivesDataSchema = KeyExecutivesDataSchema.extend({ + exercised_value: z.number().nullable().default(null).describe('Value of shares exercised.'), + unexercised_value: z.number().nullable().default(null).describe('Value of shares not exercised.'), + fiscal_year: z.number().nullable().default(null).describe('Fiscal year of the pay.'), +}).passthrough() +export type YFinanceKeyExecutivesData = z.infer + +export class YFinanceKeyExecutivesFetcher extends Fetcher { + static override transformQuery(params: Record): YFinanceKeyExecutivesQueryParams { + return YFinanceKeyExecutivesQueryParamsSchema.parse(params) + } + + static override async extractData( + query: YFinanceKeyExecutivesQueryParams, + credentials: Record | null, + ): Promise[]> { + // Need raw (unflattened) quoteSummary to access companyOfficers array + const raw = await getRawQuoteSummary(query.symbol, ['assetProfile']) + const profile = (raw as any).assetProfile + if (!profile?.companyOfficers?.length) { + throw new EmptyDataError(`No executive data found for ${query.symbol}`) + } + + // Remove maxAge from each officer entry (matches Python) + const officers: Record[] = profile.companyOfficers.map((d: any) => { + const copy = { ...d } + delete copy.maxAge + // Handle nested raw values (yahoo-finance2 sometimes wraps in { raw, fmt }) + for (const [k, v] of Object.entries(copy)) { + if (v && typeof v === 'object' && 'raw' in (v as any)) { + copy[k] = (v as any).raw + } + } + return copy + }) + + return officers + } + + static override transformData( + _query: YFinanceKeyExecutivesQueryParams, + data: Record[], + ): YFinanceKeyExecutivesData[] { + return data.map(d => { + const aliased = applyAliases(d, ALIAS_DICT) + return YFinanceKeyExecutivesDataSchema.parse(aliased) + }) + } +} diff --git a/packages/opentypebb/src/providers/yfinance/models/key-metrics.ts b/packages/opentypebb/src/providers/yfinance/models/key-metrics.ts new file mode 100644 index 00000000..87dfea62 --- /dev/null +++ b/packages/opentypebb/src/providers/yfinance/models/key-metrics.ts @@ -0,0 +1,128 @@ +/** + * YFinance Key Metrics Model. + * Maps to: openbb_yfinance/models/key_metrics.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { KeyMetricsQueryParamsSchema, KeyMetricsDataSchema } from '../../../standard-models/key-metrics.js' +import { applyAliases } from '../../../core/provider/utils/helpers.js' +import { EmptyDataError } from '../../../core/provider/utils/errors.js' +import { getQuoteSummary } from '../utils/helpers.js' + +const ALIAS_DICT: Record = { + market_cap: 'marketCap', + pe_ratio: 'trailingPE', + forward_pe: 'forwardPE', + peg_ratio: 'pegRatio', + peg_ratio_ttm: 'trailingPegRatio', + eps_ttm: 'trailingEps', + eps_forward: 'forwardEps', + enterprise_to_ebitda: 'enterpriseToEbitda', + earnings_growth: 'earningsGrowth', + earnings_growth_quarterly: 'earningsQuarterlyGrowth', + revenue_per_share: 'revenuePerShare', + revenue_growth: 'revenueGrowth', + enterprise_to_revenue: 'enterpriseToRevenue', + cash_per_share: 'totalCashPerShare', + quick_ratio: 'quickRatio', + current_ratio: 'currentRatio', + debt_to_equity: 'debtToEquity', + gross_margin: 'grossMargins', + operating_margin: 'operatingMargins', + ebitda_margin: 'ebitdaMargins', + profit_margin: 'profitMargins', + return_on_assets: 'returnOnAssets', + return_on_equity: 'returnOnEquity', + dividend_yield: 'dividendYield', + dividend_yield_5y_avg: 'fiveYearAvgDividendYield', + payout_ratio: 'payoutRatio', + book_value: 'bookValue', + price_to_book: 'priceToBook', + enterprise_value: 'enterpriseValue', + overall_risk: 'overallRisk', + audit_risk: 'auditRisk', + board_risk: 'boardRisk', + compensation_risk: 'compensationRisk', + shareholder_rights_risk: 'shareHolderRightsRisk', + price_return_1y: '52WeekChange', + currency: 'financialCurrency', +} + +export const YFinanceKeyMetricsQueryParamsSchema = KeyMetricsQueryParamsSchema +export type YFinanceKeyMetricsQueryParams = z.infer + +export const YFinanceKeyMetricsDataSchema = KeyMetricsDataSchema.extend({ + pe_ratio: z.number().nullable().default(null).describe('Price-to-earnings ratio (TTM).'), + forward_pe: z.number().nullable().default(null).describe('Forward price-to-earnings ratio.'), + peg_ratio: z.number().nullable().default(null).describe('PEG ratio (5-year expected).'), + peg_ratio_ttm: z.number().nullable().default(null).describe('PEG ratio (TTM).'), + eps_ttm: z.number().nullable().default(null).describe('Earnings per share (TTM).'), + eps_forward: z.number().nullable().default(null).describe('Forward earnings per share.'), + enterprise_to_ebitda: z.number().nullable().default(null).describe('Enterprise value to EBITDA ratio.'), + earnings_growth: z.number().nullable().default(null).describe('Earnings growth (YoY).'), + earnings_growth_quarterly: z.number().nullable().default(null).describe('Quarterly earnings growth (YoY).'), + revenue_per_share: z.number().nullable().default(null).describe('Revenue per share (TTM).'), + revenue_growth: z.number().nullable().default(null).describe('Revenue growth (YoY).'), + enterprise_to_revenue: z.number().nullable().default(null).describe('Enterprise value to revenue ratio.'), + cash_per_share: z.number().nullable().default(null).describe('Cash per share.'), + quick_ratio: z.number().nullable().default(null).describe('Quick ratio.'), + current_ratio: z.number().nullable().default(null).describe('Current ratio.'), + debt_to_equity: z.number().nullable().default(null).describe('Debt-to-equity ratio.'), + gross_margin: z.number().nullable().default(null).describe('Gross margin.'), + operating_margin: z.number().nullable().default(null).describe('Operating margin.'), + ebitda_margin: z.number().nullable().default(null).describe('EBITDA margin.'), + profit_margin: z.number().nullable().default(null).describe('Profit margin.'), + return_on_assets: z.number().nullable().default(null).describe('Return on assets.'), + return_on_equity: z.number().nullable().default(null).describe('Return on equity.'), + dividend_yield: z.number().nullable().default(null).describe('Dividend yield.'), + dividend_yield_5y_avg: z.number().nullable().default(null).describe('5-year average dividend yield.'), + payout_ratio: z.number().nullable().default(null).describe('Payout ratio.'), + book_value: z.number().nullable().default(null).describe('Book value per share.'), + price_to_book: z.number().nullable().default(null).describe('Price-to-book ratio.'), + enterprise_value: z.number().nullable().default(null).describe('Enterprise value.'), + overall_risk: z.number().nullable().default(null).describe('Overall risk score.'), + audit_risk: z.number().nullable().default(null).describe('Audit risk score.'), + board_risk: z.number().nullable().default(null).describe('Board risk score.'), + compensation_risk: z.number().nullable().default(null).describe('Compensation risk score.'), + shareholder_rights_risk: z.number().nullable().default(null).describe('Shareholder rights risk score.'), + beta: z.number().nullable().default(null).describe('Beta relative to the broad market.'), + price_return_1y: z.number().nullable().default(null).describe('One-year price return.'), +}).passthrough() +export type YFinanceKeyMetricsData = z.infer + +export class YFinanceKeyMetricsFetcher extends Fetcher { + static override transformQuery(params: Record): YFinanceKeyMetricsQueryParams { + return YFinanceKeyMetricsQueryParamsSchema.parse(params) + } + + static override async extractData( + query: YFinanceKeyMetricsQueryParams, + credentials: Record | null, + ): Promise[]> { + const symbols = query.symbol.split(',').map(s => s.trim()).filter(Boolean) + const results = await Promise.allSettled( + symbols.map(s => getQuoteSummary(s, ['defaultKeyStatistics', 'summaryDetail', 'financialData'])) + ) + const data: Record[] = [] + for (const r of results) { + if (r.status === 'fulfilled' && r.value) data.push(r.value) + } + return data + } + + static override transformData( + query: YFinanceKeyMetricsQueryParams, + data: Record[], + ): YFinanceKeyMetricsData[] { + if (!data.length) throw new EmptyDataError('No key metrics data returned') + return data.map(d => { + const aliased = applyAliases(d, ALIAS_DICT) + // Normalize 5y avg dividend yield (comes as whole number, not decimal) + if (typeof aliased.dividend_yield_5y_avg === 'number') { + aliased.dividend_yield_5y_avg = aliased.dividend_yield_5y_avg / 100 + } + return YFinanceKeyMetricsDataSchema.parse(aliased) + }) + } +} diff --git a/packages/opentypebb/src/providers/yfinance/models/losers.ts b/packages/opentypebb/src/providers/yfinance/models/losers.ts new file mode 100644 index 00000000..acc8ee7d --- /dev/null +++ b/packages/opentypebb/src/providers/yfinance/models/losers.ts @@ -0,0 +1,51 @@ +/** + * Yahoo Finance Top Losers Model. + * Maps to: openbb_yfinance/models/losers.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { EquityPerformanceQueryParamsSchema } from '../../../standard-models/equity-performance.js' +import { applyAliases } from '../../../core/provider/utils/helpers.js' +import { getPredefinedScreener } from '../utils/helpers.js' +import { YFPredefinedScreenerDataSchema, YF_SCREENER_ALIAS_DICT } from '../utils/references.js' + +export const YFLosersQueryParamsSchema = EquityPerformanceQueryParamsSchema.extend({ + limit: z.number().nullable().default(200).describe('Limit the number of results.'), +}) +export type YFLosersQueryParams = z.infer + +export const YFLosersDataSchema = YFPredefinedScreenerDataSchema +export type YFLosersData = z.infer + +export class YFLosersFetcher extends Fetcher { + static requireCredentials = false + + static override transformQuery(params: Record): YFLosersQueryParams { + return YFLosersQueryParamsSchema.parse(params) + } + + static override async extractData( + query: YFLosersQueryParams, + credentials: Record | null, + ): Promise[]> { + return getPredefinedScreener('day_losers', query.limit ?? 200) + } + + static override transformData( + query: YFLosersQueryParams, + data: Record[], + ): YFLosersData[] { + const sorted = [...data].sort((a, b) => { + const diff = Number(b.regularMarketChangePercent ?? 0) - Number(a.regularMarketChangePercent ?? 0) + return query.sort === 'desc' ? diff : -diff + }) + return sorted.map(d => { + const aliased = applyAliases(d, YF_SCREENER_ALIAS_DICT) + if (typeof aliased.percent_change === 'number') { + aliased.percent_change = aliased.percent_change / 100 + } + return YFLosersDataSchema.parse(aliased) + }) + } +} diff --git a/packages/opentypebb/src/providers/yfinance/models/options-chains.ts b/packages/opentypebb/src/providers/yfinance/models/options-chains.ts new file mode 100644 index 00000000..9f54395f --- /dev/null +++ b/packages/opentypebb/src/providers/yfinance/models/options-chains.ts @@ -0,0 +1,230 @@ +/** + * Yahoo Finance Options Chains Model. + * Maps to: openbb_yfinance/models/options_chains.py + * + * Fetches full options chain for a given symbol using yahoo-finance2's options API, + * with fallback to direct Yahoo Finance HTTP endpoint. + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { OptionsChainsQueryParamsSchema, OptionsChainsDataSchema } from '../../../standard-models/options-chains.js' +import { EmptyDataError } from '../../../core/provider/utils/errors.js' +import { getOptionsData } from '../utils/helpers.js' + +export const YFinanceOptionsChainsQueryParamsSchema = OptionsChainsQueryParamsSchema +export type YFinanceOptionsChainsQueryParams = z.infer + +export const YFinanceOptionsChainsDataSchema = OptionsChainsDataSchema +export type YFinanceOptionsChainsData = z.infer + +/** Fetch options chain using yahoo-finance2's options() API via shared singleton */ +async function fetchViaYF2(symbol: string): Promise[]> { + // Step 1: Get first expiration + list of all expirations + const optionsResult = await getOptionsData(symbol) + + const expirationDates: Date[] = optionsResult?.expirationDates ?? [] + if (!expirationDates.length) { + throw new EmptyDataError(`No options data found for ${symbol}`) + } + + const underlyingPrice = optionsResult?.quote?.regularMarketPrice ?? null + const today = new Date().toISOString().slice(0, 10) + const allContracts: Record[] = [] + + const processOptions = (options: any[], type: string, expirationStr: string) => { + for (const opt of options) { + const strike = opt.strike ?? 0 + const now = new Date() + const exp = new Date(expirationStr) + const dte = Math.max(0, Math.ceil((exp.getTime() - now.getTime()) / (1000 * 60 * 60 * 24))) + + allContracts.push({ + underlying_symbol: symbol, + underlying_price: underlyingPrice, + contract_symbol: opt.contractSymbol ?? '', + eod_date: today, + expiration: expirationStr, + dte, + strike, + option_type: type, + open_interest: opt.openInterest ?? null, + volume: opt.volume ?? null, + last_trade_price: opt.lastPrice ?? null, + last_trade_time: opt.lastTradeDate + ? (opt.lastTradeDate instanceof Date ? opt.lastTradeDate.toISOString() : String(opt.lastTradeDate)) + : null, + bid: opt.bid ?? null, + ask: opt.ask ?? null, + mark: opt.bid != null && opt.ask != null ? (opt.bid + opt.ask) / 2 : null, + change: opt.change ?? null, + change_percent: opt.percentChange != null ? opt.percentChange / 100 : null, + implied_volatility: opt.impliedVolatility ?? null, + in_the_money: opt.inTheMoney ?? null, + currency: opt.currency ?? null, + }) + } + } + + // Process first expiration (already have data) + if (optionsResult.options?.[0]) { + const firstExpStr = expirationDates[0] instanceof Date + ? expirationDates[0].toISOString().slice(0, 10) + : String(expirationDates[0]).slice(0, 10) + const firstOpts = optionsResult.options[0] + processOptions(firstOpts.calls ?? [], 'call', firstExpStr) + processOptions(firstOpts.puts ?? [], 'put', firstExpStr) + } + + // Fetch remaining expirations in batches + const remainingDates = expirationDates.slice(1) + const batchSize = 5 + for (let i = 0; i < remainingDates.length; i += batchSize) { + const batch = remainingDates.slice(i, i + batchSize) + await Promise.allSettled( + batch.map(async (expDate) => { + const dateObj = expDate instanceof Date ? expDate : new Date(expDate) + const dateStr = dateObj.toISOString().slice(0, 10) + try { + const result = await getOptionsData(symbol, dateObj) + if (result?.options?.[0]) { + processOptions(result.options[0].calls ?? [], 'call', dateStr) + processOptions(result.options[0].puts ?? [], 'put', dateStr) + } + } catch { + // Skip failed expirations + } + }) + ) + } + + return allContracts +} + +/** Fetch options via direct Yahoo Finance v7 HTTP API (fallback) */ +async function fetchViaDirect(symbol: string): Promise[]> { + const baseUrl = `https://query2.finance.yahoo.com/v7/finance/options/${encodeURIComponent(symbol)}` + + // Get first expiration + list of all expirations + const resp = await fetch(baseUrl, { signal: AbortSignal.timeout(15000) }) + if (!resp.ok) throw new EmptyDataError(`Yahoo options API returned ${resp.status}`) + const json = await resp.json() as any + const result = json?.optionChain?.result?.[0] + if (!result) throw new EmptyDataError(`No options data for ${symbol}`) + + const expirationEpochs: number[] = result.expirationDates ?? [] + if (!expirationEpochs.length) throw new EmptyDataError(`No option expirations for ${symbol}`) + + const underlyingPrice = result.quote?.regularMarketPrice ?? null + const today = new Date().toISOString().slice(0, 10) + const allContracts: Record[] = [] + + const processChain = (options: any[], type: string, expirationStr: string) => { + for (const opt of options) { + const strike = opt.strike ?? 0 + const now = new Date() + const exp = new Date(expirationStr) + const dte = Math.max(0, Math.ceil((exp.getTime() - now.getTime()) / (1000 * 60 * 60 * 24))) + + allContracts.push({ + underlying_symbol: symbol, + underlying_price: underlyingPrice, + contract_symbol: opt.contractSymbol ?? '', + eod_date: today, + expiration: expirationStr, + dte, + strike, + option_type: type, + open_interest: opt.openInterest ?? null, + volume: opt.volume ?? null, + last_trade_price: opt.lastPrice ?? null, + last_trade_time: opt.lastTradeDate + ? new Date(opt.lastTradeDate * 1000).toISOString() + : null, + bid: opt.bid ?? null, + ask: opt.ask ?? null, + mark: opt.bid != null && opt.ask != null ? (opt.bid + opt.ask) / 2 : null, + change: opt.change ?? null, + change_percent: opt.percentChange != null ? opt.percentChange / 100 : null, + implied_volatility: opt.impliedVolatility ?? null, + in_the_money: opt.inTheMoney ?? null, + currency: opt.currency ?? null, + }) + } + } + + // Process first expiration (already have data) + if (result.options?.[0]) { + const firstExpStr = new Date(expirationEpochs[0] * 1000).toISOString().slice(0, 10) + processChain(result.options[0].calls ?? [], 'call', firstExpStr) + processChain(result.options[0].puts ?? [], 'put', firstExpStr) + } + + // Fetch remaining expirations + const remaining = expirationEpochs.slice(1) + const batchSize = 5 + for (let i = 0; i < remaining.length; i += batchSize) { + const batch = remaining.slice(i, i + batchSize) + await Promise.allSettled( + batch.map(async (epoch) => { + const dateStr = new Date(epoch * 1000).toISOString().slice(0, 10) + try { + const r = await fetch(`${baseUrl}?date=${epoch}`, { signal: AbortSignal.timeout(15000) }) + if (!r.ok) return + const j = await r.json() as any + const chain = j?.optionChain?.result?.[0]?.options?.[0] + if (chain) { + processChain(chain.calls ?? [], 'call', dateStr) + processChain(chain.puts ?? [], 'put', dateStr) + } + } catch { + // Skip failed expirations + } + }) + ) + } + + return allContracts +} + +export class YFinanceOptionsChainsFetcher extends Fetcher { + static override transformQuery(params: Record): YFinanceOptionsChainsQueryParams { + return YFinanceOptionsChainsQueryParamsSchema.parse(params) + } + + static override async extractData( + query: YFinanceOptionsChainsQueryParams, + _credentials: Record | null, + ): Promise[]> { + let symbol = query.symbol.toUpperCase() + // Prefix index symbols with ^ (matching Python behavior) + if (['VIX', 'RUT', 'SPX', 'NDX'].includes(symbol)) { + symbol = '^' + symbol + } + + // Try yahoo-finance2 first, fall back to direct API + let contracts: Record[] + try { + contracts = await fetchViaYF2(symbol) + } catch { + try { + contracts = await fetchViaDirect(symbol) + } catch (err) { + throw new EmptyDataError(`Failed to fetch options for ${query.symbol}: ${err}`) + } + } + + if (!contracts.length) { + throw new EmptyDataError(`No options contracts found for ${query.symbol}`) + } + + return contracts + } + + static override transformData( + _query: YFinanceOptionsChainsQueryParams, + data: Record[], + ): YFinanceOptionsChainsData[] { + return data.map(d => OptionsChainsDataSchema.parse(d)) + } +} diff --git a/packages/opentypebb/src/providers/yfinance/models/price-target-consensus.ts b/packages/opentypebb/src/providers/yfinance/models/price-target-consensus.ts new file mode 100644 index 00000000..a1f3d40d --- /dev/null +++ b/packages/opentypebb/src/providers/yfinance/models/price-target-consensus.ts @@ -0,0 +1,66 @@ +/** + * YFinance Price Target Consensus Model. + * Maps to: openbb_yfinance/models/price_target_consensus.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { PriceTargetConsensusQueryParamsSchema, PriceTargetConsensusDataSchema } from '../../../standard-models/price-target-consensus.js' +import { applyAliases } from '../../../core/provider/utils/helpers.js' +import { EmptyDataError } from '../../../core/provider/utils/errors.js' +import { getQuoteSummary } from '../utils/helpers.js' + +const ALIAS_DICT: Record = { + target_high: 'targetHighPrice', + target_low: 'targetLowPrice', + target_consensus: 'targetMeanPrice', + target_median: 'targetMedianPrice', + recommendation: 'recommendationKey', + recommendation_mean: 'recommendationMean', + number_of_analysts: 'numberOfAnalystOpinions', + current_price: 'currentPrice', +} + +export const YFinancePriceTargetConsensusQueryParamsSchema = PriceTargetConsensusQueryParamsSchema +export type YFinancePriceTargetConsensusQueryParams = z.infer + +export const YFinancePriceTargetConsensusDataSchema = PriceTargetConsensusDataSchema.extend({ + recommendation: z.string().nullable().default(null).describe('Recommendation - buy, sell, etc.'), + recommendation_mean: z.number().nullable().default(null).describe('Mean recommendation score where 1 is strong buy and 5 is strong sell.'), + number_of_analysts: z.number().nullable().default(null).describe('Number of analysts providing opinions.'), + current_price: z.number().nullable().default(null).describe('Current price of the stock.'), + currency: z.string().nullable().default(null).describe('Currency the stock is priced in.'), +}).passthrough() +export type YFinancePriceTargetConsensusData = z.infer + +export class YFinancePriceTargetConsensusFetcher extends Fetcher { + static override transformQuery(params: Record): YFinancePriceTargetConsensusQueryParams { + if (!params.symbol) throw new Error('Symbol is a required field for yFinance.') + return YFinancePriceTargetConsensusQueryParamsSchema.parse(params) + } + + static override async extractData( + query: YFinancePriceTargetConsensusQueryParams, + credentials: Record | null, + ): Promise[]> { + const symbols = (query.symbol ?? '').split(',').map(s => s.trim()).filter(Boolean) + const results = await Promise.allSettled( + symbols.map(s => getQuoteSummary(s, ['financialData'])) + ) + const data: Record[] = [] + for (const r of results) { + if (r.status === 'fulfilled' && r.value && r.value.numberOfAnalystOpinions != null) { + data.push(r.value) + } + } + return data + } + + static override transformData( + query: YFinancePriceTargetConsensusQueryParams, + data: Record[], + ): YFinancePriceTargetConsensusData[] { + if (!data.length) throw new EmptyDataError('No price target data returned') + return data.map(d => YFinancePriceTargetConsensusDataSchema.parse(applyAliases(d, ALIAS_DICT))) + } +} diff --git a/packages/opentypebb/src/providers/yfinance/models/share-statistics.ts b/packages/opentypebb/src/providers/yfinance/models/share-statistics.ts new file mode 100644 index 00000000..74df48a7 --- /dev/null +++ b/packages/opentypebb/src/providers/yfinance/models/share-statistics.ts @@ -0,0 +1,114 @@ +/** + * YFinance Share Statistics Model. + * Maps to: openbb_yfinance/models/share_statistics.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { ShareStatisticsQueryParamsSchema, ShareStatisticsDataSchema } from '../../../standard-models/share-statistics.js' +import { applyAliases } from '../../../core/provider/utils/helpers.js' +import { EmptyDataError } from '../../../core/provider/utils/errors.js' +import { getQuoteSummary } from '../utils/helpers.js' + +const ALIAS_DICT: Record = { + outstanding_shares: 'sharesOutstanding', + float_shares: 'floatShares', + date: 'dateShortInterest', + implied_shares_outstanding: 'impliedSharesOutstanding', + short_interest: 'sharesShort', + short_percent_of_float: 'shortPercentOfFloat', + days_to_cover: 'shortRatio', + short_interest_prev_month: 'sharesShortPriorMonth', + short_interest_prev_date: 'sharesShortPreviousMonthDate', + insider_ownership: 'heldPercentInsiders', + institution_ownership: 'heldPercentInstitutions', + institution_float_ownership: 'institutionsFloatPercentHeld', + institutions_count: 'institutionsCount', +} + +const numOrNull = z.number().nullable().default(null) + +export const YFinanceShareStatisticsQueryParamsSchema = ShareStatisticsQueryParamsSchema +export type YFinanceShareStatisticsQueryParams = z.infer + +export const YFinanceShareStatisticsDataSchema = ShareStatisticsDataSchema.extend({ + implied_shares_outstanding: numOrNull.describe('Implied Shares Outstanding of common equity, assuming the conversion of all convertible subsidiary equity into common.'), + short_interest: numOrNull.describe('Number of shares that are reported short.'), + short_percent_of_float: numOrNull.describe('Percentage of shares that are reported short, as a normalized percent.'), + days_to_cover: numOrNull.describe('Number of days to repurchase the shares as a ratio of average daily volume.'), + short_interest_prev_month: numOrNull.describe('Number of shares that were reported short in the previous month.'), + short_interest_prev_date: z.string().nullable().default(null).describe('Date of the previous month short interest report.'), + insider_ownership: numOrNull.describe('Percentage of shares held by insiders, as a normalized percent.'), + institution_ownership: numOrNull.describe('Percentage of shares held by institutions, as a normalized percent.'), + institution_float_ownership: numOrNull.describe('Percentage of float held by institutions, as a normalized percent.'), + institutions_count: numOrNull.describe('Number of institutions holding shares.'), +}).passthrough() +export type YFinanceShareStatisticsData = z.infer + +export class YFinanceShareStatisticsFetcher extends Fetcher { + static override transformQuery(params: Record): YFinanceShareStatisticsQueryParams { + return YFinanceShareStatisticsQueryParamsSchema.parse(params) + } + + static override async extractData( + query: YFinanceShareStatisticsQueryParams, + credentials: Record | null, + ): Promise[]> { + const symbols = query.symbol.split(',').map(s => s.trim()).filter(Boolean) + const results: Record[] = [] + + const settled = await Promise.allSettled( + symbols.map(async (symbol) => { + const data = await getQuoteSummary(symbol, [ + 'defaultKeyStatistics', + 'majorHoldersBreakdown', + ]) + return data + }), + ) + + for (const r of settled) { + if (r.status === 'fulfilled' && r.value) { + const data = r.value as Record + // Only include if we got shares outstanding + if (data.sharesOutstanding != null) { + results.push(data) + } + } + } + + if (!results.length) { + throw new EmptyDataError('No share statistics data returned') + } + + return results + } + + static override transformData( + _query: YFinanceShareStatisticsQueryParams, + data: Record[], + ): YFinanceShareStatisticsData[] { + return data.map(d => { + // Convert epoch timestamps for date fields + if (typeof d.dateShortInterest === 'number') { + d.dateShortInterest = new Date(d.dateShortInterest * 1000).toISOString().slice(0, 10) + } + if (typeof d.sharesShortPreviousMonthDate === 'number') { + d.sharesShortPreviousMonthDate = new Date(d.sharesShortPreviousMonthDate * 1000).toISOString().slice(0, 10) + } + + // yahoo-finance2 uses insidersPercentHeld / institutionsPercentHeld + // while yfinance Python uses heldPercentInsiders / heldPercentInstitutions + // Map yahoo-finance2 names to the alias dict expected names + if (d.insidersPercentHeld != null && d.heldPercentInsiders == null) { + d.heldPercentInsiders = d.insidersPercentHeld + } + if (d.institutionsPercentHeld != null && d.heldPercentInstitutions == null) { + d.heldPercentInstitutions = d.institutionsPercentHeld + } + + const aliased = applyAliases(d, ALIAS_DICT) + return YFinanceShareStatisticsDataSchema.parse(aliased) + }) + } +} diff --git a/packages/opentypebb/src/providers/yfinance/models/undervalued-growth.ts b/packages/opentypebb/src/providers/yfinance/models/undervalued-growth.ts new file mode 100644 index 00000000..a34281d8 --- /dev/null +++ b/packages/opentypebb/src/providers/yfinance/models/undervalued-growth.ts @@ -0,0 +1,51 @@ +/** + * Yahoo Finance Undervalued Growth Equities Model. + * Maps to: openbb_yfinance/models/undervalued_growth_equities.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { EquityPerformanceQueryParamsSchema } from '../../../standard-models/equity-performance.js' +import { applyAliases } from '../../../core/provider/utils/helpers.js' +import { getPredefinedScreener } from '../utils/helpers.js' +import { YFPredefinedScreenerDataSchema, YF_SCREENER_ALIAS_DICT } from '../utils/references.js' + +export const YFUndervaluedGrowthQueryParamsSchema = EquityPerformanceQueryParamsSchema.extend({ + limit: z.number().nullable().default(200).describe('Limit the number of results.'), +}) +export type YFUndervaluedGrowthQueryParams = z.infer + +export const YFUndervaluedGrowthDataSchema = YFPredefinedScreenerDataSchema +export type YFUndervaluedGrowthData = z.infer + +export class YFUndervaluedGrowthEquitiesFetcher extends Fetcher { + static requireCredentials = false + + static override transformQuery(params: Record): YFUndervaluedGrowthQueryParams { + return YFUndervaluedGrowthQueryParamsSchema.parse(params) + } + + static override async extractData( + query: YFUndervaluedGrowthQueryParams, + credentials: Record | null, + ): Promise[]> { + return getPredefinedScreener('undervalued_growth_stocks', query.limit ?? 200) + } + + static override transformData( + query: YFUndervaluedGrowthQueryParams, + data: Record[], + ): YFUndervaluedGrowthData[] { + const sorted = [...data].sort((a, b) => { + const diff = Number(b.regularMarketChangePercent ?? 0) - Number(a.regularMarketChangePercent ?? 0) + return query.sort === 'desc' ? diff : -diff + }) + return sorted.map(d => { + const aliased = applyAliases(d, YF_SCREENER_ALIAS_DICT) + if (typeof aliased.percent_change === 'number') { + aliased.percent_change = aliased.percent_change / 100 + } + return YFUndervaluedGrowthDataSchema.parse(aliased) + }) + } +} diff --git a/packages/opentypebb/src/providers/yfinance/models/undervalued-large-caps.ts b/packages/opentypebb/src/providers/yfinance/models/undervalued-large-caps.ts new file mode 100644 index 00000000..42326fd5 --- /dev/null +++ b/packages/opentypebb/src/providers/yfinance/models/undervalued-large-caps.ts @@ -0,0 +1,51 @@ +/** + * Yahoo Finance Undervalued Large Caps Model. + * Maps to: openbb_yfinance/models/undervalued_large_caps.py + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { EquityPerformanceQueryParamsSchema } from '../../../standard-models/equity-performance.js' +import { applyAliases } from '../../../core/provider/utils/helpers.js' +import { getPredefinedScreener } from '../utils/helpers.js' +import { YFPredefinedScreenerDataSchema, YF_SCREENER_ALIAS_DICT } from '../utils/references.js' + +export const YFUndervaluedLargeCapsQueryParamsSchema = EquityPerformanceQueryParamsSchema.extend({ + limit: z.number().nullable().default(200).describe('Limit the number of results.'), +}) +export type YFUndervaluedLargeCapsQueryParams = z.infer + +export const YFUndervaluedLargeCapsDataSchema = YFPredefinedScreenerDataSchema +export type YFUndervaluedLargeCapsData = z.infer + +export class YFUndervaluedLargeCapsFetcher extends Fetcher { + static requireCredentials = false + + static override transformQuery(params: Record): YFUndervaluedLargeCapsQueryParams { + return YFUndervaluedLargeCapsQueryParamsSchema.parse(params) + } + + static override async extractData( + query: YFUndervaluedLargeCapsQueryParams, + credentials: Record | null, + ): Promise[]> { + return getPredefinedScreener('undervalued_large_caps', query.limit ?? 200) + } + + static override transformData( + query: YFUndervaluedLargeCapsQueryParams, + data: Record[], + ): YFUndervaluedLargeCapsData[] { + const sorted = [...data].sort((a, b) => { + const diff = Number(b.regularMarketChangePercent ?? 0) - Number(a.regularMarketChangePercent ?? 0) + return query.sort === 'desc' ? diff : -diff + }) + return sorted.map(d => { + const aliased = applyAliases(d, YF_SCREENER_ALIAS_DICT) + if (typeof aliased.percent_change === 'number') { + aliased.percent_change = aliased.percent_change / 100 + } + return YFUndervaluedLargeCapsDataSchema.parse(aliased) + }) + } +} diff --git a/packages/opentypebb/src/providers/yfinance/utils/helpers.ts b/packages/opentypebb/src/providers/yfinance/utils/helpers.ts new file mode 100644 index 00000000..5dea220a --- /dev/null +++ b/packages/opentypebb/src/providers/yfinance/utils/helpers.ts @@ -0,0 +1,481 @@ +/** + * Yahoo Finance helpers module. + * Maps to: openbb_yfinance/utils/helpers.py + * + * Uses yahoo-finance2 npm package for authenticated access to Yahoo Finance API. + * The package handles cookie/crumb authentication automatically. + */ + +import YahooFinance from 'yahoo-finance2' +import { EmptyDataError } from '../../../core/provider/utils/errors.js' +import { SCREENER_FIELDS } from './references.js' + +// Singleton Yahoo Finance instance — reset on persistent failures +let _yf: InstanceType | null = null +let _yfFailCount = 0 +function getYF(): InstanceType { + if (!_yf || _yfFailCount >= 3) { + _yf = new YahooFinance({ suppressNotices: ['yahooSurvey'] }) + _yfFailCount = 0 + } + return _yf +} + +function recordYFSuccess(): void { _yfFailCount = 0 } +function recordYFFailure(): void { _yfFailCount++ } + +/** Retry a function up to maxRetries times with delay between attempts */ +async function withRetry(fn: () => Promise, maxRetries = 2, delayMs = 1000): Promise { + let lastError: unknown + for (let attempt = 0; attempt <= maxRetries; attempt++) { + try { + return await fn() + } catch (err) { + lastError = err + if (attempt < maxRetries) { + await new Promise(r => setTimeout(r, delayMs * (attempt + 1))) + } + } + } + throw lastError +} + +/** + * Get data from Yahoo Finance predefined screener. + * Uses yahoo-finance2's screener() method with scrIds parameter. + * Maps to: get_custom_screener() in helpers.py + * + * @param scrId - Predefined screener ID: 'day_gainers', 'day_losers', 'most_actives', etc. + * @param count - Max results to return (default: 250) + */ +export async function getPredefinedScreener( + scrId: string, + count = 250, +): Promise[]> { + let result: any + + // Screener requires crumb authentication which can become stale in long-running + // server processes. On failure, reset the YF singleton to force a fresh crumb, + // then retry once. + for (let attempt = 0; attempt < 2; attempt++) { + const yf = getYF() + try { + result = await (yf as any).screener({ scrIds: scrId, count }) + recordYFSuccess() + break + } catch (err) { + recordYFFailure() + if (attempt === 0) { + // Force singleton reset for fresh crumb on retry + _yf = null + _yfFailCount = 0 + await new Promise(r => setTimeout(r, 1000)) + continue + } + throw err + } + } + + const quotes: any[] = result?.quotes ?? [] + if (!quotes.length) { + throw new EmptyDataError(`No data found for screener: ${scrId}`) + } + + // Normalize quotes + const output: Record[] = [] + for (const item of quotes) { + // Format earnings date if available + if (item.earningsTimestamp) { + try { + const ts = typeof item.earningsTimestamp === 'number' + ? item.earningsTimestamp + : item.earningsTimestamp instanceof Date + ? item.earningsTimestamp.getTime() / 1000 + : null + if (ts) { + item.earnings_date = new Date(ts * 1000).toISOString().replace('T', ' ').slice(0, 19) + } + } catch { + item.earnings_date = null + } + } + + const result: Record = {} + for (const k of SCREENER_FIELDS) { + result[k] = item[k] ?? null + } + + if (result.regularMarketChange != null && result.regularMarketVolume != null) { + output.push(result) + } + } + + return output +} + +/** @deprecated Use getPredefinedScreener instead */ +export const getCustomScreener = getPredefinedScreener as any + +/** + * Fetch quote summary data from Yahoo Finance for one symbol. + * Uses yahoo-finance2's quoteSummary which handles authentication. + * Maps to: yfinance Ticker.get_info() pattern. + */ +export async function getQuoteSummary( + symbol: string, + modules: string[] = ['defaultKeyStatistics', 'summaryDetail', 'summaryProfile', 'financialData', 'price'], +): Promise> { + const yf = getYF() + + let result: any + try { + result = await withRetry(() => yf.quoteSummary(symbol, { modules: modules as any })) + recordYFSuccess() + } catch (err) { + recordYFFailure() + throw err + } + + if (!result) { + throw new EmptyDataError(`No quote summary data for ${symbol}`) + } + + // Flatten all modules into a single dict + const flat: Record = { symbol } + for (const [_modName, mod] of Object.entries(result)) { + if (mod && typeof mod === 'object') { + for (const [key, value] of Object.entries(mod as Record)) { + if (value !== undefined && value !== null) { + if (value instanceof Date) { + flat[key] = value.toISOString().slice(0, 10) + } else if (typeof value !== 'object') { + flat[key] = value + } else if (typeof value === 'object' && value !== null && 'raw' in (value as any)) { + flat[key] = (value as any).raw + } + // Skip nested objects (companyOfficers, etc.) + } + } + } + } + + return flat +} + +/** + * Fetch historical chart data from Yahoo Finance. + * Uses yahoo-finance2's chart method which handles authentication. + * Maps to: yf.download() pattern. + */ +export async function getHistoricalData( + symbol: string, + options: { + startDate?: string | null + endDate?: string | null + interval?: string + } = {}, +): Promise[]> { + const yf = getYF() + const interval = options.interval ?? '1d' + + const period1 = options.startDate + ? new Date(options.startDate) + : new Date(Date.now() - 365 * 24 * 60 * 60 * 1000) + + const period2 = options.endDate + ? new Date(options.endDate) + : new Date() + + const chartResult = await withRetry(() => yf.chart(symbol, { + period1, + period2, + interval: interval as any, + })) + + if (!chartResult?.quotes?.length) { + throw new EmptyDataError(`No historical data for ${symbol}`) + } + + const isIntraday = ['1m', '2m', '5m', '15m', '30m', '60m', '90m', '1h'].includes(interval) + + const records: Record[] = [] + for (const q of chartResult.quotes) { + if (q.open == null || q.open <= 0) continue + + const date = q.date instanceof Date ? q.date : new Date(q.date as any) + const dateStr = isIntraday + ? date.toISOString().replace('T', ' ').slice(0, 19) + : date.toISOString().slice(0, 10) + + records.push({ + date: dateStr, + open: q.open ?? null, + high: q.high ?? null, + low: q.low ?? null, + close: q.close ?? null, + volume: q.volume ?? null, + ...(q.adjclose != null ? { adj_close: q.adjclose } : {}), + }) + } + + if (records.length === 0) { + throw new EmptyDataError(`No valid historical data for ${symbol}`) + } + + return records +} + +/** + * Search Yahoo Finance for symbols. + * Used by crypto-search and currency-search models. + */ +export async function searchYahooFinance( + query: string, +): Promise[]> { + const yf = getYF() + // validateResult: false — Yahoo changed typeDisp casing (e.g. "cryptocurrency" vs + // "Cryptocurrency"), causing yahoo-finance2's strict schema validation to throw. + const result: any = await withRetry(() => + (yf as any).search(query, { quotesCount: 20, newsCount: 0 }, { validateResult: false }), + ) + return (result.quotes ?? []) as Record[] +} + +/** + * Convert a camelCase string to snake_case. + * Maps to: openbb_core.provider.utils.helpers.to_snake_case + */ +function toSnakeCase(s: string): string { + return s.replace(/([A-Z])/g, '_$1').toLowerCase().replace(/^_/, '') +} + +/** + * Fetch financial statement data from Yahoo Finance via fundamentalsTimeSeries. + * Used by balance-sheet, income-statement, and cash-flow fetchers. + * + * Note: The old quoteSummary modules (balanceSheetHistory, incomeStatementHistory, + * cashflowStatementHistory) have been deprecated since Nov 2024 and return almost + * no data. fundamentalsTimeSeries returns ALL financial data fields mixed together. + * + * @param symbol - Stock ticker + * @param period - "annual" or "quarter" + * @param limit - max periods to return (default: 5) + */ +export async function getFinancialStatements( + symbol: string, + period: string, + limit = 5, +): Promise[]> { + const yf = getYF() + const type = period === 'quarter' ? 'quarterly' : 'annual' + + // Fetch 10 years back for annual, 3 years for quarterly + const yearsBack = period === 'quarter' ? 3 : 10 + const period1 = new Date() + period1.setFullYear(period1.getFullYear() - yearsBack) + + let result: any + try { + result = await withRetry(() => (yf as any).fundamentalsTimeSeries(symbol, { + period1: period1.toISOString().slice(0, 10), + period2: new Date().toISOString().slice(0, 10), + type, + module: 'all', + })) + recordYFSuccess() + } catch (err) { + recordYFFailure() + throw err + } + + if (!Array.isArray(result) || result.length === 0) { + throw new EmptyDataError(`No financial statement data for ${symbol}`) + } + + // Sort by date descending (most recent first) and apply limit + const sorted = result.sort((a: any, b: any) => { + const da = a.date instanceof Date ? a.date.getTime() : new Date(a.date).getTime() + const db = b.date instanceof Date ? b.date.getTime() : new Date(b.date).getTime() + return db - da + }) + const limited = sorted.slice(0, limit) + + // Convert each period's data to snake_case records + return limited.map((stmt: any) => { + const record: Record = {} + for (const [key, value] of Object.entries(stmt)) { + // Skip metadata fields + if (key === 'TYPE') continue + const snakeKey = toSnakeCase(key) + if (value instanceof Date) { + record[snakeKey] = value.toISOString().slice(0, 10) + } else if (value != null && typeof value === 'object' && 'raw' in (value as any)) { + record[snakeKey] = (value as any).raw + } else if (typeof value !== 'object' || value === null) { + record[snakeKey] = value ?? null + } + } + // Map 'date' → 'period_ending' for standard model + if (record.date && !record.period_ending) { + record.period_ending = record.date + delete record.date + } + return record + }) +} + +/** + * Fetch raw (unflattened) quoteSummary modules from Yahoo Finance. + * Unlike getQuoteSummary(), this preserves nested objects like companyOfficers. + * Useful for endpoints that need array-type nested data. + */ +export async function getRawQuoteSummary( + symbol: string, + modules: string[], +): Promise> { + const yf = getYF() + + let result: any + try { + result = await withRetry(() => yf.quoteSummary(symbol, { modules: modules as any })) + recordYFSuccess() + } catch (err) { + recordYFFailure() + throw err + } + + if (!result) { + throw new EmptyDataError(`No quote summary data for ${symbol}`) + } + + return result +} + +/** + * Fetch historical dividend data from Yahoo Finance using the chart API. + * Maps to: yfinance Ticker.get_dividends() pattern. + */ +export async function getHistoricalDividends( + symbol: string, + startDate?: string | null, + endDate?: string | null, +): Promise[]> { + const yf = getYF() + + const period1 = startDate + ? new Date(startDate) + : new Date('1970-01-01') + const period2 = endDate + ? new Date(endDate) + : new Date() + + let result: any + try { + result = await withRetry(() => yf.chart(symbol, { + period1, + period2, + interval: '1d', + events: 'div', + } as any)) + recordYFSuccess() + } catch (err) { + recordYFFailure() + throw err + } + + // Extract dividends from events + const dividends: Record[] = [] + const events = result?.events + if (events?.dividends) { + const divEntries = Array.isArray(events.dividends) + ? events.dividends + : Object.values(events.dividends) + for (const div of divEntries) { + const date = div.date instanceof Date + ? div.date.toISOString().slice(0, 10) + : typeof div.date === 'number' + ? new Date(div.date * 1000).toISOString().slice(0, 10) + : String(div.date ?? '').slice(0, 10) + dividends.push({ + ex_dividend_date: date, + amount: div.amount ?? div.dividend ?? 0, + }) + } + } + + if (!dividends.length) { + throw new EmptyDataError(`No dividend data found for ${symbol}`) + } + + // Filter by date range if specified + let filtered = dividends + if (startDate) { + filtered = filtered.filter(d => String(d.ex_dividend_date) >= startDate) + } + if (endDate) { + filtered = filtered.filter(d => String(d.ex_dividend_date) <= endDate) + } + + return filtered +} + +/** + * Get the list of futures chain symbols from Yahoo Finance. + * Uses quoteSummary with 'futuresChain' module on the continuation symbol (SYMBOL=F). + * Maps to: get_futures_symbols() in helpers.py + */ +export async function getFuturesSymbols(symbol: string): Promise { + try { + const result = await getRawQuoteSummary(`${symbol}=F`, ['futuresChain'] as any) + const chain: any = (result as any)?.futuresChain + if (chain?.futures && Array.isArray(chain.futures)) { + return chain.futures as string[] + } + } catch { + // Fall through to empty + } + return [] +} + +/** + * Get options chain data from Yahoo Finance for a symbol. + * Uses yahoo-finance2 options() with retry and instance reset logic. + */ +export async function getOptionsData( + symbol: string, + date?: Date | null, +): Promise { + for (let attempt = 0; attempt < 2; attempt++) { + const yf = getYF() + try { + const result = date + ? await (yf as any).options(symbol, { date }) + : await (yf as any).options(symbol) + recordYFSuccess() + return result + } catch (err) { + recordYFFailure() + if (attempt === 0) { + // Force singleton reset for fresh crumb on retry + _yf = null + _yfFailCount = 0 + await new Promise(r => setTimeout(r, 1000)) + continue + } + throw err + } + } +} + +/** + * Get news from Yahoo Finance for a symbol. + */ +export async function getYahooNews( + symbol: string, + limit = 20, +): Promise[]> { + const yf = getYF() + const result = await withRetry(() => yf.search(symbol, { quotesCount: 0, newsCount: limit })) + return (result.news ?? []) as Record[] +} + diff --git a/packages/opentypebb/src/providers/yfinance/utils/references.ts b/packages/opentypebb/src/providers/yfinance/utils/references.ts new file mode 100644 index 00000000..f11b76c0 --- /dev/null +++ b/packages/opentypebb/src/providers/yfinance/utils/references.ts @@ -0,0 +1,401 @@ +/** + * Yahoo Finance References. + * Maps to: openbb_yfinance/utils/references.py + */ + +import { z } from 'zod' +import { EquityPerformanceDataSchema } from '../../../standard-models/equity-performance.js' +import { applyAliases } from '../../../core/provider/utils/helpers.js' + +export const INTERVALS_DICT: Record = { + '1m': '1m', + '2m': '2m', + '5m': '5m', + '15m': '15m', + '30m': '30m', + '60m': '60m', + '90m': '90m', + '1h': '1h', + '1d': '1d', + '5d': '5d', + '1W': '1wk', + '1M': '1mo', + '1Q': '3mo', +} + +/** Futures month code mapping. Maps month number (1-12) to futures month letter code. */ +export const MONTHS: Record = { + 1: 'F', + 2: 'G', + 3: 'H', + 4: 'J', + 5: 'K', + 6: 'M', + 7: 'N', + 8: 'Q', + 9: 'U', + 10: 'V', + 11: 'X', + 12: 'Z', +} + +/** + * Index code → { name, ticker } mapping. + * Maps to: INDICES dict in openbb_yfinance/utils/references.py + */ +export const INDICES: Record = { + sp500: { name: 'S&P 500 Index', ticker: '^GSPC' }, + spx: { name: 'S&P 500 Index', ticker: '^SPX' }, + sp400: { name: 'S&P 400 Mid Cap Index', ticker: '^SP400' }, + sp600: { name: 'S&P 600 Small Cap Index', ticker: '^SP600' }, + sp500tr: { name: 'S&P 500 TR Index', ticker: '^SP500TR' }, + sp_xsp: { name: 'S&P 500 Mini SPX Options Index', ticker: '^XSP' }, + nyse_ny: { name: 'NYSE US 100 Index', ticker: '^NY' }, + dow_djus: { name: 'Dow Jones US Index', ticker: '^DJUS' }, + nyse: { name: 'NYSE Composite Index', ticker: '^NYA' }, + amex: { name: 'NYSE-AMEX Composite Index', ticker: '^XAX' }, + nasdaq: { name: 'Nasdaq Composite Index', ticker: '^IXIC' }, + nasdaq100: { name: 'NASDAQ 100', ticker: '^NDX' }, + nasdaq100_ew: { name: 'NASDAQ 100 Equal Weighted Index', ticker: '^NDXE' }, + nasdaq50: { name: 'NASDAQ Q50 Index', ticker: '^NXTQ' }, + russell1000: { name: 'Russell 1000 Index', ticker: '^RUI' }, + russell2000: { name: 'Russell 2000 Index', ticker: '^RUT' }, + cboe_bxr: { name: 'CBOE Russell 2000 Buy-Write Index', ticker: '^BXR' }, + cboe_bxrt: { name: 'CBOE Russell 2000 30-Delta Buy-Write Index', ticker: '^BXRT' }, + russell3000: { name: 'Russell 3000 Index', ticker: '^RUA' }, + russellvalue: { name: 'Russell 2000 Value Index', ticker: '^RUJ' }, + russellgrowth: { name: 'Russell 2000 Growth Index', ticker: '^RUO' }, + w5000: { name: 'Wilshire 5000', ticker: '^W5000' }, + w5000flt: { name: 'Wilshire 5000 Float Adjusted Index', ticker: '^W5000FLT' }, + dow_dja: { name: 'Dow Jones Composite Average Index', ticker: '^DJA' }, + dow_dji: { name: 'Dow Jones Industrial Average Index', ticker: '^DJI' }, + ca_tsx: { name: 'TSX Composite Index (CAD)', ticker: '^GSPTSE' }, + ca_banks: { name: 'S&P/TSX Composite Banks Index (CAD)', ticker: 'TXBA.TS' }, + mx_ipc: { name: 'IPC Mexico Index (MXN)', ticker: '^MXX' }, + arca_mxy: { name: 'NYSE ARCA Mexico Index (USD)', ticker: '^MXY' }, + br_bvsp: { name: 'IBOVESPA Sao Paulo Brazil Index (BRL)', ticker: '^BVSP' }, + br_ivbx: { name: 'IVBX2 Indice Valour (BRL)', ticker: '^IVBX' }, + ar_mervel: { name: 'S&P MERVAL TR Index (USD)', ticker: 'M.BA' }, + eu_fteu1: { name: 'FTSE Eurotop 100 Index (EUR)', ticker: '^FTEU1' }, + eu_speup: { name: 'S&P Europe 350 Index (EUR)', ticker: '^SPEUP' }, + eu_n100: { name: 'Euronext 100 Index (EUR)', ticker: '^N100' }, + ftse100: { name: 'FTSE Global 100 Index (GBP)', ticker: '^FTSE' }, + ftse250: { name: 'FTSE Global 250 Index (GBP)', ticker: '^FTMC' }, + ftse350: { name: 'FTSE Global 350 Index (GBP)', ticker: '^FTLC' }, + ftai: { name: 'FTSE AIM All-Share Global Index (GBP)', ticker: '^FTAI' }, + uk_ftas: { name: 'UK FTSE All-Share Index (GBP)', ticker: '^FTAS' }, + uk_spuk: { name: 'S&P United Kingdom Index (PDS)', ticker: '^SPUK' }, + uk_100: { name: 'CBOE UK 100 Index (GBP)', ticker: '^BUK100P' }, + ie_iseq: { name: 'ISEQ Irish All Shares Index (EUR)', ticker: '^ISEQ' }, + nl_aex: { name: 'Euronext Dutch 25 Index (EUR)', ticker: '^AEX' }, + nl_amx: { name: 'Euronext Dutch Mid Cap Index (EUR)', ticker: '^AMX' }, + at_atx: { name: 'Wiener Börse Austrian 20 Index (EUR)', ticker: '^ATX' }, + at_atx5: { name: 'Vienna ATX Five Index (EUR)', ticker: '^ATX5' }, + at_prime: { name: 'Vienna ATX Prime Index (EUR)', ticker: '^ATXPRIME' }, + ch_stoxx: { name: 'Zurich STXE 600 PR Index (EUR)', ticker: '^STOXX' }, + ch_stoxx50e: { name: 'Zurich ESTX 50 PR Index (EUR)', ticker: '^STOXX50E' }, + se_omx30: { name: 'OMX Stockholm 30 Index (SEK)', ticker: '^OMX' }, + se_omxspi: { name: 'OMX Stockholm All Share PI (SEK)', ticker: '^OMXSPI' }, + se_benchmark: { name: 'OMX Stockholm Benchmark GI (SEK)', ticker: '^OMXSBGI' }, + dk_benchmark: { name: 'OMX Copenhagen Benchmark GI (DKK)', ticker: '^OMXCBGI' }, + dk_omxc25: { name: 'OMX Copenhagen 25 Index (DKK)', ticker: '^OMXC25' }, + fi_omxh25: { name: 'OMX Helsinki 25 (EUR)', ticker: '^OMXH25' }, + de_dax40: { name: 'DAX Performance Index (EUR)', ticker: '^GDAXI' }, + de_mdax60: { name: 'DAX Mid Cap Performance Index (EUR)', ticker: '^MDAXI' }, + de_sdax70: { name: 'DAX Small Cap Performance Index (EUR)', ticker: '^SDAXI' }, + de_tecdax30: { name: 'DAX Tech Sector TR Index (EUR)', ticker: '^TECDAX' }, + fr_cac40: { name: 'CAC 40 PR Index (EUR)', ticker: '^FCHI' }, + fr_next20: { name: 'CAC Next 20 Index (EUR)', ticker: '^CN20' }, + it_mib40: { name: 'FTSE MIB 40 Index (EUR)', ticker: 'FTSEMIB.MI' }, + be_bel20: { name: 'BEL 20 Brussels Index (EUR)', ticker: '^BFX' }, + pt_bvlg: { name: 'Lisbon PSI All-Share Index GR (EUR)', ticker: '^BVLG' }, + es_ibex35: { name: 'IBEX 35 - Madrid CATS (EUR)', ticker: '^IBEX' }, + in_bse: { name: 'S&P Bombay SENSEX (INR)', ticker: '^BSESN' }, + in_bse500: { name: 'S&P BSE 500 Index (INR)', ticker: 'BSE-500.BO' }, + in_bse200: { name: 'S&P BSE 200 Index (INR)', ticker: 'BSE-200.BO' }, + in_bse100: { name: 'S&P BSE 100 Index (INR)', ticker: 'BSE-100.BO' }, + in_bse_mcap: { name: 'S&P Bombay Mid Cap Index (INR)', ticker: 'BSE-MIDCAP.BO' }, + in_bse_scap: { name: 'S&P Bombay Small Cap Index (INR)', ticker: 'BSE-SMLCAP.BO' }, + in_nse50: { name: 'NSE Nifty 50 Index (INR)', ticker: '^NSEI' }, + in_nse_mcap: { name: 'NSE Nifty 50 Mid Cap Index (INR)', ticker: '^NSEMDCP50' }, + in_nse_bank: { name: 'NSE Nifty Bank Industry Index (INR)', ticker: '^NSEBANK' }, + in_nse500: { name: 'NSE Nifty 500 Index (INR)', ticker: '^CRSLDX' }, + il_ta125: { name: 'Tel-Aviv 125 Index (ILS)', ticker: '^TA125.TA' }, + za_shariah: { name: 'Johannesburg Shariah All Share Index (ZAR)', ticker: '^J143.JO' }, + za_jo: { name: 'Johannesburg All Share Index (ZAR)', ticker: '^J203.JO' }, + za_jo_mcap: { name: 'Johannesburg Large and Mid Cap Index (ZAR)', ticker: '^J206.JO' }, + za_jo_altex: { name: 'Johannesburg Alt Exchange Index (ZAR)', ticker: '^J232.JO' }, + ru_moex: { name: 'MOEX Russia Index (RUB)', ticker: 'IMOEX.ME' }, + au_aord: { name: 'Australia All Ordinary Share Index (AUD)', ticker: '^AORD' }, + au_small: { name: 'S&P/ASX Small Ordinaries Index (AUD)', ticker: '^AXSO' }, + au_asx20: { name: 'S&P/ASX 20 Index (AUD)', ticker: '^ATLI' }, + au_asx50: { name: 'S&P/ASX 50 Index (AUD)', ticker: '^AFLI' }, + au_asx50_mid: { name: 'S&P/ASX Mid Cap 50 Index (AUD)', ticker: '^AXMD' }, + au_asx100: { name: 'S&P/ASX 100 Index (AUD)', ticker: '^ATOI' }, + au_asx200: { name: 'S&P/ASX 200 Index (AUD)', ticker: '^AXJO' }, + au_asx300: { name: 'S&P/ASX 300 Index (AUD)', ticker: '^AXKO' }, + au_energy: { name: 'S&P/ASX 200 Energy Sector Index (AUD)', ticker: '^AXEJ' }, + au_resources: { name: 'S&P/ASX 200 Resources Sector Index (AUD)', ticker: '^AXJR' }, + au_materials: { name: 'S&P/ASX 200 Materials Sector Index (AUD)', ticker: '^AXMJ' }, + au_mining: { name: 'S&P/ASX 300 Metals and Mining Sector Index (AUD)', ticker: '^AXMM' }, + au_industrials: { name: 'S&P/ASX 200 Industrials Sector Index (AUD)', ticker: '^AXNJ' }, + au_discretionary: { name: 'S&P/ASX 200 Consumer Discretionary Sector Index (AUD)', ticker: '^AXDJ' }, + au_staples: { name: 'S&P/ASX 200 Consumer Staples Sector Index (AUD)', ticker: '^AXSJ' }, + au_health: { name: 'S&P/ASX 200 Health Care Sector Index (AUD)', ticker: '^AXHJ' }, + au_financials: { name: 'S&P/ASX 200 Financials Sector Index (AUD)', ticker: '^AXFJ' }, + au_reit: { name: 'S&P/ASX 200 A-REIT Industry Index (AUD)', ticker: '^AXPJ' }, + au_tech: { name: 'S&P/ASX 200 Info Tech Sector Index (AUD)', ticker: '^AXIJ' }, + au_communications: { name: 'S&P/ASX 200 Communications Sector Index (AUD)', ticker: '^AXTJ' }, + au_utilities: { name: 'S&P/ASX 200 Utilities Sector Index (AUD)', ticker: '^AXUJ' }, + nz50: { name: 'S&P New Zealand 50 Index (NZD)', ticker: '^nz50' }, + nz_small: { name: 'S&P/NZX Small Cap Index (NZD)', ticker: '^NZSC' }, + kr_kospi: { name: 'KOSPI Composite Index (KRW)', ticker: '^KS11' }, + jp_arca: { name: 'NYSE ARCA Japan Index (JPY)', ticker: '^JPN' }, + jp_n225: { name: 'Nikkei 255 Index (JPY)', ticker: '^N225' }, + jp_n300: { name: 'Nikkei 300 Index (JPY)', ticker: '^N300' }, + jp_nknr: { name: 'Nikkei Avg Net TR Index (JPY)', ticker: '^NKVI.OS' }, + jp_nkrc: { name: 'Nikkei Avg Risk Control Index (JPY)', ticker: '^NKRC.OS' }, + jp_nklv: { name: 'Nikkei Avg Leverage Index (JPY)', ticker: '^NKLV.OS' }, + jp_nkcc: { name: 'Nikkei Avg Covered Call Index (JPY)', ticker: '^NKCC.OS' }, + jp_nkhd: { name: 'Nikkei Avg High Dividend Yield Index (JPY)', ticker: '^NKHD.OS' }, + jp_auto: { name: 'Nikkei 500 Auto & Auto Parts Index (JPY)', ticker: '^NG17.OS' }, + jp_fintech: { name: 'Global Fintech Japan Hedged Index (JPY)', ticker: '^FDSFTPRJPY' }, + jp_nkdh: { name: 'Nikkei Average USD Hedge Index (JPY)', ticker: '^NKDH.OS' }, + jp_nkeh: { name: 'Nikkei Average EUR Hedge Index (JPY)', ticker: '^NKEH.OS' }, + jp_ndiv: { name: 'Nikkei Average Double Inverse Index (JPY)', ticker: '^NDIV.OS' }, + cn_csi300: { name: 'China CSI 300 Index (CNY)', ticker: '000300.SS' }, + cn_sse_comp: { name: 'SSE Composite Index (CNY)', ticker: '000001.SS' }, + cn_sse_a: { name: 'SSE A Share Index (CNY)', ticker: '000002.SS' }, + cn_szse_comp: { name: 'SZSE Component Index (CNY)', ticker: '399001.SZ' }, + cn_szse_a: { name: 'SZSE A-Shares Index (CNY)', ticker: '399107.SZ' }, + tw_twii: { name: 'TSEC Weighted Index (TWD)', ticker: '^TWII' }, + tw_tcii: { name: 'TSEC Cement and Ceramics Subindex (TWD)', ticker: '^TCII' }, + tw_tfii: { name: 'TSEC Foods Subindex (TWD)', ticker: '^TFII' }, + tw_tfni: { name: 'TSEC Finance Subindex (TWD)', ticker: '^TFNI' }, + tw_tpai: { name: 'TSEC Paper and Pulp Subindex (TWD)', ticker: '^TPAI' }, + hk_hsi: { name: 'Hang Seng Index (HKD)', ticker: '^HSI' }, + hk_utilities: { name: 'Hang Seng Utilities Sector Index (HKD)', ticker: '^HSNU' }, + hk_china: { name: 'Hang Seng China-Affiliated Corporations Index (HKD)', ticker: '^HSCC' }, + hk_finance: { name: 'Hang Seng Finance Sector Index (HKD)', ticker: '^HSNF' }, + hk_properties: { name: 'Hang Seng Properties Sector Index (HKD)', ticker: '^HSNP' }, + hk_hko: { name: 'NYSE ARCA Hong Kong Options Index (USD)', ticker: '^HKO' }, + hk_titans30: { name: 'Dow Jones Hong Kong Titans 30 Index (HKD)', ticker: '^XLHK' }, + id_jkse: { name: 'Jakarta Composite Index (IDR)', ticker: '^JKSE' }, + id_lq45: { name: 'Indonesia Stock Exchange LQ45 Index (IDR)', ticker: '^JKLQ45' }, + my_klci: { name: 'FTSE Kuala Lumpur Composite Index (MYR)', ticker: '^KLSE' }, + ph_psei: { name: 'Philippine Stock Exchange Index (PHP)', ticker: 'PSEI.PS' }, + sg_sti: { name: 'STI Singapore Index (SGD)', ticker: '^STI' }, + th_set: { name: 'Thailand SET Index (THB)', ticker: '^SET.BK' }, + sp_energy_ig: { name: 'S&P 500 Energy (Industry Group) Index', ticker: '^SP500-1010' }, + sp_energy_equipment: { name: 'S&P 500 Energy Equipment & Services Industry Index', ticker: '^SP500-101010' }, + sp_energy_oil: { name: 'S&P 500 Oil, Gas & Consumable Fuels Industry Index', ticker: '^SP500-101020' }, + sp_materials_sector: { name: 'S&P 500 Materials Sector Index', ticker: '^SP500-15' }, + sp_materials_ig: { name: 'S&P 500 Materials (Industry Group) Index', ticker: '^SP500-1510' }, + sp_materials_construction: { name: 'S&P 500 Construction Materials Industry Index', ticker: '^SP500-151020' }, + sp_materials_metals: { name: 'S&P 500 Mining & Metals Industry Index', ticker: '^SP500-151040' }, + sp_industrials_sector: { name: 'S&P 500 Industrials Sector Index', ticker: '^SP500-20' }, + sp_industrials_goods_ig: { name: 'S&P 500 Capital Goods (Industry Group) Index', ticker: '^SP500-2010' }, + sp_industrials_aerospace: { name: 'S&P 500 Aerospace & Defense Industry Index', ticker: '^SP500-201010' }, + sp_industrials_building: { name: 'S&P 500 Building Products Industry Index', ticker: '^SP500-201020' }, + sp_industrials_construction: { name: 'S&P 500 Construction & Engineering Industry Index', ticker: '^SP500-201030' }, + sp_industrials_electrical: { name: 'S&P 500 Electrical Equipment Industry Index', ticker: '^SP500-201040' }, + sp_industrials_conglomerates: { name: 'S&P 500 Industrial Conglomerates Industry Index', ticker: '^SP500-201050' }, + sp_industrials_machinery: { name: 'S&P 500 Machinery Industry Index', ticker: '^SP500-201060' }, + sp_industrials_distributors: { name: 'S&P 500 Trading Companies & Distributors Industry Index', ticker: '^SP500-201070' }, + sp_industrials_services_ig: { name: 'S&P 500 Commercial & Professional Services (Industry Group) Index', ticker: '^SP500-2020' }, + sp_industrials_services_supplies: { name: 'S&P 500 Commercial Services & Supplies Industry Index', ticker: '^SP500-202010' }, + sp_industrials_transport_ig: { name: 'S&P 500 Transportation (Industry Group) Index', ticker: '^SP500-2030' }, + sp_industrials_transport_air: { name: 'S&P 500 Air Freight & Logistics Industry', ticker: '^SP500-203010' }, + sp_industrials_transport_airlines: { name: 'S&P 500 Airlines Industry Index', ticker: '^SP500-203020' }, + sp_industrials_transport_ground: { name: 'S&P 500 Road & Rail Industry Index', ticker: '^SP500-203040' }, + sp_discretionary_sector: { name: 'S&P 500 Consumer Discretionary Index', ticker: '^SP500-25' }, + sp_discretionary_autos_ig: { name: 'S&P 500 Automobiles and Components (Industry Group) Index', ticker: '^SP500-2510' }, + sp_discretionary_auto_components: { name: 'S&P 500 Auto Components Industry Index', ticker: '^SP500-251010' }, + sp_discretionary_autos: { name: 'S&P 500 Automobiles Industry Index', ticker: '^SP500-251020' }, + sp_discretionary_durables_ig: { name: 'S&P 500 Consumer Durables & Apparel (Industry Group) Index', ticker: '^SP500-2520' }, + sp_discretionary_durables_household: { name: 'S&P 500 Household Durables Industry Index', ticker: '^SP500-252010' }, + sp_discretionary_leisure: { name: 'S&P 500 Leisure Products Industry Index', ticker: '^SP500-252020' }, + sp_discretionary_textiles: { name: 'S&P 500 Textiles, Apparel & Luxury Goods Industry Index', ticker: '^SP500-252030' }, + sp_discretionary_services_consumer: { name: 'S&P 500 Consumer Services (Industry Group) Index', ticker: '^SP500-2530' }, + sp_staples_sector: { name: 'S&P 500 Consumer Staples Sector Index', ticker: '^SP500-30' }, + sp_staples_retail_ig: { name: 'S&P 500 Food & Staples Retailing (Industry Group) Index', ticker: '^SP500-3010' }, + sp_staples_food_ig: { name: 'S&P 500 Food Beverage & Tobacco (Industry Group) Index', ticker: '^SP500-3020' }, + sp_staples_beverages: { name: 'S&P 500 Beverages Industry Index', ticker: '^SP500-302010' }, + sp_staples_products_food: { name: 'S&P 500 Food Products Industry Index', ticker: '^SP500-302020' }, + sp_staples_tobacco: { name: 'S&P 500 Tobacco Industry Index', ticker: '^SP500-302030' }, + sp_staples_household_ig: { name: 'S&P 500 Household & Personal Products (Industry Group) Index', ticker: '^SP500-3030' }, + sp_staples_products_household: { name: 'S&P 500 Household Products Industry Index', ticker: '^SP500-303010' }, + sp_staples_products_personal: { name: 'S&P 500 Personal Products Industry Index', ticker: '^SP500-303020' }, + sp_health_sector: { name: 'S&P 500 Health Care Sector Index', ticker: '^SP500-35' }, + sp_health_equipment: { name: 'S&P 500 Health Care Equipment & Services (Industry Group) Index', ticker: '^SP500-3510' }, + sp_health_supplies: { name: 'S&P 500 Health Care Equipment & Supplies Industry Index', ticker: '^SP500-351010' }, + sp_health_providers: { name: 'S&P 500 Health Care Providers & Services Industry Index', ticker: '^SP500-351020' }, + sp_health_sciences: { name: 'S&P 500 Pharmaceuticals, Biotechnology & Life Sciences (Industry Group) Index', ticker: '^SP500-3520' }, + sp_health_biotech: { name: 'S&P 500 Biotechnology Industry Index', ticker: '^SP500-352010' }, + sp_health_pharma: { name: 'S&P 500 Pharmaceuticals Industry Index', ticker: '^SP500-352020' }, + sp_financials_sector: { name: 'S&P 500 Financials Sector Index', ticker: '^SP500-40' }, + sp_financials_diversified_ig: { name: 'S&P 500 Diversified Financials (Industry Group) Index', ticker: '^SP500-4020' }, + sp_financials_services: { name: 'S&P 500 Diversified Financial Services Industry Index', ticker: '^SP500-402010' }, + sp_financials_consumer: { name: 'S&P 500 Consumer Finance Industry Index', ticker: '^SP500-402020' }, + sp_financials_capital: { name: 'S&P 500 Capital Markets Industry Index', ticker: '^SP500-402030' }, + sp_it_sector: { name: 'S&P 500 IT Sector Index', ticker: '^SP500-45' }, + sp_it_saas_ig: { name: 'S&P 500 Software and Services (Industry Group) Index', ticker: '^SP500-4510' }, + sp_it_software: { name: 'S&P 500 Software Industry Index', ticker: '^SP500-451030' }, + sp_it_hardware: { name: 'S&P 500 Technology Hardware Equipment (Industry Group) Index', ticker: '^SP500-4520' }, + sp_it_semi: { name: 'S&P 500 Semiconductor & Semiconductor Equipment Industry', ticker: '^SP500-453010' }, + sp_communications_sector: { name: 'S&P 500 Communications Sector Index', ticker: '^SP500-50' }, + sp_communications_telecom: { name: 'S&P 500 Diversified Telecommunications Services Industry Index', ticker: '^SP500-501010' }, + sp_utilities_sector: { name: 'S&P 500 Utilities Sector Index', ticker: '^SP500-55' }, + sp_utilities_electricity: { name: 'S&P 500 Electric Utilities Index', ticker: '^SP500-551010' }, + sp_utilities_multis: { name: 'S&P 500 Multi-Utilities Industry Index', ticker: '^SP500-551030' }, + sp_re_sector: { name: 'S&P 500 Real Estate Sector Index', ticker: '^SP500-60' }, + sp_re_ig: { name: 'S&P 500 Real Estate (Industry Group) Index', ticker: '^SP500-6010' }, + sphyda: { name: 'S&P High Yield Aristocrats Index', ticker: '^SPHYDA' }, + dow_djt: { name: 'Dow Jones Transportation Average Index', ticker: '^DJT' }, + dow_dju: { name: 'Dow Jones Utility Average Index', ticker: '^DJU' }, + dow_rci: { name: 'Dow Jones Composite All REIT Index', ticker: '^RCI' }, + reit_fnar: { name: 'FTSE Nareit All Equity REITs Index', ticker: '^FNAR' }, + nq_ixch: { name: 'NASDAQ Health Care Index', ticker: '^IXCH' }, + nq_nbi: { name: 'NASDAQ Biotech Index', ticker: '^NBI' }, + nq_tech: { name: 'NASDAQ 100 Technology Sector Index', ticker: '^NDXT' }, + nq_ex_tech: { name: 'NASDAQ 100 Ex-Tech Sector Index', ticker: '^NDXX' }, + nq_ixtc: { name: 'NASDAQ Telecommunications Index', ticker: '^IXTC' }, + nq_inds: { name: 'NASDAQ Industrial Index', ticker: '^INDS' }, + nq_ixco: { name: 'NASDAQ Computer Index', ticker: '^INCO' }, + nq_bank: { name: 'NASDAQ Bank Index', ticker: '^BANK' }, + nq_bkx: { name: 'KBW NASDAQ Bank Index', ticker: '^BKX' }, + nq_krx: { name: 'KBW NASDAQ Regional Bank Index', ticker: '^KRX' }, + nq_kix: { name: 'KBW NASDAQ Insurance Index', ticker: '^KIX' }, + nq_ksx: { name: 'KBW NASDAQ Capital Markets Index', ticker: '^KSX' }, + nq_tran: { name: 'NASDAQ Transportation Index', ticker: '^TRAN' }, + ice_auto: { name: 'ICE FactSet Global NextGen Auto Index', ticker: '^ICEFSNA' }, + ice_comm: { name: 'ICE FactSet Global NextGen Communications Index', ticker: '^ICEFSNC' }, + nyse_nyl: { name: 'NYSE World Leaders Index', ticker: '^NYL' }, + nyse_nyi: { name: 'NYSE International 100 Index', ticker: '^NYI' }, + nyse_nyy: { name: 'NYSE TMT Index', ticker: '^NYY' }, + nyse_fang: { name: 'NYSE FANG+TM index', ticker: '^NYFANG' }, + arca_xmi: { name: 'NYSE ARCA Major Market Index', ticker: '^XMI' }, + arca_xbd: { name: 'NYSE ARCA Securities Broker/Dealer Index', ticker: '^XBD' }, + arca_xii: { name: 'NYSE ARCA Institutional Index', ticker: '^XII' }, + arca_xoi: { name: 'NYSE ARCA Oil and Gas Index', ticker: '^XOI' }, + arca_xng: { name: 'NYSE ARCA Natural Gas Index', ticker: '^XNG' }, + arca_hui: { name: 'NYSE ARCA Gold Bugs Index', ticker: '^HUI' }, + arca_ixb: { name: 'NYSE Materials Select Sector Index', ticker: '^IXB' }, + arca_drg: { name: 'NYSE ARCA Pharmaceutical Index', ticker: '^DRG' }, + arca_btk: { name: 'NYSE ARCA Biotech Index', ticker: '^BKT' }, + arca_pse: { name: 'NYSE ARCA Tech 100 Index', ticker: '^PSE' }, + arca_nwx: { name: 'NYSE ARCA Networking Index', ticker: '^NWX' }, + arca_xci: { name: 'NYSE ARCA Computer Tech Index', ticker: '^XCI' }, + arca_xal: { name: 'NYSE ARCA Airline Index', ticker: '^XAL' }, + arca_xtc: { name: 'NYSE ARCA N.A. Telecom Industry Index', ticker: '^XTC' }, + phlx_sox: { name: 'PHLX Semiconductor Index', ticker: '^SOX' }, + phlx_xau: { name: 'PHLX Gold/Silver Index', ticker: '^XAU' }, + phlx_hgx: { name: 'PHLX Housing Sector Index', ticker: '^HGX' }, + phlx_osx: { name: 'PHLX Oil Services Sector Index', ticker: '^OSX' }, + phlx_uty: { name: 'PHLX Utility Sector Index', ticker: '^UTY' }, + w5klcg: { name: 'Wilshire US Large Cap Growth Index', ticker: '^W5KLCG' }, + w5klcv: { name: 'Wilshire US Large Cap Value Index', ticker: '^W5KLCV' }, + reit_wgreit: { name: 'Wilshire Global REIT Index', ticker: '^WGREIT' }, + reit_wgresi: { name: 'Wilshire Global Real Estate Sector Index', ticker: '^WGRESI' }, + reit_wilreit: { name: 'Wilshire US REIT Index', ticker: '^WILREIT' }, + reit_wilresi: { name: 'Wilshire US Real Estate Security Index', ticker: '^WILRESI' }, + cboe_bxm: { name: 'CBOE Buy-Write Monthly Index', ticker: '^BXM' }, + cboe_vix: { name: 'CBOE S&P 500 Volatility Index', ticker: '^VIX' }, + cboe_vix9d: { name: 'CBOE S&P 500 9-Day Volatility Index', ticker: '^VIX9D' }, + cboe_vix3m: { name: 'CBOE S&P 500 3-Month Volatility Index', ticker: '^VIX3M' }, + cboe_vin: { name: 'CBOE Near-Term VIX Index', ticker: '^VIN' }, + cboe_vvix: { name: 'CBOE VIX Volatility Index', ticker: '^VVIX' }, + cboe_shortvol: { name: 'CBOE Short VIX Futures Index', ticker: '^SHORTVOL' }, + cboe_skew: { name: 'CBOE Skew Index', ticker: '^SKEW' }, + cboe_vxn: { name: 'CBOE NASDAQ 100 Volatility Index', ticker: '^VXN' }, + cboe_gvz: { name: 'CBOE Gold Volatility Index', ticker: '^GVZ' }, + cboe_ovx: { name: 'CBOE Crude Oil Volatility Index', ticker: '^OVX' }, + cboe_tnx: { name: 'CBOE Interest Rate 10 Year T-Note', ticker: '^TNX' }, + cboe_tyx: { name: 'CBOE 30 year Treasury Yields', ticker: '^TYX' }, + cboe_irx: { name: 'CBOE 13 Week Treasury Bill', ticker: '^IRX' }, + cboe_evz: { name: 'CBOE Euro Currency Volatility Index', ticker: '^EVZ' }, + cboe_rvx: { name: 'CBOE Russell 2000 Volatility Index', ticker: '^RVX' }, + move: { name: 'ICE BofAML Move Index', ticker: '^MOVE' }, + dxy: { name: 'US Dollar Index', ticker: 'DX-Y.NYB' }, + crypto200: { name: 'CMC Crypto 200 Index by Solacti', ticker: '^CMC200' }, +} + +export const SCREENER_FIELDS = [ + 'symbol', + 'shortName', + 'regularMarketPrice', + 'regularMarketChange', + 'regularMarketChangePercent', + 'regularMarketVolume', + 'regularMarketOpen', + 'regularMarketDayHigh', + 'regularMarketDayLow', + 'regularMarketPreviousClose', + 'fiftyDayAverage', + 'twoHundredDayAverage', + 'fiftyTwoWeekHigh', + 'fiftyTwoWeekLow', + 'marketCap', + 'sharesOutstanding', + 'epsTrailingTwelveMonths', + 'forwardPE', + 'epsForward', + 'bookValue', + 'priceToBook', + 'trailingAnnualDividendYield', + 'currency', + 'exchange', + 'exchangeTimezoneName', + 'earnings_date', +] as const + +export const YF_SCREENER_ALIAS_DICT: Record = { + name: 'shortName', + price: 'regularMarketPrice', + change: 'regularMarketChange', + percent_change: 'regularMarketChangePercent', + volume: 'regularMarketVolume', + open: 'regularMarketOpen', + high: 'regularMarketDayHigh', + low: 'regularMarketDayLow', + previous_close: 'regularMarketPreviousClose', + ma50: 'fiftyDayAverage', + ma200: 'twoHundredDayAverage', + year_high: 'fiftyTwoWeekHigh', + year_low: 'fiftyTwoWeekLow', + market_cap: 'marketCap', + shares_outstanding: 'sharesOutstanding', + book_value: 'bookValue', + price_to_book: 'priceToBook', + eps_ttm: 'epsTrailingTwelveMonths', + pe_forward: 'forwardPE', + dividend_yield: 'trailingAnnualDividendYield', + earnings_date: 'earnings_date', + currency: 'currency', + exchange_timezone: 'exchangeTimezoneName', +} + +export const YFPredefinedScreenerDataSchema = EquityPerformanceDataSchema.extend({ + open: z.number().nullable().default(null).describe('Open price for the day.'), + high: z.number().nullable().default(null).describe('High price for the day.'), + low: z.number().nullable().default(null).describe('Low price for the day.'), + previous_close: z.number().nullable().default(null).describe('Previous close price.'), + ma50: z.number().nullable().default(null).describe('50-day moving average.'), + ma200: z.number().nullable().default(null).describe('200-day moving average.'), + year_high: z.number().nullable().default(null).describe('52-week high.'), + year_low: z.number().nullable().default(null).describe('52-week low.'), + market_cap: z.number().nullable().default(null).describe('Market Cap.'), + shares_outstanding: z.number().nullable().default(null).describe('Shares outstanding.'), + book_value: z.number().nullable().default(null).describe('Book value per share.'), + price_to_book: z.number().nullable().default(null).describe('Price to book ratio.'), + eps_ttm: z.number().nullable().default(null).describe('Earnings per share over the trailing twelve months.'), + eps_forward: z.number().nullable().default(null).describe('Forward earnings per share.'), + pe_forward: z.number().nullable().default(null).describe('Forward price-to-earnings ratio.'), + dividend_yield: z.number().nullable().default(null).describe('Trailing twelve month dividend yield.'), + exchange: z.string().nullable().default(null).describe('Exchange where the stock is listed.'), + exchange_timezone: z.string().nullable().default(null).describe('Timezone of the exchange.'), + earnings_date: z.string().nullable().default(null).describe('Most recent earnings date.'), + currency: z.string().nullable().default(null).describe('Currency of the price data.'), +}).passthrough() + +export type YFPredefinedScreenerData = z.infer diff --git a/packages/opentypebb/src/server.ts b/packages/opentypebb/src/server.ts new file mode 100644 index 00000000..9f36d151 --- /dev/null +++ b/packages/opentypebb/src/server.ts @@ -0,0 +1,42 @@ +/** + * OpenTypeBB — HTTP Server entry point. + * + * Usage: + * npx tsx src/server.ts + * # or after build: + * node dist/server.js + * + * Environment variables: + * OPENTYPEBB_PORT — Server port (default: 6901) + * FMP_API_KEY — Financial Modeling Prep API key + * + * Credentials can also be passed per-request via: + * X-OpenBB-Credentials: {"fmp_api_key": "..."} + */ + +import { setupProxy } from './core/utils/proxy.js' +import { createApp, startServer } from './core/api/rest-api.js' +import { createExecutor, loadAllRouters } from './core/api/app-loader.js' + +// Must be called before any fetch() calls +setupProxy() + +// Build default credentials from environment variables +const defaultCredentials: Record = {} +if (process.env.FMP_API_KEY) { + defaultCredentials.fmp_api_key = process.env.FMP_API_KEY +} + +// Create executor with all providers loaded +const executor = createExecutor() + +// Create Hono app +const app = createApp(defaultCredentials) + +// Load and mount all extension routers +const rootRouter = loadAllRouters() +rootRouter.mountToHono(app, executor) + +// Start server +const port = parseInt(process.env.OPENTYPEBB_PORT ?? '6901', 10) +startServer(app, port) diff --git a/packages/opentypebb/src/standard-models/analyst-estimates.ts b/packages/opentypebb/src/standard-models/analyst-estimates.ts new file mode 100644 index 00000000..11772680 --- /dev/null +++ b/packages/opentypebb/src/standard-models/analyst-estimates.ts @@ -0,0 +1,43 @@ +/** + * Analyst Estimates Standard Model. + * Maps to: standard_models/analyst_estimates.py + */ + +import { z } from 'zod' + +// --- Query Params --- + +export const AnalystEstimatesQueryParamsSchema = z.object({ + symbol: z.string().transform(v => v.toUpperCase()).describe('Symbol to get data for.'), +}) + +export type AnalystEstimatesQueryParams = z.infer + +// --- Data --- + +export const AnalystEstimatesDataSchema = z.object({ + symbol: z.string().describe('Symbol representing the entity.'), + date: z.string().describe('The date of the data.'), + estimated_revenue_low: z.number().nullable().default(null).describe('Estimated revenue low.'), + estimated_revenue_high: z.number().nullable().default(null).describe('Estimated revenue high.'), + estimated_revenue_avg: z.number().nullable().default(null).describe('Estimated revenue average.'), + estimated_sga_expense_low: z.number().nullable().default(null).describe('Estimated SGA expense low.'), + estimated_sga_expense_high: z.number().nullable().default(null).describe('Estimated SGA expense high.'), + estimated_sga_expense_avg: z.number().nullable().default(null).describe('Estimated SGA expense average.'), + estimated_ebitda_low: z.number().nullable().default(null).describe('Estimated EBITDA low.'), + estimated_ebitda_high: z.number().nullable().default(null).describe('Estimated EBITDA high.'), + estimated_ebitda_avg: z.number().nullable().default(null).describe('Estimated EBITDA average.'), + estimated_ebit_low: z.number().nullable().default(null).describe('Estimated EBIT low.'), + estimated_ebit_high: z.number().nullable().default(null).describe('Estimated EBIT high.'), + estimated_ebit_avg: z.number().nullable().default(null).describe('Estimated EBIT average.'), + estimated_net_income_low: z.number().nullable().default(null).describe('Estimated net income low.'), + estimated_net_income_high: z.number().nullable().default(null).describe('Estimated net income high.'), + estimated_net_income_avg: z.number().nullable().default(null).describe('Estimated net income average.'), + estimated_eps_low: z.number().nullable().default(null).describe('Estimated EPS low.'), + estimated_eps_high: z.number().nullable().default(null).describe('Estimated EPS high.'), + estimated_eps_avg: z.number().nullable().default(null).describe('Estimated EPS average.'), + number_analyst_estimated_revenue: z.number().nullable().default(null).describe('Number of analysts estimating revenue.'), + number_analysts_estimated_eps: z.number().nullable().default(null).describe('Number of analysts estimating EPS.'), +}).passthrough() + +export type AnalystEstimatesData = z.infer diff --git a/packages/opentypebb/src/standard-models/available-indicators.ts b/packages/opentypebb/src/standard-models/available-indicators.ts new file mode 100644 index 00000000..6bbd7524 --- /dev/null +++ b/packages/opentypebb/src/standard-models/available-indicators.ts @@ -0,0 +1,21 @@ +/** + * Available Indicators Standard Model. + * Maps to: openbb_core/provider/standard_models/available_indicators.py + */ + +import { z } from 'zod' + +export const AvailableIndicatorsQueryParamsSchema = z.object({}).passthrough() + +export type AvailableIndicatorsQueryParams = z.infer + +export const AvailableIndicatorsDataSchema = z.object({ + symbol_root: z.string().nullable().default(null).describe('The root symbol representing the indicator.'), + symbol: z.string().nullable().default(null).describe('Symbol representing the entity.'), + country: z.string().nullable().default(null).describe('The name of the country, region, or entity represented by the symbol.'), + iso: z.string().nullable().default(null).describe('The ISO code of the country, region, or entity.'), + description: z.string().nullable().default(null).describe('The description of the indicator.'), + frequency: z.string().nullable().default(null).describe('The frequency of the indicator data.'), +}).passthrough() + +export type AvailableIndicatorsData = z.infer diff --git a/packages/opentypebb/src/standard-models/available-indices.ts b/packages/opentypebb/src/standard-models/available-indices.ts new file mode 100644 index 00000000..131df739 --- /dev/null +++ b/packages/opentypebb/src/standard-models/available-indices.ts @@ -0,0 +1,19 @@ +/** + * Available Indices Standard Model. + * Maps to: openbb_core/provider/standard_models/available_indices.py + */ + +import { z } from 'zod' + +export const AvailableIndicesQueryParamsSchema = z.object({}).passthrough() + +export type AvailableIndicesQueryParams = z.infer + +export const AvailableIndicesDataSchema = z.object({ + symbol: z.string().describe('Symbol representing the entity.'), + name: z.string().nullable().default(null).describe('Name of the index.'), + exchange: z.string().nullable().default(null).describe('Stock exchange where the index is listed.'), + currency: z.string().nullable().default(null).describe('Currency the index is traded in.'), +}).passthrough() + +export type AvailableIndicesData = z.infer diff --git a/packages/opentypebb/src/standard-models/balance-of-payments.ts b/packages/opentypebb/src/standard-models/balance-of-payments.ts new file mode 100644 index 00000000..9b7219a5 --- /dev/null +++ b/packages/opentypebb/src/standard-models/balance-of-payments.ts @@ -0,0 +1,27 @@ +/** + * Balance of Payments Standard Model. + * Maps to: openbb_core/provider/standard_models/balance_of_payments.py + * + * Note: Python defines multiple data classes (BP6BopUsdData, ECBMain, ECBSummary, etc.) + * for different provider report types. In TypeScript we define a generic base schema + * and let provider-specific fetchers extend with their own fields via .passthrough(). + */ + +import { z } from 'zod' + +export const BalanceOfPaymentsQueryParamsSchema = z.object({}).passthrough() + +export type BalanceOfPaymentsQueryParams = z.infer + +export const BalanceOfPaymentsDataSchema = z.object({ + period: z.string().nullable().default(null).describe('The date representing the beginning of the reporting period.'), + current_account: z.number().nullable().default(null).describe('Current Account Balance.'), + goods: z.number().nullable().default(null).describe('Goods Balance.'), + services: z.number().nullable().default(null).describe('Services Balance.'), + primary_income: z.number().nullable().default(null).describe('Primary Income Balance.'), + secondary_income: z.number().nullable().default(null).describe('Secondary Income Balance.'), + capital_account: z.number().nullable().default(null).describe('Capital Account Balance.'), + financial_account: z.number().nullable().default(null).describe('Financial Account Balance.'), +}).passthrough() + +export type BalanceOfPaymentsData = z.infer diff --git a/packages/opentypebb/src/standard-models/balance-sheet-growth.ts b/packages/opentypebb/src/standard-models/balance-sheet-growth.ts new file mode 100644 index 00000000..8f53ba8a --- /dev/null +++ b/packages/opentypebb/src/standard-models/balance-sheet-growth.ts @@ -0,0 +1,25 @@ +/** + * Balance Sheet Growth Standard Model. + * Maps to: standard_models/balance_sheet_growth.py + */ + +import { z } from 'zod' + +// --- Query Params --- + +export const BalanceSheetGrowthQueryParamsSchema = z.object({ + symbol: z.string().transform(v => v.toUpperCase()).describe('Symbol to get data for.'), + limit: z.coerce.number().int().nullable().default(null).describe('The number of data entries to return.'), +}) + +export type BalanceSheetGrowthQueryParams = z.infer + +// --- Data --- + +export const BalanceSheetGrowthDataSchema = z.object({ + period_ending: z.string().describe('The end date of the reporting period.'), + fiscal_period: z.string().nullable().default(null).describe('The fiscal period of the report.'), + fiscal_year: z.coerce.number().int().nullable().default(null).describe('The fiscal year of the fiscal period.'), +}).passthrough() + +export type BalanceSheetGrowthData = z.infer diff --git a/packages/opentypebb/src/standard-models/balance-sheet.ts b/packages/opentypebb/src/standard-models/balance-sheet.ts new file mode 100644 index 00000000..6c4169a5 --- /dev/null +++ b/packages/opentypebb/src/standard-models/balance-sheet.ts @@ -0,0 +1,21 @@ +/** + * Balance Sheet Standard Model. + * Maps to: openbb_core/provider/standard_models/balance_sheet.py + */ + +import { z } from 'zod' + +export const BalanceSheetQueryParamsSchema = z.object({ + symbol: z.string().transform((v) => v.toUpperCase()), + limit: z.number().int().nonnegative().nullable().default(null).describe('The number of data entries to return.'), +}).passthrough() + +export type BalanceSheetQueryParams = z.infer + +export const BalanceSheetDataSchema = z.object({ + period_ending: z.string().describe('The end date of the reporting period.'), + fiscal_period: z.string().nullable().default(null).describe('The fiscal period of the report.'), + fiscal_year: z.number().int().nullable().default(null).describe('The fiscal year of the fiscal period.'), +}).passthrough() + +export type BalanceSheetData = z.infer diff --git a/packages/opentypebb/src/standard-models/calendar-dividend.ts b/packages/opentypebb/src/standard-models/calendar-dividend.ts new file mode 100644 index 00000000..5b980f66 --- /dev/null +++ b/packages/opentypebb/src/standard-models/calendar-dividend.ts @@ -0,0 +1,29 @@ +/** + * Dividend Calendar Standard Model. + * Maps to: standard_models/calendar_dividend.py + */ + +import { z } from 'zod' + +// --- Query Params --- + +export const CalendarDividendQueryParamsSchema = z.object({ + start_date: z.string().nullable().default(null).describe('Start date of the data, in YYYY-MM-DD format.'), + end_date: z.string().nullable().default(null).describe('End date of the data, in YYYY-MM-DD format.'), +}) + +export type CalendarDividendQueryParams = z.infer + +// --- Data --- + +export const CalendarDividendDataSchema = z.object({ + ex_dividend_date: z.string().describe('The ex-dividend date.'), + symbol: z.string().describe('Symbol representing the entity.'), + amount: z.number().nullable().default(null).describe('The dividend amount per share.'), + name: z.string().nullable().default(null).describe('Name of the entity.'), + record_date: z.string().nullable().default(null).describe('The record date of ownership for eligibility.'), + payment_date: z.string().nullable().default(null).describe('The payment date of the dividend.'), + declaration_date: z.string().nullable().default(null).describe('Declaration date of the dividend.'), +}).passthrough() + +export type CalendarDividendData = z.infer diff --git a/packages/opentypebb/src/standard-models/calendar-earnings.ts b/packages/opentypebb/src/standard-models/calendar-earnings.ts new file mode 100644 index 00000000..9c9f321f --- /dev/null +++ b/packages/opentypebb/src/standard-models/calendar-earnings.ts @@ -0,0 +1,23 @@ +/** + * Earnings Calendar Standard Model. + * Maps to: openbb_core/provider/standard_models/calendar_earnings.py + */ + +import { z } from 'zod' + +export const CalendarEarningsQueryParamsSchema = z.object({ + start_date: z.string().nullable().default(null).describe('Start date of the data, in YYYY-MM-DD format.'), + end_date: z.string().nullable().default(null).describe('End date of the data, in YYYY-MM-DD format.'), +}).passthrough() + +export type CalendarEarningsQueryParams = z.infer + +export const CalendarEarningsDataSchema = z.object({ + report_date: z.string().describe('The date of the earnings report.'), + symbol: z.string().describe('Symbol representing the entity requested in the data.'), + name: z.string().nullable().default(null).describe('Name of the entity.'), + eps_previous: z.number().nullable().default(null).describe('The earnings-per-share from the same previously reported period.'), + eps_consensus: z.number().nullable().default(null).describe('The analyst consensus earnings-per-share estimate.'), +}).passthrough() + +export type CalendarEarningsData = z.infer diff --git a/packages/opentypebb/src/standard-models/calendar-ipo.ts b/packages/opentypebb/src/standard-models/calendar-ipo.ts new file mode 100644 index 00000000..5889b99e --- /dev/null +++ b/packages/opentypebb/src/standard-models/calendar-ipo.ts @@ -0,0 +1,26 @@ +/** + * IPO Calendar Standard Model. + * Maps to: standard_models/calendar_ipo.py + */ + +import { z } from 'zod' + +// --- Query Params --- + +export const CalendarIpoQueryParamsSchema = z.object({ + symbol: z.string().nullable().default(null).describe('Symbol to get data for.'), + start_date: z.string().nullable().default(null).describe('Start date of the data, in YYYY-MM-DD format.'), + end_date: z.string().nullable().default(null).describe('End date of the data, in YYYY-MM-DD format.'), + limit: z.coerce.number().int().nullable().default(100).describe('The number of data entries to return.'), +}) + +export type CalendarIpoQueryParams = z.infer + +// --- Data --- + +export const CalendarIpoDataSchema = z.object({ + symbol: z.string().nullable().default(null).describe('Symbol representing the entity.'), + ipo_date: z.string().nullable().default(null).describe('The date of the IPO.'), +}).passthrough() + +export type CalendarIpoData = z.infer diff --git a/packages/opentypebb/src/standard-models/calendar-splits.ts b/packages/opentypebb/src/standard-models/calendar-splits.ts new file mode 100644 index 00000000..d045f114 --- /dev/null +++ b/packages/opentypebb/src/standard-models/calendar-splits.ts @@ -0,0 +1,26 @@ +/** + * Calendar Splits Standard Model. + * Maps to: standard_models/calendar_splits.py + */ + +import { z } from 'zod' + +// --- Query Params --- + +export const CalendarSplitsQueryParamsSchema = z.object({ + start_date: z.string().nullable().default(null).describe('Start date of the data, in YYYY-MM-DD format.'), + end_date: z.string().nullable().default(null).describe('End date of the data, in YYYY-MM-DD format.'), +}) + +export type CalendarSplitsQueryParams = z.infer + +// --- Data --- + +export const CalendarSplitsDataSchema = z.object({ + date: z.string().describe('The date of the data.'), + symbol: z.string().describe('Symbol representing the entity.'), + numerator: z.number().describe('Numerator of the stock split.'), + denominator: z.number().describe('Denominator of the stock split.'), +}).passthrough() + +export type CalendarSplitsData = z.infer diff --git a/packages/opentypebb/src/standard-models/cash-flow-growth.ts b/packages/opentypebb/src/standard-models/cash-flow-growth.ts new file mode 100644 index 00000000..9de892ef --- /dev/null +++ b/packages/opentypebb/src/standard-models/cash-flow-growth.ts @@ -0,0 +1,25 @@ +/** + * Cash Flow Statement Growth Standard Model. + * Maps to: standard_models/cash_flow_growth.py + */ + +import { z } from 'zod' + +// --- Query Params --- + +export const CashFlowStatementGrowthQueryParamsSchema = z.object({ + symbol: z.string().transform(v => v.toUpperCase()).describe('Symbol to get data for.'), + limit: z.coerce.number().int().nullable().default(null).describe('The number of data entries to return.'), +}) + +export type CashFlowStatementGrowthQueryParams = z.infer + +// --- Data --- + +export const CashFlowStatementGrowthDataSchema = z.object({ + period_ending: z.string().describe('The end date of the reporting period.'), + fiscal_period: z.string().nullable().default(null).describe('The fiscal period of the report.'), + fiscal_year: z.coerce.number().int().nullable().default(null).describe('The fiscal year of the fiscal period.'), +}).passthrough() + +export type CashFlowStatementGrowthData = z.infer diff --git a/packages/opentypebb/src/standard-models/cash-flow.ts b/packages/opentypebb/src/standard-models/cash-flow.ts new file mode 100644 index 00000000..0d1a3110 --- /dev/null +++ b/packages/opentypebb/src/standard-models/cash-flow.ts @@ -0,0 +1,21 @@ +/** + * Cash Flow Statement Standard Model. + * Maps to: openbb_core/provider/standard_models/cash_flow_statement.py + */ + +import { z } from 'zod' + +export const CashFlowStatementQueryParamsSchema = z.object({ + symbol: z.string().transform((v) => v.toUpperCase()), + limit: z.number().int().nonnegative().nullable().default(5).describe('The number of data entries to return.'), +}).passthrough() + +export type CashFlowStatementQueryParams = z.infer + +export const CashFlowStatementDataSchema = z.object({ + period_ending: z.string().describe('The end date of the reporting period.'), + fiscal_period: z.string().nullable().default(null).describe('The fiscal period of the report.'), + fiscal_year: z.number().int().nullable().default(null).describe('The fiscal year of the fiscal period.'), +}).passthrough() + +export type CashFlowStatementData = z.infer diff --git a/packages/opentypebb/src/standard-models/central-bank-holdings.ts b/packages/opentypebb/src/standard-models/central-bank-holdings.ts new file mode 100644 index 00000000..329c55ed --- /dev/null +++ b/packages/opentypebb/src/standard-models/central-bank-holdings.ts @@ -0,0 +1,18 @@ +/** + * Central Bank Holdings Standard Model. + * Maps to: openbb_core/provider/standard_models/central_bank_holdings.py + */ + +import { z } from 'zod' + +export const CentralBankHoldingsQueryParamsSchema = z.object({ + date: z.string().nullable().default(null).describe('A specific date to get data for.'), +}).passthrough() + +export type CentralBankHoldingsQueryParams = z.infer + +export const CentralBankHoldingsDataSchema = z.object({ + date: z.string().describe('The date of the data.'), +}).passthrough() + +export type CentralBankHoldingsData = z.infer diff --git a/packages/opentypebb/src/standard-models/company-filings.ts b/packages/opentypebb/src/standard-models/company-filings.ts new file mode 100644 index 00000000..5f732996 --- /dev/null +++ b/packages/opentypebb/src/standard-models/company-filings.ts @@ -0,0 +1,18 @@ +/** + * Company Filings Standard Model. + * Maps to: standard_models/company_filings.py + */ + +import { z } from 'zod' + +export const CompanyFilingsQueryParamsSchema = z.object({ + symbol: z.string().nullable().default(null).transform(v => v ? v.toUpperCase() : null).describe('Symbol to get data for.'), +}) +export type CompanyFilingsQueryParams = z.infer + +export const CompanyFilingsDataSchema = z.object({ + filing_date: z.string().describe('The date of the filing.'), + report_type: z.string().nullable().default(null).describe('Type of filing.'), + report_url: z.string().describe('URL to the filing.'), +}).passthrough() +export type CompanyFilingsData = z.infer diff --git a/packages/opentypebb/src/standard-models/company-news.ts b/packages/opentypebb/src/standard-models/company-news.ts new file mode 100644 index 00000000..fe909c14 --- /dev/null +++ b/packages/opentypebb/src/standard-models/company-news.ts @@ -0,0 +1,28 @@ +/** + * Company News Standard Model. + * Maps to: openbb_core/provider/standard_models/company_news.py + */ + +import { z } from 'zod' + +export const CompanyNewsQueryParamsSchema = z.object({ + symbol: z.string().nullable().default(null).transform((v) => v?.toUpperCase() ?? null), + start_date: z.string().nullable().default(null).describe('Start date of the data, in YYYY-MM-DD format.'), + end_date: z.string().nullable().default(null).describe('End date of the data, in YYYY-MM-DD format.'), + limit: z.number().int().nonnegative().nullable().default(null).describe('The number of data entries to return.'), +}).passthrough() + +export type CompanyNewsQueryParams = z.infer + +export const CompanyNewsDataSchema = z.object({ + date: z.string().nullable().default(null).describe('The date of publication.'), + title: z.string().describe('Title of the article.'), + author: z.string().nullable().default(null).describe('Author of the article.'), + excerpt: z.string().nullable().default(null).describe('Excerpt of the article text.'), + body: z.string().nullable().default(null).describe('Body of the article text.'), + images: z.unknown().nullable().default(null).describe('Images associated with the article.'), + url: z.string().describe('URL to the article.'), + symbols: z.string().nullable().default(null).describe('Symbols associated with the article.'), +}).passthrough() + +export type CompanyNewsData = z.infer diff --git a/packages/opentypebb/src/standard-models/composite-leading-indicator.ts b/packages/opentypebb/src/standard-models/composite-leading-indicator.ts new file mode 100644 index 00000000..fc180936 --- /dev/null +++ b/packages/opentypebb/src/standard-models/composite-leading-indicator.ts @@ -0,0 +1,21 @@ +/** + * Composite Leading Indicator Standard Model. + * Maps to: openbb_core/provider/standard_models/composite_leading_indicator.py + */ + +import { z } from 'zod' + +export const CompositeLeadingIndicatorQueryParamsSchema = z.object({ + start_date: z.string().nullable().default(null).describe('Start date of the data, in YYYY-MM-DD format.'), + end_date: z.string().nullable().default(null).describe('End date of the data, in YYYY-MM-DD format.'), +}).passthrough() + +export type CompositeLeadingIndicatorQueryParams = z.infer + +export const CompositeLeadingIndicatorDataSchema = z.object({ + date: z.string().describe('The date of the data.'), + value: z.number().nullable().default(null).describe('CLI value.'), + country: z.string().describe('Country for the CLI value.'), +}).passthrough() + +export type CompositeLeadingIndicatorData = z.infer diff --git a/packages/opentypebb/src/standard-models/consumer-price-index.ts b/packages/opentypebb/src/standard-models/consumer-price-index.ts new file mode 100644 index 00000000..8c894ec6 --- /dev/null +++ b/packages/opentypebb/src/standard-models/consumer-price-index.ts @@ -0,0 +1,25 @@ +/** + * Consumer Price Index Standard Model. + * Maps to: openbb_core/provider/standard_models/consumer_price_index.py + */ + +import { z } from 'zod' + +export const ConsumerPriceIndexQueryParamsSchema = z.object({ + country: z.string().default('united_states').describe('The country to get data for.'), + transform: z.string().default('yoy').describe('Transformation of the CPI data.'), + frequency: z.enum(['annual', 'quarter', 'monthly']).default('monthly').describe('The frequency of the data.'), + harmonized: z.boolean().default(false).describe('If true, returns harmonized data.'), + start_date: z.string().nullable().default(null).describe('Start date of the data, in YYYY-MM-DD format.'), + end_date: z.string().nullable().default(null).describe('End date of the data, in YYYY-MM-DD format.'), +}).passthrough() + +export type ConsumerPriceIndexQueryParams = z.infer + +export const ConsumerPriceIndexDataSchema = z.object({ + date: z.string().describe('The date of the data.'), + country: z.string().describe('The country.'), + value: z.number().describe('CPI index value or period change.'), +}).passthrough() + +export type ConsumerPriceIndexData = z.infer diff --git a/packages/opentypebb/src/standard-models/country-interest-rates.ts b/packages/opentypebb/src/standard-models/country-interest-rates.ts new file mode 100644 index 00000000..e5c8e746 --- /dev/null +++ b/packages/opentypebb/src/standard-models/country-interest-rates.ts @@ -0,0 +1,22 @@ +/** + * Country Interest Rates Standard Model. + * Maps to: openbb_core/provider/standard_models/country_interest_rates.py + */ + +import { z } from 'zod' + +export const CountryInterestRatesQueryParamsSchema = z.object({ + country: z.string().default('united_states').describe('The country to get data for.'), + start_date: z.string().nullable().default(null).describe('Start date of the data, in YYYY-MM-DD format.'), + end_date: z.string().nullable().default(null).describe('End date of the data, in YYYY-MM-DD format.'), +}).passthrough() + +export type CountryInterestRatesQueryParams = z.infer + +export const CountryInterestRatesDataSchema = z.object({ + date: z.string().nullable().default(null).describe('The date of the data.'), + value: z.number().nullable().default(null).describe('The interest rate value.'), + country: z.string().nullable().default(null).describe('Country for which the interest rate is given.'), +}).passthrough() + +export type CountryInterestRatesData = z.infer diff --git a/packages/opentypebb/src/standard-models/country-profile.ts b/packages/opentypebb/src/standard-models/country-profile.ts new file mode 100644 index 00000000..fe1e83ae --- /dev/null +++ b/packages/opentypebb/src/standard-models/country-profile.ts @@ -0,0 +1,31 @@ +/** + * Country Profile Standard Model. + * Maps to: openbb_core/provider/standard_models/country_profile.py + */ + +import { z } from 'zod' + +export const CountryProfileQueryParamsSchema = z.object({ + country: z.string().transform(v => v.toLowerCase().replace(/ /g, '_')).describe('The country to get data for.'), +}).passthrough() + +export type CountryProfileQueryParams = z.infer + +export const CountryProfileDataSchema = z.object({ + country: z.string().describe('The country.'), + population: z.number().nullable().default(null).describe('Population.'), + gdp_usd: z.number().nullable().default(null).describe('Gross Domestic Product, in billions of USD.'), + gdp_qoq: z.number().nullable().default(null).describe('GDP growth quarter-over-quarter change.'), + gdp_yoy: z.number().nullable().default(null).describe('GDP growth year-over-year change.'), + cpi_yoy: z.number().nullable().default(null).describe('Consumer Price Index year-over-year change.'), + core_yoy: z.number().nullable().default(null).describe('Core Consumer Price Index year-over-year change.'), + retail_sales_yoy: z.number().nullable().default(null).describe('Retail Sales year-over-year change.'), + industrial_production_yoy: z.number().nullable().default(null).describe('Industrial Production year-over-year change.'), + policy_rate: z.number().nullable().default(null).describe('Short term policy rate.'), + yield_10y: z.number().nullable().default(null).describe('10-year government bond yield.'), + govt_debt_gdp: z.number().nullable().default(null).describe('Government debt as percent of GDP.'), + current_account_gdp: z.number().nullable().default(null).describe('Current account balance as percent of GDP.'), + jobless_rate: z.number().nullable().default(null).describe('Unemployment rate.'), +}).passthrough() + +export type CountryProfileData = z.infer diff --git a/packages/opentypebb/src/standard-models/crypto-historical.ts b/packages/opentypebb/src/standard-models/crypto-historical.ts new file mode 100644 index 00000000..41e82160 --- /dev/null +++ b/packages/opentypebb/src/standard-models/crypto-historical.ts @@ -0,0 +1,26 @@ +/** + * Crypto Historical Price Standard Model. + * Maps to: openbb_core/provider/standard_models/crypto_historical.py + */ + +import { z } from 'zod' + +export const CryptoHistoricalQueryParamsSchema = z.object({ + symbol: z.string().transform((v) => v.toUpperCase()), + start_date: z.string().nullable().default(null).describe('Start date of the data, in YYYY-MM-DD format.'), + end_date: z.string().nullable().default(null).describe('End date of the data, in YYYY-MM-DD format.'), +}).passthrough() + +export type CryptoHistoricalQueryParams = z.infer + +export const CryptoHistoricalDataSchema = z.object({ + date: z.string().describe('The date of the data.'), + open: z.number().nullable().default(null).describe('The open price.'), + high: z.number().nullable().default(null).describe('The high price.'), + low: z.number().nullable().default(null).describe('The low price.'), + close: z.number().describe('The close price.'), + volume: z.number().nullable().default(null).describe('The trading volume.'), + vwap: z.number().nullable().default(null).describe('Volume Weighted Average Price over the period.'), +}).passthrough() + +export type CryptoHistoricalData = z.infer diff --git a/packages/opentypebb/src/standard-models/crypto-search.ts b/packages/opentypebb/src/standard-models/crypto-search.ts new file mode 100644 index 00000000..bcb54017 --- /dev/null +++ b/packages/opentypebb/src/standard-models/crypto-search.ts @@ -0,0 +1,19 @@ +/** + * Crypto Search Standard Model. + * Maps to: openbb_core/provider/standard_models/crypto_search.py + */ + +import { z } from 'zod' + +export const CryptoSearchQueryParamsSchema = z.object({ + query: z.string().nullable().default(null).describe('Search query.'), +}).passthrough() + +export type CryptoSearchQueryParams = z.infer + +export const CryptoSearchDataSchema = z.object({ + symbol: z.string().describe('Symbol representing the entity requested in the data. (Crypto)'), + name: z.string().nullable().default(null).describe('Name of the crypto.'), +}).passthrough() + +export type CryptoSearchData = z.infer diff --git a/packages/opentypebb/src/standard-models/currency-historical.ts b/packages/opentypebb/src/standard-models/currency-historical.ts new file mode 100644 index 00000000..0665b970 --- /dev/null +++ b/packages/opentypebb/src/standard-models/currency-historical.ts @@ -0,0 +1,26 @@ +/** + * Currency Historical Price Standard Model. + * Maps to: openbb_core/provider/standard_models/currency_historical.py + */ + +import { z } from 'zod' + +export const CurrencyHistoricalQueryParamsSchema = z.object({ + symbol: z.string().transform((v) => v.toUpperCase().replace(/-/g, '')), + start_date: z.string().nullable().default(null).describe('Start date of the data, in YYYY-MM-DD format.'), + end_date: z.string().nullable().default(null).describe('End date of the data, in YYYY-MM-DD format.'), +}).passthrough() + +export type CurrencyHistoricalQueryParams = z.infer + +export const CurrencyHistoricalDataSchema = z.object({ + date: z.string().describe('The date of the data.'), + open: z.number().nullable().default(null).describe('The open price.'), + high: z.number().nullable().default(null).describe('The high price.'), + low: z.number().nullable().default(null).describe('The low price.'), + close: z.number().describe('The close price.'), + volume: z.number().nullable().default(null).describe('The trading volume.'), + vwap: z.number().nullable().default(null).describe('Volume Weighted Average Price over the period.'), +}).passthrough() + +export type CurrencyHistoricalData = z.infer diff --git a/packages/opentypebb/src/standard-models/currency-pairs.ts b/packages/opentypebb/src/standard-models/currency-pairs.ts new file mode 100644 index 00000000..eba2e110 --- /dev/null +++ b/packages/opentypebb/src/standard-models/currency-pairs.ts @@ -0,0 +1,19 @@ +/** + * Currency Available Pairs Standard Model. + * Maps to: openbb_core/provider/standard_models/currency_pairs.py + */ + +import { z } from 'zod' + +export const CurrencyPairsQueryParamsSchema = z.object({ + query: z.string().nullable().default(null).describe('Query to search for currency pairs.'), +}).passthrough() + +export type CurrencyPairsQueryParams = z.infer + +export const CurrencyPairsDataSchema = z.object({ + symbol: z.string().describe('Symbol representing the entity requested in the data.'), + name: z.string().nullable().default(null).describe('Name of the currency pair.'), +}).passthrough() + +export type CurrencyPairsData = z.infer diff --git a/packages/opentypebb/src/standard-models/currency-snapshots.ts b/packages/opentypebb/src/standard-models/currency-snapshots.ts new file mode 100644 index 00000000..e9a4c9e0 --- /dev/null +++ b/packages/opentypebb/src/standard-models/currency-snapshots.ts @@ -0,0 +1,30 @@ +/** + * Currency Snapshots Standard Model. + * Maps to: openbb_core/provider/standard_models/currency_snapshots.py + */ + +import { z } from 'zod' + +export const CurrencySnapshotsQueryParamsSchema = z.object({ + base: z.string().default('usd').describe('The base currency symbol.'), + quote_type: z.enum(['direct', 'indirect']).default('indirect').describe('Whether the quote is direct or indirect.'), + counter_currencies: z.string().nullable().default(null).describe('An optional comma-separated list of counter currency symbols to filter for.'), +}).passthrough() + +export type CurrencySnapshotsQueryParams = z.infer + +const numOrNull = z.number().nullable().default(null) + +export const CurrencySnapshotsDataSchema = z.object({ + base_currency: z.string().describe('The base, or domestic, currency.'), + counter_currency: z.string().describe('The counter, or foreign, currency.'), + last_rate: z.number().describe('The exchange rate, relative to the base currency.'), + open: numOrNull.describe('Opening price.'), + high: numOrNull.describe('High price.'), + low: numOrNull.describe('Low price.'), + close: numOrNull.describe('Close price.'), + volume: numOrNull.describe('Trading volume.'), + prev_close: numOrNull.describe('Previous close price.'), +}).passthrough() + +export type CurrencySnapshotsData = z.infer diff --git a/packages/opentypebb/src/standard-models/direction-of-trade.ts b/packages/opentypebb/src/standard-models/direction-of-trade.ts new file mode 100644 index 00000000..494834af --- /dev/null +++ b/packages/opentypebb/src/standard-models/direction-of-trade.ts @@ -0,0 +1,29 @@ +/** + * Direction of Trade Standard Model. + * Maps to: openbb_core/provider/standard_models/direction_of_trade.py + */ + +import { z } from 'zod' + +export const DirectionOfTradeQueryParamsSchema = z.object({ + country: z.string().nullable().default(null).describe('The country to get data for. None is equivalent to all.'), + counterpart: z.string().nullable().default(null).describe('Counterpart country to the trade.'), + direction: z.enum(['exports', 'imports', 'balance', 'all']).default('balance').describe('Trade direction.'), + start_date: z.string().nullable().default(null).describe('Start date of the data, in YYYY-MM-DD format.'), + end_date: z.string().nullable().default(null).describe('End date of the data, in YYYY-MM-DD format.'), + frequency: z.enum(['month', 'quarter', 'annual']).default('month').describe('The frequency of the data.'), +}).passthrough() + +export type DirectionOfTradeQueryParams = z.infer + +export const DirectionOfTradeDataSchema = z.object({ + date: z.string().describe('The date of the data.'), + symbol: z.string().nullable().default(null).describe('Symbol representing the entity.'), + country: z.string().describe('The country.'), + counterpart: z.string().describe('Counterpart country or region to the trade.'), + title: z.string().nullable().default(null).describe('Title corresponding to the symbol.'), + value: z.number().describe('Trade value.'), + scale: z.string().nullable().default(null).describe('Scale of the value.'), +}).passthrough() + +export type DirectionOfTradeData = z.infer diff --git a/packages/opentypebb/src/standard-models/discovery-filings.ts b/packages/opentypebb/src/standard-models/discovery-filings.ts new file mode 100644 index 00000000..2d65d253 --- /dev/null +++ b/packages/opentypebb/src/standard-models/discovery-filings.ts @@ -0,0 +1,26 @@ +/** + * Discovery Filings Standard Model. + * Maps to: openbb_core/provider/standard_models/discovery_filings.py + */ + +import { z } from 'zod' + +export const DiscoveryFilingsQueryParamsSchema = z.object({ + start_date: z.string().nullable().default(null).describe('Start date of the data, in YYYY-MM-DD format.'), + end_date: z.string().nullable().default(null).describe('End date of the data, in YYYY-MM-DD format.'), + form_type: z.string().nullable().default(null).describe('Filter by form type.'), + limit: z.coerce.number().nullable().default(null).describe('The number of data entries to return.'), +}).passthrough() + +export type DiscoveryFilingsQueryParams = z.infer + +export const DiscoveryFilingsDataSchema = z.object({ + symbol: z.string().describe('Symbol representing the entity.'), + cik: z.string().describe('CIK number.'), + filing_date: z.string().describe('The filing date.'), + accepted_date: z.string().describe('The accepted date.'), + form_type: z.string().describe('The form type of the filing.'), + link: z.string().describe('URL to the filing page on the SEC site.'), +}).passthrough() + +export type DiscoveryFilingsData = z.infer diff --git a/packages/opentypebb/src/standard-models/earnings-call-transcript.ts b/packages/opentypebb/src/standard-models/earnings-call-transcript.ts new file mode 100644 index 00000000..e26e96b7 --- /dev/null +++ b/packages/opentypebb/src/standard-models/earnings-call-transcript.ts @@ -0,0 +1,24 @@ +/** + * Earnings Call Transcript Standard Model. + * Maps to: openbb_core/provider/standard_models/earnings_call_transcript.py + */ + +import { z } from 'zod' + +export const EarningsCallTranscriptQueryParamsSchema = z.object({ + symbol: z.string().describe('Symbol to get data for.'), + year: z.coerce.number().nullable().default(null).describe('Year of the earnings call transcript.'), + quarter: z.coerce.number().nullable().default(null).describe('Quarterly period of the earnings call transcript (1-4).'), +}).passthrough() + +export type EarningsCallTranscriptQueryParams = z.infer + +export const EarningsCallTranscriptDataSchema = z.object({ + symbol: z.string().describe('Symbol representing the entity.'), + year: z.number().describe('Year of the earnings call transcript.'), + quarter: z.string().describe('Quarter of the earnings call transcript.'), + date: z.string().describe('The date of the data.'), + content: z.string().describe('Content of the earnings call transcript.'), +}).passthrough() + +export type EarningsCallTranscriptData = z.infer diff --git a/packages/opentypebb/src/standard-models/economic-calendar.ts b/packages/opentypebb/src/standard-models/economic-calendar.ts new file mode 100644 index 00000000..a599942b --- /dev/null +++ b/packages/opentypebb/src/standard-models/economic-calendar.ts @@ -0,0 +1,34 @@ +/** + * Economic Calendar Standard Model. + * Maps to: standard_models/economic_calendar.py + */ + +import { z } from 'zod' + +// --- Query Params --- + +export const EconomicCalendarQueryParamsSchema = z.object({ + start_date: z.string().nullable().default(null).describe('Start date of the data, in YYYY-MM-DD format.'), + end_date: z.string().nullable().default(null).describe('End date of the data, in YYYY-MM-DD format.'), +}) + +export type EconomicCalendarQueryParams = z.infer + +// --- Data --- + +export const EconomicCalendarDataSchema = z.object({ + date: z.string().nullable().default(null).describe('The date of the data.'), + country: z.string().nullable().default(null).describe('Country of event.'), + category: z.string().nullable().default(null).describe('Category of event.'), + event: z.string().nullable().default(null).describe('Event name.'), + importance: z.string().nullable().default(null).describe('The importance level for the event.'), + source: z.string().nullable().default(null).describe('Source of the data.'), + currency: z.string().nullable().default(null).describe('Currency of the data.'), + unit: z.string().nullable().default(null).describe('Unit of the data.'), + consensus: z.union([z.string(), z.number()]).nullable().default(null).describe('Average forecast among a representative group of economists.'), + previous: z.union([z.string(), z.number()]).nullable().default(null).describe('Value for the previous period after the revision.'), + revised: z.union([z.string(), z.number()]).nullable().default(null).describe('Revised previous value, if applicable.'), + actual: z.union([z.string(), z.number()]).nullable().default(null).describe('Latest released value.'), +}).passthrough() + +export type EconomicCalendarData = z.infer diff --git a/packages/opentypebb/src/standard-models/economic-indicators.ts b/packages/opentypebb/src/standard-models/economic-indicators.ts new file mode 100644 index 00000000..2478de30 --- /dev/null +++ b/packages/opentypebb/src/standard-models/economic-indicators.ts @@ -0,0 +1,26 @@ +/** + * Economic Indicators Standard Model. + * Maps to: openbb_core/provider/standard_models/economic_indicators.py + */ + +import { z } from 'zod' + +export const EconomicIndicatorsQueryParamsSchema = z.object({ + symbol: z.string().describe('Symbol to get data for.'), + country: z.string().nullable().default(null).describe('The country to get data for.'), + frequency: z.string().nullable().default(null).describe('The frequency of the data.'), + start_date: z.string().nullable().default(null).describe('Start date of the data, in YYYY-MM-DD format.'), + end_date: z.string().nullable().default(null).describe('End date of the data, in YYYY-MM-DD format.'), +}).passthrough() + +export type EconomicIndicatorsQueryParams = z.infer + +export const EconomicIndicatorsDataSchema = z.object({ + date: z.string().nullable().default(null).describe('The date of the data.'), + symbol_root: z.string().nullable().default(null).describe('The root symbol for the indicator (e.g. GDP).'), + symbol: z.string().nullable().default(null).describe('Symbol representing the entity.'), + country: z.string().nullable().default(null).describe('The country represented by the data.'), + value: z.number().nullable().default(null).describe('The value of the indicator.'), +}).passthrough() + +export type EconomicIndicatorsData = z.infer diff --git a/packages/opentypebb/src/standard-models/equity-discovery.ts b/packages/opentypebb/src/standard-models/equity-discovery.ts new file mode 100644 index 00000000..7c51e212 --- /dev/null +++ b/packages/opentypebb/src/standard-models/equity-discovery.ts @@ -0,0 +1,27 @@ +/** + * Equity Discovery Standard Models (Gainers, Losers, Active). + * Maps to: openbb_core/provider/standard_models/equity_gainers.py (and similar) + * + * Note: In OpenBB Python, equity_gainers.py does not exist as a standard model. + * The gainers/losers/active endpoints are provider-specific. We define a common + * standard model here for TypeScript consistency. + */ + +import { z } from 'zod' + +export const EquityDiscoveryQueryParamsSchema = z.object({ + sort: z.string().nullable().default(null).describe('Sort order.'), +}).passthrough() + +export type EquityDiscoveryQueryParams = z.infer + +export const EquityDiscoveryDataSchema = z.object({ + symbol: z.string().describe('Symbol representing the entity.'), + name: z.string().nullable().default(null).describe('Name of the entity.'), + price: z.number().nullable().default(null).describe('Last price.'), + change: z.number().nullable().default(null).describe('Change in price.'), + percent_change: z.number().nullable().default(null).describe('Percent change in price.'), + volume: z.number().nullable().default(null).describe('Trading volume.'), +}).passthrough() + +export type EquityDiscoveryData = z.infer diff --git a/packages/opentypebb/src/standard-models/equity-historical.ts b/packages/opentypebb/src/standard-models/equity-historical.ts new file mode 100644 index 00000000..64762b77 --- /dev/null +++ b/packages/opentypebb/src/standard-models/equity-historical.ts @@ -0,0 +1,26 @@ +/** + * Equity Historical Price Standard Model. + * Maps to: openbb_core/provider/standard_models/equity_historical.py + */ + +import { z } from 'zod' + +export const EquityHistoricalQueryParamsSchema = z.object({ + symbol: z.string().transform((v) => v.toUpperCase()), + start_date: z.string().nullable().default(null).describe('Start date of the data, in YYYY-MM-DD format.'), + end_date: z.string().nullable().default(null).describe('End date of the data, in YYYY-MM-DD format.'), +}).passthrough() + +export type EquityHistoricalQueryParams = z.infer + +export const EquityHistoricalDataSchema = z.object({ + date: z.string().describe('The date of the data.'), + open: z.number().describe('The open price.'), + high: z.number().describe('The high price.'), + low: z.number().describe('The low price.'), + close: z.number().describe('The close price.'), + volume: z.number().nullable().default(null).describe('The trading volume.'), + vwap: z.number().nullable().default(null).describe('Volume Weighted Average Price over the period.'), +}).passthrough() + +export type EquityHistoricalData = z.infer diff --git a/packages/opentypebb/src/standard-models/equity-info.ts b/packages/opentypebb/src/standard-models/equity-info.ts new file mode 100644 index 00000000..d69894f1 --- /dev/null +++ b/packages/opentypebb/src/standard-models/equity-info.ts @@ -0,0 +1,55 @@ +/** + * Equity Info Standard Model. + * Maps to: openbb_core/provider/standard_models/equity_info.py + */ + +import { z } from 'zod' + +export const EquityInfoQueryParamsSchema = z.object({ + symbol: z.string().transform((v) => v.toUpperCase()), +}).passthrough() + +export type EquityInfoQueryParams = z.infer + +export const EquityInfoDataSchema = z.object({ + symbol: z.string().describe('Symbol representing the entity requested in the data.'), + name: z.string().nullable().default(null).describe('Common name of the company.'), + cik: z.string().nullable().default(null).describe('Central Index Key (CIK) for the requested entity.'), + cusip: z.string().nullable().default(null).describe('CUSIP identifier for the company.'), + isin: z.string().nullable().default(null).describe('International Securities Identification Number.'), + lei: z.string().nullable().default(null).describe('Legal Entity Identifier assigned to the company.'), + legal_name: z.string().nullable().default(null).describe('Official legal name of the company.'), + stock_exchange: z.string().nullable().default(null).describe('Stock exchange where the company is traded.'), + sic: z.number().int().nullable().default(null).describe('Standard Industrial Classification code.'), + short_description: z.string().nullable().default(null).describe('Short description of the company.'), + long_description: z.string().nullable().default(null).describe('Long description of the company.'), + ceo: z.string().nullable().default(null).describe('Chief Executive Officer of the company.'), + company_url: z.string().nullable().default(null).describe("URL of the company's website."), + business_address: z.string().nullable().default(null).describe("Address of the company's headquarters."), + mailing_address: z.string().nullable().default(null).describe('Mailing address of the company.'), + business_phone_no: z.string().nullable().default(null).describe("Phone number of the company's headquarters."), + hq_address1: z.string().nullable().default(null).describe("Address of the company's headquarters."), + hq_address2: z.string().nullable().default(null).describe("Address of the company's headquarters."), + hq_address_city: z.string().nullable().default(null).describe("City of the company's headquarters."), + hq_address_postal_code: z.string().nullable().default(null).describe("Zip code of the company's headquarters."), + hq_state: z.string().nullable().default(null).describe("State of the company's headquarters."), + hq_country: z.string().nullable().default(null).describe("Country of the company's headquarters."), + inc_state: z.string().nullable().default(null).describe('State in which the company is incorporated.'), + inc_country: z.string().nullable().default(null).describe('Country in which the company is incorporated.'), + employees: z.number().int().nullable().default(null).describe('Number of employees.'), + entity_legal_form: z.string().nullable().default(null).describe('Legal form of the company.'), + entity_status: z.string().nullable().default(null).describe('Status of the company.'), + latest_filing_date: z.string().nullable().default(null).describe("Date of the company's latest filing."), + irs_number: z.string().nullable().default(null).describe('IRS number assigned to the company.'), + sector: z.string().nullable().default(null).describe('Sector in which the company operates.'), + industry_category: z.string().nullable().default(null).describe('Category of industry.'), + industry_group: z.string().nullable().default(null).describe('Group of industry.'), + template: z.string().nullable().default(null).describe("Template used to standardize the company's financial statements."), + standardized_active: z.boolean().nullable().default(null).describe('Whether the company is active or not.'), + first_fundamental_date: z.string().nullable().default(null).describe("Date of the company's first fundamental."), + last_fundamental_date: z.string().nullable().default(null).describe("Date of the company's last fundamental."), + first_stock_price_date: z.string().nullable().default(null).describe("Date of the company's first stock price."), + last_stock_price_date: z.string().nullable().default(null).describe("Date of the company's last stock price."), +}).passthrough() + +export type EquityInfoData = z.infer diff --git a/packages/opentypebb/src/standard-models/equity-peers.ts b/packages/opentypebb/src/standard-models/equity-peers.ts new file mode 100644 index 00000000..a037b04e --- /dev/null +++ b/packages/opentypebb/src/standard-models/equity-peers.ts @@ -0,0 +1,16 @@ +/** + * Equity Peers Standard Model. + * Maps to: standard_models/equity_peers.py + */ + +import { z } from 'zod' + +export const EquityPeersQueryParamsSchema = z.object({ + symbol: z.string().transform(v => v.toUpperCase()).describe('Symbol to get data for.'), +}) +export type EquityPeersQueryParams = z.infer + +export const EquityPeersDataSchema = z.object({ + symbol: z.string().describe('Symbol representing the entity.'), +}).passthrough() +export type EquityPeersData = z.infer diff --git a/packages/opentypebb/src/standard-models/equity-performance.ts b/packages/opentypebb/src/standard-models/equity-performance.ts new file mode 100644 index 00000000..5e7be5bb --- /dev/null +++ b/packages/opentypebb/src/standard-models/equity-performance.ts @@ -0,0 +1,23 @@ +/** + * Equity Performance Standard Model. + * Maps to: openbb_core/provider/standard_models/equity_performance.py + */ + +import { z } from 'zod' + +export const EquityPerformanceQueryParamsSchema = z.object({ + sort: z.enum(['asc', 'desc']).default('desc').describe("Sort order. Possible values: 'asc', 'desc'. Default: 'desc'."), +}).passthrough() + +export type EquityPerformanceQueryParams = z.infer + +export const EquityPerformanceDataSchema = z.object({ + symbol: z.string().describe('Symbol representing the entity requested in the data.'), + name: z.string().nullable().default(null).describe('Name of the entity.'), + price: z.number().describe('Last price.'), + change: z.number().describe('Change in price.'), + percent_change: z.number().describe('Percent change.'), + volume: z.number().nullable().default(null).describe('Trading volume.'), +}).passthrough() + +export type EquityPerformanceData = z.infer diff --git a/packages/opentypebb/src/standard-models/equity-quote.ts b/packages/opentypebb/src/standard-models/equity-quote.ts new file mode 100644 index 00000000..ca70e0df --- /dev/null +++ b/packages/opentypebb/src/standard-models/equity-quote.ts @@ -0,0 +1,38 @@ +/** + * Equity Quote Standard Model. + * Maps to: openbb_core/provider/standard_models/equity_quote.py + */ + +import { z } from 'zod' + +export const EquityQuoteQueryParamsSchema = z.object({ + symbol: z.string().transform((v) => v.toUpperCase()), +}).passthrough() + +export type EquityQuoteQueryParams = z.infer + +export const EquityQuoteDataSchema = z.object({ + symbol: z.string().describe('Symbol representing the entity requested in the data.'), + asset_type: z.string().nullable().default(null).describe('Type of asset - i.e, stock, ETF, etc.'), + name: z.string().nullable().default(null).describe('Name of the company or asset.'), + exchange: z.string().nullable().default(null).describe('The name or symbol of the venue where the data is from.'), + bid: z.number().nullable().default(null).describe('Price of the top bid order.'), + bid_size: z.number().int().nullable().default(null).describe('Number of round lot orders at the bid price.'), + ask: z.number().nullable().default(null).describe('Price of the top ask order.'), + ask_size: z.number().int().nullable().default(null).describe('Number of round lot orders at the ask price.'), + last_price: z.number().nullable().default(null).describe('Price of the last trade.'), + last_size: z.number().int().nullable().default(null).describe('Size of the last trade.'), + last_timestamp: z.string().nullable().default(null).describe('Date and Time when the last price was recorded.'), + open: z.number().nullable().default(null).describe('The open price.'), + high: z.number().nullable().default(null).describe('The high price.'), + low: z.number().nullable().default(null).describe('The low price.'), + close: z.number().nullable().default(null).describe('The close price.'), + volume: z.number().nullable().default(null).describe('The trading volume.'), + prev_close: z.number().nullable().default(null).describe('The previous close price.'), + change: z.number().nullable().default(null).describe('Change in price from previous close.'), + change_percent: z.number().nullable().default(null).describe('Change in price as a normalized percentage.'), + year_high: z.number().nullable().default(null).describe('The one year high (52W High).'), + year_low: z.number().nullable().default(null).describe('The one year low (52W Low).'), +}).passthrough() + +export type EquityQuoteData = z.infer diff --git a/packages/opentypebb/src/standard-models/equity-screener.ts b/packages/opentypebb/src/standard-models/equity-screener.ts new file mode 100644 index 00000000..1010ff9a --- /dev/null +++ b/packages/opentypebb/src/standard-models/equity-screener.ts @@ -0,0 +1,15 @@ +/** + * Equity Screener Standard Model. + * Maps to: standard_models/equity_screener.py + */ + +import { z } from 'zod' + +export const EquityScreenerQueryParamsSchema = z.object({}) +export type EquityScreenerQueryParams = z.infer + +export const EquityScreenerDataSchema = z.object({ + symbol: z.string().describe('Symbol representing the entity.'), + name: z.string().nullable().default(null).describe('Name of the company.'), +}).passthrough() +export type EquityScreenerData = z.infer diff --git a/packages/opentypebb/src/standard-models/esg-score.ts b/packages/opentypebb/src/standard-models/esg-score.ts new file mode 100644 index 00000000..a43a156a --- /dev/null +++ b/packages/opentypebb/src/standard-models/esg-score.ts @@ -0,0 +1,30 @@ +/** + * ESG Score Standard Model. + * Maps to: openbb_core/provider/standard_models/esg.py + */ + +import { z } from 'zod' + +const numOrNull = z.number().nullable().default(null) + +export const EsgScoreQueryParamsSchema = z.object({ + symbol: z.string().describe('Symbol to get data for.'), +}).passthrough() + +export type EsgScoreQueryParams = z.infer + +export const EsgScoreDataSchema = z.object({ + symbol: z.string().describe('Symbol representing the entity.'), + cik: z.string().nullable().default(null).describe('CIK number.'), + company_name: z.string().nullable().default(null).describe('Company name.'), + form_type: z.string().nullable().default(null).describe('Form type.'), + accepted_date: z.string().nullable().default(null).describe('Accepted date.'), + date: z.string().nullable().default(null).describe('The date of the data.'), + environmental_score: numOrNull.describe('Environmental score.'), + social_score: numOrNull.describe('Social score.'), + governance_score: numOrNull.describe('Governance score.'), + esg_score: numOrNull.describe('ESG score.'), + url: z.string().nullable().default(null).describe('URL to the filing.'), +}).passthrough() + +export type EsgScoreData = z.infer diff --git a/packages/opentypebb/src/standard-models/etf-countries.ts b/packages/opentypebb/src/standard-models/etf-countries.ts new file mode 100644 index 00000000..f28008fa --- /dev/null +++ b/packages/opentypebb/src/standard-models/etf-countries.ts @@ -0,0 +1,18 @@ +/** + * ETF Countries Standard Model. + * Maps to: standard_models/etf_countries.py + */ + +import { z } from 'zod' + +export const EtfCountriesQueryParamsSchema = z.object({ + symbol: z.string().transform(v => v.toUpperCase()).describe('Symbol to get data for.'), +}) +export type EtfCountriesQueryParams = z.infer + +export const EtfCountriesDataSchema = z.object({ + symbol: z.string().nullable().default(null).describe('Symbol representing the entity.'), + country: z.string().describe('Country of exposure.'), + weight: z.number().describe('Exposure of the ETF to the country in normalized percentage points.'), +}).passthrough() +export type EtfCountriesData = z.infer diff --git a/packages/opentypebb/src/standard-models/etf-equity-exposure.ts b/packages/opentypebb/src/standard-models/etf-equity-exposure.ts new file mode 100644 index 00000000..5c2aebc0 --- /dev/null +++ b/packages/opentypebb/src/standard-models/etf-equity-exposure.ts @@ -0,0 +1,20 @@ +/** + * ETF Equity Exposure Standard Model. + * Maps to: standard_models/etf_equity_exposure.py + */ + +import { z } from 'zod' + +export const EtfEquityExposureQueryParamsSchema = z.object({ + symbol: z.string().transform(v => v.toUpperCase()).describe('Symbol to get data for.'), +}) +export type EtfEquityExposureQueryParams = z.infer + +export const EtfEquityExposureDataSchema = z.object({ + equity_symbol: z.string().describe('The symbol of the equity.'), + etf_symbol: z.string().describe('The symbol of the ETF.'), + weight: z.number().nullable().default(null).describe('The weight of the equity in the ETF.'), + market_value: z.number().nullable().default(null).describe('The market value of the equity in the ETF.'), + shares: z.number().nullable().default(null).describe('The number of shares held.'), +}).passthrough() +export type EtfEquityExposureData = z.infer diff --git a/packages/opentypebb/src/standard-models/etf-holdings.ts b/packages/opentypebb/src/standard-models/etf-holdings.ts new file mode 100644 index 00000000..782fd922 --- /dev/null +++ b/packages/opentypebb/src/standard-models/etf-holdings.ts @@ -0,0 +1,17 @@ +/** + * ETF Holdings Standard Model. + * Maps to: standard_models/etf_holdings.py + */ + +import { z } from 'zod' + +export const EtfHoldingsQueryParamsSchema = z.object({ + symbol: z.string().transform(v => v.toUpperCase()).describe('Symbol to get data for.'), +}) +export type EtfHoldingsQueryParams = z.infer + +export const EtfHoldingsDataSchema = z.object({ + symbol: z.string().nullable().default(null).describe('Symbol representing the holding.'), + name: z.string().nullable().default(null).describe('Name of the holding.'), +}).passthrough() +export type EtfHoldingsData = z.infer diff --git a/packages/opentypebb/src/standard-models/etf-info.ts b/packages/opentypebb/src/standard-models/etf-info.ts new file mode 100644 index 00000000..b97def42 --- /dev/null +++ b/packages/opentypebb/src/standard-models/etf-info.ts @@ -0,0 +1,22 @@ +/** + * ETF Info Standard Model. + * Maps to: standard_models/etf_info.py + */ + +import { z } from 'zod' + +export const EtfInfoQueryParamsSchema = z.object({ + symbol: z.string().transform(v => v.toUpperCase()).describe('Symbol to get data for.'), +}) +export type EtfInfoQueryParams = z.infer + +export const EtfInfoDataSchema = z.object({ + symbol: z.string().describe('Symbol representing the entity.'), + name: z.string().nullable().default(null).describe('Name of the ETF.'), + issuer: z.string().nullable().default(null).describe('Company of the ETF.'), + domicile: z.string().nullable().default(null).describe('Domicile of the ETF.'), + website: z.string().nullable().default(null).describe('Website of the ETF.'), + description: z.string().nullable().default(null).describe('Description of the ETF.'), + inception_date: z.string().nullable().default(null).describe('Inception date of the ETF.'), +}).passthrough() +export type EtfInfoData = z.infer diff --git a/packages/opentypebb/src/standard-models/etf-search.ts b/packages/opentypebb/src/standard-models/etf-search.ts new file mode 100644 index 00000000..bfd8e721 --- /dev/null +++ b/packages/opentypebb/src/standard-models/etf-search.ts @@ -0,0 +1,17 @@ +/** + * ETF Search Standard Model. + * Maps to: standard_models/etf_search.py + */ + +import { z } from 'zod' + +export const EtfSearchQueryParamsSchema = z.object({ + query: z.string().nullable().default(null).describe('Search query.'), +}) +export type EtfSearchQueryParams = z.infer + +export const EtfSearchDataSchema = z.object({ + symbol: z.string().describe('Symbol of the ETF.'), + name: z.string().nullable().default(null).describe('Name of the ETF.'), +}).passthrough() +export type EtfSearchData = z.infer diff --git a/packages/opentypebb/src/standard-models/etf-sectors.ts b/packages/opentypebb/src/standard-models/etf-sectors.ts new file mode 100644 index 00000000..a0fe7590 --- /dev/null +++ b/packages/opentypebb/src/standard-models/etf-sectors.ts @@ -0,0 +1,18 @@ +/** + * ETF Sectors Standard Model. + * Maps to: standard_models/etf_sectors.py + */ + +import { z } from 'zod' + +export const EtfSectorsQueryParamsSchema = z.object({ + symbol: z.string().transform(v => v.toUpperCase()).describe('Symbol to get data for.'), +}) +export type EtfSectorsQueryParams = z.infer + +export const EtfSectorsDataSchema = z.object({ + symbol: z.string().nullable().default(null).describe('Symbol representing the entity.'), + sector: z.string().describe('Sector of exposure.'), + weight: z.number().describe('Exposure of the ETF to the sector in normalized percentage points.'), +}).passthrough() +export type EtfSectorsData = z.infer diff --git a/packages/opentypebb/src/standard-models/executive-compensation.ts b/packages/opentypebb/src/standard-models/executive-compensation.ts new file mode 100644 index 00000000..e41c58f4 --- /dev/null +++ b/packages/opentypebb/src/standard-models/executive-compensation.ts @@ -0,0 +1,30 @@ +/** + * Executive Compensation Standard Model. + * Maps to: standard_models/executive_compensation.py + */ + +import { z } from 'zod' + +export const ExecutiveCompensationQueryParamsSchema = z.object({ + symbol: z.string().transform(v => v.toUpperCase()).describe('Symbol to get data for.'), +}) +export type ExecutiveCompensationQueryParams = z.infer + +const numOrNull = z.number().nullable().default(null) + +export const ExecutiveCompensationDataSchema = z.object({ + symbol: z.string().describe('Symbol representing the entity.'), + cik: z.string().nullable().default(null).describe('CIK number.'), + report_date: z.string().nullable().default(null).describe('Date of reported compensation.'), + company_name: z.string().nullable().default(null).describe('The name of the company.'), + executive: z.string().nullable().default(null).describe('Name and position.'), + year: z.number().nullable().default(null).describe('Year of the compensation.'), + salary: numOrNull.describe('Base salary.'), + bonus: numOrNull.describe('Bonus payments.'), + stock_award: numOrNull.describe('Stock awards.'), + option_award: numOrNull.describe('Option awards.'), + incentive_plan_compensation: numOrNull.describe('Incentive plan compensation.'), + all_other_compensation: numOrNull.describe('All other compensation.'), + total: numOrNull.describe('Total compensation.'), +}).passthrough() +export type ExecutiveCompensationData = z.infer diff --git a/packages/opentypebb/src/standard-models/export-destinations.ts b/packages/opentypebb/src/standard-models/export-destinations.ts new file mode 100644 index 00000000..aef865d2 --- /dev/null +++ b/packages/opentypebb/src/standard-models/export-destinations.ts @@ -0,0 +1,20 @@ +/** + * Export Destinations Standard Model. + * Maps to: openbb_core/provider/standard_models/export_destinations.py + */ + +import { z } from 'zod' + +export const ExportDestinationsQueryParamsSchema = z.object({ + country: z.string().describe('The country to get data for.'), +}).passthrough() + +export type ExportDestinationsQueryParams = z.infer + +export const ExportDestinationsDataSchema = z.object({ + origin_country: z.string().describe('The country of origin.'), + destination_country: z.string().describe('The destination country.'), + value: z.number().describe('The value of the export.'), +}).passthrough() + +export type ExportDestinationsData = z.infer diff --git a/packages/opentypebb/src/standard-models/financial-ratios.ts b/packages/opentypebb/src/standard-models/financial-ratios.ts new file mode 100644 index 00000000..d267705f --- /dev/null +++ b/packages/opentypebb/src/standard-models/financial-ratios.ts @@ -0,0 +1,22 @@ +/** + * Financial Ratios Standard Model. + * Maps to: openbb_core/provider/standard_models/financial_ratios.py + */ + +import { z } from 'zod' + +export const FinancialRatiosQueryParamsSchema = z.object({ + symbol: z.string().transform((v) => v.toUpperCase()), + limit: z.number().int().nullable().default(null).describe('The number of data entries to return.'), +}).passthrough() + +export type FinancialRatiosQueryParams = z.infer + +export const FinancialRatiosDataSchema = z.object({ + symbol: z.string().nullable().default(null).describe('Symbol representing the entity requested in the data.'), + period_ending: z.string().nullable().default(null).describe('The date of the data.'), + fiscal_period: z.string().nullable().default(null).describe('Period of the financial ratios.'), + fiscal_year: z.number().int().nullable().default(null).describe('Fiscal year.'), +}).passthrough() + +export type FinancialRatiosData = z.infer diff --git a/packages/opentypebb/src/standard-models/forward-ebitda-estimates.ts b/packages/opentypebb/src/standard-models/forward-ebitda-estimates.ts new file mode 100644 index 00000000..2a1dba81 --- /dev/null +++ b/packages/opentypebb/src/standard-models/forward-ebitda-estimates.ts @@ -0,0 +1,35 @@ +/** + * Forward EBITDA Estimates Standard Model. + * Maps to: standard_models/forward_ebitda_estimates.py + */ + +import { z } from 'zod' + +// --- Query Params --- + +export const ForwardEbitdaEstimatesQueryParamsSchema = z.object({ + symbol: z.string().transform(v => v.toUpperCase()).describe('Symbol to get data for.'), +}) + +export type ForwardEbitdaEstimatesQueryParams = z.infer + +// --- Data --- + +export const ForwardEbitdaEstimatesDataSchema = z.object({ + symbol: z.string().describe('Symbol representing the entity.'), + name: z.string().nullable().default(null).describe('Name of the entity.'), + last_updated: z.string().nullable().default(null).describe('Last updated timestamp.'), + period_ending: z.string().nullable().default(null).describe('The end date of the reporting period.'), + fiscal_year: z.number().nullable().default(null).describe('Fiscal year for the estimate.'), + fiscal_period: z.string().nullable().default(null).describe('Fiscal period for the estimate.'), + calendar_year: z.number().nullable().default(null).describe('Calendar year for the estimate.'), + calendar_period: z.string().nullable().default(null).describe('Calendar period for the estimate.'), + low_estimate: z.number().nullable().default(null).describe('Low analyst estimate.'), + high_estimate: z.number().nullable().default(null).describe('High analyst estimate.'), + mean: z.number().nullable().default(null).describe('Mean analyst estimate.'), + median: z.number().nullable().default(null).describe('Median analyst estimate.'), + standard_deviation: z.number().nullable().default(null).describe('Standard deviation of estimates.'), + number_of_analysts: z.number().nullable().default(null).describe('Number of analysts providing estimates.'), +}).passthrough() + +export type ForwardEbitdaEstimatesData = z.infer diff --git a/packages/opentypebb/src/standard-models/forward-eps-estimates.ts b/packages/opentypebb/src/standard-models/forward-eps-estimates.ts new file mode 100644 index 00000000..51b13d81 --- /dev/null +++ b/packages/opentypebb/src/standard-models/forward-eps-estimates.ts @@ -0,0 +1,34 @@ +/** + * Forward EPS Estimates Standard Model. + * Maps to: standard_models/forward_eps_estimates.py + */ + +import { z } from 'zod' + +// --- Query Params --- + +export const ForwardEpsEstimatesQueryParamsSchema = z.object({ + symbol: z.string().transform(v => v.toUpperCase()).describe('Symbol to get data for.'), +}) + +export type ForwardEpsEstimatesQueryParams = z.infer + +// --- Data --- + +export const ForwardEpsEstimatesDataSchema = z.object({ + symbol: z.string().describe('Symbol representing the entity.'), + name: z.string().nullable().default(null).describe('Name of the entity.'), + date: z.string().nullable().default(null).describe('The date of the data.'), + fiscal_year: z.number().nullable().default(null).describe('Fiscal year for the estimate.'), + fiscal_period: z.string().nullable().default(null).describe('Fiscal period for the estimate.'), + calendar_year: z.number().nullable().default(null).describe('Calendar year for the estimate.'), + calendar_period: z.string().nullable().default(null).describe('Calendar period for the estimate.'), + low_estimate: z.number().nullable().default(null).describe('Low analyst estimate.'), + high_estimate: z.number().nullable().default(null).describe('High analyst estimate.'), + mean: z.number().nullable().default(null).describe('Mean analyst estimate.'), + median: z.number().nullable().default(null).describe('Median analyst estimate.'), + standard_deviation: z.number().nullable().default(null).describe('Standard deviation of estimates.'), + number_of_analysts: z.number().nullable().default(null).describe('Number of analysts providing estimates.'), +}).passthrough() + +export type ForwardEpsEstimatesData = z.infer diff --git a/packages/opentypebb/src/standard-models/futures-curve.ts b/packages/opentypebb/src/standard-models/futures-curve.ts new file mode 100644 index 00000000..4ba9c79a --- /dev/null +++ b/packages/opentypebb/src/standard-models/futures-curve.ts @@ -0,0 +1,21 @@ +/** + * Futures Curve Standard Model. + * Maps to: openbb_core/provider/standard_models/futures_curve.py + */ + +import { z } from 'zod' + +export const FuturesCurveQueryParamsSchema = z.object({ + symbol: z.string().transform(v => v.toUpperCase()).describe('Symbol to get data for.'), + date: z.string().nullable().default(null).describe('A specific date to get data for.'), +}).passthrough() + +export type FuturesCurveQueryParams = z.infer + +export const FuturesCurveDataSchema = z.object({ + date: z.string().nullable().default(null).describe('The date of the data.'), + expiration: z.string().describe('Futures expiration month.'), + price: z.number().nullable().default(null).describe('The price of the futures contract.'), +}).passthrough() + +export type FuturesCurveData = z.infer diff --git a/packages/opentypebb/src/standard-models/futures-historical.ts b/packages/opentypebb/src/standard-models/futures-historical.ts new file mode 100644 index 00000000..f35eafeb --- /dev/null +++ b/packages/opentypebb/src/standard-models/futures-historical.ts @@ -0,0 +1,26 @@ +/** + * Futures Historical Price Standard Model. + * Maps to: openbb_core/provider/standard_models/futures_historical.py + */ + +import { z } from 'zod' + +export const FuturesHistoricalQueryParamsSchema = z.object({ + symbol: z.string().transform(v => v.toUpperCase()).describe('Symbol to get data for.'), + start_date: z.string().nullable().default(null).describe('Start date of the data, in YYYY-MM-DD format.'), + end_date: z.string().nullable().default(null).describe('End date of the data, in YYYY-MM-DD format.'), + expiration: z.string().nullable().default(null).describe('Future expiry date with format YYYY-MM.'), +}).passthrough() + +export type FuturesHistoricalQueryParams = z.infer + +export const FuturesHistoricalDataSchema = z.object({ + date: z.string().describe('The date of the data.'), + open: z.number().describe('Opening price.'), + high: z.number().describe('High price.'), + low: z.number().describe('Low price.'), + close: z.number().describe('Close price.'), + volume: z.number().describe('Trading volume.'), +}).passthrough() + +export type FuturesHistoricalData = z.infer diff --git a/packages/opentypebb/src/standard-models/futures-info.ts b/packages/opentypebb/src/standard-models/futures-info.ts new file mode 100644 index 00000000..004e42a9 --- /dev/null +++ b/packages/opentypebb/src/standard-models/futures-info.ts @@ -0,0 +1,16 @@ +/** + * Futures Info Standard Model. + * Maps to: openbb_core/provider/standard_models/futures_info.py + */ + +import { z } from 'zod' + +export const FuturesInfoQueryParamsSchema = z.object({}).passthrough() + +export type FuturesInfoQueryParams = z.infer + +export const FuturesInfoDataSchema = z.object({ + symbol: z.string().describe('Symbol representing the entity.'), +}).passthrough() + +export type FuturesInfoData = z.infer diff --git a/packages/opentypebb/src/standard-models/futures-instruments.ts b/packages/opentypebb/src/standard-models/futures-instruments.ts new file mode 100644 index 00000000..31b29fc4 --- /dev/null +++ b/packages/opentypebb/src/standard-models/futures-instruments.ts @@ -0,0 +1,14 @@ +/** + * Futures Instruments Standard Model. + * Maps to: openbb_core/provider/standard_models/futures_instruments.py + */ + +import { z } from 'zod' + +export const FuturesInstrumentsQueryParamsSchema = z.object({}).passthrough() + +export type FuturesInstrumentsQueryParams = z.infer + +export const FuturesInstrumentsDataSchema = z.object({}).passthrough() + +export type FuturesInstrumentsData = z.infer diff --git a/packages/opentypebb/src/standard-models/government-trades.ts b/packages/opentypebb/src/standard-models/government-trades.ts new file mode 100644 index 00000000..3066f867 --- /dev/null +++ b/packages/opentypebb/src/standard-models/government-trades.ts @@ -0,0 +1,21 @@ +/** + * Government Trades Standard Model. + * Maps to: standard_models/government_trades.py + */ + +import { z } from 'zod' + +export const GovernmentTradesQueryParamsSchema = z.object({ + symbol: z.string().nullable().default(null).transform(v => v ? v.toUpperCase() : null).describe('Symbol to get data for.'), + chamber: z.enum(['house', 'senate', 'all']).default('all').describe('Government Chamber.'), + limit: z.coerce.number().nullable().default(null).describe('The number of data entries to return.'), +}) +export type GovernmentTradesQueryParams = z.infer + +export const GovernmentTradesDataSchema = z.object({ + symbol: z.string().nullable().default(null).describe('Symbol representing the entity.'), + date: z.string().describe('The date of the data.'), + transaction_date: z.string().nullable().default(null).describe('Date of Transaction.'), + representative: z.string().nullable().default(null).describe('Name of Representative.'), +}).passthrough() +export type GovernmentTradesData = z.infer diff --git a/packages/opentypebb/src/standard-models/historical-dividends.ts b/packages/opentypebb/src/standard-models/historical-dividends.ts new file mode 100644 index 00000000..cef0d208 --- /dev/null +++ b/packages/opentypebb/src/standard-models/historical-dividends.ts @@ -0,0 +1,20 @@ +/** + * Historical Dividends Standard Model. + * Maps to: standard_models/historical_dividends.py + */ + +import { z } from 'zod' + +export const HistoricalDividendsQueryParamsSchema = z.object({ + symbol: z.string().transform(v => v.toUpperCase()).describe('Symbol to get data for.'), + start_date: z.string().nullable().default(null).describe('Start date of the data, in YYYY-MM-DD format.'), + end_date: z.string().nullable().default(null).describe('End date of the data, in YYYY-MM-DD format.'), +}) +export type HistoricalDividendsQueryParams = z.infer + +export const HistoricalDividendsDataSchema = z.object({ + symbol: z.string().nullable().default(null).describe('Symbol representing the entity.'), + ex_dividend_date: z.string().describe('The ex-dividend date - the date on which the stock begins trading without rights to the dividend.'), + amount: z.number().describe('The dividend amount per share.'), +}).passthrough() +export type HistoricalDividendsData = z.infer diff --git a/packages/opentypebb/src/standard-models/historical-employees.ts b/packages/opentypebb/src/standard-models/historical-employees.ts new file mode 100644 index 00000000..38cd75f8 --- /dev/null +++ b/packages/opentypebb/src/standard-models/historical-employees.ts @@ -0,0 +1,20 @@ +/** + * Historical Employees Standard Model. + * Maps to: standard_models/historical_employees.py + */ + +import { z } from 'zod' + +export const HistoricalEmployeesQueryParamsSchema = z.object({ + symbol: z.string().transform(v => v.toUpperCase()).describe('Symbol to get data for.'), + start_date: z.string().nullable().default(null).describe('Start date of the data, in YYYY-MM-DD format.'), + end_date: z.string().nullable().default(null).describe('End date of the data, in YYYY-MM-DD format.'), +}) +export type HistoricalEmployeesQueryParams = z.infer + +export const HistoricalEmployeesDataSchema = z.object({ + date: z.string().describe('The date of the data.'), + symbol: z.string().nullable().default(null).describe('Symbol representing the entity.'), + employees: z.number().describe('Number of employees.'), +}).passthrough() +export type HistoricalEmployeesData = z.infer diff --git a/packages/opentypebb/src/standard-models/historical-eps.ts b/packages/opentypebb/src/standard-models/historical-eps.ts new file mode 100644 index 00000000..dfe12f88 --- /dev/null +++ b/packages/opentypebb/src/standard-models/historical-eps.ts @@ -0,0 +1,21 @@ +/** + * Historical EPS Standard Model. + * Maps to: standard_models/historical_eps.py + */ + +import { z } from 'zod' + +export const HistoricalEpsQueryParamsSchema = z.object({ + symbol: z.string().transform(v => v.toUpperCase()).describe('Symbol to get data for.'), +}) +export type HistoricalEpsQueryParams = z.infer + +const numOrNull = z.number().nullable().default(null) + +export const HistoricalEpsDataSchema = z.object({ + symbol: z.string().describe('Symbol representing the entity.'), + date: z.string().describe('The date of the data.'), + eps_actual: numOrNull.describe('Actual EPS.'), + eps_estimated: numOrNull.describe('Estimated EPS.'), +}).passthrough() +export type HistoricalEpsData = z.infer diff --git a/packages/opentypebb/src/standard-models/historical-market-cap.ts b/packages/opentypebb/src/standard-models/historical-market-cap.ts new file mode 100644 index 00000000..9c0afc1d --- /dev/null +++ b/packages/opentypebb/src/standard-models/historical-market-cap.ts @@ -0,0 +1,22 @@ +/** + * Historical Market Cap Standard Model. + * Maps to: openbb_core/provider/standard_models/historical_market_cap.py + */ + +import { z } from 'zod' + +export const HistoricalMarketCapQueryParamsSchema = z.object({ + symbol: z.string().describe('Symbol to get data for.'), + start_date: z.string().nullable().default(null).describe('Start date of the data, in YYYY-MM-DD format.'), + end_date: z.string().nullable().default(null).describe('End date of the data, in YYYY-MM-DD format.'), +}).passthrough() + +export type HistoricalMarketCapQueryParams = z.infer + +export const HistoricalMarketCapDataSchema = z.object({ + symbol: z.string().describe('Symbol representing the entity.'), + date: z.string().describe('The date of the data.'), + market_cap: z.number().describe('Market capitalization.'), +}).passthrough() + +export type HistoricalMarketCapData = z.infer diff --git a/packages/opentypebb/src/standard-models/historical-splits.ts b/packages/opentypebb/src/standard-models/historical-splits.ts new file mode 100644 index 00000000..67aabac2 --- /dev/null +++ b/packages/opentypebb/src/standard-models/historical-splits.ts @@ -0,0 +1,19 @@ +/** + * Historical Splits Standard Model. + * Maps to: standard_models/historical_splits.py + */ + +import { z } from 'zod' + +export const HistoricalSplitsQueryParamsSchema = z.object({ + symbol: z.string().transform(v => v.toUpperCase()).describe('Symbol to get data for.'), +}) +export type HistoricalSplitsQueryParams = z.infer + +export const HistoricalSplitsDataSchema = z.object({ + date: z.string().describe('The date of the data.'), + numerator: z.number().nullable().default(null).describe('Numerator of the split.'), + denominator: z.number().nullable().default(null).describe('Denominator of the split.'), + split_ratio: z.string().nullable().default(null).describe('Split ratio.'), +}).passthrough() +export type HistoricalSplitsData = z.infer diff --git a/packages/opentypebb/src/standard-models/income-statement-growth.ts b/packages/opentypebb/src/standard-models/income-statement-growth.ts new file mode 100644 index 00000000..5f8845c3 --- /dev/null +++ b/packages/opentypebb/src/standard-models/income-statement-growth.ts @@ -0,0 +1,25 @@ +/** + * Income Statement Growth Standard Model. + * Maps to: standard_models/income_statement_growth.py + */ + +import { z } from 'zod' + +// --- Query Params --- + +export const IncomeStatementGrowthQueryParamsSchema = z.object({ + symbol: z.string().transform(v => v.toUpperCase()).describe('Symbol to get data for.'), + limit: z.coerce.number().int().nullable().default(null).describe('The number of data entries to return.'), +}) + +export type IncomeStatementGrowthQueryParams = z.infer + +// --- Data --- + +export const IncomeStatementGrowthDataSchema = z.object({ + period_ending: z.string().describe('The end date of the reporting period.'), + fiscal_period: z.string().nullable().default(null).describe('The fiscal period of the report.'), + fiscal_year: z.coerce.number().int().nullable().default(null).describe('The fiscal year of the fiscal period.'), +}).passthrough() + +export type IncomeStatementGrowthData = z.infer diff --git a/packages/opentypebb/src/standard-models/income-statement.ts b/packages/opentypebb/src/standard-models/income-statement.ts new file mode 100644 index 00000000..d57f4476 --- /dev/null +++ b/packages/opentypebb/src/standard-models/income-statement.ts @@ -0,0 +1,21 @@ +/** + * Income Statement Standard Model. + * Maps to: openbb_core/provider/standard_models/income_statement.py + */ + +import { z } from 'zod' + +export const IncomeStatementQueryParamsSchema = z.object({ + symbol: z.string().transform((v) => v.toUpperCase()), + limit: z.number().int().nonnegative().nullable().default(null).describe('The number of data entries to return.'), +}).passthrough() + +export type IncomeStatementQueryParams = z.infer + +export const IncomeStatementDataSchema = z.object({ + period_ending: z.string().describe('The end date of the reporting period.'), + fiscal_period: z.string().nullable().default(null).describe('The fiscal period of the report.'), + fiscal_year: z.number().int().nullable().default(null).describe('The fiscal year of the fiscal period.'), +}).passthrough() + +export type IncomeStatementData = z.infer diff --git a/packages/opentypebb/src/standard-models/index-constituents.ts b/packages/opentypebb/src/standard-models/index-constituents.ts new file mode 100644 index 00000000..f0192526 --- /dev/null +++ b/packages/opentypebb/src/standard-models/index-constituents.ts @@ -0,0 +1,19 @@ +/** + * Index Constituents Standard Model. + * Maps to: openbb_core/provider/standard_models/index_constituents.py + */ + +import { z } from 'zod' + +export const IndexConstituentsQueryParamsSchema = z.object({ + symbol: z.string().describe('Symbol to get data for.'), +}).passthrough() + +export type IndexConstituentsQueryParams = z.infer + +export const IndexConstituentsDataSchema = z.object({ + symbol: z.string().describe('Symbol representing the entity.'), + name: z.string().nullable().default(null).describe('Name of the constituent company in the index.'), +}).passthrough() + +export type IndexConstituentsData = z.infer diff --git a/packages/opentypebb/src/standard-models/index-historical.ts b/packages/opentypebb/src/standard-models/index-historical.ts new file mode 100644 index 00000000..5db070a0 --- /dev/null +++ b/packages/opentypebb/src/standard-models/index-historical.ts @@ -0,0 +1,28 @@ +/** + * Index Historical Standard Model. + * Maps to: openbb_core/provider/standard_models/index_historical.py + */ + +import { z } from 'zod' + +const numOrNull = z.number().nullable().default(null) + +export const IndexHistoricalQueryParamsSchema = z.object({ + symbol: z.string().describe('Symbol to get data for.'), + start_date: z.string().nullable().default(null).describe('Start date of the data, in YYYY-MM-DD format.'), + end_date: z.string().nullable().default(null).describe('End date of the data, in YYYY-MM-DD format.'), +}).passthrough() + +export type IndexHistoricalQueryParams = z.infer + +export const IndexHistoricalDataSchema = z.object({ + symbol: z.string().nullable().default(null).describe('Symbol representing the entity.'), + date: z.string().describe('The date of the data.'), + open: numOrNull.describe('Opening price.'), + high: numOrNull.describe('High price.'), + low: numOrNull.describe('Low price.'), + close: numOrNull.describe('Close price.'), + volume: numOrNull.describe('Trading volume.'), +}).passthrough() + +export type IndexHistoricalData = z.infer diff --git a/packages/opentypebb/src/standard-models/index-search.ts b/packages/opentypebb/src/standard-models/index-search.ts new file mode 100644 index 00000000..56875c41 --- /dev/null +++ b/packages/opentypebb/src/standard-models/index-search.ts @@ -0,0 +1,20 @@ +/** + * Index Search Standard Model. + * Maps to: openbb_core/provider/standard_models/index_search.py + */ + +import { z } from 'zod' + +export const IndexSearchQueryParamsSchema = z.object({ + query: z.string().default('').describe('Search query.'), + is_symbol: z.boolean().default(false).describe('Whether to search by ticker symbol.'), +}).passthrough() + +export type IndexSearchQueryParams = z.infer + +export const IndexSearchDataSchema = z.object({ + symbol: z.string().describe('Symbol representing the entity.'), + name: z.string().describe('Name of the index.'), +}).passthrough() + +export type IndexSearchData = z.infer diff --git a/packages/opentypebb/src/standard-models/index-sectors.ts b/packages/opentypebb/src/standard-models/index-sectors.ts new file mode 100644 index 00000000..0dfcec57 --- /dev/null +++ b/packages/opentypebb/src/standard-models/index-sectors.ts @@ -0,0 +1,19 @@ +/** + * Index Sectors Standard Model. + * Maps to: openbb_core/provider/standard_models/index_sectors.py + */ + +import { z } from 'zod' + +export const IndexSectorsQueryParamsSchema = z.object({ + symbol: z.string().transform(v => v.toUpperCase()).describe('Symbol to get data for.'), +}).passthrough() + +export type IndexSectorsQueryParams = z.infer + +export const IndexSectorsDataSchema = z.object({ + sector: z.string().describe('The sector name.'), + weight: z.number().describe('The weight of the sector in the index.'), +}).passthrough() + +export type IndexSectorsData = z.infer diff --git a/packages/opentypebb/src/standard-models/index.ts b/packages/opentypebb/src/standard-models/index.ts new file mode 100644 index 00000000..357fac1e --- /dev/null +++ b/packages/opentypebb/src/standard-models/index.ts @@ -0,0 +1,578 @@ +/** + * Standard Models — re-export all standard model schemas and types. + * Maps to: openbb_core/provider/standard_models/ + */ + +export { + EquityHistoricalQueryParamsSchema, + type EquityHistoricalQueryParams, + EquityHistoricalDataSchema, + type EquityHistoricalData, +} from './equity-historical.js' + +export { + EquityInfoQueryParamsSchema, + type EquityInfoQueryParams, + EquityInfoDataSchema, + type EquityInfoData, +} from './equity-info.js' + +export { + EquityQuoteQueryParamsSchema, + type EquityQuoteQueryParams, + EquityQuoteDataSchema, + type EquityQuoteData, +} from './equity-quote.js' + +export { + CompanyNewsQueryParamsSchema, + type CompanyNewsQueryParams, + CompanyNewsDataSchema, + type CompanyNewsData, +} from './company-news.js' + +export { + WorldNewsQueryParamsSchema, + type WorldNewsQueryParams, + WorldNewsDataSchema, + type WorldNewsData, +} from './world-news.js' + +export { + CryptoHistoricalQueryParamsSchema, + type CryptoHistoricalQueryParams, + CryptoHistoricalDataSchema, + type CryptoHistoricalData, +} from './crypto-historical.js' + +export { + CurrencyHistoricalQueryParamsSchema, + type CurrencyHistoricalQueryParams, + CurrencyHistoricalDataSchema, + type CurrencyHistoricalData, +} from './currency-historical.js' + +export { + BalanceSheetQueryParamsSchema, + type BalanceSheetQueryParams, + BalanceSheetDataSchema, + type BalanceSheetData, +} from './balance-sheet.js' + +export { + IncomeStatementQueryParamsSchema, + type IncomeStatementQueryParams, + IncomeStatementDataSchema, + type IncomeStatementData, +} from './income-statement.js' + +export { + CashFlowStatementQueryParamsSchema, + type CashFlowStatementQueryParams, + CashFlowStatementDataSchema, + type CashFlowStatementData, +} from './cash-flow.js' + +export { + FinancialRatiosQueryParamsSchema, + type FinancialRatiosQueryParams, + FinancialRatiosDataSchema, + type FinancialRatiosData, +} from './financial-ratios.js' + +export { + KeyMetricsQueryParamsSchema, + type KeyMetricsQueryParams, + KeyMetricsDataSchema, + type KeyMetricsData, +} from './key-metrics.js' + +export { + InsiderTradingQueryParamsSchema, + type InsiderTradingQueryParams, + InsiderTradingDataSchema, + type InsiderTradingData, +} from './insider-trading.js' + +export { + CalendarEarningsQueryParamsSchema, + type CalendarEarningsQueryParams, + CalendarEarningsDataSchema, + type CalendarEarningsData, +} from './calendar-earnings.js' + +export { + EquityDiscoveryQueryParamsSchema, + type EquityDiscoveryQueryParams, + EquityDiscoveryDataSchema, + type EquityDiscoveryData, +} from './equity-discovery.js' + +export { + PriceTargetConsensusQueryParamsSchema, + type PriceTargetConsensusQueryParams, + PriceTargetConsensusDataSchema, + type PriceTargetConsensusData, +} from './price-target-consensus.js' + +export { + CryptoSearchQueryParamsSchema, + type CryptoSearchQueryParams, + CryptoSearchDataSchema, + type CryptoSearchData, +} from './crypto-search.js' + +export { + CurrencyPairsQueryParamsSchema, + type CurrencyPairsQueryParams, + CurrencyPairsDataSchema, + type CurrencyPairsData, +} from './currency-pairs.js' + +export { + EquityPerformanceQueryParamsSchema, + type EquityPerformanceQueryParams, + EquityPerformanceDataSchema, + type EquityPerformanceData, +} from './equity-performance.js' + +export { + BalanceSheetGrowthQueryParamsSchema, + type BalanceSheetGrowthQueryParams, + BalanceSheetGrowthDataSchema, + type BalanceSheetGrowthData, +} from './balance-sheet-growth.js' + +export { + IncomeStatementGrowthQueryParamsSchema, + type IncomeStatementGrowthQueryParams, + IncomeStatementGrowthDataSchema, + type IncomeStatementGrowthData, +} from './income-statement-growth.js' + +export { + CashFlowStatementGrowthQueryParamsSchema, + type CashFlowStatementGrowthQueryParams, + CashFlowStatementGrowthDataSchema, + type CashFlowStatementGrowthData, +} from './cash-flow-growth.js' + +export { + CalendarDividendQueryParamsSchema, + type CalendarDividendQueryParams, + CalendarDividendDataSchema, + type CalendarDividendData, +} from './calendar-dividend.js' + +export { + CalendarSplitsQueryParamsSchema, + type CalendarSplitsQueryParams, + CalendarSplitsDataSchema, + type CalendarSplitsData, +} from './calendar-splits.js' + +export { + CalendarIpoQueryParamsSchema, + type CalendarIpoQueryParams, + CalendarIpoDataSchema, + type CalendarIpoData, +} from './calendar-ipo.js' + +export { + EconomicCalendarQueryParamsSchema, + type EconomicCalendarQueryParams, + EconomicCalendarDataSchema, + type EconomicCalendarData, +} from './economic-calendar.js' + +export { + AnalystEstimatesQueryParamsSchema, + type AnalystEstimatesQueryParams, + AnalystEstimatesDataSchema, + type AnalystEstimatesData, +} from './analyst-estimates.js' + +export { + ForwardEpsEstimatesQueryParamsSchema, + type ForwardEpsEstimatesQueryParams, + ForwardEpsEstimatesDataSchema, + type ForwardEpsEstimatesData, +} from './forward-eps-estimates.js' + +export { + ForwardEbitdaEstimatesQueryParamsSchema, + type ForwardEbitdaEstimatesQueryParams, + ForwardEbitdaEstimatesDataSchema, + type ForwardEbitdaEstimatesData, +} from './forward-ebitda-estimates.js' + +export { + PriceTargetQueryParamsSchema, + type PriceTargetQueryParams, + PriceTargetDataSchema, + type PriceTargetData, +} from './price-target.js' + +export { + EtfInfoQueryParamsSchema, + type EtfInfoQueryParams, + EtfInfoDataSchema, + type EtfInfoData, +} from './etf-info.js' + +export { + EtfHoldingsQueryParamsSchema, + type EtfHoldingsQueryParams, + EtfHoldingsDataSchema, + type EtfHoldingsData, +} from './etf-holdings.js' + +export { + EtfSectorsQueryParamsSchema, + type EtfSectorsQueryParams, + EtfSectorsDataSchema, + type EtfSectorsData, +} from './etf-sectors.js' + +export { + EtfCountriesQueryParamsSchema, + type EtfCountriesQueryParams, + EtfCountriesDataSchema, + type EtfCountriesData, +} from './etf-countries.js' + +export { + EtfEquityExposureQueryParamsSchema, + type EtfEquityExposureQueryParams, + EtfEquityExposureDataSchema, + type EtfEquityExposureData, +} from './etf-equity-exposure.js' + +export { + EtfSearchQueryParamsSchema, + type EtfSearchQueryParams, + EtfSearchDataSchema, + type EtfSearchData, +} from './etf-search.js' + +export { + KeyExecutivesQueryParamsSchema, + type KeyExecutivesQueryParams, + KeyExecutivesDataSchema, + type KeyExecutivesData, +} from './key-executives.js' + +export { + HistoricalDividendsQueryParamsSchema, + type HistoricalDividendsQueryParams, + HistoricalDividendsDataSchema, + type HistoricalDividendsData, +} from './historical-dividends.js' + +export { + ShareStatisticsQueryParamsSchema, + type ShareStatisticsQueryParams, + ShareStatisticsDataSchema, + type ShareStatisticsData, +} from './share-statistics.js' + +export { + ExecutiveCompensationQueryParamsSchema, + type ExecutiveCompensationQueryParams, + ExecutiveCompensationDataSchema, + type ExecutiveCompensationData, +} from './executive-compensation.js' + +export { + GovernmentTradesQueryParamsSchema, + type GovernmentTradesQueryParams, + GovernmentTradesDataSchema, + type GovernmentTradesData, +} from './government-trades.js' + +export { + InstitutionalOwnershipQueryParamsSchema, + type InstitutionalOwnershipQueryParams, + InstitutionalOwnershipDataSchema, + type InstitutionalOwnershipData, +} from './institutional-ownership.js' + +export { + HistoricalSplitsQueryParamsSchema, + type HistoricalSplitsQueryParams, + HistoricalSplitsDataSchema, + type HistoricalSplitsData, +} from './historical-splits.js' + +export { + HistoricalEpsQueryParamsSchema, + type HistoricalEpsQueryParams, + HistoricalEpsDataSchema, + type HistoricalEpsData, +} from './historical-eps.js' + +export { + HistoricalEmployeesQueryParamsSchema, + type HistoricalEmployeesQueryParams, + HistoricalEmployeesDataSchema, + type HistoricalEmployeesData, +} from './historical-employees.js' + +export { + EquityPeersQueryParamsSchema, + type EquityPeersQueryParams, + EquityPeersDataSchema, + type EquityPeersData, +} from './equity-peers.js' + +export { + EquityScreenerQueryParamsSchema, + type EquityScreenerQueryParams, + EquityScreenerDataSchema, + type EquityScreenerData, +} from './equity-screener.js' + +export { + CompanyFilingsQueryParamsSchema, + type CompanyFilingsQueryParams, + CompanyFilingsDataSchema, + type CompanyFilingsData, +} from './company-filings.js' + +export { + RecentPerformanceQueryParamsSchema, + type RecentPerformanceQueryParams, + RecentPerformanceDataSchema, + type RecentPerformanceData, +} from './recent-performance.js' + +export { + MarketSnapshotsQueryParamsSchema, + type MarketSnapshotsQueryParams, + MarketSnapshotsDataSchema, + type MarketSnapshotsData, +} from './market-snapshots.js' + +export { + CurrencySnapshotsQueryParamsSchema, + type CurrencySnapshotsQueryParams, + CurrencySnapshotsDataSchema, + type CurrencySnapshotsData, +} from './currency-snapshots.js' + +export { + AvailableIndicesQueryParamsSchema, + type AvailableIndicesQueryParams, + AvailableIndicesDataSchema, + type AvailableIndicesData, +} from './available-indices.js' + +export { + IndexConstituentsQueryParamsSchema, + type IndexConstituentsQueryParams, + IndexConstituentsDataSchema, + type IndexConstituentsData, +} from './index-constituents.js' + +export { + IndexHistoricalQueryParamsSchema, + type IndexHistoricalQueryParams, + IndexHistoricalDataSchema, + type IndexHistoricalData, +} from './index-historical.js' + +export { + RiskPremiumQueryParamsSchema, + type RiskPremiumQueryParams, + RiskPremiumDataSchema, + type RiskPremiumData, +} from './risk-premium.js' + +export { + TreasuryRatesQueryParamsSchema, + type TreasuryRatesQueryParams, + TreasuryRatesDataSchema, + type TreasuryRatesData, +} from './treasury-rates.js' + +export { + RevenueBusinessLineQueryParamsSchema, + type RevenueBusinessLineQueryParams, + RevenueBusinessLineDataSchema, + type RevenueBusinessLineData, +} from './revenue-business-line.js' + +export { + RevenueGeographicQueryParamsSchema, + type RevenueGeographicQueryParams, + RevenueGeographicDataSchema, + type RevenueGeographicData, +} from './revenue-geographic.js' + +export { + EarningsCallTranscriptQueryParamsSchema, + type EarningsCallTranscriptQueryParams, + EarningsCallTranscriptDataSchema, + type EarningsCallTranscriptData, +} from './earnings-call-transcript.js' + +export { + DiscoveryFilingsQueryParamsSchema, + type DiscoveryFilingsQueryParams, + DiscoveryFilingsDataSchema, + type DiscoveryFilingsData, +} from './discovery-filings.js' + +export { + HistoricalMarketCapQueryParamsSchema, + type HistoricalMarketCapQueryParams, + HistoricalMarketCapDataSchema, + type HistoricalMarketCapData, +} from './historical-market-cap.js' + +export { + EsgScoreQueryParamsSchema, + type EsgScoreQueryParams, + EsgScoreDataSchema, + type EsgScoreData, +} from './esg-score.js' + +export { + FuturesHistoricalQueryParamsSchema, + type FuturesHistoricalQueryParams, + FuturesHistoricalDataSchema, + type FuturesHistoricalData, +} from './futures-historical.js' + +export { + FuturesCurveQueryParamsSchema, + type FuturesCurveQueryParams, + FuturesCurveDataSchema, + type FuturesCurveData, +} from './futures-curve.js' + +export { + OptionsChainsQueryParamsSchema, + type OptionsChainsQueryParams, + OptionsChainsDataSchema, + type OptionsChainsData, +} from './options-chains.js' + +export { + OptionsSnapshotsQueryParamsSchema, + type OptionsSnapshotsQueryParams, + OptionsSnapshotsDataSchema, + type OptionsSnapshotsData, +} from './options-snapshots.js' + +export { + OptionsUnusualQueryParamsSchema, + type OptionsUnusualQueryParams, + OptionsUnusualDataSchema, + type OptionsUnusualData, +} from './options-unusual.js' + +export { + IndexSearchQueryParamsSchema, + type IndexSearchQueryParams, + IndexSearchDataSchema, + type IndexSearchData, +} from './index-search.js' + +export { + IndexSectorsQueryParamsSchema, + type IndexSectorsQueryParams, + IndexSectorsDataSchema, + type IndexSectorsData, +} from './index-sectors.js' + +export { + SP500MultiplesQueryParamsSchema, + type SP500MultiplesQueryParams, + SP500MultiplesDataSchema, + type SP500MultiplesData, +} from './sp500-multiples.js' + +export { + AvailableIndicatorsQueryParamsSchema, + type AvailableIndicatorsQueryParams, + AvailableIndicatorsDataSchema, + type AvailableIndicatorsData, +} from './available-indicators.js' + +export { + ConsumerPriceIndexQueryParamsSchema, + type ConsumerPriceIndexQueryParams, + ConsumerPriceIndexDataSchema, + type ConsumerPriceIndexData, +} from './consumer-price-index.js' + +export { + CompositeLeadingIndicatorQueryParamsSchema, + type CompositeLeadingIndicatorQueryParams, + CompositeLeadingIndicatorDataSchema, + type CompositeLeadingIndicatorData, +} from './composite-leading-indicator.js' + +export { + CountryInterestRatesQueryParamsSchema, + type CountryInterestRatesQueryParams, + CountryInterestRatesDataSchema, + type CountryInterestRatesData, +} from './country-interest-rates.js' + +export { + BalanceOfPaymentsQueryParamsSchema, + type BalanceOfPaymentsQueryParams, + BalanceOfPaymentsDataSchema, + type BalanceOfPaymentsData, +} from './balance-of-payments.js' + +export { + CentralBankHoldingsQueryParamsSchema, + type CentralBankHoldingsQueryParams, + CentralBankHoldingsDataSchema, + type CentralBankHoldingsData, +} from './central-bank-holdings.js' + +export { + CountryProfileQueryParamsSchema, + type CountryProfileQueryParams, + CountryProfileDataSchema, + type CountryProfileData, +} from './country-profile.js' + +export { + DirectionOfTradeQueryParamsSchema, + type DirectionOfTradeQueryParams, + DirectionOfTradeDataSchema, + type DirectionOfTradeData, +} from './direction-of-trade.js' + +export { + ExportDestinationsQueryParamsSchema, + type ExportDestinationsQueryParams, + ExportDestinationsDataSchema, + type ExportDestinationsData, +} from './export-destinations.js' + +export { + EconomicIndicatorsQueryParamsSchema, + type EconomicIndicatorsQueryParams, + EconomicIndicatorsDataSchema, + type EconomicIndicatorsData, +} from './economic-indicators.js' + +export { + FuturesInfoQueryParamsSchema, + type FuturesInfoQueryParams, + FuturesInfoDataSchema, + type FuturesInfoData, +} from './futures-info.js' + +export { + FuturesInstrumentsQueryParamsSchema, + type FuturesInstrumentsQueryParams, + FuturesInstrumentsDataSchema, + type FuturesInstrumentsData, +} from './futures-instruments.js' diff --git a/packages/opentypebb/src/standard-models/insider-trading.ts b/packages/opentypebb/src/standard-models/insider-trading.ts new file mode 100644 index 00000000..c45c2811 --- /dev/null +++ b/packages/opentypebb/src/standard-models/insider-trading.ts @@ -0,0 +1,33 @@ +/** + * Insider Trading Standard Model. + * Maps to: openbb_core/provider/standard_models/insider_trading.py + */ + +import { z } from 'zod' + +export const InsiderTradingQueryParamsSchema = z.object({ + symbol: z.string().transform((v) => v.toUpperCase()), + limit: z.number().int().nullable().default(null).describe('The number of data entries to return.'), +}).passthrough() + +export type InsiderTradingQueryParams = z.infer + +export const InsiderTradingDataSchema = z.object({ + symbol: z.string().nullable().default(null).describe('Symbol representing the entity requested in the data.'), + company_cik: z.string().nullable().default(null).describe('CIK number of the company.'), + filing_date: z.string().nullable().default(null).describe('Filing date of the trade.'), + transaction_date: z.string().nullable().default(null).describe('Date of the transaction.'), + owner_cik: z.union([z.number(), z.string()]).nullable().default(null).describe("Reporting individual's CIK."), + owner_name: z.string().nullable().default(null).describe('Name of the reporting individual.'), + owner_title: z.string().nullable().default(null).describe('The title held by the reporting individual.'), + ownership_type: z.string().nullable().default(null).describe('Type of ownership, e.g., direct or indirect.'), + transaction_type: z.string().nullable().default(null).describe('Type of transaction being reported.'), + acquisition_or_disposition: z.string().nullable().default(null).describe('Acquisition or disposition of the shares.'), + security_type: z.string().nullable().default(null).describe('The type of security transacted.'), + securities_owned: z.number().nullable().default(null).describe('Number of securities owned by the reporting individual.'), + securities_transacted: z.number().nullable().default(null).describe('Number of securities transacted.'), + transaction_price: z.number().nullable().default(null).describe('The price of the transaction.'), + filing_url: z.string().nullable().default(null).describe('Link to the filing.'), +}).passthrough() + +export type InsiderTradingData = z.infer diff --git a/packages/opentypebb/src/standard-models/institutional-ownership.ts b/packages/opentypebb/src/standard-models/institutional-ownership.ts new file mode 100644 index 00000000..7576177b --- /dev/null +++ b/packages/opentypebb/src/standard-models/institutional-ownership.ts @@ -0,0 +1,18 @@ +/** + * Institutional Ownership Standard Model. + * Maps to: standard_models/institutional_ownership.py + */ + +import { z } from 'zod' + +export const InstitutionalOwnershipQueryParamsSchema = z.object({ + symbol: z.string().transform(v => v.toUpperCase()).describe('Symbol to get data for.'), +}) +export type InstitutionalOwnershipQueryParams = z.infer + +export const InstitutionalOwnershipDataSchema = z.object({ + symbol: z.string().describe('Symbol representing the entity.'), + cik: z.string().nullable().default(null).describe('CIK number.'), + date: z.string().describe('The date of the data.'), +}).passthrough() +export type InstitutionalOwnershipData = z.infer diff --git a/packages/opentypebb/src/standard-models/key-executives.ts b/packages/opentypebb/src/standard-models/key-executives.ts new file mode 100644 index 00000000..bdba8b2d --- /dev/null +++ b/packages/opentypebb/src/standard-models/key-executives.ts @@ -0,0 +1,21 @@ +/** + * Key Executives Standard Model. + * Maps to: standard_models/key_executives.py + */ + +import { z } from 'zod' + +export const KeyExecutivesQueryParamsSchema = z.object({ + symbol: z.string().transform(v => v.toUpperCase()).describe('Symbol to get data for.'), +}) +export type KeyExecutivesQueryParams = z.infer + +export const KeyExecutivesDataSchema = z.object({ + title: z.string().describe('Designation of the key executive.'), + name: z.string().describe('Name of the key executive.'), + pay: z.number().nullable().default(null).describe('Pay of the key executive.'), + currency_pay: z.string().nullable().default(null).describe('Currency of the pay.'), + gender: z.string().nullable().default(null).describe('Gender of the key executive.'), + year_born: z.number().nullable().default(null).describe('Birth year of the key executive.'), +}).passthrough() +export type KeyExecutivesData = z.infer diff --git a/packages/opentypebb/src/standard-models/key-metrics.ts b/packages/opentypebb/src/standard-models/key-metrics.ts new file mode 100644 index 00000000..fdbbb3fa --- /dev/null +++ b/packages/opentypebb/src/standard-models/key-metrics.ts @@ -0,0 +1,23 @@ +/** + * Key Metrics Standard Model. + * Maps to: openbb_core/provider/standard_models/key_metrics.py + */ + +import { z } from 'zod' + +export const KeyMetricsQueryParamsSchema = z.object({ + symbol: z.string().transform((v) => v.toUpperCase()), +}).passthrough() + +export type KeyMetricsQueryParams = z.infer + +export const KeyMetricsDataSchema = z.object({ + symbol: z.string().describe('Symbol representing the entity requested in the data.'), + period_ending: z.string().nullable().default(null).describe('End date of the reporting period.'), + fiscal_year: z.number().int().nullable().default(null).describe('Fiscal year for the fiscal period.'), + fiscal_period: z.string().nullable().default(null).describe('Fiscal period for the data.'), + currency: z.string().nullable().default(null).describe('Currency in which the data is reported.'), + market_cap: z.number().nullable().default(null).describe('Market capitalization.'), +}).passthrough() + +export type KeyMetricsData = z.infer diff --git a/packages/opentypebb/src/standard-models/market-snapshots.ts b/packages/opentypebb/src/standard-models/market-snapshots.ts new file mode 100644 index 00000000..c8c0fa4f --- /dev/null +++ b/packages/opentypebb/src/standard-models/market-snapshots.ts @@ -0,0 +1,28 @@ +/** + * Market Snapshots Standard Model. + * Maps to: openbb_core/provider/standard_models/market_snapshots.py + */ + +import { z } from 'zod' + +export const MarketSnapshotsQueryParamsSchema = z.object({}).passthrough() + +export type MarketSnapshotsQueryParams = z.infer + +const numOrNull = z.number().nullable().default(null) + +export const MarketSnapshotsDataSchema = z.object({ + exchange: z.string().nullable().default(null).describe('Exchange the security is listed on.'), + symbol: z.string().describe('Symbol representing the entity.'), + name: z.string().nullable().default(null).describe('Name of the company, fund, or security.'), + open: numOrNull.describe('Opening price.'), + high: numOrNull.describe('High price.'), + low: numOrNull.describe('Low price.'), + close: numOrNull.describe('Close price.'), + volume: numOrNull.describe('Trading volume.'), + prev_close: numOrNull.describe('Previous close price.'), + change: numOrNull.describe('The change in price from the previous close.'), + change_percent: numOrNull.describe('The change in price from the previous close, as a normalized percent.'), +}).passthrough() + +export type MarketSnapshotsData = z.infer diff --git a/packages/opentypebb/src/standard-models/options-chains.ts b/packages/opentypebb/src/standard-models/options-chains.ts new file mode 100644 index 00000000..acfab1ee --- /dev/null +++ b/packages/opentypebb/src/standard-models/options-chains.ts @@ -0,0 +1,54 @@ +/** + * Options Chains Standard Model. + * Maps to: openbb_core/provider/standard_models/options_chains.py + * + * Note: Python uses list-typed fields + model_serializer to zip into records. + * In TypeScript we define the per-record schema directly. + */ + +import { z } from 'zod' + +export const OptionsChainsQueryParamsSchema = z.object({ + symbol: z.string().transform(v => v.toUpperCase()).describe('Symbol to get data for.'), +}).passthrough() + +export type OptionsChainsQueryParams = z.infer + +export const OptionsChainsDataSchema = z.object({ + underlying_symbol: z.string().nullable().default(null).describe('Underlying symbol for the option.'), + underlying_price: z.number().nullable().default(null).describe('Price of the underlying stock.'), + contract_symbol: z.string().describe('Contract symbol for the option.'), + eod_date: z.string().nullable().default(null).describe('Date for which the options chains are returned.'), + expiration: z.string().describe('Expiration date of the contract.'), + dte: z.number().nullable().default(null).describe('Days to expiration of the contract.'), + strike: z.number().describe('Strike price of the contract.'), + option_type: z.string().describe('Call or Put.'), + contract_size: z.number().nullable().default(null).describe('Number of underlying units per contract.'), + open_interest: z.number().nullable().default(null).describe('Open interest on the contract.'), + volume: z.number().nullable().default(null).describe('Trading volume.'), + theoretical_price: z.number().nullable().default(null).describe('Theoretical value of the option.'), + last_trade_price: z.number().nullable().default(null).describe('Last trade price of the option.'), + last_trade_size: z.number().nullable().default(null).describe('Last trade size of the option.'), + last_trade_time: z.string().nullable().default(null).describe('The timestamp of the last trade.'), + tick: z.string().nullable().default(null).describe('Whether the last tick was up or down in price.'), + bid: z.number().nullable().default(null).describe('Current bid price for the option.'), + bid_size: z.number().nullable().default(null).describe('Bid size for the option.'), + ask: z.number().nullable().default(null).describe('Current ask price for the option.'), + ask_size: z.number().nullable().default(null).describe('Ask size for the option.'), + mark: z.number().nullable().default(null).describe('The mid-price between the latest bid and ask.'), + open: z.number().nullable().default(null).describe('Opening price.'), + high: z.number().nullable().default(null).describe('High price.'), + low: z.number().nullable().default(null).describe('Low price.'), + close: z.number().nullable().default(null).describe('Close price.'), + prev_close: z.number().nullable().default(null).describe('Previous close price.'), + change: z.number().nullable().default(null).describe('The change in the price of the option.'), + change_percent: z.number().nullable().default(null).describe('Change, in percent, of the option.'), + implied_volatility: z.number().nullable().default(null).describe('Implied volatility of the option.'), + delta: z.number().nullable().default(null).describe('Delta of the option.'), + gamma: z.number().nullable().default(null).describe('Gamma of the option.'), + theta: z.number().nullable().default(null).describe('Theta of the option.'), + vega: z.number().nullable().default(null).describe('Vega of the option.'), + rho: z.number().nullable().default(null).describe('Rho of the option.'), +}).passthrough() + +export type OptionsChainsData = z.infer diff --git a/packages/opentypebb/src/standard-models/options-snapshots.ts b/packages/opentypebb/src/standard-models/options-snapshots.ts new file mode 100644 index 00000000..f49e0df9 --- /dev/null +++ b/packages/opentypebb/src/standard-models/options-snapshots.ts @@ -0,0 +1,32 @@ +/** + * Options Snapshots Standard Model. + * Maps to: openbb_core/provider/standard_models/options_snapshots.py + * + * Note: Python uses list-typed fields. In TypeScript we define per-record schema. + */ + +import { z } from 'zod' + +export const OptionsSnapshotsQueryParamsSchema = z.object({}).passthrough() + +export type OptionsSnapshotsQueryParams = z.infer + +export const OptionsSnapshotsDataSchema = z.object({ + underlying_symbol: z.string().describe('Ticker symbol of the underlying asset.'), + contract_symbol: z.string().describe('Symbol of the options contract.'), + expiration: z.string().describe('Expiration date of the options contract.'), + dte: z.number().nullable().default(null).describe('Number of days to expiration.'), + strike: z.number().describe('Strike price of the options contract.'), + option_type: z.string().describe('The type of option.'), + volume: z.number().nullable().default(null).describe('Trading volume.'), + open_interest: z.number().nullable().default(null).describe('Open interest at the time.'), + last_price: z.number().nullable().default(null).describe('Last trade price.'), + last_size: z.number().nullable().default(null).describe('Lot size of the last trade.'), + last_timestamp: z.string().nullable().default(null).describe('Timestamp of the last price.'), + open: z.number().nullable().default(null).describe('Opening price.'), + high: z.number().nullable().default(null).describe('High price.'), + low: z.number().nullable().default(null).describe('Low price.'), + close: z.number().nullable().default(null).describe('Close price.'), +}).passthrough() + +export type OptionsSnapshotsData = z.infer diff --git a/packages/opentypebb/src/standard-models/options-unusual.ts b/packages/opentypebb/src/standard-models/options-unusual.ts new file mode 100644 index 00000000..cea9d7db --- /dev/null +++ b/packages/opentypebb/src/standard-models/options-unusual.ts @@ -0,0 +1,19 @@ +/** + * Unusual Options Standard Model. + * Maps to: openbb_core/provider/standard_models/options_unusual.py + */ + +import { z } from 'zod' + +export const OptionsUnusualQueryParamsSchema = z.object({ + symbol: z.string().nullable().default(null).transform(v => v ? v.toUpperCase() : null).describe('Symbol to get data for (the underlying symbol).'), +}).passthrough() + +export type OptionsUnusualQueryParams = z.infer + +export const OptionsUnusualDataSchema = z.object({ + underlying_symbol: z.string().nullable().default(null).describe('Symbol of the underlying asset.'), + contract_symbol: z.string().describe('Contract symbol for the option.'), +}).passthrough() + +export type OptionsUnusualData = z.infer diff --git a/packages/opentypebb/src/standard-models/price-target-consensus.ts b/packages/opentypebb/src/standard-models/price-target-consensus.ts new file mode 100644 index 00000000..fd838b67 --- /dev/null +++ b/packages/opentypebb/src/standard-models/price-target-consensus.ts @@ -0,0 +1,23 @@ +/** + * Price Target Consensus Standard Model. + * Maps to: openbb_core/provider/standard_models/price_target_consensus.py + */ + +import { z } from 'zod' + +export const PriceTargetConsensusQueryParamsSchema = z.object({ + symbol: z.string().nullable().default(null).transform((v) => v?.toUpperCase() ?? null), +}).passthrough() + +export type PriceTargetConsensusQueryParams = z.infer + +export const PriceTargetConsensusDataSchema = z.object({ + symbol: z.string().describe('Symbol representing the entity requested in the data.'), + name: z.string().nullable().default(null).describe('The company name.'), + target_high: z.number().nullable().default(null).describe('High target of the price target consensus.'), + target_low: z.number().nullable().default(null).describe('Low target of the price target consensus.'), + target_consensus: z.number().nullable().default(null).describe('Consensus target of the price target consensus.'), + target_median: z.number().nullable().default(null).describe('Median target of the price target consensus.'), +}).passthrough() + +export type PriceTargetConsensusData = z.infer diff --git a/packages/opentypebb/src/standard-models/price-target.ts b/packages/opentypebb/src/standard-models/price-target.ts new file mode 100644 index 00000000..1cfee7da --- /dev/null +++ b/packages/opentypebb/src/standard-models/price-target.ts @@ -0,0 +1,38 @@ +/** + * Price Target Standard Model. + * Maps to: standard_models/price_target.py + */ + +import { z } from 'zod' + +// --- Query Params --- + +export const PriceTargetQueryParamsSchema = z.object({ + symbol: z.string().transform(v => v.toUpperCase()).describe('Symbol to get data for.'), + limit: z.coerce.number().int().nullable().default(200).describe('The number of data entries to return.'), +}) + +export type PriceTargetQueryParams = z.infer + +// --- Data --- + +export const PriceTargetDataSchema = z.object({ + published_date: z.string().nullable().default(null).describe('Published date of the price target.'), + published_time: z.string().nullable().default(null).describe('Published time of the price target.'), + symbol: z.string().describe('Symbol representing the entity.'), + exchange: z.string().nullable().default(null).describe('Exchange where the stock is listed.'), + company_name: z.string().nullable().default(null).describe('Name of the company.'), + analyst_name: z.string().nullable().default(null).describe('Analyst name.'), + analyst_firm: z.string().nullable().default(null).describe('Analyst firm.'), + currency: z.string().nullable().default(null).describe('Currency of the price target.'), + price_target: z.number().nullable().default(null).describe('Price target.'), + adj_price_target: z.number().nullable().default(null).describe('Adjusted price target.'), + price_target_previous: z.number().nullable().default(null).describe('Previous price target.'), + previous_adj_price_target: z.number().nullable().default(null).describe('Previous adjusted price target.'), + price_when_posted: z.number().nullable().default(null).describe('Price when posted.'), + rating_current: z.string().nullable().default(null).describe('Current rating.'), + rating_previous: z.string().nullable().default(null).describe('Previous rating.'), + action: z.string().nullable().default(null).describe('Description of the rating change.'), +}).passthrough() + +export type PriceTargetData = z.infer diff --git a/packages/opentypebb/src/standard-models/recent-performance.ts b/packages/opentypebb/src/standard-models/recent-performance.ts new file mode 100644 index 00000000..b8367ea1 --- /dev/null +++ b/packages/opentypebb/src/standard-models/recent-performance.ts @@ -0,0 +1,36 @@ +/** + * Recent Performance Standard Model. + * Maps to: openbb_core/provider/standard_models/recent_performance.py + */ + +import { z } from 'zod' + +export const RecentPerformanceQueryParamsSchema = z.object({ + symbol: z.string().describe('Symbol to get data for.'), +}).passthrough() + +export type RecentPerformanceQueryParams = z.infer + +const numOrNull = z.number().nullable().default(null) + +export const RecentPerformanceDataSchema = z.object({ + symbol: z.string().nullable().default(null).describe('The ticker symbol.'), + one_day: numOrNull.describe('One-day return.'), + wtd: numOrNull.describe('Week to date return.'), + one_week: numOrNull.describe('One-week return.'), + mtd: numOrNull.describe('Month to date return.'), + one_month: numOrNull.describe('One-month return.'), + qtd: numOrNull.describe('Quarter to date return.'), + three_month: numOrNull.describe('Three-month return.'), + six_month: numOrNull.describe('Six-month return.'), + ytd: numOrNull.describe('Year to date return.'), + one_year: numOrNull.describe('One-year return.'), + two_year: numOrNull.describe('Two-year return.'), + three_year: numOrNull.describe('Three-year return.'), + four_year: numOrNull.describe('Four-year return.'), + five_year: numOrNull.describe('Five-year return.'), + ten_year: numOrNull.describe('Ten-year return.'), + max: numOrNull.describe('Return from the beginning of the time series.'), +}).passthrough() + +export type RecentPerformanceData = z.infer diff --git a/packages/opentypebb/src/standard-models/revenue-business-line.ts b/packages/opentypebb/src/standard-models/revenue-business-line.ts new file mode 100644 index 00000000..8e32f849 --- /dev/null +++ b/packages/opentypebb/src/standard-models/revenue-business-line.ts @@ -0,0 +1,23 @@ +/** + * Revenue By Business Line Standard Model. + * Maps to: openbb_core/provider/standard_models/revenue_business_line.py + */ + +import { z } from 'zod' + +export const RevenueBusinessLineQueryParamsSchema = z.object({ + symbol: z.string().describe('Symbol to get data for.'), +}).passthrough() + +export type RevenueBusinessLineQueryParams = z.infer + +export const RevenueBusinessLineDataSchema = z.object({ + period_ending: z.string().describe('The end date of the reporting period.'), + fiscal_period: z.string().nullable().default(null).describe('The fiscal period of the reporting period.'), + fiscal_year: z.number().nullable().default(null).describe('The fiscal year of the reporting period.'), + filing_date: z.string().nullable().default(null).describe('The filing date of the report.'), + business_line: z.string().nullable().default(null).describe('The business line represented by the revenue data.'), + revenue: z.number().describe('The total revenue attributed to the business line.'), +}).passthrough() + +export type RevenueBusinessLineData = z.infer diff --git a/packages/opentypebb/src/standard-models/revenue-geographic.ts b/packages/opentypebb/src/standard-models/revenue-geographic.ts new file mode 100644 index 00000000..31703b6a --- /dev/null +++ b/packages/opentypebb/src/standard-models/revenue-geographic.ts @@ -0,0 +1,23 @@ +/** + * Revenue by Geographic Segments Standard Model. + * Maps to: openbb_core/provider/standard_models/revenue_geographic.py + */ + +import { z } from 'zod' + +export const RevenueGeographicQueryParamsSchema = z.object({ + symbol: z.string().describe('Symbol to get data for.'), +}).passthrough() + +export type RevenueGeographicQueryParams = z.infer + +export const RevenueGeographicDataSchema = z.object({ + period_ending: z.string().describe('The end date of the reporting period.'), + fiscal_period: z.string().nullable().default(null).describe('The fiscal period of the reporting period.'), + fiscal_year: z.number().nullable().default(null).describe('The fiscal year of the reporting period.'), + filing_date: z.string().nullable().default(null).describe('The filing date of the report.'), + region: z.string().nullable().default(null).describe('The region represented by the revenue data.'), + revenue: z.number().describe('The total revenue attributed to the region.'), +}).passthrough() + +export type RevenueGeographicData = z.infer diff --git a/packages/opentypebb/src/standard-models/risk-premium.ts b/packages/opentypebb/src/standard-models/risk-premium.ts new file mode 100644 index 00000000..fb20092c --- /dev/null +++ b/packages/opentypebb/src/standard-models/risk-premium.ts @@ -0,0 +1,19 @@ +/** + * Risk Premium Standard Model. + * Maps to: openbb_core/provider/standard_models/risk_premium.py + */ + +import { z } from 'zod' + +export const RiskPremiumQueryParamsSchema = z.object({}).passthrough() + +export type RiskPremiumQueryParams = z.infer + +export const RiskPremiumDataSchema = z.object({ + country: z.string().describe('Market country.'), + continent: z.string().nullable().default(null).describe('Continent of the country.'), + total_equity_risk_premium: z.number().nullable().default(null).describe('Total equity risk premium for the country.'), + country_risk_premium: z.number().nullable().default(null).describe('Country-specific risk premium.'), +}).passthrough() + +export type RiskPremiumData = z.infer diff --git a/packages/opentypebb/src/standard-models/share-statistics.ts b/packages/opentypebb/src/standard-models/share-statistics.ts new file mode 100644 index 00000000..aca57db7 --- /dev/null +++ b/packages/opentypebb/src/standard-models/share-statistics.ts @@ -0,0 +1,22 @@ +/** + * Share Statistics Standard Model. + * Maps to: standard_models/share_statistics.py + */ + +import { z } from 'zod' + +export const ShareStatisticsQueryParamsSchema = z.object({ + symbol: z.string().transform(v => v.toUpperCase()).describe('Symbol to get data for.'), +}) +export type ShareStatisticsQueryParams = z.infer + +const numOrNull = z.number().nullable().default(null) + +export const ShareStatisticsDataSchema = z.object({ + symbol: z.string().describe('Symbol representing the entity requested in the data.'), + date: z.string().nullable().default(null).describe('The date of the data.'), + free_float: numOrNull.describe('Percentage of unrestricted shares of a publicly-traded company.'), + float_shares: numOrNull.describe('Number of shares available for trading by the general public.'), + outstanding_shares: numOrNull.describe('Total number of shares of a publicly-traded company.'), +}).passthrough() +export type ShareStatisticsData = z.infer diff --git a/packages/opentypebb/src/standard-models/sp500-multiples.ts b/packages/opentypebb/src/standard-models/sp500-multiples.ts new file mode 100644 index 00000000..0e3b62ed --- /dev/null +++ b/packages/opentypebb/src/standard-models/sp500-multiples.ts @@ -0,0 +1,22 @@ +/** + * SP500 Multiples Standard Model. + * Maps to: openbb_core/provider/standard_models/sp500_multiples.py + */ + +import { z } from 'zod' + +export const SP500MultiplesQueryParamsSchema = z.object({ + series_name: z.string().default('pe_month').describe('The name of the series. Defaults to pe_month.'), + start_date: z.string().nullable().default(null).describe('Start date of the data, in YYYY-MM-DD format.'), + end_date: z.string().nullable().default(null).describe('End date of the data, in YYYY-MM-DD format.'), +}).passthrough() + +export type SP500MultiplesQueryParams = z.infer + +export const SP500MultiplesDataSchema = z.object({ + date: z.string().describe('The date of the data.'), + name: z.string().describe('Name of the series.'), + value: z.number().describe('Value of the series.'), +}).passthrough() + +export type SP500MultiplesData = z.infer diff --git a/packages/opentypebb/src/standard-models/treasury-rates.ts b/packages/opentypebb/src/standard-models/treasury-rates.ts new file mode 100644 index 00000000..3f8af7ba --- /dev/null +++ b/packages/opentypebb/src/standard-models/treasury-rates.ts @@ -0,0 +1,34 @@ +/** + * Treasury Rates Standard Model. + * Maps to: openbb_core/provider/standard_models/treasury_rates.py + */ + +import { z } from 'zod' + +const rateField = z.number().nullable().default(null) + +export const TreasuryRatesQueryParamsSchema = z.object({ + start_date: z.string().nullable().default(null).describe('Start date of the data, in YYYY-MM-DD format.'), + end_date: z.string().nullable().default(null).describe('End date of the data, in YYYY-MM-DD format.'), +}).passthrough() + +export type TreasuryRatesQueryParams = z.infer + +export const TreasuryRatesDataSchema = z.object({ + date: z.string().describe('The date of the data.'), + week_4: rateField.describe('4 week Treasury bills rate.'), + month_1: rateField.describe('1 month Treasury rate.'), + month_2: rateField.describe('2 month Treasury rate.'), + month_3: rateField.describe('3 month Treasury rate.'), + month_6: rateField.describe('6 month Treasury rate.'), + year_1: rateField.describe('1 year Treasury rate.'), + year_2: rateField.describe('2 year Treasury rate.'), + year_3: rateField.describe('3 year Treasury rate.'), + year_5: rateField.describe('5 year Treasury rate.'), + year_7: rateField.describe('7 year Treasury rate.'), + year_10: rateField.describe('10 year Treasury rate.'), + year_20: rateField.describe('20 year Treasury rate.'), + year_30: rateField.describe('30 year Treasury rate.'), +}).passthrough() + +export type TreasuryRatesData = z.infer diff --git a/packages/opentypebb/src/standard-models/world-news.ts b/packages/opentypebb/src/standard-models/world-news.ts new file mode 100644 index 00000000..f8030db3 --- /dev/null +++ b/packages/opentypebb/src/standard-models/world-news.ts @@ -0,0 +1,26 @@ +/** + * World News Standard Model. + * Maps to: openbb_core/provider/standard_models/world_news.py + */ + +import { z } from 'zod' + +export const WorldNewsQueryParamsSchema = z.object({ + start_date: z.string().nullable().default(null).describe('Start date of the data, in YYYY-MM-DD format.'), + end_date: z.string().nullable().default(null).describe('End date of the data, in YYYY-MM-DD format.'), + limit: z.number().int().nonnegative().nullable().default(null).describe('The number of data entries to return.'), +}).passthrough() + +export type WorldNewsQueryParams = z.infer + +export const WorldNewsDataSchema = z.object({ + date: z.string().describe('The date of publication.'), + title: z.string().describe('Title of the article.'), + author: z.string().nullable().default(null).describe('Author of the article.'), + excerpt: z.string().nullable().default(null).describe('Excerpt of the article text.'), + body: z.string().nullable().default(null).describe('Body of the article text.'), + images: z.unknown().nullable().default(null).describe('Images associated with the article.'), + url: z.string().nullable().default(null).describe('URL to the article.'), +}).passthrough() + +export type WorldNewsData = z.infer diff --git a/packages/opentypebb/tsconfig.json b/packages/opentypebb/tsconfig.json new file mode 100644 index 00000000..87332f7d --- /dev/null +++ b/packages/opentypebb/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2023", + "module": "ESNext", + "moduleResolution": "bundler", + "esModuleInterop": true, + "strict": true, + "outDir": "dist", + "rootDir": ".", + "skipLibCheck": true, + "resolveJsonModule": true, + "declaration": true, + "sourceMap": true, + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["src"], + "exclude": ["node_modules", "dist", "**/*.test.ts"] +} diff --git a/packages/opentypebb/tsup.config.ts b/packages/opentypebb/tsup.config.ts new file mode 100644 index 00000000..c7e5cd50 --- /dev/null +++ b/packages/opentypebb/tsup.config.ts @@ -0,0 +1,13 @@ +import { defineConfig } from 'tsup' + +export default defineConfig({ + entry: { + index: 'src/index.ts', + server: 'src/server.ts', + }, + format: ['esm'], + dts: true, + clean: true, + target: 'node20', + splitting: true, +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c0a79b7e..aff7a5a5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -62,6 +62,9 @@ importers: json5: specifier: ^2.2.3 version: 2.2.3 + opentypebb: + specifier: link:./packages/opentypebb + version: link:packages/opentypebb pino: specifier: ^10.3.1 version: 10.3.1 diff --git a/src/extension/analysis-kit/adapter.ts b/src/extension/analysis-kit/adapter.ts index 8f9e7b92..7030215d 100644 --- a/src/extension/analysis-kit/adapter.ts +++ b/src/extension/analysis-kit/adapter.ts @@ -8,9 +8,7 @@ import { tool } from 'ai' import { z } from 'zod' -import type { OpenBBEquityClient } from '@/openbb/equity/client' -import type { OpenBBCryptoClient } from '@/openbb/crypto/client' -import type { OpenBBCurrencyClient } from '@/openbb/currency/client' +import type { EquityClientLike, CryptoClientLike, CurrencyClientLike } from '@/openbb/sdk/types' import { IndicatorCalculator } from './indicator/calculator' import type { IndicatorContext, OhlcvData } from './indicator/types' @@ -40,9 +38,9 @@ function buildStartDate(interval: string): string { function buildContext( asset: 'equity' | 'crypto' | 'currency', - equityClient: OpenBBEquityClient, - cryptoClient: OpenBBCryptoClient, - currencyClient: OpenBBCurrencyClient, + equityClient: EquityClientLike, + cryptoClient: CryptoClientLike, + currencyClient: CurrencyClientLike, ): IndicatorContext { return { getHistoricalData: async (symbol, interval) => { @@ -68,9 +66,9 @@ function buildContext( } export function createAnalysisTools( - equityClient: OpenBBEquityClient, - cryptoClient: OpenBBCryptoClient, - currencyClient: OpenBBCurrencyClient, + equityClient: EquityClientLike, + cryptoClient: CryptoClientLike, + currencyClient: CurrencyClientLike, ) { return { calculateIndicator: tool({ diff --git a/src/extension/equity/adapter.ts b/src/extension/equity/adapter.ts index 5f24c57e..75f7a13c 100644 --- a/src/extension/equity/adapter.ts +++ b/src/extension/equity/adapter.ts @@ -8,9 +8,9 @@ import { tool } from 'ai' import { z } from 'zod' -import type { OpenBBEquityClient } from '@/openbb/equity/client' +import type { EquityClientLike } from '@/openbb/sdk/types' -export function createEquityTools(equityClient: OpenBBEquityClient) { +export function createEquityTools(equityClient: EquityClientLike) { return { equityGetProfile: tool({ description: `Get company profile and key valuation metrics for a stock. diff --git a/src/extension/market/adapter.ts b/src/extension/market/adapter.ts index 1b89bd88..857ba1eb 100644 --- a/src/extension/market/adapter.ts +++ b/src/extension/market/adapter.ts @@ -12,13 +12,12 @@ import { tool } from 'ai' import { z } from 'zod' import type { SymbolIndex } from '@/openbb/equity/SymbolIndex' -import type { OpenBBCryptoClient } from '@/openbb/crypto/client' -import type { OpenBBCurrencyClient } from '@/openbb/currency/client' +import type { CryptoClientLike, CurrencyClientLike } from '@/openbb/sdk/types' export function createMarketSearchTools( symbolIndex: SymbolIndex, - cryptoClient: OpenBBCryptoClient, - currencyClient: OpenBBCurrencyClient, + cryptoClient: CryptoClientLike, + currencyClient: CurrencyClientLike, ) { return { marketSearchForResearch: tool({ diff --git a/src/extension/news/adapter.ts b/src/extension/news/adapter.ts index f8eec646..0df9231e 100644 --- a/src/extension/news/adapter.ts +++ b/src/extension/news/adapter.ts @@ -7,10 +7,10 @@ import { tool } from 'ai' import { z } from 'zod' -import type { OpenBBNewsClient } from '@/openbb/news/client' +import type { NewsClientLike } from '@/openbb/sdk/types' export function createNewsTools( - newsClient: OpenBBNewsClient, + newsClient: NewsClientLike, providers: { companyProvider: string; worldProvider: string }, ) { return { diff --git a/src/main.ts b/src/main.ts index 5e56ab97..5c46b618 100644 --- a/src/main.ts +++ b/src/main.ts @@ -22,13 +22,10 @@ import type { AccountSetup, GitExportState, ITradingGit, IPlatform } from './ext import { Brain, createBrainTools } from './extension/brain/index.js' import type { BrainExportState } from './extension/brain/index.js' import { createBrowserTools } from './extension/browser/index.js' -import { OpenBBEquityClient, SymbolIndex } from './openbb/equity/index.js' +import { SymbolIndex } from './openbb/equity/index.js' import { createEquityTools } from './extension/equity/index.js' -import { OpenBBCryptoClient } from './openbb/crypto/index.js' -import { OpenBBCurrencyClient } from './openbb/currency/index.js' -import { OpenBBEconomyClient } from './openbb/economy/index.js' -import { OpenBBCommodityClient } from './openbb/commodity/index.js' -import { OpenBBNewsClient } from './openbb/news/index.js' +import { getSDKExecutor, buildRouteMap, SDKEquityClient, SDKCryptoClient, SDKCurrencyClient, SDKNewsClient, SDKEconomyClient, SDKCommodityClient } from './openbb/sdk/index.js' +import { buildSDKCredentials } from './openbb/credential-map.js' import { createMarketSearchTools } from './extension/market/index.js' import { createNewsTools } from './extension/news/index.js' import { createAnalysisTools } from './extension/analysis-kit/index.js' @@ -218,16 +215,18 @@ async function main() { }) await newsStore.init() - // ==================== OpenBB Clients ==================== + // ==================== OpenBB SDK Clients ==================== - const providerKeys = config.openbb.providerKeys const { providers } = config.openbb - const equityClient = new OpenBBEquityClient(config.openbb.apiUrl, providers.equity, providerKeys) - const cryptoClient = new OpenBBCryptoClient(config.openbb.apiUrl, providers.crypto, providerKeys) - const currencyClient = new OpenBBCurrencyClient(config.openbb.apiUrl, providers.currency, providerKeys) - const commodityClient = new OpenBBCommodityClient(config.openbb.apiUrl, undefined, providerKeys) - const economyClient = new OpenBBEconomyClient(config.openbb.apiUrl, undefined, providerKeys) - const newsClient = new OpenBBNewsClient(config.openbb.apiUrl, undefined, providerKeys) + const executor = getSDKExecutor() + const routeMap = buildRouteMap() + const credentials = buildSDKCredentials(config.openbb.providerKeys) + const equityClient = new SDKEquityClient(executor, 'equity', providers.equity, credentials, routeMap) + const cryptoClient = new SDKCryptoClient(executor, 'crypto', providers.crypto, credentials, routeMap) + const currencyClient = new SDKCurrencyClient(executor, 'currency', providers.currency, credentials, routeMap) + const commodityClient = new SDKCommodityClient(executor, 'commodity', undefined, credentials, routeMap) + const economyClient = new SDKEconomyClient(executor, 'economy', undefined, credentials, routeMap) + const newsClient = new SDKNewsClient(executor, 'news', undefined, credentials, routeMap) // ==================== Equity Symbol Index ==================== diff --git a/src/openbb/credential-map.ts b/src/openbb/credential-map.ts index 0e91eed9..52fb89b8 100644 --- a/src/openbb/credential-map.ts +++ b/src/openbb/credential-map.ts @@ -35,3 +35,21 @@ export function buildCredentialsHeader( return Object.keys(mapped).length > 0 ? JSON.stringify(mapped) : undefined } + +/** + * Build credentials object for OpenTypeBB SDK executor. + * Same mapping as buildCredentialsHeader, but returns a plain object + * instead of a JSON string (executor.execute() accepts Record). + */ +export function buildSDKCredentials( + providerKeys: Record | undefined, +): Record { + if (!providerKeys) return {} + + const mapped: Record = {} + for (const [k, v] of Object.entries(providerKeys)) { + if (v && keyMapping[k]) mapped[keyMapping[k]] = v + } + + return mapped +} diff --git a/src/openbb/equity/SymbolIndex.ts b/src/openbb/equity/SymbolIndex.ts index 040ad648..a9d9a76d 100644 --- a/src/openbb/equity/SymbolIndex.ts +++ b/src/openbb/equity/SymbolIndex.ts @@ -15,7 +15,7 @@ import { readFile, writeFile, mkdir } from 'fs/promises' import { resolve, dirname } from 'path' -import type { OpenBBEquityClient } from './client' +import type { EquityClientLike } from '../sdk/types.js' // ==================== Types ==================== @@ -57,7 +57,7 @@ export class SymbolIndex { * 优先从磁盘缓存加载(<24h),否则从 OpenBB API 拉取全量列表。 * API 失败时降级到过期缓存。全部失败则以空索引启动(不中断)。 */ - async load(client: OpenBBEquityClient): Promise { + async load(client: EquityClientLike): Promise { // 1. 尝试读缓存 const cached = await this.readCache() if (cached && !this.isExpired(cached.cachedAt)) { @@ -124,7 +124,7 @@ export class SymbolIndex { // ==================== Internal ==================== - private async fetchFromApi(client: OpenBBEquityClient): Promise { + private async fetchFromApi(client: EquityClientLike): Promise { const allEntries: SymbolEntry[] = [] const seen = new Set() diff --git a/src/openbb/sdk/base-client.ts b/src/openbb/sdk/base-client.ts new file mode 100644 index 00000000..1d717bab --- /dev/null +++ b/src/openbb/sdk/base-client.ts @@ -0,0 +1,45 @@ +/** + * SDK Base Client + * + * Replaces HTTP fetch with in-process executor.execute() calls. + * All 6 domain-specific SDK clients (equity, crypto, currency, news, economy, commodity) + * extend this class and expose the same method signatures as their HTTP counterparts. + * + * Data flow: + * client.getQuote(params) → this.request('/price/quote', params) + * → resolve model via routeMap: '/equity/price/quote' → 'EquityQuote' + * → executor.execute('fmp', 'EquityQuote', params, credentials) + */ + +import type { QueryExecutor } from 'opentypebb' + +export class SDKBaseClient { + constructor( + protected executor: QueryExecutor, + protected routePrefix: string, // 'equity' | 'crypto' | 'currency' | 'news' | 'economy' | 'commodity' + protected defaultProvider: string | undefined, + protected credentials: Record, + protected routeMap: Map, + ) {} + + protected async request>( + path: string, + params: Record = {}, + ): Promise { + const fullPath = `/${this.routePrefix}${path}` + const model = this.routeMap.get(fullPath) + if (!model) { + throw new Error(`No SDK route for: ${fullPath}`) + } + + const provider = (params.provider as string) ?? this.defaultProvider + if (!provider) { + throw new Error(`No provider specified for: ${fullPath}`) + } + + // Remove 'provider' from params — executor takes it as a separate argument + const { provider: _, ...cleanParams } = params + + return this.executor.execute(provider, model, cleanParams, this.credentials) as Promise + } +} diff --git a/src/openbb/sdk/commodity-client.ts b/src/openbb/sdk/commodity-client.ts new file mode 100644 index 00000000..45b10573 --- /dev/null +++ b/src/openbb/sdk/commodity-client.ts @@ -0,0 +1,36 @@ +/** + * SDK Commodity Client + * + * Drop-in replacement for OpenBBCommodityClient. + * + * NOTE: OpenTypeBB does not yet have commodity routes. These methods will throw + * "No SDK route for: /commodity/..." until the corresponding fetchers are added. + */ + +import { SDKBaseClient } from './base-client.js' + +export class SDKCommodityClient extends SDKBaseClient { + async getSpotPrices(params: Record) { + return this.request('/price/spot', params) + } + + async getPsdData(params: Record) { + return this.request('/psd_data', params) + } + + async getPetroleumStatus(params: Record) { + return this.request('/petroleum_status_report', params) + } + + async getEnergyOutlook(params: Record) { + return this.request('/short_term_energy_outlook', params) + } + + async getPsdReport(params: Record) { + return this.request('/psd_report', params) + } + + async getWeatherBulletins(params: Record = {}) { + return this.request('/weather_bulletins', params) + } +} diff --git a/src/openbb/sdk/crypto-client.ts b/src/openbb/sdk/crypto-client.ts new file mode 100644 index 00000000..82b2a9b1 --- /dev/null +++ b/src/openbb/sdk/crypto-client.ts @@ -0,0 +1,17 @@ +/** + * SDK Crypto Client + * + * Drop-in replacement for OpenBBCryptoClient. + */ + +import { SDKBaseClient } from './base-client.js' + +export class SDKCryptoClient extends SDKBaseClient { + async getHistorical(params: Record) { + return this.request('/price/historical', params) + } + + async search(params: Record) { + return this.request('/search', params) + } +} diff --git a/src/openbb/sdk/currency-client.ts b/src/openbb/sdk/currency-client.ts new file mode 100644 index 00000000..26bc7674 --- /dev/null +++ b/src/openbb/sdk/currency-client.ts @@ -0,0 +1,25 @@ +/** + * SDK Currency Client + * + * Drop-in replacement for OpenBBCurrencyClient. + */ + +import { SDKBaseClient } from './base-client.js' + +export class SDKCurrencyClient extends SDKBaseClient { + async getHistorical(params: Record) { + return this.request('/price/historical', params) + } + + async search(params: Record) { + return this.request('/search', params) + } + + async getReferenceRates(params: Record) { + return this.request('/reference_rates', params) + } + + async getSnapshots(params: Record) { + return this.request('/snapshots', params) + } +} diff --git a/src/openbb/sdk/economy-client.ts b/src/openbb/sdk/economy-client.ts new file mode 100644 index 00000000..653f39cd --- /dev/null +++ b/src/openbb/sdk/economy-client.ts @@ -0,0 +1,187 @@ +/** + * SDK Economy Client + * + * Drop-in replacement for OpenBBEconomyClient. + */ + +import { SDKBaseClient } from './base-client.js' + +export class SDKEconomyClient extends SDKBaseClient { + // ==================== Core ==================== + + async getCalendar(params: Record = {}) { + return this.request('/calendar', params) + } + + async getCPI(params: Record) { + return this.request('/cpi', params) + } + + async getRiskPremium(params: Record) { + return this.request('/risk_premium', params) + } + + async getBalanceOfPayments(params: Record) { + return this.request('/balance_of_payments', params) + } + + async getMoneyMeasures(params: Record = {}) { + return this.request('/money_measures', params) + } + + async getUnemployment(params: Record = {}) { + return this.request('/unemployment', params) + } + + async getCompositeLeadingIndicator(params: Record = {}) { + return this.request('/composite_leading_indicator', params) + } + + async getCountryProfile(params: Record) { + return this.request('/country_profile', params) + } + + async getAvailableIndicators(params: Record = {}) { + return this.request('/available_indicators', params) + } + + async getIndicators(params: Record) { + return this.request('/indicators', params) + } + + async getCentralBankHoldings(params: Record = {}) { + return this.request('/central_bank_holdings', params) + } + + async getSharePriceIndex(params: Record = {}) { + return this.request('/share_price_index', params) + } + + async getHousePriceIndex(params: Record = {}) { + return this.request('/house_price_index', params) + } + + async getInterestRates(params: Record = {}) { + return this.request('/interest_rates', params) + } + + async getRetailPrices(params: Record = {}) { + return this.request('/retail_prices', params) + } + + async getPrimaryDealerPositioning(params: Record = {}) { + return this.request('/primary_dealer_positioning', params) + } + + async getPCE(params: Record = {}) { + return this.request('/pce', params) + } + + async getExportDestinations(params: Record) { + return this.request('/export_destinations', params) + } + + async getPrimaryDealerFails(params: Record = {}) { + return this.request('/primary_dealer_fails', params) + } + + async getDirectionOfTrade(params: Record) { + return this.request('/direction_of_trade', params) + } + + async getFomcDocuments(params: Record = {}) { + return this.request('/fomc_documents', params) + } + + async getTotalFactorProductivity(params: Record = {}) { + return this.request('/total_factor_productivity', params) + } + + // ==================== FRED ==================== + + async fredSearch(params: Record) { + return this.request('/fred_search', params) + } + + async fredSeries(params: Record) { + return this.request('/fred_series', params) + } + + async fredReleaseTable(params: Record) { + return this.request('/fred_release_table', params) + } + + async fredRegional(params: Record) { + return this.request('/fred_regional', params) + } + + // ==================== GDP ==================== + + async getGdpForecast(params: Record = {}) { + return this.request('/gdp/forecast', params) + } + + async getGdpNominal(params: Record = {}) { + return this.request('/gdp/nominal', params) + } + + async getGdpReal(params: Record = {}) { + return this.request('/gdp/real', params) + } + + // ==================== Survey ==================== + + async getBlsSeries(params: Record) { + return this.request('/survey/bls_series', params) + } + + async getBlsSearch(params: Record) { + return this.request('/survey/bls_search', params) + } + + async getSloos(params: Record = {}) { + return this.request('/survey/sloos', params) + } + + async getUniversityOfMichigan(params: Record = {}) { + return this.request('/survey/university_of_michigan', params) + } + + async getEconomicConditionsChicago(params: Record = {}) { + return this.request('/survey/economic_conditions_chicago', params) + } + + async getManufacturingOutlookTexas(params: Record = {}) { + return this.request('/survey/manufacturing_outlook_texas', params) + } + + async getManufacturingOutlookNY(params: Record = {}) { + return this.request('/survey/manufacturing_outlook_ny', params) + } + + async getNonfarmPayrolls(params: Record = {}) { + return this.request('/survey/nonfarm_payrolls', params) + } + + async getInflationExpectations(params: Record = {}) { + return this.request('/survey/inflation_expectations', params) + } + + // ==================== Shipping ==================== + + async getPortInfo(params: Record = {}) { + return this.request('/shipping/port_info', params) + } + + async getPortVolume(params: Record = {}) { + return this.request('/shipping/port_volume', params) + } + + async getChokepointInfo(params: Record = {}) { + return this.request('/shipping/chokepoint_info', params) + } + + async getChokepointVolume(params: Record = {}) { + return this.request('/shipping/chokepoint_volume', params) + } +} diff --git a/src/openbb/sdk/equity-client.ts b/src/openbb/sdk/equity-client.ts new file mode 100644 index 00000000..b60931d2 --- /dev/null +++ b/src/openbb/sdk/equity-client.ts @@ -0,0 +1,306 @@ +/** + * SDK Equity Client + * + * Drop-in replacement for OpenBBEquityClient — same method signatures, + * but calls OpenTypeBB's executor instead of HTTP fetch. + */ + +import { SDKBaseClient } from './base-client.js' + +export class SDKEquityClient extends SDKBaseClient { + // ==================== Price ==================== + + async getHistorical(params: Record) { + return this.request('/price/historical', params) + } + + async getQuote(params: Record) { + return this.request('/price/quote', params) + } + + async getNBBO(params: Record) { + return this.request('/price/nbbo', params) + } + + async getPricePerformance(params: Record) { + return this.request('/price/performance', params) + } + + // ==================== Info ==================== + + async search(params: Record) { + return this.request('/search', params) + } + + async screener(params: Record) { + return this.request('/screener', params) + } + + async getProfile(params: Record) { + return this.request('/profile', params) + } + + async getMarketSnapshots(params: Record = {}) { + return this.request('/market_snapshots', params) + } + + async getHistoricalMarketCap(params: Record) { + return this.request('/historical_market_cap', params) + } + + // ==================== Fundamental ==================== + + async getBalanceSheet(params: Record) { + return this.request('/fundamental/balance', params) + } + + async getBalanceSheetGrowth(params: Record) { + return this.request('/fundamental/balance_growth', params) + } + + async getIncomeStatement(params: Record) { + return this.request('/fundamental/income', params) + } + + async getIncomeStatementGrowth(params: Record) { + return this.request('/fundamental/income_growth', params) + } + + async getCashFlow(params: Record) { + return this.request('/fundamental/cash', params) + } + + async getCashFlowGrowth(params: Record) { + return this.request('/fundamental/cash_growth', params) + } + + async getReportedFinancials(params: Record) { + return this.request('/fundamental/reported_financials', params) + } + + async getFinancialRatios(params: Record) { + return this.request('/fundamental/ratios', params) + } + + async getKeyMetrics(params: Record) { + return this.request('/fundamental/metrics', params) + } + + async getDividends(params: Record) { + return this.request('/fundamental/dividends', params) + } + + async getEarningsHistory(params: Record) { + return this.request('/fundamental/historical_eps', params) + } + + async getEmployeeCount(params: Record) { + return this.request('/fundamental/employee_count', params) + } + + async getManagement(params: Record) { + return this.request('/fundamental/management', params) + } + + async getManagementCompensation(params: Record) { + return this.request('/fundamental/management_compensation', params) + } + + async getFilings(params: Record) { + return this.request('/fundamental/filings', params) + } + + async getSplits(params: Record) { + return this.request('/fundamental/historical_splits', params) + } + + async getTranscript(params: Record) { + return this.request('/fundamental/transcript', params) + } + + async getTrailingDividendYield(params: Record) { + return this.request('/fundamental/trailing_dividend_yield', params) + } + + async getRevenuePerGeography(params: Record) { + return this.request('/fundamental/revenue_per_geography', params) + } + + async getRevenuePerSegment(params: Record) { + return this.request('/fundamental/revenue_per_segment', params) + } + + async getEsgScore(params: Record) { + return this.request('/fundamental/esg_score', params) + } + + async getSearchAttributes(params: Record) { + return this.request('/fundamental/search_attributes', params) + } + + async getLatestAttributes(params: Record) { + return this.request('/fundamental/latest_attributes', params) + } + + async getHistoricalAttributes(params: Record) { + return this.request('/fundamental/historical_attributes', params) + } + + // ==================== Calendar ==================== + + async getCalendarIpo(params: Record = {}) { + return this.request('/calendar/ipo', params) + } + + async getCalendarDividend(params: Record = {}) { + return this.request('/calendar/dividend', params) + } + + async getCalendarSplits(params: Record = {}) { + return this.request('/calendar/splits', params) + } + + async getCalendarEarnings(params: Record = {}) { + return this.request('/calendar/earnings', params) + } + + async getCalendarEvents(params: Record = {}) { + return this.request('/calendar/events', params) + } + + // ==================== Estimates ==================== + + async getPriceTarget(params: Record) { + return this.request('/estimates/price_target', params) + } + + async getAnalystEstimates(params: Record) { + return this.request('/estimates/historical', params) + } + + async getEstimateConsensus(params: Record) { + return this.request('/estimates/consensus', params) + } + + async getAnalystSearch(params: Record) { + return this.request('/estimates/analyst_search', params) + } + + async getForwardSales(params: Record) { + return this.request('/estimates/forward_sales', params) + } + + async getForwardEbitda(params: Record) { + return this.request('/estimates/forward_ebitda', params) + } + + async getForwardEps(params: Record) { + return this.request('/estimates/forward_eps', params) + } + + async getForwardPe(params: Record) { + return this.request('/estimates/forward_pe', params) + } + + // ==================== Discovery ==================== + + async getGainers(params: Record = {}) { + return this.request('/discovery/gainers', params) + } + + async getLosers(params: Record = {}) { + return this.request('/discovery/losers', params) + } + + async getActive(params: Record = {}) { + return this.request('/discovery/active', params) + } + + async getUndervaluedLargeCaps(params: Record = {}) { + return this.request('/discovery/undervalued_large_caps', params) + } + + async getUndervaluedGrowth(params: Record = {}) { + return this.request('/discovery/undervalued_growth', params) + } + + async getAggressiveSmallCaps(params: Record = {}) { + return this.request('/discovery/aggressive_small_caps', params) + } + + async getGrowthTech(params: Record = {}) { + return this.request('/discovery/growth_tech', params) + } + + async getTopRetail(params: Record = {}) { + return this.request('/discovery/top_retail', params) + } + + async getDiscoveryFilings(params: Record = {}) { + return this.request('/discovery/filings', params) + } + + async getLatestFinancialReports(params: Record = {}) { + return this.request('/discovery/latest_financial_reports', params) + } + + // ==================== Ownership ==================== + + async getMajorHolders(params: Record) { + return this.request('/ownership/major_holders', params) + } + + async getInstitutional(params: Record) { + return this.request('/ownership/institutional', params) + } + + async getInsiderTrading(params: Record) { + return this.request('/ownership/insider_trading', params) + } + + async getShareStatistics(params: Record) { + return this.request('/ownership/share_statistics', params) + } + + async getForm13F(params: Record) { + return this.request('/ownership/form_13f', params) + } + + async getGovernmentTrades(params: Record = {}) { + return this.request('/ownership/government_trades', params) + } + + // ==================== Shorts ==================== + + async getFailsToDeliver(params: Record) { + return this.request('/shorts/fails_to_deliver', params) + } + + async getShortVolume(params: Record) { + return this.request('/shorts/short_volume', params) + } + + async getShortInterest(params: Record) { + return this.request('/shorts/short_interest', params) + } + + // ==================== Compare ==================== + + async getPeers(params: Record) { + return this.request('/compare/peers', params) + } + + async getCompareGroups(params: Record = {}) { + return this.request('/compare/groups', params) + } + + async getCompareCompanyFacts(params: Record) { + return this.request('/compare/company_facts', params) + } + + // ==================== DarkPool ==================== + + async getOtc(params: Record) { + return this.request('/darkpool/otc', params) + } +} diff --git a/src/openbb/sdk/executor.ts b/src/openbb/sdk/executor.ts new file mode 100644 index 00000000..8b87cc97 --- /dev/null +++ b/src/openbb/sdk/executor.ts @@ -0,0 +1,16 @@ +/** + * SDK Executor Singleton + * + * Creates and caches a QueryExecutor instance from OpenTypeBB. + * The executor can call any of the 114 fetcher models across 11 providers + * without HTTP overhead. + */ + +import { createExecutor, type QueryExecutor } from 'opentypebb' + +let _executor: QueryExecutor | null = null + +export function getSDKExecutor(): QueryExecutor { + if (!_executor) _executor = createExecutor() + return _executor +} diff --git a/src/openbb/sdk/index.ts b/src/openbb/sdk/index.ts new file mode 100644 index 00000000..bccc38d5 --- /dev/null +++ b/src/openbb/sdk/index.ts @@ -0,0 +1,16 @@ +/** + * OpenTypeBB SDK Integration + * + * Provides in-process data fetching via OpenTypeBB's executor, + * replacing the Python OpenBB sidecar HTTP calls. + */ + +export { getSDKExecutor } from './executor.js' +export { buildRouteMap } from './route-map.js' +export { SDKBaseClient } from './base-client.js' +export { SDKEquityClient } from './equity-client.js' +export { SDKCryptoClient } from './crypto-client.js' +export { SDKCurrencyClient } from './currency-client.js' +export { SDKNewsClient } from './news-client.js' +export { SDKEconomyClient } from './economy-client.js' +export { SDKCommodityClient } from './commodity-client.js' diff --git a/src/openbb/sdk/news-client.ts b/src/openbb/sdk/news-client.ts new file mode 100644 index 00000000..9c99ab95 --- /dev/null +++ b/src/openbb/sdk/news-client.ts @@ -0,0 +1,17 @@ +/** + * SDK News Client + * + * Drop-in replacement for OpenBBNewsClient. + */ + +import { SDKBaseClient } from './base-client.js' + +export class SDKNewsClient extends SDKBaseClient { + async getWorldNews(params: Record = {}) { + return this.request('/world', params) + } + + async getCompanyNews(params: Record) { + return this.request('/company', params) + } +} diff --git a/src/openbb/sdk/route-map.ts b/src/openbb/sdk/route-map.ts new file mode 100644 index 00000000..fa4615df --- /dev/null +++ b/src/openbb/sdk/route-map.ts @@ -0,0 +1,28 @@ +/** + * Route Map Builder + * + * Dynamically builds a path → model name mapping from OpenTypeBB's router system. + * e.g. '/equity/price/quote' → 'EquityQuote' + * + * This mapping allows SDKBaseClient.request(path) to resolve which fetcher model + * to call for each API path, providing a drop-in replacement for HTTP routing. + */ + +import { loadAllRouters } from 'opentypebb' + +let _routeMap: Map | null = null + +export function buildRouteMap(): Map { + if (_routeMap) return _routeMap + + const root = loadAllRouters() + const commands = root.getCommandMap() // Map + const map = new Map() + + for (const [path, cmd] of commands) { + map.set(path, cmd.model) + } + + _routeMap = map + return map +} diff --git a/src/openbb/sdk/types.ts b/src/openbb/sdk/types.ts new file mode 100644 index 00000000..7e7e2dcf --- /dev/null +++ b/src/openbb/sdk/types.ts @@ -0,0 +1,38 @@ +/** + * Duck-typed interfaces for OpenBB clients. + * + * Both the HTTP clients (OpenBBEquityClient etc.) and SDK clients (SDKEquityClient etc.) + * satisfy these interfaces, allowing adapters to accept either implementation. + */ + +export interface EquityClientLike { + search(params: Record): Promise[]> + getHistorical(params: Record): Promise[]> + getProfile(params: Record): Promise[]> + getKeyMetrics(params: Record): Promise[]> + getIncomeStatement(params: Record): Promise[]> + getBalanceSheet(params: Record): Promise[]> + getCashFlow(params: Record): Promise[]> + getFinancialRatios(params: Record): Promise[]> + getEstimateConsensus(params: Record): Promise[]> + getCalendarEarnings(params?: Record): Promise[]> + getInsiderTrading(params: Record): Promise[]> + getGainers(params?: Record): Promise[]> + getLosers(params?: Record): Promise[]> + getActive(params?: Record): Promise[]> +} + +export interface CryptoClientLike { + search(params: Record): Promise[]> + getHistorical(params: Record): Promise[]> +} + +export interface CurrencyClientLike { + search(params: Record): Promise[]> + getHistorical(params: Record): Promise[]> +} + +export interface NewsClientLike { + getWorldNews(params?: Record): Promise[]> + getCompanyNews(params: Record): Promise[]> +} From a57538385bb040b4d4b68d037c0fa0a29fe9747d Mon Sep 17 00:00:00 2001 From: Ame Date: Wed, 11 Mar 2026 16:37:16 +0800 Subject: [PATCH 03/23] fix(opentypebb): point exports to src/ so tsx resolves without build open-alice runs via `tsx src/main.ts` which transpiles .ts on-the-fly. The previous exports pointed to dist/ (compiled JS), meaning a build step was required after clone. Now exports point directly to src/*.ts, letting tsx handle transpilation. dist/ is no longer needed at runtime. Co-Authored-By: Claude Opus 4.6 --- packages/opentypebb/package.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/opentypebb/package.json b/packages/opentypebb/package.json index 9b69c27a..8fadd0fe 100644 --- a/packages/opentypebb/package.json +++ b/packages/opentypebb/package.json @@ -5,12 +5,12 @@ "type": "module", "exports": { ".": { - "import": "./dist/index.js", - "types": "./dist/index.d.ts" + "import": "./src/index.ts", + "types": "./src/index.ts" }, "./server": { - "import": "./dist/server.js", - "types": "./dist/server.d.ts" + "import": "./src/server.ts", + "types": "./src/server.ts" } }, "scripts": { From 4a1b91f6be96e5a9addd6b420defe50487eb141f Mon Sep 17 00:00:00 2001 From: Ame Date: Wed, 11 Mar 2026 16:58:33 +0800 Subject: [PATCH 04/23] refactor(ui): redesign DataSourcesPage with two distinct conceptual zones Replace flat linear layout with two visually separated zones that reflect the fundamental difference between structured market data (OpenBB) and accumulative open intelligence (news feeds). - Remove SDKSelector "Active Sources" section; enable/disable moves into each zone's header toggle - Add Zone component: bordered card with title, subtitle, badge, and toggle - Market Data Engine zone: unified asset provider grid merges per-class provider select with inline API key input and test button; utility/macro providers (FRED, BLS, EIA, etc.) in separate collapsible section - Open Intelligence zone: feeds list promoted to primary position above settings; settings rendered in compact two-column layout - Add PROVIDER_KEY_MAP to drive which providers need keys and their key names - Fix handleKeyChange to merge full providerKeys before saving, preventing asset vs utility key sections from clobbering each other Co-Authored-By: Claude Sonnet 4.6 --- ui/src/pages/DataSourcesPage.tsx | 793 ++++++++++++++++++------------- 1 file changed, 454 insertions(+), 339 deletions(-) diff --git a/ui/src/pages/DataSourcesPage.tsx b/ui/src/pages/DataSourcesPage.tsx index 855226bb..79be3d61 100644 --- a/ui/src/pages/DataSourcesPage.tsx +++ b/ui/src/pages/DataSourcesPage.tsx @@ -1,15 +1,13 @@ import { useState } from 'react' import { api, type AppConfig, type NewsCollectorConfig, type NewsCollectorFeed } from '../api' import { SaveIndicator } from '../components/SaveIndicator' -import { SDKSelector, DATASOURCE_OPTIONS } from '../components/SDKSelector' -import { Section, Field, inputClass } from '../components/form' +import { Field, inputClass } from '../components/form' import { Toggle } from '../components/Toggle' import { useConfigPage } from '../hooks/useConfigPage' import type { SaveStatus } from '../hooks/useAutoSave' type OpenbbConfig = Record -/** Combine two save statuses for the header indicator */ function combineStatus(a: SaveStatus, b: SaveStatus): SaveStatus { if (a === 'error' || b === 'error') return 'error' if (a === 'saving' || b === 'saving') return 'saving' @@ -17,113 +15,7 @@ function combineStatus(a: SaveStatus, b: SaveStatus): SaveStatus { return 'idle' } -export function DataSourcesPage() { - const openbb = useConfigPage({ - section: 'openbb', - extract: (full: AppConfig) => (full as Record).openbb as OpenbbConfig, - }) - - const news = useConfigPage({ - section: 'newsCollector', - extract: (full: AppConfig) => (full as Record).newsCollector as NewsCollectorConfig, - }) - - const status = combineStatus(openbb.status, news.status) - const loadError = openbb.loadError || news.loadError - const retry = () => { openbb.retry(); news.retry() } - - // Derive selected data source IDs from enabled flags - const selected: string[] = [] - if (openbb.config) { - if ((openbb.config as Record).enabled !== false) selected.push('openbb') - } else { - selected.push('openbb') // default enabled - } - if (news.config) { - if (news.config.enabled !== false) selected.push('newsCollector') - } else { - selected.push('newsCollector') // default enabled - } - - const handleToggle = (id: string) => { - if (id === 'openbb' && openbb.config) { - const cur = (openbb.config as Record).enabled !== false - openbb.updateConfigImmediate({ enabled: !cur } as Partial) - } else if (id === 'newsCollector' && news.config) { - news.updateConfigImmediate({ enabled: !news.config.enabled }) - } - } - - const openbbEnabled = selected.includes('openbb') - const newsEnabled = selected.includes('newsCollector') - - return ( -
- {/* Header */} -
-
-
-

Data Sources

-

- Market data and news feed configuration. -

-
- -
-
- - {/* Content */} -
-
- {/* Data source selector cards */} -
- -
- - {/* OpenBB section */} - {openbbEnabled && openbb.config && ( - <> - - - - )} - - {/* News Collector section */} - {newsEnabled && news.config && ( - <> - - news.updateConfigImmediate({ feeds })} - /> - - )} -
- {loadError &&

Failed to load configuration.

} -
-
- ) -} - -// ==================== OpenBB: Connection ==================== +// ==================== Constants ==================== const PROVIDER_OPTIONS: Record = { equity: ['yfinance', 'fmp', 'intrinio', 'tiingo', 'alpha_vantage'], @@ -141,264 +33,333 @@ const ASSET_LABELS: Record = { newsWorld: 'News (World)', } -interface ConnectionSectionProps { - openbb: OpenbbConfig - onChange: (patch: Partial) => void - onChangeImmediate: (patch: Partial) => void +/** Maps provider name → providerKeys key. null means free, no key required. */ +const PROVIDER_KEY_MAP: Record = { + yfinance: null, + fmp: 'fmp', + intrinio: 'intrinio', + tiingo: 'tiingo', + alpha_vantage: 'alpha_vantage', + benzinga: 'benzinga', + biztoc: 'biztoc', } -function ConnectionSection({ openbb, onChange, onChangeImmediate }: ConnectionSectionProps) { - const [testing, setTesting] = useState(false) - const [testStatus, setTestStatus] = useState<'idle' | 'ok' | 'error'>('idle') +/** Macro/utility providers used by dedicated endpoints (not per-asset-class selectable) */ +const UTILITY_PROVIDERS = [ + { key: 'fred', name: 'FRED', desc: 'Federal Reserve Economic Data — CPI, GDP, interest rates, macro indicators.', hint: 'Free — fredaccount.stlouisfed.org/apikeys' }, + { key: 'bls', name: 'BLS', desc: 'Bureau of Labor Statistics — employment, payrolls, wages, CPI.', hint: 'Free — registrationapps.bls.gov/bls_registration' }, + { key: 'eia', name: 'EIA', desc: 'Energy Information Administration — petroleum status, energy reports.', hint: 'Free — eia.gov/opendata' }, + { key: 'econdb', name: 'EconDB', desc: 'Global macro indicators, country profiles, shipping data.', hint: 'Optional — works without key (limited). econdb.com' }, + { key: 'nasdaq', name: 'Nasdaq', desc: 'Nasdaq Data Link — dividend/earnings calendars, short interest.', hint: 'Freemium — data.nasdaq.com' }, + { key: 'tradingeconomics', name: 'Trading Economics', desc: '20M+ indicators across 196 countries, economic calendar.', hint: 'Paid — tradingeconomics.com' }, +] as const - const apiUrl = (openbb.apiUrl as string) || 'http://localhost:6900' - const providers = (openbb.providers ?? { equity: 'yfinance', crypto: 'yfinance', currency: 'yfinance', newsCompany: 'yfinance', newsWorld: 'fmp' }) as Record +// ==================== Zone ==================== - const testConnection = async () => { - setTesting(true) - setTestStatus('idle') - try { - const res = await fetch(`${apiUrl}/api/v1/equity/search?query=AAPL&provider=sec`, { signal: AbortSignal.timeout(5000) }) - setTestStatus(res.ok ? 'ok' : 'error') - } catch { - setTestStatus('error') - } finally { - setTesting(false) - } - } +interface ZoneProps { + title: string + subtitle: string + badge?: string + enabled: boolean + onToggle: () => void + children: React.ReactNode +} +function Zone({ title, subtitle, badge, enabled, onToggle, children }: ZoneProps) { return ( -
- - { onChange({ apiUrl: e.target.value }); setTestStatus('idle') }} - placeholder="http://localhost:6900" - /> - - -
- -

Each asset class uses its own data provider. Commodity and economy endpoints use dedicated providers (FRED, EIA, BLS, etc.) per-endpoint.

-
- {Object.entries(PROVIDER_OPTIONS).map(([asset, options]) => ( -
- - -
- ))} +
+
+
+
+

{title}

+ {badge && ( + + {badge} + + )} +
+

{subtitle}

+
- -
- - {testStatus !== 'idle' && ( -
- )} +
+ {children}
-
+ ) } -// ==================== OpenBB: Provider Keys ==================== +// ==================== Market Data Engine ==================== -const FREE_PROVIDERS = [ - { key: 'fred', name: 'FRED', desc: 'Federal Reserve Economic Data — CPI, GDP, interest rates, and thousands of macro indicators.', hint: 'Free — get your key at fredaccount.stlouisfed.org/apikeys' }, - { key: 'bls', name: 'BLS', desc: 'Bureau of Labor Statistics — employment, nonfarm payrolls, wages, and CPI by region.', hint: 'Free — register at registrationapps.bls.gov/bls_registration' }, - { key: 'eia', name: 'EIA', desc: 'Energy Information Administration — petroleum status, energy outlook reports.', hint: 'Free — register at eia.gov/opendata' }, - { key: 'econdb', name: 'EconDB', desc: 'Global macro indicators, country profiles, and port shipping data.', hint: 'Optional — works without key (limited). Register at econdb.com' }, -] as const - -const PAID_PROVIDERS = [ - { key: 'fmp', name: 'FMP', desc: 'Financial Modeling Prep — financial statements, fundamentals, economic calendar, news.', hint: 'Freemium — 250 req/day free at financialmodelingprep.com' }, - { key: 'benzinga', name: 'Benzinga', desc: 'Real-time news, analyst ratings and price targets.', hint: 'Paid — plans at benzinga.com' }, - { key: 'tiingo', name: 'Tiingo', desc: 'News and historical market data.', hint: 'Freemium — free tier at tiingo.com' }, - { key: 'biztoc', name: 'Biztoc', desc: 'Aggregated business and finance news.', hint: 'Freemium — register at biztoc.com' }, - { key: 'nasdaq', name: 'Nasdaq', desc: 'Nasdaq Data Link — dividend/earnings calendars, short interest.', hint: 'Freemium — sign up at data.nasdaq.com' }, - { key: 'intrinio', name: 'Intrinio', desc: 'Equity fundamentals, options data, institutional ownership.', hint: 'Paid — free trial at intrinio.com' }, - { key: 'tradingeconomics', name: 'Trading Economics', desc: 'Global economic calendar, 20M+ indicators across 196 countries.', hint: 'Paid — plans at tradingeconomics.com' }, -] as const - -const ALL_PROVIDER_KEYS = [...FREE_PROVIDERS, ...PAID_PROVIDERS].map((p) => p.key) +interface AssetProviderGridProps { + providers: Record + providerKeys: Record + onProviderChange: (asset: string, provider: string) => void + onKeyChange: (keyName: string, value: string) => void +} -function ProviderKeysSection({ - openbb, - onChange, -}: { - openbb: OpenbbConfig - onChange: (patch: Partial) => void -}) { - const existing = (openbb.providerKeys ?? {}) as Record - const [keys, setKeys] = useState>(() => { - const init: Record = {} - for (const k of ALL_PROVIDER_KEYS) init[k] = existing[k] || '' - return init - }) +function AssetProviderGrid({ providers, providerKeys, onProviderChange, onKeyChange }: AssetProviderGridProps) { + const [localKeys, setLocalKeys] = useState>(() => ({ ...providerKeys })) const [testStatus, setTestStatus] = useState>({}) - const setKey = (k: string, v: string) => { - setKeys((prev) => ({ ...prev, [k]: v })) - setTestStatus((prev) => ({ ...prev, [k]: 'idle' })) - const updated = { ...keys, [k]: v } - const providerKeys: Record = {} - for (const [key, val] of Object.entries(updated)) { - if (val) providerKeys[key] = val - } - onChange({ providerKeys }) + const handleKeyChange = (keyName: string, value: string) => { + setLocalKeys((prev) => ({ ...prev, [keyName]: value })) + setTestStatus((prev) => ({ ...prev, [keyName]: 'idle' })) + onKeyChange(keyName, value) } - const testProvider = async (provider: string) => { - const key = keys[provider] + const testProvider = async (provider: string, keyName: string) => { + const key = localKeys[keyName] if (!key) return - setTestStatus((prev) => ({ ...prev, [provider]: 'testing' })) + setTestStatus((prev) => ({ ...prev, [keyName]: 'testing' })) try { const result = await api.openbb.testProvider(provider, key) - setTestStatus((prev) => ({ ...prev, [provider]: result.ok ? 'ok' : 'error' })) + setTestStatus((prev) => ({ ...prev, [keyName]: result.ok ? 'ok' : 'error' })) } catch { - setTestStatus((prev) => ({ ...prev, [provider]: 'error' })) + setTestStatus((prev) => ({ ...prev, [keyName]: 'error' })) } } - const [expanded, setExpanded] = useState(false) - const configuredCount = Object.values(keys).filter(Boolean).length + return ( +
+

Asset Providers

+ {Object.entries(PROVIDER_OPTIONS).map(([asset, options]) => { + const selectedProvider = providers[asset] || options[0] + const keyName = PROVIDER_KEY_MAP[selectedProvider] ?? null + const status = keyName ? (testStatus[keyName] || 'idle') : 'idle' - const renderGroup = (label: string, providers: ReadonlyArray<{ key: string; name: string; desc: string; hint: string }>) => ( -
-

{label}

- {providers.map(({ key, name, desc, hint }) => { - const status = testStatus[key] || 'idle' return ( - -

{desc}

-

{hint}

-
- setKey(key, e.target.value)} - placeholder="Not configured" - /> - -
-
+
+ {ASSET_LABELS[asset]} + + {keyName ? ( + <> + handleKeyChange(keyName, e.target.value)} + placeholder="API key" + /> + + + ) : ( + Free + )} +
) })}
) +} + +interface UtilityProvidersSectionProps { + providerKeys: Record + onKeyChange: (keyName: string, value: string) => void +} + +function UtilityProvidersSection({ providerKeys, onKeyChange }: UtilityProvidersSectionProps) { + const [expanded, setExpanded] = useState(false) + const [localKeys, setLocalKeys] = useState>(() => { + const init: Record = {} + for (const p of UTILITY_PROVIDERS) init[p.key] = providerKeys[p.key] || '' + return init + }) + const [testStatus, setTestStatus] = useState>({}) + + const configuredCount = Object.values(localKeys).filter(Boolean).length + + const handleKeyChange = (keyName: string, value: string) => { + setLocalKeys((prev) => ({ ...prev, [keyName]: value })) + setTestStatus((prev) => ({ ...prev, [keyName]: 'idle' })) + onKeyChange(keyName, value) + } + + const testProvider = async (keyName: string) => { + const key = localKeys[keyName] + if (!key) return + setTestStatus((prev) => ({ ...prev, [keyName]: 'testing' })) + try { + const result = await api.openbb.testProvider(keyName, key) + setTestStatus((prev) => ({ ...prev, [keyName]: result.ok ? 'ok' : 'error' })) + } catch { + setTestStatus((prev) => ({ ...prev, [keyName]: 'error' })) + } + } return ( -
+
{expanded && (
-

- Optional data providers powered by OpenBB. The default yfinance covers equities, crypto and forex for free. Adding API keys here unlocks macro economic data (CPI, GDP, employment), energy reports, and expanded fundamentals. +

+ Used by dedicated macro endpoints (FRED for CPI/GDP, BLS for employment, EIA for energy). Not per-asset-class selectable.

- {renderGroup('Free', FREE_PROVIDERS)} - {renderGroup('Paid / Freemium', PAID_PROVIDERS)} +
+ {UTILITY_PROVIDERS.map(({ key, name, desc, hint }) => { + const status = testStatus[key] || 'idle' + return ( + +

{desc}

+

{hint}

+
+ handleKeyChange(key, e.target.value)} + placeholder="Not configured" + /> + +
+
+ ) + })} +
)}
) } -// ==================== News Collector: Settings ==================== - -interface NewsSettingsProps { - config: NewsCollectorConfig - onChange: (patch: Partial) => void - onChangeImmediate: (patch: Partial) => void +interface MarketDataZoneProps { + openbb: OpenbbConfig + enabled: boolean + onToggle: () => void + onChange: (patch: Partial) => void + onChangeImmediate: (patch: Partial) => void } -function NewsCollectorSettingsSection({ config, onChange, onChangeImmediate }: NewsSettingsProps) { +function MarketDataZone({ openbb, enabled, onToggle, onChange, onChangeImmediate }: MarketDataZoneProps) { + const [testing, setTesting] = useState(false) + const [testStatus, setTestStatus] = useState<'idle' | 'ok' | 'error'>('idle') + + const apiUrl = (openbb.apiUrl as string) || 'http://localhost:6900' + const providers = (openbb.providers ?? { + equity: 'yfinance', crypto: 'yfinance', currency: 'yfinance', newsCompany: 'yfinance', newsWorld: 'fmp', + }) as Record + const providerKeys = (openbb.providerKeys ?? {}) as Record + + const testConnection = async () => { + setTesting(true) + setTestStatus('idle') + try { + const res = await fetch(`${apiUrl}/api/v1/equity/search?query=AAPL&provider=sec`, { signal: AbortSignal.timeout(5000) }) + setTestStatus(res.ok ? 'ok' : 'error') + } catch { + setTestStatus('error') + } finally { + setTesting(false) + } + } + + const handleProviderChange = (asset: string, provider: string) => { + onChangeImmediate({ providers: { ...providers, [asset]: provider } }) + } + + const handleKeyChange = (keyName: string, value: string) => { + const all = (openbb.providerKeys ?? {}) as Record + const updated = { ...all, [keyName]: value } + const cleaned: Record = {} + for (const [k, v] of Object.entries(updated)) { + if (v) cleaned[k] = v + } + onChange({ providerKeys: cleaned }) + } + return ( -
- - onChange({ intervalMinutes: Number(e.target.value) || 10 })} - /> - - - - onChange({ retentionDays: Number(e.target.value) || 7 })} - /> - - -
-
- Piggyback OpenBB -

- Also capture results from newsGetWorld / newsGetCompany into the news store. -

+ {/* Connection */} +
+

Connection

+
+ { onChange({ apiUrl: e.target.value }); setTestStatus('idle') }} + placeholder="http://localhost:6900" + /> + + {testStatus !== 'idle' && ( +
+ )}
- onChangeImmediate({ piggybackOpenBB: v })} - />
-
+ + {/* Asset providers + inline keys */} + + + {/* Utility/macro providers */} + + ) } -// ==================== News Collector: Feeds ==================== +// ==================== Open Intelligence ==================== function FeedsSection({ feeds, @@ -411,9 +372,7 @@ function FeedsSection({ const [newUrl, setNewUrl] = useState('') const [newSource, setNewSource] = useState('') - const removeFeed = (index: number) => { - onChange(feeds.filter((_, i) => i !== index)) - } + const removeFeed = (index: number) => onChange(feeds.filter((_, i) => i !== index)) const addFeed = () => { if (!newName.trim() || !newUrl.trim() || !newSource.trim()) return @@ -424,17 +383,19 @@ function FeedsSection({ } return ( -
+
+

RSS Feeds

+

+ Collected articles are searchable via globNews / grepNews / readNews. Changes take effect on the next fetch cycle. +

+ {/* Existing feeds */} {feeds.length > 0 && ( -
+
{feeds.map((feed, i) => (

{feed.name}

@@ -455,9 +416,8 @@ function FeedsSection({ ))}
)} - {feeds.length === 0 && ( -

No feeds configured.

+

No feeds configured.

)} {/* Add feed form */} @@ -466,31 +426,16 @@ function FeedsSection({
- setNewName(e.target.value)} - placeholder="e.g. CoinDesk" - /> + setNewName(e.target.value)} placeholder="e.g. CoinDesk" />
- setNewSource(e.target.value)} - placeholder="e.g. coindesk" - /> + setNewSource(e.target.value)} placeholder="e.g. coindesk" />
- setNewUrl(e.target.value)} - placeholder="https://example.com/rss.xml" - /> + setNewUrl(e.target.value)} placeholder="https://example.com/rss.xml" />
-
+
+ ) +} + +interface CompactNewsSettingsProps { + config: NewsCollectorConfig + onChange: (patch: Partial) => void + onChangeImmediate: (patch: Partial) => void +} + +function CompactNewsSettings({ config, onChange, onChangeImmediate }: CompactNewsSettingsProps) { + return ( +
+

Settings

+
+
+ + onChange({ intervalMinutes: Number(e.target.value) || 10 })} + /> +
+
+ + onChange({ retentionDays: Number(e.target.value) || 7 })} + /> +
+
+
+
+ Piggyback OpenBB +

+ Capture results from newsGetWorld / newsGetCompany into the news store. +

+
+ onChangeImmediate({ piggybackOpenBB: v })} /> +
+
+ ) +} + +interface OpenIntelligenceZoneProps { + config: NewsCollectorConfig + enabled: boolean + onToggle: () => void + onChange: (patch: Partial) => void + onChangeImmediate: (patch: Partial) => void +} + +function OpenIntelligenceZone({ config, enabled, onToggle, onChange, onChangeImmediate }: OpenIntelligenceZoneProps) { + const badge = config.feeds.length > 0 ? `${config.feeds.length} feeds` : undefined + + return ( + + onChangeImmediate({ feeds })} + /> + + + ) +} + +// ==================== Page ==================== + +const DEFAULT_NEWS_CONFIG: NewsCollectorConfig = { + enabled: true, + intervalMinutes: 10, + maxInMemory: 2000, + retentionDays: 7, + piggybackOpenBB: true, + feeds: [], +} + +export function DataSourcesPage() { + const openbb = useConfigPage({ + section: 'openbb', + extract: (full: AppConfig) => (full as Record).openbb as OpenbbConfig, + }) + + const news = useConfigPage({ + section: 'newsCollector', + extract: (full: AppConfig) => (full as Record).newsCollector as NewsCollectorConfig, + }) + + const status = combineStatus(openbb.status, news.status) + const loadError = openbb.loadError || news.loadError + const retry = () => { openbb.retry(); news.retry() } + + const openbbEnabled = !openbb.config || (openbb.config as Record).enabled !== false + const newsEnabled = !news.config || news.config.enabled !== false + + return ( +
+ {/* Header */} +
+
+
+

Data Sources

+

+ Market data and news feed configuration. +

+
+ +
+
+ + {/* Content */} +
+
+ {/* Market Data Engine zone */} + {openbb.config ? ( + openbb.updateConfigImmediate({ enabled: !openbbEnabled } as Partial)} + onChange={openbb.updateConfig} + onChangeImmediate={openbb.updateConfigImmediate} + /> + ) : ( + {}} + > +

Loading…

+
+ )} + + {/* Open Intelligence zone */} + {news.config ? ( + news.updateConfigImmediate({ enabled: !newsEnabled })} + onChange={news.updateConfig} + onChangeImmediate={news.updateConfigImmediate} + /> + ) : ( + {}} + > +

Loading…

+
+ )} +
+ {loadError &&

Failed to load configuration.

} +
+
) } From 174ba333cbbf0e9a24c6d8953dc147a921f01b05 Mon Sep 17 00:00:00 2001 From: Ame Date: Wed, 11 Mar 2026 17:22:45 +0800 Subject: [PATCH 05/23] feat(openbb): add backend selector and embedded API server support Expose two independent config dimensions in the Market Data Engine: 1. Data backend selection (sdk | openbb): - SDK: uses opentypebb in-process executor (default, no external deps) - OpenBB: connects to an external OpenBB HTTP server via apiUrl - main.ts conditionally instantiates SDK or HTTP clients based on config - Connection URL field now only shown when external backend is selected 2. Embedded OpenBB API server: - New src/openbb/api-server.ts starts an opentypebb Hono HTTP server - Exposes OpenBB-compatible /api/v1/* endpoints for external tools - Configurable port (default 6901), toggle in UI - Runs independently of which backend Alice uses internally Config schema additions (src/core/config.ts): - openbb.dataBackend: 'sdk' | 'openbb' (default: 'sdk') - openbb.apiServer: { enabled: boolean, port: number } (default: off/6901) Co-Authored-By: Claude Sonnet 4.6 --- src/core/config.ts | 5 ++ src/main.ts | 45 +++++++++--- src/openbb/api-server.ts | 26 +++++++ ui/src/pages/DataSourcesPage.tsx | 113 ++++++++++++++++++++++++------- 4 files changed, 153 insertions(+), 36 deletions(-) create mode 100644 src/openbb/api-server.ts diff --git a/src/core/config.ts b/src/core/config.ts index 0ef8adc5..bf9f59a0 100644 --- a/src/core/config.ts +++ b/src/core/config.ts @@ -122,6 +122,11 @@ const openbbSchema = z.object({ tiingo: z.string().optional(), biztoc: z.string().optional(), }).default({}), + dataBackend: z.enum(['sdk', 'openbb']).default('sdk'), + apiServer: z.object({ + enabled: z.boolean().default(false), + port: z.number().int().min(1024).max(65535).default(6901), + }).default({ enabled: false, port: 6901 }), }) const compactionSchema = z.object({ diff --git a/src/main.ts b/src/main.ts index 5c46b618..b914524b 100644 --- a/src/main.ts +++ b/src/main.ts @@ -24,8 +24,14 @@ import type { BrainExportState } from './extension/brain/index.js' import { createBrowserTools } from './extension/browser/index.js' import { SymbolIndex } from './openbb/equity/index.js' import { createEquityTools } from './extension/equity/index.js' -import { getSDKExecutor, buildRouteMap, SDKEquityClient, SDKCryptoClient, SDKCurrencyClient, SDKNewsClient, SDKEconomyClient, SDKCommodityClient } from './openbb/sdk/index.js' +import { getSDKExecutor, buildRouteMap, SDKEquityClient, SDKCryptoClient, SDKCurrencyClient, SDKNewsClient } from './openbb/sdk/index.js' +import type { EquityClientLike, CryptoClientLike, CurrencyClientLike, NewsClientLike } from './openbb/sdk/types.js' import { buildSDKCredentials } from './openbb/credential-map.js' +import { OpenBBEquityClient } from './openbb/equity/client.js' +import { OpenBBCryptoClient } from './openbb/crypto/client.js' +import { OpenBBCurrencyClient } from './openbb/currency/client.js' +import { OpenBBNewsClient } from './openbb/news/client.js' +import { startEmbeddedOpenBBServer } from './openbb/api-server.js' import { createMarketSearchTools } from './extension/market/index.js' import { createNewsTools } from './extension/news/index.js' import { createAnalysisTools } from './extension/analysis-kit/index.js' @@ -215,18 +221,35 @@ async function main() { }) await newsStore.init() - // ==================== OpenBB SDK Clients ==================== + // ==================== OpenBB Clients ==================== const { providers } = config.openbb - const executor = getSDKExecutor() - const routeMap = buildRouteMap() - const credentials = buildSDKCredentials(config.openbb.providerKeys) - const equityClient = new SDKEquityClient(executor, 'equity', providers.equity, credentials, routeMap) - const cryptoClient = new SDKCryptoClient(executor, 'crypto', providers.crypto, credentials, routeMap) - const currencyClient = new SDKCurrencyClient(executor, 'currency', providers.currency, credentials, routeMap) - const commodityClient = new SDKCommodityClient(executor, 'commodity', undefined, credentials, routeMap) - const economyClient = new SDKEconomyClient(executor, 'economy', undefined, credentials, routeMap) - const newsClient = new SDKNewsClient(executor, 'news', undefined, credentials, routeMap) + + let equityClient: EquityClientLike + let cryptoClient: CryptoClientLike + let currencyClient: CurrencyClientLike + let newsClient: NewsClientLike + + if (config.openbb.dataBackend === 'openbb') { + const url = config.openbb.apiUrl + const keys = config.openbb.providerKeys + equityClient = new OpenBBEquityClient(url, providers.equity, keys) + cryptoClient = new OpenBBCryptoClient(url, providers.crypto, keys) + currencyClient = new OpenBBCurrencyClient(url, providers.currency, keys) + newsClient = new OpenBBNewsClient(url, undefined, keys) + } else { + const executor = getSDKExecutor() + const routeMap = buildRouteMap() + const credentials = buildSDKCredentials(config.openbb.providerKeys) + equityClient = new SDKEquityClient(executor, 'equity', providers.equity, credentials, routeMap) + cryptoClient = new SDKCryptoClient(executor, 'crypto', providers.crypto, credentials, routeMap) + currencyClient = new SDKCurrencyClient(executor, 'currency', providers.currency, credentials, routeMap) + newsClient = new SDKNewsClient(executor, 'news', undefined, credentials, routeMap) + } + + if (config.openbb.apiServer.enabled) { + startEmbeddedOpenBBServer(config.openbb.apiServer.port) + } // ==================== Equity Symbol Index ==================== diff --git a/src/openbb/api-server.ts b/src/openbb/api-server.ts new file mode 100644 index 00000000..c05904b8 --- /dev/null +++ b/src/openbb/api-server.ts @@ -0,0 +1,26 @@ +/** + * Embedded OpenBB API Server + * + * Starts an OpenBB-compatible HTTP server using opentypebb in-process. + * Exposes the same REST endpoints as the Python OpenBB sidecar, allowing + * external tools to connect to Alice's built-in data engine. + */ + +import { Hono } from 'hono' +import { cors } from 'hono/cors' +import { serve } from '@hono/node-server' +import { createExecutor, loadAllRouters } from 'opentypebb' + +export function startEmbeddedOpenBBServer(port: number): void { + const executor = createExecutor() + + const app = new Hono() + app.use(cors()) + app.get('/api/v1/health', (c) => c.json({ status: 'ok' })) + + const rootRouter = loadAllRouters() + rootRouter.mountToHono(app, executor) + + serve({ fetch: app.fetch, port }) + console.log(`[openbb] Embedded API server listening on http://localhost:${port}`) +} diff --git a/ui/src/pages/DataSourcesPage.tsx b/ui/src/pages/DataSourcesPage.tsx index 79be3d61..781220b4 100644 --- a/ui/src/pages/DataSourcesPage.tsx +++ b/ui/src/pages/DataSourcesPage.tsx @@ -273,7 +273,9 @@ function MarketDataZone({ openbb, enabled, onToggle, onChange, onChangeImmediate const [testing, setTesting] = useState(false) const [testStatus, setTestStatus] = useState<'idle' | 'ok' | 'error'>('idle') + const dataBackend = (openbb.dataBackend as string) || 'sdk' const apiUrl = (openbb.apiUrl as string) || 'http://localhost:6900' + const apiServer = (openbb.apiServer as { enabled: boolean; port: number } | undefined) ?? { enabled: false, port: 6901 } const providers = (openbb.providers ?? { equity: 'yfinance', crypto: 'yfinance', currency: 'yfinance', newsCompany: 'yfinance', newsWorld: 'fmp', }) as Record @@ -313,35 +315,64 @@ function MarketDataZone({ openbb, enabled, onToggle, onChange, onChangeImmediate enabled={enabled} onToggle={onToggle} > - {/* Connection */} + {/* Backend selector */}
-

Connection

-
- { onChange({ apiUrl: e.target.value }); setTestStatus('idle') }} - placeholder="http://localhost:6900" - /> - - {testStatus !== 'idle' && ( -
- )} +

Data Backend

+
+ {(['sdk', 'openbb'] as const).map((backend, i) => ( + + ))}
+

+ {dataBackend === 'sdk' + ? 'Uses the built-in TypeScript OpenBB engine. No external process required.' + : 'Connects to an external OpenBB HTTP server (Python sidecar or custom).'} +

+ {/* Connection — only shown for external OpenBB backend */} + {dataBackend === 'openbb' && ( +
+

Connection

+
+ { onChange({ apiUrl: e.target.value }); setTestStatus('idle') }} + placeholder="http://localhost:6900" + /> + + {testStatus !== 'idle' && ( +
+ )} +
+
+ )} + {/* Asset providers + inline keys */} + {/* Embedded API server */} +
+

Embedded API Server

+
+
+

Expose OpenBB HTTP API

+

+ Start an OpenBB-compatible HTTP server at Alice startup. Other services can connect to{' '} + http://localhost:{apiServer.port}. +

+ {apiServer.enabled && ( +
+ + onChange({ apiServer: { ...apiServer, port: Number(e.target.value) || 6901 } })} + /> +
+ )} +
+ onChangeImmediate({ apiServer: { ...apiServer, enabled: v } })} + /> +
+
+ {/* Utility/macro providers */} Date: Wed, 11 Mar 2026 17:47:00 +0800 Subject: [PATCH 06/23] docs: update README to reflect TypeScript-native OpenBB engine - Market data feature now describes opentypebb as no-sidecar-required - openbb.json config row updated with dataBackend and apiServer fields - Project structure expanded with sdk/, api-server.ts, and credential-map.ts Co-Authored-By: Claude Sonnet 4.6 --- README.md | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 6ba31613..8e4ac7ff 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ Your one-person Wall Street. Alice is an AI trading agent that gives you your ow - **Dual AI provider** — switch between Claude Code CLI and Vercel AI SDK at runtime, no restart needed - **Unified trading** — multi-account architecture supporting CCXT (Bybit, OKX, Binance, etc.) and Alpaca (US equities) with a git-like workflow (stage, commit, push) - **Guard pipeline** — extensible pre-execution safety checks (max position size, cooldown between trades, symbol whitelist) -- **Market data** — OpenBB-powered equity, crypto, commodity, and currency data layers with unified symbol search (`marketSearchForResearch`) and technical indicator calculator +- **Market data** — TypeScript-native OpenBB engine (`opentypebb`) with no external sidecar required. Covers equity, crypto, commodity, currency, and macro data with unified symbol search (`marketSearchForResearch`) and technical indicator calculator. Can also expose an embedded OpenBB-compatible HTTP API for external tools - **Equity research** — company profiles, financial statements, ratios, analyst estimates, earnings calendar, insider trading, and market movers (top gainers, losers, most active) - **News collector** — background RSS collection from configurable feeds with archive search tools (`globNews`/`grepNews`/`readNews`). Also captures OpenBB news API results via piggyback - **Cognitive state** — persistent "brain" with frontal lobe memory, emotion tracking, and commit history @@ -166,7 +166,7 @@ All config lives in `data/config/` as JSON files with Zod validation. Missing fi | `crypto.json` | CCXT exchange config + API keys, allowed symbols, guards | | `securities.json` | Alpaca broker config + API keys, allowed symbols, guards | | `connectors.json` | Web/MCP server ports, Telegram bot credentials + enable, MCP Ask enable | -| `openbb.json` | OpenBB API URL, per-asset-class data providers, provider API keys | +| `openbb.json` | Data backend (`sdk` / `openbb`), per-asset-class providers, provider API keys, embedded HTTP server config | | `news-collector.json` | RSS feeds, fetch interval, retention period, OpenBB piggyback toggle | | `compaction.json` | Context window limits, auto-compaction thresholds | | `heartbeat.json` | Heartbeat enable/disable, interval, active hours | @@ -212,12 +212,15 @@ src/ brain/ # Cognitive state (memory, emotion) browser/ # Browser automation bridge (via OpenClaw) openbb/ - equity/ # OpenBB equity data layer (price, fundamentals, estimates, etc.) - crypto/ # OpenBB crypto data layer - currency/ # OpenBB currency data layer - commodity/ # OpenBB commodity data layer (EIA, spot prices) - economy/ # OpenBB macro economy data layer - news/ # OpenBB news data layer + sdk/ # In-process opentypebb SDK clients (equity, crypto, currency, news, economy, commodity) + api-server.ts # Embedded OpenBB-compatible HTTP server (optional, port 6901) + equity/ # Equity data layer + SymbolIndex (SEC/TMX local cache) + crypto/ # Crypto data layer + currency/ # Currency/forex data layer + commodity/ # Commodity data layer (EIA, spot prices) + economy/ # Macro economy data layer + news/ # News data layer + credential-map.ts # Maps config key names to OpenBB credential field names connectors/ web/ # Web UI chat (Hono, SSE push) telegram/ # Telegram bot (grammY, polling, commands) From a95d10aa86a72edb5594b606bcc028b4375fdbf1 Mon Sep 17 00:00:00 2001 From: Ame Date: Wed, 11 Mar 2026 19:05:27 +0800 Subject: [PATCH 07/23] feat(opentypebb): add ~40 economy/commodity routes with FRED, OECD, BLS, EIA providers Implements Phase 12 of the OpenBB migration: populate all missing economy and commodity endpoints so the SDK's 42 economy methods and 6 commodity methods resolve to real fetchers instead of throwing "No SDK route". New providers: BLS (labor statistics), EIA (energy data), Stub (shipping placeholders). New sub-routers: survey (9 routes), GDP (3), shipping (4 stubs), commodity/price (1). FRED helpers extracted into shared utility for 18 Federal Reserve fetchers. OECD helpers extracted for 6 new international data fetchers. Total route count: 143 (up from ~103). Co-Authored-By: Claude Opus 4.6 --- .../opentypebb/src/core/api/app-loader.ts | 8 + .../extensions/commodity/commodity-router.ts | 35 +++ .../commodity/price/price-router.ts | 20 ++ .../src/extensions/economy/economy-router.ts | 151 +++++++++++ .../src/extensions/economy/gdp/gdp-router.ts | 38 +++ .../economy/shipping/shipping-router.ts | 50 ++++ .../economy/survey/survey-router.ts | 92 +++++++ .../opentypebb/src/providers/bls/index.ts | 18 ++ .../src/providers/bls/models/bls-search.ts | 66 +++++ .../src/providers/bls/models/bls-series.ts | 90 +++++++ .../opentypebb/src/providers/eia/index.ts | 23 ++ .../eia/models/petroleum-status-report.ts | 88 +++++++ .../eia/models/short-term-energy-outlook.ts | 92 +++++++ .../src/providers/federal_reserve/index.ts | 36 +++ .../models/economic-conditions-chicago.ts | 49 ++++ .../federal_reserve/models/fomc-documents.ts | 68 +++++ .../federal_reserve/models/fred-regional.ts | 42 +++ .../models/fred-release-table.ts | 40 +++ .../federal_reserve/models/fred-search.ts | 45 ++++ .../federal_reserve/models/fred-series.ts | 47 ++++ .../models/inflation-expectations.ts | 51 ++++ .../models/manufacturing-outlook-ny.ts | 44 ++++ .../models/manufacturing-outlook-texas.ts | 46 ++++ .../federal_reserve/models/money-measures.ts | 52 ++++ .../models/nonfarm-payrolls.ts | 50 ++++ .../providers/federal_reserve/models/pce.ts | 46 ++++ .../models/primary-dealer-fails.ts | 50 ++++ .../models/primary-dealer-positioning.ts | 52 ++++ .../providers/federal_reserve/models/sloos.ts | 49 ++++ .../models/total-factor-productivity.ts | 44 ++++ .../federal_reserve/models/unemployment.ts | 52 ++++ .../models/university-of-michigan.ts | 52 ++++ .../federal_reserve/utils/fred-helpers.ts | 221 ++++++++++++++++ .../opentypebb/src/providers/oecd/index.ts | 12 + .../src/providers/oecd/models/gdp-forecast.ts | 56 ++++ .../src/providers/oecd/models/gdp-nominal.ts | 55 ++++ .../src/providers/oecd/models/gdp-real.ts | 55 ++++ .../oecd/models/house-price-index.ts | 55 ++++ .../providers/oecd/models/retail-prices.ts | 56 ++++ .../oecd/models/share-price-index.ts | 56 ++++ .../src/providers/oecd/utils/oecd-helpers.ts | 103 ++++++++ .../opentypebb/src/providers/stub/index.ts | 27 ++ .../providers/stub/models/shipping-stubs.ts | 87 ++++++ .../src/providers/yfinance/index.ts | 2 + .../yfinance/models/commodity-spot-price.ts | 99 +++++++ .../src/standard-models/bls-search.ts | 20 ++ .../src/standard-models/bls-series.ts | 22 ++ .../src/standard-models/chokepoint-info.ts | 21 ++ .../src/standard-models/chokepoint-volume.ts | 22 ++ .../standard-models/commodity-spot-price.ts | 25 ++ .../economic-conditions-chicago.ts | 21 ++ .../src/standard-models/fomc-documents.ts | 22 ++ .../src/standard-models/fred-regional.ts | 25 ++ .../src/standard-models/fred-release-table.ts | 23 ++ .../src/standard-models/fred-search.ts | 25 ++ .../src/standard-models/fred-series.ts | 22 ++ .../src/standard-models/gdp-forecast.ts | 22 ++ .../src/standard-models/gdp-nominal.ts | 22 ++ .../src/standard-models/gdp-real.ts | 22 ++ .../src/standard-models/house-price-index.ts | 22 ++ .../opentypebb/src/standard-models/index.ts | 247 ++++++++++++++++++ .../standard-models/inflation-expectations.ts | 23 ++ .../manufacturing-outlook-ny.ts | 22 ++ .../manufacturing-outlook-texas.ts | 22 ++ .../src/standard-models/money-measures.ts | 22 ++ .../src/standard-models/nonfarm-payrolls.ts | 22 ++ .../opentypebb/src/standard-models/pce.ts | 21 ++ .../petroleum-status-report.ts | 29 ++ .../src/standard-models/port-info.ts | 21 ++ .../src/standard-models/port-volume.ts | 21 ++ .../standard-models/primary-dealer-fails.ts | 19 ++ .../primary-dealer-positioning.ts | 19 ++ .../src/standard-models/retail-prices.ts | 22 ++ .../src/standard-models/share-price-index.ts | 22 ++ .../short-term-energy-outlook.ts | 30 +++ .../opentypebb/src/standard-models/sloos.ts | 21 ++ .../total-factor-productivity.ts | 20 ++ .../src/standard-models/unemployment.ts | 23 ++ .../standard-models/university-of-michigan.ts | 24 ++ 79 files changed, 3556 insertions(+) create mode 100644 packages/opentypebb/src/extensions/commodity/commodity-router.ts create mode 100644 packages/opentypebb/src/extensions/commodity/price/price-router.ts create mode 100644 packages/opentypebb/src/extensions/economy/gdp/gdp-router.ts create mode 100644 packages/opentypebb/src/extensions/economy/shipping/shipping-router.ts create mode 100644 packages/opentypebb/src/extensions/economy/survey/survey-router.ts create mode 100644 packages/opentypebb/src/providers/bls/index.ts create mode 100644 packages/opentypebb/src/providers/bls/models/bls-search.ts create mode 100644 packages/opentypebb/src/providers/bls/models/bls-series.ts create mode 100644 packages/opentypebb/src/providers/eia/index.ts create mode 100644 packages/opentypebb/src/providers/eia/models/petroleum-status-report.ts create mode 100644 packages/opentypebb/src/providers/eia/models/short-term-energy-outlook.ts create mode 100644 packages/opentypebb/src/providers/federal_reserve/models/economic-conditions-chicago.ts create mode 100644 packages/opentypebb/src/providers/federal_reserve/models/fomc-documents.ts create mode 100644 packages/opentypebb/src/providers/federal_reserve/models/fred-regional.ts create mode 100644 packages/opentypebb/src/providers/federal_reserve/models/fred-release-table.ts create mode 100644 packages/opentypebb/src/providers/federal_reserve/models/fred-search.ts create mode 100644 packages/opentypebb/src/providers/federal_reserve/models/fred-series.ts create mode 100644 packages/opentypebb/src/providers/federal_reserve/models/inflation-expectations.ts create mode 100644 packages/opentypebb/src/providers/federal_reserve/models/manufacturing-outlook-ny.ts create mode 100644 packages/opentypebb/src/providers/federal_reserve/models/manufacturing-outlook-texas.ts create mode 100644 packages/opentypebb/src/providers/federal_reserve/models/money-measures.ts create mode 100644 packages/opentypebb/src/providers/federal_reserve/models/nonfarm-payrolls.ts create mode 100644 packages/opentypebb/src/providers/federal_reserve/models/pce.ts create mode 100644 packages/opentypebb/src/providers/federal_reserve/models/primary-dealer-fails.ts create mode 100644 packages/opentypebb/src/providers/federal_reserve/models/primary-dealer-positioning.ts create mode 100644 packages/opentypebb/src/providers/federal_reserve/models/sloos.ts create mode 100644 packages/opentypebb/src/providers/federal_reserve/models/total-factor-productivity.ts create mode 100644 packages/opentypebb/src/providers/federal_reserve/models/unemployment.ts create mode 100644 packages/opentypebb/src/providers/federal_reserve/models/university-of-michigan.ts create mode 100644 packages/opentypebb/src/providers/federal_reserve/utils/fred-helpers.ts create mode 100644 packages/opentypebb/src/providers/oecd/models/gdp-forecast.ts create mode 100644 packages/opentypebb/src/providers/oecd/models/gdp-nominal.ts create mode 100644 packages/opentypebb/src/providers/oecd/models/gdp-real.ts create mode 100644 packages/opentypebb/src/providers/oecd/models/house-price-index.ts create mode 100644 packages/opentypebb/src/providers/oecd/models/retail-prices.ts create mode 100644 packages/opentypebb/src/providers/oecd/models/share-price-index.ts create mode 100644 packages/opentypebb/src/providers/oecd/utils/oecd-helpers.ts create mode 100644 packages/opentypebb/src/providers/stub/index.ts create mode 100644 packages/opentypebb/src/providers/stub/models/shipping-stubs.ts create mode 100644 packages/opentypebb/src/providers/yfinance/models/commodity-spot-price.ts create mode 100644 packages/opentypebb/src/standard-models/bls-search.ts create mode 100644 packages/opentypebb/src/standard-models/bls-series.ts create mode 100644 packages/opentypebb/src/standard-models/chokepoint-info.ts create mode 100644 packages/opentypebb/src/standard-models/chokepoint-volume.ts create mode 100644 packages/opentypebb/src/standard-models/commodity-spot-price.ts create mode 100644 packages/opentypebb/src/standard-models/economic-conditions-chicago.ts create mode 100644 packages/opentypebb/src/standard-models/fomc-documents.ts create mode 100644 packages/opentypebb/src/standard-models/fred-regional.ts create mode 100644 packages/opentypebb/src/standard-models/fred-release-table.ts create mode 100644 packages/opentypebb/src/standard-models/fred-search.ts create mode 100644 packages/opentypebb/src/standard-models/fred-series.ts create mode 100644 packages/opentypebb/src/standard-models/gdp-forecast.ts create mode 100644 packages/opentypebb/src/standard-models/gdp-nominal.ts create mode 100644 packages/opentypebb/src/standard-models/gdp-real.ts create mode 100644 packages/opentypebb/src/standard-models/house-price-index.ts create mode 100644 packages/opentypebb/src/standard-models/inflation-expectations.ts create mode 100644 packages/opentypebb/src/standard-models/manufacturing-outlook-ny.ts create mode 100644 packages/opentypebb/src/standard-models/manufacturing-outlook-texas.ts create mode 100644 packages/opentypebb/src/standard-models/money-measures.ts create mode 100644 packages/opentypebb/src/standard-models/nonfarm-payrolls.ts create mode 100644 packages/opentypebb/src/standard-models/pce.ts create mode 100644 packages/opentypebb/src/standard-models/petroleum-status-report.ts create mode 100644 packages/opentypebb/src/standard-models/port-info.ts create mode 100644 packages/opentypebb/src/standard-models/port-volume.ts create mode 100644 packages/opentypebb/src/standard-models/primary-dealer-fails.ts create mode 100644 packages/opentypebb/src/standard-models/primary-dealer-positioning.ts create mode 100644 packages/opentypebb/src/standard-models/retail-prices.ts create mode 100644 packages/opentypebb/src/standard-models/share-price-index.ts create mode 100644 packages/opentypebb/src/standard-models/short-term-energy-outlook.ts create mode 100644 packages/opentypebb/src/standard-models/sloos.ts create mode 100644 packages/opentypebb/src/standard-models/total-factor-productivity.ts create mode 100644 packages/opentypebb/src/standard-models/unemployment.ts create mode 100644 packages/opentypebb/src/standard-models/university-of-michigan.ts diff --git a/packages/opentypebb/src/core/api/app-loader.ts b/packages/opentypebb/src/core/api/app-loader.ts index ca0eed96..6bcd3b4e 100644 --- a/packages/opentypebb/src/core/api/app-loader.ts +++ b/packages/opentypebb/src/core/api/app-loader.ts @@ -23,6 +23,9 @@ import { imfProvider } from '../../providers/imf/index.js' import { ecbProvider } from '../../providers/ecb/index.js' import { federalReserveProvider } from '../../providers/federal_reserve/index.js' import { intrinioProvider } from '../../providers/intrinio/index.js' +import { blsProvider } from '../../providers/bls/index.js' +import { eiaProvider } from '../../providers/eia/index.js' +import { stubProvider } from '../../providers/stub/index.js' // --- Extension routers --- import { equityRouter } from '../../extensions/equity/equity-router.js' @@ -33,6 +36,7 @@ import { economyRouter } from '../../extensions/economy/economy-router.js' import { etfRouter } from '../../extensions/etf/etf-router.js' import { indexRouter } from '../../extensions/index/index-router.js' import { derivativesRouter } from '../../extensions/derivatives/derivatives-router.js' +import { commodityRouter } from '../../extensions/commodity/commodity-router.js' /** * Create and populate a Registry with all available providers. @@ -51,6 +55,9 @@ export function createRegistry(): Registry { registry.includeProvider(ecbProvider) registry.includeProvider(federalReserveProvider) registry.includeProvider(intrinioProvider) + registry.includeProvider(blsProvider) + registry.includeProvider(eiaProvider) + registry.includeProvider(stubProvider) return registry } @@ -76,5 +83,6 @@ export function loadAllRouters(): Router { root.includeRouter(etfRouter) root.includeRouter(indexRouter) root.includeRouter(derivativesRouter) + root.includeRouter(commodityRouter) return root } diff --git a/packages/opentypebb/src/extensions/commodity/commodity-router.ts b/packages/opentypebb/src/extensions/commodity/commodity-router.ts new file mode 100644 index 00000000..055bc687 --- /dev/null +++ b/packages/opentypebb/src/extensions/commodity/commodity-router.ts @@ -0,0 +1,35 @@ +/** + * Commodity Router. + * Maps to: openbb_commodity/commodity_router.py + */ + +import { Router } from '../../core/app/router.js' +import { commodityPriceRouter } from './price/price-router.js' + +export const commodityRouter = new Router({ + prefix: '/commodity', + description: 'Commodity market data.', +}) + +// --- Include sub-routers --- +commodityRouter.includeRouter(commodityPriceRouter) + +// --- Root-level commands --- + +commodityRouter.command({ + model: 'PetroleumStatusReport', + path: '/petroleum_status_report', + description: 'Get EIA Weekly Petroleum Status Report data.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'PetroleumStatusReport', params, credentials) + }, +}) + +commodityRouter.command({ + model: 'ShortTermEnergyOutlook', + path: '/short_term_energy_outlook', + description: 'Get EIA Short-Term Energy Outlook (STEO) data.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'ShortTermEnergyOutlook', params, credentials) + }, +}) diff --git a/packages/opentypebb/src/extensions/commodity/price/price-router.ts b/packages/opentypebb/src/extensions/commodity/price/price-router.ts new file mode 100644 index 00000000..b212a748 --- /dev/null +++ b/packages/opentypebb/src/extensions/commodity/price/price-router.ts @@ -0,0 +1,20 @@ +/** + * Commodity Price Sub-Router. + * Maps to: openbb_commodity/price/ + */ + +import { Router } from '../../../core/app/router.js' + +export const commodityPriceRouter = new Router({ + prefix: '/price', + description: 'Commodity price data.', +}) + +commodityPriceRouter.command({ + model: 'CommoditySpotPrice', + path: '/spot', + description: 'Get historical spot/futures prices for commodities.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'CommoditySpotPrice', params, credentials) + }, +}) diff --git a/packages/opentypebb/src/extensions/economy/economy-router.ts b/packages/opentypebb/src/extensions/economy/economy-router.ts index 4da6859b..6a121dc8 100644 --- a/packages/opentypebb/src/extensions/economy/economy-router.ts +++ b/packages/opentypebb/src/extensions/economy/economy-router.ts @@ -4,12 +4,22 @@ */ import { Router } from '../../core/app/router.js' +import { surveyRouter } from './survey/survey-router.js' +import { gdpRouter } from './gdp/gdp-router.js' +import { shippingRouter } from './shipping/shipping-router.js' export const economyRouter = new Router({ prefix: '/economy', description: 'Economic data.', }) +// --- Include sub-routers --- +economyRouter.includeRouter(surveyRouter) +economyRouter.includeRouter(gdpRouter) +economyRouter.includeRouter(shippingRouter) + +// --- Root-level commands --- + economyRouter.command({ model: 'EconomicCalendar', path: '/calendar', @@ -126,3 +136,144 @@ economyRouter.command({ return executor.execute(provider, 'EconomicIndicators', params, credentials) }, }) + +economyRouter.command({ + model: 'RiskPremium', + path: '/risk_premium', + description: 'Get market risk premium by country.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'RiskPremium', params, credentials) + }, +}) + +// --- FRED endpoints --- + +economyRouter.command({ + model: 'FredSearch', + path: '/fred_search', + description: 'Search FRED economic data series.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'FredSearch', params, credentials) + }, +}) + +economyRouter.command({ + model: 'FredSeries', + path: '/fred_series', + description: 'Get FRED series observations.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'FredSeries', params, credentials) + }, +}) + +economyRouter.command({ + model: 'FredReleaseTable', + path: '/fred_release_table', + description: 'Get FRED release table data.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'FredReleaseTable', params, credentials) + }, +}) + +economyRouter.command({ + model: 'FredRegional', + path: '/fred_regional', + description: 'Get FRED regional (GeoFRED) data.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'FredRegional', params, credentials) + }, +}) + +// --- Macro indicators --- + +economyRouter.command({ + model: 'Unemployment', + path: '/unemployment', + description: 'Get unemployment rate data.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'Unemployment', params, credentials) + }, +}) + +economyRouter.command({ + model: 'MoneyMeasures', + path: '/money_measures', + description: 'Get money supply measures (M1, M2).', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'MoneyMeasures', params, credentials) + }, +}) + +economyRouter.command({ + model: 'PersonalConsumptionExpenditures', + path: '/pce', + description: 'Get Personal Consumption Expenditures (PCE) price index.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'PersonalConsumptionExpenditures', params, credentials) + }, +}) + +economyRouter.command({ + model: 'TotalFactorProductivity', + path: '/total_factor_productivity', + description: 'Get total factor productivity data.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'TotalFactorProductivity', params, credentials) + }, +}) + +economyRouter.command({ + model: 'FomcDocuments', + path: '/fomc_documents', + description: 'Get FOMC meeting documents and rate decisions.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'FomcDocuments', params, credentials) + }, +}) + +economyRouter.command({ + model: 'PrimaryDealerPositioning', + path: '/primary_dealer_positioning', + description: 'Get primary dealer positioning data.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'PrimaryDealerPositioning', params, credentials) + }, +}) + +economyRouter.command({ + model: 'PrimaryDealerFails', + path: '/primary_dealer_fails', + description: 'Get primary dealer fails-to-deliver data.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'PrimaryDealerFails', params, credentials) + }, +}) + +// --- OECD endpoints --- + +economyRouter.command({ + model: 'SharePriceIndex', + path: '/share_price_index', + description: 'Get share price index data from OECD.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'SharePriceIndex', params, credentials) + }, +}) + +economyRouter.command({ + model: 'HousePriceIndex', + path: '/house_price_index', + description: 'Get house price index data from OECD.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'HousePriceIndex', params, credentials) + }, +}) + +economyRouter.command({ + model: 'RetailPrices', + path: '/retail_prices', + description: 'Get retail price data from OECD.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'RetailPrices', params, credentials) + }, +}) diff --git a/packages/opentypebb/src/extensions/economy/gdp/gdp-router.ts b/packages/opentypebb/src/extensions/economy/gdp/gdp-router.ts new file mode 100644 index 00000000..5cdd4434 --- /dev/null +++ b/packages/opentypebb/src/extensions/economy/gdp/gdp-router.ts @@ -0,0 +1,38 @@ +/** + * Economy GDP Sub-Router. + * Maps to: openbb_economy/gdp/gdp_router.py + */ + +import { Router } from '../../../core/app/router.js' + +export const gdpRouter = new Router({ + prefix: '/gdp', + description: 'GDP data.', +}) + +gdpRouter.command({ + model: 'GdpForecast', + path: '/forecast', + description: 'Get GDP forecast data.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'GdpForecast', params, credentials) + }, +}) + +gdpRouter.command({ + model: 'GdpNominal', + path: '/nominal', + description: 'Get nominal GDP data.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'GdpNominal', params, credentials) + }, +}) + +gdpRouter.command({ + model: 'GdpReal', + path: '/real', + description: 'Get real GDP data.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'GdpReal', params, credentials) + }, +}) diff --git a/packages/opentypebb/src/extensions/economy/shipping/shipping-router.ts b/packages/opentypebb/src/extensions/economy/shipping/shipping-router.ts new file mode 100644 index 00000000..08ec3f2b --- /dev/null +++ b/packages/opentypebb/src/extensions/economy/shipping/shipping-router.ts @@ -0,0 +1,50 @@ +/** + * Shipping Sub-Router. + * Maps to: openbb_economy/shipping/ + * + * Note: These endpoints are stubs — they register the routes but always + * throw EmptyDataError until a reliable public data source is integrated. + */ + +import { Router } from '../../../core/app/router.js' + +export const shippingRouter = new Router({ + prefix: '/shipping', + description: 'Global shipping and trade route data.', +}) + +shippingRouter.command({ + model: 'PortInfo', + path: '/port_info', + description: 'Get information about a port.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'PortInfo', params, credentials) + }, +}) + +shippingRouter.command({ + model: 'PortVolume', + path: '/port_volume', + description: 'Get shipping volume data for a port.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'PortVolume', params, credentials) + }, +}) + +shippingRouter.command({ + model: 'ChokepointInfo', + path: '/chokepoint_info', + description: 'Get information about a maritime chokepoint.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'ChokepointInfo', params, credentials) + }, +}) + +shippingRouter.command({ + model: 'ChokepointVolume', + path: '/chokepoint_volume', + description: 'Get transit volume data for a maritime chokepoint.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'ChokepointVolume', params, credentials) + }, +}) diff --git a/packages/opentypebb/src/extensions/economy/survey/survey-router.ts b/packages/opentypebb/src/extensions/economy/survey/survey-router.ts new file mode 100644 index 00000000..97a9fd4f --- /dev/null +++ b/packages/opentypebb/src/extensions/economy/survey/survey-router.ts @@ -0,0 +1,92 @@ +/** + * Economy Survey Sub-Router. + * Maps to: openbb_economy/survey/survey_router.py + */ + +import { Router } from '../../../core/app/router.js' + +export const surveyRouter = new Router({ + prefix: '/survey', + description: 'Economic survey data.', +}) + +surveyRouter.command({ + model: 'NonfarmPayrolls', + path: '/nonfarm_payrolls', + description: 'Get nonfarm payrolls data.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'NonfarmPayrolls', params, credentials) + }, +}) + +surveyRouter.command({ + model: 'InflationExpectations', + path: '/inflation_expectations', + description: 'Get inflation expectations data.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'InflationExpectations', params, credentials) + }, +}) + +surveyRouter.command({ + model: 'Sloos', + path: '/sloos', + description: 'Get Senior Loan Officer Opinion Survey (SLOOS) data.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'Sloos', params, credentials) + }, +}) + +surveyRouter.command({ + model: 'UniversityOfMichigan', + path: '/university_of_michigan', + description: 'Get University of Michigan Consumer Sentiment data.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'UniversityOfMichigan', params, credentials) + }, +}) + +surveyRouter.command({ + model: 'EconomicConditionsChicago', + path: '/economic_conditions_chicago', + description: 'Get Chicago Fed National Activity Index data.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'EconomicConditionsChicago', params, credentials) + }, +}) + +surveyRouter.command({ + model: 'ManufacturingOutlookTexas', + path: '/manufacturing_outlook_texas', + description: 'Get Dallas Fed Manufacturing Outlook Survey data.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'ManufacturingOutlookTexas', params, credentials) + }, +}) + +surveyRouter.command({ + model: 'ManufacturingOutlookNY', + path: '/manufacturing_outlook_ny', + description: 'Get NY Fed Empire State Manufacturing Survey data.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'ManufacturingOutlookNY', params, credentials) + }, +}) + +surveyRouter.command({ + model: 'BlsSeries', + path: '/bls_series', + description: 'Get BLS time series data.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'BlsSeries', params, credentials) + }, +}) + +surveyRouter.command({ + model: 'BlsSearch', + path: '/bls_search', + description: 'Search BLS data series.', + handler: async (executor, provider, params, credentials) => { + return executor.execute(provider, 'BlsSearch', params, credentials) + }, +}) diff --git a/packages/opentypebb/src/providers/bls/index.ts b/packages/opentypebb/src/providers/bls/index.ts new file mode 100644 index 00000000..47477994 --- /dev/null +++ b/packages/opentypebb/src/providers/bls/index.ts @@ -0,0 +1,18 @@ +/** + * BLS (Bureau of Labor Statistics) Provider Module. + */ + +import { Provider } from '../../core/provider/abstract/provider.js' +import { BLSBlsSeriesFetcher } from './models/bls-series.js' +import { BLSBlsSearchFetcher } from './models/bls-search.js' + +export const blsProvider = new Provider({ + name: 'bls', + website: 'https://www.bls.gov', + description: 'Bureau of Labor Statistics — US labor market and price data.', + credentials: ['api_key'], + fetcherDict: { + BlsSeries: BLSBlsSeriesFetcher, + BlsSearch: BLSBlsSearchFetcher, + }, +}) diff --git a/packages/opentypebb/src/providers/bls/models/bls-search.ts b/packages/opentypebb/src/providers/bls/models/bls-search.ts new file mode 100644 index 00000000..99d19fc3 --- /dev/null +++ b/packages/opentypebb/src/providers/bls/models/bls-search.ts @@ -0,0 +1,66 @@ +/** + * BLS Search Fetcher. + * BLS doesn't have a search API, so we provide a curated list of common series. + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { BlsSearchQueryParamsSchema, BlsSearchDataSchema } from '../../../standard-models/bls-search.js' +import { EmptyDataError } from '../../../core/provider/utils/errors.js' + +export const BLSBlsSearchQueryParamsSchema = BlsSearchQueryParamsSchema +export type BLSBlsSearchQueryParams = z.infer + +// Curated list of commonly used BLS series +const COMMON_SERIES = [ + { series_id: 'CUUR0000SA0', title: 'CPI-U All Items (Urban Consumers)', survey_abbreviation: 'CU' }, + { series_id: 'CUUR0000SA0L1E', title: 'CPI-U Core (Less Food and Energy)', survey_abbreviation: 'CU' }, + { series_id: 'LNS14000000', title: 'Unemployment Rate (Seasonally Adjusted)', survey_abbreviation: 'LN' }, + { series_id: 'CES0000000001', title: 'Total Nonfarm Payrolls', survey_abbreviation: 'CE' }, + { series_id: 'CES0500000003', title: 'Average Hourly Earnings (Private)', survey_abbreviation: 'CE' }, + { series_id: 'LNS11300000', title: 'Labor Force Participation Rate', survey_abbreviation: 'LN' }, + { series_id: 'CUSR0000SAF11', title: 'CPI Food at Home', survey_abbreviation: 'CU' }, + { series_id: 'CUUR0000SETB01', title: 'CPI Gasoline', survey_abbreviation: 'CU' }, + { series_id: 'CUUR0000SETA01', title: 'CPI New Vehicles', survey_abbreviation: 'CU' }, + { series_id: 'CUUR0000SEHA', title: 'CPI Rent of Primary Residence', survey_abbreviation: 'CU' }, + { series_id: 'JTS000000000000000JOR', title: 'JOLTS Job Openings Rate', survey_abbreviation: 'JT' }, + { series_id: 'JTS000000000000000QUR', title: 'JOLTS Quits Rate', survey_abbreviation: 'JT' }, + { series_id: 'WPUFD49104', title: 'PPI Final Demand', survey_abbreviation: 'WP' }, + { series_id: 'WPUFD49116', title: 'PPI Final Demand Less Food Energy Trade', survey_abbreviation: 'WP' }, + { series_id: 'CES0500000008', title: 'Average Weekly Hours (Private)', survey_abbreviation: 'CE' }, + { series_id: 'LNS12032194', title: 'Employment-Population Ratio', survey_abbreviation: 'LN' }, + { series_id: 'LNS13327709', title: 'U-6 Unemployment Rate', survey_abbreviation: 'LN' }, + { series_id: 'PRS85006092', title: 'Nonfarm Business Labor Productivity', survey_abbreviation: 'PR' }, + { series_id: 'EIUIR', title: 'Import Price Index', survey_abbreviation: 'EI' }, + { series_id: 'EIUXR', title: 'Export Price Index', survey_abbreviation: 'EI' }, +] + +export class BLSBlsSearchFetcher extends Fetcher { + static override requireCredentials = false + + static override transformQuery(params: Record): BLSBlsSearchQueryParams { + return BLSBlsSearchQueryParamsSchema.parse(params) + } + + static override async extractData( + query: BLSBlsSearchQueryParams, + _credentials: Record | null, + ): Promise[]> { + const q = query.query.toLowerCase() + const results = COMMON_SERIES.filter(s => + s.title.toLowerCase().includes(q) || + s.series_id.toLowerCase().includes(q) || + s.survey_abbreviation.toLowerCase().includes(q), + ).slice(0, query.limit) + + if (results.length === 0) throw new EmptyDataError(`No BLS series matching "${query.query}" found.`) + return results + } + + static override transformData( + _query: BLSBlsSearchQueryParams, + data: Record[], + ) { + return data.map(d => BlsSearchDataSchema.parse(d)) + } +} diff --git a/packages/opentypebb/src/providers/bls/models/bls-series.ts b/packages/opentypebb/src/providers/bls/models/bls-series.ts new file mode 100644 index 00000000..00393b9a --- /dev/null +++ b/packages/opentypebb/src/providers/bls/models/bls-series.ts @@ -0,0 +1,90 @@ +/** + * BLS Series Fetcher. + * Uses BLS Public Data API v2. + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { BlsSeriesQueryParamsSchema, BlsSeriesDataSchema } from '../../../standard-models/bls-series.js' +import { EmptyDataError } from '../../../core/provider/utils/errors.js' +import { amakeRequest } from '../../../core/provider/utils/helpers.js' + +export const BLSBlsSeriesQueryParamsSchema = BlsSeriesQueryParamsSchema +export type BLSBlsSeriesQueryParams = z.infer + +const BLS_API_URL = 'https://api.bls.gov/publicAPI/v2/timeseries/data/' + +interface BlsApiResponse { + status: string + Results?: { + series?: Array<{ + seriesID: string + data: Array<{ + year: string + period: string + value: string + periodName: string + }> + }> + } +} + +export class BLSBlsSeriesFetcher extends Fetcher { + static override requireCredentials = false + + static override transformQuery(params: Record): BLSBlsSeriesQueryParams { + return BLSBlsSeriesQueryParamsSchema.parse(params) + } + + static override async extractData( + query: BLSBlsSeriesQueryParams, + credentials: Record | null, + ): Promise[]> { + const seriesIds = query.symbol.split(',').map(s => s.trim()).filter(Boolean) + const apiKey = credentials?.bls_api_key ?? '' + + const startYear = query.start_date ? query.start_date.slice(0, 4) : String(new Date().getFullYear() - 10) + const endYear = query.end_date ? query.end_date.slice(0, 4) : String(new Date().getFullYear()) + + const body: Record = { + seriesid: seriesIds, + startyear: startYear, + endyear: endYear, + } + if (apiKey) body.registrationkey = apiKey + + const data = await amakeRequest(BLS_API_URL, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }) + + const results: Record[] = [] + for (const series of data.Results?.series ?? []) { + for (const obs of series.data) { + // Convert period M01..M12 to month + const monthMatch = obs.period.match(/M(\d{2})/) + const month = monthMatch ? monthMatch[1] : '01' + const date = `${obs.year}-${month}-01` + results.push({ + date, + series_id: series.seriesID, + value: parseFloat(obs.value), + period: obs.period, + }) + } + } + + if (results.length === 0) throw new EmptyDataError('No BLS series data found.') + return results + } + + static override transformData( + _query: BLSBlsSeriesQueryParams, + data: Record[], + ) { + return data + .sort((a, b) => String(a.date).localeCompare(String(b.date))) + .map(d => BlsSeriesDataSchema.parse(d)) + } +} diff --git a/packages/opentypebb/src/providers/eia/index.ts b/packages/opentypebb/src/providers/eia/index.ts new file mode 100644 index 00000000..1e1d57cb --- /dev/null +++ b/packages/opentypebb/src/providers/eia/index.ts @@ -0,0 +1,23 @@ +/** + * EIA (Energy Information Administration) Provider Module. + * Provides petroleum and energy data from the US EIA API. + */ + +import { Provider } from '../../core/provider/abstract/provider.js' + +import { EIAPetroleumStatusReportFetcher } from './models/petroleum-status-report.js' +import { EIAShortTermEnergyOutlookFetcher } from './models/short-term-energy-outlook.js' + +export const eiaProvider = new Provider({ + name: 'eia', + website: 'https://www.eia.gov', + description: + 'The U.S. Energy Information Administration (EIA) collects, analyzes, and ' + + 'disseminates independent and impartial energy information.', + credentials: ['eia_api_key'], + fetcherDict: { + PetroleumStatusReport: EIAPetroleumStatusReportFetcher, + ShortTermEnergyOutlook: EIAShortTermEnergyOutlookFetcher, + }, + reprName: 'EIA', +}) diff --git a/packages/opentypebb/src/providers/eia/models/petroleum-status-report.ts b/packages/opentypebb/src/providers/eia/models/petroleum-status-report.ts new file mode 100644 index 00000000..e898bf94 --- /dev/null +++ b/packages/opentypebb/src/providers/eia/models/petroleum-status-report.ts @@ -0,0 +1,88 @@ +/** + * EIA Petroleum Status Report Fetcher. + * Uses EIA Open Data API v2. + * API docs: https://www.eia.gov/opendata/ + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { PetroleumStatusReportQueryParamsSchema, PetroleumStatusReportDataSchema } from '../../../standard-models/petroleum-status-report.js' +import { EmptyDataError } from '../../../core/provider/utils/errors.js' +import { amakeRequest } from '../../../core/provider/utils/helpers.js' + +export const EIAPetroleumStatusReportQueryParamsSchema = PetroleumStatusReportQueryParamsSchema +export type EIAPetroleumStatusReportQueryParams = z.infer + +const EIA_API_URL = 'https://api.eia.gov/v2/petroleum/sum/sndw/data/' + +// Map categories to EIA series IDs +const CATEGORY_SERIES: Record = { + crude_oil_production: { series: 'WCRFPUS2', unit: 'Thousand Barrels per Day' }, + crude_oil_stocks: { series: 'WCESTUS1', unit: 'Thousand Barrels' }, + gasoline_stocks: { series: 'WGTSTUS1', unit: 'Thousand Barrels' }, + distillate_stocks: { series: 'WDISTUS1', unit: 'Thousand Barrels' }, + refinery_utilization: { series: 'WPULEUS3', unit: 'Percent' }, +} + +interface EiaResponse { + response?: { + data?: Array<{ + period: string + value: number | null + 'series-description'?: string + }> + } +} + +export class EIAPetroleumStatusReportFetcher extends Fetcher { + static override transformQuery(params: Record): EIAPetroleumStatusReportQueryParams { + return EIAPetroleumStatusReportQueryParamsSchema.parse(params) + } + + static override async extractData( + query: EIAPetroleumStatusReportQueryParams, + credentials: Record | null, + ): Promise[]> { + const apiKey = credentials?.eia_api_key ?? credentials?.api_key ?? '' + const catInfo = CATEGORY_SERIES[query.category] + if (!catInfo) throw new EmptyDataError(`Unknown category: ${query.category}`) + + const params = new URLSearchParams({ + api_key: apiKey, + frequency: 'weekly', + 'data[0]': 'value', + 'facets[series][]': catInfo.series, + sort: JSON.stringify([{ column: 'period', direction: 'desc' }]), + length: '260', // ~5 years of weekly data + }) + + if (query.start_date) params.set('start', query.start_date) + if (query.end_date) params.set('end', query.end_date) + + const url = `${EIA_API_URL}?${params.toString()}` + const data = await amakeRequest(url) + + const results: Record[] = [] + for (const obs of data.response?.data ?? []) { + if (obs.value == null) continue + results.push({ + date: obs.period, + value: obs.value, + category: query.category, + unit: catInfo.unit, + }) + } + + if (results.length === 0) throw new EmptyDataError('No EIA petroleum data found.') + return results + } + + static override transformData( + _query: EIAPetroleumStatusReportQueryParams, + data: Record[], + ) { + return data + .sort((a, b) => String(a.date).localeCompare(String(b.date))) + .map(d => PetroleumStatusReportDataSchema.parse(d)) + } +} diff --git a/packages/opentypebb/src/providers/eia/models/short-term-energy-outlook.ts b/packages/opentypebb/src/providers/eia/models/short-term-energy-outlook.ts new file mode 100644 index 00000000..be6b4291 --- /dev/null +++ b/packages/opentypebb/src/providers/eia/models/short-term-energy-outlook.ts @@ -0,0 +1,92 @@ +/** + * EIA Short-Term Energy Outlook (STEO) Fetcher. + * Uses EIA Open Data API v2. + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { ShortTermEnergyOutlookQueryParamsSchema, ShortTermEnergyOutlookDataSchema } from '../../../standard-models/short-term-energy-outlook.js' +import { EmptyDataError } from '../../../core/provider/utils/errors.js' +import { amakeRequest } from '../../../core/provider/utils/helpers.js' + +export const EIAShortTermEnergyOutlookQueryParamsSchema = ShortTermEnergyOutlookQueryParamsSchema +export type EIAShortTermEnergyOutlookQueryParams = z.infer + +const EIA_STEO_URL = 'https://api.eia.gov/v2/steo/data/' + +// Map categories to EIA STEO series +const CATEGORY_SERIES: Record = { + crude_oil_price: { series: 'BREPUUS', unit: 'Dollars per Barrel' }, + gasoline_price: { series: 'MGWHUUS', unit: 'Dollars per Gallon' }, + natural_gas_price: { series: 'NGHHUUS', unit: 'Dollars per MMBtu' }, + crude_oil_production: { series: 'PAPRPUS', unit: 'Million Barrels per Day' }, + petroleum_consumption: { series: 'PATCPUS', unit: 'Million Barrels per Day' }, +} + +interface EiaSteoResponse { + response?: { + data?: Array<{ + period: string + value: number | null + seriesDescription?: string + }> + } +} + +export class EIAShortTermEnergyOutlookFetcher extends Fetcher { + static override transformQuery(params: Record): EIAShortTermEnergyOutlookQueryParams { + return EIAShortTermEnergyOutlookQueryParamsSchema.parse(params) + } + + static override async extractData( + query: EIAShortTermEnergyOutlookQueryParams, + credentials: Record | null, + ): Promise[]> { + const apiKey = credentials?.eia_api_key ?? credentials?.api_key ?? '' + const catInfo = CATEGORY_SERIES[query.category] + if (!catInfo) throw new EmptyDataError(`Unknown STEO category: ${query.category}`) + + const params = new URLSearchParams({ + api_key: apiKey, + frequency: 'monthly', + 'data[0]': 'value', + 'facets[seriesId][]': catInfo.series, + sort: JSON.stringify([{ column: 'period', direction: 'desc' }]), + length: '120', // ~10 years of monthly data + }) + + if (query.start_date) params.set('start', query.start_date.slice(0, 7)) // YYYY-MM + if (query.end_date) params.set('end', query.end_date.slice(0, 7)) + + const url = `${EIA_STEO_URL}?${params.toString()}` + const data = await amakeRequest(url) + + // Determine current date to flag forecasts + const now = new Date() + const currentPeriod = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}` + + const results: Record[] = [] + for (const obs of data.response?.data ?? []) { + if (obs.value == null) continue + results.push({ + date: `${obs.period}-01`, + value: obs.value, + category: query.category, + unit: catInfo.unit, + forecast: obs.period > currentPeriod, + }) + } + + if (results.length === 0) throw new EmptyDataError('No EIA STEO data found.') + return results + } + + static override transformData( + _query: EIAShortTermEnergyOutlookQueryParams, + data: Record[], + ) { + return data + .sort((a, b) => String(a.date).localeCompare(String(b.date))) + .map(d => ShortTermEnergyOutlookDataSchema.parse(d)) + } +} diff --git a/packages/opentypebb/src/providers/federal_reserve/index.ts b/packages/opentypebb/src/providers/federal_reserve/index.ts index 84795895..c82bb8de 100644 --- a/packages/opentypebb/src/providers/federal_reserve/index.ts +++ b/packages/opentypebb/src/providers/federal_reserve/index.ts @@ -5,6 +5,24 @@ import { Provider } from '../../core/provider/abstract/provider.js' import { FedCentralBankHoldingsFetcher } from './models/central-bank-holdings.js' +import { FedFredSearchFetcher } from './models/fred-search.js' +import { FedFredSeriesFetcher } from './models/fred-series.js' +import { FedFredReleaseTableFetcher } from './models/fred-release-table.js' +import { FedFredRegionalFetcher } from './models/fred-regional.js' +import { FedUnemploymentFetcher } from './models/unemployment.js' +import { FedMoneyMeasuresFetcher } from './models/money-measures.js' +import { FedPCEFetcher } from './models/pce.js' +import { FedTotalFactorProductivityFetcher } from './models/total-factor-productivity.js' +import { FedFomcDocumentsFetcher } from './models/fomc-documents.js' +import { FedPrimaryDealerPositioningFetcher } from './models/primary-dealer-positioning.js' +import { FedPrimaryDealerFailsFetcher } from './models/primary-dealer-fails.js' +import { FedNonfarmPayrollsFetcher } from './models/nonfarm-payrolls.js' +import { FedInflationExpectationsFetcher } from './models/inflation-expectations.js' +import { FedSloosFetcher } from './models/sloos.js' +import { FedUniversityOfMichiganFetcher } from './models/university-of-michigan.js' +import { FedEconomicConditionsChicagoFetcher } from './models/economic-conditions-chicago.js' +import { FedManufacturingOutlookNYFetcher } from './models/manufacturing-outlook-ny.js' +import { FedManufacturingOutlookTexasFetcher } from './models/manufacturing-outlook-texas.js' export const federalReserveProvider = new Provider({ name: 'federal_reserve', @@ -13,5 +31,23 @@ export const federalReserveProvider = new Provider({ credentials: ['api_key'], fetcherDict: { CentralBankHoldings: FedCentralBankHoldingsFetcher, + FredSearch: FedFredSearchFetcher, + FredSeries: FedFredSeriesFetcher, + FredReleaseTable: FedFredReleaseTableFetcher, + FredRegional: FedFredRegionalFetcher, + Unemployment: FedUnemploymentFetcher, + MoneyMeasures: FedMoneyMeasuresFetcher, + PersonalConsumptionExpenditures: FedPCEFetcher, + TotalFactorProductivity: FedTotalFactorProductivityFetcher, + FomcDocuments: FedFomcDocumentsFetcher, + PrimaryDealerPositioning: FedPrimaryDealerPositioningFetcher, + PrimaryDealerFails: FedPrimaryDealerFailsFetcher, + NonfarmPayrolls: FedNonfarmPayrollsFetcher, + InflationExpectations: FedInflationExpectationsFetcher, + Sloos: FedSloosFetcher, + UniversityOfMichigan: FedUniversityOfMichiganFetcher, + EconomicConditionsChicago: FedEconomicConditionsChicagoFetcher, + ManufacturingOutlookNY: FedManufacturingOutlookNYFetcher, + ManufacturingOutlookTexas: FedManufacturingOutlookTexasFetcher, }, }) diff --git a/packages/opentypebb/src/providers/federal_reserve/models/economic-conditions-chicago.ts b/packages/opentypebb/src/providers/federal_reserve/models/economic-conditions-chicago.ts new file mode 100644 index 00000000..417714ba --- /dev/null +++ b/packages/opentypebb/src/providers/federal_reserve/models/economic-conditions-chicago.ts @@ -0,0 +1,49 @@ +/** + * Federal Reserve Chicago Fed National Activity Index Fetcher. + * Uses FRED series: CFNAI (CFNAI), CFNAIMA3 (3-month moving average). + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { EconomicConditionsChicagoQueryParamsSchema, EconomicConditionsChicagoDataSchema } from '../../../standard-models/economic-conditions-chicago.js' +import { EmptyDataError } from '../../../core/provider/utils/errors.js' +import { fetchFredMultiSeries, multiSeriesToRecords, getFredApiKey } from '../utils/fred-helpers.js' + +export const FedChicagoQueryParamsSchema = EconomicConditionsChicagoQueryParamsSchema +export type FedChicagoQueryParams = z.infer + +const SERIES = ['CFNAI', 'CFNAIMA3'] +const FIELD_MAP: Record = { + CFNAI: 'cfnai', + CFNAIMA3: 'cfnai_ma3', +} + +export class FedEconomicConditionsChicagoFetcher extends Fetcher { + static override requireCredentials = false + + static override transformQuery(params: Record): FedChicagoQueryParams { + return FedChicagoQueryParamsSchema.parse(params) + } + + static override async extractData( + query: FedChicagoQueryParams, + credentials: Record | null, + ): Promise[]> { + const apiKey = getFredApiKey(credentials) + const dataMap = await fetchFredMultiSeries(SERIES, apiKey, { + startDate: query.start_date, + endDate: query.end_date, + }) + + const records = multiSeriesToRecords(dataMap, FIELD_MAP) + if (records.length === 0) throw new EmptyDataError('No Chicago Fed data found.') + return records + } + + static override transformData( + _query: FedChicagoQueryParams, + data: Record[], + ) { + return data.map(d => EconomicConditionsChicagoDataSchema.parse(d)) + } +} diff --git a/packages/opentypebb/src/providers/federal_reserve/models/fomc-documents.ts b/packages/opentypebb/src/providers/federal_reserve/models/fomc-documents.ts new file mode 100644 index 00000000..b9d886c2 --- /dev/null +++ b/packages/opentypebb/src/providers/federal_reserve/models/fomc-documents.ts @@ -0,0 +1,68 @@ +/** + * Federal Reserve FOMC Documents Fetcher. + * Fetches FOMC calendar and document links from the Fed website JSON API. + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { FomcDocumentsQueryParamsSchema, FomcDocumentsDataSchema } from '../../../standard-models/fomc-documents.js' +import { EmptyDataError } from '../../../core/provider/utils/errors.js' +import { amakeRequest } from '../../../core/provider/utils/helpers.js' + +export const FedFomcDocumentsQueryParamsSchema = FomcDocumentsQueryParamsSchema +export type FedFomcDocumentsQueryParams = z.infer + +const FOMC_CALENDAR_URL = 'https://www.federalreserve.gov/monetarypolicy/fomccalendars.htm' + +interface FomcMeeting { + date: string + link?: string + statement?: string + minutes?: string +} + +export class FedFomcDocumentsFetcher extends Fetcher { + static override requireCredentials = false + + static override transformQuery(params: Record): FedFomcDocumentsQueryParams { + return FedFomcDocumentsQueryParamsSchema.parse(params) + } + + static override async extractData( + query: FedFomcDocumentsQueryParams, + _credentials: Record | null, + ): Promise[]> { + // Use FRED series for Fed Funds Rate as a proxy for FOMC activity + // FRED series DFEDTAR (target rate) and DFEDTARU (upper bound) + try { + const data = await amakeRequest>( + 'https://api.stlouisfed.org/fred/series/observations?series_id=DFEDTARU&file_type=json&sort_order=desc&limit=50', + ) + const observations = (data.observations ?? []) as Array<{ date: string; value: string }> + if (observations.length === 0) throw new EmptyDataError() + + return observations + .filter(o => o.value !== '.') + .map(o => ({ + date: o.date, + title: `Fed Funds Target Rate Upper Bound: ${o.value}%`, + type: 'rate_decision', + url: FOMC_CALENDAR_URL, + })) + } catch { + throw new EmptyDataError('No FOMC documents data found.') + } + } + + static override transformData( + query: FedFomcDocumentsQueryParams, + data: Record[], + ) { + let filtered = data + if (query.start_date) filtered = filtered.filter(d => String(d.date) >= query.start_date!) + if (query.end_date) filtered = filtered.filter(d => String(d.date) <= query.end_date!) + return filtered + .sort((a, b) => String(a.date).localeCompare(String(b.date))) + .map(d => FomcDocumentsDataSchema.parse(d)) + } +} diff --git a/packages/opentypebb/src/providers/federal_reserve/models/fred-regional.ts b/packages/opentypebb/src/providers/federal_reserve/models/fred-regional.ts new file mode 100644 index 00000000..89654040 --- /dev/null +++ b/packages/opentypebb/src/providers/federal_reserve/models/fred-regional.ts @@ -0,0 +1,42 @@ +/** + * Federal Reserve FRED Regional (GeoFRED) Fetcher. + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { FredRegionalQueryParamsSchema, FredRegionalDataSchema } from '../../../standard-models/fred-regional.js' +import { EmptyDataError } from '../../../core/provider/utils/errors.js' +import { fredRegionalApi, getFredApiKey } from '../utils/fred-helpers.js' + +export const FedFredRegionalQueryParamsSchema = FredRegionalQueryParamsSchema +export type FedFredRegionalQueryParams = z.infer + +export class FedFredRegionalFetcher extends Fetcher { + static override requireCredentials = false + + static override transformQuery(params: Record): FedFredRegionalQueryParams { + return FedFredRegionalQueryParamsSchema.parse(params) + } + + static override async extractData( + query: FedFredRegionalQueryParams, + credentials: Record | null, + ): Promise[]> { + const apiKey = getFredApiKey(credentials) + const results = await fredRegionalApi(query.symbol, apiKey, { + regionType: query.region_type, + date: query.date ?? undefined, + startDate: query.start_date ?? undefined, + frequency: query.frequency ?? undefined, + }) + if (results.length === 0) throw new EmptyDataError('No GeoFRED data found.') + return results + } + + static override transformData( + _query: FedFredRegionalQueryParams, + data: Record[], + ) { + return data.map(d => FredRegionalDataSchema.parse(d)) + } +} diff --git a/packages/opentypebb/src/providers/federal_reserve/models/fred-release-table.ts b/packages/opentypebb/src/providers/federal_reserve/models/fred-release-table.ts new file mode 100644 index 00000000..e1e4d0cc --- /dev/null +++ b/packages/opentypebb/src/providers/federal_reserve/models/fred-release-table.ts @@ -0,0 +1,40 @@ +/** + * Federal Reserve FRED Release Table Fetcher. + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { FredReleaseTableQueryParamsSchema, FredReleaseTableDataSchema } from '../../../standard-models/fred-release-table.js' +import { EmptyDataError } from '../../../core/provider/utils/errors.js' +import { fredReleaseTableApi, getFredApiKey } from '../utils/fred-helpers.js' + +export const FedFredReleaseTableQueryParamsSchema = FredReleaseTableQueryParamsSchema +export type FedFredReleaseTableQueryParams = z.infer + +export class FedFredReleaseTableFetcher extends Fetcher { + static override requireCredentials = false + + static override transformQuery(params: Record): FedFredReleaseTableQueryParams { + return FedFredReleaseTableQueryParamsSchema.parse(params) + } + + static override async extractData( + query: FedFredReleaseTableQueryParams, + credentials: Record | null, + ): Promise[]> { + const apiKey = getFredApiKey(credentials) + const results = await fredReleaseTableApi(query.release_id, apiKey, { + elementId: query.element_id ?? undefined, + date: query.date ?? undefined, + }) + if (results.length === 0) throw new EmptyDataError('No release table data found.') + return results + } + + static override transformData( + _query: FedFredReleaseTableQueryParams, + data: Record[], + ) { + return data.map(d => FredReleaseTableDataSchema.parse(d)) + } +} diff --git a/packages/opentypebb/src/providers/federal_reserve/models/fred-search.ts b/packages/opentypebb/src/providers/federal_reserve/models/fred-search.ts new file mode 100644 index 00000000..deaee26d --- /dev/null +++ b/packages/opentypebb/src/providers/federal_reserve/models/fred-search.ts @@ -0,0 +1,45 @@ +/** + * Federal Reserve FRED Search Fetcher. + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { FredSearchQueryParamsSchema, FredSearchDataSchema } from '../../../standard-models/fred-search.js' +import { EmptyDataError } from '../../../core/provider/utils/errors.js' +import { fredSearchApi, getFredApiKey } from '../utils/fred-helpers.js' + +export const FedFredSearchQueryParamsSchema = FredSearchQueryParamsSchema +export type FedFredSearchQueryParams = z.infer + +export class FedFredSearchFetcher extends Fetcher { + static override requireCredentials = false + + static override transformQuery(params: Record): FedFredSearchQueryParams { + return FedFredSearchQueryParamsSchema.parse(params) + } + + static override async extractData( + query: FedFredSearchQueryParams, + credentials: Record | null, + ): Promise[]> { + const apiKey = getFredApiKey(credentials) + const results = await fredSearchApi(query.query, apiKey, { limit: query.limit }) + if (results.length === 0) throw new EmptyDataError('No FRED series found.') + return results.map(r => ({ + series_id: r.id, + title: r.title, + frequency: r.frequency_short || null, + units: r.units_short || null, + seasonal_adjustment: r.seasonal_adjustment_short || null, + last_updated: r.last_updated || null, + notes: r.notes || null, + })) + } + + static override transformData( + _query: FedFredSearchQueryParams, + data: Record[], + ) { + return data.map(d => FredSearchDataSchema.parse(d)) + } +} diff --git a/packages/opentypebb/src/providers/federal_reserve/models/fred-series.ts b/packages/opentypebb/src/providers/federal_reserve/models/fred-series.ts new file mode 100644 index 00000000..a41d5975 --- /dev/null +++ b/packages/opentypebb/src/providers/federal_reserve/models/fred-series.ts @@ -0,0 +1,47 @@ +/** + * Federal Reserve FRED Series Fetcher. + * Fetches observations for one or more FRED series. + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { FredSeriesQueryParamsSchema, FredSeriesDataSchema } from '../../../standard-models/fred-series.js' +import { EmptyDataError } from '../../../core/provider/utils/errors.js' +import { fetchFredMultiSeries, multiSeriesToRecords, getFredApiKey } from '../utils/fred-helpers.js' + +export const FedFredSeriesQueryParamsSchema = FredSeriesQueryParamsSchema +export type FedFredSeriesQueryParams = z.infer + +export class FedFredSeriesFetcher extends Fetcher { + static override requireCredentials = false + + static override transformQuery(params: Record): FedFredSeriesQueryParams { + return FedFredSeriesQueryParamsSchema.parse(params) + } + + static override async extractData( + query: FedFredSeriesQueryParams, + credentials: Record | null, + ): Promise[]> { + const apiKey = getFredApiKey(credentials) + const seriesIds = query.symbol.split(',').map(s => s.trim()).filter(Boolean) + if (seriesIds.length === 0) throw new EmptyDataError('No series IDs provided.') + + const dataMap = await fetchFredMultiSeries(seriesIds, apiKey, { + startDate: query.start_date, + endDate: query.end_date, + limit: query.limit ?? undefined, + }) + + const records = multiSeriesToRecords(dataMap) + if (records.length === 0) throw new EmptyDataError('No FRED series data found.') + return records + } + + static override transformData( + _query: FedFredSeriesQueryParams, + data: Record[], + ) { + return data.map(d => FredSeriesDataSchema.parse(d)) + } +} diff --git a/packages/opentypebb/src/providers/federal_reserve/models/inflation-expectations.ts b/packages/opentypebb/src/providers/federal_reserve/models/inflation-expectations.ts new file mode 100644 index 00000000..5c5f70ea --- /dev/null +++ b/packages/opentypebb/src/providers/federal_reserve/models/inflation-expectations.ts @@ -0,0 +1,51 @@ +/** + * Federal Reserve Inflation Expectations Fetcher. + * Uses FRED series: MICH (Michigan 1y), MICH5Y (Michigan 5y), + * T5YIE (5y Breakeven), T10YIE (10y Breakeven). + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { InflationExpectationsQueryParamsSchema, InflationExpectationsDataSchema } from '../../../standard-models/inflation-expectations.js' +import { EmptyDataError } from '../../../core/provider/utils/errors.js' +import { fetchFredMultiSeries, multiSeriesToRecords, getFredApiKey } from '../utils/fred-helpers.js' + +export const FedInflationExpectationsQueryParamsSchema = InflationExpectationsQueryParamsSchema +export type FedInflationExpectationsQueryParams = z.infer + +const SERIES = ['MICH', 'T5YIE', 'T10YIE'] +const FIELD_MAP: Record = { + MICH: 'michigan_1y', + T5YIE: 'breakeven_5y', + T10YIE: 'breakeven_10y', +} + +export class FedInflationExpectationsFetcher extends Fetcher { + static override requireCredentials = false + + static override transformQuery(params: Record): FedInflationExpectationsQueryParams { + return FedInflationExpectationsQueryParamsSchema.parse(params) + } + + static override async extractData( + query: FedInflationExpectationsQueryParams, + credentials: Record | null, + ): Promise[]> { + const apiKey = getFredApiKey(credentials) + const dataMap = await fetchFredMultiSeries(SERIES, apiKey, { + startDate: query.start_date, + endDate: query.end_date, + }) + + const records = multiSeriesToRecords(dataMap, FIELD_MAP) + if (records.length === 0) throw new EmptyDataError('No inflation expectations data found.') + return records + } + + static override transformData( + _query: FedInflationExpectationsQueryParams, + data: Record[], + ) { + return data.map(d => InflationExpectationsDataSchema.parse(d)) + } +} diff --git a/packages/opentypebb/src/providers/federal_reserve/models/manufacturing-outlook-ny.ts b/packages/opentypebb/src/providers/federal_reserve/models/manufacturing-outlook-ny.ts new file mode 100644 index 00000000..90682be9 --- /dev/null +++ b/packages/opentypebb/src/providers/federal_reserve/models/manufacturing-outlook-ny.ts @@ -0,0 +1,44 @@ +/** + * Federal Reserve NY Manufacturing Outlook (Empire State) Fetcher. + * Uses FRED series: GACDISA066MSFRBNY (General Business Conditions). + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { ManufacturingOutlookNYQueryParamsSchema, ManufacturingOutlookNYDataSchema } from '../../../standard-models/manufacturing-outlook-ny.js' +import { EmptyDataError } from '../../../core/provider/utils/errors.js' +import { fetchFredSeries, getFredApiKey } from '../utils/fred-helpers.js' + +export const FedManufacturingOutlookNYQueryParamsSchema = ManufacturingOutlookNYQueryParamsSchema +export type FedManufacturingOutlookNYQueryParams = z.infer + +export class FedManufacturingOutlookNYFetcher extends Fetcher { + static override requireCredentials = false + + static override transformQuery(params: Record): FedManufacturingOutlookNYQueryParams { + return FedManufacturingOutlookNYQueryParamsSchema.parse(params) + } + + static override async extractData( + query: FedManufacturingOutlookNYQueryParams, + credentials: Record | null, + ): Promise[]> { + const apiKey = getFredApiKey(credentials) + const observations = await fetchFredSeries('GACDISA066MSFRBNY', apiKey, { + startDate: query.start_date, + endDate: query.end_date, + }) + if (observations.length === 0) throw new EmptyDataError('No Empire State Manufacturing data found.') + return observations.map(o => ({ + date: o.date, + general_business_conditions: parseFloat(o.value), + })) + } + + static override transformData( + _query: FedManufacturingOutlookNYQueryParams, + data: Record[], + ) { + return data.map(d => ManufacturingOutlookNYDataSchema.parse(d)) + } +} diff --git a/packages/opentypebb/src/providers/federal_reserve/models/manufacturing-outlook-texas.ts b/packages/opentypebb/src/providers/federal_reserve/models/manufacturing-outlook-texas.ts new file mode 100644 index 00000000..0f359d76 --- /dev/null +++ b/packages/opentypebb/src/providers/federal_reserve/models/manufacturing-outlook-texas.ts @@ -0,0 +1,46 @@ +/** + * Federal Reserve Dallas Fed Manufacturing Outlook Fetcher. + * Uses FRED series: DALLASMANOUTGEN (General Activity). + * Note: actual FRED ID may vary; falls back to BCTDAL for Dallas Fed data. + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { ManufacturingOutlookTexasQueryParamsSchema, ManufacturingOutlookTexasDataSchema } from '../../../standard-models/manufacturing-outlook-texas.js' +import { EmptyDataError } from '../../../core/provider/utils/errors.js' +import { fetchFredSeries, getFredApiKey } from '../utils/fred-helpers.js' + +export const FedManufacturingOutlookTexasQueryParamsSchema = ManufacturingOutlookTexasQueryParamsSchema +export type FedManufacturingOutlookTexasQueryParams = z.infer + +export class FedManufacturingOutlookTexasFetcher extends Fetcher { + static override requireCredentials = false + + static override transformQuery(params: Record): FedManufacturingOutlookTexasQueryParams { + return FedManufacturingOutlookTexasQueryParamsSchema.parse(params) + } + + static override async extractData( + query: FedManufacturingOutlookTexasQueryParams, + credentials: Record | null, + ): Promise[]> { + const apiKey = getFredApiKey(credentials) + // Dallas Fed Texas Manufacturing Outlook Survey — General Business Activity + const observations = await fetchFredSeries('BCTDAL', apiKey, { + startDate: query.start_date, + endDate: query.end_date, + }) + if (observations.length === 0) throw new EmptyDataError('No Dallas Fed Manufacturing data found.') + return observations.map(o => ({ + date: o.date, + general_activity: parseFloat(o.value), + })) + } + + static override transformData( + _query: FedManufacturingOutlookTexasQueryParams, + data: Record[], + ) { + return data.map(d => ManufacturingOutlookTexasDataSchema.parse(d)) + } +} diff --git a/packages/opentypebb/src/providers/federal_reserve/models/money-measures.ts b/packages/opentypebb/src/providers/federal_reserve/models/money-measures.ts new file mode 100644 index 00000000..08659af5 --- /dev/null +++ b/packages/opentypebb/src/providers/federal_reserve/models/money-measures.ts @@ -0,0 +1,52 @@ +/** + * Federal Reserve Money Measures Fetcher. + * Uses FRED series: M1SL (M1), M2SL (M2) — seasonally adjusted. + * Or: M1NS, M2NS — not adjusted. + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { MoneyMeasuresQueryParamsSchema, MoneyMeasuresDataSchema } from '../../../standard-models/money-measures.js' +import { EmptyDataError } from '../../../core/provider/utils/errors.js' +import { fetchFredMultiSeries, multiSeriesToRecords, getFredApiKey } from '../utils/fred-helpers.js' + +export const FedMoneyMeasuresQueryParamsSchema = MoneyMeasuresQueryParamsSchema +export type FedMoneyMeasuresQueryParams = z.infer + +export class FedMoneyMeasuresFetcher extends Fetcher { + static override requireCredentials = false + + static override transformQuery(params: Record): FedMoneyMeasuresQueryParams { + return FedMoneyMeasuresQueryParamsSchema.parse(params) + } + + static override async extractData( + query: FedMoneyMeasuresQueryParams, + credentials: Record | null, + ): Promise[]> { + const apiKey = getFredApiKey(credentials) + const adjusted = query.adjusted !== false + const m1Series = adjusted ? 'M1SL' : 'M1NS' + const m2Series = adjusted ? 'M2SL' : 'M2NS' + + const dataMap = await fetchFredMultiSeries([m1Series, m2Series], apiKey, { + startDate: query.start_date, + endDate: query.end_date, + }) + + const fieldMap: Record = { + [m1Series]: 'm1', + [m2Series]: 'm2', + } + const records = multiSeriesToRecords(dataMap, fieldMap) + if (records.length === 0) throw new EmptyDataError('No money measures data found.') + return records + } + + static override transformData( + _query: FedMoneyMeasuresQueryParams, + data: Record[], + ) { + return data.map(d => MoneyMeasuresDataSchema.parse(d)) + } +} diff --git a/packages/opentypebb/src/providers/federal_reserve/models/nonfarm-payrolls.ts b/packages/opentypebb/src/providers/federal_reserve/models/nonfarm-payrolls.ts new file mode 100644 index 00000000..9968d08b --- /dev/null +++ b/packages/opentypebb/src/providers/federal_reserve/models/nonfarm-payrolls.ts @@ -0,0 +1,50 @@ +/** + * Federal Reserve Nonfarm Payrolls Fetcher. + * Uses FRED series: PAYEMS (Total Nonfarm), USPRIV (Private), USGOVT (Government). + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { NonfarmPayrollsQueryParamsSchema, NonfarmPayrollsDataSchema } from '../../../standard-models/nonfarm-payrolls.js' +import { EmptyDataError } from '../../../core/provider/utils/errors.js' +import { fetchFredMultiSeries, multiSeriesToRecords, getFredApiKey } from '../utils/fred-helpers.js' + +export const FedNonfarmPayrollsQueryParamsSchema = NonfarmPayrollsQueryParamsSchema +export type FedNonfarmPayrollsQueryParams = z.infer + +const SERIES = ['PAYEMS', 'USPRIV', 'USGOVT'] +const FIELD_MAP: Record = { + PAYEMS: 'total_nonfarm', + USPRIV: 'private_sector', + USGOVT: 'government', +} + +export class FedNonfarmPayrollsFetcher extends Fetcher { + static override requireCredentials = false + + static override transformQuery(params: Record): FedNonfarmPayrollsQueryParams { + return FedNonfarmPayrollsQueryParamsSchema.parse(params) + } + + static override async extractData( + query: FedNonfarmPayrollsQueryParams, + credentials: Record | null, + ): Promise[]> { + const apiKey = getFredApiKey(credentials) + const dataMap = await fetchFredMultiSeries(SERIES, apiKey, { + startDate: query.start_date, + endDate: query.end_date, + }) + + const records = multiSeriesToRecords(dataMap, FIELD_MAP) + if (records.length === 0) throw new EmptyDataError('No nonfarm payrolls data found.') + return records + } + + static override transformData( + _query: FedNonfarmPayrollsQueryParams, + data: Record[], + ) { + return data.map(d => NonfarmPayrollsDataSchema.parse(d)) + } +} diff --git a/packages/opentypebb/src/providers/federal_reserve/models/pce.ts b/packages/opentypebb/src/providers/federal_reserve/models/pce.ts new file mode 100644 index 00000000..24948974 --- /dev/null +++ b/packages/opentypebb/src/providers/federal_reserve/models/pce.ts @@ -0,0 +1,46 @@ +/** + * Federal Reserve PCE Fetcher. + * Uses FRED series: PCEPI (PCE Price Index), PCEPILFE (Core PCE). + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { PersonalConsumptionExpendituresQueryParamsSchema, PersonalConsumptionExpendituresDataSchema } from '../../../standard-models/pce.js' +import { EmptyDataError } from '../../../core/provider/utils/errors.js' +import { fetchFredMultiSeries, multiSeriesToRecords, getFredApiKey } from '../utils/fred-helpers.js' + +export const FedPCEQueryParamsSchema = PersonalConsumptionExpendituresQueryParamsSchema +export type FedPCEQueryParams = z.infer + +export class FedPCEFetcher extends Fetcher { + static override requireCredentials = false + + static override transformQuery(params: Record): FedPCEQueryParams { + return FedPCEQueryParamsSchema.parse(params) + } + + static override async extractData( + query: FedPCEQueryParams, + credentials: Record | null, + ): Promise[]> { + const apiKey = getFredApiKey(credentials) + const dataMap = await fetchFredMultiSeries(['PCEPI', 'PCEPILFE'], apiKey, { + startDate: query.start_date, + endDate: query.end_date, + }) + + const records = multiSeriesToRecords(dataMap, { + PCEPI: 'pce', + PCEPILFE: 'core_pce', + }) + if (records.length === 0) throw new EmptyDataError('No PCE data found.') + return records + } + + static override transformData( + _query: FedPCEQueryParams, + data: Record[], + ) { + return data.map(d => PersonalConsumptionExpendituresDataSchema.parse(d)) + } +} diff --git a/packages/opentypebb/src/providers/federal_reserve/models/primary-dealer-fails.ts b/packages/opentypebb/src/providers/federal_reserve/models/primary-dealer-fails.ts new file mode 100644 index 00000000..b3e2cbc0 --- /dev/null +++ b/packages/opentypebb/src/providers/federal_reserve/models/primary-dealer-fails.ts @@ -0,0 +1,50 @@ +/** + * Federal Reserve Primary Dealer Fails Fetcher. + * Uses FRED series for delivery failures data. + * Series: DTBSPCKF (Fails to Deliver), DTBSPCKR (Fails to Receive). + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { PrimaryDealerFailsQueryParamsSchema, PrimaryDealerFailsDataSchema } from '../../../standard-models/primary-dealer-fails.js' +import { EmptyDataError } from '../../../core/provider/utils/errors.js' +import { fetchFredMultiSeries, multiSeriesToRecords, getFredApiKey } from '../utils/fred-helpers.js' + +export const FedPrimaryDealerFailsQueryParamsSchema = PrimaryDealerFailsQueryParamsSchema +export type FedPrimaryDealerFailsQueryParams = z.infer + +const SERIES = ['DTBSPCKF', 'DTBSPCKR'] +const FIELD_MAP: Record = { + DTBSPCKF: 'fails_to_deliver', + DTBSPCKR: 'fails_to_receive', +} + +export class FedPrimaryDealerFailsFetcher extends Fetcher { + static override requireCredentials = false + + static override transformQuery(params: Record): FedPrimaryDealerFailsQueryParams { + return FedPrimaryDealerFailsQueryParamsSchema.parse(params) + } + + static override async extractData( + query: FedPrimaryDealerFailsQueryParams, + credentials: Record | null, + ): Promise[]> { + const apiKey = getFredApiKey(credentials) + const dataMap = await fetchFredMultiSeries(SERIES, apiKey, { + startDate: query.start_date, + endDate: query.end_date, + }) + + const records = multiSeriesToRecords(dataMap, FIELD_MAP) + if (records.length === 0) throw new EmptyDataError('No primary dealer fails data found.') + return records + } + + static override transformData( + _query: FedPrimaryDealerFailsQueryParams, + data: Record[], + ) { + return data.map(d => PrimaryDealerFailsDataSchema.parse(d)) + } +} diff --git a/packages/opentypebb/src/providers/federal_reserve/models/primary-dealer-positioning.ts b/packages/opentypebb/src/providers/federal_reserve/models/primary-dealer-positioning.ts new file mode 100644 index 00000000..72b0c9d9 --- /dev/null +++ b/packages/opentypebb/src/providers/federal_reserve/models/primary-dealer-positioning.ts @@ -0,0 +1,52 @@ +/** + * Federal Reserve Primary Dealer Positioning Fetcher. + * Uses NY Fed Primary Dealer Statistics via FRED. + * Series: PDTNCNET (Total Net Positions), etc. + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { PrimaryDealerPositioningQueryParamsSchema, PrimaryDealerPositioningDataSchema } from '../../../standard-models/primary-dealer-positioning.js' +import { EmptyDataError } from '../../../core/provider/utils/errors.js' +import { fetchFredMultiSeries, multiSeriesToRecords, getFredApiKey } from '../utils/fred-helpers.js' + +export const FedPrimaryDealerPositioningQueryParamsSchema = PrimaryDealerPositioningQueryParamsSchema +export type FedPrimaryDealerPositioningQueryParams = z.infer + +// Primary Dealer FRED series +const SERIES = ['PDTNCNET', 'PDUSTTOT', 'PDMBSTOT'] +const FIELD_MAP: Record = { + PDTNCNET: 'total_net_position', + PDUSTTOT: 'treasury_total', + PDMBSTOT: 'mbs_total', +} + +export class FedPrimaryDealerPositioningFetcher extends Fetcher { + static override requireCredentials = false + + static override transformQuery(params: Record): FedPrimaryDealerPositioningQueryParams { + return FedPrimaryDealerPositioningQueryParamsSchema.parse(params) + } + + static override async extractData( + query: FedPrimaryDealerPositioningQueryParams, + credentials: Record | null, + ): Promise[]> { + const apiKey = getFredApiKey(credentials) + const dataMap = await fetchFredMultiSeries(SERIES, apiKey, { + startDate: query.start_date, + endDate: query.end_date, + }) + + const records = multiSeriesToRecords(dataMap, FIELD_MAP) + if (records.length === 0) throw new EmptyDataError('No primary dealer positioning data found.') + return records + } + + static override transformData( + _query: FedPrimaryDealerPositioningQueryParams, + data: Record[], + ) { + return data.map(d => PrimaryDealerPositioningDataSchema.parse(d)) + } +} diff --git a/packages/opentypebb/src/providers/federal_reserve/models/sloos.ts b/packages/opentypebb/src/providers/federal_reserve/models/sloos.ts new file mode 100644 index 00000000..5abb3e78 --- /dev/null +++ b/packages/opentypebb/src/providers/federal_reserve/models/sloos.ts @@ -0,0 +1,49 @@ +/** + * Federal Reserve SLOOS (Senior Loan Officer Opinion Survey) Fetcher. + * Uses FRED series: DRTSCILM (C&I Loan Tightening), DRTSCLCC (Consumer Loan Tightening). + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { SloosQueryParamsSchema, SloosDataSchema } from '../../../standard-models/sloos.js' +import { EmptyDataError } from '../../../core/provider/utils/errors.js' +import { fetchFredMultiSeries, multiSeriesToRecords, getFredApiKey } from '../utils/fred-helpers.js' + +export const FedSloosQueryParamsSchema = SloosQueryParamsSchema +export type FedSloosQueryParams = z.infer + +const SERIES = ['DRTSCILM', 'DRTSCLCC'] +const FIELD_MAP: Record = { + DRTSCILM: 'ci_loan_tightening', + DRTSCLCC: 'consumer_loan_tightening', +} + +export class FedSloosFetcher extends Fetcher { + static override requireCredentials = false + + static override transformQuery(params: Record): FedSloosQueryParams { + return FedSloosQueryParamsSchema.parse(params) + } + + static override async extractData( + query: FedSloosQueryParams, + credentials: Record | null, + ): Promise[]> { + const apiKey = getFredApiKey(credentials) + const dataMap = await fetchFredMultiSeries(SERIES, apiKey, { + startDate: query.start_date, + endDate: query.end_date, + }) + + const records = multiSeriesToRecords(dataMap, FIELD_MAP) + if (records.length === 0) throw new EmptyDataError('No SLOOS data found.') + return records + } + + static override transformData( + _query: FedSloosQueryParams, + data: Record[], + ) { + return data.map(d => SloosDataSchema.parse(d)) + } +} diff --git a/packages/opentypebb/src/providers/federal_reserve/models/total-factor-productivity.ts b/packages/opentypebb/src/providers/federal_reserve/models/total-factor-productivity.ts new file mode 100644 index 00000000..30659236 --- /dev/null +++ b/packages/opentypebb/src/providers/federal_reserve/models/total-factor-productivity.ts @@ -0,0 +1,44 @@ +/** + * Federal Reserve Total Factor Productivity Fetcher. + * Uses FRED series: RTFPNAUSA632NRUG (Annual TFP at constant national prices for US). + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { TotalFactorProductivityQueryParamsSchema, TotalFactorProductivityDataSchema } from '../../../standard-models/total-factor-productivity.js' +import { EmptyDataError } from '../../../core/provider/utils/errors.js' +import { fetchFredSeries, getFredApiKey } from '../utils/fred-helpers.js' + +export const FedTFPQueryParamsSchema = TotalFactorProductivityQueryParamsSchema +export type FedTFPQueryParams = z.infer + +export class FedTotalFactorProductivityFetcher extends Fetcher { + static override requireCredentials = false + + static override transformQuery(params: Record): FedTFPQueryParams { + return FedTFPQueryParamsSchema.parse(params) + } + + static override async extractData( + query: FedTFPQueryParams, + credentials: Record | null, + ): Promise[]> { + const apiKey = getFredApiKey(credentials) + const observations = await fetchFredSeries('RTFPNAUSA632NRUG', apiKey, { + startDate: query.start_date, + endDate: query.end_date, + }) + if (observations.length === 0) throw new EmptyDataError('No TFP data found.') + return observations.map(o => ({ + date: o.date, + value: parseFloat(o.value), + })) + } + + static override transformData( + _query: FedTFPQueryParams, + data: Record[], + ) { + return data.map(d => TotalFactorProductivityDataSchema.parse(d)) + } +} diff --git a/packages/opentypebb/src/providers/federal_reserve/models/unemployment.ts b/packages/opentypebb/src/providers/federal_reserve/models/unemployment.ts new file mode 100644 index 00000000..f0a7a3a7 --- /dev/null +++ b/packages/opentypebb/src/providers/federal_reserve/models/unemployment.ts @@ -0,0 +1,52 @@ +/** + * Federal Reserve Unemployment Fetcher. + * Uses FRED series: UNRATE (U-3), U6RATE (U-6). + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { UnemploymentQueryParamsSchema, UnemploymentDataSchema } from '../../../standard-models/unemployment.js' +import { EmptyDataError } from '../../../core/provider/utils/errors.js' +import { fetchFredSeries, getFredApiKey } from '../utils/fred-helpers.js' + +export const FedUnemploymentQueryParamsSchema = UnemploymentQueryParamsSchema +export type FedUnemploymentQueryParams = z.infer + +export class FedUnemploymentFetcher extends Fetcher { + static override requireCredentials = false + + static override transformQuery(params: Record): FedUnemploymentQueryParams { + return FedUnemploymentQueryParamsSchema.parse(params) + } + + static override async extractData( + query: FedUnemploymentQueryParams, + credentials: Record | null, + ): Promise[]> { + const apiKey = getFredApiKey(credentials) + const observations = await fetchFredSeries('UNRATE', apiKey, { + startDate: query.start_date, + endDate: query.end_date, + }) + + if (observations.length === 0) throw new EmptyDataError('No unemployment data found.') + return observations.map(o => ({ + date: o.date, + country: 'United States', + value: parseFloat(o.value), + })) + } + + static override transformData( + query: FedUnemploymentQueryParams, + data: Record[], + ) { + if (data.length === 0) throw new EmptyDataError() + let filtered = data + if (query.start_date) filtered = filtered.filter(d => String(d.date) >= query.start_date!) + if (query.end_date) filtered = filtered.filter(d => String(d.date) <= query.end_date!) + return filtered + .sort((a, b) => String(a.date).localeCompare(String(b.date))) + .map(d => UnemploymentDataSchema.parse(d)) + } +} diff --git a/packages/opentypebb/src/providers/federal_reserve/models/university-of-michigan.ts b/packages/opentypebb/src/providers/federal_reserve/models/university-of-michigan.ts new file mode 100644 index 00000000..2dc7ed8c --- /dev/null +++ b/packages/opentypebb/src/providers/federal_reserve/models/university-of-michigan.ts @@ -0,0 +1,52 @@ +/** + * Federal Reserve University of Michigan Consumer Sentiment Fetcher. + * Uses FRED series: UMCSENT (Sentiment), UMCSENT1 is not available, so we use + * UMCSENT (Consumer Sentiment), CURRCOND (Current Conditions), EXPINF1YR + EXPINF5YR. + * Actual FRED IDs: UMCSENT, UMCSENT (we approximate with available data). + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { UniversityOfMichiganQueryParamsSchema, UniversityOfMichiganDataSchema } from '../../../standard-models/university-of-michigan.js' +import { EmptyDataError } from '../../../core/provider/utils/errors.js' +import { fetchFredMultiSeries, multiSeriesToRecords, getFredApiKey } from '../utils/fred-helpers.js' + +export const FedUMichQueryParamsSchema = UniversityOfMichiganQueryParamsSchema +export type FedUMichQueryParams = z.infer + +// FRED series for Michigan survey data +const SERIES = ['UMCSENT', 'MICH'] +const FIELD_MAP: Record = { + UMCSENT: 'consumer_sentiment', + MICH: 'inflation_expectation_1y', +} + +export class FedUniversityOfMichiganFetcher extends Fetcher { + static override requireCredentials = false + + static override transformQuery(params: Record): FedUMichQueryParams { + return FedUMichQueryParamsSchema.parse(params) + } + + static override async extractData( + query: FedUMichQueryParams, + credentials: Record | null, + ): Promise[]> { + const apiKey = getFredApiKey(credentials) + const dataMap = await fetchFredMultiSeries(SERIES, apiKey, { + startDate: query.start_date, + endDate: query.end_date, + }) + + const records = multiSeriesToRecords(dataMap, FIELD_MAP) + if (records.length === 0) throw new EmptyDataError('No University of Michigan data found.') + return records + } + + static override transformData( + _query: FedUMichQueryParams, + data: Record[], + ) { + return data.map(d => UniversityOfMichiganDataSchema.parse(d)) + } +} diff --git a/packages/opentypebb/src/providers/federal_reserve/utils/fred-helpers.ts b/packages/opentypebb/src/providers/federal_reserve/utils/fred-helpers.ts new file mode 100644 index 00000000..6f7659ae --- /dev/null +++ b/packages/opentypebb/src/providers/federal_reserve/utils/fred-helpers.ts @@ -0,0 +1,221 @@ +/** + * FRED API shared helpers. + * + * Provides reusable functions for fetching data from the + * Federal Reserve Economic Data (FRED) API. + */ + +import { amakeRequest } from '../../../core/provider/utils/helpers.js' +import { EmptyDataError } from '../../../core/provider/utils/errors.js' + +const FRED_BASE = 'https://api.stlouisfed.org/fred' + +export interface FredObservation { + date: string + value: string +} + +export interface FredSeriesInfo { + id: string + title: string + frequency_short: string + units_short: string + seasonal_adjustment_short: string + last_updated: string + notes: string +} + +/** + * Build a FRED API URL with common parameters. + */ +function buildFredUrl( + endpoint: string, + params: Record, + apiKey: string, +): string { + const url = new URL(`${FRED_BASE}/${endpoint}`) + url.searchParams.set('file_type', 'json') + if (apiKey) url.searchParams.set('api_key', apiKey) + for (const [k, v] of Object.entries(params)) { + if (v !== undefined && v !== null && v !== '') { + url.searchParams.set(k, String(v)) + } + } + return url.toString() +} + +/** + * Fetch observations for a single FRED series. + */ +export async function fetchFredSeries( + seriesId: string, + apiKey: string, + opts: { + startDate?: string | null + endDate?: string | null + limit?: number + sortOrder?: 'asc' | 'desc' + frequency?: string + units?: string + } = {}, +): Promise { + const url = buildFredUrl('series/observations', { + series_id: seriesId, + observation_start: opts.startDate ?? undefined, + observation_end: opts.endDate ?? undefined, + limit: opts.limit, + sort_order: opts.sortOrder ?? 'asc', + frequency: opts.frequency, + units: opts.units, + }, apiKey) + + const data = await amakeRequest<{ observations?: FredObservation[] }>(url) + return (data.observations ?? []).filter(o => o.value !== '.') +} + +/** + * Fetch multiple FRED series and merge by date. + * Returns records keyed by date, with each series as a field. + */ +export async function fetchFredMultiSeries( + seriesIds: string[], + apiKey: string, + opts: { + startDate?: string | null + endDate?: string | null + limit?: number + frequency?: string + } = {}, +): Promise>> { + const dataMap: Record> = {} + + for (const seriesId of seriesIds) { + try { + const observations = await fetchFredSeries(seriesId, apiKey, { + startDate: opts.startDate, + endDate: opts.endDate, + limit: opts.limit, + frequency: opts.frequency, + }) + for (const obs of observations) { + const val = parseFloat(obs.value) + if (!dataMap[obs.date]) dataMap[obs.date] = {} + dataMap[obs.date][seriesId] = isNaN(val) ? null : val + } + } catch { + // Skip series that fail + } + } + + return dataMap +} + +/** + * Search FRED series by keyword. + */ +export async function fredSearchApi( + query: string, + apiKey: string, + opts: { limit?: number; offset?: number } = {}, +): Promise { + const url = buildFredUrl('series/search', { + search_text: query, + limit: opts.limit ?? 100, + offset: opts.offset ?? 0, + }, apiKey) + + const data = await amakeRequest<{ seriess?: FredSeriesInfo[] }>(url) + return data.seriess ?? [] +} + +/** + * Fetch a FRED release table. + */ +export async function fredReleaseTableApi( + releaseId: string, + apiKey: string, + opts: { elementId?: number; date?: string } = {}, +): Promise[]> { + const url = buildFredUrl('release/tables', { + release_id: releaseId, + element_id: opts.elementId, + include_observation_values: 'true', + observation_date: opts.date, + }, apiKey) + + const data = await amakeRequest<{ elements?: Record }>(url) + if (!data.elements) return [] + + return Object.values(data.elements).map(el => el as Record) +} + +/** + * Fetch FRED regional/GeoFRED data. + */ +export async function fredRegionalApi( + seriesGroup: string, + apiKey: string, + opts: { + regionType?: string + date?: string + startDate?: string + seasonalAdjustment?: string + units?: string + frequency?: string + transformationCode?: string + } = {}, +): Promise[]> { + const url = buildFredUrl('geofred/series/data', { + series_group: seriesGroup, + region_type: opts.regionType ?? 'state', + date: opts.date, + start_date: opts.startDate, + season: opts.seasonalAdjustment ?? 'SA', + units: opts.units, + frequency: opts.frequency, + transformation: opts.transformationCode, + }, apiKey) + + const data = await amakeRequest<{ meta?: Record; data?: Record }>(url) + if (!data.data) return [] + + // GeoFRED returns { data: { "2024-01-01": [{ region: ..., value: ... }, ...] } } + const results: Record[] = [] + for (const [date, regions] of Object.entries(data.data)) { + if (Array.isArray(regions)) { + for (const region of regions) { + results.push({ date, ...(region as Record) }) + } + } + } + return results +} + +/** + * Convert a FRED multi-series result to an array of flat records. + */ +export function multiSeriesToRecords( + dataMap: Record>, + fieldMap?: Record, +): Record[] { + return Object.entries(dataMap) + .sort(([a], [b]) => a.localeCompare(b)) + .map(([date, values]) => { + const record: Record = { date } + if (fieldMap) { + for (const [seriesId, fieldName] of Object.entries(fieldMap)) { + record[fieldName] = values[seriesId] ?? null + } + } else { + Object.assign(record, values) + } + return record + }) +} + +/** + * Get credentials helper — extracts FRED API key. + */ +export function getFredApiKey(credentials: Record | null): string { + return credentials?.fred_api_key ?? credentials?.api_key ?? '' +} diff --git a/packages/opentypebb/src/providers/oecd/index.ts b/packages/opentypebb/src/providers/oecd/index.ts index cff0f348..a8499969 100644 --- a/packages/opentypebb/src/providers/oecd/index.ts +++ b/packages/opentypebb/src/providers/oecd/index.ts @@ -7,6 +7,12 @@ import { Provider } from '../../core/provider/abstract/provider.js' import { OECDCompositeLeadingIndicatorFetcher } from './models/composite-leading-indicator.js' import { OECDConsumerPriceIndexFetcher } from './models/consumer-price-index.js' import { OECDCountryInterestRatesFetcher } from './models/country-interest-rates.js' +import { OECDGdpForecastFetcher } from './models/gdp-forecast.js' +import { OECDGdpNominalFetcher } from './models/gdp-nominal.js' +import { OECDGdpRealFetcher } from './models/gdp-real.js' +import { OECDSharePriceIndexFetcher } from './models/share-price-index.js' +import { OECDHousePriceIndexFetcher } from './models/house-price-index.js' +import { OECDRetailPricesFetcher } from './models/retail-prices.js' export const oecdProvider = new Provider({ name: 'oecd', @@ -16,5 +22,11 @@ export const oecdProvider = new Provider({ CompositeLeadingIndicator: OECDCompositeLeadingIndicatorFetcher, ConsumerPriceIndex: OECDConsumerPriceIndexFetcher, CountryInterestRates: OECDCountryInterestRatesFetcher, + GdpForecast: OECDGdpForecastFetcher, + GdpNominal: OECDGdpNominalFetcher, + GdpReal: OECDGdpRealFetcher, + SharePriceIndex: OECDSharePriceIndexFetcher, + HousePriceIndex: OECDHousePriceIndexFetcher, + RetailPrices: OECDRetailPricesFetcher, }, }) diff --git a/packages/opentypebb/src/providers/oecd/models/gdp-forecast.ts b/packages/opentypebb/src/providers/oecd/models/gdp-forecast.ts new file mode 100644 index 00000000..d2224841 --- /dev/null +++ b/packages/opentypebb/src/providers/oecd/models/gdp-forecast.ts @@ -0,0 +1,56 @@ +/** + * OECD GDP Forecast Fetcher. + * Uses OECD Economic Outlook dataset. + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { GdpForecastDataSchema } from '../../../standard-models/gdp-forecast.js' +import { EmptyDataError } from '../../../core/provider/utils/errors.js' +import { fetchOecdCsv, resolveCountryCode, periodToDate, CODE_TO_NAME, FREQ_MAP, filterAndSort } from '../utils/oecd-helpers.js' + +export const OECDGdpForecastQueryParamsSchema = z.object({ + country: z.string().default('united_states'), + start_date: z.string().nullable().default(null), + end_date: z.string().nullable().default(null), + frequency: z.enum(['annual', 'quarter']).default('annual'), +}).passthrough() + +export type OECDGdpForecastQueryParams = z.infer + +export class OECDGdpForecastFetcher extends Fetcher { + static override requireCredentials = false + + static override transformQuery(params: Record): OECDGdpForecastQueryParams { + return OECDGdpForecastQueryParamsSchema.parse(params) + } + + static override async extractData( + query: OECDGdpForecastQueryParams, + _credentials: Record | null, + ): Promise[]> { + const cc = resolveCountryCode(query.country) + const freq = FREQ_MAP[query.frequency] ?? 'A' + const rows = await fetchOecdCsv( + 'OECD.SDD.NAD,DSD_NAMAIN1@DF_TABLE1_EXPENDITURE_HCPC,1.0', + `${cc}.${freq}.S1.S1.B1GQ._Z._Z._Z.V.GY.`, + ) + + return rows + .filter(r => r.OBS_VALUE && r.OBS_VALUE !== '') + .map(r => ({ + date: periodToDate(r.TIME_PERIOD ?? ''), + country: CODE_TO_NAME[r.REF_AREA] ?? r.REF_AREA ?? query.country, + value: parseFloat(r.OBS_VALUE), + })) + } + + static override transformData( + query: OECDGdpForecastQueryParams, + data: Record[], + ) { + if (data.length === 0) throw new EmptyDataError() + return filterAndSort(data, query.start_date, query.end_date) + .map(d => GdpForecastDataSchema.parse(d)) + } +} diff --git a/packages/opentypebb/src/providers/oecd/models/gdp-nominal.ts b/packages/opentypebb/src/providers/oecd/models/gdp-nominal.ts new file mode 100644 index 00000000..73641021 --- /dev/null +++ b/packages/opentypebb/src/providers/oecd/models/gdp-nominal.ts @@ -0,0 +1,55 @@ +/** + * OECD GDP Nominal Fetcher. + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { GdpNominalDataSchema } from '../../../standard-models/gdp-nominal.js' +import { EmptyDataError } from '../../../core/provider/utils/errors.js' +import { fetchOecdCsv, resolveCountryCode, periodToDate, CODE_TO_NAME, FREQ_MAP, filterAndSort } from '../utils/oecd-helpers.js' + +export const OECDGdpNominalQueryParamsSchema = z.object({ + country: z.string().default('united_states'), + start_date: z.string().nullable().default(null), + end_date: z.string().nullable().default(null), + frequency: z.enum(['annual', 'quarter']).default('annual'), +}).passthrough() + +export type OECDGdpNominalQueryParams = z.infer + +export class OECDGdpNominalFetcher extends Fetcher { + static override requireCredentials = false + + static override transformQuery(params: Record): OECDGdpNominalQueryParams { + return OECDGdpNominalQueryParamsSchema.parse(params) + } + + static override async extractData( + query: OECDGdpNominalQueryParams, + _credentials: Record | null, + ): Promise[]> { + const cc = resolveCountryCode(query.country) + const freq = FREQ_MAP[query.frequency] ?? 'A' + const rows = await fetchOecdCsv( + 'OECD.SDD.NAD,DSD_NAMAIN1@DF_TABLE1_EXPENDITURE_HCPC,1.0', + `${cc}.${freq}.S1.S1.B1GQ._Z._Z._Z.V.N.`, + ) + + return rows + .filter(r => r.OBS_VALUE && r.OBS_VALUE !== '') + .map(r => ({ + date: periodToDate(r.TIME_PERIOD ?? ''), + country: CODE_TO_NAME[r.REF_AREA] ?? r.REF_AREA ?? query.country, + value: parseFloat(r.OBS_VALUE), + })) + } + + static override transformData( + query: OECDGdpNominalQueryParams, + data: Record[], + ) { + if (data.length === 0) throw new EmptyDataError() + return filterAndSort(data, query.start_date, query.end_date) + .map(d => GdpNominalDataSchema.parse(d)) + } +} diff --git a/packages/opentypebb/src/providers/oecd/models/gdp-real.ts b/packages/opentypebb/src/providers/oecd/models/gdp-real.ts new file mode 100644 index 00000000..c90bd565 --- /dev/null +++ b/packages/opentypebb/src/providers/oecd/models/gdp-real.ts @@ -0,0 +1,55 @@ +/** + * OECD GDP Real Fetcher. + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { GdpRealDataSchema } from '../../../standard-models/gdp-real.js' +import { EmptyDataError } from '../../../core/provider/utils/errors.js' +import { fetchOecdCsv, resolveCountryCode, periodToDate, CODE_TO_NAME, FREQ_MAP, filterAndSort } from '../utils/oecd-helpers.js' + +export const OECDGdpRealQueryParamsSchema = z.object({ + country: z.string().default('united_states'), + start_date: z.string().nullable().default(null), + end_date: z.string().nullable().default(null), + frequency: z.enum(['annual', 'quarter']).default('annual'), +}).passthrough() + +export type OECDGdpRealQueryParams = z.infer + +export class OECDGdpRealFetcher extends Fetcher { + static override requireCredentials = false + + static override transformQuery(params: Record): OECDGdpRealQueryParams { + return OECDGdpRealQueryParamsSchema.parse(params) + } + + static override async extractData( + query: OECDGdpRealQueryParams, + _credentials: Record | null, + ): Promise[]> { + const cc = resolveCountryCode(query.country) + const freq = FREQ_MAP[query.frequency] ?? 'A' + const rows = await fetchOecdCsv( + 'OECD.SDD.NAD,DSD_NAMAIN1@DF_TABLE1_EXPENDITURE_HCPC,1.0', + `${cc}.${freq}.S1.S1.B1GQ._Z._Z._Z.L.N.`, + ) + + return rows + .filter(r => r.OBS_VALUE && r.OBS_VALUE !== '') + .map(r => ({ + date: periodToDate(r.TIME_PERIOD ?? ''), + country: CODE_TO_NAME[r.REF_AREA] ?? r.REF_AREA ?? query.country, + value: parseFloat(r.OBS_VALUE), + })) + } + + static override transformData( + query: OECDGdpRealQueryParams, + data: Record[], + ) { + if (data.length === 0) throw new EmptyDataError() + return filterAndSort(data, query.start_date, query.end_date) + .map(d => GdpRealDataSchema.parse(d)) + } +} diff --git a/packages/opentypebb/src/providers/oecd/models/house-price-index.ts b/packages/opentypebb/src/providers/oecd/models/house-price-index.ts new file mode 100644 index 00000000..44285115 --- /dev/null +++ b/packages/opentypebb/src/providers/oecd/models/house-price-index.ts @@ -0,0 +1,55 @@ +/** + * OECD House Price Index Fetcher. + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { HousePriceIndexDataSchema } from '../../../standard-models/house-price-index.js' +import { EmptyDataError } from '../../../core/provider/utils/errors.js' +import { fetchOecdCsv, resolveCountryCode, periodToDate, CODE_TO_NAME, FREQ_MAP, filterAndSort } from '../utils/oecd-helpers.js' + +export const OECDHousePriceIndexQueryParamsSchema = z.object({ + country: z.string().default('united_states'), + frequency: z.enum(['annual', 'quarter', 'monthly']).default('quarter'), + start_date: z.string().nullable().default(null), + end_date: z.string().nullable().default(null), +}).passthrough() + +export type OECDHousePriceIndexQueryParams = z.infer + +export class OECDHousePriceIndexFetcher extends Fetcher { + static override requireCredentials = false + + static override transformQuery(params: Record): OECDHousePriceIndexQueryParams { + return OECDHousePriceIndexQueryParamsSchema.parse(params) + } + + static override async extractData( + query: OECDHousePriceIndexQueryParams, + _credentials: Record | null, + ): Promise[]> { + const cc = resolveCountryCode(query.country) + const freq = FREQ_MAP[query.frequency] ?? 'Q' + const rows = await fetchOecdCsv( + 'OECD.SDD.TPS,DSD_AN_HOUSE_PRICES@DF_HOUSE_PRICES,1.0', + `${cc}.${freq}.RHP._T.IX.`, + ) + + return rows + .filter(r => r.OBS_VALUE && r.OBS_VALUE !== '') + .map(r => ({ + date: periodToDate(r.TIME_PERIOD ?? ''), + country: CODE_TO_NAME[r.REF_AREA] ?? r.REF_AREA ?? query.country, + value: parseFloat(r.OBS_VALUE), + })) + } + + static override transformData( + query: OECDHousePriceIndexQueryParams, + data: Record[], + ) { + if (data.length === 0) throw new EmptyDataError() + return filterAndSort(data, query.start_date, query.end_date) + .map(d => HousePriceIndexDataSchema.parse(d)) + } +} diff --git a/packages/opentypebb/src/providers/oecd/models/retail-prices.ts b/packages/opentypebb/src/providers/oecd/models/retail-prices.ts new file mode 100644 index 00000000..2200bfb9 --- /dev/null +++ b/packages/opentypebb/src/providers/oecd/models/retail-prices.ts @@ -0,0 +1,56 @@ +/** + * OECD Retail Prices Fetcher. + * Uses OECD MEI Prices dataset. + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { RetailPricesDataSchema } from '../../../standard-models/retail-prices.js' +import { EmptyDataError } from '../../../core/provider/utils/errors.js' +import { fetchOecdCsv, resolveCountryCode, periodToDate, CODE_TO_NAME, FREQ_MAP, filterAndSort } from '../utils/oecd-helpers.js' + +export const OECDRetailPricesQueryParamsSchema = z.object({ + country: z.string().default('united_states'), + frequency: z.enum(['annual', 'quarter', 'monthly']).default('monthly'), + start_date: z.string().nullable().default(null), + end_date: z.string().nullable().default(null), +}).passthrough() + +export type OECDRetailPricesQueryParams = z.infer + +export class OECDRetailPricesFetcher extends Fetcher { + static override requireCredentials = false + + static override transformQuery(params: Record): OECDRetailPricesQueryParams { + return OECDRetailPricesQueryParamsSchema.parse(params) + } + + static override async extractData( + query: OECDRetailPricesQueryParams, + _credentials: Record | null, + ): Promise[]> { + const cc = resolveCountryCode(query.country) + const freq = FREQ_MAP[query.frequency] ?? 'M' + const rows = await fetchOecdCsv( + 'OECD.SDD.TPS,DSD_PRICES@DF_PRICES_ALL,1.0', + `${cc}.${freq}.N.CPI.PA._T.N.`, + ) + + return rows + .filter(r => r.OBS_VALUE && r.OBS_VALUE !== '') + .map(r => ({ + date: periodToDate(r.TIME_PERIOD ?? ''), + country: CODE_TO_NAME[r.REF_AREA] ?? r.REF_AREA ?? query.country, + value: parseFloat(r.OBS_VALUE), + })) + } + + static override transformData( + query: OECDRetailPricesQueryParams, + data: Record[], + ) { + if (data.length === 0) throw new EmptyDataError() + return filterAndSort(data, query.start_date, query.end_date) + .map(d => RetailPricesDataSchema.parse(d)) + } +} diff --git a/packages/opentypebb/src/providers/oecd/models/share-price-index.ts b/packages/opentypebb/src/providers/oecd/models/share-price-index.ts new file mode 100644 index 00000000..cd0d2c11 --- /dev/null +++ b/packages/opentypebb/src/providers/oecd/models/share-price-index.ts @@ -0,0 +1,56 @@ +/** + * OECD Share Price Index Fetcher. + * Uses OECD Main Economic Indicators (MEI) dataset. + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { SharePriceIndexDataSchema } from '../../../standard-models/share-price-index.js' +import { EmptyDataError } from '../../../core/provider/utils/errors.js' +import { fetchOecdCsv, resolveCountryCode, periodToDate, CODE_TO_NAME, FREQ_MAP, filterAndSort } from '../utils/oecd-helpers.js' + +export const OECDSharePriceIndexQueryParamsSchema = z.object({ + country: z.string().default('united_states'), + frequency: z.enum(['annual', 'quarter', 'monthly']).default('monthly'), + start_date: z.string().nullable().default(null), + end_date: z.string().nullable().default(null), +}).passthrough() + +export type OECDSharePriceIndexQueryParams = z.infer + +export class OECDSharePriceIndexFetcher extends Fetcher { + static override requireCredentials = false + + static override transformQuery(params: Record): OECDSharePriceIndexQueryParams { + return OECDSharePriceIndexQueryParamsSchema.parse(params) + } + + static override async extractData( + query: OECDSharePriceIndexQueryParams, + _credentials: Record | null, + ): Promise[]> { + const cc = resolveCountryCode(query.country) + const freq = FREQ_MAP[query.frequency] ?? 'M' + const rows = await fetchOecdCsv( + 'OECD.SDD.STES,DSD_KEI@DF_KEI,4.0', + `${cc}.${freq}.SHARE._Z.IX._T.`, + ) + + return rows + .filter(r => r.OBS_VALUE && r.OBS_VALUE !== '') + .map(r => ({ + date: periodToDate(r.TIME_PERIOD ?? ''), + country: CODE_TO_NAME[r.REF_AREA] ?? r.REF_AREA ?? query.country, + value: parseFloat(r.OBS_VALUE), + })) + } + + static override transformData( + query: OECDSharePriceIndexQueryParams, + data: Record[], + ) { + if (data.length === 0) throw new EmptyDataError() + return filterAndSort(data, query.start_date, query.end_date) + .map(d => SharePriceIndexDataSchema.parse(d)) + } +} diff --git a/packages/opentypebb/src/providers/oecd/utils/oecd-helpers.ts b/packages/opentypebb/src/providers/oecd/utils/oecd-helpers.ts new file mode 100644 index 00000000..8db496b1 --- /dev/null +++ b/packages/opentypebb/src/providers/oecd/utils/oecd-helpers.ts @@ -0,0 +1,103 @@ +/** + * OECD SDMX API shared helpers. + * Extracted from the existing CPI/CLI/InterestRates fetchers to avoid repetition. + */ + +import { nativeFetch } from '../../../core/provider/utils/helpers.js' +import { EmptyDataError } from '../../../core/provider/utils/errors.js' + +export const COUNTRY_MAP: Record = { + united_states: 'USA', united_kingdom: 'GBR', japan: 'JPN', germany: 'DEU', + france: 'FRA', italy: 'ITA', canada: 'CAN', australia: 'AUS', + south_korea: 'KOR', mexico: 'MEX', brazil: 'BRA', china: 'CHN', + india: 'IND', turkey: 'TUR', south_africa: 'ZAF', russia: 'RUS', + spain: 'ESP', netherlands: 'NLD', switzerland: 'CHE', sweden: 'SWE', + norway: 'NOR', denmark: 'DNK', finland: 'FIN', belgium: 'BEL', + austria: 'AUT', ireland: 'IRL', portugal: 'PRT', greece: 'GRC', + new_zealand: 'NZL', israel: 'ISR', poland: 'POL', czech_republic: 'CZE', + hungary: 'HUN', colombia: 'COL', chile: 'CHL', indonesia: 'IDN', +} + +export const CODE_TO_NAME: Record = Object.fromEntries( + Object.entries(COUNTRY_MAP).map(([k, v]) => [v, k.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase())]), +) + +export const FREQ_MAP: Record = { annual: 'A', quarter: 'Q', monthly: 'M' } + +/** + * Parse simple CSV text into rows. + */ +export function parseCSV(text: string): Record[] { + const lines = text.trim().split('\n') + if (lines.length < 2) return [] + const headers = lines[0].split(',') + return lines.slice(1).map(line => { + const values = line.split(',') + const row: Record = {} + headers.forEach((h, i) => { row[h.trim()] = values[i]?.trim() ?? '' }) + return row + }) +} + +/** + * Convert OECD period format to date string. + * "2024" → "2024-01-01", "2024-01" → "2024-01-01", "2024-Q1" → "2024-01-01" + */ +export function periodToDate(period: string): string { + if (period.includes('-Q')) { + const [year, q] = period.split('-Q') + const month = String((parseInt(q) - 1) * 3 + 1).padStart(2, '0') + return `${year}-${month}-01` + } + if (period.length === 7) return period + '-01' + if (period.length === 4) return period + '-01-01' + return period +} + +/** + * Fetch data from OECD SDMX REST API in CSV format. + */ +export async function fetchOecdCsv( + dataflow: string, + dimensions: string, +): Promise[]> { + const url = + `https://sdmx.oecd.org/public/rest/data/${dataflow}` + + `/${dimensions}` + + `?dimensionAtObservation=TIME_PERIOD&detail=dataonly&format=csvfile` + + try { + const resp = await nativeFetch(url, { + headers: { Accept: 'application/vnd.sdmx.data+csv; charset=utf-8' }, + timeoutMs: 30000, + }) + if (resp.status !== 200) throw new EmptyDataError(`OECD API returned ${resp.status}`) + const rows = parseCSV(resp.text) + if (!rows.length) throw new EmptyDataError() + return rows + } catch (err) { + if (err instanceof EmptyDataError) throw err + throw new EmptyDataError(`Failed to fetch OECD data: ${err}`) + } +} + +/** + * Resolve country name to OECD 3-letter code. + */ +export function resolveCountryCode(country: string): string { + return COUNTRY_MAP[country] ?? country.toUpperCase() +} + +/** + * Apply date filters and sort results. + */ +export function filterAndSort( + data: T[], + startDate?: string | null, + endDate?: string | null, +): T[] { + let filtered = data + if (startDate) filtered = filtered.filter(d => String(d.date) >= startDate) + if (endDate) filtered = filtered.filter(d => String(d.date) <= endDate) + return filtered.sort((a, b) => String(a.date).localeCompare(String(b.date))) +} diff --git a/packages/opentypebb/src/providers/stub/index.ts b/packages/opentypebb/src/providers/stub/index.ts new file mode 100644 index 00000000..0877a7b2 --- /dev/null +++ b/packages/opentypebb/src/providers/stub/index.ts @@ -0,0 +1,27 @@ +/** + * Stub Provider Module. + * Contains placeholder fetchers for endpoints that don't yet have a reliable public data source. + * These register the models in the registry so routes can be created, but always throw EmptyDataError. + */ + +import { Provider } from '../../core/provider/abstract/provider.js' + +import { + StubPortInfoFetcher, + StubPortVolumeFetcher, + StubChokepointInfoFetcher, + StubChokepointVolumeFetcher, +} from './models/shipping-stubs.js' + +export const stubProvider = new Provider({ + name: 'stub', + website: '', + description: 'Placeholder provider for endpoints awaiting a public data source.', + fetcherDict: { + PortInfo: StubPortInfoFetcher, + PortVolume: StubPortVolumeFetcher, + ChokepointInfo: StubChokepointInfoFetcher, + ChokepointVolume: StubChokepointVolumeFetcher, + }, + reprName: 'Stub', +}) diff --git a/packages/opentypebb/src/providers/stub/models/shipping-stubs.ts b/packages/opentypebb/src/providers/stub/models/shipping-stubs.ts new file mode 100644 index 00000000..51790042 --- /dev/null +++ b/packages/opentypebb/src/providers/stub/models/shipping-stubs.ts @@ -0,0 +1,87 @@ +/** + * Shipping Stub Fetchers. + * + * These register the shipping endpoints with proper schemas but always throw + * EmptyDataError. Once a reliable public API for shipping data is found, + * replace with real fetchers. + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { PortInfoQueryParamsSchema } from '../../../standard-models/port-info.js' +import { PortVolumeQueryParamsSchema } from '../../../standard-models/port-volume.js' +import { ChokepointInfoQueryParamsSchema } from '../../../standard-models/chokepoint-info.js' +import { ChokepointVolumeQueryParamsSchema } from '../../../standard-models/chokepoint-volume.js' +import { EmptyDataError } from '../../../core/provider/utils/errors.js' + +// --- Port Info --- + +export class StubPortInfoFetcher extends Fetcher { + static override requireCredentials = false + + static override transformQuery(params: Record) { + return PortInfoQueryParamsSchema.parse(params) + } + + static override async extractData(): Promise[]> { + throw new EmptyDataError('Port info data source not yet implemented. No reliable public API available.') + } + + static override transformData(_query: unknown, data: Record[]) { + return data + } +} + +// --- Port Volume --- + +export class StubPortVolumeFetcher extends Fetcher { + static override requireCredentials = false + + static override transformQuery(params: Record) { + return PortVolumeQueryParamsSchema.parse(params) + } + + static override async extractData(): Promise[]> { + throw new EmptyDataError('Port volume data source not yet implemented. No reliable public API available.') + } + + static override transformData(_query: unknown, data: Record[]) { + return data + } +} + +// --- Chokepoint Info --- + +export class StubChokepointInfoFetcher extends Fetcher { + static override requireCredentials = false + + static override transformQuery(params: Record) { + return ChokepointInfoQueryParamsSchema.parse(params) + } + + static override async extractData(): Promise[]> { + throw new EmptyDataError('Chokepoint info data source not yet implemented. No reliable public API available.') + } + + static override transformData(_query: unknown, data: Record[]) { + return data + } +} + +// --- Chokepoint Volume --- + +export class StubChokepointVolumeFetcher extends Fetcher { + static override requireCredentials = false + + static override transformQuery(params: Record) { + return ChokepointVolumeQueryParamsSchema.parse(params) + } + + static override async extractData(): Promise[]> { + throw new EmptyDataError('Chokepoint volume data source not yet implemented. No reliable public API available.') + } + + static override transformData(_query: unknown, data: Record[]) { + return data + } +} diff --git a/packages/opentypebb/src/providers/yfinance/index.ts b/packages/opentypebb/src/providers/yfinance/index.ts index b6830078..86ae2c9d 100644 --- a/packages/opentypebb/src/providers/yfinance/index.ts +++ b/packages/opentypebb/src/providers/yfinance/index.ts @@ -37,6 +37,7 @@ import { YFinanceEtfInfoFetcher } from './models/etf-info.js' import { YFinanceEquityScreenerFetcher } from './models/equity-screener.js' import { YFinanceFuturesCurveFetcher } from './models/futures-curve.js' import { YFinanceOptionsChainsFetcher } from './models/options-chains.js' +import { YFinanceCommoditySpotPriceFetcher } from './models/commodity-spot-price.js' export const yfinanceProvider = new Provider({ name: 'yfinance', @@ -77,6 +78,7 @@ export const yfinanceProvider = new Provider({ EquityScreener: YFinanceEquityScreenerFetcher, FuturesCurve: YFinanceFuturesCurveFetcher, OptionsChains: YFinanceOptionsChainsFetcher, + CommoditySpotPrice: YFinanceCommoditySpotPriceFetcher, }, reprName: 'Yahoo Finance', }) diff --git a/packages/opentypebb/src/providers/yfinance/models/commodity-spot-price.ts b/packages/opentypebb/src/providers/yfinance/models/commodity-spot-price.ts new file mode 100644 index 00000000..f0b984a8 --- /dev/null +++ b/packages/opentypebb/src/providers/yfinance/models/commodity-spot-price.ts @@ -0,0 +1,99 @@ +/** + * YFinance Commodity Spot Price Fetcher. + * Uses Yahoo Finance futures symbols (GC=F for gold, CL=F for crude, etc.) + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { CommoditySpotPriceQueryParamsSchema, CommoditySpotPriceDataSchema } from '../../../standard-models/commodity-spot-price.js' +import { EmptyDataError } from '../../../core/provider/utils/errors.js' +import { getHistoricalData } from '../utils/helpers.js' + +export const YFinanceCommoditySpotPriceQueryParamsSchema = CommoditySpotPriceQueryParamsSchema +export type YFinanceCommoditySpotPriceQueryParams = z.infer + +// Well-known commodity futures symbols +const COMMODITY_MAP: Record = { + gold: 'GC=F', + silver: 'SI=F', + platinum: 'PL=F', + palladium: 'PA=F', + copper: 'HG=F', + crude_oil: 'CL=F', + wti: 'CL=F', + brent: 'BZ=F', + natural_gas: 'NG=F', + heating_oil: 'HO=F', + gasoline: 'RB=F', + corn: 'ZC=F', + wheat: 'ZW=F', + soybeans: 'ZS=F', + sugar: 'SB=F', + coffee: 'KC=F', + cocoa: 'CC=F', + cotton: 'CT=F', + lumber: 'LBS=F', + live_cattle: 'LE=F', + lean_hogs: 'HE=F', +} + +function resolveSymbol(sym: string): string { + const lower = sym.toLowerCase().trim() + return COMMODITY_MAP[lower] ?? sym.trim() +} + +export class YFinanceCommoditySpotPriceFetcher extends Fetcher { + static override requireCredentials = false + + static override transformQuery(params: Record): YFinanceCommoditySpotPriceQueryParams { + const now = new Date() + if (!params.start_date) { + const oneYearAgo = new Date(now) + oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1) + params.start_date = oneYearAgo.toISOString().slice(0, 10) + } + if (!params.end_date) { + params.end_date = now.toISOString().slice(0, 10) + } + return YFinanceCommoditySpotPriceQueryParamsSchema.parse(params) + } + + static override async extractData( + query: YFinanceCommoditySpotPriceQueryParams, + _credentials: Record | null, + ): Promise[]> { + const symbols = query.symbol.split(',').map(s => resolveSymbol(s)).filter(Boolean) + + const allData: Record[] = [] + const results = await Promise.allSettled( + symbols.map(async (sym) => { + const data = await getHistoricalData(sym, { + startDate: query.start_date ?? undefined, + endDate: query.end_date ?? undefined, + interval: '1d', + }) + return data.map((d: Record) => ({ ...d, symbol: sym })) + }), + ) + + for (const result of results) { + if (result.status === 'fulfilled') { + allData.push(...result.value) + } + } + + if (allData.length === 0) { + throw new EmptyDataError('No commodity spot price data found.') + } + return allData + } + + static override transformData( + _query: YFinanceCommoditySpotPriceQueryParams, + data: Record[], + ) { + return data + .sort((a, b) => String(a.date).localeCompare(String(b.date))) + .map(d => CommoditySpotPriceDataSchema.parse(d)) + } +} diff --git a/packages/opentypebb/src/standard-models/bls-search.ts b/packages/opentypebb/src/standard-models/bls-search.ts new file mode 100644 index 00000000..85ed0c54 --- /dev/null +++ b/packages/opentypebb/src/standard-models/bls-search.ts @@ -0,0 +1,20 @@ +/** + * BLS Search Standard Model. + */ + +import { z } from 'zod' + +export const BlsSearchQueryParamsSchema = z.object({ + query: z.string().describe('Search query for BLS series.'), + limit: z.number().default(50).describe('Maximum number of results.'), +}).passthrough() + +export type BlsSearchQueryParams = z.infer + +export const BlsSearchDataSchema = z.object({ + series_id: z.string().describe('BLS series ID.'), + title: z.string().nullable().default(null).describe('Series title.'), + survey_abbreviation: z.string().nullable().default(null).describe('Survey abbreviation.'), +}).passthrough() + +export type BlsSearchData = z.infer diff --git a/packages/opentypebb/src/standard-models/bls-series.ts b/packages/opentypebb/src/standard-models/bls-series.ts new file mode 100644 index 00000000..a9250f71 --- /dev/null +++ b/packages/opentypebb/src/standard-models/bls-series.ts @@ -0,0 +1,22 @@ +/** + * BLS Series Standard Model. + */ + +import { z } from 'zod' + +export const BlsSeriesQueryParamsSchema = z.object({ + symbol: z.string().describe('BLS series ID(s), comma-separated for multiple.'), + start_date: z.string().nullable().default(null).describe('Start date in YYYY-MM-DD.'), + end_date: z.string().nullable().default(null).describe('End date in YYYY-MM-DD.'), +}).passthrough() + +export type BlsSeriesQueryParams = z.infer + +export const BlsSeriesDataSchema = z.object({ + date: z.string().describe('Observation date.'), + series_id: z.string().nullable().default(null).describe('BLS series identifier.'), + value: z.number().nullable().default(null).describe('Observation value.'), + period: z.string().nullable().default(null).describe('BLS period code (e.g., M01).'), +}).passthrough() + +export type BlsSeriesData = z.infer diff --git a/packages/opentypebb/src/standard-models/chokepoint-info.ts b/packages/opentypebb/src/standard-models/chokepoint-info.ts new file mode 100644 index 00000000..f6e2392b --- /dev/null +++ b/packages/opentypebb/src/standard-models/chokepoint-info.ts @@ -0,0 +1,21 @@ +/** + * Chokepoint Info Standard Model (Stub). + */ + +import { z } from 'zod' + +export const ChokepointInfoQueryParamsSchema = z.object({ + chokepoint: z.string().default('').describe('Chokepoint name (e.g., "Suez Canal", "Strait of Hormuz").'), +}).passthrough() + +export type ChokepointInfoQueryParams = z.infer + +export const ChokepointInfoDataSchema = z.object({ + name: z.string().nullable().default(null).describe('Chokepoint name.'), + region: z.string().nullable().default(null).describe('Geographic region.'), + latitude: z.number().nullable().default(null).describe('Latitude.'), + longitude: z.number().nullable().default(null).describe('Longitude.'), + description: z.string().nullable().default(null).describe('Description.'), +}).passthrough() + +export type ChokepointInfoData = z.infer diff --git a/packages/opentypebb/src/standard-models/chokepoint-volume.ts b/packages/opentypebb/src/standard-models/chokepoint-volume.ts new file mode 100644 index 00000000..23a67208 --- /dev/null +++ b/packages/opentypebb/src/standard-models/chokepoint-volume.ts @@ -0,0 +1,22 @@ +/** + * Chokepoint Volume Standard Model (Stub). + */ + +import { z } from 'zod' + +export const ChokepointVolumeQueryParamsSchema = z.object({ + chokepoint: z.string().default('').describe('Chokepoint name.'), + start_date: z.string().nullable().default(null).describe('Start date in YYYY-MM-DD.'), + end_date: z.string().nullable().default(null).describe('End date in YYYY-MM-DD.'), +}).passthrough() + +export type ChokepointVolumeQueryParams = z.infer + +export const ChokepointVolumeDataSchema = z.object({ + date: z.string().describe('Observation date.'), + chokepoint: z.string().nullable().default(null).describe('Chokepoint name.'), + volume: z.number().nullable().default(null).describe('Transit volume.'), + unit: z.string().nullable().default(null).describe('Unit of measurement.'), +}).passthrough() + +export type ChokepointVolumeData = z.infer diff --git a/packages/opentypebb/src/standard-models/commodity-spot-price.ts b/packages/opentypebb/src/standard-models/commodity-spot-price.ts new file mode 100644 index 00000000..79fd4fc9 --- /dev/null +++ b/packages/opentypebb/src/standard-models/commodity-spot-price.ts @@ -0,0 +1,25 @@ +/** + * Commodity Spot Price Standard Model. + */ + +import { z } from 'zod' + +export const CommoditySpotPriceQueryParamsSchema = z.object({ + symbol: z.string().describe('Commodity futures symbol(s), comma-separated (e.g., "GC=F,CL=F,SI=F").'), + start_date: z.string().nullable().default(null).describe('Start date in YYYY-MM-DD.'), + end_date: z.string().nullable().default(null).describe('End date in YYYY-MM-DD.'), +}).passthrough() + +export type CommoditySpotPriceQueryParams = z.infer + +export const CommoditySpotPriceDataSchema = z.object({ + date: z.string().describe('Trade date.'), + symbol: z.string().nullable().default(null).describe('Commodity symbol.'), + open: z.number().nullable().default(null).describe('Opening price.'), + high: z.number().nullable().default(null).describe('High price.'), + low: z.number().nullable().default(null).describe('Low price.'), + close: z.number().nullable().default(null).describe('Closing price.'), + volume: z.number().nullable().default(null).describe('Trade volume.'), +}).passthrough() + +export type CommoditySpotPriceData = z.infer diff --git a/packages/opentypebb/src/standard-models/economic-conditions-chicago.ts b/packages/opentypebb/src/standard-models/economic-conditions-chicago.ts new file mode 100644 index 00000000..1f1630c0 --- /dev/null +++ b/packages/opentypebb/src/standard-models/economic-conditions-chicago.ts @@ -0,0 +1,21 @@ +/** + * Chicago Fed National Activity Index Standard Model. + * Maps to: openbb_core/provider/standard_models/economic_conditions_chicago.py + */ + +import { z } from 'zod' + +export const EconomicConditionsChicagoQueryParamsSchema = z.object({ + start_date: z.string().nullable().default(null).describe('Start date in YYYY-MM-DD.'), + end_date: z.string().nullable().default(null).describe('End date in YYYY-MM-DD.'), +}).passthrough() + +export type EconomicConditionsChicagoQueryParams = z.infer + +export const EconomicConditionsChicagoDataSchema = z.object({ + date: z.string().describe('The date of the data.'), + cfnai: z.number().nullable().default(null).describe('Chicago Fed National Activity Index.'), + cfnai_ma3: z.number().nullable().default(null).describe('CFNAI 3-month moving average.'), +}).passthrough() + +export type EconomicConditionsChicagoData = z.infer diff --git a/packages/opentypebb/src/standard-models/fomc-documents.ts b/packages/opentypebb/src/standard-models/fomc-documents.ts new file mode 100644 index 00000000..2368aaf5 --- /dev/null +++ b/packages/opentypebb/src/standard-models/fomc-documents.ts @@ -0,0 +1,22 @@ +/** + * FOMC Documents Standard Model. + * Maps to: openbb_core/provider/standard_models/fomc_documents.py + */ + +import { z } from 'zod' + +export const FomcDocumentsQueryParamsSchema = z.object({ + start_date: z.string().nullable().default(null).describe('Start date in YYYY-MM-DD.'), + end_date: z.string().nullable().default(null).describe('End date in YYYY-MM-DD.'), +}).passthrough() + +export type FomcDocumentsQueryParams = z.infer + +export const FomcDocumentsDataSchema = z.object({ + date: z.string().describe('Meeting or document date.'), + title: z.string().nullable().default(null).describe('Document title.'), + type: z.string().nullable().default(null).describe('Document type (statement, minutes, etc).'), + url: z.string().nullable().default(null).describe('URL to the document.'), +}).passthrough() + +export type FomcDocumentsData = z.infer diff --git a/packages/opentypebb/src/standard-models/fred-regional.ts b/packages/opentypebb/src/standard-models/fred-regional.ts new file mode 100644 index 00000000..b7b805d8 --- /dev/null +++ b/packages/opentypebb/src/standard-models/fred-regional.ts @@ -0,0 +1,25 @@ +/** + * FRED Regional / GeoFRED Standard Model. + * Maps to: openbb_core/provider/standard_models/fred_regional.py + */ + +import { z } from 'zod' + +export const FredRegionalQueryParamsSchema = z.object({ + symbol: z.string().describe('FRED series group ID for GeoFRED data.'), + region_type: z.string().default('state').describe('Region type: state, msa, county, etc.'), + date: z.string().nullable().default(null).describe('Observation date in YYYY-MM-DD.'), + start_date: z.string().nullable().default(null).describe('Start date for data range.'), + frequency: z.string().nullable().default(null).describe('Data frequency.'), +}).passthrough() + +export type FredRegionalQueryParams = z.infer + +export const FredRegionalDataSchema = z.object({ + date: z.string().describe('Observation date.'), + region: z.string().nullable().default(null).describe('Region name.'), + code: z.string().nullable().default(null).describe('Region code.'), + value: z.number().nullable().default(null).describe('Observation value.'), +}).passthrough() + +export type FredRegionalData = z.infer diff --git a/packages/opentypebb/src/standard-models/fred-release-table.ts b/packages/opentypebb/src/standard-models/fred-release-table.ts new file mode 100644 index 00000000..c78dada4 --- /dev/null +++ b/packages/opentypebb/src/standard-models/fred-release-table.ts @@ -0,0 +1,23 @@ +/** + * FRED Release Table Standard Model. + * Maps to: openbb_core/provider/standard_models/fred_release_table.py + */ + +import { z } from 'zod' + +export const FredReleaseTableQueryParamsSchema = z.object({ + release_id: z.string().describe('FRED release ID.'), + element_id: z.number().nullable().default(null).describe('Element ID within release.'), + date: z.string().nullable().default(null).describe('Observation date in YYYY-MM-DD.'), +}).passthrough() + +export type FredReleaseTableQueryParams = z.infer + +export const FredReleaseTableDataSchema = z.object({ + element_id: z.number().nullable().default(null).describe('Element ID.'), + name: z.string().nullable().default(null).describe('Element name.'), + level: z.string().nullable().default(null).describe('Element level.'), + value: z.string().nullable().default(null).describe('Observation value.'), +}).passthrough() + +export type FredReleaseTableData = z.infer diff --git a/packages/opentypebb/src/standard-models/fred-search.ts b/packages/opentypebb/src/standard-models/fred-search.ts new file mode 100644 index 00000000..ccaa918c --- /dev/null +++ b/packages/opentypebb/src/standard-models/fred-search.ts @@ -0,0 +1,25 @@ +/** + * FRED Search Standard Model. + * Maps to: openbb_core/provider/standard_models/fred_search.py + */ + +import { z } from 'zod' + +export const FredSearchQueryParamsSchema = z.object({ + query: z.string().describe('Search query for FRED series.'), + limit: z.number().default(100).describe('Maximum number of results.'), +}).passthrough() + +export type FredSearchQueryParams = z.infer + +export const FredSearchDataSchema = z.object({ + series_id: z.string().describe('FRED series ID.'), + title: z.string().describe('Series title.'), + frequency: z.string().nullable().default(null).describe('Data frequency.'), + units: z.string().nullable().default(null).describe('Data units.'), + seasonal_adjustment: z.string().nullable().default(null).describe('Seasonal adjustment.'), + last_updated: z.string().nullable().default(null).describe('Last updated timestamp.'), + notes: z.string().nullable().default(null).describe('Series notes.'), +}).passthrough() + +export type FredSearchData = z.infer diff --git a/packages/opentypebb/src/standard-models/fred-series.ts b/packages/opentypebb/src/standard-models/fred-series.ts new file mode 100644 index 00000000..49e9c743 --- /dev/null +++ b/packages/opentypebb/src/standard-models/fred-series.ts @@ -0,0 +1,22 @@ +/** + * FRED Series Standard Model. + * Maps to: openbb_core/provider/standard_models/fred_series.py + */ + +import { z } from 'zod' + +export const FredSeriesQueryParamsSchema = z.object({ + symbol: z.string().describe('FRED series ID(s), comma-separated for multiple.'), + start_date: z.string().nullable().default(null).describe('Start date in YYYY-MM-DD.'), + end_date: z.string().nullable().default(null).describe('End date in YYYY-MM-DD.'), + limit: z.number().nullable().default(null).describe('Max observations per series.'), + frequency: z.string().nullable().default(null).describe('Aggregation frequency.'), +}).passthrough() + +export type FredSeriesQueryParams = z.infer + +export const FredSeriesDataSchema = z.object({ + date: z.string().describe('Observation date.'), +}).passthrough() + +export type FredSeriesData = z.infer diff --git a/packages/opentypebb/src/standard-models/gdp-forecast.ts b/packages/opentypebb/src/standard-models/gdp-forecast.ts new file mode 100644 index 00000000..7abd72fa --- /dev/null +++ b/packages/opentypebb/src/standard-models/gdp-forecast.ts @@ -0,0 +1,22 @@ +/** + * GDP Forecast Standard Model. + */ + +import { z } from 'zod' + +export const GdpForecastQueryParamsSchema = z.object({ + country: z.string().default('united_states').describe('Country to get GDP forecast for.'), + start_date: z.string().nullable().default(null).describe('Start date in YYYY-MM-DD.'), + end_date: z.string().nullable().default(null).describe('End date in YYYY-MM-DD.'), + frequency: z.enum(['annual', 'quarter']).default('annual').describe('Data frequency.'), +}).passthrough() + +export type GdpForecastQueryParams = z.infer + +export const GdpForecastDataSchema = z.object({ + date: z.string().describe('The date of the data.'), + country: z.string().nullable().default(null).describe('Country name.'), + value: z.number().nullable().default(null).describe('GDP forecast value.'), +}).passthrough() + +export type GdpForecastData = z.infer diff --git a/packages/opentypebb/src/standard-models/gdp-nominal.ts b/packages/opentypebb/src/standard-models/gdp-nominal.ts new file mode 100644 index 00000000..9b2d0b6c --- /dev/null +++ b/packages/opentypebb/src/standard-models/gdp-nominal.ts @@ -0,0 +1,22 @@ +/** + * GDP Nominal Standard Model. + */ + +import { z } from 'zod' + +export const GdpNominalQueryParamsSchema = z.object({ + country: z.string().default('united_states').describe('Country to get nominal GDP for.'), + start_date: z.string().nullable().default(null).describe('Start date in YYYY-MM-DD.'), + end_date: z.string().nullable().default(null).describe('End date in YYYY-MM-DD.'), + frequency: z.enum(['annual', 'quarter']).default('annual').describe('Data frequency.'), +}).passthrough() + +export type GdpNominalQueryParams = z.infer + +export const GdpNominalDataSchema = z.object({ + date: z.string().describe('The date of the data.'), + country: z.string().nullable().default(null).describe('Country name.'), + value: z.number().nullable().default(null).describe('Nominal GDP value.'), +}).passthrough() + +export type GdpNominalData = z.infer diff --git a/packages/opentypebb/src/standard-models/gdp-real.ts b/packages/opentypebb/src/standard-models/gdp-real.ts new file mode 100644 index 00000000..1e305d4c --- /dev/null +++ b/packages/opentypebb/src/standard-models/gdp-real.ts @@ -0,0 +1,22 @@ +/** + * GDP Real Standard Model. + */ + +import { z } from 'zod' + +export const GdpRealQueryParamsSchema = z.object({ + country: z.string().default('united_states').describe('Country to get real GDP for.'), + start_date: z.string().nullable().default(null).describe('Start date in YYYY-MM-DD.'), + end_date: z.string().nullable().default(null).describe('End date in YYYY-MM-DD.'), + frequency: z.enum(['annual', 'quarter']).default('annual').describe('Data frequency.'), +}).passthrough() + +export type GdpRealQueryParams = z.infer + +export const GdpRealDataSchema = z.object({ + date: z.string().describe('The date of the data.'), + country: z.string().nullable().default(null).describe('Country name.'), + value: z.number().nullable().default(null).describe('Real GDP value.'), +}).passthrough() + +export type GdpRealData = z.infer diff --git a/packages/opentypebb/src/standard-models/house-price-index.ts b/packages/opentypebb/src/standard-models/house-price-index.ts new file mode 100644 index 00000000..edd9489c --- /dev/null +++ b/packages/opentypebb/src/standard-models/house-price-index.ts @@ -0,0 +1,22 @@ +/** + * House Price Index Standard Model. + */ + +import { z } from 'zod' + +export const HousePriceIndexQueryParamsSchema = z.object({ + country: z.string().default('united_states').describe('Country to get house price index for.'), + frequency: z.enum(['annual', 'quarter', 'monthly']).default('quarter').describe('Data frequency.'), + start_date: z.string().nullable().default(null).describe('Start date in YYYY-MM-DD.'), + end_date: z.string().nullable().default(null).describe('End date in YYYY-MM-DD.'), +}).passthrough() + +export type HousePriceIndexQueryParams = z.infer + +export const HousePriceIndexDataSchema = z.object({ + date: z.string().describe('The date of the data.'), + country: z.string().nullable().default(null).describe('Country name.'), + value: z.number().nullable().default(null).describe('House price index value.'), +}).passthrough() + +export type HousePriceIndexData = z.infer diff --git a/packages/opentypebb/src/standard-models/index.ts b/packages/opentypebb/src/standard-models/index.ts index 357fac1e..dab7f62d 100644 --- a/packages/opentypebb/src/standard-models/index.ts +++ b/packages/opentypebb/src/standard-models/index.ts @@ -576,3 +576,250 @@ export { FuturesInstrumentsDataSchema, type FuturesInstrumentsData, } from './futures-instruments.js' + +// --- FRED --- + +export { + FredSearchQueryParamsSchema, + type FredSearchQueryParams, + FredSearchDataSchema, + type FredSearchData, +} from './fred-search.js' + +export { + FredSeriesQueryParamsSchema, + type FredSeriesQueryParams, + FredSeriesDataSchema, + type FredSeriesData, +} from './fred-series.js' + +export { + FredReleaseTableQueryParamsSchema, + type FredReleaseTableQueryParams, + FredReleaseTableDataSchema, + type FredReleaseTableData, +} from './fred-release-table.js' + +export { + FredRegionalQueryParamsSchema, + type FredRegionalQueryParams, + FredRegionalDataSchema, + type FredRegionalData, +} from './fred-regional.js' + +// --- Macro indicators --- + +export { + UnemploymentQueryParamsSchema, + type UnemploymentQueryParams, + UnemploymentDataSchema, + type UnemploymentData, +} from './unemployment.js' + +export { + MoneyMeasuresQueryParamsSchema, + type MoneyMeasuresQueryParams, + MoneyMeasuresDataSchema, + type MoneyMeasuresData, +} from './money-measures.js' + +export { + PersonalConsumptionExpendituresQueryParamsSchema, + type PersonalConsumptionExpendituresQueryParams, + PersonalConsumptionExpendituresDataSchema, + type PersonalConsumptionExpendituresData, +} from './pce.js' + +export { + TotalFactorProductivityQueryParamsSchema, + type TotalFactorProductivityQueryParams, + TotalFactorProductivityDataSchema, + type TotalFactorProductivityData, +} from './total-factor-productivity.js' + +export { + FomcDocumentsQueryParamsSchema, + type FomcDocumentsQueryParams, + FomcDocumentsDataSchema, + type FomcDocumentsData, +} from './fomc-documents.js' + +export { + PrimaryDealerPositioningQueryParamsSchema, + type PrimaryDealerPositioningQueryParams, + PrimaryDealerPositioningDataSchema, + type PrimaryDealerPositioningData, +} from './primary-dealer-positioning.js' + +export { + PrimaryDealerFailsQueryParamsSchema, + type PrimaryDealerFailsQueryParams, + PrimaryDealerFailsDataSchema, + type PrimaryDealerFailsData, +} from './primary-dealer-fails.js' + +// --- Survey --- + +export { + NonfarmPayrollsQueryParamsSchema, + type NonfarmPayrollsQueryParams, + NonfarmPayrollsDataSchema, + type NonfarmPayrollsData, +} from './nonfarm-payrolls.js' + +export { + InflationExpectationsQueryParamsSchema, + type InflationExpectationsQueryParams, + InflationExpectationsDataSchema, + type InflationExpectationsData, +} from './inflation-expectations.js' + +export { + SloosQueryParamsSchema, + type SloosQueryParams, + SloosDataSchema, + type SloosData, +} from './sloos.js' + +export { + UniversityOfMichiganQueryParamsSchema, + type UniversityOfMichiganQueryParams, + UniversityOfMichiganDataSchema, + type UniversityOfMichiganData, +} from './university-of-michigan.js' + +export { + EconomicConditionsChicagoQueryParamsSchema, + type EconomicConditionsChicagoQueryParams, + EconomicConditionsChicagoDataSchema, + type EconomicConditionsChicagoData, +} from './economic-conditions-chicago.js' + +export { + ManufacturingOutlookNYQueryParamsSchema, + type ManufacturingOutlookNYQueryParams, + ManufacturingOutlookNYDataSchema, + type ManufacturingOutlookNYData, +} from './manufacturing-outlook-ny.js' + +export { + ManufacturingOutlookTexasQueryParamsSchema, + type ManufacturingOutlookTexasQueryParams, + ManufacturingOutlookTexasDataSchema, + type ManufacturingOutlookTexasData, +} from './manufacturing-outlook-texas.js' + +// --- GDP --- + +export { + GdpForecastQueryParamsSchema, + type GdpForecastQueryParams, + GdpForecastDataSchema, + type GdpForecastData, +} from './gdp-forecast.js' + +export { + GdpNominalQueryParamsSchema, + type GdpNominalQueryParams, + GdpNominalDataSchema, + type GdpNominalData, +} from './gdp-nominal.js' + +export { + GdpRealQueryParamsSchema, + type GdpRealQueryParams, + GdpRealDataSchema, + type GdpRealData, +} from './gdp-real.js' + +// --- OECD --- + +export { + SharePriceIndexQueryParamsSchema, + type SharePriceIndexQueryParams, + SharePriceIndexDataSchema, + type SharePriceIndexData, +} from './share-price-index.js' + +export { + HousePriceIndexQueryParamsSchema, + type HousePriceIndexQueryParams, + HousePriceIndexDataSchema, + type HousePriceIndexData, +} from './house-price-index.js' + +export { + RetailPricesQueryParamsSchema, + type RetailPricesQueryParams, + RetailPricesDataSchema, + type RetailPricesData, +} from './retail-prices.js' + +// --- BLS --- + +export { + BlsSeriesQueryParamsSchema, + type BlsSeriesQueryParams, + BlsSeriesDataSchema, + type BlsSeriesData, +} from './bls-series.js' + +export { + BlsSearchQueryParamsSchema, + type BlsSearchQueryParams, + BlsSearchDataSchema, + type BlsSearchData, +} from './bls-search.js' + +// --- Commodity --- + +export { + CommoditySpotPriceQueryParamsSchema, + type CommoditySpotPriceQueryParams, + CommoditySpotPriceDataSchema, + type CommoditySpotPriceData, +} from './commodity-spot-price.js' + +export { + PetroleumStatusReportQueryParamsSchema, + type PetroleumStatusReportQueryParams, + PetroleumStatusReportDataSchema, + type PetroleumStatusReportData, +} from './petroleum-status-report.js' + +export { + ShortTermEnergyOutlookQueryParamsSchema, + type ShortTermEnergyOutlookQueryParams, + ShortTermEnergyOutlookDataSchema, + type ShortTermEnergyOutlookData, +} from './short-term-energy-outlook.js' + +// --- Shipping (Stubs) --- + +export { + PortInfoQueryParamsSchema, + type PortInfoQueryParams, + PortInfoDataSchema, + type PortInfoData, +} from './port-info.js' + +export { + PortVolumeQueryParamsSchema, + type PortVolumeQueryParams, + PortVolumeDataSchema, + type PortVolumeData, +} from './port-volume.js' + +export { + ChokepointInfoQueryParamsSchema, + type ChokepointInfoQueryParams, + ChokepointInfoDataSchema, + type ChokepointInfoData, +} from './chokepoint-info.js' + +export { + ChokepointVolumeQueryParamsSchema, + type ChokepointVolumeQueryParams, + ChokepointVolumeDataSchema, + type ChokepointVolumeData, +} from './chokepoint-volume.js' diff --git a/packages/opentypebb/src/standard-models/inflation-expectations.ts b/packages/opentypebb/src/standard-models/inflation-expectations.ts new file mode 100644 index 00000000..b641ec26 --- /dev/null +++ b/packages/opentypebb/src/standard-models/inflation-expectations.ts @@ -0,0 +1,23 @@ +/** + * Inflation Expectations Standard Model. + * Maps to: openbb_core/provider/standard_models/inflation_expectations.py + */ + +import { z } from 'zod' + +export const InflationExpectationsQueryParamsSchema = z.object({ + start_date: z.string().nullable().default(null).describe('Start date in YYYY-MM-DD.'), + end_date: z.string().nullable().default(null).describe('End date in YYYY-MM-DD.'), +}).passthrough() + +export type InflationExpectationsQueryParams = z.infer + +export const InflationExpectationsDataSchema = z.object({ + date: z.string().describe('The date of the data.'), + michigan_1y: z.number().nullable().default(null).describe('University of Michigan 1-year inflation expectation.'), + michigan_5y: z.number().nullable().default(null).describe('University of Michigan 5-year inflation expectation.'), + breakeven_5y: z.number().nullable().default(null).describe('5-Year breakeven inflation rate (T5YIE).'), + breakeven_10y: z.number().nullable().default(null).describe('10-Year breakeven inflation rate (T10YIE).'), +}).passthrough() + +export type InflationExpectationsData = z.infer diff --git a/packages/opentypebb/src/standard-models/manufacturing-outlook-ny.ts b/packages/opentypebb/src/standard-models/manufacturing-outlook-ny.ts new file mode 100644 index 00000000..de16e3b6 --- /dev/null +++ b/packages/opentypebb/src/standard-models/manufacturing-outlook-ny.ts @@ -0,0 +1,22 @@ +/** + * NY Fed Manufacturing Outlook Standard Model. + * Maps to: openbb_core/provider/standard_models/manufacturing_outlook_ny.py + */ + +import { z } from 'zod' + +export const ManufacturingOutlookNYQueryParamsSchema = z.object({ + start_date: z.string().nullable().default(null).describe('Start date in YYYY-MM-DD.'), + end_date: z.string().nullable().default(null).describe('End date in YYYY-MM-DD.'), +}).passthrough() + +export type ManufacturingOutlookNYQueryParams = z.infer + +export const ManufacturingOutlookNYDataSchema = z.object({ + date: z.string().describe('The date of the data.'), + general_business_conditions: z.number().nullable().default(null).describe('Empire State general business conditions index.'), + new_orders: z.number().nullable().default(null).describe('New orders diffusion index.'), + employees: z.number().nullable().default(null).describe('Number of employees diffusion index.'), +}).passthrough() + +export type ManufacturingOutlookNYData = z.infer diff --git a/packages/opentypebb/src/standard-models/manufacturing-outlook-texas.ts b/packages/opentypebb/src/standard-models/manufacturing-outlook-texas.ts new file mode 100644 index 00000000..6ce8748b --- /dev/null +++ b/packages/opentypebb/src/standard-models/manufacturing-outlook-texas.ts @@ -0,0 +1,22 @@ +/** + * Dallas Fed Manufacturing Outlook Standard Model. + * Maps to: openbb_core/provider/standard_models/manufacturing_outlook_texas.py + */ + +import { z } from 'zod' + +export const ManufacturingOutlookTexasQueryParamsSchema = z.object({ + start_date: z.string().nullable().default(null).describe('Start date in YYYY-MM-DD.'), + end_date: z.string().nullable().default(null).describe('End date in YYYY-MM-DD.'), +}).passthrough() + +export type ManufacturingOutlookTexasQueryParams = z.infer + +export const ManufacturingOutlookTexasDataSchema = z.object({ + date: z.string().describe('The date of the data.'), + general_activity: z.number().nullable().default(null).describe('General business activity index.'), + production: z.number().nullable().default(null).describe('Production index.'), + new_orders: z.number().nullable().default(null).describe('New orders index.'), +}).passthrough() + +export type ManufacturingOutlookTexasData = z.infer diff --git a/packages/opentypebb/src/standard-models/money-measures.ts b/packages/opentypebb/src/standard-models/money-measures.ts new file mode 100644 index 00000000..69069ce7 --- /dev/null +++ b/packages/opentypebb/src/standard-models/money-measures.ts @@ -0,0 +1,22 @@ +/** + * Money Measures Standard Model. + * Maps to: openbb_core/provider/standard_models/money_measures.py + */ + +import { z } from 'zod' + +export const MoneyMeasuresQueryParamsSchema = z.object({ + start_date: z.string().nullable().default(null).describe('Start date in YYYY-MM-DD.'), + end_date: z.string().nullable().default(null).describe('End date in YYYY-MM-DD.'), + adjusted: z.boolean().default(true).describe('If true, returns seasonally adjusted data.'), +}).passthrough() + +export type MoneyMeasuresQueryParams = z.infer + +export const MoneyMeasuresDataSchema = z.object({ + date: z.string().describe('The date of the data.'), + m1: z.number().nullable().default(null).describe('M1 money supply (billions USD).'), + m2: z.number().nullable().default(null).describe('M2 money supply (billions USD).'), +}).passthrough() + +export type MoneyMeasuresData = z.infer diff --git a/packages/opentypebb/src/standard-models/nonfarm-payrolls.ts b/packages/opentypebb/src/standard-models/nonfarm-payrolls.ts new file mode 100644 index 00000000..a9017642 --- /dev/null +++ b/packages/opentypebb/src/standard-models/nonfarm-payrolls.ts @@ -0,0 +1,22 @@ +/** + * Nonfarm Payrolls Standard Model. + * Maps to: openbb_core/provider/standard_models/nonfarm_payrolls.py + */ + +import { z } from 'zod' + +export const NonfarmPayrollsQueryParamsSchema = z.object({ + start_date: z.string().nullable().default(null).describe('Start date in YYYY-MM-DD.'), + end_date: z.string().nullable().default(null).describe('End date in YYYY-MM-DD.'), +}).passthrough() + +export type NonfarmPayrollsQueryParams = z.infer + +export const NonfarmPayrollsDataSchema = z.object({ + date: z.string().describe('The date of the data.'), + total_nonfarm: z.number().nullable().default(null).describe('Total nonfarm payrolls (thousands).'), + private_sector: z.number().nullable().default(null).describe('Private sector payrolls (thousands).'), + government: z.number().nullable().default(null).describe('Government payrolls (thousands).'), +}).passthrough() + +export type NonfarmPayrollsData = z.infer diff --git a/packages/opentypebb/src/standard-models/pce.ts b/packages/opentypebb/src/standard-models/pce.ts new file mode 100644 index 00000000..6434319d --- /dev/null +++ b/packages/opentypebb/src/standard-models/pce.ts @@ -0,0 +1,21 @@ +/** + * Personal Consumption Expenditures (PCE) Standard Model. + * Maps to: openbb_core/provider/standard_models/pce.py + */ + +import { z } from 'zod' + +export const PersonalConsumptionExpendituresQueryParamsSchema = z.object({ + start_date: z.string().nullable().default(null).describe('Start date in YYYY-MM-DD.'), + end_date: z.string().nullable().default(null).describe('End date in YYYY-MM-DD.'), +}).passthrough() + +export type PersonalConsumptionExpendituresQueryParams = z.infer + +export const PersonalConsumptionExpendituresDataSchema = z.object({ + date: z.string().describe('The date of the data.'), + pce: z.number().nullable().default(null).describe('PCE price index.'), + core_pce: z.number().nullable().default(null).describe('Core PCE price index (excluding food and energy).'), +}).passthrough() + +export type PersonalConsumptionExpendituresData = z.infer diff --git a/packages/opentypebb/src/standard-models/petroleum-status-report.ts b/packages/opentypebb/src/standard-models/petroleum-status-report.ts new file mode 100644 index 00000000..ad5bda0e --- /dev/null +++ b/packages/opentypebb/src/standard-models/petroleum-status-report.ts @@ -0,0 +1,29 @@ +/** + * Petroleum Status Report Standard Model. + * Data from EIA Weekly Petroleum Status Report. + */ + +import { z } from 'zod' + +export const PetroleumStatusReportQueryParamsSchema = z.object({ + category: z.enum([ + 'crude_oil_production', + 'crude_oil_stocks', + 'gasoline_stocks', + 'distillate_stocks', + 'refinery_utilization', + ]).default('crude_oil_stocks').describe('Petroleum data category.'), + start_date: z.string().nullable().default(null).describe('Start date in YYYY-MM-DD.'), + end_date: z.string().nullable().default(null).describe('End date in YYYY-MM-DD.'), +}).passthrough() + +export type PetroleumStatusReportQueryParams = z.infer + +export const PetroleumStatusReportDataSchema = z.object({ + date: z.string().describe('Observation date.'), + value: z.number().nullable().default(null).describe('Observation value.'), + category: z.string().nullable().default(null).describe('Data category.'), + unit: z.string().nullable().default(null).describe('Unit of measurement.'), +}).passthrough() + +export type PetroleumStatusReportData = z.infer diff --git a/packages/opentypebb/src/standard-models/port-info.ts b/packages/opentypebb/src/standard-models/port-info.ts new file mode 100644 index 00000000..ab79ad88 --- /dev/null +++ b/packages/opentypebb/src/standard-models/port-info.ts @@ -0,0 +1,21 @@ +/** + * Port Info Standard Model (Stub). + */ + +import { z } from 'zod' + +export const PortInfoQueryParamsSchema = z.object({ + port: z.string().default('').describe('Port code or name.'), +}).passthrough() + +export type PortInfoQueryParams = z.infer + +export const PortInfoDataSchema = z.object({ + port_code: z.string().nullable().default(null).describe('Port code.'), + port_name: z.string().nullable().default(null).describe('Port name.'), + country: z.string().nullable().default(null).describe('Country.'), + latitude: z.number().nullable().default(null).describe('Latitude.'), + longitude: z.number().nullable().default(null).describe('Longitude.'), +}).passthrough() + +export type PortInfoData = z.infer diff --git a/packages/opentypebb/src/standard-models/port-volume.ts b/packages/opentypebb/src/standard-models/port-volume.ts new file mode 100644 index 00000000..faebc48a --- /dev/null +++ b/packages/opentypebb/src/standard-models/port-volume.ts @@ -0,0 +1,21 @@ +/** + * Port Volume Standard Model (Stub). + */ + +import { z } from 'zod' + +export const PortVolumeQueryParamsSchema = z.object({ + port: z.string().default('').describe('Port code or name.'), + start_date: z.string().nullable().default(null).describe('Start date in YYYY-MM-DD.'), + end_date: z.string().nullable().default(null).describe('End date in YYYY-MM-DD.'), +}).passthrough() + +export type PortVolumeQueryParams = z.infer + +export const PortVolumeDataSchema = z.object({ + date: z.string().describe('Observation date.'), + port_code: z.string().nullable().default(null).describe('Port code.'), + volume: z.number().nullable().default(null).describe('Shipping volume (TEUs).'), +}).passthrough() + +export type PortVolumeData = z.infer diff --git a/packages/opentypebb/src/standard-models/primary-dealer-fails.ts b/packages/opentypebb/src/standard-models/primary-dealer-fails.ts new file mode 100644 index 00000000..b28f004b --- /dev/null +++ b/packages/opentypebb/src/standard-models/primary-dealer-fails.ts @@ -0,0 +1,19 @@ +/** + * Primary Dealer Fails Standard Model. + * Maps to: openbb_core/provider/standard_models/primary_dealer_fails.py + */ + +import { z } from 'zod' + +export const PrimaryDealerFailsQueryParamsSchema = z.object({ + start_date: z.string().nullable().default(null).describe('Start date in YYYY-MM-DD.'), + end_date: z.string().nullable().default(null).describe('End date in YYYY-MM-DD.'), +}).passthrough() + +export type PrimaryDealerFailsQueryParams = z.infer + +export const PrimaryDealerFailsDataSchema = z.object({ + date: z.string().describe('The date of the data.'), +}).passthrough() + +export type PrimaryDealerFailsData = z.infer diff --git a/packages/opentypebb/src/standard-models/primary-dealer-positioning.ts b/packages/opentypebb/src/standard-models/primary-dealer-positioning.ts new file mode 100644 index 00000000..1c0ed44d --- /dev/null +++ b/packages/opentypebb/src/standard-models/primary-dealer-positioning.ts @@ -0,0 +1,19 @@ +/** + * Primary Dealer Positioning Standard Model. + * Maps to: openbb_core/provider/standard_models/primary_dealer_positioning.py + */ + +import { z } from 'zod' + +export const PrimaryDealerPositioningQueryParamsSchema = z.object({ + start_date: z.string().nullable().default(null).describe('Start date in YYYY-MM-DD.'), + end_date: z.string().nullable().default(null).describe('End date in YYYY-MM-DD.'), +}).passthrough() + +export type PrimaryDealerPositioningQueryParams = z.infer + +export const PrimaryDealerPositioningDataSchema = z.object({ + date: z.string().describe('The date of the data.'), +}).passthrough() + +export type PrimaryDealerPositioningData = z.infer diff --git a/packages/opentypebb/src/standard-models/retail-prices.ts b/packages/opentypebb/src/standard-models/retail-prices.ts new file mode 100644 index 00000000..acc7ea37 --- /dev/null +++ b/packages/opentypebb/src/standard-models/retail-prices.ts @@ -0,0 +1,22 @@ +/** + * Retail Prices Standard Model. + */ + +import { z } from 'zod' + +export const RetailPricesQueryParamsSchema = z.object({ + country: z.string().default('united_states').describe('Country to get retail price data for.'), + frequency: z.enum(['annual', 'quarter', 'monthly']).default('monthly').describe('Data frequency.'), + start_date: z.string().nullable().default(null).describe('Start date in YYYY-MM-DD.'), + end_date: z.string().nullable().default(null).describe('End date in YYYY-MM-DD.'), +}).passthrough() + +export type RetailPricesQueryParams = z.infer + +export const RetailPricesDataSchema = z.object({ + date: z.string().describe('The date of the data.'), + country: z.string().nullable().default(null).describe('Country name.'), + value: z.number().nullable().default(null).describe('Retail price index value.'), +}).passthrough() + +export type RetailPricesData = z.infer diff --git a/packages/opentypebb/src/standard-models/share-price-index.ts b/packages/opentypebb/src/standard-models/share-price-index.ts new file mode 100644 index 00000000..a3761c15 --- /dev/null +++ b/packages/opentypebb/src/standard-models/share-price-index.ts @@ -0,0 +1,22 @@ +/** + * Share Price Index Standard Model. + */ + +import { z } from 'zod' + +export const SharePriceIndexQueryParamsSchema = z.object({ + country: z.string().default('united_states').describe('Country to get share price index for.'), + frequency: z.enum(['annual', 'quarter', 'monthly']).default('monthly').describe('Data frequency.'), + start_date: z.string().nullable().default(null).describe('Start date in YYYY-MM-DD.'), + end_date: z.string().nullable().default(null).describe('End date in YYYY-MM-DD.'), +}).passthrough() + +export type SharePriceIndexQueryParams = z.infer + +export const SharePriceIndexDataSchema = z.object({ + date: z.string().describe('The date of the data.'), + country: z.string().nullable().default(null).describe('Country name.'), + value: z.number().nullable().default(null).describe('Share price index value.'), +}).passthrough() + +export type SharePriceIndexData = z.infer diff --git a/packages/opentypebb/src/standard-models/short-term-energy-outlook.ts b/packages/opentypebb/src/standard-models/short-term-energy-outlook.ts new file mode 100644 index 00000000..b427118d --- /dev/null +++ b/packages/opentypebb/src/standard-models/short-term-energy-outlook.ts @@ -0,0 +1,30 @@ +/** + * Short-Term Energy Outlook Standard Model. + * Data from EIA STEO reports. + */ + +import { z } from 'zod' + +export const ShortTermEnergyOutlookQueryParamsSchema = z.object({ + category: z.enum([ + 'crude_oil_price', + 'gasoline_price', + 'natural_gas_price', + 'crude_oil_production', + 'petroleum_consumption', + ]).default('crude_oil_price').describe('STEO data category.'), + start_date: z.string().nullable().default(null).describe('Start date in YYYY-MM-DD.'), + end_date: z.string().nullable().default(null).describe('End date in YYYY-MM-DD.'), +}).passthrough() + +export type ShortTermEnergyOutlookQueryParams = z.infer + +export const ShortTermEnergyOutlookDataSchema = z.object({ + date: z.string().describe('Observation date.'), + value: z.number().nullable().default(null).describe('Observation value.'), + category: z.string().nullable().default(null).describe('Data category.'), + unit: z.string().nullable().default(null).describe('Unit of measurement.'), + forecast: z.boolean().nullable().default(null).describe('Whether this is a forecast value.'), +}).passthrough() + +export type ShortTermEnergyOutlookData = z.infer diff --git a/packages/opentypebb/src/standard-models/sloos.ts b/packages/opentypebb/src/standard-models/sloos.ts new file mode 100644 index 00000000..5939bf5c --- /dev/null +++ b/packages/opentypebb/src/standard-models/sloos.ts @@ -0,0 +1,21 @@ +/** + * Senior Loan Officer Opinion Survey (SLOOS) Standard Model. + * Maps to: openbb_core/provider/standard_models/sloos.py + */ + +import { z } from 'zod' + +export const SloosQueryParamsSchema = z.object({ + start_date: z.string().nullable().default(null).describe('Start date in YYYY-MM-DD.'), + end_date: z.string().nullable().default(null).describe('End date in YYYY-MM-DD.'), +}).passthrough() + +export type SloosQueryParams = z.infer + +export const SloosDataSchema = z.object({ + date: z.string().describe('The date of the data.'), + ci_loan_tightening: z.number().nullable().default(null).describe('Net % tightening standards for C&I loans to large firms.'), + consumer_loan_tightening: z.number().nullable().default(null).describe('Net % tightening standards for consumer loans.'), +}).passthrough() + +export type SloosData = z.infer diff --git a/packages/opentypebb/src/standard-models/total-factor-productivity.ts b/packages/opentypebb/src/standard-models/total-factor-productivity.ts new file mode 100644 index 00000000..94eb524c --- /dev/null +++ b/packages/opentypebb/src/standard-models/total-factor-productivity.ts @@ -0,0 +1,20 @@ +/** + * Total Factor Productivity Standard Model. + * Maps to: openbb_core/provider/standard_models/total_factor_productivity.py + */ + +import { z } from 'zod' + +export const TotalFactorProductivityQueryParamsSchema = z.object({ + start_date: z.string().nullable().default(null).describe('Start date in YYYY-MM-DD.'), + end_date: z.string().nullable().default(null).describe('End date in YYYY-MM-DD.'), +}).passthrough() + +export type TotalFactorProductivityQueryParams = z.infer + +export const TotalFactorProductivityDataSchema = z.object({ + date: z.string().describe('The date of the data.'), + value: z.number().nullable().default(null).describe('Total factor productivity value.'), +}).passthrough() + +export type TotalFactorProductivityData = z.infer diff --git a/packages/opentypebb/src/standard-models/unemployment.ts b/packages/opentypebb/src/standard-models/unemployment.ts new file mode 100644 index 00000000..41314825 --- /dev/null +++ b/packages/opentypebb/src/standard-models/unemployment.ts @@ -0,0 +1,23 @@ +/** + * Unemployment Standard Model. + * Maps to: openbb_core/provider/standard_models/unemployment.py + */ + +import { z } from 'zod' + +export const UnemploymentQueryParamsSchema = z.object({ + country: z.string().default('united_states').describe('The country to get data for.'), + start_date: z.string().nullable().default(null).describe('Start date in YYYY-MM-DD.'), + end_date: z.string().nullable().default(null).describe('End date in YYYY-MM-DD.'), + frequency: z.enum(['annual', 'quarter', 'monthly']).default('monthly').describe('Data frequency.'), +}).passthrough() + +export type UnemploymentQueryParams = z.infer + +export const UnemploymentDataSchema = z.object({ + date: z.string().describe('The date of the data.'), + country: z.string().nullable().default(null).describe('Country name.'), + value: z.number().nullable().default(null).describe('Unemployment rate value (percent).'), +}).passthrough() + +export type UnemploymentData = z.infer diff --git a/packages/opentypebb/src/standard-models/university-of-michigan.ts b/packages/opentypebb/src/standard-models/university-of-michigan.ts new file mode 100644 index 00000000..9b47141b --- /dev/null +++ b/packages/opentypebb/src/standard-models/university-of-michigan.ts @@ -0,0 +1,24 @@ +/** + * University of Michigan Consumer Sentiment Standard Model. + * Maps to: openbb_core/provider/standard_models/university_of_michigan.py + */ + +import { z } from 'zod' + +export const UniversityOfMichiganQueryParamsSchema = z.object({ + start_date: z.string().nullable().default(null).describe('Start date in YYYY-MM-DD.'), + end_date: z.string().nullable().default(null).describe('End date in YYYY-MM-DD.'), +}).passthrough() + +export type UniversityOfMichiganQueryParams = z.infer + +export const UniversityOfMichiganDataSchema = z.object({ + date: z.string().describe('The date of the data.'), + consumer_sentiment: z.number().nullable().default(null).describe('Index of Consumer Sentiment.'), + current_conditions: z.number().nullable().default(null).describe('Index of Current Economic Conditions.'), + expectations: z.number().nullable().default(null).describe('Index of Consumer Expectations.'), + inflation_expectation_1y: z.number().nullable().default(null).describe('Median expected price change next 12 months (%).'), + inflation_expectation_5y: z.number().nullable().default(null).describe('Median expected price change next 5 years (%).'), +}).passthrough() + +export type UniversityOfMichiganData = z.infer From 0ee0e4835e5284b5f8101796b60ce0d315ba2dd5 Mon Sep 17 00:00:00 2001 From: Ame Date: Wed, 11 Mar 2026 19:45:07 +0800 Subject: [PATCH 08/23] feat(web): add sub-channel support with per-channel AI config Web UI now supports multiple independent chat channels. Each channel has its own session (isolated context), and can optionally override the system prompt, AI provider/model, and tool permissions. Backend: - AskOptions: add disabledTools and provider fields - ProviderRouter: per-request provider override before falling back to config - VercelAIProvider: filter tools by disabledTools (skip agent cache when active) - ClaudeCodeProvider: merge disabledTools into disallowedTools - WebPlugin: multi-session + per-channel SSE routing - Chat routes: accept channelId in POST, channel query in history/events - New channels CRUD route (GET/POST/PUT/DELETE /api/channels) - Config: webSubchannelSchema + read/write helpers for web-subchannels.json Frontend: - Channel tab bar in ChatPage with create/delete support - API client for channels CRUD - chat.send/history/connectSSE accept channel parameter Co-Authored-By: Claude Opus 4.6 --- .../claude-code/claude-code-provider.ts | 9 +- .../vercel-ai-sdk/vercel-provider.ts | 18 ++- src/connectors/web/routes/channels.ts | 123 ++++++++++++++ src/connectors/web/routes/chat.ts | 48 ++++-- src/connectors/web/web-plugin.ts | 51 ++++-- src/core/ai-provider.ts | 19 +++ src/core/config.ts | 29 ++++ src/core/types.ts | 4 +- ui/src/api/channels.ts | 55 +++++++ ui/src/api/chat.ts | 18 ++- ui/src/api/index.ts | 3 + ui/src/api/types.ts | 10 ++ ui/src/pages/ChatPage.tsx | 150 ++++++++++++++++-- 13 files changed, 486 insertions(+), 51 deletions(-) create mode 100644 src/connectors/web/routes/channels.ts create mode 100644 ui/src/api/channels.ts diff --git a/src/ai-providers/claude-code/claude-code-provider.ts b/src/ai-providers/claude-code/claude-code-provider.ts index efc1f358..2b77e72d 100644 --- a/src/ai-providers/claude-code/claude-code-provider.ts +++ b/src/ai-providers/claude-code/claude-code-provider.ts @@ -41,11 +41,16 @@ export class ClaudeCodeProvider implements AIProvider { async askWithSession(prompt: string, session: SessionStore, opts?: AskOptions): Promise { const config = await this.resolveConfig() + // Merge per-channel disabledTools with global disallowedTools + const claudeCode = opts?.disabledTools?.length + ? { ...config, disallowedTools: [...(config.disallowedTools ?? []), ...opts.disabledTools] } + : config return askClaudeCodeWithSession(prompt, session, { - claudeCode: config, + claudeCode, compaction: this.compaction, - ...opts, + historyPreamble: opts?.historyPreamble, systemPrompt: opts?.systemPrompt ?? this.systemPrompt, + maxHistoryEntries: opts?.maxHistoryEntries, }) } } diff --git a/src/ai-providers/vercel-ai-sdk/vercel-provider.ts b/src/ai-providers/vercel-ai-sdk/vercel-provider.ts index c692579c..fccfcc83 100644 --- a/src/ai-providers/vercel-ai-sdk/vercel-provider.ts +++ b/src/ai-providers/vercel-ai-sdk/vercel-provider.ts @@ -32,13 +32,21 @@ export class VercelAIProvider implements AIProvider { ) {} /** Lazily create or return the cached agent, re-creating when config, tools, or system prompt change. */ - private async resolveAgent(systemPrompt?: string): Promise { + private async resolveAgent(systemPrompt?: string, disabledTools?: string[]): Promise { const { model, key } = await createModelFromConfig() - const tools = await this.getTools() - const toolCount = Object.keys(tools).length + const allTools = await this.getTools() + + // Per-channel tool override: skip cache and create a fresh agent with filtered tools + if (disabledTools?.length) { + const disabledSet = new Set(disabledTools) + const tools = Object.fromEntries(Object.entries(allTools).filter(([name]) => !disabledSet.has(name))) + return createAgent(model, tools, systemPrompt ?? this.instructions, this.maxSteps) + } + + const toolCount = Object.keys(allTools).length const effectivePrompt = systemPrompt ?? null if (key !== this.cachedKey || toolCount !== this.cachedToolCount || effectivePrompt !== this.cachedSystemPrompt) { - this.cachedAgent = createAgent(model, tools, systemPrompt ?? this.instructions, this.maxSteps) + this.cachedAgent = createAgent(model, allTools, systemPrompt ?? this.instructions, this.maxSteps) this.cachedKey = key this.cachedToolCount = toolCount this.cachedSystemPrompt = effectivePrompt @@ -63,7 +71,7 @@ export class VercelAIProvider implements AIProvider { async askWithSession(prompt: string, session: SessionStore, opts?: AskOptions): Promise { // historyPreamble and maxHistoryEntries are not used: Vercel passes native ModelMessage[] with no text wrapping needed. - const agent = await this.resolveAgent(opts?.systemPrompt) + const agent = await this.resolveAgent(opts?.systemPrompt, opts?.disabledTools) await session.appendUser(prompt, 'human') diff --git a/src/connectors/web/routes/channels.ts b/src/connectors/web/routes/channels.ts new file mode 100644 index 00000000..0892be46 --- /dev/null +++ b/src/connectors/web/routes/channels.ts @@ -0,0 +1,123 @@ +import { Hono } from 'hono' +import { SessionStore } from '../../../core/session.js' +import { readWebSubchannels, writeWebSubchannels } from '../../../core/config.js' +import type { WebChannel } from '../../../core/types.js' +import type { SSEClient } from './chat.js' + +interface ChannelsDeps { + sessions: Map + sseByChannel: Map> +} + +/** Channels CRUD: GET /, POST /, PUT /:id, DELETE /:id */ +export function createChannelsRoutes({ sessions, sseByChannel }: ChannelsDeps) { + const app = new Hono() + + /** GET / — list all channels (default first, then sub-channels) */ + app.get('/', async (c) => { + const subChannels = await readWebSubchannels() + const channels = [ + { id: 'default', label: 'Alice' }, + ...subChannels, + ] + return c.json({ channels }) + }) + + /** POST / — create a new sub-channel */ + app.post('/', async (c) => { + const body = await c.req.json() as { + id?: string + label?: string + systemPrompt?: string + provider?: string + disabledTools?: string[] + } + + if (!body.id || !/^[a-z0-9-_]+$/.test(body.id)) { + return c.json({ error: 'id must be lowercase alphanumeric with hyphens/underscores' }, 400) + } + if (body.id === 'default') { + return c.json({ error: 'cannot use reserved id "default"' }, 400) + } + if (!body.label?.trim()) { + return c.json({ error: 'label is required' }, 400) + } + + const existing = await readWebSubchannels() + if (existing.find((ch) => ch.id === body.id)) { + return c.json({ error: 'channel id already exists' }, 409) + } + + const newChannel: WebChannel = { + id: body.id, + label: body.label.trim(), + ...(body.systemPrompt ? { systemPrompt: body.systemPrompt } : {}), + ...(body.provider === 'claude-code' || body.provider === 'vercel-ai-sdk' + ? { provider: body.provider } + : {}), + ...(body.disabledTools?.length ? { disabledTools: body.disabledTools } : {}), + } + + await writeWebSubchannels([...existing, newChannel]) + + // Initialize session and SSE map for the new channel + const session = new SessionStore(`web/${body.id}`) + await session.restore() + sessions.set(body.id, session) + sseByChannel.set(body.id, new Map()) + + return c.json({ channel: newChannel }, 201) + }) + + /** PUT /:id — update a sub-channel */ + app.put('/:id', async (c) => { + const id = c.req.param('id') + if (id === 'default') return c.json({ error: 'cannot modify default channel' }, 400) + + const body = await c.req.json() as { + label?: string + systemPrompt?: string + provider?: string + disabledTools?: string[] + } + + const existing = await readWebSubchannels() + const idx = existing.findIndex((ch) => ch.id === id) + if (idx === -1) return c.json({ error: 'channel not found' }, 404) + + const updated: WebChannel = { + ...existing[idx], + ...(body.label !== undefined ? { label: body.label } : {}), + ...(body.systemPrompt !== undefined ? { systemPrompt: body.systemPrompt || undefined } : {}), + ...(body.provider === 'claude-code' || body.provider === 'vercel-ai-sdk' + ? { provider: body.provider } + : body.provider === null || body.provider === '' + ? { provider: undefined } + : {}), + ...(body.disabledTools !== undefined ? { disabledTools: body.disabledTools?.length ? body.disabledTools : undefined } : {}), + } + existing[idx] = updated + await writeWebSubchannels(existing) + + return c.json({ channel: updated }) + }) + + /** DELETE /:id — delete a sub-channel */ + app.delete('/:id', async (c) => { + const id = c.req.param('id') + if (id === 'default') return c.json({ error: 'cannot delete default channel' }, 400) + + const existing = await readWebSubchannels() + if (!existing.find((ch) => ch.id === id)) return c.json({ error: 'channel not found' }, 404) + + await writeWebSubchannels(existing.filter((ch) => ch.id !== id)) + + // Clean up in-memory state + sessions.delete(id) + sseByChannel.delete(id) + + return c.json({ success: true }) + }) + + return app +} diff --git a/src/connectors/web/routes/chat.ts b/src/connectors/web/routes/chat.ts index ba8db2f4..592997a1 100644 --- a/src/connectors/web/routes/chat.ts +++ b/src/connectors/web/routes/chat.ts @@ -4,7 +4,9 @@ import { readFile } from 'node:fs/promises' import { randomUUID } from 'node:crypto' import { extname, join } from 'node:path' import type { EngineContext } from '../../../core/types.js' +import type { AskOptions } from '../../../core/ai-provider.js' import { SessionStore, toChatHistory } from '../../../core/session.js' +import { readWebSubchannels } from '../../../core/config.js' import { persistMedia, resolveMediaPath } from '../../../core/media-store.js' export interface SSEClient { @@ -14,29 +16,45 @@ export interface SSEClient { interface ChatDeps { ctx: EngineContext - session: SessionStore - sseClients: Map + sessions: Map + sseByChannel: Map> } /** Chat routes: POST /, GET /history, GET /events (SSE) */ -export function createChatRoutes({ ctx, session, sseClients }: ChatDeps) { +export function createChatRoutes({ ctx, sessions, sseByChannel }: ChatDeps) { const app = new Hono() app.post('/', async (c) => { - const body = await c.req.json<{ message?: string }>() + const body = await c.req.json() as { message?: string; channelId?: string } const message = body.message?.trim() if (!message) return c.json({ error: 'message is required' }, 400) - const receivedEntry = await ctx.eventLog.append('message.received', { - channel: 'web', to: 'default', prompt: message, - }) + const channelId = body.channelId ?? 'default' + const session = sessions.get(channelId) + if (!session) return c.json({ error: 'channel not found' }, 404) - const result = await ctx.engine.askWithSession(message, session, { + // Build AskOptions from channel config (if not default) + const opts: AskOptions = { historyPreamble: 'The following is the recent conversation from the Web UI. Use it as context if the user references earlier messages.', + } + if (channelId !== 'default') { + const channels = await readWebSubchannels() + const channel = channels.find((ch) => ch.id === channelId) + if (channel) { + if (channel.systemPrompt) opts.systemPrompt = channel.systemPrompt + if (channel.disabledTools?.length) opts.disabledTools = channel.disabledTools + if (channel.provider) opts.provider = channel.provider + } + } + + const receivedEntry = await ctx.eventLog.append('message.received', { + channel: 'web', to: channelId, prompt: message, }) + const result = await ctx.engine.askWithSession(message, session, opts) + await ctx.eventLog.append('message.sent', { - channel: 'web', to: 'default', prompt: message, + channel: 'web', to: channelId, prompt: message, reply: result.text, durationMs: Date.now() - receivedEntry.ts, }) @@ -52,14 +70,22 @@ export function createChatRoutes({ ctx, session, sseClients }: ChatDeps) { app.get('/history', async (c) => { const limit = Number(c.req.query('limit')) || 100 + const channelId = c.req.query('channel') ?? 'default' + const session = sessions.get(channelId) + if (!session) return c.json({ error: 'channel not found' }, 404) const entries = await session.readActive() return c.json({ messages: toChatHistory(entries).slice(-limit) }) }) app.get('/events', (c) => { + const channelId = c.req.query('channel') ?? 'default' + // Create SSE client map for this channel if it doesn't exist yet + if (!sseByChannel.has(channelId)) sseByChannel.set(channelId, new Map()) + const channelClients = sseByChannel.get(channelId)! + return streamSSE(c, async (stream) => { const clientId = randomUUID() - sseClients.set(clientId, { + channelClients.set(clientId, { id: clientId, send: (data) => { stream.writeSSE({ data }).catch(() => {}) }, }) @@ -70,7 +96,7 @@ export function createChatRoutes({ ctx, session, sseClients }: ChatDeps) { stream.onAbort(() => { clearInterval(pingInterval) - sseClients.delete(clientId) + channelClients.delete(clientId) }) await new Promise(() => {}) diff --git a/src/connectors/web/web-plugin.ts b/src/connectors/web/web-plugin.ts index 8bb78185..07efdb06 100644 --- a/src/connectors/web/web-plugin.ts +++ b/src/connectors/web/web-plugin.ts @@ -1,13 +1,15 @@ -import { Hono } from 'hono' +import { Hono, type Context } from 'hono' import { cors } from 'hono/cors' import { serve } from '@hono/node-server' import { serveStatic } from '@hono/node-server/serve-static' import { resolve } from 'node:path' import type { Plugin, EngineContext } from '../../core/types.js' import { SessionStore, type ContentBlock } from '../../core/session.js' -import type { ConnectorCenter, Connector } from '../../core/connector-center.js' +import type { Connector } from '../../core/connector-center.js' import { persistMedia } from '../../core/media-store.js' +import { readWebSubchannels } from '../../core/config.js' import { createChatRoutes, createMediaRoutes, type SSEClient } from './routes/chat.js' +import { createChannelsRoutes } from './routes/channels.js' import { createConfigRoutes, createOpenbbRoutes } from './routes/config.js' import { createEventsRoutes } from './routes/events.js' import { createCronRoutes } from './routes/cron.js' @@ -24,19 +26,38 @@ export interface WebConfig { export class WebPlugin implements Plugin { name = 'web' private server: ReturnType | null = null - private sseClients = new Map() + /** SSE clients grouped by channel ID. Default channel: 'default'. */ + private sseByChannel = new Map>() private unregisterConnector?: () => void constructor(private config: WebConfig) {} async start(ctx: EngineContext) { - // Initialize session (mirrors Telegram's per-user pattern, single user for web) - const session = new SessionStore('web/default') - await session.restore() + // Load sub-channel definitions + const subChannels = await readWebSubchannels() + + // Initialize sessions for the default channel and all sub-channels + const sessions = new Map() + + const defaultSession = new SessionStore('web/default') + await defaultSession.restore() + sessions.set('default', defaultSession) + + for (const ch of subChannels) { + const session = new SessionStore(`web/${ch.id}`) + await session.restore() + sessions.set(ch.id, session) + } + + // Initialize SSE map for known channels (entries are created lazily too) + this.sseByChannel.set('default', new Map()) + for (const ch of subChannels) { + this.sseByChannel.set(ch.id, new Map()) + } const app = new Hono() - app.onError((err, c) => { + app.onError((err: Error, c: Context) => { if (err instanceof SyntaxError) { return c.json({ error: 'Invalid JSON' }, 400) } @@ -47,7 +68,8 @@ export class WebPlugin implements Plugin { app.use('/api/*', cors()) // ==================== Mount route modules ==================== - app.route('/api/chat', createChatRoutes({ ctx, session, sseClients: this.sseClients })) + app.route('/api/chat', createChatRoutes({ ctx, sessions, sseByChannel: this.sseByChannel })) + app.route('/api/channels', createChannelsRoutes({ sessions, sseByChannel: this.sseByChannel })) app.route('/api/media', createMediaRoutes()) app.route('/api/config', createConfigRoutes({ onConnectorsChange: async () => { await ctx.reconnectConnectors() }, @@ -67,8 +89,9 @@ export class WebPlugin implements Plugin { app.get('*', serveStatic({ root: uiRoot, path: 'index.html' })) // ==================== Connector registration ==================== + // The web connector only targets the main 'default' channel (heartbeat/cron notifications). this.unregisterConnector = ctx.connectorCenter.register( - this.createConnector(this.sseClients, session), + this.createConnector(this.sseByChannel, defaultSession), ) // ==================== Start server ==================== @@ -78,13 +101,13 @@ export class WebPlugin implements Plugin { } async stop() { - this.sseClients.clear() + this.sseByChannel.clear() this.unregisterConnector?.() this.server?.close() } private createConnector( - sseClients: Map, + sseByChannel: Map>, session: SessionStore, ): Connector { return { @@ -107,7 +130,9 @@ export class WebPlugin implements Plugin { source: payload.source, }) - for (const client of sseClients.values()) { + // Only broadcast to default channel SSE clients (heartbeat/cron stay in main channel) + const defaultClients = sseByChannel.get('default') ?? new Map() + for (const client of defaultClients.values()) { try { client.send(data) } catch { /* client disconnected */ } } @@ -121,7 +146,7 @@ export class WebPlugin implements Plugin { source: payload.source, }) - return { delivered: sseClients.size > 0 } + return { delivered: defaultClients.size > 0 } }, } } diff --git a/src/core/ai-provider.ts b/src/core/ai-provider.ts index f3d047bc..761ee443 100644 --- a/src/core/ai-provider.ts +++ b/src/core/ai-provider.ts @@ -31,6 +31,17 @@ export interface AskOptions { * Vercel AI SDK: not used (compaction via `compactIfNeeded` controls context size). */ maxHistoryEntries?: number + /** + * Tool names to disable for this call, in addition to the global disabled list. + * Claude Code: merged into `disallowedTools` CLI option. + * Vercel AI SDK: filtered out from the tool map before the agent is created. + */ + disabledTools?: string[] + /** + * AI provider to use for this call, overriding the global ai-provider.json config. + * Falls back to global config if not specified. + */ + provider?: 'claude-code' | 'vercel-ai-sdk' } export interface ProviderResult { @@ -64,6 +75,14 @@ export class ProviderRouter implements AIProvider { } async askWithSession(prompt: string, session: SessionStore, opts?: AskOptions): Promise { + // Per-request provider override takes precedence over global config + if (opts?.provider === 'claude-code' && this.claudeCode) { + return this.claudeCode.askWithSession(prompt, session, opts) + } + if (opts?.provider === 'vercel-ai-sdk') { + return this.vercel.askWithSession(prompt, session, opts) + } + // Fall back to global config const config = await readAIProviderConfig() if (config.backend === 'claude-code' && this.claudeCode) { return this.claudeCode.askWithSession(prompt, session, opts) diff --git a/src/core/config.ts b/src/core/config.ts index bf9f59a0..f47ba04f 100644 --- a/src/core/config.ts +++ b/src/core/config.ts @@ -172,6 +172,22 @@ export const toolsSchema = z.object({ disabled: z.array(z.string()).default([]), }) +export const webSubchannelSchema = z.object({ + /** URL-safe identifier. Used as session path segment: data/sessions/web/{id}.jsonl */ + id: z.string().regex(/^[a-z0-9-_]+$/, 'id must be lowercase alphanumeric with hyphens/underscores'), + label: z.string().min(1), + /** System prompt override for this channel. */ + systemPrompt: z.string().optional(), + /** AI provider override ('claude-code' | 'vercel-ai-sdk'). Falls back to global config if omitted. */ + provider: z.enum(['claude-code', 'vercel-ai-sdk']).optional(), + /** Tool names to disable in addition to the global disabled list. */ + disabledTools: z.array(z.string()).optional(), +}) + +export const webSubchannelsSchema = z.array(webSubchannelSchema) + +export type WebChannel = z.infer + // ==================== Platform + Account Config ==================== const guardConfigSchema = z.object({ @@ -526,3 +542,16 @@ export async function writeConfigSection(section: ConfigSection, data: unknown): await writeFile(resolve(CONFIG_DIR, sectionFiles[section]), JSON.stringify(validated, null, 2) + '\n') return validated } + +/** Read web sub-channel definitions from disk. Returns empty array if file missing. */ +export async function readWebSubchannels(): Promise { + const raw = await loadJsonFile('web-subchannels.json') + return webSubchannelsSchema.parse(raw ?? []) +} + +/** Write web sub-channel definitions to disk. */ +export async function writeWebSubchannels(channels: WebChannel[]): Promise { + const validated = webSubchannelsSchema.parse(channels) + await mkdir(CONFIG_DIR, { recursive: true }) + await writeFile(resolve(CONFIG_DIR, 'web-subchannels.json'), JSON.stringify(validated, null, 2) + '\n') +} diff --git a/src/core/types.ts b/src/core/types.ts index 84b83569..652db9dd 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -2,13 +2,13 @@ import type { AccountManager } from '../extension/trading/index.js' import type { ITradingGit } from '../extension/trading/git/interfaces.js' import type { CronEngine } from '../task/cron/engine.js' import type { Heartbeat } from '../task/heartbeat/index.js' -import type { Config } from './config.js' +import type { Config, WebChannel } from './config.js' import type { ConnectorCenter } from './connector-center.js' import type { Engine } from './engine.js' import type { EventLog } from './event-log.js' import type { ToolCenter } from './tool-center.js' -export type { Config } +export type { Config, WebChannel } export interface Plugin { name: string diff --git a/ui/src/api/channels.ts b/ui/src/api/channels.ts new file mode 100644 index 00000000..88726c36 --- /dev/null +++ b/ui/src/api/channels.ts @@ -0,0 +1,55 @@ +import { headers } from './client' +import type { WebChannel } from './types' + +export interface ChannelListItem { + id: string + label: string + systemPrompt?: string + provider?: 'claude-code' | 'vercel-ai-sdk' + disabledTools?: string[] +} + +export const channelsApi = { + async list(): Promise<{ channels: ChannelListItem[] }> { + const res = await fetch('/api/channels') + if (!res.ok) throw new Error('Failed to load channels') + return res.json() + }, + + async create(data: Omit & { id: string }): Promise<{ channel: ChannelListItem }> { + const res = await fetch('/api/channels', { + method: 'POST', + headers, + body: JSON.stringify(data), + }) + if (!res.ok) { + const err = await res.json().catch(() => ({ error: res.statusText })) + throw new Error(err.error || res.statusText) + } + return res.json() + }, + + async update(id: string, data: Partial>): Promise<{ channel: ChannelListItem }> { + const res = await fetch(`/api/channels/${encodeURIComponent(id)}`, { + method: 'PUT', + headers, + body: JSON.stringify(data), + }) + if (!res.ok) { + const err = await res.json().catch(() => ({ error: res.statusText })) + throw new Error(err.error || res.statusText) + } + return res.json() + }, + + async remove(id: string): Promise { + const res = await fetch(`/api/channels/${encodeURIComponent(id)}`, { + method: 'DELETE', + headers, + }) + if (!res.ok) { + const err = await res.json().catch(() => ({ error: res.statusText })) + throw new Error(err.error || res.statusText) + } + }, +} diff --git a/ui/src/api/chat.ts b/ui/src/api/chat.ts index e5609f41..aa333ebd 100644 --- a/ui/src/api/chat.ts +++ b/ui/src/api/chat.ts @@ -2,11 +2,11 @@ import { headers } from './client' import type { ChatResponse, ChatHistoryItem } from './types' export const chatApi = { - async send(message: string): Promise { + async send(message: string, channelId?: string): Promise { const res = await fetch('/api/chat', { method: 'POST', headers, - body: JSON.stringify({ message }), + body: JSON.stringify({ message, ...(channelId ? { channelId } : {}) }), }) if (!res.ok) { const err = await res.json().catch(() => ({ error: res.statusText })) @@ -15,14 +15,20 @@ export const chatApi = { return res.json() }, - async history(limit = 100): Promise<{ messages: ChatHistoryItem[] }> { - const res = await fetch(`/api/chat/history?limit=${limit}`) + async history(limit = 100, channel?: string): Promise<{ messages: ChatHistoryItem[] }> { + const params = new URLSearchParams({ limit: String(limit) }) + if (channel) params.set('channel', channel) + const res = await fetch(`/api/chat/history?${params}`) if (!res.ok) throw new Error('Failed to load history') return res.json() }, - connectSSE(onMessage: (data: { type: string; kind?: string; text: string; media?: Array<{ type: string; url: string }> }) => void): EventSource { - const es = new EventSource('/api/chat/events') + connectSSE( + onMessage: (data: { type: string; kind?: string; text: string; media?: Array<{ type: string; url: string }> }) => void, + channel?: string, + ): EventSource { + const url = channel ? `/api/chat/events?channel=${encodeURIComponent(channel)}` : '/api/chat/events' + const es = new EventSource(url) es.onmessage = (event) => { try { const data = JSON.parse(event.data) diff --git a/ui/src/api/index.ts b/ui/src/api/index.ts index 7f65d65e..c0cdd916 100644 --- a/ui/src/api/index.ts +++ b/ui/src/api/index.ts @@ -11,6 +11,7 @@ import { tradingApi } from './trading' import { openbbApi } from './openbb' import { devApi } from './dev' import { toolsApi } from './tools' +import { channelsApi } from './channels' export const api = { chat: chatApi, config: configApi, @@ -21,10 +22,12 @@ export const api = { openbb: openbbApi, dev: devApi, tools: toolsApi, + channels: channelsApi, } // Re-export all types for convenience export type { + WebChannel, ChatMessage, ChatResponse, ToolCall, diff --git a/ui/src/api/types.ts b/ui/src/api/types.ts index ca67a3ab..7e92bd3d 100644 --- a/ui/src/api/types.ts +++ b/ui/src/api/types.ts @@ -1,3 +1,13 @@ +// ==================== Channels ==================== + +export interface WebChannel { + id: string + label: string + systemPrompt?: string + provider?: 'claude-code' | 'vercel-ai-sdk' + disabledTools?: string[] +} + // ==================== Chat ==================== export interface ChatMessage { diff --git a/ui/src/pages/ChatPage.tsx b/ui/src/pages/ChatPage.tsx index 00364953..75edf84c 100644 --- a/ui/src/pages/ChatPage.tsx +++ b/ui/src/pages/ChatPage.tsx @@ -1,5 +1,6 @@ import { useState, useEffect, useRef, useCallback } from 'react' -import { api, type ChatHistoryItem, type ToolCall } from '../api' +import { api, type ToolCall } from '../api' +import type { ChannelListItem } from '../api/channels' import { useSSE } from '../hooks/useSSE' import { ChatMessage, ToolCallGroup, ThinkingIndicator } from '../components/ChatMessage' import { ChatInput } from '../components/ChatInput' @@ -14,6 +15,12 @@ interface ChatPageProps { } export function ChatPage({ onSSEStatus }: ChatPageProps) { + const [channels, setChannels] = useState([{ id: 'default', label: 'Alice' }]) + const [activeChannel, setActiveChannel] = useState('default') + const [showNewChannel, setShowNewChannel] = useState(false) + const [newChannelId, setNewChannelId] = useState('') + const [newChannelLabel, setNewChannelLabel] = useState('') + const [newChannelError, setNewChannelError] = useState('') const [messages, setMessages] = useState([]) const [isWaiting, setIsWaiting] = useState(false) const [showScrollBtn, setShowScrollBtn] = useState(false) @@ -21,6 +28,8 @@ export function ChatPage({ onSSEStatus }: ChatPageProps) { const messagesEndRef = useRef(null) const userScrolledUp = useRef(false) const containerRef = useRef(null) + const activeChannelRef = useRef(activeChannel) + activeChannelRef.current = activeChannel // Auto-scroll to bottom const scrollToBottom = useCallback(() => { @@ -45,10 +54,16 @@ export function ChatPage({ onSSEStatus }: ChatPageProps) { return () => el.removeEventListener('scroll', onScroll) }, []) - // Load chat history + // Load channels list on mount useEffect(() => { - api.chat.history(100).then(({ messages }) => { - setMessages(messages.map((m): DisplayItem => { + api.channels.list().then(({ channels: ch }) => setChannels(ch)).catch(() => {}) + }, []) + + // Load chat history when active channel changes + useEffect(() => { + const channel = activeChannel === 'default' ? undefined : activeChannel + api.chat.history(100, channel).then(({ messages: msgs }) => { + setMessages(msgs.map((m): DisplayItem => { if (m.kind === 'text' && m.metadata?.kind === 'notification') { return { ...m, role: 'notification', _id: nextId.current++ } } @@ -57,11 +72,12 @@ export function ChatPage({ onSSEStatus }: ChatPageProps) { }).catch((err) => { console.warn('Failed to load history:', err) }) - }, []) + }, [activeChannel]) - // Connect SSE for push notifications + report connection status + // SSE for the active channel + const sseChannel = activeChannel === 'default' ? undefined : activeChannel useSSE({ - url: '/api/chat/events', + url: sseChannel ? `/api/chat/events?channel=${encodeURIComponent(sseChannel)}` : '/api/chat/events', onMessage: (data) => { if (data.type === 'message' && data.text) { const role = data.kind === 'message' ? 'assistant' : 'notification' @@ -71,7 +87,7 @@ export function ChatPage({ onSSEStatus }: ChatPageProps) { ]) } }, - onStatus: onSSEStatus, + onStatus: activeChannel === 'default' ? onSSEStatus : undefined, }) // Send message @@ -80,7 +96,8 @@ export function ChatPage({ onSSEStatus }: ChatPageProps) { setIsWaiting(true) try { - const data = await api.chat.send(text) + const channel = activeChannelRef.current === 'default' ? undefined : activeChannelRef.current + const data = await api.chat.send(text, channel) if (data.text) { const media = data.media?.length ? data.media : undefined @@ -103,8 +120,109 @@ export function ChatPage({ onSSEStatus }: ChatPageProps) { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }) }, []) + const handleCreateChannel = useCallback(async () => { + setNewChannelError('') + if (!newChannelId.trim() || !newChannelLabel.trim()) { + setNewChannelError('ID and label are required') + return + } + try { + const { channel } = await api.channels.create({ id: newChannelId.trim(), label: newChannelLabel.trim() }) + setChannels((prev) => [...prev, channel]) + setActiveChannel(channel.id) + setShowNewChannel(false) + setNewChannelId('') + setNewChannelLabel('') + } catch (err) { + setNewChannelError(err instanceof Error ? err.message : 'Failed to create channel') + } + }, [newChannelId, newChannelLabel]) + + const handleDeleteChannel = useCallback(async (id: string) => { + try { + await api.channels.remove(id) + setChannels((prev) => prev.filter((ch) => ch.id !== id)) + if (activeChannel === id) setActiveChannel('default') + } catch (err) { + console.error('Failed to delete channel:', err) + } + }, [activeChannel]) + + const activeChannelConfig = channels.find((ch) => ch.id === activeChannel) + return (
+ {/* Channel tabs */} +
+ {channels.map((ch) => ( +
+ + {ch.id !== 'default' && ( + + )} +
+ ))} + +
+ + {/* New channel form */} + {showNewChannel && ( +
+ setNewChannelId(e.target.value.toLowerCase().replace(/[^a-z0-9-_]/g, ''))} + className="text-sm px-2 py-1 rounded border border-border bg-bg text-text placeholder:text-text-muted focus:outline-none focus:border-accent w-36" + /> + setNewChannelLabel(e.target.value)} + className="text-sm px-2 py-1 rounded border border-border bg-bg text-text placeholder:text-text-muted focus:outline-none focus:border-accent w-32" + /> + + + {newChannelError && {newChannelError}} +
+ )} + {/* Messages */}
{messages.length === 0 && !isWaiting && ( @@ -115,8 +233,17 @@ export function ChatPage({ onSSEStatus }: ChatPageProps) {
-

Hi, I'm Alice

-

Send a message to start chatting

+ {activeChannel === 'default' ? ( + <> +

Hi, I'm Alice

+

Send a message to start chatting

+ + ) : ( + <> +

{activeChannelConfig?.label ?? activeChannel}

+

Send a message to start chatting

+ + )}
)} @@ -125,7 +252,6 @@ export function ChatPage({ onSSEStatus }: ChatPageProps) { const prev = i > 0 ? messages[i - 1] : undefined if (msg.kind === 'tool_calls') { - // Tool calls get compact spacing, grouped under the preceding assistant block const prevIsAssistantish = prev != null && ( prev.kind === 'tool_calls' || (prev.kind === 'text' && prev.role === 'assistant') From 0e0cd0d9bd39e572f9b036da96975db79029aee3 Mon Sep 17 00:00:00 2001 From: Ame Date: Wed, 11 Mar 2026 21:23:08 +0800 Subject: [PATCH 09/23] feat(ui): redesign channel UI with popover + config modal Replace tab bar with minimal # icon popover for channel switching. Sub-channel view shows thin context bar with back navigation. Add ChannelConfigModal for editing label, system prompt, provider, and per-channel tool permissions. Co-Authored-By: Claude Opus 4.6 --- ui/src/components/ChannelConfigModal.tsx | 182 ++++++++++++++++ ui/src/pages/ChatPage.tsx | 265 ++++++++++++++++------- 2 files changed, 368 insertions(+), 79 deletions(-) create mode 100644 ui/src/components/ChannelConfigModal.tsx diff --git a/ui/src/components/ChannelConfigModal.tsx b/ui/src/components/ChannelConfigModal.tsx new file mode 100644 index 00000000..d96df57d --- /dev/null +++ b/ui/src/components/ChannelConfigModal.tsx @@ -0,0 +1,182 @@ +import { useState, useEffect } from 'react' +import { api } from '../api' +import type { ChannelListItem } from '../api/channels' +import type { ToolInfo } from '../api/tools' + +interface ChannelConfigModalProps { + channel: ChannelListItem + onClose: () => void + onSaved: (updated: ChannelListItem) => void +} + +export function ChannelConfigModal({ channel, onClose, onSaved }: ChannelConfigModalProps) { + const [label, setLabel] = useState(channel.label) + const [systemPrompt, setSystemPrompt] = useState(channel.systemPrompt ?? '') + const [provider, setProvider] = useState(channel.provider ?? '') + const [disabledTools, setDisabledTools] = useState>(new Set(channel.disabledTools ?? [])) + const [tools, setTools] = useState([]) + const [saving, setSaving] = useState(false) + const [error, setError] = useState('') + + useEffect(() => { + api.tools.load().then(({ inventory }) => setTools(inventory)).catch(() => {}) + }, []) + + const handleSave = async () => { + setSaving(true) + setError('') + try { + const { channel: updated } = await api.channels.update(channel.id, { + label: label.trim() || channel.label, + systemPrompt: systemPrompt.trim() || undefined, + provider: (provider as 'claude-code' | 'vercel-ai-sdk') || undefined, + disabledTools: disabledTools.size > 0 ? [...disabledTools] : undefined, + }) + onSaved(updated) + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to save') + } finally { + setSaving(false) + } + } + + const toggleTool = (name: string) => { + setDisabledTools((prev) => { + const next = new Set(prev) + if (next.has(name)) next.delete(name) + else next.add(name) + return next + }) + } + + // Group tools by group name + const toolGroups = tools.reduce>((acc, t) => { + ;(acc[t.group] ??= []).push(t) + return acc + }, {}) + + return ( +
+
e.stopPropagation()} + > + {/* Header */} +
+

+ # + {channel.id} +

+ +
+ + {/* Body */} +
+ {/* Label */} +
+ + setLabel(e.target.value)} + className="w-full text-sm px-3 py-2 rounded-lg border border-border bg-bg-secondary text-text placeholder:text-text-muted focus:outline-none focus:border-accent" + /> +
+ + {/* System Prompt */} +
+ +