[Feature] Add Workspace App To openbb-nasdaq For Company Filings As PDFs (#7135)

* add custom app launcher to openbb-nasdaq

* f-string

* linters

* linters

* readme

* mypy

* grammar police

* cleanup test code

---------

Co-authored-by: Theodore Aptekarev <aptekarev@gmail.com>
This commit is contained in:
Danglewood
2025-06-04 08:09:39 -07:00
committed by GitHub
parent 3a6e8f95be
commit a83885e4d5
7 changed files with 1920 additions and 3 deletions

View File

@@ -10,4 +10,67 @@ To install the extension:
pip install openbb-nasdaq
```
Documentation available [here](https://docs.openbb.co/platform/developer_guide/contributing).
## Coverage
The following endpoints are covered by this extension:
- `obb.economy.calendar`
- `obb.equity.calendar.dividend`
- `obb.equity.calendar.earnings`
- `obb.equity.calendar.ipo`
- `obb.equity.search`
- `obb.equity.screener`
- `obb.equity.fundamental.dividend`
- `obb.equity.fundamental.filings`
Router extensions are required (`openbb-economy` & `openbb-equity`) to populate endpoints, but fetchers can be used independent of the Python and API interfaces.
```python
from openbb_nasdaq import nasdaq_provider
nasdaq_provider.fetcher_dict
```
```python
{'CalendarDividend': openbb_nasdaq.models.calendar_dividend.NasdaqCalendarDividendFetcher,
'CalendarEarnings': openbb_nasdaq.models.calendar_earnings.NasdaqCalendarEarningsFetcher,
'CalendarIpo': openbb_nasdaq.models.calendar_ipo.NasdaqCalendarIpoFetcher,
'CompanyFilings': openbb_nasdaq.models.company_filings.NasdaqCompanyFilingsFetcher,
'EconomicCalendar': openbb_nasdaq.models.economic_calendar.NasdaqEconomicCalendarFetcher,
'EquitySearch': openbb_nasdaq.models.equity_search.NasdaqEquitySearchFetcher,
'EquityScreener': openbb_nasdaq.models.equity_screener.NasdaqEquityScreenerFetcher,
'HistoricalDividends': openbb_nasdaq.models.historical_dividends.NasdaqHistoricalDividendsFetcher,
'TopRetail': openbb_nasdaq.models.top_retail.NasdaqTopRetailFetcher}
```
## OpenBB Workspace App
This package includes a standalone Workspace application for viewing market calendars and company filings.
Launch it from the command line, with the environment active, by entering:
```sh
openbb-api --app openbb_nasdaq.app:app --factory
```
This will start the FastAPI application via Uvicorn and serve the configuration files to Workspace when added as a backend data source.
To see all the launch arguments, use:
```sh
openbb-api --help
```
The application is served by a factory function, and the FastAPI instance can be utilized by calling the `main` function.
```python
from openbb_nasdaq.app import main
app = main()
```
## OpenBB Documentation
OpenBB Platform documentation is available [here](https://docs.openbb.co/platform).
OpenBB Workspace documentation is available [here](https://docs.openbb.co/workspace).

View File

@@ -0,0 +1,727 @@
"""OpenBB Workspace Application.
This includes widgets and endpoints for fetching and displaying
SEC filings, as well as calendars for earnings, dividends, ipos,
and global economic events from the public Nasdaq website.
To use this app, launch it with:
```bash
openbb-api --app openbb_nasdaq.app:main --factory
```
Then navigate to the OpenBB Workspace and add the server as a backend data source.
Open the server's URL in your browser for more information on connecting to the OpenBB Workspace.
"""
# pylint: disable=too-many-statements, too-many-locals, line-too-long
def main():
"""Return a FastAPI app instance."""
# pylint: disable=import-outside-toplevel
from datetime import datetime
from typing import Annotated, Literal, Optional
from async_lru import alru_cache
from fastapi import Depends, FastAPI
from openbb_nasdaq.models.calendar_dividend import NasdaqCalendarDividendFetcher
from openbb_nasdaq.models.calendar_earnings import NasdaqCalendarEarningsFetcher
from openbb_nasdaq.models.calendar_ipo import NasdaqCalendarIpoFetcher
from openbb_nasdaq.models.company_filings import NasdaqCompanyFilingsFetcher
from openbb_nasdaq.models.economic_calendar import NasdaqEconomicCalendarFetcher
from openbb_nasdaq.models.equity_search import NasdaqEquitySearchFetcher
from pandas import DataFrame
listings = DataFrame()
current_year = int(datetime.now().year)
years = sorted(
[{"label": str(i), "value": i} for i in range(1994, current_year + 1)],
key=lambda x: x["value"], # type: ignore
reverse=True,
)
app = FastAPI()
async def get_listings() -> DataFrame:
"""Get the Nasdaq listings."""
return listings
Nasdaqlistings = Annotated[
DataFrame,
Depends(get_listings),
]
async def startup_event():
"""Startup event for the FastAPI app."""
nonlocal listings
fetcher = NasdaqEquitySearchFetcher()
directory = await fetcher.fetch_data({}, {})
listings = DataFrame([d.model_dump() for d in directory]).query( # type: ignore
"test_issue == 'N' and etf == 'N'"
" and not name.str.contains('%')"
" and not name.str.contains('Unit')"
" and not name.str.contains('Rights')"
" and not name.str.contains('Warrant')"
" and not name.str.contains('Preferred')"
)
app.add_event_handler("startup", startup_event)
@app.get("/get_symbol_choices", include_in_schema=False)
async def get_symbol_choices(
directory: Nasdaqlistings, # type: ignore
symbol: Optional[str] = None,
year: Optional[int] = None,
form_group: Optional[str] = None,
) -> list:
"""Get symbol choices for the Nasdaq listings."""
if symbol and year and form_group:
return await get_document_choices(symbol, year, form_group)
items = directory.set_index("symbol")["name"].to_dict()
return [
{"value": k, "label": k, "extraInfo": {"description": v}}
for k, v in items.items()
]
@alru_cache
async def get_document_choices(
symbol: Optional[str] = None,
year: Optional[int] = None,
form_group: Literal[
"annual",
"quarterly",
"proxy",
"insider",
"8k",
"registration",
"comment",
] = "8k",
) -> list:
"""Get document choices for the given symbol."""
fetcher = NasdaqCompanyFilingsFetcher()
params = {
"symbol": symbol,
"year": year,
"form_group": form_group,
}
items = await fetcher.fetch_data(params, {})
docs: list = []
for item in items:
form = item.model_dump() # type: ignore
title = f"{form.get('filing_date', '')} - {'8-K' if form_group == '8k' else form_group.title()}"
url = form.get("pdf_url")
if not url:
continue
docs.append({"label": title, "value": url})
return docs
@app.get(
"/open_document",
openapi_extra={
"widget_config": {
"type": "multi_file_viewer",
"name": "SEC Filings",
"description": "View SEC filings by company.",
"refetchInterval": False,
"widgetId": "nasdaq_company_filings",
"params": [
{
"paramName": "document_url",
"label": "Document URL",
"description": "Select the document to open.",
"type": "endpoint",
"optionsEndpoint": "/get_symbol_choices",
"optionsParams": {
"symbol": "$symbol",
"year": "$year",
"form_group": "$form_group",
},
"show": False,
"roles": ["fileSelector"],
"multiSelect": True,
},
{
"paramName": "symbol",
"label": "Symbol",
"description": (
"Ticker symbol for the company."
" Foreign (dual-listed) companies may not be required to file with the SEC."
),
"type": "endpoint",
"value": "AAPL",
"optionsEndpoint": "/get_symbol_choices",
"style": {"popupWidth": 850},
},
{
"paramName": "year",
"label": "Calendar Year",
"description": "Calendar year for the filings.",
"value": years[0]["value"],
"type": "number",
"options": years,
},
{
"paramName": "form_group",
"label": "Form Group",
"description": "Form group for the filings.",
"value": "8k",
"type": "text",
"options": [
{"label": "Annual", "value": "annual"},
{"label": "Quarterly", "value": "quarterly"},
{"label": "Proxy", "value": "proxy"},
{"label": "Insider", "value": "insider"},
{"label": "8-K", "value": "8k"},
{"label": "Registration", "value": "registration"},
{"label": "Comment", "value": "comment"},
],
},
],
"gridData": {"w": 40, "h": 30},
}
},
)
async def open_document(
document_url: str,
) -> dict:
"""Open the selected PDF filing document."""
import base64 # noqa
from openbb_core.provider.utils.helpers import make_request
encoded_content: str = ""
if document_url and document_url.startswith("http"):
response = make_request(document_url)
response.raise_for_status()
encoded_content = base64.b64encode(response.content).decode("utf-8")
return {
"data_format": {
"data_type": "pdf",
"filename": document_url.split("/")[-1],
},
"content": encoded_content,
}
@app.get(
"/earnings_calendar",
openapi_extra={
"widget_config": {
"name": "Earnings Calendar",
"description": "Upcoming, and historical, company earnings releases (US Markets).",
"category": "Equity",
"subCategory": "Calendar",
"type": "table",
"searchCategory": "Equity",
"widgetId": "nasdaq_earnings_calendar",
"refetchInterval": False,
"params": [
{
"label": "Start Date",
"description": "Start date of the data.",
"type": "date",
"value": None,
"paramName": "start_date",
},
{
"label": "End Date",
"description": "End date of the data.",
"type": "date",
"value": None,
"paramName": "end_date",
},
],
"gridData": {"w": 40, "h": 20},
"data": {
"table": {
"columnsDefs": [
{
"field": "surprise_percent",
"formatterFn": "normalizedPercent",
"cellDataType": "number",
"renderFn": "greenRed",
},
]
}
},
},
},
)
async def earnings_calendar(
start_date: Optional[str] = None,
end_date: Optional[str] = None,
) -> list[NasdaqCalendarEarningsFetcher.data_type]:
"""Get the earnings calendar."""
fetcher = NasdaqCalendarEarningsFetcher()
params = {
"start_date": start_date,
"end_date": end_date,
}
return await fetcher.fetch_data(params, {}) # type: ignore
@app.get(
"/dividend_calendar",
openapi_extra={
"widget_config": {
"name": "Dividend Calendar",
"description": "Upcoming, and historical, dividend payments (US Markets).",
"category": "Equity",
"subCategory": "Calendar",
"type": "table",
"searchCategory": "Equity",
"widgetId": "nasdaq_dividends_calendar",
"refetchInterval": False,
"params": [
{
"label": "Start Date",
"description": "Start date of the data.",
"type": "date",
"value": None,
"paramName": "start_date",
},
{
"label": "End Date",
"description": "End date of the data.",
"type": "date",
"value": None,
"paramName": "end_date",
},
],
"gridData": {"w": 40, "h": 20},
}
},
)
async def dividend_calendar(
start_date: Optional[str] = None,
end_date: Optional[str] = None,
) -> list[NasdaqCalendarDividendFetcher.data_type]:
"""Get the dividend calendar."""
fetcher = NasdaqCalendarDividendFetcher()
params = {
"start_date": start_date,
"end_date": end_date,
}
return await fetcher.fetch_data(params, {}) # type: ignore
@app.get(
"/ipo_calendar",
openapi_extra={
"widget_config": {
"name": "IPO Calendar",
"description": "IPO announcements and calendar.",
"category": "Equity",
"subCategory": "Calendar",
"type": "table",
"searchCategory": "Equity",
"widgetId": "nasdaq_ipo_calendar",
"refetchInterval": False,
"params": [
{
"label": "Start Date",
"description": "Start date of the data, in YYYY-MM-DD format.",
"optional": True,
"type": "date",
"value": None,
"show": True,
"paramName": "start_date",
},
{
"label": "End Date",
"description": "End date of the data, in YYYY-MM-DD format.",
"optional": True,
"type": "date",
"value": None,
"show": True,
"paramName": "end_date",
},
{
"label": "Status",
"description": "Status of the IPO. [upcoming, priced, or withdrawn]",
"optional": True,
"type": "text",
"value": None,
"show": True,
"options": [
{"label": "upcoming", "value": "upcoming"},
{"label": "priced", "value": "priced"},
{"label": "filed", "value": "filed"},
{"label": "withdrawn", "value": "withdrawn"},
],
"paramName": "status",
},
{
"label": "Is SPO",
"description": "If True, returns data for secondary public offerings (SPOs).",
"optional": True,
"type": "boolean",
"value": False,
"show": True,
"paramName": "is_spo",
},
],
"gridData": {"w": 40, "h": 20},
}
},
)
async def ipo_calendar(
start_date: Optional[str] = None,
end_date: Optional[str] = None,
) -> list[NasdaqCalendarIpoFetcher.data_type]:
"""Get the IPO calendar."""
fetcher = NasdaqCalendarIpoFetcher()
params = {
"start_date": start_date,
"end_date": end_date,
}
return await fetcher.fetch_data(params, {}) # type: ignore
@app.get(
"/economic_calendar",
openapi_extra={
"widget_config": {
"name": "Economic Calendar",
"description": "Upcoming, and historical, macroeconomic events.",
"category": "Economy",
"subCategory": "Calendar",
"type": "table",
"searchCategory": "Equity",
"widgetId": "nasdaq_economic_calendar",
"refetchInterval": False,
"params": [
{
"paramName": "country",
"options": [
{"label": "United States", "value": "united_states"},
{"label": "China", "value": "china"},
{"label": "Hong Kong", "value": "hong_kong"},
{"label": "Japan", "value": "japan"},
{"label": "Singapore", "value": "signapore"},
{"label": "South Korea", "value": "south_korea"},
{"label": "India", "value": "india"},
{"label": "Russia", "value": "russia"},
{"label": "Euro Zone", "value": "euro_zone"},
{"label": "Germany", "value": "germany"},
{"label": "United Kingdom", "value": "united_kingdom"},
{"label": "France", "value": "france"},
{"label": "Spain", "value": "spain"},
{"label": "Italy", "value": "italy"},
{"label": "Switzerland", "value": "switzerland"},
{"label": "Canada", "value": "canada"},
{"label": "Australia", "value": "australia"},
{"label": "New Zealand", "value": "new_zealand"},
{"label": "South Africa", "value": "south_africa"},
{"label": "Brazil", "value": "brazil"},
],
},
],
"gridData": {"w": 40, "h": 20},
"data": {
"table": {
"showAll": False,
"columnsDefs": [
{
"field": "date",
"pinned": "left",
"formatterFn": None,
"headerName": "Date",
"headerTooltip": "The date of the data.",
"cellDataType": "date",
},
{
"field": "country",
"formatterFn": None,
"headerName": "Country",
"headerTooltip": "Country of event.",
"cellDataType": "text",
},
{
"field": "event",
"formatterFn": None,
"renderFn": "hoverCard",
"renderFnParams": {
"hoverCard": {
"cellField": "value",
"markdown": "{description}",
}
},
"headerName": "Event",
"headerTooltip": "Event name.",
"cellDataType": "text",
},
{
"field": "consensus",
"formatterFn": None,
"headerName": "Consensus",
"headerTooltip": "Average forecast among a representative group of economists.",
"cellDataType": "number",
},
{
"field": "actual",
"formatterFn": None,
"headerName": "Actual",
"headerTooltip": "Latest released value.",
"cellDataType": "number",
},
{
"field": "previous",
"formatterFn": None,
"headerName": "Previous",
"headerTooltip": (
"Value for the previous period after"
+ " the revision (if revision is applicable)."
),
"cellDataType": "number",
},
{
"field": "revised",
"formatterFn": None,
"headerName": "Revised",
"headerTooltip": "Revised previous value, if applicable.",
"cellDataType": "number",
},
],
}
},
}
},
)
async def economic_calendar(
start_date: Optional[str] = None,
end_date: Optional[str] = None,
country: Optional[str] = None,
):
"""Get the dividend calendar."""
fetcher = NasdaqEconomicCalendarFetcher()
params = {
"start_date": start_date,
"end_date": end_date,
}
if country:
params["country"] = country
data: list[NasdaqEconomicCalendarFetcher.data_type] = await fetcher.fetch_data(params, {}) # type: ignore
output: list = []
for row in data:
markdown = (
f"### **{row.event}**\n\n{row.description}"
if row.description
else row.event
)
output.append(
{
"date": row.date.strftime("%Y-%m-%d"),
"country": row.country,
"event": {
"value": row.event,
"description": markdown,
},
"consensus": row.consensus,
"actual": row.actual,
"previous": row.previous,
"revised": row.revised,
}
)
return output
@app.get(
"/apps.json",
include_in_schema=False,
)
async def get_apps() -> dict:
return {
"name": "Company Filings & Market Calendars",
"img": "https://logosandtypes.com/wp-content/uploads/2020/07/nasdaq.svg",
"img_dark": "",
"img_light": "",
"description": (
"View company filings and market calendars, downloaded from the public Nasdaq website."
" This app is not affiliated with, or authoriazed by, Nasdaq."
),
"allowCustomization": True,
"tabs": {
"earnings": {
"id": "earnings",
"name": "Earnings Calendar & Filings",
"layout": [
{
"i": "nasdaq_earnings_calendar",
"x": 0,
"y": 2,
"w": 40,
"h": 10,
"state": {
"chartView": {"enabled": False, "chartType": "line"},
"columnState": {
"default": {
"sort": {
"sortModel": [
{"colId": "market_cap", "sort": "desc"}
]
},
"columnPinning": {
"leftColIds": [
"symbol",
"name",
"period_ending",
],
"rightColIds": [],
},
"columnOrder": {
"orderedColIds": [
"report_date",
"reporting_time",
"symbol",
"name",
"market_cap",
"eps_previous",
"eps_consensus",
"num_estimates",
"eps_actual",
"surprise_percent",
"period_ending",
"previous_report_date",
]
},
}
},
},
},
{
"i": "nasdaq_company_filings",
"x": 0,
"y": 12,
"w": 40,
"h": 30,
},
],
},
"dividend-calendar": {
"id": "dividend-calendar",
"name": "Dividend Calendar",
"layout": [
{
"i": "nasdaq_dividends_calendar",
"x": 0,
"y": 2,
"w": 40,
"h": 20,
"state": {
"chartView": {"enabled": False, "chartType": "line"},
"columnState": {
"default": {
"sort": {
"sortModel": [
{
"colId": "ex_dividend_date",
"sort": "asc",
},
{"colId": "amount", "sort": "desc"},
{
"colId": "annualized_amount",
"sort": "desc",
},
]
},
"columnPinning": {
"leftColIds": ["symbol", "name"],
"rightColIds": [],
},
"columnOrder": {
"orderedColIds": [
"ex_dividend_date",
"symbol",
"amount",
"name",
"record_date",
"payment_date",
"declaration_date",
"annualized_amount",
]
},
}
},
},
}
],
},
"ipo-calendar": {
"id": "ipo-calendar",
"name": "IPO Calendar",
"layout": [
{
"i": "nasdaq_ipo_calendar",
"x": 0,
"y": 2,
"w": 40,
"h": 21,
"state": {
"chartView": {"enabled": False, "chartType": "line"},
"columnState": {
"default": {
"columnPinning": {
"leftColIds": [
"ipo_date",
"symbol",
"name",
],
"rightColIds": [],
},
"columnOrder": {
"orderedColIds": [
"ipo_date",
"symbol",
"name",
"offer_amount",
"share_count",
"share_price",
"deal_status",
"exchange",
"id",
]
},
}
},
},
}
],
},
"economic-calendar": {
"id": "economic-calendar",
"name": "Economic Calendar",
"layout": [
{
"i": "nasdaq_economic_calendar",
"x": 0,
"y": 2,
"w": 40,
"h": 20,
}
],
},
},
"groups": [],
}
return app

View File

@@ -166,6 +166,21 @@ doc = ["Sphinx (>=8.2,<9.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)",
test = ["anyio[trio]", "blockbuster (>=1.5.23)", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1) ; python_version >= \"3.10\"", "uvloop (>=0.21) ; platform_python_implementation == \"CPython\" and platform_system != \"Windows\" and python_version < \"3.14\""]
trio = ["trio (>=0.26.1)"]
[[package]]
name = "async-lru"
version = "2.0.5"
description = "Simple LRU cache for asyncio"
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "async_lru-2.0.5-py3-none-any.whl", hash = "sha256:ab95404d8d2605310d345932697371a5f40def0487c03d6d0ad9138de52c9943"},
{file = "async_lru-2.0.5.tar.gz", hash = "sha256:481d52ccdd27275f42c43a928b4a50c3bfb2d67af4e78b170e3e0bb39c66e5bb"},
]
[package.dependencies]
typing_extensions = {version = ">=4.0.0", markers = "python_version < \"3.11\""}
[[package]]
name = "async-timeout"
version = "5.0.1"
@@ -370,6 +385,30 @@ files = [
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
]
[[package]]
name = "deepdiff"
version = "8.5.0"
description = "Deep Difference and Search of any Python object/data. Recreate objects by adding adding deltas to each other."
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "deepdiff-8.5.0-py3-none-any.whl", hash = "sha256:d4599db637f36a1c285f5fdfc2cd8d38bde8d8be8636b65ab5e425b67c54df26"},
{file = "deepdiff-8.5.0.tar.gz", hash = "sha256:a4dd3529fa8d4cd5b9cbb6e3ea9c95997eaa919ba37dac3966c1b8f872dc1cd1"},
]
[package.dependencies]
orderly-set = ">=5.4.1,<6"
[package.extras]
cli = ["click (>=8.1.0,<8.2.0)", "pyyaml (>=6.0.0,<6.1.0)"]
coverage = ["coverage (>=7.6.0,<7.7.0)"]
dev = ["bump2version (>=1.0.0,<1.1.0)", "ipdb (>=0.13.0,<0.14.0)", "jsonpickle (>=4.0.0,<4.1.0)", "nox (==2025.5.1)", "numpy (>=2.0,<3.0) ; python_version < \"3.10\"", "numpy (>=2.2.0,<2.3.0) ; python_version >= \"3.10\"", "orjson (>=3.10.0,<3.11.0)", "pandas (>=2.2.0,<2.3.0)", "polars (>=1.21.0,<1.22.0)", "python-dateutil (>=2.9.0,<2.10.0)", "tomli (>=2.2.0,<2.3.0)", "tomli-w (>=1.2.0,<1.3.0)"]
docs = ["Sphinx (>=6.2.0,<6.3.0)", "sphinx-sitemap (>=2.6.0,<2.7.0)", "sphinxemoji (>=0.3.0,<0.4.0)"]
optimize = ["orjson"]
static = ["flake8 (>=7.1.0,<7.2.0)", "flake8-pyproject (>=1.2.3,<1.3.0)", "pydantic (>=2.10.0,<2.11.0)"]
test = ["pytest (>=8.3.0,<8.4.0)", "pytest-benchmark (>=5.1.0,<5.2.0)", "pytest-cov (>=6.0.0,<6.1.0)", "python-dotenv (>=1.0.0,<1.1.0)"]
[[package]]
name = "distro"
version = "1.9.0"
@@ -934,6 +973,34 @@ uuid7 = ">=0.1.0,<0.2.0"
uvicorn = ">=0.34.2,<0.35.0"
websockets = ">=15.0,<16.0"
[[package]]
name = "openbb-platform-api"
version = "1.1.10"
description = "OpenBB Platform API: Launch script and widgets builder for the OpenBB Platform API and Workspace Backend Connector."
optional = false
python-versions = "<3.13,>=3.9.21"
groups = ["main"]
files = [
{file = "openbb_platform_api-1.1.10-py3-none-any.whl", hash = "sha256:18da196dac5b2be9e7fc8632eaa495826ae1bcfd3365d319c5c8550219919e2c"},
{file = "openbb_platform_api-1.1.10.tar.gz", hash = "sha256:8a0cb7dd9f183587a262965cfaa7b7b4098e9fa112614ebdcb5eb40d35e70c84"},
]
[package.dependencies]
deepdiff = "*"
openbb-core = ">=1.4.7,<2.0.0"
[[package]]
name = "orderly-set"
version = "5.4.1"
description = "Orderly set"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "orderly_set-5.4.1-py3-none-any.whl", hash = "sha256:b5e21d21680bd9ef456885db800c5cb4f76a03879880c0175e1b077fb166fd83"},
{file = "orderly_set-5.4.1.tar.gz", hash = "sha256:a1fb5a4fdc5e234e9e8d8e5c1bbdbc4540f4dfe50d12bf17c8bc5dbf1c9c878d"},
]
[[package]]
name = "pandas"
version = "2.2.3"
@@ -1788,4 +1855,4 @@ type = ["pytest-mypy"]
[metadata]
lock-version = "2.1"
python-versions = ">=3.9.21,<3.13"
content-hash = "e87dd4d308fbc419916e8f08f30702b1ea7005caf3424a3220cafac9139c31b3"
content-hash = "8d6ad6be9cd05a1d7c1ec900e35fafc7551844a836386799eafbb4a2641e185b"

View File

@@ -9,7 +9,9 @@ packages = [{ include = "openbb_nasdaq" }]
[tool.poetry.dependencies]
python = ">=3.9.21,<3.13"
openbb-core = "^1.4.6"
openbb-core = "^1.4.7"
openbb-platform-api = "^1.1.10"
async-lru = "^2.0.5"
random-user-agent = "^1.0.1"
nasdaq-data-link = "^1.0.4"

View File

@@ -0,0 +1,188 @@
{
"name": "Company Filings & Market Calendars",
"img": "https://logosandtypes.com/wp-content/uploads/2020/07/nasdaq.svg",
"img_dark": "",
"img_light": "",
"description": "View company filings and market calendars, downloaded from the public Nasdaq website. This app is not affiliated with, or authoriazed by, Nasdaq.",
"allowCustomization": true,
"tabs": {
"earnings": {
"id": "earnings",
"name": "Earnings Calendar & Filings",
"layout": [
{
"i": "nasdaq_earnings_calendar",
"x": 0,
"y": 2,
"w": 40,
"h": 10,
"state": {
"chartView": {
"enabled": false,
"chartType": "line"
},
"columnState": {
"default": {
"sort": {
"sortModel": [
{
"colId": "market_cap",
"sort": "desc"
}
]
},
"columnPinning": {
"leftColIds": [
"symbol",
"name",
"period_ending"
],
"rightColIds": []
},
"columnOrder": {
"orderedColIds": [
"report_date",
"reporting_time",
"symbol",
"name",
"market_cap",
"eps_previous",
"eps_consensus",
"num_estimates",
"eps_actual",
"surprise_percent",
"period_ending",
"previous_report_date"
]
}
}
}
}
},
{
"i": "nasdaq_company_filings",
"x": 0,
"y": 12,
"w": 40,
"h": 30
}
]
},
"dividend-calendar": {
"id": "dividend-calendar",
"name": "Dividend Calendar",
"layout": [
{
"i": "nasdaq_dividends_calendar",
"x": 0,
"y": 2,
"w": 40,
"h": 20,
"state": {
"chartView": {
"enabled": false,
"chartType": "line"
},
"columnState": {
"default": {
"sort": {
"sortModel": [
{
"colId": "ex_dividend_date",
"sort": "asc"
},
{
"colId": "amount",
"sort": "desc"
},
{
"colId": "annualized_amount",
"sort": "desc"
}
]
},
"columnPinning": {
"leftColIds": [
"symbol",
"name"
],
"rightColIds": []
},
"columnOrder": {
"orderedColIds": [
"ex_dividend_date",
"symbol",
"amount",
"name",
"record_date",
"payment_date",
"declaration_date",
"annualized_amount"
]
}
}
}
}
}
]
},
"ipo-calendar": {
"id": "ipo-calendar",
"name": "IPO Calendar",
"layout": [
{
"i": "nasdaq_ipo_calendar",
"x": 0,
"y": 2,
"w": 40,
"h": 21,
"state": {
"chartView": {
"enabled": false,
"chartType": "line"
},
"columnState": {
"default": {
"columnPinning": {
"leftColIds": [
"ipo_date",
"symbol",
"name"
],
"rightColIds": []
},
"columnOrder": {
"orderedColIds": [
"ipo_date",
"symbol",
"name",
"offer_amount",
"share_count",
"share_price",
"deal_status",
"exchange",
"id"
]
}
}
}
}
}
]
},
"economic-calendar": {
"id": "economic-calendar",
"name": "Economic Calendar",
"layout": [
{
"i": "nasdaq_economic_calendar",
"x": 0,
"y": 2,
"w": 40,
"h": 20
}
]
}
},
"groups": []
}

View File

@@ -0,0 +1,807 @@
{
"nasdaq_company_filings": {
"name": "SEC Filings",
"description": "View SEC filings by company.",
"category": "Open Document",
"type": "multi_file_viewer",
"searchCategory": "Open Document",
"widgetId": "nasdaq_company_filings",
"params": [
{
"label": "Document URL",
"description": "Select the document to open.",
"optional": false,
"type": "endpoint",
"value": null,
"show": false,
"paramName": "document_url",
"optionsEndpoint": "/get_symbol_choices",
"optionsParams": {
"symbol": "$symbol",
"year": "$year",
"form_group": "$form_group"
},
"roles": [
"fileSelector"
],
"multiSelect": true
},
{
"paramName": "symbol",
"label": "Symbol",
"description": "Ticker symbol for the company. Foreign (dual-listed) companies may not be required to file with the SEC.",
"type": "endpoint",
"value": "AAPL",
"optionsEndpoint": "/get_symbol_choices",
"style": {
"popupWidth": 850
}
},
{
"paramName": "year",
"label": "Calendar Year",
"description": "Calendar year for the filings.",
"value": 2025,
"type": "number",
"options": [
{
"label": "2025",
"value": 2025
},
{
"label": "2024",
"value": 2024
},
{
"label": "2023",
"value": 2023
},
{
"label": "2022",
"value": 2022
},
{
"label": "2021",
"value": 2021
},
{
"label": "2020",
"value": 2020
},
{
"label": "2019",
"value": 2019
},
{
"label": "2018",
"value": 2018
},
{
"label": "2017",
"value": 2017
},
{
"label": "2016",
"value": 2016
},
{
"label": "2015",
"value": 2015
},
{
"label": "2014",
"value": 2014
},
{
"label": "2013",
"value": 2013
},
{
"label": "2012",
"value": 2012
},
{
"label": "2011",
"value": 2011
},
{
"label": "2010",
"value": 2010
},
{
"label": "2009",
"value": 2009
},
{
"label": "2008",
"value": 2008
},
{
"label": "2007",
"value": 2007
},
{
"label": "2006",
"value": 2006
},
{
"label": "2005",
"value": 2005
},
{
"label": "2004",
"value": 2004
},
{
"label": "2003",
"value": 2003
},
{
"label": "2002",
"value": 2002
},
{
"label": "2001",
"value": 2001
},
{
"label": "2000",
"value": 2000
},
{
"label": "1999",
"value": 1999
},
{
"label": "1998",
"value": 1998
},
{
"label": "1997",
"value": 1997
},
{
"label": "1996",
"value": 1996
},
{
"label": "1995",
"value": 1995
},
{
"label": "1994",
"value": 1994
}
]
},
{
"paramName": "form_group",
"label": "Form Group",
"description": "Form group for the filings.",
"value": "8k",
"type": "text",
"options": [
{
"label": "Annual",
"value": "annual"
},
{
"label": "Quarterly",
"value": "quarterly"
},
{
"label": "Proxy",
"value": "proxy"
},
{
"label": "Insider",
"value": "insider"
},
{
"label": "8-K",
"value": "8k"
},
{
"label": "Registration",
"value": "registration"
},
{
"label": "Comment",
"value": "comment"
}
]
}
],
"endpoint": "open_document",
"runButton": false,
"gridData": {
"w": 40,
"h": 30
},
"data": {
"dataKey": "",
"table": {
"showAll": true
}
},
"source": [
"Custom"
],
"refetchInterval": false
},
"nasdaq_earnings_calendar": {
"name": "Earnings Calendar",
"description": "Upcoming, and historical, company earnings releases (US Markets).",
"category": "Equity",
"type": "table",
"searchCategory": "Equity",
"widgetId": "nasdaq_earnings_calendar",
"params": [
{
"label": "Start Date",
"description": "Start date of the data.",
"optional": true,
"type": "date",
"value": null,
"show": true,
"paramName": "start_date"
},
{
"label": "End Date",
"description": "End date of the data.",
"optional": true,
"type": "date",
"value": null,
"show": true,
"paramName": "end_date"
}
],
"endpoint": "earnings_calendar",
"runButton": false,
"gridData": {
"w": 40,
"h": 20
},
"data": {
"dataKey": "",
"table": {
"showAll": true,
"columnsDefs": [
{
"field": "report_date",
"formatterFn": null,
"headerName": "Report Date",
"headerTooltip": "The date of the earnings report.",
"cellDataType": "date"
},
{
"field": "symbol",
"pinned": "left",
"formatterFn": null,
"headerName": "Symbol",
"headerTooltip": "Symbol representing the entity requested in the data.",
"cellDataType": "text"
},
{
"field": "name",
"pinned": "left",
"formatterFn": null,
"headerName": "Name",
"headerTooltip": "Name of the entity.",
"cellDataType": "text"
},
{
"field": "eps_previous",
"formatterFn": null,
"headerName": "EPS Previous",
"headerTooltip": "The earnings-per-share from the same previously reported period.",
"cellDataType": "number"
},
{
"field": "eps_consensus",
"formatterFn": null,
"headerName": "EPS Consensus",
"headerTooltip": "The analyst conesus earnings-per-share estimate.",
"cellDataType": "number"
},
{
"field": "eps_actual",
"formatterFn": null,
"headerName": "EPS Actual",
"headerTooltip": "The actual earnings per share (USD) announced.",
"cellDataType": "number"
},
{
"field": "surprise_percent",
"formatterFn": "normalizedPercent",
"headerName": "Surprise Percent",
"headerTooltip": "The earnings surprise as normalized percentage points.",
"cellDataType": "number",
"renderFn": "greenRed"
},
{
"field": "num_estimates",
"formatterFn": null,
"headerName": "Num Estimates",
"headerTooltip": "The number of analysts providing estimates for the consensus.",
"cellDataType": "number"
},
{
"field": "period_ending",
"pinned": "left",
"formatterFn": null,
"headerName": "Period Ending",
"headerTooltip": "The fiscal period end date.",
"cellDataType": "text"
},
{
"field": "previous_report_date",
"formatterFn": null,
"headerName": "Previous Report Date",
"headerTooltip": "The previous report date for the same period last year.",
"cellDataType": "date"
},
{
"field": "reporting_time",
"formatterFn": null,
"headerName": "Reporting Time",
"headerTooltip": "The reporting time - e.g. after market close.",
"cellDataType": "text"
},
{
"field": "market_cap",
"formatterFn": null,
"headerName": "Market Cap",
"headerTooltip": "The market cap (USD) of the reporting entity.",
"cellDataType": "number"
}
]
}
},
"source": [
"Custom"
],
"subCategory": "Calendar",
"refetchInterval": false
},
"nasdaq_dividends_calendar": {
"name": "Dividend Calendar",
"description": "Upcoming, and historical, dividend payments (US Markets).",
"category": "Equity",
"type": "table",
"searchCategory": "Equity",
"widgetId": "nasdaq_dividends_calendar",
"params": [
{
"label": "Start Date",
"description": "Start date of the data.",
"optional": true,
"type": "date",
"value": null,
"show": true,
"paramName": "start_date"
},
{
"label": "End Date",
"description": "End date of the data.",
"optional": true,
"type": "date",
"value": null,
"show": true,
"paramName": "end_date"
}
],
"endpoint": "dividend_calendar",
"runButton": false,
"gridData": {
"w": 40,
"h": 20
},
"data": {
"dataKey": "",
"table": {
"showAll": true,
"columnsDefs": [
{
"field": "ex_dividend_date",
"formatterFn": null,
"headerName": "Ex Dividend Date",
"headerTooltip": "The ex-dividend date - the date on which the stock begins trading without rights to the dividend.",
"cellDataType": "date"
},
{
"field": "symbol",
"pinned": "left",
"formatterFn": null,
"headerName": "Symbol",
"headerTooltip": "Symbol representing the entity requested in the data.",
"cellDataType": "text"
},
{
"field": "amount",
"formatterFn": null,
"headerName": "Amount",
"headerTooltip": "The dividend amount per share.",
"cellDataType": "number"
},
{
"field": "name",
"pinned": "left",
"formatterFn": null,
"headerName": "Name",
"headerTooltip": "Name of the entity.",
"cellDataType": "text"
},
{
"field": "record_date",
"formatterFn": null,
"headerName": "Record Date",
"headerTooltip": "The record date of ownership for eligibility.",
"cellDataType": "date"
},
{
"field": "payment_date",
"formatterFn": null,
"headerName": "Payment Date",
"headerTooltip": "The payment date of the dividend.",
"cellDataType": "date"
},
{
"field": "declaration_date",
"formatterFn": null,
"headerName": "Declaration Date",
"headerTooltip": "Declaration date of the dividend.",
"cellDataType": "date"
},
{
"field": "annualized_amount",
"formatterFn": null,
"headerName": "Annualized Amount",
"headerTooltip": "The indicated annualized dividend amount.",
"cellDataType": "number"
}
]
}
},
"source": [
"Custom"
],
"subCategory": "Calendar",
"refetchInterval": false
},
"nasdaq_ipo_calendar": {
"name": "IPO Calendar",
"description": "IPO announcements and calendar.",
"category": "Equity",
"type": "table",
"searchCategory": "Equity",
"widgetId": "nasdaq_ipo_calendar",
"params": [
{
"label": "Start Date",
"description": "Start date of the data, in YYYY-MM-DD format.",
"optional": true,
"type": "date",
"value": null,
"show": true,
"paramName": "start_date"
},
{
"label": "End Date",
"description": "End date of the data, in YYYY-MM-DD format.",
"optional": true,
"type": "date",
"value": null,
"show": true,
"paramName": "end_date"
},
{
"label": "Status",
"description": "Status of the IPO. [upcoming, priced, or withdrawn]",
"optional": true,
"type": "text",
"show": true,
"options": [
{
"label": "upcoming",
"value": "upcoming"
},
{
"label": "priced",
"value": "priced"
},
{
"label": "filed",
"value": "filed"
},
{
"label": "withdrawn",
"value": "withdrawn"
}
],
"paramName": "status"
},
{
"label": "Is SPO",
"description": "If True, returns data for secondary public offerings (SPOs).",
"optional": true,
"type": "boolean",
"value": false,
"show": true,
"paramName": "is_spo"
}
],
"endpoint": "ipo_calendar",
"runButton": false,
"gridData": {
"w": 40,
"h": 20
},
"data": {
"dataKey": "",
"table": {
"showAll": true,
"columnsDefs": [
{
"field": "symbol",
"pinned": "left",
"formatterFn": null,
"headerName": "Symbol",
"headerTooltip": "Symbol representing the entity requested in the data.",
"cellDataType": "text"
},
{
"field": "ipo_date",
"formatterFn": null,
"headerName": "IPO Date",
"headerTooltip": "The date of the IPO, when the stock first trades on a major exchange.",
"cellDataType": "date"
},
{
"field": "name",
"pinned": "left",
"formatterFn": null,
"headerName": "Name",
"headerTooltip": "The name of the company.",
"cellDataType": "text"
},
{
"field": "offer_amount",
"formatterFn": null,
"headerName": "Offer Amount",
"headerTooltip": "The dollar value of the shares offered.",
"cellDataType": "number"
},
{
"field": "share_count",
"formatterFn": null,
"headerName": "Share Count",
"headerTooltip": "The number of shares offered.",
"cellDataType": "number"
},
{
"field": "expected_price_date",
"formatterFn": null,
"headerName": "Expected Price Date",
"headerTooltip": "The date the pricing is expected.",
"cellDataType": "date"
},
{
"field": "filed_date",
"formatterFn": null,
"headerName": "Filed Date",
"headerTooltip": "The date the IPO was filed.",
"cellDataType": "date"
},
{
"field": "withdraw_date",
"formatterFn": null,
"headerName": "Withdraw Date",
"headerTooltip": "The date the IPO was withdrawn.",
"cellDataType": "date"
},
{
"field": "deal_status",
"formatterFn": null,
"headerName": "Deal Status",
"headerTooltip": "The status of the deal.",
"cellDataType": "text"
}
]
}
},
"source": [
"Custom"
],
"subCategory": "Calendar",
"refetchInterval": false
},
"nasdaq_economic_calendar": {
"name": "Economic Calendar",
"description": "Upcoming, and historical, macroeconomic events.",
"category": "Economy",
"type": "table",
"searchCategory": "Equity",
"widgetId": "nasdaq_economic_calendar",
"params": [
{
"label": "Start Date",
"description": "Start Date",
"optional": true,
"type": "date",
"value": null,
"show": true,
"paramName": "start_date"
},
{
"label": "End Date",
"description": "End Date",
"optional": true,
"type": "date",
"value": null,
"show": true,
"paramName": "end_date"
},
{
"label": "Country",
"description": "Country",
"optional": true,
"type": "text",
"value": null,
"show": true,
"paramName": "country",
"options": [
{
"label": "United States",
"value": "united_states"
},
{
"label": "China",
"value": "china"
},
{
"label": "Hong Kong",
"value": "hong_kong"
},
{
"label": "Japan",
"value": "japan"
},
{
"label": "Singapore",
"value": "signapore"
},
{
"label": "South Korea",
"value": "south_korea"
},
{
"label": "India",
"value": "india"
},
{
"label": "Russia",
"value": "russia"
},
{
"label": "Euro Zone",
"value": "euro_zone"
},
{
"label": "Germany",
"value": "germany"
},
{
"label": "United Kingdom",
"value": "united_kingdom"
},
{
"label": "France",
"value": "france"
},
{
"label": "Spain",
"value": "spain"
},
{
"label": "Italy",
"value": "italy"
},
{
"label": "Switzerland",
"value": "switzerland"
},
{
"label": "Canada",
"value": "canada"
},
{
"label": "Australia",
"value": "australia"
},
{
"label": "New Zealand",
"value": "new_zealand"
},
{
"label": "South Africa",
"value": "south_africa"
},
{
"label": "Brazil",
"value": "brazil"
}
]
}
],
"endpoint": "economic_calendar",
"runButton": false,
"gridData": {
"w": 40,
"h": 20
},
"data": {
"dataKey": "",
"table": {
"showAll": false,
"columnsDefs": [
{
"field": "date",
"pinned": "left",
"headerName": "Date",
"headerTooltip": "The date of the data.",
"cellDataType": "date"
},
{
"field": "country",
"headerName": "Country",
"headerTooltip": "Country of event.",
"cellDataType": "text"
},
{
"field": "event",
"renderFn": "hoverCard",
"renderFnParams": {
"hoverCard": {
"cellField": "value",
"markdown": "{description}"
}
},
"headerName": "Event",
"headerTooltip": "Event name.",
"cellDataType": "text"
},
{
"field": "consensus",
"headerName": "Consensus",
"headerTooltip": "Average forecast among a representative group of economists.",
"cellDataType": "number"
},
{
"field": "actual",
"headerName": "Actual",
"headerTooltip": "Latest released value.",
"cellDataType": "number"
},
{
"field": "previous",
"headerName": "Previous",
"headerTooltip": "Value for the previous period after the revision (if revision is applicable).",
"cellDataType": "number"
},
{
"field": "revised",
"headerName": "Revised",
"headerTooltip": "Revised previous value, if applicable.",
"cellDataType": "number"
}
]
}
},
"source": [
"Custom"
],
"subCategory": "Calendar",
"refetchInterval": false
}
}

View File

@@ -0,0 +1,63 @@
"""App tests for Nasdaq provider."""
# flake8: noqa:E501
# pylint: disable=redefined-outer-name, unused-argument, line-too-long
import json
import os
from pathlib import Path
import pytest
from fastapi import FastAPI
from openbb_nasdaq.app import main
from openbb_platform_api.utils.widgets import build_json
@pytest.fixture(scope="module")
def nasdaq_app():
"""Fixture to serve FastAPI app instance."""
app = main()
yield app
@pytest.fixture(scope="module")
def expected_apps():
"""Load expected apps.json data."""
current_dir = Path(os.path.dirname(os.path.abspath(__file__)))
with open(current_dir / "record" / "expected_apps.json", encoding="utf-8") as f:
return json.load(f)
@pytest.fixture(scope="module")
def expected_widgets():
"""Load expected widgets.json data."""
current_dir = Path(os.path.dirname(os.path.abspath(__file__)))
with open(current_dir / "record" / "expected_widgets.json", encoding="utf-8") as f:
return json.load(f)
def test_app_is_fastapi_instance(nasdaq_app):
"""Test that the factory function returns a FastAPI instance."""
assert isinstance(nasdaq_app, FastAPI)
def test_app_has_routes(nasdaq_app):
"""Test that the app has at least one route."""
assert len(nasdaq_app.routes) > 0
@pytest.mark.asyncio
async def test_apps_json(nasdaq_app, expected_apps):
"""Test the /apps.json endpoint. This looks for changes and verifies the endpoint works."""
route = [d for d in nasdaq_app.routes if d.path == "/apps.json"][0]
response = await route.endpoint()
assert isinstance(response, dict)
assert response == expected_apps
def test_widgets_json(nasdaq_app, expected_widgets):
"""Test the /widgets.json endpoint. This looks for changes and verifies that the widgets are being generated correctly."""
openapi_json: dict = nasdaq_app.openapi()
assert isinstance(openapi_json, dict)
response = build_json(openapi_json, [])
assert isinstance(response, dict)
assert response == expected_widgets