Skip to content

Commit

Permalink
Adding AV and StockEOD (#5365)
Browse files Browse the repository at this point in the history
* boilerplate for av provider

* adding stock eod for av

* ruff n black

* changing available functions for better ux

* docstring

* move debug_mode out of system settings (#5366)

* move debug_mode out of system settings

* Update router.py

* update readme with debug mode

* dev_mode hub service + openbb_ namespace to Env

* update readme

* copy env to avoid problems w global state

* Linting

* rebuild python interface + fix py38

* Add @classproperty to fetcher for nested return types (#5371)

* add classproperty to fetcher and cpi.py

* type var bound

* validate model in registry map

* better exception msg

* fix fred mypy and add data_type

* typo

* specific model validation

* specific model validation

* reformat error msg

* better

* msg

* add black & ruff to core deps

* supress linter errors if not debug mode

* pylint + regen fixedincome

* py38 compat

* fix bug getting provider for placeholder cmds + ruff

* Revert "fix bug getting provider for placeholder cmds + ruff"

This reverts commit 1328c0b.

* ruff

* avoid bug for placeholder and no provider

* expose available providers in provider interface

* poetry toml to match ci ruff version

* first we ruff and then we black

* add back noqa: E501 to docstrings inside package

* use singleton for provider interface: same functionality (#5372)

* use singleton for provider interface: same functionality

* ruff

* docstring typo

* update readme with tuna reference

* move to correct readme

* Merge

* Some error handling (#5357)

* Removed prints (#5367)

* Removed prints

* FIxed typing

* FIxed typing

* Fixed typing

* Fixed typing

* Fixed typing

* Fixed typing

* Fixed typing

* Fix

* Reverted stuff

* Feature/move package (#5374)

* move package to openbb_sdk

* avoid test writing files

* black

* revert toml changes, will do it in another PR

* run build in a separate process

* change error msg

* rename func

* change msg

* rename func + msg

* change param name

* update readme

* lazy load some deps

* Update README.md

* modules is better

* doc

* ruff

* pylint

* fix autocomplete (#5383)

* Core tests (#5375)

* makeing the function easier - @montezdesousa pls review this commit

* improve update_provider_choices readbility

* unncessary classmethod

* command runner tests

* fixing wrong attribution

* some tests for the charting manager

* changing fetcher and models to have a shorter name

* @the-prax suggestions

* fixing references to av

* making function a private fild so the user don't access it

* unbreaking changes

* Improve snake_case generation

* adding av to pyproject.toml

* stating the function as a private field while keeping validation for function specific fields

---------

Co-authored-by: montezdesousa <[email protected]>
Co-authored-by: Diogo Sousa <[email protected]>
Co-authored-by: Igor Radovanovic <[email protected]>
Co-authored-by: Colin Delahunty <[email protected]>
  • Loading branch information
5 people authored Aug 29, 2023
1 parent 172a923 commit 080ecff
Show file tree
Hide file tree
Showing 9 changed files with 305 additions and 2 deletions.
11 changes: 11 additions & 0 deletions openbb_sdk/providers/alpha_vantage/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# OpenBB Alpha Vantage Provider

This extension integrates the Alpha Vantage data provider into the OpenBB SDK.

## Installation

To install the extension, run the following command in this folder:

```bash
pip install .
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
"""Alpha Vantage Provider module."""
from openbb_provider.abstract.provider import Provider

from openbb_alpha_vantage.models.stock_eod import AVStockEODFetcher

alpha_vantage_provider = Provider(
name="alpha_vantage",
website="https://www.alphavantage.co/documentation/",
description="""Alpha Vantage provides realtime and historical
financial market data through a set of powerful and developer-friendly data APIs
and spreadsheets. From traditional asset classes (e.g., stocks, ETFs, mutual funds)
to economic indicators, from foreign exchange rates to commodities,
from fundamental data to technical indicators, Alpha Vantage
is your one-stop-shop for enterprise-grade global market data delivered through
cloud-based APIs, Excel, and Google Sheets. """,
required_credentials=["api_key"],
fetcher_dict={
"StockEOD": AVStockEODFetcher,
},
)
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
"""Alpha Vantage Stock End of Day fetcher."""


from datetime import datetime
from typing import Any, Dict, List, Literal, Optional, get_args

import pandas as pd
from dateutil.relativedelta import relativedelta
from openbb_provider.abstract.fetcher import Fetcher
from openbb_provider.standard_models.stock_eod import StockEODData, StockEODQueryParams
from openbb_provider.utils.descriptions import DATA_DESCRIPTIONS, QUERY_DESCRIPTIONS
from openbb_provider.utils.helpers import get_querystring
from pydantic import Field, NonNegativeFloat, PositiveFloat, root_validator, validator


class AVStockEODQueryParams(StockEODQueryParams):
"""Alpha Vantage Stock End of Day Query.
Source: https://www.alphavantage.co/documentation/
"""

_function: Literal[
"TIME_SERIES_INTRADAY",
"TIME_SERIES_DAILY",
"TIME_SERIES_WEEKLY",
"TIME_SERIES_MONTHLY",
"TIME_SERIES_DAILY_ADJUSTED",
"TIME_SERIES_WEEKLY_ADJUSTED",
"TIME_SERIES_MONTHLY_ADJUSTED",
] = Field(
description="The time series of your choice. ",
default="TIME_SERIES_DAILY",
)
period: Literal["intraday", "daily", "weekly", "monthly"] = Field(
default="daily", description=QUERY_DESCRIPTIONS.get("period", "")
)
interval: Optional[Literal["1min", "5min", "15min", "30min", "60min"]] = Field(
description="The interval between two consecutive data points in the time series.",
default="60min",
available_on_functions=["TIME_SERIES_INTRADAY"],
required_on_functions=["TIME_SERIES_INTRADAY"],
)
adjusted: Optional[bool] = Field(
description="Output time series is adjusted by historical split and dividend events.",
default=True,
available_on_functions=["TIME_SERIES_INTRADAY"],
)
extended_hours: Optional[bool] = Field(
description="Extended trading hours during pre-market and after-hours.",
default=False,
available_on_functions=["TIME_SERIES_INTRADAY"],
)
month: Optional[str] = Field(
description="Query a specific month in history (in YYYY-MM format).",
default=None,
available_on_functions=["TIME_SERIES_INTRADAY"],
)
outputsize: Optional[Literal["compact", "full"]] = Field(
description="Compact returns only the latest 100 data points in the intraday "
"time series; full returns trailing 30 days of the most recent intraday data "
"if the month parameter (see above) is not specified, or the full intraday "
"data for a specific month in history if the month parameter is specified.",
default="full",
available_on_functions=[
"TIME_SERIES_INTRADAY",
"TIME_SERIES_DAILY",
"TIME_SERIES_DAILY_ADJUSTED",
],
)

@root_validator
def setup_function(cls, values): # pylint: disable=E0213
"""Set the function based on the period."""

functions_based_on_period = {
"intraday": "TIME_SERIES_INTRADAY",
"daily": "TIME_SERIES_DAILY",
"weekly": "TIME_SERIES_WEEKLY",
"monthly": "TIME_SERIES_MONTHLY",
}
values["_function"] = functions_based_on_period[values["period"]]
return values

@root_validator
def adjusted_function_validate(cls, values): # pylint: disable=E0213
"""
Validate that the function is adjusted if the `adjusted` parameter is set to True.
"""

function = values["_function"]
adjusted = values.get("adjusted", None)

if function != "TIME_SERIES_INTRADAY":
values["_function"] = function if not adjusted else f"{function}_ADJUSTED"

return values

@root_validator
def on_functions_validate(cls, values): # pylint: disable=E0213
"""
Validate that the functions used on custom extra Field attributes
`available_on_functions` and `required_on_functions` are valid functions.
"""
custom_attributes = ["available_on_functions", "required_on_functions"]

fields = cls.__fields__
available_functions = get_args(cls.__annotations__["_function"])

if values["_function"] not in available_functions:
raise ValueError(
f"Function {values['_function']} must be on of the following: {available_functions}"
)

def validate_functions(functions: List[str]):
for f in functions:
if f not in available_functions:
raise ValueError(
f"Function {f} must be on of the following: {available_functions}"
)

for field in fields:
for attr in custom_attributes:
if functions := fields[field].field_info.extra.get(attr, None):
validate_functions(functions)

return values

@root_validator
def on_functions_criteria_validate(cls, values): # pylint: disable=E0213
"""
Validate that the fields are set to None if the function is not available
and that the required fields are not None if the function is required.
"""

fields = cls.__fields__
function = values["_function"]

for field in fields:
if (
available_on_functions := fields[field].field_info.extra.get(
"available_on_functions", None
)
) and function not in available_on_functions:
values[field] = None
if (
(
required_on_functions := fields[field].field_info.extra.get(
"required_on_functions", None
)
)
and function in required_on_functions
and values[field] is None
):
raise ValueError(f"Field {field} is required on function {function}")

return values

@validator("month")
def month_validate(cls, v): # pylint: disable=E0213
"""Validate month, check if the month is in YYYY-MM format."""
if v is not None:
try:
datetime.strptime(v, "%Y-%m")
except ValueError as e:
raise e
return v


class AVStockEODData(StockEODData):
"""Alpha Vantage Stock End of Day Data."""

class Config:
"""Pydantic alias config using fields dict."""

fields = {"date": "timestamp", "adj_close": "adjusted_close"}

adjusted_close: PositiveFloat = Field(
description=DATA_DESCRIPTIONS.get("adj_close", "")
)
dividend_amount: NonNegativeFloat = Field(
description="Dividend amount paid for the corresponding date.",
)
split_coefficient: NonNegativeFloat = Field(
description="Split coefficient for the corresponding date.",
)


class AVStockEODFetcher(
Fetcher[
AVStockEODQueryParams,
List[AVStockEODData],
]
):
"""Transform the query, extract and transform the data from the Alpha Vantage endpoints."""

@staticmethod
def transform_query(params: Dict[str, Any]) -> AVStockEODQueryParams:
"""Transform the query."""

transformed_params = params

now = datetime.now().date()
if params.get("start_date") is None:
transformed_params["start_date"] = now - relativedelta(years=1)

if params.get("end_date") is None:
transformed_params["end_date"] = now

return AVStockEODQueryParams(**transformed_params)

@staticmethod
def extract_data(
query: AVStockEODQueryParams,
credentials: Optional[Dict[str, str]],
**kwargs: Any,
) -> dict:
"""Return the raw data from the Alpha Vantage endpoint."""

api_key = credentials.get("alpha_vantage_api_key") if credentials else ""

query_dict = query.dict()
query_dict["function"] = query_dict.pop("_function")

query_str = get_querystring(query_dict, ["start_date", "end_date"])

url = f"https://www.alphavantage.co/query?{query_str}&datatype=csv&apikey={api_key}"

data = pd.read_csv(url)
data["timestamp"] = pd.to_datetime(data["timestamp"])

data = data[
(data["timestamp"] >= pd.to_datetime(query.start_date))
& (data["timestamp"] <= pd.to_datetime(query.end_date))
]

return data.to_dict("records")

@staticmethod
def transform_data(
data: dict,
) -> List[AVStockEODData]:
"""Transform the data to the standard format."""

return [AVStockEODData.parse_obj(d) for d in data]
Empty file.
18 changes: 18 additions & 0 deletions openbb_sdk/providers/alpha_vantage/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[tool.poetry]
name = "openbb-alpha-vantage"
version = "0.1.0"
description = ""
authors = ["OpenBB Team <[email protected]>"]
readme = "README.md"
packages = [{ include = "openbb_alpha_vantage" }]

[tool.poetry.dependencies]
python = "^3.8"
openbb-provider = { path = "../../sdk/provider" }

[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

[tool.poetry.plugins."openbb_provider_extension"]
alpha_vantage = "openbb_alpha_vantage:alpha_vantage_provider"
1 change: 1 addition & 0 deletions openbb_sdk/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ openbb-polygon = { path = "./providers/polygon" }
openbb-fred = { path = "./providers/fred" }
openbb-yfinance = { path = "./providers/yfinance" }
openbb-cboe = { path = "./providers/cboe"}
openbb-alpha-vantage = { path = "./providers/alpha-vantage" }


[tool.poetry.group.dev.dependencies]
Expand Down
5 changes: 4 additions & 1 deletion openbb_sdk/sdk/core/openbb_core/app/command_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,12 +94,14 @@ def merge_args_and_kwargs(

@staticmethod
def update_command_context(
func: Callable,
kwargs: Dict[str, Any],
system_settings: SystemSettings,
user_settings: UserSettings,
) -> Dict[str, Any]:
"""Update the command context with the available user and system settings."""
if "cc" in kwargs:
argcount = func.__code__.co_argcount
if "cc" in func.__code__.co_varnames[:argcount]:
kwargs["cc"] = CommandContext(
user_settings=user_settings,
system_settings=system_settings,
Expand Down Expand Up @@ -202,6 +204,7 @@ def build(
kwargs=kwargs,
)
kwargs = cls.update_command_context(
func=func,
kwargs=kwargs,
system_settings=system_settings,
user_settings=user_settings,
Expand Down
8 changes: 7 additions & 1 deletion openbb_sdk/sdk/provider/openbb_provider/utils/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,5 +99,11 @@ def make_request(


def to_snake_case(string: str) -> str:
"""Convert a string to snake case."""
s1 = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", string)
return re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1).lower()
return (
re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1)
.lower()
.replace(" ", "_")
.replace("__", "_")
)

0 comments on commit 080ecff

Please sign in to comment.