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

feat: add option to query price data for a specific day #59

Merged
merged 11 commits into from
Oct 19, 2023
39 changes: 0 additions & 39 deletions CHANGELOG.md

This file was deleted.

12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ DE0007164600

As a final step, the asset provides some recent price-related data:
```python
print(asset.price_data.as_json())
print(asset.get_latest_price_data().as_json())
```
```bash
{
Expand All @@ -120,6 +120,16 @@ print(asset.price_data.as_json())
}
```

In addition, you directly access the individual values, e.g., price value `last`:
```python
asset.get_latest_price_data().last
```
```bash
126.16
```

<br>

> [!NOTE]
> Price data are currently only supported for funds and stocks.
> Feel free to send me a feature request if you'd like to see this feature
Expand Down
2 changes: 1 addition & 1 deletion justfile
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,4 @@ pretty: autoflake ruff black

# run all unit tests
unit-tests:
poetry run pytest --cov=vistafetch --cov-fail-under=90 --cov-report term-missing:skip-covered --no-cov-on-fail tests/
export TZ="UTC"; poetry run pytest --cov=vistafetch --cov-fail-under=90 --cov-report term-missing:skip-covered --no-cov-on-fail tests/
47 changes: 46 additions & 1 deletion tests/model/asset/test_financial_asset.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,8 @@ def test_price_data(self, session_mock: MagicMock):
"wkn": "TEST00",
}

result = Fund.model_validate(test_input).price_data
fund = Fund.model_validate(test_input)
result = fund.price_data

self.assertTrue(isinstance(result, PriceData))
self.assertEqual(EXPECTED_PRICE_DATA_CURRENCY, result.currency_symbol)
Expand All @@ -97,3 +98,47 @@ def test_price_data(self, session_mock: MagicMock):
),
result.datetime_open,
)
self.assertIsNotNone(fund.market.id_notation)

@patch(
"vistafetch.model.asset.financial_asset.api_session.get",
side_effect=mock_api_call,
)
def test_price_data_get_latest_price_data(self, session_mock: MagicMock):
test_input = {
"displayType": "fund",
"entityType": "FUND",
"isin": "DE00000000",
"name": "demon",
"symbol": "TEST",
"tinyName": "demon",
"wkn": "TEST00",
}

result = Fund.model_validate(test_input).get_latest_price_data()
self.assertEqual(EXPECTED_PRICE_DATA_CURRENCY, result.currency_symbol)
self.assertEqual(EXPECTED_PRICE_DATA_LAST, result.last)

@patch(
"vistafetch.model.asset.financial_asset.api_session.get",
side_effect=mock_api_call,
)
def test_price_data_get_one_day_price_data(self, session_mock: MagicMock):
test_input = {
"displayType": "fund",
"entityType": "FUND",
"isin": "DE00000000",
"name": "demon",
"symbol": "TEST",
"tinyName": "demon",
"wkn": "TEST00",
}

result = Fund.model_validate(test_input).get_day_price_data(
day=datetime.today()
)
self.assertTrue(isinstance(result, PriceData))
self.assertEqual(
datetime(year=2023, month=10, day=13, hour=12), result.datetime_high
)
self.assertEqual(EXPECTED_PRICE_DATA_LAST, result.last)
24 changes: 21 additions & 3 deletions tests/test_utils/requests_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,31 @@
from requests import HTTPError

EXPECTED_PRICE_DATA_CURRENCY = "EUR"
EXPECTED_PRICE_DATA_HIGH = 7.135
EXPECTED_PRICE_DATA_LAST = 7.09
EXPECTED_PRICE_DATA_LOW = 7.02
EXPECTED_PRICE_DATA_OPEN = 7.045
EXPECTED_PRICE_DATA_TS = 1697198400
EXPECTED_PRICE_DATA = {
"isoCurrency": EXPECTED_PRICE_DATA_CURRENCY,
"open": 7.045,
"low": 7.02,
"open": EXPECTED_PRICE_DATA_OPEN,
"low": EXPECTED_PRICE_DATA_LOW,
"datetimeOpen": "2023-08-25T07:00:21.999+00:00",
"last": EXPECTED_PRICE_DATA_LAST,
"addendum": "",
"datetimeHigh": "2023-08-25T10:11:52.000+00:00",
"datetimeLow": "2023-08-25T15:07:10.000+00:00",
"high": 7.135,
"high": EXPECTED_PRICE_DATA_HIGH,
"datetimeLast": "2023-08-25T15:35:10.000+00:00",
"market": {"idNotation": 163500},
}
EXPECTED_PRICE_DATA_DAY = {
"isoCurrency": EXPECTED_PRICE_DATA_CURRENCY,
"datetimeLast": [EXPECTED_PRICE_DATA_TS],
"first": [EXPECTED_PRICE_DATA_OPEN],
"last": [EXPECTED_PRICE_DATA_LAST],
"high": [EXPECTED_PRICE_DATA_HIGH],
"low": [EXPECTED_PRICE_DATA_LOW],
}


