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/v4-price-performance: % returns for various horizons, comparable across multiple tickers. #5618

Merged
merged 7 commits into from
Oct 30, 2023
Merged
Show file tree
Hide file tree
Changes from 5 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
35 changes: 32 additions & 3 deletions openbb_platform/extensions/etf/integration/test_etf_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ def headers():

@pytest.mark.parametrize(
"params",
[({})],
[({"query": None, "provider": "fmp"})],
)
@pytest.mark.integration
def test_etf_search(params, headers):
Expand All @@ -36,8 +36,22 @@ def test_etf_search(params, headers):
@pytest.mark.parametrize(
"params",
[
({"symbol": "IOO", "start_date": "2023-01-01", "end_date": "2023-06-06"}),
({"symbol": "MISL", "start_date": "2023-01-01", "end_date": "2023-06-06"}),
(
{
"symbol": "IOO",
"start_date": "2023-01-01",
"end_date": "2023-06-06",
"provider": "yfinance",
}
),
(
{
"symbol": "MISL",
"start_date": "2023-01-01",
"end_date": "2023-06-06",
"provider": "yfinance",
}
),
],
)
@pytest.mark.integration
Expand All @@ -49,3 +63,18 @@ def test_etf_historical(params, headers):
result = requests.get(url, headers=headers, timeout=10)
assert isinstance(result, requests.Response)
assert result.status_code == 200


@pytest.mark.parametrize(
"params",
[({"symbol": "SPY,VOO,QQQ,IWM,IWN,GOVT,JNK", "provider": "fmp"})],
)
@pytest.mark.integration
def test_etf_price_performance(params, headers):
params = {p: v for p, v in params.items() if v}

query_str = get_querystring(params, [])
url = f"http://0.0.0.0:8000/api/v1/etf/price_performance?{query_str}"
result = requests.get(url, headers=headers, timeout=10)
assert isinstance(result, requests.Response)
assert result.status_code == 200
34 changes: 31 additions & 3 deletions openbb_platform/extensions/etf/integration/test_etf_python.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ def obb(pytestconfig): # pylint: disable=inconsistent-return-statements
@pytest.mark.parametrize(
"params",
[
({}),
({"query": None, "provider": "fmp"}),
],
)
@pytest.mark.integration
Expand All @@ -35,8 +35,22 @@ def test_etf_search(params, obb):
@pytest.mark.parametrize(
"params",
[
({"symbol": "IOO", "start_date": "2023-01-01", "end_date": "2023-06-06"}),
({"symbol": "MISL", "start_date": "2023-01-01", "end_date": "2023-06-06"}),
(
{
"symbol": "IOO",
"start_date": "2023-01-01",
"end_date": "2023-06-06",
"provider": "yfinance",
}
),
(
{
"symbol": "MISL",
"start_date": "2023-01-01",
"end_date": "2023-06-06",
"provider": "yfinance",
}
),
],
)
@pytest.mark.integration
Expand All @@ -47,3 +61,17 @@ def test_etf_historical(params, obb):
assert result
assert isinstance(result, OBBject)
assert len(result.results) > 0


@pytest.mark.parametrize(
"params",
[({"symbol": "SPY,VOO,QQQ,IWM,IWN,GOVT,JNK", "provider": "fmp"})],
)
@pytest.mark.integration
def test_etf_price_performance(params, obb):
params = {p: v for p, v in params.items() if v}

result = obb.etf.price_performance(**params)
assert result
assert isinstance(result, OBBject)
assert len(result.results) > 0
11 changes: 11 additions & 0 deletions openbb_platform/extensions/etf/openbb_etf/etf_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,14 @@ def historical(
) -> OBBject[BaseModel]:
"""ETF Historical Market Price."""
return OBBject(results=Query(**locals()).execute())


@router.command(model="PricePerformance")
def price_performance(
cc: CommandContext,
provider_choices: ProviderChoices,
standard_params: StandardParams,
extra_params: ExtraParams,
) -> OBBject[BaseModel]:
"""Price performance as a return, over different periods."""
return OBBject(results=Query(**locals()).execute())
15 changes: 15 additions & 0 deletions openbb_platform/extensions/stocks/integration/test_stocks_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -946,3 +946,18 @@ def test_stocks_info(params, headers):
result = requests.get(url, headers=headers, timeout=10)
assert isinstance(result, requests.Response)
assert result.status_code == 200


@pytest.mark.parametrize(
"params",
[({"symbol": "AAPL,NVDA,QQQ,INTC", "provider": "fmp"})],
)
@pytest.mark.integration
def test_stocks_price_performance(params, headers):
params = {p: v for p, v in params.items() if v}

