diff --git a/openbb_platform/core/openbb_core/provider/standard_models/market_state.py b/openbb_platform/core/openbb_core/provider/standard_models/market_state.py new file mode 100644 index 000000000000..6317e23dbd13 --- /dev/null +++ b/openbb_platform/core/openbb_core/provider/standard_models/market_state.py @@ -0,0 +1,29 @@ +from datetime import datetime + +from openbb_core.provider.abstract.data import Data +from openbb_core.provider.abstract.query_params import QueryParams +from pydantic import Field + + +class MarketStateQueryParams(QueryParams): + exchange: str = Field(description="Exchange MIC, acronym, or name to check market state for.") + + +class MarketStateData(Data): + exchange: str = Field(description="Normalized exchange acronym.") + mic: str = Field(description="ISO 10383 MIC code for the exchange.") + status: str = Field(description="Raw market-state status returned by the provider.") + is_open: bool = Field(description="Fail-closed market state. Only `OPEN` evaluates to `True`.") + issued_at: datetime | None = Field(default=None, description="Receipt issue timestamp.") + expires_at: datetime | None = Field(default=None, description="Receipt expiry timestamp.") + ttl_seconds: int | None = Field( + default=None, + description="Receipt time-to-live in seconds when both timestamps are available.", + ) + issuer: str | None = Field(default=None, description="Receipt issuer.") + source: str | None = Field(default=None, description="Market-state source.") + halt_detection: str | None = Field(default=None, description="Halt detection mode reported by the provider.") + receipt_mode: str | None = Field(default=None, description="Receipt mode reported by the provider.") + schema_version: str | None = Field(default=None, description="Provider receipt schema version.") + public_key_id: str | None = Field(default=None, description="Identifier for the signing public key.") + signature: str | None = Field(default=None, description="Signature attached to the receipt payload.") diff --git a/openbb_platform/dev_install.py b/openbb_platform/dev_install.py index a1c6aac46e3c..3619c6007730 100644 --- a/openbb_platform/dev_install.py +++ b/openbb_platform/dev_install.py @@ -62,6 +62,7 @@ openbb-famafrench = { path = "./providers/famafrench", optional = true, develop = true } openbb-finra = { path = "./providers/finra", optional = true, develop = true } openbb-finviz = { path = "./providers/finviz", optional = true, develop = true } +openbb-headless-oracle = { path = "./providers/headless_oracle", optional = true, develop = true } openbb-multpl = { path = "./providers/multpl", optional = true, develop = true } openbb-nasdaq = { path = "./providers/nasdaq", optional = true, develop = true } openbb-seeking-alpha = { path = "./providers/seeking_alpha", optional = true, develop = true } @@ -91,11 +92,7 @@ def extract_dependencies(local_dep_path, dev: bool = False): .get("dev", {}) .get("dependencies", {}) ) - return ( - package_pyproject_toml.get("tool", {}) - .get("poetry", {}) - .get("dependencies", {}) - ) + return package_pyproject_toml.get("tool", {}).get("poetry", {}).get("dependencies", {}) return {} @@ -118,18 +115,14 @@ def install_platform_local(_extras: bool = False): local_deps = loads(LOCAL_DEPS).get("tool", {}).get("poetry", {})["dependencies"] with open(PYPROJECT) as f: pyproject_toml = load(f) - pyproject_toml.get("tool", {}).get("poetry", {}).get("dependencies", {}).update( - local_deps - ) + pyproject_toml.get("tool", {}).get("poetry", {}).get("dependencies", {}).update(local_deps) if _extras: dev_dependencies = get_all_dev_dependencies() - pyproject_toml.get("tool", {}).get("poetry", {}).setdefault( - "group", {} - ).setdefault("dev", {}).setdefault("dependencies", {}) - pyproject_toml.get("tool", {}).get("poetry", {})["group"]["dev"][ - "dependencies" - ].update(dev_dependencies) + pyproject_toml.get("tool", {}).get("poetry", {}).setdefault("group", {}).setdefault("dev", {}).setdefault( + "dependencies", {} + ) + pyproject_toml.get("tool", {}).get("poetry", {})["group"]["dev"]["dependencies"].update(dev_dependencies) TEMP_PYPROJECT = dumps(pyproject_toml) @@ -173,9 +166,7 @@ def install_platform_cli(): pyproject_toml = load(f) # remove "openbb" from dependencies - pyproject_toml.get("tool", {}).get("poetry", {}).get("dependencies", {}).pop( - "openbb", None - ) + pyproject_toml.get("tool", {}).get("poetry", {}).get("dependencies", {}).pop("openbb", None) TEMP_PYPROJECT = dumps(pyproject_toml) diff --git a/openbb_platform/extensions/equity/integration/test_equity_api.py b/openbb_platform/extensions/equity/integration/test_equity_api.py index dabce1688e1e..1d6e09601e9c 100644 --- a/openbb_platform/extensions/equity/integration/test_equity_api.py +++ b/openbb_platform/extensions/equity/integration/test_equity_api.py @@ -870,9 +870,7 @@ def test_equity_fundamental_revenue_per_segment(params, headers): params = {p: v for p, v in params.items() if v} query_str = get_querystring(params, []) - url = ( - f"http://0.0.0.0:8000/api/v1/equity/fundamental/revenue_per_segment?{query_str}" - ) + url = f"http://0.0.0.0:8000/api/v1/equity/fundamental/revenue_per_segment?{query_str}" result = requests.get(url, headers=headers, timeout=10) assert isinstance(result, requests.Response) assert result.status_code == 200 @@ -1629,9 +1627,7 @@ def test_equity_discovery_aggressive_small_caps(params, headers): params = {p: v for p, v in params.items() if v} query_str = get_querystring(params, []) - url = ( - f"http://0.0.0.0:8000/api/v1/equity/discovery/aggressive_small_caps?{query_str}" - ) + url = f"http://0.0.0.0:8000/api/v1/equity/discovery/aggressive_small_caps?{query_str}" result = requests.get(url, headers=headers, timeout=10) assert isinstance(result, requests.Response) assert result.status_code == 200 @@ -1832,6 +1828,23 @@ def test_equity_fundamental_historical_eps(params, headers): assert result.status_code == 200 +@pytest.mark.parametrize( + "params", + [ + ({"exchange": "XNYS", "provider": "headless_oracle"}), + ], +) +@pytest.mark.integration +def test_equity_market_state(params, headers): + params = {p: v for p, v in params.items() if v} + + query_str = get_querystring(params, []) + url = f"http://0.0.0.0:8000/api/v1/equity/market_state?{query_str}" + result = requests.get(url, headers=headers, timeout=10) + assert isinstance(result, requests.Response) + assert result.status_code == 200 + + @pytest.mark.parametrize( "params", [{"provider": "tiingo", "symbol": "AAPL", "limit": 10}], @@ -1889,9 +1902,7 @@ def test_equity_fundamental_reported_financials(params, headers): params = {p: v for p, v in params.items() if v} query_str = get_querystring(params, []) - url = ( - f"http://0.0.0.0:8000/api/v1/equity/fundamental/reported_financials?{query_str}" - ) + url = f"http://0.0.0.0:8000/api/v1/equity/fundamental/reported_financials?{query_str}" result = requests.get(url, headers=headers, timeout=10) assert isinstance(result, requests.Response) assert result.status_code == 200 diff --git a/openbb_platform/extensions/equity/integration/test_equity_python.py b/openbb_platform/extensions/equity/integration/test_equity_python.py index 6d2137c6061c..2ef0571b8cbf 100644 --- a/openbb_platform/extensions/equity/integration/test_equity_python.py +++ b/openbb_platform/extensions/equity/integration/test_equity_python.py @@ -1712,6 +1712,22 @@ def test_equity_market_snapshots(params, obb): assert len(result.results) > 0 +@pytest.mark.parametrize( + "params", + [ + ({"exchange": "XNYS", "provider": "headless_oracle"}), + ], +) +@pytest.mark.integration +def test_equity_market_state(params, obb): + result = obb.equity.market_state(**params) + assert result + assert isinstance(result, OBBject) + assert result.results is not None + assert result.results.mic == "XNYS" + assert result.results.status is not None + + @pytest.mark.parametrize( "params", [ diff --git a/openbb_platform/extensions/equity/openbb_equity/equity_router.py b/openbb_platform/extensions/equity/openbb_equity/equity_router.py index c34423cb2661..7d79b3745c62 100644 --- a/openbb_platform/extensions/equity/openbb_equity/equity_router.py +++ b/openbb_platform/extensions/equity/openbb_equity/equity_router.py @@ -59,9 +59,7 @@ async def search( return await OBBject.from_query(Query(**locals())) -@router.command( - model="EquityScreener", examples=[APIEx(parameters={"provider": "fmp"})] -) +@router.command(model="EquityScreener", examples=[APIEx(parameters={"provider": "fmp"})]) async def screener( cc: CommandContext, provider_choices: ProviderChoices, @@ -89,9 +87,7 @@ async def profile( return await OBBject.from_query(Query(**locals())) -@router.command( - model="MarketSnapshots", examples=[APIEx(parameters={"provider": "fmp"})] -) +@router.command(model="MarketSnapshots", examples=[APIEx(parameters={"provider": "fmp"})]) async def market_snapshots( cc: CommandContext, provider_choices: ProviderChoices, @@ -102,6 +98,26 @@ async def market_snapshots( return await OBBject.from_query(Query(**locals())) +@router.command( + model="MarketState", + examples=[ + APIEx(parameters={"exchange": "XNYS", "provider": "headless_oracle"}), + APIEx( + description="Check market state using an exchange acronym.", + parameters={"exchange": "NASDAQ", "provider": "headless_oracle"}, + ), + ], +) +async def market_state( + cc: CommandContext, + provider_choices: ProviderChoices, + standard_params: StandardParams, + extra_params: ExtraParams, +) -> OBBject: + """Get a signed market-state receipt for an exchange.""" + return await OBBject.from_query(Query(**locals())) + + @router.command( model="HistoricalMarketCap", examples=[APIEx(parameters={"provider": "fmp", "symbol": "AAPL"})], diff --git a/openbb_platform/providers/headless_oracle/README.md b/openbb_platform/providers/headless_oracle/README.md new file mode 100644 index 000000000000..d8b0272e8a46 --- /dev/null +++ b/openbb_platform/providers/headless_oracle/README.md @@ -0,0 +1,15 @@ +# Headless Oracle Provider + +This provider exposes signed market-state receipts from Headless Oracle for OpenBB. + +## Supported models + +- `MarketState` + +## Example + +```python +from openbb import obb + +obb.equity.market_state(exchange="XNYS", provider="headless_oracle") +``` diff --git a/openbb_platform/providers/headless_oracle/__init__.py b/openbb_platform/providers/headless_oracle/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/openbb_platform/providers/headless_oracle/openbb_headless_oracle/__init__.py b/openbb_platform/providers/headless_oracle/openbb_headless_oracle/__init__.py new file mode 100644 index 000000000000..e664cff4bf04 --- /dev/null +++ b/openbb_platform/providers/headless_oracle/openbb_headless_oracle/__init__.py @@ -0,0 +1,13 @@ +from openbb_core.provider.abstract.provider import Provider +from openbb_headless_oracle.models.market_state import HeadlessOracleMarketStateFetcher + +headless_oracle_provider = Provider( + name="headless_oracle", + website="https://headlessoracle.com", + description="Signed market-state receipts for exchange-open verification.", + credentials=[], + fetcher_dict={ + "MarketState": HeadlessOracleMarketStateFetcher, + }, + repr_name="Headless Oracle", +) diff --git a/openbb_platform/providers/headless_oracle/openbb_headless_oracle/models/market_state.py b/openbb_platform/providers/headless_oracle/openbb_headless_oracle/models/market_state.py new file mode 100644 index 000000000000..710f8c021512 --- /dev/null +++ b/openbb_platform/providers/headless_oracle/openbb_headless_oracle/models/market_state.py @@ -0,0 +1,100 @@ +from __future__ import annotations + +from datetime import datetime +from typing import Any + +from openbb_core.app.model.abstract.error import OpenBBError +from openbb_core.provider.abstract.fetcher import Fetcher +from openbb_core.provider.standard_models.market_state import ( + MarketStateData, + MarketStateQueryParams, +) +from openbb_core.provider.utils.exchange_utils import Exchange +from pydantic import Field + + +class HeadlessOracleMarketStateQueryParams(MarketStateQueryParams): + exchange: Exchange = Field(description="Exchange MIC, acronym, or name to check market state for.") + + +class HeadlessOracleMarketStateFetcher( + Fetcher[ + HeadlessOracleMarketStateQueryParams, + MarketStateData, + ] +): + require_credentials = False + + @staticmethod + def transform_query(params: dict[str, Any]) -> HeadlessOracleMarketStateQueryParams: + return HeadlessOracleMarketStateQueryParams(**params) + + @staticmethod + def extract_data( + query: HeadlessOracleMarketStateQueryParams, + credentials: dict[str, str] | None = None, + **kwargs: Any, + ) -> dict[str, Any]: + # pylint: disable=import-outside-toplevel + from openbb_core.provider.utils.helpers import make_request + + url = f"https://headlessoracle.com/v5/demo?mic={query.exchange.mic}" + + try: + response = make_request(url, timeout=30) + if response.status_code != 200: + raise OpenBBError(f"Headless Oracle request failed with status {response.status_code}: {response.text}") + + return response.json() + except OpenBBError: + raise + except Exception as error: + raise OpenBBError(error) from error + + @staticmethod + def transform_data( + query: HeadlessOracleMarketStateQueryParams, + data: dict[str, Any], + **kwargs: Any, + ) -> MarketStateData: + payload = data.get("receipt") + if not isinstance(payload, dict): + raise OpenBBError("Headless Oracle response did not include a valid receipt payload.") + + missing_fields = [field for field in ("mic", "status", "issued_at", "expires_at") if not payload.get(field)] + if missing_fields: + raise OpenBBError( + f"Headless Oracle receipt payload is missing required field(s): {', '.join(missing_fields)}" + ) + + status = str(payload.get("status") or "UNKNOWN").upper() + issued_at = HeadlessOracleMarketStateFetcher._parse_datetime(payload.get("issued_at")) + expires_at = HeadlessOracleMarketStateFetcher._parse_datetime(payload.get("expires_at")) + ttl_seconds = ( + int((expires_at - issued_at).total_seconds()) if issued_at is not None and expires_at is not None else None + ) + + return MarketStateData( + exchange=query.exchange.acronym, + mic=str(payload.get("mic") or query.exchange.mic), + status=status, + is_open=status == "OPEN", + issued_at=issued_at, + expires_at=expires_at, + ttl_seconds=ttl_seconds, + issuer=payload.get("issuer"), + source=payload.get("source"), + halt_detection=payload.get("halt_detection"), + receipt_mode=payload.get("receipt_mode"), + schema_version=payload.get("schema_version"), + public_key_id=payload.get("public_key_id") or payload.get("key_id"), + signature=payload.get("signature"), + ) + + @staticmethod + def _parse_datetime(value: Any) -> datetime | None: + if value in (None, ""): + return None + if isinstance(value, datetime): + return value + return datetime.fromisoformat(str(value).replace("Z", "+00:00")) diff --git a/openbb_platform/providers/headless_oracle/pyproject.toml b/openbb_platform/providers/headless_oracle/pyproject.toml new file mode 100644 index 000000000000..e9376976bc15 --- /dev/null +++ b/openbb_platform/providers/headless_oracle/pyproject.toml @@ -0,0 +1,19 @@ +[tool.poetry] +name = "openbb-headless-oracle" +version = "1.0.0" +description = "Headless Oracle provider for OpenBB" +authors = ["OpenBB Team "] +license = "AGPL-3.0-only" +readme = "README.md" +packages = [{ include = "openbb_headless_oracle" }] + +[tool.poetry.dependencies] +python = ">=3.10,<4" +openbb-core = "^1.6.7" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" + +[tool.poetry.plugins."openbb_provider_extension"] +headless_oracle = "openbb_headless_oracle:headless_oracle_provider" diff --git a/openbb_platform/providers/headless_oracle/tests/record/http/test_headless_oracle_fetchers/test_headless_oracle_market_state_fetcher_urllib3_v2.yaml b/openbb_platform/providers/headless_oracle/tests/record/http/test_headless_oracle_fetchers/test_headless_oracle_market_state_fetcher_urllib3_v2.yaml new file mode 100644 index 000000000000..3a2a352873b3 --- /dev/null +++ b/openbb_platform/providers/headless_oracle/tests/record/http/test_headless_oracle_fetchers/test_headless_oracle_market_state_fetcher_urllib3_v2.yaml @@ -0,0 +1,78 @@ +interactions: +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate, zstd + Connection: + - keep-alive + method: GET + uri: https://headlessoracle.com/v5/demo?mic=XNYS + response: + body: + string: '{"receipt_id":"74f3e7b7-5b70-4163-96d3-f78574c10fd6","issued_at":"2026-04-08T18:24:49.087Z","expires_at":"2026-04-08T18:25:49.087Z","issuer":"headlessoracle.com","mic":"XNYS","status":"OPEN","source":"SCHEDULE","halt_detection":"active","receipt_mode":"demo","schema_version":"v5.0","public_key_id":"key_2026_v1","signature":"9481fb791e8156e444748eba09f53d03724668bd586619d15be7540c6442ea976b3ac570ca1cd21db88ed1e0180eb680a547b15718fc151adfd2b6c4b9f96102","receipt":{"receipt_id":"74f3e7b7-5b70-4163-96d3-f78574c10fd6","issued_at":"2026-04-08T18:24:49.087Z","expires_at":"2026-04-08T18:25:49.087Z","issuer":"headlessoracle.com","mic":"XNYS","status":"OPEN","source":"SCHEDULE","halt_detection":"active","receipt_mode":"demo","schema_version":"v5.0","public_key_id":"key_2026_v1","signature":"9481fb791e8156e444748eba09f53d03724668bd586619d15be7540c6442ea976b3ac570ca1cd21db88ed1e0180eb680a547b15718fc151adfd2b6c4b9f96102"},"discovery_url":"https://headlessoracle.com/.well-known/mcp/server-card.json","extensions":{"bazaar":{"discoverable":true,"category":"financial-data","tags":["market-state","exchange-status","pre-trade","attestation","Ed25519","trading-hours","holiday-calendar","fail-closed","28-exchanges","signed-receipt","MIC"],"description":"Ed25519-signed + market-state receipt for 28 global exchanges. Pre-trade verification gate + for autonomous financial agents. UNKNOWN = CLOSED. Real-time session status, + holiday-aware calendar, 60-second TTL."}}}' + headers: + Access-Control-Allow-Headers: + - Content-Type, X-Oracle-Key, X-Payment, Payment-Signature + Access-Control-Allow-Methods: + - GET, POST, OPTIONS + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Payment-Required, Payment-Response, X-Payment-Required, X-Oracle-Plan, X-RateLimit-Limit, + X-RateLimit-Remaining, X-RateLimit-Reset, X-Trial-Remaining, X-Attestation-Mode + CF-RAY: + - 9e934fa24ccd79cf-LAX + Cache-Control: + - no-store + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none'; frame-ancestors 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 08 Apr 2026 18:24:49 GMT + Link: + - ; rel="llms-txt" + Nel: + - '{"report_to":"cf-nel","success_fraction":0.0,"max_age":604800}' + Permissions-Policy: + - camera=(), microphone=(), geolocation=() + Referrer-Policy: + - strict-origin-when-cross-origin + Report-To: + - '{"group":"cf-nel","max_age":604800,"endpoints":[{"url":"https://a.nel.cloudflare.com/report/v4?s=RaomkkP8OJHKQh2sIs4xhJwVcI4C7kbnbk3kJif3kC676VPPbg1mOx2rzx6gOnw%2BwJAC%2FMqVYEt4fUy5EnsQH8knwf1vSTNIXEws0Gss5Mffd33PbIncfpjhc6mM%2F3p7S47BgJB92jIBzQVNuOKSL7I%3D"}]}' + Server: + - cloudflare + Strict-Transport-Security: + - max-age=15552000; includeSubDomains; preload + Transfer-Encoding: + - chunked + X-Attestation-Mode: + - demo + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - DENY + X-Oracle-Plan: + - free + X-Oracle-Version: + - v5 + X-RateLimit-Limit: + - '500' + X-RateLimit-Remaining: + - '500' + X-RateLimit-Reset: + - '2026-04-09T00:00:00.000Z' + status: + code: 200 + message: OK +version: 1 diff --git a/openbb_platform/providers/headless_oracle/tests/test_headless_oracle_fetchers.py b/openbb_platform/providers/headless_oracle/tests/test_headless_oracle_fetchers.py new file mode 100644 index 000000000000..f11914773fc7 --- /dev/null +++ b/openbb_platform/providers/headless_oracle/tests/test_headless_oracle_fetchers.py @@ -0,0 +1,78 @@ +from datetime import datetime, timezone + +import pytest +from openbb_core.app.model.abstract.error import OpenBBError +from openbb_headless_oracle.models.market_state import HeadlessOracleMarketStateFetcher + + +@pytest.fixture(scope="module") +def vcr_config(): + return { + "filter_headers": [("User-Agent", None)], + "filter_query_parameters": [None], + } + + +@pytest.mark.record_http +def test_headless_oracle_market_state_fetcher(): + params = {"exchange": "XNYS"} + + fetcher = HeadlessOracleMarketStateFetcher() + result = fetcher.test(params, None) + assert result is None + + +def test_headless_oracle_market_state_transform_data_fail_closed(): + query = HeadlessOracleMarketStateFetcher.transform_query({"exchange": "XNYS"}) + payload = { + "receipt": { + "mic": "XNYS", + "status": "UNKNOWN", + "issued_at": "2026-04-08T18:26:25.387Z", + "expires_at": "2026-04-08T18:27:25.387Z", + "issuer": "headlessoracle.com", + "source": "SCHEDULE", + "halt_detection": "active", + "receipt_mode": "demo", + "schema_version": "v5.0", + "public_key_id": "key_2026_v1", + "signature": "sig", + } + } + + result = HeadlessOracleMarketStateFetcher.transform_data(query, payload) + + assert result.exchange == "NYSE" + assert result.mic == "XNYS" + assert result.status == "UNKNOWN" + assert result.is_open is False + assert result.issued_at == datetime(2026, 4, 8, 18, 26, 25, 387000, tzinfo=timezone.utc) + assert result.expires_at == datetime(2026, 4, 8, 18, 27, 25, 387000, tzinfo=timezone.utc) + assert result.ttl_seconds == 60 + assert result.issuer == "headlessoracle.com" + assert result.source == "SCHEDULE" + assert result.halt_detection == "active" + assert result.receipt_mode == "demo" + assert result.schema_version == "v5.0" + assert result.public_key_id == "key_2026_v1" + assert result.signature == "sig" + + +def test_headless_oracle_market_state_transform_data_requires_receipt_payload(): + query = HeadlessOracleMarketStateFetcher.transform_query({"exchange": "XNYS"}) + + with pytest.raises(OpenBBError, match="did not include a valid receipt payload"): + HeadlessOracleMarketStateFetcher.transform_data(query, {"status": "UNKNOWN"}) + + +def test_headless_oracle_market_state_transform_data_requires_core_fields(): + query = HeadlessOracleMarketStateFetcher.transform_query({"exchange": "XNYS"}) + payload = { + "receipt": { + "mic": "XNYS", + "status": "OPEN", + } + } + + with pytest.raises(OpenBBError, match=r"missing required field\(s\): issued_at, expires_at"): + HeadlessOracleMarketStateFetcher.transform_data(query, payload) diff --git a/openbb_platform/pyproject.toml b/openbb_platform/pyproject.toml index c1783507240b..f19df2b1b8a0 100644 --- a/openbb_platform/pyproject.toml +++ b/openbb_platform/pyproject.toml @@ -52,6 +52,7 @@ openbb-ecb = { version = "^1.6.0", optional = true } openbb-famafrench = { version = "^1.2.0", optional = true } openbb-finra = { version = "^1.6.0", optional = true } openbb-finviz = { version = "^1.5.0", optional = true } +openbb-headless-oracle = { version = "^1.0.0", optional = true } openbb-multpl = { version = "^1.3.0", optional = true } openbb-nasdaq = { version = "^1.6.1", optional = true } openbb-seeking-alpha = { version = "^1.6.0", optional = true } @@ -77,6 +78,7 @@ econometrics = ["openbb-econometrics"] famafrench = ["openbb-famafrench"] finra = ["openbb-finra"] finviz = ["openbb-finviz"] +headless_oracle = ["openbb-headless-oracle"] mcp_server = ["openbb-mcp-server"] nasdaq = ["openbb-nasdaq"] multpl = ["openbb-multpl"] @@ -100,6 +102,7 @@ all = [ "openbb-famafrench", "openbb-finra", "openbb-finviz", + "openbb-headless-oracle", "openbb-mcp-server", "openbb-multpl", "openbb-nasdaq",