Expand All @@ -37,6 +50,11 @@ def json(self):
json_data={"quote": EXPECTED_PRICE_DATA},
status_code=200,
)
elif "eod_history" in args[0]:
return MockResponse(
json_data=EXPECTED_PRICE_DATA_DAY,
status_code=200,
)
elif "searchValue" in args[0]:
return MockResponse(
json_data={
Expand Down
2 changes: 2 additions & 0 deletions vistafetch/model/asset/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@

from vistafetch.model.asset.financial_data import PriceData
from vistafetch.model.asset.bond import Bond
from vistafetch.model.asset.derivative import Derivative
from vistafetch.model.asset.fund import Fund
from vistafetch.model.asset.index import Index
from vistafetch.model.asset.metal import PreciousMetal
from vistafetch.model.asset.stock import Stock

__all__ = [
"Bond",
"Derivative",
"Fund",
"Index",
"PreciousMetal",
Expand Down
25 changes: 25 additions & 0 deletions vistafetch/model/asset/derivative.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"""Model a derivative."""
import logging
from typing import Literal

from vistafetch.model.asset.financial_asset import FinancialAsset
from vistafetch.model.asset.financial_asset_type import FinancialAssetType

__all__ = [
"Derivative",
]

logger = logging.getLogger(__name__)


class Derivative(FinancialAsset):
"""Models a derivative within the scope of this library.

An exchange-traded derivative is a standardized
financial contract that is listed and traded on a regulated exchange.

"""

_type = FinancialAssetType.DERIVATIVE

entity_type: Literal[_type.value] # type: ignore
67 changes: 64 additions & 3 deletions vistafetch/model/asset/financial_asset.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
"""Model a financial asset in the context of this library."""
import datetime
import logging
from abc import ABC
from functools import cached_property
from typing import Literal
from typing import Literal, Optional

from pydantic import Field
from requests import HTTPError

from vistafetch.constants import ONVISTA_API_BASE_URL
Expand All @@ -15,6 +18,20 @@
"FinancialAsset",
]

logger = logging.getLogger(__name__)


class FinancialAssetMarket(VistaEntity):
"""Market-related information of a financial asset.

Attributes
----------
id_notation: identifier of the market identifier

"""

id_notation: int


class FinancialAsset(VistaEntity, ABC):
"""General description of a financial asset in the context of this library.
Expand Down Expand Up @@ -54,8 +71,35 @@ class FinancialAsset(VistaEntity, ABC):
name: str
tiny_name: str
wkn: str
market: Optional[FinancialAssetMarket] = Field(default=None)

def __query_price_data(self) -> PriceData:
def __query_day_price_data(self, day: datetime.date) -> PriceData:
if self._type == FinancialAssetType.UNKNOWN:
raise NotImplementedError(
"`price_data` is called directly on the "
"abstract class `Financial Asset`. "
"Please use a valid financial asset class."
)

response = api_session.get(
f"{ONVISTA_API_BASE_URL}instruments/{self.entity_type}/ISIN:{self.isin}/eod_history?idNotation={self.market.id_notation}&range=D1&startDate={day.year}-{day.month}-{day.day}" # type: ignore
)

price_raw = response.json()

return PriceData.model_construct(
currency_symbol=price_raw["isoCurrency"],
datetime_high=datetime.datetime.fromtimestamp(price_raw["datetimeLast"][0]),
datetime_last=datetime.datetime.fromtimestamp(price_raw["datetimeLast"][0]),
datetime_low=datetime.datetime.fromtimestamp(price_raw["datetimeLast"][0]),
datetime_open=datetime.datetime.fromtimestamp(price_raw["datetimeLast"][0]),
high=price_raw["high"][0],
last=price_raw["last"][0],
low=price_raw["low"][0],
open=price_raw["first"][0],
)

def __query_latest_price_data(self) -> PriceData:
if self._type == FinancialAssetType.UNKNOWN:
raise NotImplementedError(
"`price_data` is called directly on the "
Expand All @@ -76,9 +120,26 @@ def __query_price_data(self) -> PriceData:
f"API response does not contain expected data: {response_dict}"
)

self.market = FinancialAssetMarket.model_validate(
response_dict["quote"]["market"]
)

return PriceData.model_validate(response_dict["quote"])

@cached_property
def price_data(self) -> PriceData:
"""Get the price data available for this financial asset."""
return self.__query_price_data()
return self.__query_latest_price_data()

def get_day_price_data(self, day: datetime.date) -> PriceData:
"""Get the price data for this financial asset for a specific day."""
# check if market information are available
# if not we need to make an additional API call
if self.market is None:
self.__query_latest_price_data()

return self.__query_day_price_data(day)

def get_latest_price_data(self) -> PriceData:
"""Get the latest available price data for this financial asset."""
return self.price_data
4 changes: 4 additions & 0 deletions vistafetch/model/asset/financial_asset_type.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,20 @@ class FinancialAssetType(Enum):
----------
BOND: A debt security that represents a loan made by
an investor to a borrower.
DERIVATIVE: A derivative is a standardized financial contract that
is listed and traded on a regulated exchange.
FUND: An investment fund, e.g., mutual fund, ETF, etc.
INDEX: A basket of securities representing a particular market or
a segment of it.
METAL: A precious metal traded via an exchange.
STOCK: Share of a corporation or company.
UNKNOWN: Unknown type of financial asset.
Should only be used for abstract modeling.

"""

BOND = "BOND"
DERIVATIVE = "DERIVATIVE"
FUND = "FUND"
INDEX = "INDEX"
METAL = "PRECIOUS_METAL"
Expand Down
4 changes: 2 additions & 2 deletions vistafetch/model/search_result.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from rich.console import Console
from rich.table import Table

from vistafetch.model.asset import Bond, Fund, Index, PreciousMetal, Stock
from vistafetch.model.asset import Bond, Derivative, Fund, Index, PreciousMetal, Stock
from vistafetch.model.asset.financial_asset import FinancialAsset
from vistafetch.model.base import VistaEntity

Expand All @@ -29,7 +29,7 @@ class SearchResult(VistaEntity):
"""

expires: datetime
assets: List[Union[Bond, Fund, Index, PreciousMetal, Stock]] = Field(
assets: List[Union[Bond, Derivative, Fund, Index, PreciousMetal, Stock]] = Field(
alias="list", discriminator="entity_type"
)
search_value: str
Expand Down
Loading