kensho-kfinance 2.2.5__py3-none-any.whl → 2.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of kensho-kfinance might be problematic. Click here for more details.
- {kensho_kfinance-2.2.5.dist-info → kensho_kfinance-2.4.0.dist-info}/METADATA +40 -7
- {kensho_kfinance-2.2.5.dist-info → kensho_kfinance-2.4.0.dist-info}/RECORD +23 -16
- kfinance/CHANGELOG.md +9 -0
- kfinance/fetch.py +10 -0
- kfinance/kfinance.py +227 -11
- kfinance/mcp.py +101 -0
- kfinance/pydantic_models.py +8 -0
- kfinance/tests/test_example_notebook.py +4 -2
- kfinance/tests/test_fetch.py +12 -0
- kfinance/tests/test_mcp.py +16 -0
- kfinance/tests/test_objects.py +165 -2
- kfinance/tests/test_tools.py +217 -1
- kfinance/tool_calling/__init__.py +8 -0
- kfinance/tool_calling/get_earnings.py +30 -0
- kfinance/tool_calling/get_latest_earnings.py +27 -0
- kfinance/tool_calling/get_next_earnings.py +27 -0
- kfinance/tool_calling/get_transcript.py +23 -0
- kfinance/tool_calling/prompts.py +16 -0
- kfinance/version.py +2 -2
- {kensho_kfinance-2.2.5.dist-info → kensho_kfinance-2.4.0.dist-info}/WHEEL +0 -0
- {kensho_kfinance-2.2.5.dist-info → kensho_kfinance-2.4.0.dist-info}/licenses/AUTHORS.md +0 -0
- {kensho_kfinance-2.2.5.dist-info → kensho_kfinance-2.4.0.dist-info}/licenses/LICENSE +0 -0
- {kensho_kfinance-2.2.5.dist-info → kensho_kfinance-2.4.0.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: kensho-kfinance
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.4.0
|
|
4
4
|
Summary: Python CLI for kFinance
|
|
5
5
|
Author-email: Luke Brown <luke.brown@kensho.com>, Michelle Keoy <michelle.keoy@kensho.com>, Keith Page <keith.page@kensho.com>, Matthew Rosen <matthew.rosen@kensho.com>, Nick Roshdieh <nick.roshdieh@kensho.com>
|
|
6
6
|
Project-URL: source, https://github.com/kensho-technologies/kfinance
|
|
@@ -13,8 +13,10 @@ Description-Content-Type: text/markdown
|
|
|
13
13
|
License-File: LICENSE
|
|
14
14
|
License-File: AUTHORS.md
|
|
15
15
|
Requires-Dist: cachetools<6,>=5.5
|
|
16
|
+
Requires-Dist: click<=9,>=8.2.1
|
|
17
|
+
Requires-Dist: fastmcp<3,>=2
|
|
16
18
|
Requires-Dist: langchain-core>=0.3.15
|
|
17
|
-
Requires-Dist: langchain-google-genai<3,>=2.1.
|
|
19
|
+
Requires-Dist: langchain-google-genai<3,>=2.1.5
|
|
18
20
|
Requires-Dist: numpy>=1.22.4
|
|
19
21
|
Requires-Dist: pandas>=2.0.0
|
|
20
22
|
Requires-Dist: pillow>=10
|
|
@@ -29,7 +31,8 @@ Requires-Dist: urllib3>=1.21.1
|
|
|
29
31
|
Provides-Extra: dev
|
|
30
32
|
Requires-Dist: coverage<8,>=7.6.10; extra == "dev"
|
|
31
33
|
Requires-Dist: ipykernel<7,>=6.29; extra == "dev"
|
|
32
|
-
Requires-Dist:
|
|
34
|
+
Requires-Dist: langchain-anthropic<1,>=0.3.10; extra == "dev"
|
|
35
|
+
Requires-Dist: mypy<2,>=1.16.0; extra == "dev"
|
|
33
36
|
Requires-Dist: nbconvert<8,>=7.16; extra == "dev"
|
|
34
37
|
Requires-Dist: nbformat<6,>5.10; extra == "dev"
|
|
35
38
|
Requires-Dist: nbqa<2,>1.9; extra == "dev"
|
|
@@ -51,7 +54,7 @@ Any questions or suggestions can be sent to the [kFinance Maintainers](kfinance-
|
|
|
51
54
|
|
|
52
55
|
# Setup
|
|
53
56
|
|
|
54
|
-
You can install kFinance on [PyPI](https://pypi.org/project/kensho-kfinance/) via
|
|
57
|
+
You can install kFinance on [PyPI](https://pypi.org/project/kensho-kfinance/) via
|
|
55
58
|
|
|
56
59
|
`pip install kensho-kfinance`
|
|
57
60
|
|
|
@@ -61,11 +64,41 @@ To receive access, please email [S&P Global Market Intelligence](market.intellig
|
|
|
61
64
|
|
|
62
65
|
Once access is obtained, get started using the [Authentication Guide](https://docs.kensho.com/llmreadyapi/kf-authentication) and [Usage Guide](https://docs.kensho.com/llmreadyapi/usage).
|
|
63
66
|
|
|
64
|
-
|
|
67
|
+
To get started, we provide some notebooks:
|
|
68
|
+
|
|
69
|
+
- The [LLM-ready API Basic Usage](example_notebooks%2Fbasic_usage.ipynb) notebook demonstrates how
|
|
70
|
+
fetch data with the kFinance client.
|
|
71
|
+
- The [tool_calling notebooks](example_notebooks%2Ftool_calling) show how the kFinance library can
|
|
72
|
+
be used for tool calling. We provide notebooks for OpenAI (GPT), Anthropic (Claude), and Google
|
|
73
|
+
(Gemini). Each of these integrations comes in a langchain version, which uses langchain as a
|
|
74
|
+
wrapper to simplify the integration, and as a lower level non-langchain version.
|
|
75
|
+
|
|
76
|
+
We also provide an [interactive notebook](example_notebooks/basic_usage.ipynb) that demonstrates some usage examples.
|
|
77
|
+
|
|
78
|
+
# MCP (Model Context Protocol)
|
|
79
|
+
|
|
80
|
+
To run the kFinance MCP server use:
|
|
81
|
+
|
|
82
|
+
`python -m kfinance.mcp`
|
|
83
|
+
|
|
84
|
+
This function initializes and starts an MCP server that exposes the kFinance tools. The server supports multiple authentication methods and transport protocols to accommodate different deployment scenarios.
|
|
85
|
+
|
|
86
|
+
The server's full signature is as follows:
|
|
87
|
+
|
|
88
|
+
`kfinance.mcp [--stdio,-s]/[--sse, ] --refresh-token <refresh-token> --client-id <client-id> --private-key <private-key>`
|
|
89
|
+
|
|
90
|
+
Authentication Methods (in order of precedence):
|
|
91
|
+
1. Refresh Token: Uses an existing refresh token for authentication. The `--refresh-token <refresh-token>` argument must be provided.
|
|
92
|
+
2. Key Pair: Uses client ID and private key for authentication. Both the `--client-id <client-id>` and `--private-key <private-key>` arguments must be provided.
|
|
93
|
+
3. Browser: Falls back to browser-based authentication flow. This occurs if no auth arguments are provided.
|
|
94
|
+
|
|
95
|
+
Transport Layers:
|
|
96
|
+
- stdio can be set by passing either `--stdio` or `-s`
|
|
97
|
+
- sse can be set by passing `--sse` or no other transport related flag
|
|
65
98
|
|
|
66
99
|
# Versioning
|
|
67
|
-
The kFinance uses semantic versioning (major, minor, patch).
|
|
68
|
-
To bump the version, add a new entry in [CHANGELOG.md](kfinance%2FCHANGELOG.md).
|
|
100
|
+
The kFinance uses semantic versioning (major, minor, patch).
|
|
101
|
+
To bump the version, add a new entry in [CHANGELOG.md](kfinance%2FCHANGELOG.md).
|
|
69
102
|
This will generate a new version of the library as part of the release process.
|
|
70
103
|
|
|
71
104
|
# License
|
|
@@ -1,31 +1,34 @@
|
|
|
1
|
-
kensho_kfinance-2.
|
|
2
|
-
kensho_kfinance-2.
|
|
3
|
-
kfinance/CHANGELOG.md,sha256=
|
|
1
|
+
kensho_kfinance-2.4.0.dist-info/licenses/AUTHORS.md,sha256=0h9ClbI0pu1oKj1M28ROUsaxrbZg-6ukQGl6X4y9noI,68
|
|
2
|
+
kensho_kfinance-2.4.0.dist-info/licenses/LICENSE,sha256=bsY4blvSgq6o0FMQ3RXa2NCgco--nHCCchLXzxr6kms,83
|
|
3
|
+
kfinance/CHANGELOG.md,sha256=snsLVUBbhNjIau_BsoWKdxxCAPbPHM_rUQ3pNsseF_k,1446
|
|
4
4
|
kfinance/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
5
|
kfinance/batch_request_handling.py,sha256=p6p_G4_BL06GgeKlh7P1k9CUqOMahWCLEw1NoBwbLvU,5698
|
|
6
6
|
kfinance/constants.py,sha256=UuFzqL253-2tRQfma785K9tfaZGv-o821tO2tVLwc5Q,48813
|
|
7
|
-
kfinance/fetch.py,sha256=
|
|
8
|
-
kfinance/kfinance.py,sha256=
|
|
7
|
+
kfinance/fetch.py,sha256=yaCih8PAkOhVHb3tvmBW0x2w4QmXJiyUATu6Yx-xzP4,23851
|
|
8
|
+
kfinance/kfinance.py,sha256=9lcarUW4fLHJA_U7_1ihCMbNJWs0AztvKf_XEd0qupk,59889
|
|
9
|
+
kfinance/mcp.py,sha256=57OoTrxo5x_v7u1s3WFQNG0LySXyLEGc6CKSVpJLJVw,3717
|
|
9
10
|
kfinance/meta_classes.py,sha256=3V0nSXDDoake5o7kXnrqXuqNIiwI75KR4IYxFqSPhTE,20736
|
|
10
11
|
kfinance/prompt.py,sha256=PtVB8c_FcSlVdyGgByAnIFGzuUuBaEjciCqnBJl1hSQ,25133
|
|
11
12
|
kfinance/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
-
kfinance/pydantic_models.py,sha256=
|
|
13
|
+
kfinance/pydantic_models.py,sha256=avpbPqwrAyLqsCbrmFpK_B8_fj1nPlBHrnPxRcBaSkE,774
|
|
13
14
|
kfinance/server_thread.py,sha256=jUnt1YGoYDkqqz1MbCwd44zJs1T_Z2BCgvj75bdtLgA,2574
|
|
14
|
-
kfinance/version.py,sha256=
|
|
15
|
+
kfinance/version.py,sha256=KyHVhKcn4Ob8_JP09Buz8x5uB4b7RJZLl3FBlZ8sWSY,511
|
|
15
16
|
kfinance/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
16
17
|
kfinance/tests/conftest.py,sha256=voB-w8P_6L3Nel3rdgylXKe5WWaS1q7nCFt1O04uqoY,948
|
|
17
18
|
kfinance/tests/test_batch_requests.py,sha256=uXJF2IcRdyBm5SthwIUHMKtkGZ21MY84pg_k1JeSNOY,11430
|
|
18
19
|
kfinance/tests/test_client.py,sha256=O7icZCSDhlQ9WGhzoXlpiSvbuA-mQNJHBYVsilyP_dE,2209
|
|
19
|
-
kfinance/tests/test_example_notebook.py,sha256=
|
|
20
|
-
kfinance/tests/test_fetch.py,sha256=
|
|
20
|
+
kfinance/tests/test_example_notebook.py,sha256=XHwDKw2avyMonTmi3snCcFWNfZhEJOkpBGOZNrMLrhk,6470
|
|
21
|
+
kfinance/tests/test_fetch.py,sha256=nfnz_ZxE-W3KMzpDaRClX55fQJrRjwLTha-rHTallmE,16713
|
|
21
22
|
kfinance/tests/test_group_objects.py,sha256=SoMEZmkG4RYdgWOAwxLHHtzIQho92KM01YbQXPUg578,1689
|
|
22
|
-
kfinance/tests/
|
|
23
|
-
kfinance/tests/
|
|
23
|
+
kfinance/tests/test_mcp.py,sha256=-pmeZ0PDp8GmXWcrWmaNNqMe7OVZONZLrjA0Y4b82os,594
|
|
24
|
+
kfinance/tests/test_objects.py,sha256=0nDmCFrVcfI8VBo1Ph3YqXNo3uPLsSUiQkjEEHsax1M,30416
|
|
25
|
+
kfinance/tests/test_tools.py,sha256=EWWWyPlGGES8Cn43_VaDAT7Vdp1nlIETE5dtOrP163o,24447
|
|
24
26
|
kfinance/tool_calling/README.md,sha256=omJq7Us6r4U45QB7hRpLjRJ5BMalCkZkh4uXBjTbJXc,2022
|
|
25
|
-
kfinance/tool_calling/__init__.py,sha256=
|
|
27
|
+
kfinance/tool_calling/__init__.py,sha256=V5BVcJkLT1zC0QKZIQjLb-cZiQYV4T9Egj64uH807WE,2215
|
|
26
28
|
kfinance/tool_calling/get_business_relationship_from_identifier.py,sha256=CipXvyqEjPm6BXYP0CA9Kp1BIyiIEm7abp85x1zXRV4,1472
|
|
27
29
|
kfinance/tool_calling/get_capitalization_from_identifier.py,sha256=TdWdJDeI-jSL-1YfhnnwIA9D1SXobidvoHrjK42QmqQ,1521
|
|
28
30
|
kfinance/tool_calling/get_cusip_from_ticker.py,sha256=houhGCYXoSzaaTtCvOBf3pPsYiSbcV1Ej5nAyGuMWcU,644
|
|
31
|
+
kfinance/tool_calling/get_earnings.py,sha256=7xavYvqwq4fO0vpks25eDM87bCYwZxEQSwgT0I8jAok,1165
|
|
29
32
|
kfinance/tool_calling/get_earnings_call_datetimes_from_identifier.py,sha256=Zp7gHP-z2ePu484wWx4xw89hl0z7NLcG3qliJT-6rUo,782
|
|
30
33
|
kfinance/tool_calling/get_financial_line_item_from_identifier.py,sha256=bMK-GUMz_wmJ-7iFS72sM8AvvUvR83JYRWzYtr9Fofw,2153
|
|
31
34
|
kfinance/tool_calling/get_financial_statement_from_identifier.py,sha256=9ouzC43skA78uI3dh3bvtM86db5RGxuWQAAJ24y-kBM,1906
|
|
@@ -33,12 +36,16 @@ kfinance/tool_calling/get_history_metadata_from_identifier.py,sha256=kKIInfRC1Pl
|
|
|
33
36
|
kfinance/tool_calling/get_info_from_identifier.py,sha256=PRhSYpYs_iUcIVFGYCMN_7QJyhewua7c7pqngPIp-qg,766
|
|
34
37
|
kfinance/tool_calling/get_isin_from_ticker.py,sha256=2fJBcA-rNGbVOQmQ7qJEYxqejQwJ6nyWOBSFlzxG7dY,638
|
|
35
38
|
kfinance/tool_calling/get_latest.py,sha256=btGeVBmvX5QJutzrKfE6spatGGejDuHxtq53NoAaGNk,786
|
|
39
|
+
kfinance/tool_calling/get_latest_earnings.py,sha256=pQdExGdztGY3pHcak0bd6ULNf_ROPc91bJ7zAVSKQkk,1117
|
|
36
40
|
kfinance/tool_calling/get_n_quarters_ago.py,sha256=A0ilwPKUqU0YYQSz3gNsVF0Jy4YttXrSaDhYj7y8GHA,713
|
|
41
|
+
kfinance/tool_calling/get_next_earnings.py,sha256=knzQw-m-hscCvTuDUXG9v_dObKJBGn5BbDZWGKHKQcw,1097
|
|
37
42
|
kfinance/tool_calling/get_prices_from_identifier.py,sha256=ViJkwLDvStB7grc8RuoKSDXQM399Wru4-OY3E8k1l_U,1882
|
|
38
43
|
kfinance/tool_calling/get_segments_from_identifier.py,sha256=WIqJ1wWE6Z87VBREGu42nRc6_eJqUbGKcE9elzqBQJE,1867
|
|
44
|
+
kfinance/tool_calling/get_transcript.py,sha256=eB-IsRwD-mllsMOYRZbH35caQ1Y3teKft0tmI9nVL-A,756
|
|
45
|
+
kfinance/tool_calling/prompts.py,sha256=Yw1DJIMh90cjL-8q6_RMRiSjCtFDXvJAy7QiV5_uAU8,911
|
|
39
46
|
kfinance/tool_calling/resolve_identifier.py,sha256=npslr6bBCu0qEDV1-8d24F5OC3nQ1KBMphuMbHVC1AU,626
|
|
40
47
|
kfinance/tool_calling/shared_models.py,sha256=K-NPQyE_7Ew6Cs0zxG1xO2O47gp5uDHdHtWD7wUDZX4,2132
|
|
41
|
-
kensho_kfinance-2.
|
|
42
|
-
kensho_kfinance-2.
|
|
43
|
-
kensho_kfinance-2.
|
|
44
|
-
kensho_kfinance-2.
|
|
48
|
+
kensho_kfinance-2.4.0.dist-info/METADATA,sha256=yWX6RmluCSz_qoG7EVTJnKJcsOByeZYXmu3M88-ch2A,5191
|
|
49
|
+
kensho_kfinance-2.4.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
50
|
+
kensho_kfinance-2.4.0.dist-info/top_level.txt,sha256=kT_kNwVhfQoOAecY8W7uYah5xaHMoHoAdBIvXh6DaKM,9
|
|
51
|
+
kensho_kfinance-2.4.0.dist-info/RECORD,,
|
kfinance/CHANGELOG.md
CHANGED
kfinance/fetch.py
CHANGED
|
@@ -567,3 +567,13 @@ class KFinanceApiClient:
|
|
|
567
567
|
"""
|
|
568
568
|
url = f"{self.url_base}company_groups/industry/{industry_classification}/{industry_code}"
|
|
569
569
|
return self.fetch(url)
|
|
570
|
+
|
|
571
|
+
def fetch_earnings(self, company_id: int) -> dict:
|
|
572
|
+
"""Get the earnings for a company."""
|
|
573
|
+
url = f"{self.url_base}earnings/{company_id}"
|
|
574
|
+
return self.fetch(url)
|
|
575
|
+
|
|
576
|
+
def fetch_transcript(self, key_dev_id: int) -> dict:
|
|
577
|
+
"""Get the transcript for an earnings item."""
|
|
578
|
+
url = f"{self.url_base}transcript/{key_dev_id}"
|
|
579
|
+
return self.fetch(url)
|
kfinance/kfinance.py
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from collections.abc import Sequence
|
|
3
4
|
from concurrent.futures import ThreadPoolExecutor
|
|
5
|
+
from copy import deepcopy
|
|
4
6
|
from datetime import date, datetime, timezone
|
|
5
7
|
from functools import cached_property
|
|
6
8
|
from io import BytesIO
|
|
7
9
|
import logging
|
|
8
10
|
import re
|
|
9
11
|
from sys import stdout
|
|
10
|
-
from typing import TYPE_CHECKING, Any, Callable, Iterable, NamedTuple, Optional
|
|
12
|
+
from typing import TYPE_CHECKING, Any, Callable, Iterable, NamedTuple, Optional, overload
|
|
11
13
|
from urllib.parse import urljoin
|
|
12
14
|
import webbrowser
|
|
13
15
|
|
|
@@ -39,6 +41,7 @@ from .meta_classes import (
|
|
|
39
41
|
DelegatedCompanyFunctionsMetaClass,
|
|
40
42
|
)
|
|
41
43
|
from .prompt import PROMPT
|
|
44
|
+
from .pydantic_models import TranscriptComponent
|
|
42
45
|
from .server_thread import ServerThread
|
|
43
46
|
|
|
44
47
|
|
|
@@ -48,6 +51,12 @@ if TYPE_CHECKING:
|
|
|
48
51
|
logger = logging.getLogger(__name__)
|
|
49
52
|
|
|
50
53
|
|
|
54
|
+
class NoEarningsDataError(Exception):
|
|
55
|
+
"""Exception raised when no earnings data is found for a company."""
|
|
56
|
+
|
|
57
|
+
pass
|
|
58
|
+
|
|
59
|
+
|
|
51
60
|
class TradingItem:
|
|
52
61
|
"""Trading Class
|
|
53
62
|
|
|
@@ -199,6 +208,96 @@ class TradingItem:
|
|
|
199
208
|
return image
|
|
200
209
|
|
|
201
210
|
|
|
211
|
+
class Transcript(Sequence[TranscriptComponent]):
|
|
212
|
+
"""Transcript class that represents earnings item transcript components"""
|
|
213
|
+
|
|
214
|
+
def __init__(self, transcript_components: list[dict[str, str]]):
|
|
215
|
+
"""Initialize the Transcript object
|
|
216
|
+
|
|
217
|
+
:param transcript_components: List of transcript component dictionaries
|
|
218
|
+
:type transcript_components: list[dict[str, str]]
|
|
219
|
+
"""
|
|
220
|
+
self._components = [TranscriptComponent(**component) for component in transcript_components]
|
|
221
|
+
self._raw_transcript: str | None = None
|
|
222
|
+
|
|
223
|
+
@overload
|
|
224
|
+
def __getitem__(self, index: int) -> TranscriptComponent: ...
|
|
225
|
+
|
|
226
|
+
@overload
|
|
227
|
+
def __getitem__(self, index: slice) -> list[TranscriptComponent]: ...
|
|
228
|
+
|
|
229
|
+
def __getitem__(self, index: int | slice) -> TranscriptComponent | list[TranscriptComponent]:
|
|
230
|
+
return self._components[index]
|
|
231
|
+
|
|
232
|
+
def __len__(self) -> int:
|
|
233
|
+
return len(self._components)
|
|
234
|
+
|
|
235
|
+
@property
|
|
236
|
+
def raw(self) -> str:
|
|
237
|
+
"""Get the raw transcript as a single string
|
|
238
|
+
|
|
239
|
+
:return: Raw transcript text with speaker names and double newlines between components
|
|
240
|
+
:rtype: str
|
|
241
|
+
"""
|
|
242
|
+
if self._raw_transcript is not None:
|
|
243
|
+
return self._raw_transcript
|
|
244
|
+
|
|
245
|
+
raw_components = []
|
|
246
|
+
for component in self._components:
|
|
247
|
+
speaker = component.person_name
|
|
248
|
+
text = component.text
|
|
249
|
+
raw_components.append(f"{speaker}: {text}")
|
|
250
|
+
|
|
251
|
+
self._raw_transcript = "\n\n".join(raw_components)
|
|
252
|
+
return self._raw_transcript
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
class Earnings:
|
|
256
|
+
"""Earnings class that represents an earnings item"""
|
|
257
|
+
|
|
258
|
+
def __init__(
|
|
259
|
+
self,
|
|
260
|
+
kfinance_api_client: "KFinanceApiClient",
|
|
261
|
+
name: str,
|
|
262
|
+
datetime: datetime,
|
|
263
|
+
key_dev_id: int,
|
|
264
|
+
):
|
|
265
|
+
"""Initialize the Earnings object
|
|
266
|
+
|
|
267
|
+
:param kfinance_api_client: The KFinanceApiClient used to fetch data
|
|
268
|
+
:type kfinance_api_client: KFinanceApiClient
|
|
269
|
+
:param name: The earnings name
|
|
270
|
+
:type name: str
|
|
271
|
+
:param datetime: The earnings datetime
|
|
272
|
+
:type datetime: datetime
|
|
273
|
+
:param key_dev_id: The key dev ID for the earnings
|
|
274
|
+
:type key_dev_id: int
|
|
275
|
+
"""
|
|
276
|
+
self.kfinance_api_client = kfinance_api_client
|
|
277
|
+
self.name = name
|
|
278
|
+
self.datetime = datetime
|
|
279
|
+
self.key_dev_id = key_dev_id
|
|
280
|
+
self._transcript: Transcript | None = None
|
|
281
|
+
|
|
282
|
+
def __str__(self) -> str:
|
|
283
|
+
"""String representation for the earnings object"""
|
|
284
|
+
return f"{type(self).__module__}.{type(self).__qualname__} of {self.key_dev_id}"
|
|
285
|
+
|
|
286
|
+
@property
|
|
287
|
+
def transcript(self) -> Transcript:
|
|
288
|
+
"""Get the transcript for this earnings
|
|
289
|
+
|
|
290
|
+
:return: The transcript object containing all components
|
|
291
|
+
:rtype: Transcript
|
|
292
|
+
"""
|
|
293
|
+
if self._transcript is not None:
|
|
294
|
+
return self._transcript
|
|
295
|
+
|
|
296
|
+
transcript_data = self.kfinance_api_client.fetch_transcript(self.key_dev_id)
|
|
297
|
+
self._transcript = Transcript(transcript_data["transcript"])
|
|
298
|
+
return self._transcript
|
|
299
|
+
|
|
300
|
+
|
|
202
301
|
class Company(CompanyFunctionsMetaClass):
|
|
203
302
|
"""Company class
|
|
204
303
|
|
|
@@ -219,6 +318,7 @@ class Company(CompanyFunctionsMetaClass):
|
|
|
219
318
|
super().__init__()
|
|
220
319
|
self.kfinance_api_client = kfinance_api_client
|
|
221
320
|
self.company_id = company_id
|
|
321
|
+
self._all_earnings: list[Earnings] | None = None
|
|
222
322
|
|
|
223
323
|
def __str__(self) -> str:
|
|
224
324
|
"""String representation for the company object"""
|
|
@@ -248,16 +348,6 @@ class Company(CompanyFunctionsMetaClass):
|
|
|
248
348
|
security_ids = self.kfinance_api_client.fetch_securities(self.company_id)["securities"]
|
|
249
349
|
return Securities(kfinance_api_client=self.kfinance_api_client, security_ids=security_ids)
|
|
250
350
|
|
|
251
|
-
@cached_property
|
|
252
|
-
def latest_earnings_call(self) -> None:
|
|
253
|
-
"""Set and return the latest earnings call item for the object
|
|
254
|
-
|
|
255
|
-
:raises NotImplementedError: This function is not yet implemented
|
|
256
|
-
"""
|
|
257
|
-
raise NotImplementedError(
|
|
258
|
-
"The latest earnings call property of company class not implemented yet"
|
|
259
|
-
)
|
|
260
|
-
|
|
261
351
|
@cached_property
|
|
262
352
|
def info(self) -> dict:
|
|
263
353
|
"""Get the company info
|
|
@@ -398,6 +488,121 @@ class Company(CompanyFunctionsMetaClass):
|
|
|
398
488
|
]
|
|
399
489
|
]
|
|
400
490
|
|
|
491
|
+
@property
|
|
492
|
+
def all_earnings(self) -> list[Earnings]:
|
|
493
|
+
"""Retrieve and cache all earnings items for this company"""
|
|
494
|
+
if self._all_earnings is not None:
|
|
495
|
+
return self._all_earnings
|
|
496
|
+
|
|
497
|
+
earnings_data = self.kfinance_api_client.fetch_earnings(self.company_id)
|
|
498
|
+
self._all_earnings = []
|
|
499
|
+
|
|
500
|
+
for earnings in earnings_data["earnings"]:
|
|
501
|
+
if "keydevid" in earnings:
|
|
502
|
+
earnings["key_dev_id"] = deepcopy(earnings["keydevid"])
|
|
503
|
+
del earnings["keydevid"]
|
|
504
|
+
earnings_datetime = datetime.fromisoformat(
|
|
505
|
+
earnings["datetime"].replace("Z", "+00:00")
|
|
506
|
+
).replace(tzinfo=timezone.utc)
|
|
507
|
+
|
|
508
|
+
self._all_earnings.append(
|
|
509
|
+
Earnings(
|
|
510
|
+
kfinance_api_client=self.kfinance_api_client,
|
|
511
|
+
name=earnings["name"],
|
|
512
|
+
datetime=earnings_datetime,
|
|
513
|
+
key_dev_id=earnings["key_dev_id"],
|
|
514
|
+
)
|
|
515
|
+
)
|
|
516
|
+
|
|
517
|
+
return self._all_earnings
|
|
518
|
+
|
|
519
|
+
def earnings(
|
|
520
|
+
self, start_date: date | None = None, end_date: date | None = None
|
|
521
|
+
) -> list[Earnings]:
|
|
522
|
+
"""Get earnings for the company within date range sorted in descending order by date
|
|
523
|
+
|
|
524
|
+
:param start_date: Start date filter, defaults to None
|
|
525
|
+
:type start_date: date, optional
|
|
526
|
+
:param end_date: End date filter, defaults to None
|
|
527
|
+
:type end_date: date, optional
|
|
528
|
+
:return: List of earnings objects
|
|
529
|
+
:rtype: list[Earnings]
|
|
530
|
+
"""
|
|
531
|
+
if not self.all_earnings:
|
|
532
|
+
return []
|
|
533
|
+
|
|
534
|
+
if start_date is not None:
|
|
535
|
+
start_date_utc = datetime.combine(start_date, datetime.min.time()).replace(
|
|
536
|
+
tzinfo=timezone.utc
|
|
537
|
+
)
|
|
538
|
+
|
|
539
|
+
else:
|
|
540
|
+
start_date_utc = None
|
|
541
|
+
|
|
542
|
+
if end_date is not None:
|
|
543
|
+
end_date_utc = datetime.combine(end_date, datetime.max.time()).replace(
|
|
544
|
+
tzinfo=timezone.utc
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
else:
|
|
548
|
+
end_date_utc = None
|
|
549
|
+
|
|
550
|
+
filtered_earnings = []
|
|
551
|
+
|
|
552
|
+
for earnings in self.all_earnings:
|
|
553
|
+
# Apply date filtering if provided
|
|
554
|
+
if start_date_utc is not None and earnings.datetime < start_date_utc:
|
|
555
|
+
continue
|
|
556
|
+
|
|
557
|
+
if end_date_utc is not None and earnings.datetime > end_date_utc:
|
|
558
|
+
continue
|
|
559
|
+
|
|
560
|
+
filtered_earnings.append(earnings)
|
|
561
|
+
|
|
562
|
+
return filtered_earnings
|
|
563
|
+
|
|
564
|
+
@property
|
|
565
|
+
def latest_earnings(self) -> Earnings | None:
|
|
566
|
+
"""Get the most recent past earnings
|
|
567
|
+
|
|
568
|
+
:return: The most recent earnings or None if no data available
|
|
569
|
+
:rtype: Earnings | None
|
|
570
|
+
"""
|
|
571
|
+
if not self.all_earnings:
|
|
572
|
+
return None
|
|
573
|
+
|
|
574
|
+
now = datetime.now(timezone.utc)
|
|
575
|
+
past_earnings = [
|
|
576
|
+
earnings_item for earnings_item in self.all_earnings if earnings_item.datetime <= now
|
|
577
|
+
]
|
|
578
|
+
|
|
579
|
+
if not past_earnings:
|
|
580
|
+
return None
|
|
581
|
+
|
|
582
|
+
# Sort by datetime descending and get the most recent
|
|
583
|
+
return max(past_earnings, key=lambda x: x.datetime)
|
|
584
|
+
|
|
585
|
+
@property
|
|
586
|
+
def next_earnings(self) -> Earnings | None:
|
|
587
|
+
"""Get the next upcoming earnings
|
|
588
|
+
|
|
589
|
+
:return: The next earnings or None if no data available
|
|
590
|
+
:rtype: Earnings | None
|
|
591
|
+
"""
|
|
592
|
+
if not self.all_earnings:
|
|
593
|
+
return None
|
|
594
|
+
|
|
595
|
+
now = datetime.now(timezone.utc)
|
|
596
|
+
future_earnings = [
|
|
597
|
+
earnings_item for earnings_item in self.all_earnings if earnings_item.datetime > now
|
|
598
|
+
]
|
|
599
|
+
|
|
600
|
+
if not future_earnings:
|
|
601
|
+
return None
|
|
602
|
+
|
|
603
|
+
# Sort by datetime ascending and get the earliest
|
|
604
|
+
return min(future_earnings, key=lambda x: x.datetime)
|
|
605
|
+
|
|
401
606
|
|
|
402
607
|
class Security:
|
|
403
608
|
"""Security class
|
|
@@ -1345,6 +1550,17 @@ class Client:
|
|
|
1345
1550
|
kfinance_api_client=self.kfinance_api_client, trading_item_id=trading_item_id
|
|
1346
1551
|
)
|
|
1347
1552
|
|
|
1553
|
+
def transcript(self, key_dev_id: int) -> Transcript:
|
|
1554
|
+
"""Generate Transcript object from key_dev_id
|
|
1555
|
+
|
|
1556
|
+
:param key_dev_id: The key dev ID for the earnings
|
|
1557
|
+
:type key_dev_id: int
|
|
1558
|
+
:return: The transcript specified by the key dev id
|
|
1559
|
+
:rtype: Transcript
|
|
1560
|
+
"""
|
|
1561
|
+
transcript_data = self.kfinance_api_client.fetch_transcript(key_dev_id)
|
|
1562
|
+
return Transcript(transcript_data["transcript"])
|
|
1563
|
+
|
|
1348
1564
|
@staticmethod
|
|
1349
1565
|
def get_latest(use_local_timezone: bool = True) -> LatestPeriods:
|
|
1350
1566
|
"""Get the latest annual reporting year, latest quarterly reporting quarter and year, and current date.
|
kfinance/mcp.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
from textwrap import dedent
|
|
2
|
+
from typing import Literal, Optional
|
|
3
|
+
|
|
4
|
+
import click
|
|
5
|
+
from fastmcp import FastMCP
|
|
6
|
+
from fastmcp.utilities.logging import get_logger
|
|
7
|
+
|
|
8
|
+
from kfinance.kfinance import Client
|
|
9
|
+
from kfinance.tool_calling.shared_models import KfinanceTool
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
logger = get_logger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def build_doc_string(tool: KfinanceTool) -> str:
|
|
16
|
+
"""Build a formatted documentation string for a Kfinance tool.
|
|
17
|
+
|
|
18
|
+
This function takes a KfinanceTool object and constructs a comprehensive
|
|
19
|
+
documentation string that includes the tool's description and detailed
|
|
20
|
+
information about its arguments, including default values and descriptions.
|
|
21
|
+
|
|
22
|
+
:param tool: The Kfinance tool object containing metadata about the tool's functionality, description, and argument schema.
|
|
23
|
+
:type tool: KfinanceTool
|
|
24
|
+
:return: A formatted documentation string containing for the tool description with detailed argument information.
|
|
25
|
+
:rtype: str
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
description = dedent(f"""
|
|
29
|
+
{tool.description}
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
""").strip()
|
|
33
|
+
|
|
34
|
+
for arg_name, arg_field in tool.args_schema.model_fields.items():
|
|
35
|
+
default_value_description = (
|
|
36
|
+
f"Default: {arg_field.default}. " if not arg_field.is_required() else ""
|
|
37
|
+
)
|
|
38
|
+
param_description = f"\n {arg_name}: {default_value_description}{arg_field.description}"
|
|
39
|
+
description += param_description
|
|
40
|
+
|
|
41
|
+
return description
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@click.command()
|
|
45
|
+
@click.option("--stdio/--sse", "-s/ ", default=False)
|
|
46
|
+
@click.option("--refresh-token", required=False)
|
|
47
|
+
@click.option("--client-id", required=False)
|
|
48
|
+
@click.option("--private-key", required=False)
|
|
49
|
+
def run_mcp(
|
|
50
|
+
stdio: bool,
|
|
51
|
+
refresh_token: Optional[str] = None,
|
|
52
|
+
client_id: Optional[str] = None,
|
|
53
|
+
private_key: Optional[str] = None,
|
|
54
|
+
) -> None:
|
|
55
|
+
"""Run the Kfinance MCP server with specified configuration.
|
|
56
|
+
|
|
57
|
+
This function initializes and starts an MCP server that exposes Kfinance
|
|
58
|
+
tools. The server supports multiple authentication methods and
|
|
59
|
+
transport protocols to accommodate different deployment scenarios.
|
|
60
|
+
|
|
61
|
+
Authentication Methods (in order of precedence):
|
|
62
|
+
1. Refresh Token: Uses an existing refresh token for authentication
|
|
63
|
+
2. Key Pair: Uses client ID and private key for authentication
|
|
64
|
+
3. Browser: Falls back to browser-based authentication flow
|
|
65
|
+
|
|
66
|
+
:param stdio: If True, use STDIO transport; if False, use SSE transport.
|
|
67
|
+
:type stdio: bool
|
|
68
|
+
:param refresh_token: OAuth refresh token for authentication
|
|
69
|
+
:type refresh_token: str
|
|
70
|
+
:param client_id: Client id for key-pair authentication
|
|
71
|
+
:type client_id: str
|
|
72
|
+
:param private_key: Private key for key-pair authentication.
|
|
73
|
+
:type private_key: str
|
|
74
|
+
"""
|
|
75
|
+
transport: Literal["stdio", "sse"] = "stdio" if stdio else "sse"
|
|
76
|
+
logger.info("Sever will run with %s transport", transport)
|
|
77
|
+
if refresh_token:
|
|
78
|
+
logger.info("The client will be authenticated using a refresh token")
|
|
79
|
+
kfinance_client = Client(refresh_token=refresh_token)
|
|
80
|
+
elif client_id and private_key:
|
|
81
|
+
logger.info("The client will be authenticated using a key pair")
|
|
82
|
+
kfinance_client = Client(client_id=client_id, private_key=private_key)
|
|
83
|
+
else:
|
|
84
|
+
logger.info("The client will be authenticated using a browser")
|
|
85
|
+
kfinance_client = Client()
|
|
86
|
+
|
|
87
|
+
kfinance_mcp: FastMCP = FastMCP("Kfinance")
|
|
88
|
+
for tool in kfinance_client.langchain_tools:
|
|
89
|
+
logger.info("Adding %s to server", tool.name)
|
|
90
|
+
kfinance_mcp.tool(
|
|
91
|
+
name_or_fn=getattr(tool, "_run"),
|
|
92
|
+
name=tool.name,
|
|
93
|
+
description=build_doc_string(tool),
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
logger.info("Server starting")
|
|
97
|
+
kfinance_mcp.run(transport=transport)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
if __name__ == "__main__":
|
|
101
|
+
run_mcp()
|
kfinance/pydantic_models.py
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
from pydantic import BaseModel
|
|
2
2
|
|
|
3
3
|
|
|
4
|
+
class TranscriptComponent(BaseModel):
|
|
5
|
+
"""A transcript component with person name, text, and component type."""
|
|
6
|
+
|
|
7
|
+
person_name: str
|
|
8
|
+
text: str
|
|
9
|
+
component_type: str
|
|
10
|
+
|
|
11
|
+
|
|
4
12
|
class RelationshipResponseNoName(BaseModel):
|
|
5
13
|
"""A response from the relationship endpoint before adding the company name.
|
|
6
14
|
|
|
@@ -44,7 +44,7 @@ def jupyter_kernel_name() -> str:
|
|
|
44
44
|
|
|
45
45
|
def test_run_notebook(jupyter_kernel_name: str):
|
|
46
46
|
"""
|
|
47
|
-
GIVEN the
|
|
47
|
+
GIVEN the basic_usage.ipynb notebook
|
|
48
48
|
WHEN the notebook gets run with a mock client and mock responses
|
|
49
49
|
THEN all cells of the notebook complete without errors.
|
|
50
50
|
"""
|
|
@@ -165,7 +165,9 @@ def test_run_notebook(jupyter_kernel_name: str):
|
|
|
165
165
|
""")
|
|
166
166
|
|
|
167
167
|
# Load the notebook
|
|
168
|
-
notebook_path = Path(
|
|
168
|
+
notebook_path = Path(
|
|
169
|
+
Path(__file__).parent.parent.parent, "example_notebooks", "basic_usage.ipynb"
|
|
170
|
+
)
|
|
169
171
|
with notebook_path.open() as f:
|
|
170
172
|
nb = nbformat.read(f, as_version=4)
|
|
171
173
|
|
kfinance/tests/test_fetch.py
CHANGED
|
@@ -138,6 +138,18 @@ class TestFetchItem(TestCase):
|
|
|
138
138
|
self.kfinance_api_client.fetch_earnings_dates(company_id=company_id)
|
|
139
139
|
self.kfinance_api_client.fetch.assert_called_once_with(expected_fetch_url)
|
|
140
140
|
|
|
141
|
+
def test_fetch_earnings(self) -> None:
|
|
142
|
+
company_id = 21719
|
|
143
|
+
expected_fetch_url = f"{self.kfinance_api_client.url_base}earnings/{company_id}"
|
|
144
|
+
self.kfinance_api_client.fetch_earnings(company_id=company_id)
|
|
145
|
+
self.kfinance_api_client.fetch.assert_called_once_with(expected_fetch_url)
|
|
146
|
+
|
|
147
|
+
def test_fetch_transcript(self) -> None:
|
|
148
|
+
key_dev_id = 12345
|
|
149
|
+
expected_fetch_url = f"{self.kfinance_api_client.url_base}transcript/{key_dev_id}"
|
|
150
|
+
self.kfinance_api_client.fetch_transcript(key_dev_id=key_dev_id)
|
|
151
|
+
self.kfinance_api_client.fetch.assert_called_once_with(expected_fetch_url)
|
|
152
|
+
|
|
141
153
|
def test_fetch_ticker_geography_groups(self) -> None:
|
|
142
154
|
country_iso_code = "USA"
|
|
143
155
|
expected_fetch_url = (
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from typing import Type
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from kfinance.kfinance import Client
|
|
6
|
+
from kfinance.mcp import build_doc_string
|
|
7
|
+
from kfinance.tool_calling import ALL_TOOLS
|
|
8
|
+
from kfinance.tool_calling.shared_models import KfinanceTool
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TestDocStringBuilding:
|
|
12
|
+
@pytest.mark.parametrize("tool_class", ALL_TOOLS)
|
|
13
|
+
def test_build_doc_string(self, mock_client: Client, tool_class: Type[KfinanceTool]):
|
|
14
|
+
"""This test build the docstring for each tool. A success is considered if no exception is raised"""
|
|
15
|
+
tool = tool_class(kfinance_client=mock_client)
|
|
16
|
+
build_doc_string(tool)
|
kfinance/tests/test_objects.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from datetime import datetime, timezone
|
|
1
|
+
from datetime import date, datetime, timezone
|
|
2
2
|
from io import BytesIO
|
|
3
3
|
import re
|
|
4
4
|
from typing import Optional
|
|
@@ -7,8 +7,9 @@ from unittest import TestCase
|
|
|
7
7
|
import numpy as np
|
|
8
8
|
import pandas as pd
|
|
9
9
|
from PIL.Image import open as image_open
|
|
10
|
+
import time_machine
|
|
10
11
|
|
|
11
|
-
from kfinance.kfinance import Company, Security, Ticker, TradingItem
|
|
12
|
+
from kfinance.kfinance import Company, Earnings, Security, Ticker, TradingItem, Transcript
|
|
12
13
|
|
|
13
14
|
|
|
14
15
|
msft_company_id = "21835"
|
|
@@ -54,6 +55,25 @@ MOCK_COMPANY_DB = {
|
|
|
54
55
|
"iso_country": "USA",
|
|
55
56
|
},
|
|
56
57
|
"earnings_call_dates": {"earnings": ["2004-07-22T21:30:00"]},
|
|
58
|
+
"earnings": {
|
|
59
|
+
"earnings": [
|
|
60
|
+
{
|
|
61
|
+
"name": "Microsoft Corporation, Q4 2024 Earnings Call, Jul 25, 2024",
|
|
62
|
+
"key_dev_id": 1916266380,
|
|
63
|
+
"datetime": "2024-07-25T21:30:00",
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"name": "Microsoft Corporation, Q1 2025 Earnings Call, Oct 24, 2024",
|
|
67
|
+
"keydevid": 1916266381,
|
|
68
|
+
"datetime": "2024-10-24T21:30:00",
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
"name": "Microsoft Corporation, Q2 2025 Earnings Call, Jan 25, 2025",
|
|
72
|
+
"keydevid": 1916266382,
|
|
73
|
+
"datetime": "2025-01-25T21:30:00",
|
|
74
|
+
},
|
|
75
|
+
]
|
|
76
|
+
},
|
|
57
77
|
"statements": {
|
|
58
78
|
"income_statement": {
|
|
59
79
|
"statements": {
|
|
@@ -91,6 +111,32 @@ MOCK_COMPANY_DB = {
|
|
|
91
111
|
}
|
|
92
112
|
}
|
|
93
113
|
|
|
114
|
+
MOCK_TRANSCRIPT_DB = {
|
|
115
|
+
1916266380: {
|
|
116
|
+
"transcript": [
|
|
117
|
+
{
|
|
118
|
+
"component_type": "Presentation Operator Message",
|
|
119
|
+
"person_name": "Operator",
|
|
120
|
+
"text": "Good morning, and welcome to Microsoft's Fourth Quarter 2024 Earnings Conference Call.",
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
"component_type": "Presenter Speech",
|
|
124
|
+
"person_name": "Satya Nadella",
|
|
125
|
+
"text": "Thank you for joining us today. We had an exceptional quarter with strong growth across all segments.",
|
|
126
|
+
},
|
|
127
|
+
]
|
|
128
|
+
},
|
|
129
|
+
1916266381: {
|
|
130
|
+
"transcript": [
|
|
131
|
+
{
|
|
132
|
+
"component_type": "Presentation Operator Message",
|
|
133
|
+
"person_name": "Operator",
|
|
134
|
+
"text": "Good morning, and welcome to Microsoft's First Quarter 2025 Earnings Conference Call.",
|
|
135
|
+
}
|
|
136
|
+
]
|
|
137
|
+
},
|
|
138
|
+
}
|
|
139
|
+
|
|
94
140
|
|
|
95
141
|
MOCK_SECURITY_DB = {msft_security_id: {"isin": msft_isin, "cusip": msft_cusip}}
|
|
96
142
|
|
|
@@ -215,6 +261,14 @@ class MockKFinanceApiClient:
|
|
|
215
261
|
"""Get a segment"""
|
|
216
262
|
return MOCK_COMPANY_DB[company_id]
|
|
217
263
|
|
|
264
|
+
def fetch_earnings(self, company_id: int) -> dict:
|
|
265
|
+
"""Get the earnings for a company."""
|
|
266
|
+
return MOCK_COMPANY_DB[company_id]["earnings"]
|
|
267
|
+
|
|
268
|
+
def fetch_transcript(self, key_dev_id: int) -> dict:
|
|
269
|
+
"""Get the transcript for an earnings item."""
|
|
270
|
+
return MOCK_TRANSCRIPT_DB[key_dev_id]
|
|
271
|
+
|
|
218
272
|
|
|
219
273
|
class TestTradingItem(TestCase):
|
|
220
274
|
def setUp(self):
|
|
@@ -619,3 +673,112 @@ class TestTicker(TestCase):
|
|
|
619
673
|
expected_dataframe.index.name = "date"
|
|
620
674
|
market_caps = self.msft_ticker_from_ticker.market_cap()
|
|
621
675
|
pd.testing.assert_frame_equal(expected_dataframe, market_caps)
|
|
676
|
+
|
|
677
|
+
|
|
678
|
+
class TestTranscript(TestCase):
|
|
679
|
+
def setUp(self):
|
|
680
|
+
"""setup tests"""
|
|
681
|
+
self.transcript_components = [
|
|
682
|
+
{
|
|
683
|
+
"component_type": "Presentation Operator Message",
|
|
684
|
+
"person_name": "Operator",
|
|
685
|
+
"text": "Good morning, and welcome to Microsoft's Fourth Quarter 2024 Earnings Conference Call.",
|
|
686
|
+
},
|
|
687
|
+
{
|
|
688
|
+
"component_type": "Presenter Speech",
|
|
689
|
+
"person_name": "Satya Nadella",
|
|
690
|
+
"text": "Thank you for joining us today. We had an exceptional quarter with strong growth across all segments.",
|
|
691
|
+
},
|
|
692
|
+
]
|
|
693
|
+
self.transcript = Transcript(self.transcript_components)
|
|
694
|
+
|
|
695
|
+
def test_transcript_length(self):
|
|
696
|
+
"""test transcript length"""
|
|
697
|
+
self.assertEqual(len(self.transcript), 2)
|
|
698
|
+
|
|
699
|
+
def test_transcript_indexing(self):
|
|
700
|
+
"""test transcript indexing"""
|
|
701
|
+
self.assertEqual(
|
|
702
|
+
self.transcript[0].person_name, self.transcript_components[0]["person_name"]
|
|
703
|
+
)
|
|
704
|
+
self.assertEqual(self.transcript[0].text, self.transcript_components[0]["text"])
|
|
705
|
+
self.assertEqual(
|
|
706
|
+
self.transcript[0].component_type, self.transcript_components[0]["component_type"]
|
|
707
|
+
)
|
|
708
|
+
self.assertEqual(
|
|
709
|
+
self.transcript[1].person_name, self.transcript_components[1]["person_name"]
|
|
710
|
+
)
|
|
711
|
+
self.assertEqual(self.transcript[1].text, self.transcript_components[1]["text"])
|
|
712
|
+
self.assertEqual(
|
|
713
|
+
self.transcript[1].component_type, self.transcript_components[1]["component_type"]
|
|
714
|
+
)
|
|
715
|
+
|
|
716
|
+
def test_transcript_raw(self):
|
|
717
|
+
"""test transcript raw property"""
|
|
718
|
+
expected_raw = "Operator: Good morning, and welcome to Microsoft's Fourth Quarter 2024 Earnings Conference Call.\n\nSatya Nadella: Thank you for joining us today. We had an exceptional quarter with strong growth across all segments."
|
|
719
|
+
self.assertEqual(self.transcript.raw, expected_raw)
|
|
720
|
+
|
|
721
|
+
|
|
722
|
+
class TestEarnings(TestCase):
|
|
723
|
+
def setUp(self):
|
|
724
|
+
"""setup tests"""
|
|
725
|
+
self.kfinance_api_client = MockKFinanceApiClient()
|
|
726
|
+
self.earnings = Earnings(
|
|
727
|
+
kfinance_api_client=self.kfinance_api_client,
|
|
728
|
+
name="Microsoft Corporation, Q4 2024 Earnings Call, Jul 25, 2024",
|
|
729
|
+
datetime=datetime.fromisoformat("2024-07-25T21:30:00").replace(tzinfo=timezone.utc),
|
|
730
|
+
key_dev_id=1916266380,
|
|
731
|
+
)
|
|
732
|
+
|
|
733
|
+
def test_earnings_attributes(self):
|
|
734
|
+
"""test earnings attributes"""
|
|
735
|
+
self.assertEqual(
|
|
736
|
+
self.earnings.name, "Microsoft Corporation, Q4 2024 Earnings Call, Jul 25, 2024"
|
|
737
|
+
)
|
|
738
|
+
self.assertEqual(self.earnings.key_dev_id, 1916266380)
|
|
739
|
+
expected_datetime = datetime.fromisoformat("2024-07-25T21:30:00").replace(
|
|
740
|
+
tzinfo=timezone.utc
|
|
741
|
+
)
|
|
742
|
+
self.assertEqual(self.earnings.datetime, expected_datetime)
|
|
743
|
+
|
|
744
|
+
def test_earnings_transcript(self):
|
|
745
|
+
"""test earnings transcript property"""
|
|
746
|
+
transcript = self.earnings.transcript
|
|
747
|
+
self.assertIsInstance(transcript, Transcript)
|
|
748
|
+
self.assertEqual(len(transcript), 2)
|
|
749
|
+
self.assertEqual(transcript[0].person_name, "Operator")
|
|
750
|
+
self.assertEqual(transcript[1].person_name, "Satya Nadella")
|
|
751
|
+
|
|
752
|
+
|
|
753
|
+
class TestCompanyEarnings(TestCase):
|
|
754
|
+
def setUp(self):
|
|
755
|
+
"""setup tests"""
|
|
756
|
+
self.kfinance_api_client = MockKFinanceApiClient()
|
|
757
|
+
self.msft_company = Company(self.kfinance_api_client, msft_company_id)
|
|
758
|
+
|
|
759
|
+
def test_company_earnings(self):
|
|
760
|
+
"""test company earnings method"""
|
|
761
|
+
earnings_list = self.msft_company.earnings()
|
|
762
|
+
self.assertEqual(len(earnings_list), 3)
|
|
763
|
+
self.assertIsInstance(earnings_list[0], Earnings)
|
|
764
|
+
self.assertEqual(earnings_list[0].key_dev_id, 1916266380)
|
|
765
|
+
|
|
766
|
+
def test_company_earnings_with_date_filter(self):
|
|
767
|
+
"""test company earnings method with date filtering"""
|
|
768
|
+
start_date = date(2024, 8, 1)
|
|
769
|
+
end_date = date(2024, 12, 31)
|
|
770
|
+
earnings_list = self.msft_company.earnings(start_date=start_date, end_date=end_date)
|
|
771
|
+
self.assertEqual(len(earnings_list), 1)
|
|
772
|
+
self.assertEqual(earnings_list[0].key_dev_id, 1916266381)
|
|
773
|
+
|
|
774
|
+
@time_machine.travel(datetime(2025, 2, 1, 12, tzinfo=timezone.utc))
|
|
775
|
+
def test_company_latest_earnings(self):
|
|
776
|
+
"""test company latest_earnings property"""
|
|
777
|
+
latest_earnings = self.msft_company.latest_earnings
|
|
778
|
+
self.assertEqual(latest_earnings.key_dev_id, 1916266382)
|
|
779
|
+
|
|
780
|
+
@time_machine.travel(datetime(2024, 6, 1, 12, tzinfo=timezone.utc))
|
|
781
|
+
def test_company_next_earnings(self):
|
|
782
|
+
"""test company next_earnings property"""
|
|
783
|
+
next_earnings = self.msft_company.next_earnings
|
|
784
|
+
self.assertEqual(next_earnings.key_dev_id, 1916266380)
|
kfinance/tests/test_tools.py
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
from datetime import date, datetime
|
|
2
2
|
|
|
3
3
|
from langchain_core.utils.function_calling import convert_to_openai_tool
|
|
4
|
+
from pytest import raises
|
|
4
5
|
from requests_mock import Mocker
|
|
5
6
|
import time_machine
|
|
6
7
|
|
|
7
8
|
from kfinance.constants import BusinessRelationshipType, Capitalization, SegmentType, StatementType
|
|
8
|
-
from kfinance.kfinance import Client
|
|
9
|
+
from kfinance.kfinance import Client, NoEarningsDataError
|
|
9
10
|
from kfinance.tests.conftest import SPGI_COMPANY_ID, SPGI_SECURITY_ID, SPGI_TRADING_ITEM_ID
|
|
10
11
|
from kfinance.tool_calling import (
|
|
12
|
+
GetEarnings,
|
|
11
13
|
GetEarningsCallDatetimesFromIdentifier,
|
|
12
14
|
GetFinancialLineItemFromIdentifier,
|
|
13
15
|
GetFinancialStatementFromIdentifier,
|
|
@@ -15,8 +17,11 @@ from kfinance.tool_calling import (
|
|
|
15
17
|
GetInfoFromIdentifier,
|
|
16
18
|
GetIsinFromTicker,
|
|
17
19
|
GetLatest,
|
|
20
|
+
GetLatestEarnings,
|
|
21
|
+
GetNextEarnings,
|
|
18
22
|
GetNQuartersAgo,
|
|
19
23
|
GetPricesFromIdentifier,
|
|
24
|
+
GetTranscript,
|
|
20
25
|
ResolveIdentifier,
|
|
21
26
|
)
|
|
22
27
|
from kfinance.tool_calling.get_business_relationship_from_identifier import (
|
|
@@ -42,6 +47,7 @@ from kfinance.tool_calling.get_segments_from_identifier import (
|
|
|
42
47
|
GetSegmentsFromIdentifier,
|
|
43
48
|
GetSegmentsFromIdentifierArgs,
|
|
44
49
|
)
|
|
50
|
+
from kfinance.tool_calling.get_transcript import GetTranscriptArgs
|
|
45
51
|
from kfinance.tool_calling.shared_models import ToolArgsWithIdentifier
|
|
46
52
|
|
|
47
53
|
|
|
@@ -422,3 +428,213 @@ class TestResolveIdentifier:
|
|
|
422
428
|
"security_id": SPGI_SECURITY_ID,
|
|
423
429
|
"trading_item_id": SPGI_TRADING_ITEM_ID,
|
|
424
430
|
}
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
class TestGetLatestEarnings:
|
|
434
|
+
def test_get_latest_earnings(self, requests_mock: Mocker, mock_client: Client):
|
|
435
|
+
"""
|
|
436
|
+
GIVEN the GetLatestEarnings tool
|
|
437
|
+
WHEN we request the latest earnings for SPGI
|
|
438
|
+
THEN we get back the latest SPGI earnings
|
|
439
|
+
"""
|
|
440
|
+
earnings_data = {
|
|
441
|
+
"earnings": [
|
|
442
|
+
{
|
|
443
|
+
"name": "SPGI Q4 2024 Earnings Call",
|
|
444
|
+
"datetime": "2025-02-11T13:30:00Z",
|
|
445
|
+
"keydevid": 12345,
|
|
446
|
+
},
|
|
447
|
+
{
|
|
448
|
+
"name": "SPGI Q3 2024 Earnings Call",
|
|
449
|
+
"datetime": "2024-10-30T12:30:00Z",
|
|
450
|
+
"keydevid": 12344,
|
|
451
|
+
},
|
|
452
|
+
]
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
requests_mock.get(
|
|
456
|
+
url=f"https://kfinance.kensho.com/api/v1/earnings/{SPGI_COMPANY_ID}",
|
|
457
|
+
json=earnings_data,
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
expected_response = {
|
|
461
|
+
"name": "SPGI Q4 2024 Earnings Call",
|
|
462
|
+
"key_dev_id": 12345,
|
|
463
|
+
"datetime": "2025-02-11T13:30:00+00:00",
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
tool = GetLatestEarnings(kfinance_client=mock_client)
|
|
467
|
+
response = tool.run(ToolArgsWithIdentifier(identifier="SPGI").model_dump(mode="json"))
|
|
468
|
+
assert response == expected_response
|
|
469
|
+
|
|
470
|
+
def test_get_latest_earnings_no_data(self, requests_mock: Mocker, mock_client: Client):
|
|
471
|
+
"""
|
|
472
|
+
GIVEN the GetLatestEarnings tool
|
|
473
|
+
WHEN we request the latest earnings for a company with no data
|
|
474
|
+
THEN we get a NoEarningsDataError exception
|
|
475
|
+
"""
|
|
476
|
+
earnings_data = {"earnings": []}
|
|
477
|
+
|
|
478
|
+
requests_mock.get(
|
|
479
|
+
url=f"https://kfinance.kensho.com/api/v1/earnings/{SPGI_COMPANY_ID}",
|
|
480
|
+
json=earnings_data,
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
tool = GetLatestEarnings(kfinance_client=mock_client)
|
|
484
|
+
with raises(NoEarningsDataError, match="Latest earnings for SPGI not found"):
|
|
485
|
+
tool.run(ToolArgsWithIdentifier(identifier="SPGI").model_dump(mode="json"))
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
class TestGetNextEarnings:
|
|
489
|
+
def test_get_next_earnings_(self, requests_mock: Mocker, mock_client: Client):
|
|
490
|
+
"""
|
|
491
|
+
GIVEN the GetNextEarnings tool
|
|
492
|
+
WHEN we request the next earnings for SPGI
|
|
493
|
+
THEN we get back the next SPGI earnings
|
|
494
|
+
"""
|
|
495
|
+
earnings_data = {
|
|
496
|
+
"earnings": [
|
|
497
|
+
{
|
|
498
|
+
"name": "SPGI Q1 2025 Earnings Call",
|
|
499
|
+
"datetime": "2025-04-29T12:30:00Z",
|
|
500
|
+
"keydevid": 12346,
|
|
501
|
+
},
|
|
502
|
+
{
|
|
503
|
+
"name": "SPGI Q4 2024 Earnings Call",
|
|
504
|
+
"datetime": "2025-02-11T13:30:00Z",
|
|
505
|
+
"keydevid": 12345,
|
|
506
|
+
},
|
|
507
|
+
]
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
requests_mock.get(
|
|
511
|
+
url=f"https://kfinance.kensho.com/api/v1/earnings/{SPGI_COMPANY_ID}",
|
|
512
|
+
json=earnings_data,
|
|
513
|
+
)
|
|
514
|
+
|
|
515
|
+
expected_response = {
|
|
516
|
+
"name": "SPGI Q1 2025 Earnings Call",
|
|
517
|
+
"key_dev_id": 12346,
|
|
518
|
+
"datetime": "2025-04-29T12:30:00+00:00",
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
with time_machine.travel("2025-03-01T00:00:00+00:00"):
|
|
522
|
+
tool = GetNextEarnings(kfinance_client=mock_client)
|
|
523
|
+
response = tool.run(ToolArgsWithIdentifier(identifier="SPGI").model_dump(mode="json"))
|
|
524
|
+
assert response == expected_response
|
|
525
|
+
|
|
526
|
+
def test_get_next_earnings_no_data(self, requests_mock: Mocker, mock_client: Client):
|
|
527
|
+
"""
|
|
528
|
+
GIVEN the GetNextEarnings tool
|
|
529
|
+
WHEN we request the next earnings for a company with no data
|
|
530
|
+
THEN we get a NoEarningsDataError exception
|
|
531
|
+
"""
|
|
532
|
+
earnings_data = {"earnings": []}
|
|
533
|
+
|
|
534
|
+
requests_mock.get(
|
|
535
|
+
url=f"https://kfinance.kensho.com/api/v1/earnings/{SPGI_COMPANY_ID}",
|
|
536
|
+
json=earnings_data,
|
|
537
|
+
)
|
|
538
|
+
|
|
539
|
+
with time_machine.travel("2025-03-01T00:00:00+00:00"):
|
|
540
|
+
tool = GetNextEarnings(kfinance_client=mock_client)
|
|
541
|
+
with raises(NoEarningsDataError, match="Next earnings for SPGI not found"):
|
|
542
|
+
tool.run(ToolArgsWithIdentifier(identifier="SPGI").model_dump(mode="json"))
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
class TestGetEarnings:
|
|
546
|
+
def test_get_earnings(self, requests_mock: Mocker, mock_client: Client):
|
|
547
|
+
"""
|
|
548
|
+
GIVEN the GetEarnings tool
|
|
549
|
+
WHEN we request all earnings for SPGI
|
|
550
|
+
THEN we get back all SPGI earnings
|
|
551
|
+
"""
|
|
552
|
+
earnings_data = {
|
|
553
|
+
"earnings": [
|
|
554
|
+
{
|
|
555
|
+
"name": "SPGI Q1 2025 Earnings Call",
|
|
556
|
+
"datetime": "2025-04-29T12:30:00Z",
|
|
557
|
+
"keydevid": 12346,
|
|
558
|
+
},
|
|
559
|
+
{
|
|
560
|
+
"name": "SPGI Q4 2024 Earnings Call",
|
|
561
|
+
"datetime": "2025-02-11T13:30:00Z",
|
|
562
|
+
"keydevid": 12345,
|
|
563
|
+
},
|
|
564
|
+
]
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
requests_mock.get(
|
|
568
|
+
url=f"https://kfinance.kensho.com/api/v1/earnings/{SPGI_COMPANY_ID}",
|
|
569
|
+
json=earnings_data,
|
|
570
|
+
)
|
|
571
|
+
|
|
572
|
+
expected_response = [
|
|
573
|
+
{
|
|
574
|
+
"name": "SPGI Q1 2025 Earnings Call",
|
|
575
|
+
"key_dev_id": 12346,
|
|
576
|
+
"datetime": "2025-04-29T12:30:00+00:00",
|
|
577
|
+
},
|
|
578
|
+
{
|
|
579
|
+
"name": "SPGI Q4 2024 Earnings Call",
|
|
580
|
+
"key_dev_id": 12345,
|
|
581
|
+
"datetime": "2025-02-11T13:30:00+00:00",
|
|
582
|
+
},
|
|
583
|
+
]
|
|
584
|
+
|
|
585
|
+
tool = GetEarnings(kfinance_client=mock_client)
|
|
586
|
+
response = tool.run(ToolArgsWithIdentifier(identifier="SPGI").model_dump(mode="json"))
|
|
587
|
+
assert response == expected_response
|
|
588
|
+
|
|
589
|
+
def test_get_earnings_no_data(self, requests_mock: Mocker, mock_client: Client):
|
|
590
|
+
"""
|
|
591
|
+
GIVEN the GetEarnings tool
|
|
592
|
+
WHEN we request all earnings for a company with no data
|
|
593
|
+
THEN we get a NoEarningslDataError exception
|
|
594
|
+
"""
|
|
595
|
+
earnings_data = {"earnings": []}
|
|
596
|
+
|
|
597
|
+
requests_mock.get(
|
|
598
|
+
url=f"https://kfinance.kensho.com/api/v1/earnings/{SPGI_COMPANY_ID}",
|
|
599
|
+
json=earnings_data,
|
|
600
|
+
)
|
|
601
|
+
|
|
602
|
+
tool = GetEarnings(kfinance_client=mock_client)
|
|
603
|
+
with raises(NoEarningsDataError, match="Earnings for SPGI not found"):
|
|
604
|
+
tool.run(ToolArgsWithIdentifier(identifier="SPGI").model_dump(mode="json"))
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
class TestGetTranscript:
|
|
608
|
+
def test_get_transcript(self, requests_mock: Mocker, mock_client: Client):
|
|
609
|
+
"""
|
|
610
|
+
GIVEN the GetTranscript tool
|
|
611
|
+
WHEN we request a transcript by key_dev_id
|
|
612
|
+
THEN we get back the transcript text
|
|
613
|
+
"""
|
|
614
|
+
transcript_data = {
|
|
615
|
+
"transcript": [
|
|
616
|
+
{
|
|
617
|
+
"person_name": "Operator",
|
|
618
|
+
"text": "Good morning, everyone.",
|
|
619
|
+
"component_type": "speech",
|
|
620
|
+
},
|
|
621
|
+
{
|
|
622
|
+
"person_name": "CEO",
|
|
623
|
+
"text": "Thank you for joining us today.",
|
|
624
|
+
"component_type": "speech",
|
|
625
|
+
},
|
|
626
|
+
]
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
requests_mock.get(
|
|
630
|
+
url="https://kfinance.kensho.com/api/v1/transcript/12345",
|
|
631
|
+
json=transcript_data,
|
|
632
|
+
)
|
|
633
|
+
|
|
634
|
+
expected_response = (
|
|
635
|
+
"Operator: Good morning, everyone.\n\nCEO: Thank you for joining us today."
|
|
636
|
+
)
|
|
637
|
+
|
|
638
|
+
tool = GetTranscript(kfinance_client=mock_client)
|
|
639
|
+
response = tool.run(GetTranscriptArgs(key_dev_id=12345).model_dump(mode="json"))
|
|
640
|
+
assert response == expected_response
|
|
@@ -5,6 +5,7 @@ from kfinance.tool_calling.get_business_relationship_from_identifier import (
|
|
|
5
5
|
)
|
|
6
6
|
from kfinance.tool_calling.get_capitalization_from_identifier import GetCapitalizationFromIdentifier
|
|
7
7
|
from kfinance.tool_calling.get_cusip_from_ticker import GetCusipFromTicker
|
|
8
|
+
from kfinance.tool_calling.get_earnings import GetEarnings
|
|
8
9
|
from kfinance.tool_calling.get_earnings_call_datetimes_from_identifier import (
|
|
9
10
|
GetEarningsCallDatetimesFromIdentifier,
|
|
10
11
|
)
|
|
@@ -20,11 +21,14 @@ from kfinance.tool_calling.get_history_metadata_from_identifier import (
|
|
|
20
21
|
from kfinance.tool_calling.get_info_from_identifier import GetInfoFromIdentifier
|
|
21
22
|
from kfinance.tool_calling.get_isin_from_ticker import GetIsinFromTicker
|
|
22
23
|
from kfinance.tool_calling.get_latest import GetLatest
|
|
24
|
+
from kfinance.tool_calling.get_latest_earnings import GetLatestEarnings
|
|
23
25
|
from kfinance.tool_calling.get_n_quarters_ago import GetNQuartersAgo
|
|
26
|
+
from kfinance.tool_calling.get_next_earnings import GetNextEarnings
|
|
24
27
|
from kfinance.tool_calling.get_prices_from_identifier import GetPricesFromIdentifier
|
|
25
28
|
from kfinance.tool_calling.get_segments_from_identifier import (
|
|
26
29
|
GetSegmentsFromIdentifier,
|
|
27
30
|
)
|
|
31
|
+
from kfinance.tool_calling.get_transcript import GetTranscript
|
|
28
32
|
from kfinance.tool_calling.resolve_identifier import ResolveIdentifier
|
|
29
33
|
from kfinance.tool_calling.shared_models import KfinanceTool
|
|
30
34
|
|
|
@@ -36,6 +40,10 @@ ALL_TOOLS: list[Type[KfinanceTool]] = [
|
|
|
36
40
|
GetCusipFromTicker,
|
|
37
41
|
GetInfoFromIdentifier,
|
|
38
42
|
GetEarningsCallDatetimesFromIdentifier,
|
|
43
|
+
GetEarnings,
|
|
44
|
+
GetLatestEarnings,
|
|
45
|
+
GetNextEarnings,
|
|
46
|
+
GetTranscript,
|
|
39
47
|
GetHistoryMetadataFromIdentifier,
|
|
40
48
|
GetPricesFromIdentifier,
|
|
41
49
|
GetCapitalizationFromIdentifier,
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from typing import Type
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
|
|
5
|
+
from kfinance.constants import Permission
|
|
6
|
+
from kfinance.kfinance import NoEarningsDataError
|
|
7
|
+
from kfinance.tool_calling.shared_models import KfinanceTool, ToolArgsWithIdentifier
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class GetEarnings(KfinanceTool):
|
|
11
|
+
name: str = "get_earnings"
|
|
12
|
+
description: str = "Get all earnings for a given identifier. Returns a list of dictionaries, each with 'name' (str), 'key_dev_id' (int), and 'datetime' (str in ISO 8601 format with UTC timezone) attributes."
|
|
13
|
+
args_schema: Type[BaseModel] = ToolArgsWithIdentifier
|
|
14
|
+
required_permission: Permission | None = Permission.EarningsPermission
|
|
15
|
+
|
|
16
|
+
def _run(self, identifier: str) -> list[dict]:
|
|
17
|
+
ticker = self.kfinance_client.ticker(identifier)
|
|
18
|
+
earnings = ticker.company.earnings()
|
|
19
|
+
|
|
20
|
+
if not earnings:
|
|
21
|
+
raise NoEarningsDataError(f"Earnings for {identifier} not found")
|
|
22
|
+
|
|
23
|
+
return [
|
|
24
|
+
{
|
|
25
|
+
"name": earnings_item.name,
|
|
26
|
+
"key_dev_id": earnings_item.key_dev_id,
|
|
27
|
+
"datetime": earnings_item.datetime.isoformat(),
|
|
28
|
+
}
|
|
29
|
+
for earnings_item in earnings
|
|
30
|
+
]
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from typing import Type
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
|
|
5
|
+
from kfinance.constants import Permission
|
|
6
|
+
from kfinance.kfinance import NoEarningsDataError
|
|
7
|
+
from kfinance.tool_calling.shared_models import KfinanceTool, ToolArgsWithIdentifier
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class GetLatestEarnings(KfinanceTool):
|
|
11
|
+
name: str = "get_latest_earnings"
|
|
12
|
+
description: str = "Get the latest earnings for a given identifier. Returns a dictionary with 'name' (str), 'key_dev_id' (int), and 'datetime' (str in ISO 8601 format with UTC timezone) attributes."
|
|
13
|
+
args_schema: Type[BaseModel] = ToolArgsWithIdentifier
|
|
14
|
+
required_permission: Permission | None = Permission.EarningsPermission
|
|
15
|
+
|
|
16
|
+
def _run(self, identifier: str) -> dict:
|
|
17
|
+
ticker = self.kfinance_client.ticker(identifier)
|
|
18
|
+
latest_earnings = ticker.company.latest_earnings
|
|
19
|
+
|
|
20
|
+
if latest_earnings is None:
|
|
21
|
+
raise NoEarningsDataError(f"Latest earnings for {identifier} not found")
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
"name": latest_earnings.name,
|
|
25
|
+
"key_dev_id": latest_earnings.key_dev_id,
|
|
26
|
+
"datetime": latest_earnings.datetime.isoformat(),
|
|
27
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from typing import Type
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
|
|
5
|
+
from kfinance.constants import Permission
|
|
6
|
+
from kfinance.kfinance import NoEarningsDataError
|
|
7
|
+
from kfinance.tool_calling.shared_models import KfinanceTool, ToolArgsWithIdentifier
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class GetNextEarnings(KfinanceTool):
|
|
11
|
+
name: str = "get_next_earnings"
|
|
12
|
+
description: str = "Get the next earnings for a given identifier. Returns a dictionary with 'name' (str), 'key_dev_id' (int), and 'datetime' (str in ISO 8601 format with UTC timezone) attributes."
|
|
13
|
+
args_schema: Type[BaseModel] = ToolArgsWithIdentifier
|
|
14
|
+
required_permission: Permission | None = Permission.EarningsPermission
|
|
15
|
+
|
|
16
|
+
def _run(self, identifier: str) -> dict:
|
|
17
|
+
ticker = self.kfinance_client.ticker(identifier)
|
|
18
|
+
next_earnings = ticker.company.next_earnings
|
|
19
|
+
|
|
20
|
+
if next_earnings is None:
|
|
21
|
+
raise NoEarningsDataError(f"Next earnings for {identifier} not found")
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
"name": next_earnings.name,
|
|
25
|
+
"key_dev_id": next_earnings.key_dev_id,
|
|
26
|
+
"datetime": next_earnings.datetime.isoformat(),
|
|
27
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from typing import Type
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, Field
|
|
4
|
+
|
|
5
|
+
from kfinance.constants import Permission
|
|
6
|
+
from kfinance.tool_calling.shared_models import KfinanceTool
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class GetTranscriptArgs(BaseModel):
|
|
10
|
+
"""Tool argument with a key_dev_id."""
|
|
11
|
+
|
|
12
|
+
key_dev_id: int = Field(description="The key dev ID for the earnings call")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class GetTranscript(KfinanceTool):
|
|
16
|
+
name: str = "get_transcript"
|
|
17
|
+
description: str = "Get the raw transcript text for an earnings call by key dev ID."
|
|
18
|
+
args_schema: Type[BaseModel] = GetTranscriptArgs
|
|
19
|
+
required_permission: Permission | None = Permission.EarningsPermission
|
|
20
|
+
|
|
21
|
+
def _run(self, key_dev_id: int) -> str:
|
|
22
|
+
transcript = self.kfinance_client.transcript(key_dev_id)
|
|
23
|
+
return transcript.raw
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from kfinance.tool_calling import GetFinancialLineItemFromIdentifier, GetLatest
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
BASE_PROMPT = f"""
|
|
5
|
+
You are an agent that calls one or more tools to retrieve data to answer questions from
|
|
6
|
+
financial analysts. Use the supplied tools to answer the user's questions.
|
|
7
|
+
|
|
8
|
+
- Always use the `{GetLatest.model_fields["name"].default}` function when asked about the last or most recent quarter or
|
|
9
|
+
when the time is unspecified in the question.
|
|
10
|
+
- Try to use `{GetFinancialLineItemFromIdentifier.model_fields["name"].default}` for questions about a company's
|
|
11
|
+
finances.
|
|
12
|
+
- If the tools do not respond with data that answers the question, then respond by saying that
|
|
13
|
+
you don't have the data available.
|
|
14
|
+
- Keep calling tools until you have the answer or the tool says the data is not available.
|
|
15
|
+
- Label large numbers with "million" or "billion" and currency symbols if appropriate.
|
|
16
|
+
"""
|
kfinance/version.py
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|