query_str = get_querystring(params, [])
url = f"http://0.0.0.0:8000/api/v1/stocks/price_performance?{query_str}"
result = requests.get(url, headers=headers, timeout=10)
assert isinstance(result, requests.Response)
assert result.status_code == 200
Original file line number Diff line number Diff line change
Expand Up @@ -894,3 +894,17 @@ def test_stocks_info(params, obb):
assert result
assert isinstance(result, OBBject)
assert len(result.results) > 0


@pytest.mark.parametrize(
"params",
[({"symbol": "AAPL,NVDA,QQQ,INTC", "provider": "fmp"})],
)
@pytest.mark.integration
def test_stocks_price_performance(params, obb):
params = {p: v for p, v in params.items() if v}

result = obb.stocks.price_performance(**params)
assert result
assert isinstance(result, OBBject)
assert len(result.results) > 0
11 changes: 11 additions & 0 deletions openbb_platform/extensions/stocks/openbb_stocks/stocks_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,14 @@ def info(
) -> OBBject[BaseModel]:
"""Stock Info. Get general price and performance metrics of a stock."""
return OBBject(results=Query(**locals()).execute())


@router.command(model="PricePerformance")
def price_performance(
cc: CommandContext,
provider_choices: ProviderChoices,
standard_params: StandardParams,
extra_params: ExtraParams,
) -> OBBject[BaseModel]:
"""Price performance as a return, over different periods."""
return OBBject(results=Query(**locals()).execute())
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,12 @@ def extract_data(
query: CboeStockSearchQueryParams,
credentials: Optional[Dict[str, str]],
**kwargs: Any,
) -> dict:
) -> Dict:
"""Return the raw data from the CBOE endpoint."""

data = {}
symbols = get_cboe_directory().reset_index().replace("nan", None)
target = "name" if not query.is_symbol else "symbol"
symbols = get_cboe_directory().reset_index()
target = "name" if query.is_symbol is False else "symbol"
idx = symbols[target].str.contains(query.query, case=False)
result = symbols[idx].to_dict("records")
data.update({"results": result})
Expand Down
2 changes: 2 additions & 0 deletions openbb_platform/providers/fmp/openbb_fmp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
FMPMajorIndicesConstituentsFetcher,
)
from openbb_fmp.models.major_indices_historical import FMPMajorIndicesHistoricalFetcher
from openbb_fmp.models.price_performance import FMPPricePerformanceFetcher
from openbb_fmp.models.price_target import FMPPriceTargetFetcher
from openbb_fmp.models.price_target_consensus import FMPPriceTargetConsensusFetcher
from openbb_fmp.models.revenue_business_line import FMPRevenueBusinessLineFetcher
Expand Down Expand Up @@ -96,5 +97,6 @@
"DividendCalendar": FMPDividendCalendarFetcher,
"StockQuote": FMPStockQuoteFetcher,
"FinancialRatios": FMPFinancialRatiosFetcher,
"PricePerformance": FMPPricePerformanceFetcher,
},
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
"""FMP Price Performance Model"""


from typing import Any, Dict, List, Optional

from openbb_fmp.utils.helpers import get_data_one
from openbb_provider.abstract.fetcher import Fetcher
from openbb_provider.standard_models.recent_performance import (
RecentPerformanceData,
RecentPerformanceQueryParams,
)
from pydantic import Field


class FMPPricePerformanceQueryParams(RecentPerformanceQueryParams):
"""FMP Price Performance query.

Source: https://site.financialmodelingprep.com/developer/docs/stock-split-calendar-api/
"""


class FMPPricePerformanceData(RecentPerformanceData):
"""FMP Price Performance data."""

symbol: str = Field(description="The ticker symbol.")

__alias_dict__ = {
"one_day": "1D",
"one_week": "5D",
"one_month": "1M",
"three_month": "3M",
"six_month": "6M",
"one_year": "1Y",
"three_year": "3Y",
"five_year": "5Y",
"ten_year": "10Y",
}


class FMPPricePerformanceFetcher(
Fetcher[
FMPPricePerformanceQueryParams,
List[FMPPricePerformanceData],
]
):
"""Transform the query, extract and transform the data from the FMP endpoints."""

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

@staticmethod
def extract_data(
query: FMPPricePerformanceQueryParams,
credentials: Optional[Dict[str, str]],
**kwargs: Any,
) -> Dict:
"""Return the raw data from the FMP endpoint."""
api_key = credentials.get("fmp_api_key") if credentials else ""

url = f"https://financialmodelingprep.com/api/v3/stock-price-change/{query.symbol}?apikey={api_key}"

