From 1ccb083e6c71560a6ae2fc3f9d93699b56603ad2 Mon Sep 17 00:00:00 2001 From: gnzsnz Date: Wed, 29 May 2024 17:43:31 +0200 Subject: [PATCH] Json output (#4) ## Changes * add to_json function using standard json module * json encoder - json encoder for to_json - json tests * fixes on utils_test * updates on README --- README.md | 66 +++++++++++++++++++++++++++++++++++--- ib_fundamental/__init__.py | 4 +-- ib_fundamental/utils.py | 27 ++++++++++++++-- tests/utils_test.py | 47 +++++++++++++++++++++++++++ 4 files changed, 135 insertions(+), 9 deletions(-) create mode 100644 tests/utils_test.py diff --git a/README.md b/README.md index 7861772..4d9a437 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ exchange_code NASD exchange NASDAQ irs 942404110 -# Income statement, aapl.income_quarter will pull the quarterly report +# Annual income statement, while aapl.income_quarter will pull the quarterly report aapl.income_annual map_item 2018-09-29 2019-09-28 2020-09-26 2021-09-25 2022-09-24 2023-09-30 statement_type @@ -73,7 +73,7 @@ line_id 1770 Diluted Normalized EPS 3.05148 2.97145 3.27535 5.61402 6.1132 6.13405 Income Statement # get earnings per share, appl.eps_ttm will pull trailing twelve months eps -aapl.eps_q +aapl.eps_ttm report_type period eps as_of_date @@ -106,6 +106,14 @@ as_of_date 2023-12-31 TTM 12M 6.460 2024-03-31 TTM 12M 6.460 +# get data in json format + +from ib_fundamental.utils import to_json + +# CompanyFinancials.data property contains all data in dataclass format +to_json(aapl.data.eps_ttm) +'[{"as_of_date": "2024-03-31T00:00:00", "report_type": "TTM", "period": "12M", "eps": 6.46}, {"as_of_date": "2023-12-31T00:00:00", "report_type": "TTM", "period": "12M", "eps": 6.46}, ...' + # and much more ``` @@ -156,8 +164,9 @@ This is the full list of methods of `CompanyFinancials` class - revenue_q - revenue_tt -You can use `FundamentalData` class that will return company -fundamental information in `dataclass` format +You can use `FundamentalData` class that will return company fundamental +information in `dataclass` format. Each instance of `CompanyFinancials` +contains an instance of `FundamentalData` in its `data` property. ```python from ib_fundamental.fundamental import FundamentalData @@ -186,8 +195,57 @@ from ib_fundamental.fundamental import FundamentalData 'revenue_q', 'revenue_ttm'] +# get quarterly revenue +aapl.data.revenue_q + +[Revenue(as_of_date=datetime.datetime(2024, 3, 31, 0, 0), report_type='R', period='3M', revenue=90753000000.0), + Revenue(as_of_date=datetime.datetime(2023, 12, 31, 0, 0), report_type='R', period='3M', revenue=119575000000.0), + Revenue(as_of_date=datetime.datetime(2023, 9, 30, 0, 0), report_type='R', period='3M', revenue=89498000000.0), + Revenue(as_of_date=datetime.datetime(2023, 6, 30, 0, 0), report_type='R', period='3M', revenue=81797000000.0), + Revenue(as_of_date=datetime.datetime(2023, 3, 31, 0, 0), report_type='R', period='3M', revenue=94836000000.0), + Revenue(as_of_date=datetime.datetime(2022, 12, 31, 0, 0), report_type='R', period='3M', revenue=117154000000.0), + Revenue(as_of_date=datetime.datetime(2022, 9, 30, 0, 0), report_type='R', period='3M', revenue=90146000000.0), + Revenue(as_of_date=datetime.datetime(2022, 6, 30, 0, 0), report_type='R', period='3M', revenue=82959000000.0), + Revenue(as_of_date=datetime.datetime(2022, 3, 31, 0, 0), report_type='R', period='3M', revenue=97278000000.0), + Revenue(as_of_date=datetime.datetime(2021, 12, 31, 0, 0), report_type='R', period='3M', revenue=123945000000.0), + Revenue(as_of_date=datetime.datetime(2021, 9, 30, 0, 0), report_type='R', period='3M', revenue=83360000000.0), + Revenue(as_of_date=datetime.datetime(2021, 6, 30, 0, 0), report_type='R', period='3M', revenue=81434000000.0), + Revenue(as_of_date=datetime.datetime(2021, 3, 31, 0, 0), report_type='R', period='3M', revenue=89584000000.0), + Revenue(as_of_date=datetime.datetime(2020, 12, 31, 0, 0), report_type='R', period='3M', revenue=111439000000.0), + Revenue(as_of_date=datetime.datetime(2020, 9, 30, 0, 0), report_type='R', period='3M', revenue=64698000000.0), + Revenue(as_of_date=datetime.datetime(2020, 6, 30, 0, 0), report_type='R', period='3M', revenue=59685000000.0), + Revenue(as_of_date=datetime.datetime(2020, 3, 31, 0, 0), report_type='R', period='3M', revenue=58313000000.0), + Revenue(as_of_date=datetime.datetime(2019, 12, 31, 0, 0), report_type='R', period='3M', revenue=91819000000.0), + Revenue(as_of_date=datetime.datetime(2019, 9, 30, 0, 0), report_type='R', period='3M', revenue=64040000000.0), + Revenue(as_of_date=datetime.datetime(2019, 6, 30, 0, 0), report_type='R', period='3M', revenue=53809000000.0), + Revenue(as_of_date=datetime.datetime(2019, 3, 31, 0, 0), report_type='R', period='3M', revenue=58015000000.0), + Revenue(as_of_date=datetime.datetime(2018, 12, 31, 0, 0), report_type='R', period='3M', revenue=84310000000.0), + Revenue(as_of_date=datetime.datetime(2018, 9, 30, 0, 0), report_type='R', period='3M', revenue=62900000000.0), + Revenue(as_of_date=datetime.datetime(2018, 6, 30, 0, 0), report_type='R', period='3M', revenue=53265000000.0), + Revenue(as_of_date=datetime.datetime(2018, 3, 31, 0, 0), report_type='R', period='3M', revenue=61137000000.0), + Revenue(as_of_date=datetime.datetime(2017, 12, 31, 0, 0), report_type='R', period='3M', revenue=88293000000.0), + Revenue(as_of_date=datetime.datetime(2017, 9, 30, 0, 0), report_type='R', period='3M', revenue=52579000000.0), + Revenue(as_of_date=datetime.datetime(2017, 6, 30, 0, 0), report_type='R', period='3M', revenue=45408000000.0)] + ```` +## Contributing + +If you find a bug please open an issue, pull requests are always welcome. + +To develop with `ib_fundamental` please follow these steps + +```bash +git clone https://github.com/quantbelt/ib_fundamental.git +cd ib_fundamental +# install development dependencies +pip install .[dev] +# do your things +# run linters and code quality checks +pre-commit +# run tests with tox, requires pypy310,py{310,311,312} +tox +``` [reqFundamental]: https://ib-api-reloaded.github.io/ib_async/api.html#ib_async.ib.IB.reqFundamentalData [fin_ratios]: http://web.archive.org/web/20200725010343/https://interactivebrokers.github.io/tws-api/fundamental_ratios_tags.html diff --git a/ib_fundamental/__init__.py b/ib_fundamental/__init__.py index 78eea78..bb1f7a3 100644 --- a/ib_fundamental/__init__.py +++ b/ib_fundamental/__init__.py @@ -17,7 +17,7 @@ # under the License. """IB Fundamental data""" -__all__ = ["CompanyFinancials", "objects", "utils"] +__all__ = ["CompanyFinancials", "FundamentalData", "objects", "utils"] __author__ = "Gonzalo Sáenz" __copyright__ = "Copyright 2024 Gonzalo Sáenz" __credits__ = ["Gonzalo Sáenz"] @@ -27,4 +27,4 @@ from . import objects, utils -from .fundamental import CompanyFinancials +from .fundamental import CompanyFinancials, FundamentalData diff --git a/ib_fundamental/utils.py b/ib_fundamental/utils.py index 3c7835d..1894e79 100644 --- a/ib_fundamental/utils.py +++ b/ib_fundamental/utils.py @@ -17,14 +17,16 @@ # under the License. """ -Created on Thu May 9 18:21:58 2021 - -@author: gnzsnz +ib_fundamental utility functions """ +import dataclasses +import datetime +import json import re from typing import Any, Optional +from ib_async import FundamentalRatios from pandas import DataFrame re_pattern = re.compile(r"(?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])") @@ -48,3 +50,22 @@ def camel_to_snake(camel: str) -> str: """Convert CamelCase to snake_case""" # https://stackoverflow.com/questions/1175208/elegant-python-function-to-convert-camelcase-to-snake-case return re_pattern.sub("_", camel).lower() + + +def to_json(obj: Any, **kwargs: Any) -> str: + """Convert FundamentalData attributes to JSON""" + + class EnhancedJSONEncoder(json.JSONEncoder): + """JSON encoder for dataclasses and datetime""" + + def default(self, o): + if dataclasses.is_dataclass(o): + return dataclasses.asdict(o) + elif isinstance(o, (datetime.date, datetime.datetime)): + return o.isoformat() + elif isinstance(o, FundamentalRatios): + return vars(o) + + return super().default(o) + + return json.dumps(obj, cls=EnhancedJSONEncoder, **kwargs) diff --git a/tests/utils_test.py b/tests/utils_test.py new file mode 100644 index 0000000..89de6f4 --- /dev/null +++ b/tests/utils_test.py @@ -0,0 +1,47 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +"""Tests for ib_fundamental utils""" + +import pytest + +from ib_fundamental import FundamentalData +from ib_fundamental.utils import to_json + +fund_data_methods = ( + _m + for _m in dir(FundamentalData) + if (_m[:1] != "_" and _m not in ("client", "parser")) +) + + +@pytest.fixture(params=fund_data_methods) +def fund_method(fundamental_data, request): + """JSON fixture, send all FundamentalData methods as json""" + _m = request.param + yield fundamental_data, _m + + +class TestUtils: + """Tests for utils module""" + + def test_json_inc_annual(self, fund_method): + """test json""" + _fund_data, _method = fund_method + _json = to_json(getattr(_fund_data, _method)) + # assert + assert isinstance(_json, str)