Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature] Add AlphaVantage to historical_eps #6155

Merged
merged 2 commits into from
Mar 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1693,6 +1693,14 @@ def test_equity_market_snapshots(params, headers):
"params",
[
({"symbol": "AAPL", "limit": 5, "provider": "fmp"}),
(
{
"symbol": "AAPL",
"period": "quarter",
"limit": 5,
"provider": "alpha_vantage",
}
),
],
)
@pytest.mark.integration
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1587,6 +1587,14 @@ def test_equity_market_snapshots(params, obb):
"params",
[
({"symbol": "AAPL", "limit": 5, "provider": "fmp"}),
(
{
"symbol": "AAPL",
"period": "quarter",
"limit": 5,
"provider": "alpha_vantage",
}
),
],
)
@pytest.mark.integration
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Alpha Vantage Provider module."""

from openbb_alpha_vantage.models.equity_historical import AVEquityHistoricalFetcher
from openbb_alpha_vantage.models.historical_eps import AVHistoricalEpsFetcher
from openbb_core.provider.abstract.provider import Provider

alpha_vantage_provider = Provider(
Expand All @@ -16,5 +17,6 @@
credentials=["api_key"],
fetcher_dict={
"EquityHistorical": AVEquityHistoricalFetcher,
"HistoricalEps": AVHistoricalEpsFetcher,
},
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
"""AlphaVantage Historical EPS Model."""

# pylint: disable=unused-argument

import warnings
from datetime import date as dateType
from typing import Any, Dict, List, Literal, Optional, Union

from openbb_core.provider.abstract.fetcher import Fetcher
from openbb_core.provider.standard_models.historical_eps import (
HistoricalEpsData,
HistoricalEpsQueryParams,
)
from openbb_core.provider.utils.descriptions import QUERY_DESCRIPTIONS
from openbb_core.provider.utils.errors import EmptyDataError
from openbb_core.provider.utils.helpers import (
ClientResponse,
ClientSession,
amake_requests,
)
from pydantic import Field, field_validator

_warn = warnings.warn


class AlphaVantageHistoricalEpsQueryParams(HistoricalEpsQueryParams):
"""
AlphaVantage Historical EPS Query Params.