data = get_data_one(url, **kwargs)
return data if 0 in data else {0: data}

@staticmethod
def transform_data(
data: Dict,
**kwargs: Any,
) -> List[FMPPricePerformanceData]:
return [FMPPricePerformanceData.model_validate(data[i]) for i in data]
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
interactions:
- request:
body: null
headers:
Accept:
- '*/*'
Accept-Encoding:
- gzip, deflate, br
Connection:
- keep-alive
method: GET
uri: https://financialmodelingprep.com/api/v3/stock-price-change/AAPL,SPY,QQQ,MSFT,AMZN,GOOG?apikey=MOCK_API_KEY
response:
body:
string: !!binary |
H4sIAAAAAAAAA32UP28UQQzF+3yK09WJNf47HrqTEGk4klNoQkQBoiSigIII8d3x7qyHzIlQbPO0
6/X7+dkPF7vdr3h2u/33p8fP377uX+32h8Pt2/1lV/F1KAVqs7YpuihXAkWELN86LhoCNau0adw1
gsZuKdoqFkAkx5Yln358CZkFtBTzrHkfGhpglcJZc9FEgCoWzH4WjbBC4cr5HyyLWt2hYZNUHz/9
XGpyacYgzUlD/335LwZ3t/cTguhZlLOPzoBBGw6/nQGDlarJpTNoQNxa+koEEo25Sr7aGVRwKa0+
R+BQauOUuLsFiZKjm0VTBRNGmQAgc2Cx8XUH4EHa2FFedH86nc4CIP4X+Ga+sckcAIKYlWYL3bwD
I8s0/xpxQkke2/ALqKGPeqtNA5Uqo96qEaDJ0FbriBgvOs7D58iOcNWk1L1rNSAnf9H68e7N+zPv
6mOgumVBq1Y3TySrfQURZJ3cl4iqolbKLlYADjH7mtIGoIJZZP05AImSWtoEQGN26p6/GelX55Ji
B+BUgZDH5x0Ac1tS0fB/6T8cP7ybIBg4U04sIZBQaTRiuEJA4FbLOYTQzIpPKYjxMFZLhJ2CeCyA
jbj0G6CARDptwFXkgAznHJhBoUjMRMGQY/3I5xjEDWCtsUVO/CKF65ub6/MjUJjGP1cJCzQrnKes
L0JcLUacF0GgxcmbCDC4NUxYIwdqbb6CMa8wy3MOeCGFUw5QcGmmjo/7IsQVRKaWpDsBcbbF+cXH
P2eCaLUEBgAA
headers:
Access-Control-Allow-Credentials:
- 'true'
Access-Control-Allow-Headers:
- X-Requested-With, content-type, auth-token, Authorization, stripe-signature,
APPS
Access-Control-Allow-Methods:
- GET, POST, OPTIONS
Access-Control-Allow-Origin:
- '*'
Access-Control-Max-Age:
- '3600'
Connection:
- keep-alive
Content-Encoding:
- gzip
Content-Type:
- application/json; charset=utf-8
Date:
- Sat, 28 Oct 2023 05:22:03 GMT
ETag:
- W/"604-kWkPmala6K7vrDTi6htNqwejLFY"
Server:
- nginx/1.18.0 (Ubuntu)
Transfer-Encoding:
- chunked
Vary:
- Accept-Encoding
X-Frame-Options:
- SAMEORIGIN
X-Powered-By:
- Express
status:
code: 200
message: OK
version: 1
10 changes: 10 additions & 0 deletions openbb_platform/providers/fmp/tests/test_fmp_fetchers.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
FMPMajorIndicesConstituentsFetcher,
)
from openbb_fmp.models.major_indices_historical import FMPMajorIndicesHistoricalFetcher
from openbb_fmp.models.price_performance import FMPPricePerformanceFetcher
from openbb_fmp.models.price_target import FMPPriceTargetFetcher
from openbb_fmp.models.price_target_consensus import FMPPriceTargetConsensusFetcher
from openbb_fmp.models.revenue_business_line import FMPRevenueBusinessLineFetcher
Expand Down Expand Up @@ -465,3 +466,12 @@ def test_fmp_etf_search_fetcher(credentials=test_credentials):
fetcher = FMPEtfSearchFetcher()
result = fetcher.test(params, credentials)
assert result is None


@pytest.mark.record_http
def test_fmp_price_performance_fetcher(credentials=test_credentials):
params = {"symbol": "AAPL,SPY,QQQ,MSFT,AMZN,GOOG"}

fetcher = FMPPricePerformanceFetcher()
result = fetcher.test(params, credentials)
assert result is None
Loading