Merge branch 'feature/argparse_translator' of github.com:OpenBB-finance/OpenBBTerminal into feature/argparse_translator

This commit is contained in:
hjoaquim
2024-02-22 13:54:39 +00:00
64 changed files with 113863 additions and 69368 deletions

View File

@@ -15,7 +15,7 @@ on:
branches:
- develop
- main
types: [opened, synchronize, edited, closed, labeled, unlabeled]
types: [opened, synchronize, edited, closed, labeled]
push:
branches:
- release/*
@@ -33,6 +33,7 @@ jobs:
if: github.event.pull_request.merged == false && github.event_name != 'push' && github.event.pull_request.draft == false
outputs:
check-changes: ${{ steps.check-changes.outputs.check-changes }}
check-platform-changes: ${{ steps.check-platform-changes.outputs.check-platform-changes }} # New output for openbb_platform changes
steps:
- name: Checkout
uses: actions/checkout@v1 # v1 is used to preserve the git history and works with the git diff command
@@ -40,7 +41,6 @@ jobs:
fetch-depth: 100
# The GitHub token is preserved by default but this job doesn't need
# to be able to push to GitHub.
persist-credentials: false
# Check for changes to python files, lockfiles and the openbb_terminal folder
- name: Check for changes to files to trigger unit test
@@ -54,14 +54,28 @@ jobs:
echo "check-changes=false" >> $GITHUB_OUTPUT
fi
# Check for changes to openbb_platform
- name: Check for changes to openbb_platform
id: check-platform-changes
run: |
current_branch=$(jq -r .pull_request.base.ref "$GITHUB_EVENT_PATH")
if git diff --name-only origin/$current_branch HEAD | grep -E "openbb_platform\/.*"; then
echo "check-platform-changes=true" >> $GITHUB_OUTPUT
else
echo "check-platform-changes=false" >> $GITHUB_OUTPUT
fi
- name: Show output of previous step
run: echo "check-changes=${{ steps.check-changes.outputs.check-changes }}"
run: |
echo "check-changes=${{ steps.check-changes.outputs.check-changes }}"
echo "check-platform-changes=${{ steps.check-platform-changes.outputs.check-platform-changes }}"
base-test:
name: Base Tests - Ubuntu-latest - Python 3.9
needs: [check-files-changed]
runs-on: ubuntu-latest
if: needs.check-files-changed.outputs.check-changes == 'true' && github.event.pull_request.base.ref == 'develop' && !contains(github.event.pull_request.labels.*.name, 'v4')
if: needs.check-files-changed.outputs.check-changes == 'true' && github.event.pull_request.base.ref == 'develop' && needs.check-files-changed.outputs.check-platform-changes == 'false'
steps:
- name: Checkout Code
uses: actions/checkout@v3

View File

@@ -1,5 +1,5 @@
from pydantic import BaseModel, Field
from uuid_extensions import uuid7str
from uuid_extensions import uuid7str # type: ignore
class Tagged(BaseModel):

View File

@@ -1,12 +1,12 @@
import sys
from dataclasses import dataclass
from typing import Optional
from typing import Dict, Optional
# `slots` is available on Python >= 3.10
if sys.version_info >= (3, 10):
slots_true = {"slots": True}
else:
slots_true = {}
slots_true: Dict[str, bool] = {}
class BaseMetadata:

View File

@@ -113,13 +113,13 @@ class OBBject(Tagged, Generic[T]):
return f"OBBject[{cls.results_type_repr(params)}]"
def to_df(
self, index: Optional[str] = None, sort_by: Optional[str] = None
self, index: Optional[Union[str, None]] = "date", sort_by: Optional[str] = None
) -> pd.DataFrame:
"""Alias for `to_dataframe`."""
return self.to_dataframe(index=index, sort_by=sort_by)
def to_dataframe(
self, index: Optional[str] = None, sort_by: Optional[str] = None
self, index: Optional[Union[str, None]] = "date", sort_by: Optional[str] = None
) -> pd.DataFrame:
"""Convert results field to pandas dataframe.
@@ -173,7 +173,7 @@ class OBBject(Tagged, Generic[T]):
for k, v in r.items():
# Dict[str, List[BaseModel]]
if is_list_of_basemodel(v):
dict_of_df[k] = basemodel_to_df(v, index or "date")
dict_of_df[k] = basemodel_to_df(v, index)
sort_columns = False
# Dict[str, Any]
else:
@@ -184,14 +184,14 @@ class OBBject(Tagged, Generic[T]):
# List[BaseModel]
elif is_list_of_basemodel(res):
dt: Union[List[Data], Data] = res # type: ignore
df = basemodel_to_df(dt, index or "date")
df = basemodel_to_df(dt, index)
sort_columns = False
# List[List | str | int | float] | Dict[str, Dict | List | BaseModel]
else:
try:
df = pd.DataFrame(res)
# Set index, if any
if index and index in df.columns:
if index is not None and index in df.columns:
df.set_index(index, inplace=True)
except ValueError:
@@ -234,11 +234,11 @@ class OBBject(Tagged, Generic[T]):
"Please install polars: `pip install polars pyarrow` to use this method."
) from exc
return from_pandas(self.to_dataframe().reset_index())
return from_pandas(self.to_dataframe(index=None))
def to_numpy(self) -> ndarray:
"""Convert results field to numpy array."""
return self.to_dataframe().reset_index().to_numpy()
return self.to_dataframe(index=None).to_numpy()
def to_dict(
self,
@@ -259,7 +259,7 @@ class OBBject(Tagged, Generic[T]):
Dict[str, List]
Dictionary of lists.
"""
df = self.to_dataframe() # type: ignore
df = self.to_dataframe(index=None) # type: ignore
transpose = False
if orient == "list":
transpose = True

View File

@@ -24,7 +24,7 @@ class Container:
return getattr(obbject, "to_" + output_type)()
def _get_provider(
self, choice: Optional[str], cmd: str, available: Tuple[str]
self, choice: Optional[str], cmd: str, available: Tuple[str, ...]
) -> str:
"""Get the provider to use in execution."""
if choice is None:

View File

@@ -79,7 +79,9 @@ class PackageBuilder:
def auto_build(self) -> None:
"""Trigger build if there are differences between built and installed extensions."""
if Env().AUTO_BUILD:
add, remove = PackageBuilder._diff(self.directory / "package")
add, remove = PackageBuilder._diff(
self.directory / "assets" / "extension_map.json"
)
if add:
a = ", ".join(add)
print(f"Extensions to add: {a}") # noqa: T201
@@ -98,7 +100,7 @@ class PackageBuilder:
) -> None:
"""Build the extensions for the Platform."""
self.console.log("\nBuilding extensions package...\n")
self._clean_package(modules)
self._clean(modules)
ext_map = self._get_extension_map()
self._save_extension_map(ext_map)
self._save_module_map()
@@ -107,8 +109,9 @@ class PackageBuilder:
if self.lint:
self._run_linters()
def _clean_package(self, modules: Optional[Union[str, List[str]]] = None) -> None:
"""Delete the package folder or modules before building."""
def _clean(self, modules: Optional[Union[str, List[str]]] = None) -> None:
"""Delete the assets and package folder or modules before building."""
shutil.rmtree(self.directory / "assets", ignore_errors=True)
if modules:
for module in modules:
module_path = self.directory / "package" / f"{module}.py"
@@ -141,7 +144,7 @@ class PackageBuilder:
"""Save the map of extensions available at build time."""
code = dumps(obj=dict(sorted(ext_map.items())), indent=4)
self.console.log("Writing extension map...")
self._write(code=code, name="extension_map", extension="json")
self._write(code=code, name="extension_map", extension="json", folder="assets")
def _save_module_map(self):
"""Save the module map."""
@@ -152,7 +155,7 @@ class PackageBuilder:
}
code = dumps(obj=dict(sorted(module_map.items())), indent=4)
self.console.log("\nWriting module map...")
self._write(code=code, name="module_map", extension="json")
self._write(code=code, name="module_map", extension="json", folder="assets")
def _save_modules(
self,
@@ -197,9 +200,11 @@ class PackageBuilder:
linters.ruff()
linters.black()
def _write(self, code: str, name: str, extension="py") -> None:
def _write(
self, code: str, name: str, extension: str = "py", folder: str = "package"
) -> None:
"""Write the module to the package."""
package_folder = self.directory / "package"
package_folder = self.directory / folder
package_path = package_folder / f"{name}.{extension}"
package_folder.mkdir(exist_ok=True)
@@ -209,25 +214,24 @@ class PackageBuilder:
file.write(code.replace("typing.", ""))
@staticmethod
def _read_extension_map(package: Path) -> dict:
"""Get extension map from package folder."""
ext_map_file = Path(package, "extension_map.json")
def _read(path: Path) -> dict:
"""Get content from folder."""
try:
with open(ext_map_file) as fp:
ext_map = load(fp)
with open(Path(path)) as fp:
content = load(fp)
except Exception:
ext_map = {}
content = {}
return ext_map
return content
@staticmethod
def _diff(package: Path) -> Tuple[Set[str], Set[str]]:
def _diff(path: Path) -> Tuple[Set[str], Set[str]]:
"""Check differences between built and installed extensions.
Parameters
----------
package: Path
The path to the package
path: Path
The path to the folder where the extension map is stored.
Returns
-------
@@ -235,7 +239,7 @@ class PackageBuilder:
First element: set of installed extensions that are not in the package.
Second element: set of extensions in the package that are not installed.
"""
ext_map = PackageBuilder._read_extension_map(package)
ext_map = PackageBuilder._read(path)
add: Set[str] = set()
remove: Set[str] = set()

View File

@@ -7,7 +7,7 @@ from openbb_core.app.utils import check_single_item, convert_to_basemodel
def filter_inputs(
data_processing: bool = False,
extra_info: Optional[Dict[str, List[str]]] = None,
extra_info: Optional[Dict[str, Dict[str, List[str]]]] = None,
**kwargs,
) -> dict:
"""Filter command inputs."""

View File

@@ -2,6 +2,7 @@
import ast
import json
from datetime import time
from typing import Dict, Iterable, List, Optional, Union
import numpy as np
@@ -16,7 +17,7 @@ from openbb_core.provider.abstract.data import Data
def basemodel_to_df(
data: Union[List[Data], Data],
index: Optional[Union[str, Iterable]] = None,
index: Optional[Union[None, str, Iterable]] = None,
) -> pd.DataFrame:
"""Convert list of BaseModel to a Pandas DataFrame."""
if isinstance(data, list):
@@ -32,12 +33,20 @@ def basemodel_to_df(
df = df.set_index(col_names)
df = df.drop(["is_multiindex", "multiindex_names"], axis=1)
# If the date column contains dates only, convert them to a date to avoid encoding time data.
if "date" in df.columns:
df["date"] = df["date"].apply(pd.to_datetime)
if all(t.time() == time(0, 0) for t in df["date"]):
df["date"] = df["date"].apply(lambda x: x.date())
if index and index in df.columns:
df = df.set_index(index)
# TODO: This should probably check if the index can be converted to a datetime instead of just assuming
if df.index.name == "date":
df.index = pd.to_datetime(df.index)
if index == "date":
df.set_index("date", inplace=True)
df.sort_index(axis=0, inplace=True)
else:
df = (
df.set_index(index) if index is not None and index in df.columns else df
)
return df
@@ -59,6 +68,12 @@ def df_to_basemodel(
df["multiindex_names"] = str(df.index.names)
df = df.reset_index()
# Converting to JSON will add T00:00:00.000 to all dates with no time element unless we format it as a string first.
if "date" in df.columns:
df["date"] = df["date"].apply(pd.to_datetime)
if all(t.time() == time(0, 0) for t in df["date"]):
df["date"] = df["date"].apply(lambda x: x.date().strftime("%Y-%m-%d"))
return [
Data(**d) for d in json.loads(df.to_json(orient="records", date_format="iso"))
]

View File

@@ -45,7 +45,9 @@ class CryptoHistoricalQueryParams(QueryParams):
class CryptoHistoricalData(Data):
"""Crypto Historical Price Data."""
date: datetime = Field(description=DATA_DESCRIPTIONS.get("date", ""))
date: Union[dateType, datetime] = Field(
description=DATA_DESCRIPTIONS.get("date", "")
)
open: PositiveFloat = Field(description=DATA_DESCRIPTIONS.get("open", ""))
high: PositiveFloat = Field(description=DATA_DESCRIPTIONS.get("high", ""))
low: PositiveFloat = Field(description=DATA_DESCRIPTIONS.get("low", ""))
@@ -59,4 +61,6 @@ class CryptoHistoricalData(Data):
@classmethod
def date_validate(cls, v): # pylint: disable=E0213
"""Return formatted datetime."""
return parser.isoparse(str(v))
if ":" in str(v):
return parser.isoparse(str(v))
return parser.parse(str(v)).date()

View File

@@ -46,7 +46,9 @@ class CurrencyHistoricalQueryParams(QueryParams):
class CurrencyHistoricalData(Data):
"""Currency Historical Price Data."""
date: datetime = Field(description=DATA_DESCRIPTIONS.get("date", ""))
date: Union[dateType, datetime] = Field(
description=DATA_DESCRIPTIONS.get("date", "")
)
open: PositiveFloat = Field(description=DATA_DESCRIPTIONS.get("open", ""))
high: PositiveFloat = Field(description=DATA_DESCRIPTIONS.get("high", ""))
low: PositiveFloat = Field(description=DATA_DESCRIPTIONS.get("low", ""))
@@ -61,4 +63,6 @@ class CurrencyHistoricalData(Data):
@field_validator("date", mode="before", check_fields=False)
def date_validate(cls, v): # pylint: disable=E0213
"""Return formatted datetime."""
return parser.isoparse(str(v))
if ":" in str(v):
return parser.isoparse(str(v))
return parser.parse(str(v)).date()

View File

@@ -61,7 +61,6 @@ class EquityHistoricalData(Data):
@field_validator("date", mode="before", check_fields=False)
def date_validate(cls, v): # pylint: disable=E0213
"""Return formatted datetime."""
v = parser.isoparse(str(v))
if v.hour == 0 and v.minute == 0:
return v.date()
return v
if ":" in str(v):
return parser.isoparse(str(v))
return parser.parse(str(v)).date()

View File

@@ -1,7 +1,10 @@
"""ETF Historical Price Standard Model."""
from datetime import date as dateType
from typing import Optional
from datetime import (
date as dateType,
datetime,
)
from typing import Optional, Union
from dateutil import parser
from pydantic import Field, NonNegativeInt, PositiveFloat, field_validator
@@ -37,7 +40,9 @@ class EtfHistoricalQueryParams(QueryParams):
class EtfHistoricalData(Data):
"""ETF Historical Price Data."""
date: dateType = Field(description=DATA_DESCRIPTIONS.get("date", ""))
date: Union[dateType, datetime] = Field(
description=DATA_DESCRIPTIONS.get("date", "")
)
open: PositiveFloat = Field(description=DATA_DESCRIPTIONS.get("open", ""))
high: PositiveFloat = Field(description=DATA_DESCRIPTIONS.get("high", ""))
low: PositiveFloat = Field(description=DATA_DESCRIPTIONS.get("low", ""))
@@ -49,4 +54,6 @@ class EtfHistoricalData(Data):
@field_validator("date", mode="before", check_fields=False)
def date_validate(cls, v): # pylint: disable=E0213
"""Return formatted datetime."""
return parser.isoparse(str(v))
if ":" in str(v):
return parser.isoparse(str(v))
return parser.parse(str(v)).date()

View File

@@ -1,7 +1,7 @@
"""Euro Area Yield Curve Standard Model."""
from datetime import date as dateType
from typing import Literal, Optional
from typing import Optional
from pydantic import Field
@@ -16,16 +16,14 @@ class EUYieldCurveQueryParams(QueryParams):
date: Optional[dateType] = Field(
default=None, description=QUERY_DESCRIPTIONS.get("date", "")
)
yield_curve_type: Literal["spot_rate", "instantaneous_forward", "par_yield"] = (
Field(
default="spot_rate",
description="The yield curve type.",
)
)
class EUYieldCurveData(Data):
"""Euro Area Yield Curve Data."""
maturity: str = Field(description="Yield curve rate maturity.")
rate: Optional[float] = Field(description="Yield curve rate.", default=None)
maturity: Optional[float] = Field(description="Maturity, in years.", default=None)
rate: Optional[float] = Field(
description="Yield curve rate, as a normalized percent.",
default=None,
json_schema_extra={"unit_measurement": "percent.", "frontend_multiply": 100},
)

View File

@@ -73,7 +73,6 @@ class IndexHistoricalData(Data):
@classmethod
def date_validate(cls, v):
"""Return formatted datetime."""
v = parser.isoparse(str(v))
if v.hour == 0 and v.minute == 0:
return v.date()
return v
if ":" in str(v):
return parser.isoparse(str(v))
return parser.parse(str(v)).date()

View File

@@ -16,7 +16,9 @@ from openbb_core.provider.utils.descriptions import (
class TrailingDivYieldQueryParams(QueryParams):
"""Trailing Dividend Yield Query."""
symbol: str = Field(default=None, description=QUERY_DESCRIPTIONS.get("symbol", ""))
symbol: Optional[str] = Field(
default=None, description=QUERY_DESCRIPTIONS.get("symbol", "")
)
limit: Optional[int] = Field(
default=252,
description=f"{QUERY_DESCRIPTIONS.get('limit', '')}"

View File

@@ -5,6 +5,7 @@ from unittest.mock import MagicMock
import pandas as pd
import pytest
from openbb_core.app.model.obbject import Chart, OBBject, OpenBBError
from openbb_core.app.utils import basemodel_to_df
from openbb_core.provider.abstract.data import Data
from pandas.testing import assert_frame_equal
@@ -48,6 +49,13 @@ class MockMultiData(Data):
value: float
class MockDataFrame(Data):
"""Test helper."""
date: str
value: float
@pytest.mark.parametrize(
"results, expected_df",
[
@@ -171,27 +179,27 @@ class MockMultiData(Data):
"df1": pd.DataFrame(
{
"date": [
pd.to_datetime("1956-01-01"),
pd.to_datetime("1956-02-01"),
pd.to_datetime("1956-03-01"),
pd.to_datetime("1956-01-01").date(),
pd.to_datetime("1956-02-01").date(),
pd.to_datetime("1956-03-01").date(),
],
"another_date": ["2023-09-01", "2023-09-01", "2023-09-01"],
"value": [0.0, 0.0, 0.0],
},
columns=["date", "another_date", "value"],
).set_index("date"),
),
"df2": pd.DataFrame(
{
"date": [
pd.to_datetime("1955-03-01"),
pd.to_datetime("1955-04-01"),
pd.to_datetime("1955-05-01"),
pd.to_datetime("1955-03-01").date(),
pd.to_datetime("1955-04-01").date(),
pd.to_datetime("1955-05-01").date(),
],
"another_date": ["2023-09-01", "2023-09-01", "2023-09-01"],
"value": [0.0, 0.0, 0.0],
},
columns=["date", "another_date", "value"],
).set_index("date"),
),
},
axis=1,
sort=True,
@@ -210,11 +218,11 @@ def test_to_dataframe(results, expected_df):
# Act and Assert
if isinstance(expected_df, pd.DataFrame):
result = co.to_dataframe()
result = co.to_dataframe(index=None)
assert_frame_equal(result, expected_df)
else:
with pytest.raises(expected_df.__class__) as exc_info:
co.to_dataframe()
co.to_dataframe(index=None)
assert str(exc_info.value) == str(expected_df)
@@ -256,6 +264,32 @@ def test_to_dataframe_w_args(results, index, sort_by):
assert result[sort_by].is_monotonic_increasing
@pytest.mark.parametrize(
"results",
# Test case 1: List of models with daylight savings crossover.
(
[
MockDataFrame(date="2023-11-03 00:00:00-04:00", value=10),
MockDataFrame(date="2023-11-03 08:00:00-04:00", value=9),
MockDataFrame(date="2023-11-03 16:00:00-04:00", value=8),
MockDataFrame(date="2023-11-06 00:00:00-05:00", value=11),
MockDataFrame(date="2023-11-06 08:00:00-05:00", value=7),
MockDataFrame(date="2023-11-06 16:00:00-05:00", value=12),
],
),
)
def test_to_df_daylight_savings(results):
"""Test helper."""
# Arrange
co = OBBject(results=results)
# Act and Assert
expected_df = basemodel_to_df(results, index="date")
result = co.to_dataframe(index="date")
assert isinstance(result, pd.DataFrame)
assert_frame_equal(expected_df, result)
@pytest.mark.parametrize(
"results, expected_dict",
[ # Case 1: Normal results with "date" column

View File

@@ -26,14 +26,14 @@ from typing_extensions import Annotated
@pytest.fixture(scope="module")
def tmp_package_dir(tmp_path_factory):
return tmp_path_factory.mktemp("package")
def tmp_openbb_dir(tmp_path_factory):
return tmp_path_factory.mktemp("openbb")
@pytest.fixture(scope="module")
def package_builder(tmp_package_dir):
def package_builder(tmp_openbb_dir):
"""Return package builder."""
return PackageBuilder(tmp_package_dir)
return PackageBuilder(tmp_openbb_dir)
def test_package_builder_init(package_builder):
@@ -544,14 +544,16 @@ def test_generate(docstring_generator):
assert "Returns" in doc
def test_read_extension_map(package_builder, tmp_package_dir):
def test_read_extension_map(package_builder, tmp_openbb_dir):
"""Test read extension map."""
PATH = "openbb_core.app.static.package_builder."
open_mock = mock_open()
with patch(PATH + "open", open_mock), patch(PATH + "load") as mock_load:
package_builder._read_extension_map(tmp_package_dir)
open_mock.assert_called_once_with(Path(tmp_package_dir, "extension_map.json"))
package_builder._read(Path(tmp_openbb_dir / "assets" / "extension_map.json"))
open_mock.assert_called_once_with(
Path(tmp_openbb_dir / "assets" / "extension_map.json")
)
mock_load.assert_called_once()
@@ -606,7 +608,7 @@ def test_read_extension_map(package_builder, tmp_package_dir):
)
def test_package_diff(
package_builder,
tmp_package_dir,
tmp_openbb_dir,
ext_built,
ext_installed,
ext_inst_version,
@@ -619,21 +621,19 @@ def test_package_diff(
return ext_installed.select(**{"group": group})
PATH = "openbb_core.app.static.package_builder."
with patch.object(
PackageBuilder, "_read_extension_map"
) as mock_read_extension_map, patch(
with patch.object(PackageBuilder, "_read") as mock_read, patch(
PATH + "entry_points", mock_entry_points
), patch.object(
EntryPoint, "dist", new_callable=PropertyMock
) as mock_obj:
), patch.object(EntryPoint, "dist", new_callable=PropertyMock) as mock_obj:
class MockPathDistribution:
version = ext_inst_version
mock_obj.return_value = MockPathDistribution()
mock_read_extension_map.return_value = ext_built
mock_read.return_value = ext_built
add, remove = package_builder._diff(tmp_package_dir)
add, remove = package_builder._diff(
Path(tmp_openbb_dir, "assets", "extension_map.json")
)
# We add whatever is not built, but is installed
assert add == expected_add
@@ -651,20 +651,22 @@ def test_package_diff(
({"this"}, {"that"}, False),
],
)
def test_auto_build(package_builder, tmp_package_dir, add, remove, openbb_auto_build):
def test_auto_build(package_builder, tmp_openbb_dir, add, remove, openbb_auto_build):
"""Test auto build."""
with patch.object(PackageBuilder, "_diff") as mock_package_diff, patch.object(
with patch.object(PackageBuilder, "_diff") as mock_assets_diff, patch.object(
PackageBuilder, "build"
) as mock_build, patch.object(Env, "AUTO_BUILD", openbb_auto_build):
mock_package_diff.return_value = add, remove
mock_assets_diff.return_value = add, remove
package_builder.auto_build()
if openbb_auto_build:
mock_package_diff.assert_called_once_with(Path(tmp_package_dir, "package"))
mock_assets_diff.assert_called_once_with(
Path(tmp_openbb_dir, "assets", "extension_map.json")
)
if add or remove:
mock_build.assert_called_once()
else:
mock_package_diff.assert_not_called()
mock_assets_diff.assert_not_called()
mock_build.assert_not_called()

View File

@@ -439,10 +439,9 @@ def test_fixedincome_spreads_treasury_effr(params, headers):
@parametrize(
"params",
[
({"date": "2023-01-01", "yield_curve_type": "spot_rate"}),
(
{
"rating": "A",
"rating": "aaa",
"provider": "ecb",
"date": "2023-01-01",
"yield_curve_type": "spot_rate",

View File

@@ -405,10 +405,9 @@ def test_fixedincome_spreads_treasury_effr(params, obb):
@parametrize(
"params",
[
({"date": "2023-01-01", "yield_curve_type": "spot_rate"}),
(
{
"rating": "A",
"rating": "aaa",
"provider": "ecb",
"date": "2023-01-01",
"yield_curve_type": "spot_rate",

View File

@@ -127,12 +127,12 @@ def test_quantitative_capm(params, data_type):
],
)
@pytest.mark.integration
def test_quantitative_omega_ratio(params, data_type):
def test_quantitative_performance_omega_ratio(params, data_type):
params = {p: v for p, v in params.items() if v}
data = json.dumps(get_data(data_type))
query_str = get_querystring(params, [])
url = f"http://0.0.0.0:8000/api/v1/quantitative/omega_ratio?{query_str}"
url = f"http://0.0.0.0:8000/api/v1/quantitative/performance/omega_ratio?{query_str}"
result = requests.post(url, headers=get_headers(), timeout=10, data=data)
assert isinstance(result, requests.Response)
assert result.status_code == 200
@@ -146,12 +146,12 @@ def test_quantitative_omega_ratio(params, data_type):
],
)
@pytest.mark.integration
def test_quantitative_kurtosis(params, data_type):
def test_quantitative_rolling_kurtosis(params, data_type):
params = {p: v for p, v in params.items() if v}
data = json.dumps(get_data(data_type))
query_str = get_querystring(params, [])
url = f"http://0.0.0.0:8000/api/v1/quantitative/kurtosis?{query_str}"
url = f"http://0.0.0.0:8000/api/v1/quantitative/rolling/kurtosis?{query_str}"
result = requests.post(url, headers=get_headers(), timeout=10, data=data)
assert isinstance(result, requests.Response)
assert result.status_code == 200
@@ -212,12 +212,14 @@ def test_quantitative_unitroot_test(params, data_type):
],
)
@pytest.mark.integration
def test_quantitative_sharpe_ratio(params, data_type):
def test_quantitative_performance_sharpe_ratio(params, data_type):
params = {p: v for p, v in params.items() if v}
data = json.dumps(get_data(data_type))
query_str = get_querystring(params, [])
url = f"http://0.0.0.0:8000/api/v1/quantitative/sharpe_ratio?{query_str}"
url = (
f"http://0.0.0.0:8000/api/v1/quantitative/performance/sharpe_ratio?{query_str}"
)
result = requests.post(url, headers=get_headers(), timeout=10, data=data)
assert isinstance(result, requests.Response)
assert result.status_code == 200
@@ -251,12 +253,14 @@ def test_quantitative_sharpe_ratio(params, data_type):
],
)
@pytest.mark.integration
def test_quantitative_sortino_ratio(params, data_type):
def test_quantitative_performance_sortino_ratio(params, data_type):
params = {p: v for p, v in params.items() if v}
data = json.dumps(get_data(data_type))
query_str = get_querystring(params, [])
url = f"http://0.0.0.0:8000/api/v1/quantitative/sortino_ratio?{query_str}"
url = (
f"http://0.0.0.0:8000/api/v1/quantitative/performance/sortino_ratio?{query_str}"
)
result = requests.post(url, headers=get_headers(), timeout=10, data=data)
assert isinstance(result, requests.Response)
assert result.status_code == 200
@@ -269,12 +273,66 @@ def test_quantitative_sortino_ratio(params, data_type):
],
)
@pytest.mark.integration
def test_quantitative_skewness(params, data_type):
def test_quantitative_rolling_skew(params, data_type):
params = {p: v for p, v in params.items() if v}
data = json.dumps(get_data(data_type))
query_str = get_querystring(params, [])
url = f"http://0.0.0.0:8000/api/v1/quantitative/skewness?{query_str}"
url = f"http://0.0.0.0:8000/api/v1/quantitative/rolling/skew?{query_str}"
result = requests.post(url, headers=get_headers(), timeout=60, data=data)
assert isinstance(result, requests.Response)
assert result.status_code == 200
@parametrize(
"params, data_type",
[
({"data": "", "target": "close", "window": "220", "index": "date"}, "equity"),
],
)
@pytest.mark.integration
def test_quantitative_rolling_variance(params, data_type):
params = {p: v for p, v in params.items() if v}
data = json.dumps(get_data(data_type))
query_str = get_querystring(params, [])
url = f"http://0.0.0.0:8000/api/v1/quantitative/rolling/variance?{query_str}"
result = requests.post(url, headers=get_headers(), timeout=60, data=data)
assert isinstance(result, requests.Response)
assert result.status_code == 200
@parametrize(
"params, data_type",
[
({"data": "", "target": "close", "window": "220", "index": "date"}, "equity"),
],
)
@pytest.mark.integration
def test_quantitative_rolling_stdev(params, data_type):
params = {p: v for p, v in params.items() if v}
data = json.dumps(get_data(data_type))
query_str = get_querystring(params, [])
url = f"http://0.0.0.0:8000/api/v1/quantitative/rolling/stdev?{query_str}"
result = requests.post(url, headers=get_headers(), timeout=60, data=data)
assert isinstance(result, requests.Response)
assert result.status_code == 200
@parametrize(
"params, data_type",
[
({"data": "", "target": "close", "window": "220", "index": "date"}, "equity"),
],
)
@pytest.mark.integration
def test_quantitative_rolling_mean(params, data_type):
params = {p: v for p, v in params.items() if v}
data = json.dumps(get_data(data_type))
query_str = get_querystring(params, [])
url = f"http://0.0.0.0:8000/api/v1/quantitative/rolling/mean?{query_str}"
result = requests.post(url, headers=get_headers(), timeout=60, data=data)
assert isinstance(result, requests.Response)
assert result.status_code == 200
@@ -306,12 +364,12 @@ def test_quantitative_skewness(params, data_type):
],
)
@pytest.mark.integration
def test_quantitative_quantile(params, data_type):
def test_quantitative_rolling_quantile(params, data_type):
params = {p: v for p, v in params.items() if v}
data = json.dumps(get_data(data_type))
query_str = get_querystring(params, [])
url = f"http://0.0.0.0:8000/api/v1/quantitative/quantile?{query_str}"
url = f"http://0.0.0.0:8000/api/v1/quantitative/rolling/quantile?{query_str}"
result = requests.post(url, headers=get_headers(), timeout=10, data=data)
assert isinstance(result, requests.Response)
assert result.status_code == 200
@@ -334,3 +392,133 @@ def test_quantitative_summary(params, data_type):
result = requests.post(url, headers=get_headers(), timeout=10, data=data)
assert isinstance(result, requests.Response)
assert result.status_code == 200
############
# quantitative/stats
############
@parametrize(
"params, data_type",
[
({"data": "", "target": "close", "index": "date"}, "equity"),
],
)
@pytest.mark.integration
def test_quantitative_stats_skew(params, data_type):
params = {p: v for p, v in params.items() if v}
data = json.dumps(get_data(data_type))
query_str = get_querystring(params, [])
url = f"http://0.0.0.0:8000/api/v1/quantitative/stats/skew?{query_str}"
result = requests.post(url, headers=get_headers(), timeout=60, data=data)
assert isinstance(result, requests.Response)
assert result.status_code == 200
@parametrize(
"params, data_type",
[
({"data": "", "target": "close", "index": "date"}, "equity"),
],
)
@pytest.mark.integration
def test_quantitative_stats_kurtosis(params, data_type):
params = {p: v for p, v in params.items() if v}
data = json.dumps(get_data(data_type))
query_str = get_querystring(params, [])
url = f"http://0.0.0.0:8000/api/v1/quantitative/stats/kurtosis?{query_str}"
result = requests.post(url, headers=get_headers(), timeout=60, data=data)
assert isinstance(result, requests.Response)
assert result.status_code == 200
@parametrize(
"params, data_type",
[
({"data": "", "target": "close", "index": "date"}, "equity"),
],
)
@pytest.mark.integration
def test_quantitative_stats_mean(params, data_type):
params = {p: v for p, v in params.items() if v}
data = json.dumps(get_data(data_type))
query_str = get_querystring(params, [])
url = f"http://0.0.0.0:8000/api/v1/quantitative/stats/mean?{query_str}"
result = requests.post(url, headers=get_headers(), timeout=60, data=data)
assert isinstance(result, requests.Response)
assert result.status_code == 200
@parametrize(
"params, data_type",
[
({"data": "", "target": "close", "index": "date"}, "equity"),
],
)
@pytest.mark.integration
def test_quantitative_stats_stdev(params, data_type):
params = {p: v for p, v in params.items() if v}
data = json.dumps(get_data(data_type))
query_str = get_querystring(params, [])
url = f"http://0.0.0.0:8000/api/v1/quantitative/stats/stdev?{query_str}"
result = requests.post(url, headers=get_headers(), timeout=60, data=data)
assert isinstance(result, requests.Response)
assert result.status_code == 200
@parametrize(
"params, data_type",
[
({"data": "", "target": "close", "index": "date"}, "equity"),
],
)
@pytest.mark.integration
def test_quantitative_stats_variance(params, data_type):
params = {p: v for p, v in params.items() if v}
data = json.dumps(get_data(data_type))
query_str = get_querystring(params, [])
url = f"http://0.0.0.0:8000/api/v1/quantitative/stats/variance?{query_str}"
result = requests.post(url, headers=get_headers(), timeout=60, data=data)
assert isinstance(result, requests.Response)
assert result.status_code == 200
@parametrize(
"params, data_type",
[
(
{
"data": "",
"target": "close",
"quantile_pct": "",
"index": "date",
},
"equity",
),
(
{
"data": "",
"target": "high",
"quantile_pct": "0.6",
"index": "date",
},
"crypto",
),
],
)
@pytest.mark.integration
def test_quantitative_stats_quantile(params, data_type):
params = {p: v for p, v in params.items() if v}
data = json.dumps(get_data(data_type))
query_str = get_querystring(params, [])
url = f"http://0.0.0.0:8000/api/v1/quantitative/stats/quantile?{query_str}"
result = requests.post(url, headers=get_headers(), timeout=10, data=data)
assert isinstance(result, requests.Response)
assert result.status_code == 200

View File

@@ -117,11 +117,11 @@ def test_quantitative_capm(params, data_type, obb):
],
)
@pytest.mark.integration
def test_quantitative_omega_ratio(params, data_type, obb):
def test_quantitative_performance_omega_ratio(params, data_type, obb):
params = {p: v for p, v in params.items() if v}
params["data"] = get_data(data_type)
result = obb.quantitative.omega_ratio(**params)
result = obb.quantitative.performance.omega_ratio(**params)
assert result
assert isinstance(result, OBBject)
@@ -134,11 +134,11 @@ def test_quantitative_omega_ratio(params, data_type, obb):
],
)
@pytest.mark.integration
def test_quantitative_kurtosis(params, data_type, obb):
def test_quantitative_rolling_kurtosis(params, data_type, obb):
params = {p: v for p, v in params.items() if v}
params["data"] = get_data(data_type)
result = obb.quantitative.kurtosis(**params)
result = obb.quantitative.rolling.kurtosis(**params)
assert result
assert isinstance(result, OBBject)
assert len(result.results) > 0
@@ -203,11 +203,11 @@ def test_quantitative_unitroot_test(params, data_type, obb):
],
)
@pytest.mark.integration
def test_quantitative_sharpe_ratio(params, data_type, obb):
def test_quantitative_performance_sharpe_ratio(params, data_type, obb):
params = {p: v for p, v in params.items() if v}
params["data"] = get_data(data_type)
result = obb.quantitative.sharpe_ratio(**params)
result = obb.quantitative.performance.sharpe_ratio(**params)
assert result
assert isinstance(result, OBBject)
@@ -240,11 +240,11 @@ def test_quantitative_sharpe_ratio(params, data_type, obb):
],
)
@pytest.mark.integration
def test_quantitative_sortino_ratio(params, data_type, obb):
def test_quantitative_performance_sortino_ratio(params, data_type, obb):
params = {p: v for p, v in params.items() if v}
params["data"] = get_data(data_type)
result = obb.quantitative.sortino_ratio(**params)
result = obb.quantitative.performance.sortino_ratio(**params)
assert result
assert isinstance(result, OBBject)
@@ -256,11 +256,11 @@ def test_quantitative_sortino_ratio(params, data_type, obb):
],
)
@pytest.mark.integration
def test_quantitative_skewness(params, data_type, obb):
def test_quantitative_rolling_skew(params, data_type, obb):
params = {p: v for p, v in params.items() if v}
params["data"] = get_data(data_type)
result = obb.quantitative.skewness(**params)
result = obb.quantitative.rolling.skew(**params)
assert result
assert isinstance(result, OBBject)
assert len(result.results) > 0
@@ -292,11 +292,11 @@ def test_quantitative_skewness(params, data_type, obb):
],
)
@pytest.mark.integration
def test_quantitative_quantile(params, data_type, obb):
def test_quantitative_rolling_quantile(params, data_type, obb):
params = {p: v for p, v in params.items() if v}
params["data"] = get_data(data_type)
result = obb.quantitative.quantile(**params)
result = obb.quantitative.rolling.quantile(**params)
assert result
assert isinstance(result, OBBject)
assert len(result.results) > 0
@@ -317,3 +317,228 @@ def test_quantitative_summary(params, data_type, obb):
result = obb.quantitative.summary(**params)
assert result
assert isinstance(result, OBBject)
@parametrize(
"params, data_type",
[
(
{
"data": "",
"target": "close",
"window": "10",
"quantile_pct": "",
"index": "date",
},
"equity",
),
(
{
"data": "",
"target": "high",
"window": "50",
"quantile_pct": "0.6",
"index": "date",
},
"crypto",
),
],
)
@pytest.mark.integration
def test_quantitative_rolling_stdev(params, data_type, obb):
params = {p: v for p, v in params.items() if v}
params["data"] = get_data(data_type)
result = obb.quantitative.rolling.stdev(**params)
assert result
assert isinstance(result, OBBject)
assert len(result.results) > 0
@parametrize(
"params, data_type",
[
(
{
"data": "",
"target": "close",
"window": "10",
"quantile_pct": "",
"index": "date",
},
"equity",
),
(
{
"data": "",
"target": "high",
"window": "50",
"quantile_pct": "0.6",
"index": "date",
},
"crypto",
),
],
)
@pytest.mark.integration
def test_quantitative_rolling_mean(params, data_type, obb):
params = {p: v for p, v in params.items() if v}
params["data"] = get_data(data_type)
result = obb.quantitative.rolling.mean(**params)
assert result
assert isinstance(result, OBBject)
assert len(result.results) > 0
@parametrize(
"params, data_type",
[
(
{
"data": "",
"target": "close",
"window": "10",
"quantile_pct": "",
"index": "date",
},
"equity",
),
(
{
"data": "",
"target": "high",
"window": "50",
"quantile_pct": "0.6",
"index": "date",
},
"crypto",
),
],
)
@pytest.mark.integration
def test_quantitative_rolling_variance(params, data_type, obb):
params = {p: v for p, v in params.items() if v}
params["data"] = get_data(data_type)
result = obb.quantitative.rolling.variance(**params)
assert result
assert isinstance(result, OBBject)
assert len(result.results) > 0
@parametrize(
"params, data_type",
[
({"data": "", "target": "close"}, "equity"),
],
)
@pytest.mark.integration
def test_quantitative_stats_skew(params, data_type, obb):
params = {p: v for p, v in params.items() if v}
params["data"] = get_data(data_type)
result = obb.quantitative.stats.skew(**params)
assert result
assert isinstance(result, OBBject)
assert len(result.results) > 0
@parametrize(
"params, data_type",
[
({"data": "", "target": "close"}, "equity"),
],
)
@pytest.mark.integration
def test_quantitative_stats_kurtosis(params, data_type, obb):
params = {p: v for p, v in params.items() if v}
params["data"] = get_data(data_type)
result = obb.quantitative.stats.kurtosis(**params)
assert result
assert isinstance(result, OBBject)
assert len(result.results) > 0
@parametrize(
"params, data_type",
[
({"data": "", "target": "close"}, "equity"),
],
)
@pytest.mark.integration
def test_quantitative_stats_variance(params, data_type, obb):
params = {p: v for p, v in params.items() if v}
params["data"] = get_data(data_type)
result = obb.quantitative.stats.variance(**params)
assert result
assert isinstance(result, OBBject)
assert len(result.results) > 0
@parametrize(
"params, data_type",
[
({"data": "", "target": "close"}, "equity"),
],
)
@pytest.mark.integration
def test_quantitative_stats_stdev(params, data_type, obb):
params = {p: v for p, v in params.items() if v}
params["data"] = get_data(data_type)
result = obb.quantitative.stats.stdev(**params)
assert result
assert isinstance(result, OBBject)
assert len(result.results) > 0
@parametrize(
"params, data_type",
[
({"data": "", "target": "close"}, "equity"),
],
)
@pytest.mark.integration
def test_quantitative_stats_mean(params, data_type, obb):
params = {p: v for p, v in params.items() if v}
params["data"] = get_data(data_type)
result = obb.quantitative.stats.mean(**params)
assert result
assert isinstance(result, OBBject)
assert len(result.results) > 0
@parametrize(
"params, data_type",
[
(
{
"data": "",
"target": "close",
"quantile_pct": "",
},
"equity",
),
(
{
"data": "",
"target": "close",
"quantile_pct": "0.6",
},
"crypto",
),
],
)
@pytest.mark.integration
def test_quantitative_stats_quantile(params, data_type, obb):
params = {p: v for p, v in params.items() if v}
params["data"] = get_data(data_type)
result = obb.quantitative.stats.quantile(**params)
assert result
assert isinstance(result, OBBject)
assert len(result.results) > 0

View File

@@ -0,0 +1,203 @@
from typing import List
import numpy as np
import pandas as pd
from openbb_core.app.model.obbject import OBBject
from openbb_core.app.router import Router
from openbb_core.app.utils import (
basemodel_to_df,
df_to_basemodel,
get_target_column,
)
from openbb_core.provider.abstract.data import Data
from openbb_quantitative.helpers import validate_window
from openbb_quantitative.models import (
OmegaModel,
)
from pydantic import PositiveInt
router = Router(prefix="/performance")
@router.command(
methods=["POST"],
examples=[
'stock_data = obb.equity.price.historical(symbol="TSLA", start_date="2023-01-01", provider="fmp").to_df()',
'returns = stock_data["close"].pct_change().dropna()',
'obb.quantitative.omega_ratio(data=returns, target="close")',
],
)
def omega_ratio(
data: List[Data],
target: str,
threshold_start: float = 0.0,
threshold_end: float = 1.5,
) -> OBBject[List[OmegaModel]]:
"""Calculate the Omega Ratio.
The Omega Ratio is a sophisticated metric that goes beyond traditional performance measures by considering the
probability of achieving returns above a given threshold. It offers a more nuanced view of risk and reward,
focusing on the likelihood of success rather than just average outcomes.
Parameters
----------
data : List[Data]
Time series data.
target : str
Target column name.
threshold_start : float, optional
Start threshold, by default 0.0
threshold_end : float, optional
End threshold, by default 1.5
Returns
-------
OBBject[List[OmegaModel]]
Omega ratios.
"""
df = basemodel_to_df(data)
series_target = get_target_column(df, target)
epsilon = 1e-6 # to avoid division by zero
def get_omega_ratio(df_target: pd.Series, threshold: float) -> float:
"""Get omega ratio."""
daily_threshold = (threshold + 1) ** np.sqrt(1 / 252) - 1
excess = df_target - daily_threshold
numerator = excess[excess > 0].sum()
denominator = -excess[excess < 0].sum() + epsilon
return numerator / denominator
threshold = np.linspace(threshold_start, threshold_end, 50)
results = []
for i in threshold:
omega_ = get_omega_ratio(series_target, i)
results.append(OmegaModel(threshold=i, omega=omega_))
return OBBject(results=results)
@router.command(
methods=["POST"],
examples=[
'stock_data = obb.equity.price.historical(symbol="TSLA", start_date="2023-01-01", provider="fmp").to_df()',
'returns = stock_data["close"].pct_change().dropna()',
'obb.quantitative.sharpe_ratio(data=returns, target="close")',
],
)
def sharpe_ratio(
data: List[Data],
target: str,
rfr: float = 0.0,
window: PositiveInt = 252,
index: str = "date",
) -> OBBject[List[Data]]:
"""Get Rolling Sharpe Ratio.
This function calculates the Sharpe Ratio, a metric used to assess the return of an investment compared to its risk.
By factoring in the risk-free rate, it helps you understand how much extra return you're getting for the extra
volatility that you endure by holding a riskier asset. The Sharpe Ratio is essential for investors looking to
compare the efficiency of different investments, providing a clear picture of potential rewards in relation to their
risks over a specified period. Ideal for gauging the effectiveness of investment strategies, it offers insights into
optimizing your portfolio for maximum return on risk.
Parameters
----------
data : List[Data]
Time series data.
target : str
Target column name.
rfr : float, optional
Risk-free rate, by default 0.0
window : PositiveInt, optional
Window size, by default 252
index : str, optional
Returns
-------
OBBject[List[Data]]
Sharpe ratio.
"""
df = basemodel_to_df(data, index=index)
series_target = get_target_column(df, target)
validate_window(series_target, window)
series_target.name = f"sharpe_{window}"
returns = series_target.pct_change().dropna().rolling(window).sum()
std = series_target.rolling(window).std() / np.sqrt(window)
results = ((returns - rfr) / std).dropna().reset_index(drop=False)
results = df_to_basemodel(results)
return OBBject(results=results)
@router.command(
methods=["POST"],
examples=[
'stock_data = obb.equity.price.historical(symbol="TSLA", start_date="2023-01-01", provider="fmp").to_df()',
'returns = stock_data["close"].pct_change().dropna()',
'obb.quantitative.sortino_ratio(data=stock_data, target="close")',
'obb.quantitative.sortino_ratio(data=stock_data, target="close", target_return=0.01, window=126, adjusted=True)',
],
)
def sortino_ratio(
data: List[Data],
target: str,
target_return: float = 0.0,
window: PositiveInt = 252,
adjusted: bool = False,
index: str = "date",
) -> OBBject[List[Data]]:
"""Get rolling Sortino Ratio.
The Sortino Ratio enhances the evaluation of investment returns by distinguishing harmful volatility
from total volatility. Unlike other metrics that treat all volatility as risk, this command specifically assesses
the volatility of negative returns relative to a target or desired return.
It's particularly useful for investors who are more concerned with downside risk than with overall volatility.
By calculating the Sortino Ratio, investors can better understand the risk-adjusted return of their investments,
focusing on the likelihood and impact of negative returns.
This approach offers a more nuanced tool for portfolio optimization, especially in strategies aiming
to minimize the downside.
For method & terminology see:
http://www.redrockcapital.com/Sortino__A__Sharper__Ratio_Red_Rock_Capital.pdf
Parameters
----------
data : List[Data]
Time series data.
target : str
Target column name.
target_return : float, optional
Target return, by default 0.0
window : PositiveInt, optional
Window size, by default 252
adjusted : bool, optional
Adjust sortino ratio to compare it to sharpe ratio, by default False
index:str
Index column for input data
Returns
-------
OBBject[List[Data]]
Sortino ratio.
"""
df = basemodel_to_df(data, index=index)
series_target = get_target_column(df, target)
validate_window(series_target, window)
returns = series_target.pct_change().dropna().rolling(window).sum()
downside_deviation = returns.rolling(window).apply(
lambda x: (x.values[x.values < 0]).std() / np.sqrt(252) * 100
)
results = (
((returns - target_return) / downside_deviation)
.dropna()
.reset_index(drop=False)
)
if adjusted:
results = results / np.sqrt(2)
results_ = df_to_basemodel(results)
return OBBject(results=results_)

View File

@@ -2,32 +2,37 @@
from typing import List, Literal
import numpy as np
import pandas as pd
from openbb_core.app.model.obbject import OBBject
from openbb_core.app.router import Router
from openbb_core.app.utils import (
basemodel_to_df,
df_to_basemodel,
get_target_column,
get_target_columns,
)
from openbb_core.provider.abstract.data import Data
from pydantic import NonNegativeFloat, PositiveInt
from .helpers import get_fama_raw, validate_window
from openbb_quantitative.performance.performance_router import (
router as performance_router,
)
from openbb_quantitative.rolling.rolling_router import router as rolling_router
from openbb_quantitative.stats.stats_router import router as stats_router
from .helpers import get_fama_raw
from .models import (
ADFTestModel,
CAPMModel,
KPSSTestModel,
NormalityModel,
OmegaModel,
SummaryModel,
TestModel,
UnitRootModel,
)
router = Router(prefix="")
router.include_router(rolling_router)
router.include_router(stats_router)
router.include_router(performance_router)
@router.command(
@@ -137,120 +142,7 @@ def capm(data: List[Data], target: str) -> OBBject[CAPMModel]:
return OBBject(results=results)
@router.command(
methods=["POST"],
examples=[
"stock_data = obb.equity.price.historical(symbol='TSLA', start_date='2023-01-01', provider='fmp').to_df()",
"obb.quantitative.omega_ratio(data=stock_data, target='close')",
],
)
def omega_ratio(
data: List[Data],
target: str,
threshold_start: float = 0.0,
threshold_end: float = 1.5,
) -> OBBject[List[OmegaModel]]:
"""Calculate the Omega Ratio.
The Omega Ratio is a sophisticated metric that goes beyond traditional performance measures by considering the
probability of achieving returns above a given threshold. It offers a more nuanced view of risk and reward,
focusing on the likelihood of success rather than just average outcomes.
Parameters
----------
data : List[Data]
Time series data.
target : str
Target column name.
threshold_start : float, optional
Start threshold, by default 0.0
threshold_end : float, optional
End threshold, by default 1.5
Returns
-------
OBBject[List[OmegaModel]]
Omega ratios.
"""
df = basemodel_to_df(data)
series_target = get_target_column(df, target)
epsilon = 1e-6 # to avoid division by zero
def get_omega_ratio(df_target: pd.Series, threshold: float) -> float:
"""Get omega ratio."""
daily_threshold = (threshold + 1) ** np.sqrt(1 / 252) - 1
excess = df_target - daily_threshold
numerator = excess[excess > 0].sum()
denominator = -excess[excess < 0].sum() + epsilon
return numerator / denominator
threshold = np.linspace(threshold_start, threshold_end, 50)
results = []
for i in threshold:
omega_ = get_omega_ratio(series_target, i)
results.append(OmegaModel(threshold=i, omega=omega_))
return OBBject(results=results)
@router.command(
methods=["POST"],
examples=[
"stock_data = obb.equity.price.historical(symbol='TSLA', start_date='2023-01-01', provider='fmp').to_df()",
"obb.quantitative.kurtosis(data=stock_data, target='close', window=252)",
],
)
def kurtosis(
data: List[Data], target: str, window: PositiveInt = 21, index: str = "date"
) -> OBBject[List[Data]]:
"""Get the rolling Kurtosis.
Kurtosis provides insights into the shape of the data's distribution, particularly the heaviness of its tails.
Kurtosis is a statistical measure that reveals whether the data points tend to cluster around the mean or if
outliers are more common than a normal distribution would suggest. A higher kurtosis indicates more data points are
found in the tails, suggesting potential volatility or risk.
This analysis is crucial for understanding the underlying characteristics of your data, helping to anticipate
extreme events or anomalies over a specified window of time.
Parameters
----------
data : List[Data]
Time series data.
target : str
Target column name.
window : PositiveInt
Window size.
index : str, optional
Index column name, by default "date"
Returns
-------
OBBject[List[Data]]
Kurtosis.
"""
import pandas_ta as ta # pylint: disable=import-outside-toplevel # type: ignore
df = basemodel_to_df(data, index=index)
series_target = get_target_column(df, target)
validate_window(series_target, window)
results = (
ta.kurtosis(close=series_target, length=window).dropna().reset_index(drop=False)
)
results = df_to_basemodel(results)
return OBBject(results=results)
@router.command(
methods=["POST"],
examples=[
"stock_data = obb.equity.price.historical(symbol='TSLA', start_date='2023-01-01', provider='fmp').to_df()",
"obb.quantitative.unitroot_test(data=stock_data, target='close')",
'obb.quantitative.unitroot_test(data=stock_data, target="close", fuller_reg="ct", kpss_reg="ct")',
],
)
@router.command(methods=["POST"])
def unitroot_test(
data: List[Data],
target: str,
@@ -309,238 +201,7 @@ def unitroot_test(
return OBBject(results=unitroot_summary)
@router.command(
methods=["POST"],
examples=[
"stock_data = obb.equity.price.historical(symbol='TSLA', start_date='2023-01-01', provider='fmp').to_df()",
"obb.quantitative.sharpe_ratio(data=stock_data, target='close')",
"obb.quantitative.sharpe_ratio(data=stock_data, target='close', rfr=0.01, window=126)",
],
)
def sharpe_ratio(
data: List[Data],
target: str,
rfr: float = 0.0,
window: PositiveInt = 252,
index: str = "date",
) -> OBBject[List[Data]]:
"""Get Rolling Sharpe Ratio.
This function calculates the Sharpe Ratio, a metric used to assess the return of an investment compared to its risk.
By factoring in the risk-free rate, it helps you understand how much extra return you're getting for the extra
volatility that you endure by holding a riskier asset. The Sharpe Ratio is essential for investors looking to
compare the efficiency of different investments, providing a clear picture of potential rewards in relation to their
risks over a specified period. Ideal for gauging the effectiveness of investment strategies, it offers insights into
optimizing your portfolio for maximum return on risk.
Parameters
----------
data : List[Data]
Time series data.
target : str
Target column name.
rfr : float, optional
Risk-free rate, by default 0.0
window : PositiveInt, optional
Window size, by default 252
index : str, optional
Returns
-------
OBBject[List[Data]]
Sharpe ratio.
"""
df = basemodel_to_df(data, index=index)
series_target = get_target_column(df, target)
validate_window(series_target, window)
series_target.name = f"sharpe_{window}"
returns = series_target.pct_change().dropna().rolling(window).sum()
std = series_target.rolling(window).std() / np.sqrt(window)
results = ((returns - rfr) / std).dropna().reset_index(drop=False)
results = df_to_basemodel(results)
return OBBject(results=results)
@router.command(
methods=["POST"],
examples=[
"stock_data = obb.equity.price.historical(symbol='TSLA', start_date='2023-01-01', provider='fmp').to_df()",
"obb.quantitative.sortino_ratio(data=stock_data, target='close')",
"obb.quantitative.sortino_ratio(data=stock_data, target='close', target_return=0.01, window=126, adjusted=True)",
],
)
def sortino_ratio(
data: List[Data],
target: str,
target_return: float = 0.0,
window: PositiveInt = 252,
adjusted: bool = False,
index: str = "date",
) -> OBBject[List[Data]]:
"""Get rolling Sortino Ratio.
The Sortino Ratio enhances the evaluation of investment returns by distinguishing harmful volatility
from total volatility. Unlike other metrics that treat all volatility as risk, this command specifically assesses
the volatility of negative returns relative to a target or desired return.
It's particularly useful for investors who are more concerned with downside risk than with overall volatility.
By calculating the Sortino Ratio, investors can better understand the risk-adjusted return of their investments,
focusing on the likelihood and impact of negative returns.
This approach offers a more nuanced tool for portfolio optimization, especially in strategies aiming
to minimize the downside.
For method & terminology see:
http://www.redrockcapital.com/Sortino__A__Sharper__Ratio_Red_Rock_Capital.pdf
Parameters
----------
data : List[Data]
Time series data.
target : str
Target column name.
target_return : float, optional
Target return, by default 0.0
window : PositiveInt, optional
Window size, by default 252
adjusted : bool, optional
Adjust sortino ratio to compare it to sharpe ratio, by default False
index:str
Index column for input data
Returns
-------
OBBject[List[Data]]
Sortino ratio.
"""
df = basemodel_to_df(data, index=index)
series_target = get_target_column(df, target)
validate_window(series_target, window)
returns = series_target.pct_change().dropna().rolling(window).sum()
downside_deviation = returns.rolling(window).apply(
lambda x: (x.values[x.values < 0]).std() / np.sqrt(252) * 100
)
results = (
((returns - target_return) / downside_deviation)
.dropna()
.reset_index(drop=False)
)
if adjusted:
results = results / np.sqrt(2)
results_ = df_to_basemodel(results)
return OBBject(results=results_)
@router.command(
methods=["POST"],
examples=[
"stock_data = obb.equity.price.historical(symbol='TSLA', start_date='2023-01-01', provider='fmp').to_df()",
"obb.quantitative.skewness(data=stock_data, target='close', window=252)",
],
)
def skewness(
data: List[Data], target: str, window: PositiveInt = 21, index: str = "date"
) -> OBBject[List[Data]]:
"""Get Rolling Skewness.
Skewness is a statistical measure that reveals the degree of asymmetry of a distribution around its mean.
Positive skewness indicates a distribution with an extended tail to the right, while negative skewness shows a tail
that stretches left. Understanding skewness can provide insights into potential biases in data and help anticipate
the nature of future data points. It's particularly useful for identifying the likelihood of extreme outcomes in
financial returns, enabling more informed decision-making based on the distribution's shape over a specified period.
Parameters
----------
data : List[Data]
Time series data.
target : str
Target column name.
window : PositiveInt
Window size.
index : str, optional
Index column name, by default "date"
Returns
-------
OBBject[List[Data]]
Skewness.
"""
import pandas_ta as ta # pylint: disable=import-outside-toplevel # type: ignore
df = basemodel_to_df(data, index=index)
series_target = get_target_column(df, target)
validate_window(series_target, window)
results = (
ta.skew(close=series_target, length=window).dropna().reset_index(drop=False)
)
results = df_to_basemodel(results)
return OBBject(results=results)
@router.command(
methods=["POST"],
examples=[
"stock_data = obb.equity.price.historical(symbol='TSLA', start_date='2023-01-01', provider='fmp').to_df()",
'obb.quantitative.quantile(data=stock_data, target="close", window=252, quantile_pct=0.25)',
"obb.quantitative.quantile(data=stock_data, target='close', window=252, quantile_pct=0.75)",
],
)
def quantile(
data: List[Data],
target: str,
window: PositiveInt = 21,
quantile_pct: NonNegativeFloat = 0.5,
index: str = "date",
) -> OBBject[List[Data]]:
"""Get Rolling Quantile.
Quantile is a statistical measure that identifies the value below which a given percentage of observations in a
group of data falls. By setting the quantile percentage, you can determine any point in the distribution, not just
the median. Whether you're interested in the median, quartiles, or any other position within your data's range,
this tool offers a precise way to understand the distribution's characteristics.
It's especially useful for identifying outliers, understanding dispersion, and setting thresholds for
decision-making based on the distribution of data over a specified period.
Parameters
----------
data : List[Data]
Time series data.
target : str
Target column name.
window : PositiveInt
Window size.
quantile_pct : NonNegativeFloat, optional
Quantile to get
Returns
-------
OBBject[List[Data]]
Quantile.
"""
import pandas_ta as ta # pylint: disable=import-outside-toplevel # type: ignore
df = basemodel_to_df(data, index=index)
series_target = get_target_column(df, target)
validate_window(series_target, window)
df_median = ta.median(close=series_target, length=window).to_frame()
df_quantile = ta.quantile(series_target, length=window, q=quantile_pct).to_frame()
results = (
pd.concat([df_median, df_quantile], axis=1).dropna().reset_index(drop=False)
)
results_ = df_to_basemodel(results)
return OBBject(results=results_)
@router.command(
methods=["POST"],
examples=[
"stock_data = obb.equity.price.historical(symbol='TSLA', start_date='2023-01-01', provider='fmp').to_df()",
"obb.quantitative.volatility(data=stock_data, target='close')",
],
)
@router.command(methods=["POST"])
def summary(data: List[Data], target: str) -> OBBject[SummaryModel]:
"""Get Summary Statistics.

View File

@@ -0,0 +1,319 @@
"""Rolling submenu of quantitative models for rolling statistics."""
from typing import List
import pandas as pd
from openbb_core.app.model.obbject import OBBject
from openbb_core.app.router import Router
from openbb_core.app.utils import (
basemodel_to_df,
df_to_basemodel,
get_target_column,
)
from openbb_core.provider.abstract.data import Data
from openbb_quantitative.helpers import validate_window
from openbb_quantitative.statistics import (
kurtosis_,
mean_,
skew_,
std_dev_,
var_,
)
from pydantic import NonNegativeFloat, PositiveInt
router = Router(prefix="/rolling")
@router.command(
methods=["POST"],
examples=[
'stock_data = obb.equity.price.historical(symbol="TSLA", start_date="2023-01-01", provider="fmp").to_df()',
'returns = stock_data["close"].pct_change().dropna()',
'obb.quantitative.rolling.skew(data=returns, target="close")',
],
)
def skew(
data: List[Data], target: str, window: PositiveInt = 21, index: str = "date"
) -> OBBject[List[Data]]:
"""Get Rolling Skew.
Skew is a statistical measure that reveals the degree of asymmetry of a distribution around its mean.
Positive skewness indicates a distribution with an extended tail to the right, while negative skewness shows a tail
that stretches left. Understanding skewness can provide insights into potential biases in data and help anticipate
the nature of future data points. It's particularly useful for identifying the likelihood of extreme outcomes in
financial returns, enabling more informed decision-making based on the distribution's shape over a specified period.
Parameters
----------
data : List[Data]
Time series data.
target : str
Target column name.
window : PositiveInt
Window size.
index : str, optional
Index column name, by default "date"
Returns
-------
OBBject[List[Data]]
Rolling skew.
"""
df = basemodel_to_df(data, index=index)
series_target = get_target_column(df, target)
series_target.name = f"rolling_skew_{window}"
validate_window(series_target, window)
results = (
series_target.rolling(window).apply(skew_).dropna().reset_index(drop=False)
)
results = df_to_basemodel(results)
return OBBject(results=results)
@router.command(
methods=["POST"],
examples=[
'stock_data = obb.equity.price.historical(symbol="TSLA", start_date="2023-01-01", provider="fmp").to_df()',
'returns = stock_data["close"].pct_change().dropna()',
'obb.quantitative.rolling.variance(data=returns, target="close", window=252)',
],
)
def variance(
data: List[Data], target: str, window: PositiveInt = 21, index: str = "date"
) -> OBBject[List[Data]]:
"""
Calculate the rolling variance of a target column within a given window size.
Variance measures the dispersion of a set of data points around their mean. It is a key metric for
assessing the volatility and stability of financial returns or other time series data over a specified rolling window.
Parameters:
data: List[Data]
The time series data as a list of data points.
target: str
The name of the column for which to calculate variance.
window: PositiveInt
The number of observations used for calculating the rolling measure.
index: str, optional
The name of the index column, default is "date".
Returns:
-------
OBBject[List[Data]]
An object containing the rolling variance values.
"""
df = basemodel_to_df(data, index=index)
series_target = get_target_column(df, target)
series_target.name = f"rolling_var_{window}"
validate_window(series_target, window)
results = series_target.rolling(window).apply(var_).dropna().reset_index(drop=False)
results = df_to_basemodel(results)
return OBBject(results=results)
@router.command(
methods=["POST"],
examples=[
'stock_data = obb.equity.price.historical(symbol="TSLA", start_date="2023-01-01", provider="fmp").to_df()',
'returns = stock_data["close"].pct_change().dropna()',
'obb.quantitative.rolling.stdev(data=returns, target="close", window=252)',
],
)
def stdev(
data: List[Data], target: str, window: PositiveInt = 21, index: str = "date"
) -> OBBject[List[Data]]:
"""
Calculate the rolling standard deviation of a target column within a given window size.
Standard deviation is a measure of the amount of variation or dispersion of a set of values.
It is widely used to assess the risk and volatility of financial returns or other time series data
over a specified rolling window. It is the square root of the variance.
Parameters:
data: List[Data]
The time series data as a list of data points.
target: str
The name of the column for which to calculate standard deviation.
window: PositiveInt
The number of observations used for calculating the rolling measure.
index: str, optional
The name of the index column, default is "date".
Returns:
-------
OBBject[List[Data]]
An object containing the rolling standard deviation values.
"""
df = basemodel_to_df(data, index=index)
series_target = get_target_column(df, target)
series_target.name = f"rolling_stdev_{window}"
validate_window(series_target, window)
results = (
series_target.rolling(window).apply(std_dev_).dropna().reset_index(drop=False)
)
results = df_to_basemodel(results)
return OBBject(results=results)
@router.command(
methods=["POST"],
examples=[
'stock_data = obb.equity.price.historical(symbol="TSLA", start_date="2023-01-01", provider="fmp").to_df()',
'returns = stock_data["close"].pct_change().dropna()',
'obb.quantitative.rolling.kurtosis(data=returns, target="close", window=252)',
],
)
def kurtosis(
data: List[Data], target: str, window: PositiveInt = 21, index: str = "date"
) -> OBBject[List[Data]]:
"""
Calculate the rolling kurtosis of a target column within a given window size.
Kurtosis measures the "tailedness" of the probability distribution of a real-valued random variable.
High kurtosis indicates a distribution with heavy tails (outliers), suggesting a higher risk of extreme outcomes.
Low kurtosis indicates a distribution with lighter tails (less outliers), suggesting less risk of extreme outcomes.
This function helps in assessing the risk of outliers in financial returns or other time series data over a specified
rolling window.
Parameters:
data: List[Data]
The time series data as a list of data points.
target: str
The name of the column for which to calculate kurtosis.
window: PositiveInt
The number of observations used for calculating the rolling measure.
index: str, optional
The name of the index column, default is "date".
Returns:
-------
OBBject[List[Data]]
An object containing the rolling kurtosis values.
"""
df = basemodel_to_df(data, index=index)
series_target = get_target_column(df, target)
series_target.name = f"rolling_kurtosis_{window}"
validate_window(series_target, window)
results = (
series_target.rolling(window).apply(kurtosis_).dropna().reset_index(drop=False)
)
results = df_to_basemodel(results)
return OBBject(results=results)
@router.command(
methods=["POST"],
examples=[
'stock_data = obb.equity.price.historical(symbol="TSLA", start_date="2023-01-01", provider="fmp").to_df()',
'returns = stock_data["close"].pct_change().dropna()',
'obb.quantitative.rolling.quantile(data=returns, target="close", window=252, quantile_pct=0.25)',
'obb.quantitative.rolling.quantile(data=returns, target="close", window=252, quantile_pct=0.75)',
],
)
def quantile(
data: List[Data],
target: str,
window: PositiveInt = 21,
quantile_pct: NonNegativeFloat = 0.5,
index: str = "date",
) -> OBBject[List[Data]]:
"""
Calculate the rolling quantile of a target column within a given window size at a specified quantile percentage.
Quantiles are points dividing the range of a probability distribution into intervals with equal probabilities,
or dividing the sample in the same way. This function is useful for understanding the distribution of data
within a specified window, allowing for analysis of trends, identification of outliers, and assessment of risk..
Parameters:
data: List[Data]
The time series data as a list of data points.
target: str
The name of the column for which to calculate the quantile.
window: PositiveInt
The number of observations used for calculating the rolling measure.
quantile_pct: NonNegativeFloat, optional
The quantile percentage to calculate (e.g., 0.5 for median), default is 0.5.
index: str, optional
The name of the index column, default is "date".
Returns:
-------
OBBject[List[Data]]
An object containing the rolling quantile values with the median.
"""
df = basemodel_to_df(data, index=index)
series_target = get_target_column(df, target)
validate_window(series_target, window)
roll = series_target.rolling(window)
df_median = roll.median()
df_quantile = roll.quantile(quantile_pct)
results = (
pd.concat(
[df_median, df_quantile],
axis=1,
keys=[
f"rolling_median_{window}",
f"rolling_quantile_{quantile_pct}_{window}",
],
)
.dropna()
.reset_index(drop=False)
)
results = df_to_basemodel(results)
return OBBject(results=results)
@router.command(
methods=["POST"],
examples=[
'stock_data = obb.equity.price.historical(symbol="TSLA", start_date="2023-01-01", provider="fmp").to_df()',
'returns = stock_data["close"].pct_change().dropna()',
'obb.quantitative.rolling.mean(data=returns, target="close", window=252)',
],
)
def mean(
data: List[Data], target: str, window: PositiveInt = 21, index: str = "date"
) -> OBBject[List[Data]]:
"""
Calculate the rolling mean (average) of a target column within a given window size.
The rolling mean is a simple moving average that calculates the average of a target variable over a specified window.
This function is widely used in financial analysis to smooth short-term fluctuations and highlight longer-term trends
or cycles in time series data.
Parameters:
data: List[Data]
The time series data as a list of data points.
target: str
The name of the column for which to calculate the mean.
window: PositiveInt
The number of observations used for calculating the rolling measure.
index: str, optional
The name of the index column, default is "date".
Returns:
OBBject[List[Data]]
An object containing the rolling mean values.
"""
df = basemodel_to_df(data, index=index)
series_target = get_target_column(df, target)
series_target.name = f"rolling_mean_{window}"
validate_window(series_target, window)
results = (
series_target.rolling(window).apply(mean_).dropna().reset_index(drop=False)
)
results = df_to_basemodel(results)
return OBBject(results=results)

View File

@@ -0,0 +1,41 @@
"""Statistics Functions"""
from typing import Union
from numpy import (
mean as mean_np,
ndarray,
std,
var as var_np,
)
from pandas import DataFrame, Series
from scipy import stats
# Because python is weird and these being the same name as the fastapi router functions
# which overwrites the function signature, we add the _ after the function name
def kurtosis_(data: Union[DataFrame, Series, ndarray]) -> float:
"""Kurtosis is a measure of the "tailedness" of the probability distribution of a real-valued random variable."""
return stats.kurtosis(data)
def skew_(data: Union[DataFrame, Series, ndarray]) -> float:
"""Skewness is a measure of the asymmetry of the probability distribution of a
real-valued random variable about its mean."""
return stats.skew(data)
def mean_(data: Union[DataFrame, Series, ndarray]) -> float:
"""Mean is the average of the numbers."""
return mean_np(data)
def std_dev_(data: Union[DataFrame, Series, ndarray]) -> float:
"""Standard deviation is a measure of the amount of variation or dispersion of a set of values."""
return std(data)
def var_(data: Union[DataFrame, Series, ndarray]) -> float:
"""Variance is a measure of the amount of variation or dispersion of a set of values."""
return var_np(data)

View File

@@ -0,0 +1,258 @@
"""Rolling submenu of quantitative models for rolling statistics."""
from typing import List
import pandas as pd
from openbb_core.app.model.obbject import OBBject
from openbb_core.app.router import Router
from openbb_core.app.utils import (
basemodel_to_df,
df_to_basemodel,
get_target_column,
)
from openbb_core.provider.abstract.data import Data
from openbb_quantitative.statistics import (
kurtosis_,
mean_,
skew_,
std_dev_,
var_,
)
from pydantic import NonNegativeFloat
router = Router(prefix="/stats")
@router.command(
methods=["POST"],
examples=[
'stock_data = obb.equity.price.historical(symbol="TSLA", start_date="2023-01-01", provider="fmp").to_df()',
'returns = stock_data["close"].pct_change().dropna()',
'obb.quantitative.stats.skew(data=returns, target="close")',
],
)
def skew(
data: List[Data],
target: str,
) -> OBBject[List[Data]]:
"""Get the skew. of the data set
Skew is a statistical measure that reveals the degree of asymmetry of a distribution around its mean.
Positive skewness indicates a distribution with an extended tail to the right, while negative skewness shows a tail
that stretches left. Understanding skewness can provide insights into potential biases in data and help anticipate
the nature of future data points. It's particularly useful for identifying the likelihood of extreme outcomes in
financial returns, enabling more informed decision-making based on the distribution's shape over a specified period.
Parameters
----------
data : List[Data]
Time series data.
target : str
Target column name.
Returns
-------
OBBject[List[Data]]
Rolling skew.
"""
df = basemodel_to_df(data)
series_target = get_target_column(df, target)
results = pd.DataFrame([skew_(series_target)], columns=["skew"])
results = df_to_basemodel(results)
return OBBject(results=results)
@router.command(
methods=["POST"],
examples=[
'stock_data = obb.equity.price.historical(symbol="TSLA", start_date="2023-01-01", provider="fmp").to_df()',
'returns = stock_data["close"].pct_change().dropna()',
'obb.quantitative.stats.variance(data=returns, target="close")',
],
)
def variance(data: List[Data], target: str) -> OBBject[List[Data]]:
"""
Calculate the variance of a target column.
Variance measures the dispersion of a set of data points around their mean. It is a key metric for
assessing the volatility and stability of financial returns or other time series data.
Parameters
----------
data: List[Data]
The time series data as a list of data points.
target: str
The name of the column for which to calculate variance.
Returns
-------
OBBject[List[Data]]
An object containing the rolling variance values.
"""
df = basemodel_to_df(data)
series_target = get_target_column(df, target)
results = pd.DataFrame([var_(series_target)], columns=["variance"])
results = df_to_basemodel(results)
return OBBject(results=results)
@router.command(
methods=["POST"],
examples=[
'stock_data = obb.equity.price.historical(symbol="TSLA", start_date="2023-01-01", provider="fmp").to_df()',
'returns = stock_data["close"].pct_change().dropna()',
'obb.quantitative.stats.stdev(data=returns, target="close")',
],
)
def stdev(data: List[Data], target: str) -> OBBject[List[Data]]:
"""
Calculate the rolling standard deviation of a target column.
Standard deviation is a measure of the amount of variation or dispersion of a set of values.
It is widely used to assess the risk and volatility of financial returns or other time series data
It is the square root of the variance.
Parameters
----------
data: List[Data]
The time series data as a list of data points.
target: str
The name of the column for which to calculate standard deviation.
Returns
-------
OBBject[List[Data]]
An object containing the rolling standard deviation values.
"""
df = basemodel_to_df(data)
series_target = get_target_column(df, target)
results = pd.DataFrame([std_dev_(series_target)], columns=["stdev"])
results = df_to_basemodel(results)
return OBBject(results=results)
@router.command(
methods=["POST"],
examples=[
'stock_data = obb.equity.price.historical(symbol="TSLA", start_date="2023-01-01", provider="fmp").to_df()',
'returns = stock_data["close"].pct_change().dropna()',
'obb.quantitative.stats.kurtosis(data=returns, target="close")',
],
)
def kurtosis(data: List[Data], target) -> OBBject[List[Data]]:
"""
Calculate the rolling kurtosis of a target column.
Kurtosis measures the "tailedness" of the probability distribution of a real-valued random variable.
High kurtosis indicates a distribution with heavy tails (outliers), suggesting a higher risk of extreme outcomes.
Low kurtosis indicates a distribution with lighter tails (less outliers), suggesting less risk of extreme outcomes.
This function helps in assessing the risk of outliers in financial returns or other time series data.
Parameters
data: List[Data]
The time series data as a list of data points.
target: str
The name of the column for which to calculate kurtosis.
Returns
------
OBBject[List[Data]]
An object containing the kurtosis value
"""
df = basemodel_to_df(data)
series_target = get_target_column(df, target)
results = pd.DataFrame([kurtosis_(series_target)], columns=["kurtosis"])
results = df_to_basemodel(results)
return OBBject(results=results)
@router.command(
methods=["POST"],
examples=[
'stock_data = obb.equity.price.historical(symbol="TSLA", start_date="2023-01-01", provider="fmp").to_df()',
'returns = stock_data["close"].pct_change().dropna()',
'obb.quantitative.stats.quantile(data=returns, target="close", quantile_pct=0.75)',
],
)
def quantile(
data: List[Data],
target: str,
quantile_pct: NonNegativeFloat = 0.5,
) -> OBBject[List[Data]]:
"""
Calculate the quantile of a target column at a specified quantile percentage.
Quantiles are points dividing the range of a probability distribution into intervals with equal probabilities,
or dividing the sample in the same way.
Parameters
----------
data: List[Data]
The time series data as a list of data points.
target: str
The name of the column for which to calculate the quantile.
quantile_pct: NonNegativeFloat, optional
The quantile percentage to calculate (e.g., 0.5 for median), default is 0.5.
Returns
-------
OBBject[List[Data]]
An object containing the rolling quantile values with the median.
"""
df = basemodel_to_df(
data,
)
series_target = get_target_column(df, target)
results = pd.DataFrame(
[series_target.quantile(quantile_pct)], columns=[f"{quantile_pct}_quantile"]
)
results = df_to_basemodel(results)
return OBBject(results=results)
@router.command(
methods=["POST"],
examples=[
'stock_data = obb.equity.price.historical(symbol="TSLA", start_date="2023-01-01", provider="fmp").to_df()',
'returns = stock_data["close"].pct_change().dropna()',
'obb.quantitative.stats.mean(data=returns, target="close")',
],
)
def mean(
data: List[Data],
target: str,
) -> OBBject[List[Data]]:
"""
Calculate the mean (average) of a target column.
The rolling mean is a simple moving average that calculates the average of a target variable.
This function is widely used in financial analysis to smooth short-term fluctuations and highlight longer-term trends
or cycles in time series data.
Parameters
----------
data: List[Data]
The time series data as a list of data points.
target: str
The name of the column for which to calculate the mean.
Returns
-------
OBBject[List[Data]]
An object containing the mean value.
"""
df = basemodel_to_df(data)
series_target = get_target_column(df, target)
results = pd.DataFrame([mean_(series_target)], columns=["mean"])
results = df_to_basemodel(results)
return OBBject(results=results)

View File

@@ -0,0 +1,25 @@
import pandas as pd
import pytest
from openbb_quantitative.statistics import kurtosis_, mean_, skew_, std_dev_, var_
test_data = pd.Series([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
def test_kurtosis():
assert kurtosis_(test_data) == pytest.approx(-1.224, abs=1e-3)
def test_skew():
assert skew_(test_data) == pytest.approx(0.0, abs=1e-3)
def test_std_dev():
assert std_dev_(test_data) == pytest.approx(2.872, abs=1e-3)
def test_mean():
assert mean_(test_data) == pytest.approx(5.5, abs=1e-3)
def test_var():
assert var_(test_data) == pytest.approx(8.25, abs=1e-3)

View File

@@ -59,13 +59,13 @@ def test_charting_extension_function_coverage() -> None:
assert missing_items == [], "\n".join(missing_items)
def test_missing_api_integration_tests() -> None:
def test_missing_python_integration_tests() -> None:
"""Check if there are missing tests."""
missing = check_missing_integration_tests(test_type="python")
assert not missing, "\n".join(missing)
def test_outdated_api_integration_tests() -> None:
def test_outdated_python_integration_tests() -> None:
"""Check if there are outdated tests."""
outdated = check_outdated_integration_tests(test_type="python")
assert not outdated, "\n".join(outdated)

View File

@@ -132,7 +132,7 @@ def check_router_model_functions_signature() -> List[str]:
)
if expected_return_type not in str(function.__annotations__["return"]):
missing_return_type.append(
f"{function.__name__} in {router_name}"
f"{function.__name__} in {router_name} "
f"doesn't have the expected return type: {expected_return_type}"
)

View File

@@ -136,7 +136,6 @@
"index_market": "/index/market",
"index_price": "/index/price",
"index_price_historical": "/index/price/historical",
"index_sectors": "/index/sectors",
"news": "/news",
"news_company": "/news/company",
"news_world": "/news/world",

View File

@@ -227,7 +227,7 @@ class ROUTER_equity(Container):
symbol: Annotated[
Union[str, List[str]],
OpenBBCustomParameter(
description="Symbol to get data for. Multiple items allowed: intrinio, yfinance."
description="Symbol to get data for. Multiple items allowed: fmp, intrinio, yfinance."
),
],
provider: Optional[Literal["fmp", "intrinio", "yfinance"]] = None,
@@ -238,7 +238,7 @@ class ROUTER_equity(Container):
Parameters
----------
symbol : Union[str, List[str]]
Symbol to get data for. Multiple items allowed: intrinio, yfinance.
Symbol to get data for. Multiple items allowed: fmp, intrinio, yfinance.
provider : Optional[Literal['fmp', 'intrinio', 'yfinance']]
The provider to use for the query, by default None.
If None, the provider specified in defaults is selected or 'fmp' if there is
@@ -403,7 +403,9 @@ class ROUTER_equity(Container):
},
extra_params=kwargs,
extra_info={
"symbol": {"multiple_items_allowed": ["intrinio", "yfinance"]}
"symbol": {
"multiple_items_allowed": ["fmp", "intrinio", "yfinance"]
}
},
)
)

View File

@@ -77,7 +77,7 @@ class ROUTER_equity_discovery(Container):
Percent change.
volume : float
The trading volume.
market_cap : Optional[str]
market_cap : Optional[float]
Market Cap displayed in billions. (provider: yfinance)
avg_volume_3_months : Optional[float]
Average volume over the last 3 months in millions. (provider: yfinance)
@@ -158,7 +158,7 @@ class ROUTER_equity_discovery(Container):
Percent change.
volume : float
The trading volume.
market_cap : Optional[str]
market_cap : Optional[float]
Market Cap. (provider: yfinance)
avg_volume_3_months : Optional[float]
Average volume over the last 3 months in millions. (provider: yfinance)
@@ -341,7 +341,7 @@ class ROUTER_equity_discovery(Container):
Percent change.
volume : float
The trading volume.
market_cap : Optional[str]
market_cap : Optional[float]
Market Cap. (provider: yfinance)
avg_volume_3_months : Optional[float]
Average volume over the last 3 months in millions. (provider: yfinance)
@@ -422,7 +422,7 @@ class ROUTER_equity_discovery(Container):
Percent change.
volume : float
The trading volume.
market_cap : Optional[str]
market_cap : Optional[float]
Market Cap. (provider: yfinance)
avg_volume_3_months : Optional[float]
Average volume over the last 3 months in millions. (provider: yfinance)
@@ -503,7 +503,7 @@ class ROUTER_equity_discovery(Container):
Percent change.
volume : float
The trading volume.
market_cap : Optional[str]
market_cap : Optional[float]
Market Cap. (provider: yfinance)
avg_volume_3_months : Optional[float]
Average volume over the last 3 months in millions. (provider: yfinance)
@@ -584,7 +584,7 @@ class ROUTER_equity_discovery(Container):
Percent change.
volume : float
The trading volume.
market_cap : Optional[str]
market_cap : Optional[float]
Market Cap. (provider: yfinance)
avg_volume_3_months : Optional[float]
Average volume over the last 3 months in millions. (provider: yfinance)

View File

@@ -1270,7 +1270,10 @@ class ROUTER_equity_fundamental(Container):
def historical_attributes(
self,
symbol: Annotated[
str, OpenBBCustomParameter(description="Symbol to get data for.")
Union[str, List[str]],
OpenBBCustomParameter(
description="Symbol to get data for. Multiple items allowed: intrinio."
),
],
tag: Annotated[
Union[str, List[str]],
@@ -1313,8 +1316,8 @@ class ROUTER_equity_fundamental(Container):
Parameters
----------
symbol : str
Symbol to get data for.
symbol : Union[str, List[str]]
Symbol to get data for. Multiple items allowed: intrinio.
tag : Union[str, List[str]]
Intrinio data tag ID or code. Multiple items allowed: intrinio.
start_date : Union[datetime.date, None, str]
@@ -1386,7 +1389,10 @@ class ROUTER_equity_fundamental(Container):
"sort": sort,
},
extra_params=kwargs,
extra_info={"tag": {"multiple_items_allowed": ["intrinio"]}},
extra_info={
"symbol": {"multiple_items_allowed": ["intrinio"]},
"tag": {"multiple_items_allowed": ["intrinio"]},
},
)
)
@@ -2033,7 +2039,10 @@ class ROUTER_equity_fundamental(Container):
def latest_attributes(
self,
symbol: Annotated[
str, OpenBBCustomParameter(description="Symbol to get data for.")
Union[str, List[str]],
OpenBBCustomParameter(
description="Symbol to get data for. Multiple items allowed: intrinio."
),
],
tag: Annotated[
Union[str, List[str]],
@@ -2048,8 +2057,8 @@ class ROUTER_equity_fundamental(Container):
Parameters
----------
symbol : str
Symbol to get data for.
symbol : Union[str, List[str]]
Symbol to get data for. Multiple items allowed: intrinio.
tag : Union[str, List[str]]
Intrinio data tag ID or code. Multiple items allowed: intrinio.
provider : Optional[Literal['intrinio']]
@@ -2101,7 +2110,10 @@ class ROUTER_equity_fundamental(Container):
"tag": tag,
},
extra_params=kwargs,
extra_info={"tag": {"multiple_items_allowed": ["intrinio"]}},
extra_info={
"symbol": {"multiple_items_allowed": ["intrinio"]},
"tag": {"multiple_items_allowed": ["intrinio"]},
},
)
)
@@ -3431,7 +3443,7 @@ class ROUTER_equity_fundamental(Container):
def trailing_dividend_yield(
self,
symbol: Annotated[
str, OpenBBCustomParameter(description="Symbol to get data for.")
Optional[str], OpenBBCustomParameter(description="Symbol to get data for.")
] = None,
limit: Annotated[
Optional[int],
@@ -3446,7 +3458,7 @@ class ROUTER_equity_fundamental(Container):
Parameters
----------
symbol : str
symbol : Optional[str]
Symbol to get data for.
limit : Optional[int]
The number of data entries to return. Default is 252, the number of trading days in a year.

View File

@@ -74,7 +74,7 @@ class ROUTER_equity_ownership(Container):
InsiderTrading
--------------
symbol : str
symbol : Optional[str]
Symbol representing the entity requested in the data.
company_cik : Optional[Union[int, str]]
CIK number of the company.

View File

@@ -335,7 +335,10 @@ class ROUTER_equity_price(Container):
def performance(
self,
symbol: Annotated[
str, OpenBBCustomParameter(description="Symbol to get data for.")
Union[str, List[str]],
OpenBBCustomParameter(
description="Symbol to get data for. Multiple items allowed: fmp."
),
],
provider: Optional[Literal["fmp"]] = None,
**kwargs
@@ -344,8 +347,8 @@ class ROUTER_equity_price(Container):
Parameters
----------
symbol : str
Symbol to get data for.
symbol : Union[str, List[str]]
Symbol to get data for. Multiple items allowed: fmp.
provider : Optional[Literal['fmp']]
The provider to use for the query, by default None.
If None, the provider specified in defaults is selected or 'fmp' if there is
@@ -418,6 +421,7 @@ class ROUTER_equity_price(Container):
"symbol": symbol,
},
extra_params=kwargs,
extra_info={"symbol": {"multiple_items_allowed": ["fmp"]}},
)
)

View File

@@ -32,7 +32,10 @@ class ROUTER_etf(Container):
def countries(
self,
symbol: Annotated[
str, OpenBBCustomParameter(description="Symbol to get data for. (ETF)")
Union[str, List[str]],
OpenBBCustomParameter(
description="Symbol to get data for. (ETF) Multiple items allowed: fmp."
),
],
provider: Optional[Literal["fmp"]] = None,
**kwargs
@@ -41,8 +44,8 @@ class ROUTER_etf(Container):
Parameters
----------
symbol : str
Symbol to get data for. (ETF)
symbol : Union[str, List[str]]
Symbol to get data for. (ETF) Multiple items allowed: fmp.
provider : Optional[Literal['fmp']]
The provider to use for the query, by default None.
If None, the provider specified in defaults is selected or 'fmp' if there is
@@ -87,6 +90,7 @@ class ROUTER_etf(Container):
"symbol": symbol,
},
extra_params=kwargs,
extra_info={"symbol": {"multiple_items_allowed": ["fmp"]}},
)
)
@@ -237,6 +241,9 @@ class ROUTER_etf(Container):
-------
>>> from openbb import obb
>>> obb.etf.historical(symbol="SPY")
>>> obb.etf.historical("SPY", provider="yfinance")
>>> #### This function accepts multiple tickers. ####
>>> obb.etf.historical("SPY,IWM,QQQ,DJIA", provider="yfinance")
""" # noqa: E501
return self._run(
@@ -558,7 +565,10 @@ class ROUTER_etf(Container):
def holdings_performance(
self,
symbol: Annotated[
str, OpenBBCustomParameter(description="Symbol to get data for.")
Union[str, List[str]],
OpenBBCustomParameter(
description="Symbol to get data for. Multiple items allowed: fmp."
),
],
provider: Optional[Literal["fmp"]] = None,
**kwargs
@@ -567,8 +577,8 @@ class ROUTER_etf(Container):
Parameters
----------
symbol : str
Symbol to get data for.
symbol : Union[str, List[str]]
Symbol to get data for. Multiple items allowed: fmp.
provider : Optional[Literal['fmp']]
The provider to use for the query, by default None.
If None, the provider specified in defaults is selected or 'fmp' if there is
@@ -641,6 +651,7 @@ class ROUTER_etf(Container):
"symbol": symbol,
},
extra_params=kwargs,
extra_info={"symbol": {"multiple_items_allowed": ["fmp"]}},
)
)
@@ -808,7 +819,10 @@ class ROUTER_etf(Container):
def price_performance(
self,
symbol: Annotated[
str, OpenBBCustomParameter(description="Symbol to get data for.")
Union[str, List[str]],
OpenBBCustomParameter(
description="Symbol to get data for. Multiple items allowed: fmp."
),
],
provider: Optional[Literal["fmp"]] = None,
**kwargs
@@ -817,8 +831,8 @@ class ROUTER_etf(Container):
Parameters
----------
symbol : str
Symbol to get data for.
symbol : Union[str, List[str]]
Symbol to get data for. Multiple items allowed: fmp.
provider : Optional[Literal['fmp']]
The provider to use for the query, by default None.
If None, the provider specified in defaults is selected or 'fmp' if there is
@@ -891,6 +905,7 @@ class ROUTER_etf(Container):
"symbol": symbol,
},
extra_params=kwargs,
extra_info={"symbol": {"multiple_items_allowed": ["fmp"]}},
)
)

View File

@@ -69,30 +69,32 @@ class ROUTER_fixedincome_government(Container):
-------------
date : date
The date of the data.
week_4 : Optional[float]
4 week Treasury bills rate (secondary market).
month_1 : Optional[float]
1 month treasury rate.
1 month Treasury rate.
month_2 : Optional[float]
2 month treasury rate.
2 month Treasury rate.
month_3 : Optional[float]
3 month treasury rate.
3 month Treasury rate.
month_6 : Optional[float]
6 month treasury rate.
6 month Treasury rate.
year_1 : Optional[float]
1 year treasury rate.
1 year Treasury rate.
year_2 : Optional[float]
2 year treasury rate.
2 year Treasury rate.
year_3 : Optional[float]
3 year treasury rate.
3 year Treasury rate.
year_5 : Optional[float]
5 year treasury rate.
5 year Treasury rate.
year_7 : Optional[float]
7 year treasury rate.
7 year Treasury rate.
year_10 : Optional[float]
10 year treasury rate.
10 year Treasury rate.
year_20 : Optional[float]
20 year treasury rate.
20 year Treasury rate.
year_30 : Optional[float]
30 year treasury rate.
30 year Treasury rate.
Example
-------

View File

@@ -90,9 +90,8 @@ class ROUTER_index(Container):
@validate
def constituents(
self,
index: Annotated[
str,
OpenBBCustomParameter(description="Index to fetch the constituents of."),
symbol: Annotated[
str, OpenBBCustomParameter(description="Symbol to get data for.")
],
provider: Optional[Literal["fmp"]] = None,
**kwargs
@@ -101,8 +100,8 @@ class ROUTER_index(Container):
Parameters
----------
index : str
Index to fetch the constituents of.
symbol : str
Symbol to get data for.
provider : Optional[Literal['fmp']]
The provider to use for the query, by default None.
If None, the provider specified in defaults is selected or 'fmp' if there is
@@ -160,7 +159,7 @@ class ROUTER_index(Container):
)
},
standard_params={
"index": index,
"symbol": symbol,
},
extra_params=kwargs,
)

View File

@@ -133,7 +133,7 @@ class ROUTER_news(Container):
Author of the article. (provider: benzinga, polygon)
teaser : Optional[str]
Teaser of the news. (provider: benzinga)
images : Optional[Union[List[Dict[str, str]], List[str], str]]
images : Optional[Union[List[Dict[str, str]], str, List[str]]]
URL to the images of the news. (provider: benzinga, fmp)
channels : Optional[str]
Channels associated with the news. (provider: benzinga)

View File

@@ -29,7 +29,7 @@ class ROUTER_regulators_sec(Container):
symbol: Annotated[
Union[str, List[str]],
OpenBBCustomParameter(
description="Symbol to get data for. Multiple items allowed: intrinio, yfinance."
description="Symbol to get data for. Multiple items allowed: fmp, intrinio, yfinance."
),
],
provider: Optional[Literal["sec"]] = None,
@@ -40,7 +40,7 @@ class ROUTER_regulators_sec(Container):
Parameters
----------
symbol : Union[str, List[str]]
Symbol to get data for. Multiple items allowed: intrinio, yfinance.
Symbol to get data for. Multiple items allowed: fmp, intrinio, yfinance.
provider : Optional[Literal['sec']]
The provider to use for the query, by default None.
If None, the provider specified in defaults is selected or 'sec' if there is
@@ -87,7 +87,9 @@ class ROUTER_regulators_sec(Container):
},
extra_params=kwargs,
extra_info={
"symbol": {"multiple_items_allowed": ["intrinio", "yfinance"]}
"symbol": {
"multiple_items_allowed": ["fmp", "intrinio", "yfinance"]
}
},
)
)

View File

@@ -1,12 +1,11 @@
"""Alpha Vantage Equity Historical Price Model."""
from datetime import datetime
from datetime import datetime, timedelta
from typing import Any, Dict, List, Literal, Optional
from dateutil.relativedelta import relativedelta
from openbb_alpha_vantage.utils.helpers import (
extract_key_name,
filter_by_dates,
get_interval,
)
from openbb_core.provider.abstract.fetcher import Fetcher
@@ -19,6 +18,7 @@ from openbb_core.provider.utils.descriptions import (
QUERY_DESCRIPTIONS,
)
from openbb_core.provider.utils.helpers import amake_request, get_querystring
from pandas import to_datetime
from pydantic import (
Field,
NonNegativeFloat,
@@ -201,7 +201,17 @@ class AVEquityHistoricalFetcher(
}
for date, values in content.items()
]
filter_by_dates(d, query.start_date, query.end_date)
transformed_data += d
if query.start_date and query.end_date:
transformed_data = list(
filter(
lambda x: to_datetime(query.start_date)
<= to_datetime(x["date"])
<= to_datetime(query.end_date) + timedelta(days=1),
d,
)
)
return [AVEquityHistoricalData.model_validate(d) for d in transformed_data]
return [
AVEquityHistoricalData.model_validate(d)
for d in sorted(transformed_data, key=lambda x: x["date"], reverse=False)
]

View File

@@ -1,6 +1,10 @@
"""Euro Area Yield Curve Model."""
from datetime import datetime, timedelta
# pylint: disable=unused-argument
import asyncio
import warnings
from datetime import datetime
from typing import Any, Dict, List, Literal, Optional
from openbb_core.provider.abstract.fetcher import Fetcher
@@ -8,34 +12,38 @@ from openbb_core.provider.standard_models.eu_yield_curve import (
EUYieldCurveData,
EUYieldCurveQueryParams,
)
from openbb_ecb.utils.ecb_helpers import get_series_data
from openbb_core.provider.utils.errors import EmptyDataError
from openbb_core.provider.utils.helpers import amake_request
from openbb_ecb.utils.yield_curve_series import get_yield_curve_ids
from pandas import DataFrame, to_datetime
from pydantic import Field, field_validator
_warn = warnings.warn
class ECBEUYieldCurveQueryParams(EUYieldCurveQueryParams):
"""Euro Area Yield Curve Query."""
"""ECB Yield Curve Query Params."""
rating: Literal["A", "C"] = Field(
default="A",
description="The rating type.",
rating: Literal["aaa", "all_ratings"] = Field(
default="aaa",
description="The rating type, either 'aaa' or 'all_ratings'.",
)
yield_curve_type: Literal["spot_rate", "instantaneous_forward", "par_yield"] = (
Field(
default="spot_rate",
description="The yield curve type.",
)
)
class ECBEUYieldCurveData(EUYieldCurveData):
"""Euro Area Yield Curve Data."""
"""ECB Yield Curve Data."""
__alias_dict__ = {
"rate": "OBS",
}
@field_validator("OBS", mode="before", check_fields=False)
@field_validator("rate", mode="before", check_fields=False)
@classmethod
def value_validate(cls, v):
"""Validate rate."""
try:
return float(v)
except ValueError:
return None
def normalize_percent(cls, v):
"""Normalize percent."""
return float(v) / 100 if v else None
class ECBEUYieldCurveFetcher(
@@ -44,62 +52,88 @@ class ECBEUYieldCurveFetcher(
List[ECBEUYieldCurveData],
]
):
"""Transform the query, extract and transform the data from the ECB endpoints."""
data_type = ECBEUYieldCurveData
"""ECB Yield Curve Fetcher."""
@staticmethod
def transform_query(params: Dict[str, Any]) -> ECBEUYieldCurveQueryParams:
"""Transform query."""
if params.get("date") is None:
params["date"] = datetime.now().date()
return ECBEUYieldCurveQueryParams(**params)
# pylint: disable=unused-argument
@staticmethod
def extract_data(
async def aextract_data(
query: ECBEUYieldCurveQueryParams,
credentials: Optional[Dict[str, str]],
**kwargs: Any,
) -> list:
) -> List[Dict]:
"""Extract data."""
# Check that the date is in the past
today = datetime.today().date()
if query.date and query.date >= today:
raise ValueError("Date must be in the past")
if not query.date:
date = (datetime.now() - timedelta(days=1)).strftime("%Y-%m-%d")
else:
date = query.date.strftime("%Y-%m-%d")
results = []
yield_suffixes = {
"spot_rate": "SR_",
"instantaneous_forward": "IF_",
"par_yield": "PY",
}
yield_type = f"YC.B.U2.EUR.4F.G_N_{query.rating}.SV_C_YM." + yield_suffixes.get(
query.yield_curve_type, ""
IDS = get_yield_curve_ids(
rating=query.rating,
yield_curve_type=query.yield_curve_type,
)
# Add the maturities
series_id = [f"{yield_type}{m}M" for m in [3, 6]]
series_id += [f"{yield_type}{y}Y" for y in [1, 2, 3, 5, 7, 10, 20, 30]]
YIELD_CURVE = IDS["SERIES_IDS"]
MATURITIES = IDS["MATURITIES"]
data = []
maturities = list(MATURITIES.keys())
for id_ in series_id:
d = get_series_data(id_, date)
maturity = id_.split("_")[-1]
BASE_URL = "https://data.ecb.europa.eu/data-detail-api"
for item in d:
item["maturity"] = maturity
async def get_one(maturity):
"""Each maturity is a separate download."""
url = f"{BASE_URL}/{YIELD_CURVE[maturity]}"
response = await amake_request(url=url, timeout=10)
if isinstance(response, List):
for item in response:
d = {
"date": item.get("PERIOD"),
"maturity": MATURITIES[maturity],
"rate": item.get("OBS_VALUE_AS_IS"),
}
data.extend(d)
results.append(d)
return data
tasks = [get_one(maturity) for maturity in maturities]
await asyncio.gather(*tasks)
return results
# pylint: disable=unused-argument
@staticmethod
def transform_data(
query: ECBEUYieldCurveQueryParams, data: list, **kwargs: Any
query: ECBEUYieldCurveQueryParams,
data: List[Dict],
**kwargs: Any,
) -> List[ECBEUYieldCurveData]:
"""Transform data."""
return [ECBEUYieldCurveData.model_validate(d) for d in data]
if not data:
raise EmptyDataError()
# Find the nearest date to the requested one.
df = DataFrame(data).set_index("date").query("`rate`.notnull()")
df.index = to_datetime(df.index)
dates = df.index.unique().tolist()
date = to_datetime(query.date)
nearest_date = min(dates, key=lambda d: abs(d - date)).strftime("%Y-%m-%d")
df.index = df.index.astype(str)
if nearest_date != date.strftime("%Y-%m-%d"):
_warn(f"Using nearest date: {nearest_date}")
df.index = df.index.astype(str)
results = (
df.filter(like=nearest_date, axis=0)
.sort_values("maturity")
.reset_index(drop=True)
)
return [
ECBEUYieldCurveData.model_validate(d)
for d in results.to_dict(orient="records")
]

View File

@@ -0,0 +1,92 @@
"""ECB Yield Curve Series IDs"""
from typing import Dict, Literal
RATING_DICT = {"aaa": "A", "all_ratings": "C"}
YIELD_TYPE_DICT = {
"spot_rate": "SR",
"instantaneous_forward": "IF",
"par_yield": "PY",
}
def get_yield_curve_ids(
rating: Literal["aaa", "all_ratings"] = "aaa",
yield_curve_type: Literal[
"spot_rate", "instantaneous_forward", "par_yield"
] = "spot_rate",
) -> Dict:
"""Get Yield Curve Series IDs"""
YIELD_CURVE = {
"month_3": f"YC.B.U2.EUR.4F.G_N_{RATING_DICT[rating]}.SV_C_YM.{YIELD_TYPE_DICT[yield_curve_type]}_3M",
"month_6": f"YC.B.U2.EUR.4F.G_N_{RATING_DICT[rating]}.SV_C_YM.{YIELD_TYPE_DICT[yield_curve_type]}_6M",
"year_1": f"YC.B.U2.EUR.4F.G_N_{RATING_DICT[rating]}.SV_C_YM.{YIELD_TYPE_DICT[yield_curve_type]}_1Y",
"year_2": f"YC.B.U2.EUR.4F.G_N_{RATING_DICT[rating]}.SV_C_YM.{YIELD_TYPE_DICT[yield_curve_type]}_2Y",
"year_3": f"YC.B.U2.EUR.4F.G_N_{RATING_DICT[rating]}.SV_C_YM.{YIELD_TYPE_DICT[yield_curve_type]}_3Y",
"year_4": f"YC.B.U2.EUR.4F.G_N_{RATING_DICT[rating]}.SV_C_YM.{YIELD_TYPE_DICT[yield_curve_type]}_4Y",
"year_5": f"YC.B.U2.EUR.4F.G_N_{RATING_DICT[rating]}.SV_C_YM.{YIELD_TYPE_DICT[yield_curve_type]}_5Y",
"year_6": f"YC.B.U2.EUR.4F.G_N_{RATING_DICT[rating]}.SV_C_YM.{YIELD_TYPE_DICT[yield_curve_type]}_6Y",
"year_7": f"YC.B.U2.EUR.4F.G_N_{RATING_DICT[rating]}.SV_C_YM.{YIELD_TYPE_DICT[yield_curve_type]}_7Y",
"year_8": f"YC.B.U2.EUR.4F.G_N_{RATING_DICT[rating]}.SV_C_YM.{YIELD_TYPE_DICT[yield_curve_type]}_8Y",
"year_9": f"YC.B.U2.EUR.4F.G_N_{RATING_DICT[rating]}.SV_C_YM.{YIELD_TYPE_DICT[yield_curve_type]}_9Y",
"year_10": f"YC.B.U2.EUR.4F.G_N_{RATING_DICT[rating]}.SV_C_YM.{YIELD_TYPE_DICT[yield_curve_type]}_10Y",
"year_11": f"YC.B.U2.EUR.4F.G_N_{RATING_DICT[rating]}.SV_C_YM.{YIELD_TYPE_DICT[yield_curve_type]}_11Y",
"year_12": f"YC.B.U2.EUR.4F.G_N_{RATING_DICT[rating]}.SV_C_YM.{YIELD_TYPE_DICT[yield_curve_type]}_12Y",
"year_13": f"YC.B.U2.EUR.4F.G_N_{RATING_DICT[rating]}.SV_C_YM.{YIELD_TYPE_DICT[yield_curve_type]}_13Y",
"year_14": f"YC.B.U2.EUR.4F.G_N_{RATING_DICT[rating]}.SV_C_YM.{YIELD_TYPE_DICT[yield_curve_type]}_14Y",
"year_15": f"YC.B.U2.EUR.4F.G_N_{RATING_DICT[rating]}.SV_C_YM.{YIELD_TYPE_DICT[yield_curve_type]}_15Y",
"year_16": f"YC.B.U2.EUR.4F.G_N_{RATING_DICT[rating]}.SV_C_YM.{YIELD_TYPE_DICT[yield_curve_type]}_16Y",
"year_17": f"YC.B.U2.EUR.4F.G_N_{RATING_DICT[rating]}.SV_C_YM.{YIELD_TYPE_DICT[yield_curve_type]}_17Y",
"year_18": f"YC.B.U2.EUR.4F.G_N_{RATING_DICT[rating]}.SV_C_YM.{YIELD_TYPE_DICT[yield_curve_type]}_18Y",
"year_19": f"YC.B.U2.EUR.4F.G_N_{RATING_DICT[rating]}.SV_C_YM.{YIELD_TYPE_DICT[yield_curve_type]}_19Y",
"year_20": f"YC.B.U2.EUR.4F.G_N_{RATING_DICT[rating]}.SV_C_YM.{YIELD_TYPE_DICT[yield_curve_type]}_20Y",
"year_21": f"YC.B.U2.EUR.4F.G_N_{RATING_DICT[rating]}.SV_C_YM.{YIELD_TYPE_DICT[yield_curve_type]}_21Y",
"year_22": f"YC.B.U2.EUR.4F.G_N_{RATING_DICT[rating]}.SV_C_YM.{YIELD_TYPE_DICT[yield_curve_type]}_22Y",
"year_23": f"YC.B.U2.EUR.4F.G_N_{RATING_DICT[rating]}.SV_C_YM.{YIELD_TYPE_DICT[yield_curve_type]}_23Y",
"year_24": f"YC.B.U2.EUR.4F.G_N_{RATING_DICT[rating]}.SV_C_YM.{YIELD_TYPE_DICT[yield_curve_type]}_24Y",
"year_25": f"YC.B.U2.EUR.4F.G_N_{RATING_DICT[rating]}.SV_C_YM.{YIELD_TYPE_DICT[yield_curve_type]}_25Y",
"year_26": f"YC.B.U2.EUR.4F.G_N_{RATING_DICT[rating]}.SV_C_YM.{YIELD_TYPE_DICT[yield_curve_type]}_26Y",
"year_27": f"YC.B.U2.EUR.4F.G_N_{RATING_DICT[rating]}.SV_C_YM.{YIELD_TYPE_DICT[yield_curve_type]}_27Y",
"year_28": f"YC.B.U2.EUR.4F.G_N_{RATING_DICT[rating]}.SV_C_YM.{YIELD_TYPE_DICT[yield_curve_type]}_28Y",
"year_29": f"YC.B.U2.EUR.4F.G_N_{RATING_DICT[rating]}.SV_C_YM.{YIELD_TYPE_DICT[yield_curve_type]}_29Y",
"year_30": f"YC.B.U2.EUR.4F.G_N_{RATING_DICT[rating]}.SV_C_YM.{YIELD_TYPE_DICT[yield_curve_type]}_30Y",
}
MATURITY_VALUES = {
"month_3": 0.25,
"month_6": 0.5,
"year_1": 1,
"year_2": 2,
"year_3": 3,
"year_4": 4,
"year_5": 5,
"year_6": 6,
"year_7": 7,
"year_8": 8,
"year_9": 9,
"year_10": 10,
"year_11": 11,
"year_12": 12,
"year_13": 13,
"year_14": 14,
"year_15": 15,
"year_16": 16,
"year_17": 17,
"year_18": 18,
"year_19": 19,
"year_20": 20,
"year_21": 21,
"year_22": 22,
"year_23": 23,
"year_24": 24,
"year_25": 25,
"year_26": 26,
"year_27": 27,
"year_28": 28,
"year_29": 29,
"year_30": 30,
}
return dict(SERIES_IDS=YIELD_CURVE, MATURITIES=MATURITY_VALUES)

View File

@@ -107,4 +107,7 @@ class FMPCryptoHistoricalFetcher(
query: FMPCryptoHistoricalQueryParams, data: List[Dict], **kwargs: Any
) -> List[FMPCryptoHistoricalData]:
"""Return the transformed data."""
return [FMPCryptoHistoricalData.model_validate(d) for d in data]
return [
FMPCryptoHistoricalData.model_validate(d)
for d in sorted(data, key=lambda x: x["date"], reverse=False)
]

View File

@@ -107,4 +107,7 @@ class FMPCurrencyHistoricalFetcher(
and d.get("change") != 0
and d.get("changeOverTime") != 0
]
return [FMPCurrencyHistoricalData.model_validate(d) for d in data]
return [
FMPCurrencyHistoricalData.model_validate(d)
for d in sorted(data, key=lambda x: x["date"], reverse=False)
]

View File

@@ -132,4 +132,7 @@ class FMPEquityHistoricalFetcher(
query: FMPEquityHistoricalQueryParams, data: List[Dict], **kwargs: Any
) -> List[FMPEquityHistoricalData]:
"""Return the transformed data."""
return [FMPEquityHistoricalData.model_validate(d) for d in data]
return [
FMPEquityHistoricalData.model_validate(d)
for d in sorted(data, key=lambda x: x["date"], reverse=False)
]

View File

@@ -109,7 +109,7 @@ class FMPIndexHistoricalFetcher(
query: FMPIndexHistoricalQueryParams, data: List[Dict], **kwargs: Any
) -> List[FMPIndexHistoricalData]:
"""Return the transformed data."""
if query.sort == "asc":
data = sorted(data, key=lambda x: x["date"], reverse=True)
return [FMPIndexHistoricalData.model_validate(d) for d in data]
return [
FMPIndexHistoricalData.model_validate(d)
for d in sorted(data, key=lambda x: x["date"], reverse=False)
]

View File

@@ -229,4 +229,7 @@ class IntrinioEquityHistoricalFetcher(
**kwargs: Any,
) -> List[IntrinioEquityHistoricalData]:
"""Return the transformed data."""
return [IntrinioEquityHistoricalData.model_validate(d) for d in data]
return [
IntrinioEquityHistoricalData.model_validate(d)
for d in sorted(data, key=lambda x: x["date"], reverse=False)
]

View File

@@ -152,7 +152,7 @@ class PolygonCryptoHistoricalFetcher(
for r in results:
r["t"] = datetime.fromtimestamp(r["t"] / 1000, tz=timezone("UTC"))
if query._timespan not in ["second", "minute", "hour"]:
r["t"] = r["t"].date()
r["t"] = r["t"].date().strftime("%Y-%m-%d")
else:
r["t"] = r["t"].strftime("%Y-%m-%dT%H:%M:%S%z")
if "," in query.symbol:
@@ -172,4 +172,7 @@ class PolygonCryptoHistoricalFetcher(
"""Transform the data."""
if not data:
raise EmptyDataError()
return [PolygonCryptoHistoricalData.model_validate(d) for d in data]
return [
PolygonCryptoHistoricalData.model_validate(d)
for d in sorted(data, key=lambda x: x["t"], reverse=False)
]

View File

@@ -149,7 +149,7 @@ class PolygonCurrencyHistoricalFetcher(
for r in results:
r["t"] = datetime.fromtimestamp(r["t"] / 1000, tz=timezone("UTC"))
if query._timespan not in ["second", "minute", "hour"]:
r["t"] = r["t"].date()
r["t"] = r["t"].date().strftime("%Y-%m-%d")
else:
r["t"] = r["t"].strftime("%Y-%m-%dT%H:%M:%S%z")
if "," in query.symbol:
@@ -169,4 +169,7 @@ class PolygonCurrencyHistoricalFetcher(
"""Return the transformed data."""
if not data:
raise EmptyDataError()
return [PolygonCurrencyHistoricalData.model_validate(d) for d in data]
return [
PolygonCurrencyHistoricalData.model_validate(d)
for d in sorted(data, key=lambda x: x["t"], reverse=False)
]

View File

@@ -156,9 +156,9 @@ class PolygonEquityHistoricalFetcher(
r["t"] / 1000, tz=timezone("America/New_York")
)
if query._timespan not in ["second", "minute", "hour"]:
r["t"] = r["t"].date()
r["t"] = r["t"].date().strftime("%Y-%m-%d")
else:
r["t"] = r["t"].strftime("%Y-%m-%dT%H:%M:%S")
r["t"] = r["t"].strftime("%Y-%m-%dT%H:%M:%S%z")
if "," in query.symbol:
r["symbol"] = symbol
@@ -178,4 +178,7 @@ class PolygonEquityHistoricalFetcher(
"""Transform the data from the Polygon endpoint."""
if not data:
raise EmptyDataError()
return [PolygonEquityHistoricalData.model_validate(d) for d in data]
return [
PolygonEquityHistoricalData.model_validate(d)
for d in sorted(data, key=lambda x: x["t"], reverse=False)
]

View File

@@ -151,9 +151,9 @@ class PolygonIndexHistoricalFetcher(
r["t"] / 1000, tz=timezone("America/New_York")
)
if query._timespan not in ["second", "minute", "hour"]:
r["t"] = r["t"].date()
r["t"] = r["t"].date().strftime("%Y-%m-%d")
else:
r["t"] = r["t"].strftime("%Y-%m-%dT%H:%M:%S")
r["t"] = r["t"].strftime("%Y-%m-%dT%H:%M:%S%z")
if "," in query.symbol:
r["symbol"] = symbol
@@ -173,4 +173,7 @@ class PolygonIndexHistoricalFetcher(
"""Transform the data."""
if not data:
raise EmptyDataError()
return [PolygonIndexHistoricalData.model_validate(d) for d in data]
return [
PolygonIndexHistoricalData.model_validate(d)
for d in sorted(data, key=lambda x: x["t"], reverse=False)
]

View File

@@ -17,7 +17,7 @@ from openbb_core.provider.utils.errors import EmptyDataError
from openbb_yfinance.utils.helpers import yf_download
from openbb_yfinance.utils.references import INTERVALS, PERIODS
from pandas import to_datetime
from pydantic import Field, field_validator
from pydantic import Field
class YFinanceCryptoHistoricalQueryParams(CryptoHistoricalQueryParams):
@@ -37,14 +37,6 @@ class YFinanceCryptoHistoricalQueryParams(CryptoHistoricalQueryParams):
class YFinanceCryptoHistoricalData(CryptoHistoricalData):
"""Yahoo Finance Crypto Historical Price Data."""
@field_validator("date", mode="before", check_fields=False)
@classmethod
def date_validate(cls, v):
"""Return datetime object from string."""
if isinstance(v, str):
return datetime.strptime(v, "%Y-%m-%dT%H:%M:%S")
return v
class YFinanceCryptoHistoricalFetcher(
Fetcher[
@@ -118,6 +110,9 @@ class YFinanceCryptoHistoricalFetcher(
data.reset_index(inplace=True)
data.rename(columns={"index": "date"}, inplace=True)
if query.interval in ["1d", "1W", "1M", "3M"]:
data["date"] = data["date"].dt.strftime("%Y-%m-%d")
return data.to_dict("records")
@staticmethod

View File

@@ -1,7 +1,7 @@
"""Yahoo Finance Currency Price Model."""
# ruff: noqa: SIM105
# pylint: disable=unused-argument
from datetime import datetime, timedelta
from typing import Any, Dict, List, Optional
@@ -71,7 +71,7 @@ class YFinanceCurrencyHistoricalFetcher(
@staticmethod
def extract_data(
query: YFinanceCurrencyHistoricalQueryParams, # pylint: disable=unused-argument
query: YFinanceCurrencyHistoricalQueryParams,
credentials: Optional[Dict[str, str]],
**kwargs: Any,
) -> List[Dict]:
@@ -106,6 +106,8 @@ class YFinanceCurrencyHistoricalFetcher(
data.reset_index(inplace=True)
data.rename(columns={"index": "date"}, inplace=True)
if query.interval in ["1d", "1W", "1M", "3M"]:
data["date"] = data["date"].dt.strftime("%Y-%m-%d")
return data.to_dict("records")

View File

@@ -105,7 +105,7 @@ class YFinanceEquityHistoricalFetcher(
@staticmethod
def extract_data(
query: YFinanceEquityHistoricalQueryParams, # pylint: disable=unused-argument
query: YFinanceEquityHistoricalQueryParams,
credentials: Optional[Dict[str, str]],
**kwargs: Any,
) -> List[Dict]:
@@ -161,6 +161,8 @@ class YFinanceEquityHistoricalFetcher(
data.reset_index(inplace=True)
data.rename(columns={"index": "date"}, inplace=True)
if query.interval in ["1d", "1W", "1M", "3M"]:
data["date"] = data["date"].dt.strftime("%Y-%m-%d")
return data.to_dict("records")

View File

@@ -1,6 +1,7 @@
"""Yahoo Finance Index Historical Model."""
# ruff: noqa: SIM105
# pylint: disable=unused-argument
from datetime import datetime, timedelta
from typing import Any, Dict, List, Optional
@@ -89,7 +90,7 @@ class YFinanceIndexHistoricalFetcher(
@staticmethod
def extract_data(
query: YFinanceIndexHistoricalQueryParams, # pylint: disable=unused-argument
query: YFinanceIndexHistoricalQueryParams,
credentials: Optional[Dict[str, str]],
**kwargs: Any,
) -> List[dict]:
@@ -125,6 +126,8 @@ class YFinanceIndexHistoricalFetcher(
data.reset_index(inplace=True)
data.rename(columns={"index": "date"}, inplace=True)
if query.interval in ["1d", "1W", "1M", "3M"]:
data["date"] = data["date"].dt.strftime("%Y-%m-%d")
return data.to_dict("records")

View File

@@ -37,7 +37,7 @@ def test_extension_map():
"""Ensure only required extensions are built and versions respect pyproject.toml"""
this_dir = Path(__file__).parent
ext_map = load_ext_map(
Path(this_dir, "..", "openbb", "package", "extension_map.json")
Path(this_dir, "..", "openbb", "assets", "extension_map.json")
)
req_ext = load_req_ext(Path(this_dir, "..", "pyproject.toml"))

View File

@@ -37,7 +37,7 @@ If your backend supports it is possible to pass a symbol, date or other optional
```
:::tip
The easiest way to pass optional parameters is write them into cells and reference them in the function. For example, `=OBB.BYOD(...,A1:B2)` where A1 contains "param1", B1 "value1", A2 "param2", B2 "value2" and so on.
The easiest way to pass optional parameters is to write them into cells and reference them in the function. For example, `=OBB.BYOD(...,A1:B2)` where A1 contains "param1", B1 "value1", A2 "param2", B2 "value2" and so on.
:::
#### Additional notes

61
website/content/excel/data-slicer.md vendored Normal file
View File

@@ -0,0 +1,61 @@
---
title: Data slicer
sidebar_position: 5
description: Slice Excel ranges by label or index
keywords:
- Microsoft Excel
- Add-in
- Advanced
- Slice data
- Data slicer
- Get specific fields
---
<!-- markdownlint-disable MD033 -->
import HeadTitle from '@site/src/components/General/HeadTitle.tsx';
<HeadTitle title="Data slicer | OpenBB Add-in for Excel Docs" />
To help you slice parts of data, we provide the [OBB.GET](https://docs.openbb.co/excel/reference/get) function. This function allows to slice rows, columns or range subsets. It is useful to extract specific fields from the `OBB.` custom functions. Data can be sliced by label or index.
### Example
- Suppose you called an `OBB.` function and it returned the following data at cells A1:D3:
| period_ending | revenue | cost_of_revenue | gross_profit |
|---|---|---|---|---|
| 2023/09/30 | 383 285 000 000.00 | 214 137 000 000.00 | 169 148 000 000.00 |
| 2022/09/24 | 394 328 000 000.00 | 223 546 000 000.00 | 170 782 000 000.00 |
| 2021/09/25 | 365 817 000 000.00 | 212 981 000 000.00 | 152 836 000 000.00 |
- Slicing a single row:
```excel
=OBB.GET(A1:D3,DATE(2023,9,30))
```
:::note
When passing date labels make sure to use the format `YYYY/MM/DD` or refer to a cell range containing Excel date format - DATE(year,month,day).
:::
- Slicing a single column:
```excel
=OBB.GET(A1:D3,,"revenue")
```
- Slicing by index:
```excel
=OBB.GET(A1:D3,2,3)
```
- Slicing multiple rows and columns:
```excel
=OBB.GET(A1:D3,{"2023/09/30","2021/09/25"},{"cost_of_revenue","gross_profit"})
```
:::tip
The easiest way to pass ranges is to write them into cells and reference them in the function. For example, `=OBB.GET(...,A1:A2)` where A1 contains "item1", A2 "item2".
:::

View File

@@ -1,5 +1,36 @@
{
"functions": [
{
"description": "Slice data from a range.",
"helpUrl": "https://docs.openbb.co/excel/data-slicer",
"id": "GET",
"name": "GET",
"parameters": [
{
"description": "Data to slice.",
"dimensionality": "matrix",
"name": "data",
"type": "any"
},
{
"description": "Range of row labels or indices.",
"dimensionality": "matrix",
"name": "row",
"optional": true,
"type": "any"
},
{
"description": "Range of column labels or indices.",
"dimensionality": "matrix",
"name": "column",
"optional": true,
"type": "any"
}
],
"result": {
"dimensionality": "matrix"
}
},
{
"description": "Bring Your Own Data features from OpenBB Terminal Pro.",
"helpUrl": "https://docs.openbb.co/excel/data-connectors",
@@ -4403,4 +4434,4 @@
}
}
]
}
}

View File

@@ -91,6 +91,7 @@ The `Fetcher` class implementation is based on the [TET pattern](/platform/devel
- Transform the query: the output of this method should be `QueryParams` child.
- Extract the data: the output of this method can be `Any` but it's recommended to be a `Dict` (will facilitate the next step of the fetcher's action, the data transformation).
- Transform the data: the output of this method should be a `List[Data]` or `Data` (or a child of it).
:::
### The `OBBject` class
@@ -101,9 +102,7 @@ The class includes several fields such as results, provider, warnings, chart, an
The class provides several methods for converting the fetched data into different formats, including `to_df` (or `to_dataframe`) for converting to a pandas DataFrame, `to_polars` for converting to a Polars DataFrame, `to_numpy` for converting to a numpy array, and `to_dict` for converting to a dictionary.
The `to_chart` method allows for creating or updating a chart based on the fetched data, and the show method is used to display the chart.
The class also includes a `__repr__` method for a human-readable representation of the object, and a `model_parametrized_name method` for returning the model name with its parameters.
The class also includes a `__repr__` method for a human-readable representation of the object, as well as the familiar Pydantic BaseModel methods like `model_dump()` and `model_json_schema()`.
### The `Router` class

View File

@@ -33,8 +33,14 @@ class CommandLib:
# These examples will be generated in the core, but we keep them here meanwhile
EXAMPLE_PARAMS: Dict[str, Dict] = {
"/get": {
"data": '{"a","b","c";"d","e","f"}',
"row": '"d"',
"column": '"c"',
},
"/byod": {
"widget": '"widget_name"',
"backend": '"backend_name"',
},
"crypto": {
"symbol": '"BTCUSD"',
@@ -247,11 +253,9 @@ class CommandLib:
standard_eg += f"{p_value}{sep}"
standard_eg = standard_eg.strip(f"{sep} ") + ")"
if required_eg == standard_eg:
return {"A. Required": required_eg}
# Uncomment to add standard examples
# return {"A. Required": required_eg, "B. Standard": standard_eg}
return {"A. Required": required_eg}
if cmd in ("/get", "/byod"):
return {"Required": required_eg, "Standard": standard_eg}
return {"Required": required_eg}
def get_info(self, cmd: str) -> Dict[str, Any]:
"""Get the info for a command."""
@@ -337,7 +341,7 @@ class Editor:
def get_syntax() -> str:
if sig := cmd_info["signature"]:
syntax = "## Syntax\n\n"
syntax += f"```{self.interface} wordwrap\n"
syntax += f"```{self.interface}\n"
syntax += f"{sig}\n"
syntax += "```\n\n"
return syntax
@@ -381,7 +385,7 @@ class Editor:
examples = "### Example\n\n"
for _, v in cmd_examples.items():
# examples += f"### {k}\n\n"
examples += f"```{self.interface} wordwrap\n"
examples += f"```{self.interface}\n"
examples += f"{v}\n"
examples += "```\n\n"