mirror of
https://github.com/OpenBB-finance/OpenBB.git
synced 2026-11-06 09:22:26 +00:00
Merge branch 'feature/argparse_translator' of github.com:OpenBB-finance/OpenBBTerminal into feature/argparse_translator
This commit is contained in:
22
.github/workflows/unit-test.yml
vendored
22
.github/workflows/unit-test.yml
vendored
@@ -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
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from pydantic import BaseModel, Field
|
||||
from uuid_extensions import uuid7str
|
||||
from uuid_extensions import uuid7str # type: ignore
|
||||
|
||||
|
||||
class Tagged(BaseModel):
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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"))
|
||||
]
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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},
|
||||
)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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', '')}"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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_)
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
|
||||
@@ -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}"
|
||||
)
|
||||
|
||||
|
||||
@@ -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",
|
||||
@@ -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"]
|
||||
}
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"]}},
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -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"]}},
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
-------
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
]
|
||||
|
||||
@@ -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")
|
||||
]
|
||||
|
||||
@@ -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)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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)
|
||||
]
|
||||
|
||||
@@ -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)
|
||||
]
|
||||
|
||||
@@ -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)
|
||||
]
|
||||
|
||||
@@ -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)
|
||||
]
|
||||
|
||||
@@ -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)
|
||||
]
|
||||
|
||||
@@ -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)
|
||||
]
|
||||
|
||||
@@ -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)
|
||||
]
|
||||
|
||||
@@ -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)
|
||||
]
|
||||
|
||||
@@ -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)
|
||||
]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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"))
|
||||
|
||||
|
||||
2
website/content/excel/data-connectors.md
vendored
2
website/content/excel/data-connectors.md
vendored
@@ -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
61
website/content/excel/data-slicer.md
vendored
Normal 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".
|
||||
:::
|
||||
33
website/content/excel/functions.json
vendored
33
website/content/excel/functions.json
vendored
@@ -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 @@
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user