Skip to content

Commit

Permalink
feat: add option to query price data for a specific day (#59)
Browse files Browse the repository at this point in the history
Signed-off-by: bossenti <[email protected]>
  • Loading branch information
bossenti authored Oct 19, 2023
1 parent 639fd64 commit 8c14442
Show file tree
Hide file tree
Showing 10 changed files with 176 additions and 50 deletions.
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

0 comments on commit 8c14442

Please sign in to comment.