Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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.")
Comment thread
HOYALIM marked this conversation as resolved.
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.")
25 changes: 8 additions & 17 deletions openbb_platform/dev_install.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down Expand Up @@ -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 {}


Expand All @@ -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)

Expand Down Expand Up @@ -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)

Expand Down
28 changes: 22 additions & 6 deletions openbb_platform/extensions/equity/openbb_equity/equity_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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"})],
Expand Down
15 changes: 15 additions & 0 deletions openbb_platform/providers/headless_oracle/README.md
Original file line number Diff line number Diff line change
@@ -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")
```
Original file line number Diff line number Diff line change
@@ -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=[],
Comment thread
HOYALIM marked this conversation as resolved.
fetcher_dict={
"MarketState": HeadlessOracleMarketStateFetcher,
},
repr_name="Headless Oracle",
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
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)
Comment thread
HOYALIM marked this conversation as resolved.

@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")
payload = payload if isinstance(payload, dict) else data

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"))
19 changes: 19 additions & 0 deletions openbb_platform/providers/headless_oracle/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[tool.poetry]
name = "openbb-headless-oracle"
version = "1.0.0"
description = "Headless Oracle provider for OpenBB"
authors = ["OpenBB Team <hello@openbb.co>"]
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"
Original file line number Diff line number Diff line change
@@ -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."}}}'
Comment thread
HOYALIM marked this conversation as resolved.
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:
- </llms.txt>; 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import pytest
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"}
Comment thread
HOYALIM marked this conversation as resolved.

fetcher = HeadlessOracleMarketStateFetcher()
result = fetcher.test(params, None)
assert result is None
Loading
Loading