Source: https://www.alphavantage.co/documentation/#earnings
"""

__json_schema_extra__ = {"symbol": ["multiple_items_allowed"]}

period: Literal["annual", "quarter"] = Field(
default="quarter", description=QUERY_DESCRIPTIONS.get("period", "")
)
limit: Optional[int] = Field(
default=None, description=QUERY_DESCRIPTIONS.get("limit", "")
)


class AlphaVantageHistoricalEpsData(HistoricalEpsData):
"""AlphaVantage Historical EPS Data."""

__alias_dict__ = {
"date": "fiscalDateEnding",
"eps_actual": "reportedEPS",
"eps_estimated": "estimatedEPS",
"surprise_percent": "surprisePercentage",
"reported_date": "reportedDate",
}

surprise: Optional[float] = Field(
default=None,
description="Surprise in EPS (Actual - Estimated).",
)
surprise_percent: Optional[Union[float, str]] = Field(
default=None,
description="EPS surprise as a normalized percent.",
json_schema_extra={"x-unit_measurement": "percent", "x-frontend_multiply": 100},
)
reported_date: Optional[dateType] = Field(
default=None,
description="Date of the earnings report.",
)

@field_validator(
"eps_estimated",
"eps_actual",
"surprise",
mode="before",
check_fields=False,
)
@classmethod
def validate_null(cls, v):
"""Clean None returned as a string."""
return None if str(v).strip() == "None" or str(v) == "0" else v

@field_validator("surprise_percent", mode="before", check_fields=False)
@classmethod
def normalize_percent(cls, v):
"""Normalize percent values."""
if isinstance(v, str) and v == "None" or str(v) == "0":
return None
return float(v) / 100


class AVHistoricalEpsFetcher(
Fetcher[AlphaVantageHistoricalEpsQueryParams, List[AlphaVantageHistoricalEpsData]]
):
"""AlphaVantage Historical EPS Fetcher."""

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

@staticmethod
async def aextract_data(
query: AlphaVantageHistoricalEpsQueryParams,
credentials: Optional[Dict[str, str]],
**kwargs: Any,
) -> List[Dict]:
"""Return the raw data from the AlphaVantage endpoint."""

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

BASE_URL = "https://www.alphavantage.co/query?function=EARNINGS&"

# We are allowing multiple symbols to be passed in the query, so we need to handle that.
symbols = query.symbol.split(",")

urls = [f"{BASE_URL}symbol={symbol}&apikey={api_key}" for symbol in symbols]

results = []

# We need to make a custom callback function for this async request.
async def response_callback(response: ClientResponse, _: ClientSession):
"""Response callback function."""
symbol = response.url.query.get("symbol", None)
data = await response.json()
target = (
"annualEarnings" if query.period == "annual" else "quarterlyEarnings"
)
result = []
# If data is returned, append it to the results list.
if data:
result = [
{
"symbol": symbol,
**d,
}
for d in data.get(target, []) # type: ignore
]
if query.limit is not None:
results.extend(result[: query.limit])
else:
results.extend(result)

# If no data is returned, raise a warning and move on to the next symbol.
if not data:
_warn(f"Symbol Error: No data found for {symbol}")

await amake_requests(urls, response_callback, **kwargs) # type: ignore

return results

@staticmethod
def transform_data(
query: AlphaVantageHistoricalEpsQueryParams,
data: List[Dict],
**kwargs: Any,
) -> List[AlphaVantageHistoricalEpsData]:
"""Transform the raw data into the standard model."""
if not data:
raise EmptyDataError("No data found.")
return [AlphaVantageHistoricalEpsData.model_validate(d) for d in data]
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
interactions:
- request:
body: null
headers:
Accept:
- application/json
Accept-Encoding:
- gzip, deflate
Connection:
- keep-alive
method: GET
uri: https://www.alphavantage.co/query?apikey=MOCK_API_KEY&function=EARNINGS&symbol=AAPL
response:
body:
string: !!binary |
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
headers:
Allow:
- GET, HEAD, OPTIONS
CF-Cache-Status:
- DYNAMIC
CF-RAY:
- 85d74473ed9d137e-YVR
Connection:
- keep-alive
Content-Encoding:
- gzip
Content-Type:
- application/json
Cross-Origin-Opener-Policy:
- same-origin
Date:
- Fri, 01 Mar 2024 06:59:58 GMT
Nel:
- '{"report_to":"heroku-nel","max_age":3600,"success_fraction":0.005,"failure_fraction":0.05,"response_headers":["Via"]}'
Referrer-Policy:
- same-origin
Report-To:
- '{"group":"heroku-nel","max_age":3600,"endpoints":[{"url":"https://nel.heroku.com/reports?ts=1709276398&sid=1b10b0ff-8a76-4548-befa-353fc6c6c045&s=HYLkepa3xuJf8ZP93wYuVeE7fHXoMnoAaRfYOuePI1s%3D"}]}'
Reporting-Endpoints:
- heroku-nel=https://nel.heroku.com/reports?ts=1709276398&sid=1b10b0ff-8a76-4548-befa-353fc6c6c045&s=HYLkepa3xuJf8ZP93wYuVeE7fHXoMnoAaRfYOuePI1s%3D
Server:
- cloudflare
Transfer-Encoding:
- chunked
Vary:
- Cookie, Origin
Via:
- 1.1 vegur
X-Content-Type-Options:
- nosniff
X-Frame-Options:
- DENY
status:
code: 200
message: OK
- request:
body: null
headers:
Accept:
- application/json
Accept-Encoding:
- gzip, deflate
Connection:
- keep-alive
method: GET
uri: https://www.alphavantage.co/query?apikey=MOCK_API_KEY&function=EARNINGS&symbol=MSFT
response:
body:
string: !!binary |
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
AAA=
headers:
Allow:
- GET, HEAD, OPTIONS
CF-Cache-Status:
- DYNAMIC
CF-RAY:
- 85d74473e8ad8453-YVR
Connection:
- keep-alive
Content-Encoding:
- gzip
Content-Type:
- application/json
Cross-Origin-Opener-Policy:
- same-origin
Date:
- Fri, 01 Mar 2024 06:59:58 GMT
Nel:
- '{"report_to":"heroku-nel","max_age":3600,"success_fraction":0.005,"failure_fraction":0.05,"response_headers":["Via"]}'
Referrer-Policy:
- same-origin
Report-To:
- '{"group":"heroku-nel","max_age":3600,"endpoints":[{"url":"https://nel.heroku.com/reports?ts=1709276398&sid=1b10b0ff-8a76-4548-befa-353fc6c6c045&s=HYLkepa3xuJf8ZP93wYuVeE7fHXoMnoAaRfYOuePI1s%3D"}]}'
Reporting-Endpoints:
- heroku-nel=https://nel.heroku.com/reports?ts=1709276398&sid=1b10b0ff-8a76-4548-befa-353fc6c6c045&s=HYLkepa3xuJf8ZP93wYuVeE7fHXoMnoAaRfYOuePI1s%3D
Server:
- cloudflare
Transfer-Encoding:
- chunked
Vary:
- Cookie, Origin
Via:
- 1.1 vegur
X-Content-Type-Options:
- nosniff
X-Frame-Options:
- DENY
status:
code: 200
message: OK
version: 1
Loading
Loading