kensho-kfinance 1.2.0__py3-none-any.whl → 2.0.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-1.2.0.dist-info → kensho_kfinance-2.0.0.dist-info}/METADATA +13 -9
- kensho_kfinance-2.0.0.dist-info/RECORD +40 -0
- {kensho_kfinance-1.2.0.dist-info → kensho_kfinance-2.0.0.dist-info}/WHEEL +1 -1
- kfinance/CHANGELOG.md +10 -0
- kfinance/constants.py +51 -8
- kfinance/fetch.py +68 -29
- kfinance/kfinance.py +128 -38
- kfinance/meta_classes.py +3 -9
- kfinance/tests/test_fetch.py +7 -26
- kfinance/tests/test_tools.py +430 -0
- kfinance/tool_calling/README.md +38 -0
- kfinance/tool_calling/__init__.py +47 -0
- kfinance/tool_calling/get_business_relationship_from_identifier.py +28 -0
- kfinance/tool_calling/get_capitalization_from_identifier.py +35 -0
- kfinance/tool_calling/get_company_id_from_identifier.py +14 -0
- kfinance/tool_calling/get_cusip_from_ticker.py +18 -0
- kfinance/tool_calling/get_earnings_call_datetimes_from_identifier.py +17 -0
- kfinance/tool_calling/get_financial_line_item_from_identifier.py +45 -0
- kfinance/tool_calling/get_financial_statement_from_identifier.py +41 -0
- kfinance/tool_calling/get_history_metadata_from_identifier.py +15 -0
- kfinance/tool_calling/get_info_from_identifier.py +14 -0
- kfinance/tool_calling/get_isin_from_ticker.py +18 -0
- kfinance/tool_calling/get_latest.py +21 -0
- kfinance/tool_calling/get_n_quarters_ago.py +21 -0
- kfinance/tool_calling/get_prices_from_identifier.py +44 -0
- kfinance/tool_calling/get_security_id_from_identifier.py +14 -0
- kfinance/tool_calling/get_trading_item_id_from_identifier.py +14 -0
- kfinance/tool_calling/shared_models.py +53 -0
- kfinance/version.py +2 -2
- kensho_kfinance-1.2.0.dist-info/RECORD +0 -23
- kfinance/llm_tools.py +0 -747
- kfinance/tool_schemas.py +0 -148
- {kensho_kfinance-1.2.0.dist-info → kensho_kfinance-2.0.0.dist-info}/licenses/AUTHORS.md +0 -0
- {kensho_kfinance-1.2.0.dist-info → kensho_kfinance-2.0.0.dist-info}/licenses/LICENSE +0 -0
- {kensho_kfinance-1.2.0.dist-info → kensho_kfinance-2.0.0.dist-info}/top_level.txt +0 -0
kfinance/meta_classes.py
CHANGED
|
@@ -6,7 +6,7 @@ from typing import TYPE_CHECKING, Any, Callable, Literal, Optional
|
|
|
6
6
|
import numpy as np
|
|
7
7
|
import pandas as pd
|
|
8
8
|
|
|
9
|
-
from .constants import LINE_ITEMS, BusinessRelationshipType
|
|
9
|
+
from .constants import LINE_ITEMS, BusinessRelationshipType, PeriodType
|
|
10
10
|
from .fetch import KFinanceApiClient
|
|
11
11
|
|
|
12
12
|
|
|
@@ -27,7 +27,6 @@ class CompanyFunctionsMetaClass:
|
|
|
27
27
|
|
|
28
28
|
def validate_inputs(
|
|
29
29
|
self,
|
|
30
|
-
period_type: Optional[str] = None,
|
|
31
30
|
start_year: Optional[int] = None,
|
|
32
31
|
end_year: Optional[int] = None,
|
|
33
32
|
start_quarter: Optional[int] = None,
|
|
@@ -35,9 +34,6 @@ class CompanyFunctionsMetaClass:
|
|
|
35
34
|
) -> None:
|
|
36
35
|
"""Test the time inputs for validity."""
|
|
37
36
|
|
|
38
|
-
if period_type not in {"annual", "quarterly", "ytd", "ltm", None}:
|
|
39
|
-
raise RuntimeError(f"Period type {period_type} is not valid.")
|
|
40
|
-
|
|
41
37
|
if start_year and (start_year > datetime.now().year):
|
|
42
38
|
raise ValueError("start_year is in the future")
|
|
43
39
|
|
|
@@ -54,7 +50,7 @@ class CompanyFunctionsMetaClass:
|
|
|
54
50
|
def statement(
|
|
55
51
|
self,
|
|
56
52
|
statement_type: str,
|
|
57
|
-
period_type: Optional[
|
|
53
|
+
period_type: Optional[PeriodType] = None,
|
|
58
54
|
start_year: Optional[int] = None,
|
|
59
55
|
end_year: Optional[int] = None,
|
|
60
56
|
start_quarter: Optional[int] = None,
|
|
@@ -63,7 +59,6 @@ class CompanyFunctionsMetaClass:
|
|
|
63
59
|
"""Get the company's financial statement"""
|
|
64
60
|
try:
|
|
65
61
|
self.validate_inputs(
|
|
66
|
-
period_type=period_type,
|
|
67
62
|
start_year=start_year,
|
|
68
63
|
end_year=end_year,
|
|
69
64
|
start_quarter=start_quarter,
|
|
@@ -182,7 +177,7 @@ class CompanyFunctionsMetaClass:
|
|
|
182
177
|
def line_item(
|
|
183
178
|
self,
|
|
184
179
|
line_item: str,
|
|
185
|
-
period_type: Optional[
|
|
180
|
+
period_type: Optional[PeriodType] = None,
|
|
186
181
|
start_year: Optional[int] = None,
|
|
187
182
|
end_year: Optional[int] = None,
|
|
188
183
|
start_quarter: Optional[int] = None,
|
|
@@ -191,7 +186,6 @@ class CompanyFunctionsMetaClass:
|
|
|
191
186
|
"""Get a DataFrame of a financial line item according to the date ranges."""
|
|
192
187
|
try:
|
|
193
188
|
self.validate_inputs(
|
|
194
|
-
period_type=period_type,
|
|
195
189
|
start_year=start_year,
|
|
196
190
|
end_year=end_year,
|
|
197
191
|
start_quarter=start_quarter,
|
kfinance/tests/test_fetch.py
CHANGED
|
@@ -3,6 +3,7 @@ from unittest.mock import Mock
|
|
|
3
3
|
|
|
4
4
|
import pytest
|
|
5
5
|
|
|
6
|
+
from kfinance.constants import Periodicity, PeriodType
|
|
6
7
|
from kfinance.fetch import KFinanceApiClient
|
|
7
8
|
|
|
8
9
|
|
|
@@ -50,8 +51,8 @@ class TestFetchItem(TestCase):
|
|
|
50
51
|
start_date = "2025-01-01"
|
|
51
52
|
end_date = "2025-01-31"
|
|
52
53
|
is_adjusted = False
|
|
53
|
-
periodicity =
|
|
54
|
-
expected_fetch_url = f"{self.kfinance_api_client.url_base}pricing/{trading_item_id}/{start_date}/{end_date}/{periodicity}/unadjusted"
|
|
54
|
+
periodicity = Periodicity.day
|
|
55
|
+
expected_fetch_url = f"{self.kfinance_api_client.url_base}pricing/{trading_item_id}/{start_date}/{end_date}/{periodicity.value}/unadjusted"
|
|
55
56
|
self.kfinance_api_client.fetch_history(
|
|
56
57
|
trading_item_id=trading_item_id,
|
|
57
58
|
is_adjusted=is_adjusted,
|
|
@@ -77,12 +78,12 @@ class TestFetchItem(TestCase):
|
|
|
77
78
|
company_id=company_id, statement_type=statement_type
|
|
78
79
|
)
|
|
79
80
|
self.kfinance_api_client.fetch.assert_called_with(expected_fetch_url)
|
|
80
|
-
period_type =
|
|
81
|
+
period_type = PeriodType.quarterly
|
|
81
82
|
start_year = 2024
|
|
82
83
|
end_year = 2024
|
|
83
84
|
start_quarter = 1
|
|
84
85
|
end_quarter = 4
|
|
85
|
-
expected_fetch_url = f"{self.kfinance_api_client.url_base}statements/{company_id}/{statement_type}/{period_type}/{start_year}/{end_year}/{start_quarter}/{end_quarter}"
|
|
86
|
+
expected_fetch_url = f"{self.kfinance_api_client.url_base}statements/{company_id}/{statement_type}/{period_type.value}/{start_year}/{end_year}/{start_quarter}/{end_quarter}"
|
|
86
87
|
self.kfinance_api_client.fetch_statement(
|
|
87
88
|
company_id=company_id,
|
|
88
89
|
statement_type=statement_type,
|
|
@@ -100,12 +101,12 @@ class TestFetchItem(TestCase):
|
|
|
100
101
|
expected_fetch_url = f"{self.kfinance_api_client.url_base}line_item/{company_id}/{line_item}/none/none/none/none/none"
|
|
101
102
|
self.kfinance_api_client.fetch_line_item(company_id=company_id, line_item=line_item)
|
|
102
103
|
self.kfinance_api_client.fetch.assert_called_with(expected_fetch_url)
|
|
103
|
-
period_type =
|
|
104
|
+
period_type = PeriodType.quarterly
|
|
104
105
|
start_year = 2024
|
|
105
106
|
end_year = 2024
|
|
106
107
|
start_quarter = 1
|
|
107
108
|
end_quarter = 4
|
|
108
|
-
expected_fetch_url = f"{self.kfinance_api_client.url_base}line_item/{company_id}/{line_item}/{period_type}/{start_year}/{end_year}/{start_quarter}/{end_quarter}"
|
|
109
|
+
expected_fetch_url = f"{self.kfinance_api_client.url_base}line_item/{company_id}/{line_item}/{period_type.value}/{start_year}/{end_year}/{start_quarter}/{end_quarter}"
|
|
109
110
|
self.kfinance_api_client.fetch_line_item(
|
|
110
111
|
company_id=company_id,
|
|
111
112
|
line_item=line_item,
|
|
@@ -157,26 +158,6 @@ class TestFetchItem(TestCase):
|
|
|
157
158
|
)
|
|
158
159
|
self.kfinance_api_client.fetch.assert_called_with(expected_fetch_url)
|
|
159
160
|
|
|
160
|
-
def test_fetch_ticker_industry_simple_groups(self) -> None:
|
|
161
|
-
simple_industry = "media"
|
|
162
|
-
expected_fetch_url = (
|
|
163
|
-
f"{self.kfinance_api_client.url_base}ticker_groups/industry/simple/{simple_industry}"
|
|
164
|
-
)
|
|
165
|
-
self.kfinance_api_client.fetch_ticker_simple_industry_groups(
|
|
166
|
-
simple_industry=simple_industry
|
|
167
|
-
)
|
|
168
|
-
self.kfinance_api_client.fetch.assert_called_once_with(expected_fetch_url)
|
|
169
|
-
|
|
170
|
-
def test_fetch_company_industry_simple_groups(self) -> None:
|
|
171
|
-
simple_industry = "media"
|
|
172
|
-
expected_fetch_url = (
|
|
173
|
-
f"{self.kfinance_api_client.url_base}company_groups/industry/simple/{simple_industry}"
|
|
174
|
-
)
|
|
175
|
-
self.kfinance_api_client.fetch_company_simple_industry_groups(
|
|
176
|
-
simple_industry=simple_industry
|
|
177
|
-
)
|
|
178
|
-
self.kfinance_api_client.fetch.assert_called_once_with(expected_fetch_url)
|
|
179
|
-
|
|
180
161
|
def test_fetch_ticker_exchange_groups(self) -> None:
|
|
181
162
|
exchange_code = "NYSE"
|
|
182
163
|
expected_fetch_url = (
|
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
from datetime import date, datetime
|
|
2
|
+
|
|
3
|
+
from langchain_core.utils.function_calling import convert_to_openai_tool
|
|
4
|
+
import pytest
|
|
5
|
+
from requests_mock import Mocker
|
|
6
|
+
import time_machine
|
|
7
|
+
|
|
8
|
+
from kfinance.constants import BusinessRelationshipType, Capitalization, StatementType
|
|
9
|
+
from kfinance.kfinance import Client
|
|
10
|
+
from kfinance.tool_calling import (
|
|
11
|
+
GetCompanyIdFromIdentifier,
|
|
12
|
+
GetEarningsCallDatetimesFromIdentifier,
|
|
13
|
+
GetFinancialLineItemFromIdentifier,
|
|
14
|
+
GetFinancialStatementFromIdentifier,
|
|
15
|
+
GetHistoryMetadataFromIdentifier,
|
|
16
|
+
GetInfoFromIdentifier,
|
|
17
|
+
GetIsinFromTicker,
|
|
18
|
+
GetLatest,
|
|
19
|
+
GetNQuartersAgo,
|
|
20
|
+
GetPricesFromIdentifier,
|
|
21
|
+
GetSecurityIdFromIdentifier,
|
|
22
|
+
GetTradingItemIdFromIdentifier,
|
|
23
|
+
)
|
|
24
|
+
from kfinance.tool_calling.get_business_relationship_from_identifier import (
|
|
25
|
+
GetBusinessRelationshipFromIdentifier,
|
|
26
|
+
GetBusinessRelationshipFromIdentifierArgs,
|
|
27
|
+
)
|
|
28
|
+
from kfinance.tool_calling.get_capitalization_from_identifier import (
|
|
29
|
+
GetCapitalizationFromIdentifier,
|
|
30
|
+
GetCapitalizationFromIdentifierArgs,
|
|
31
|
+
)
|
|
32
|
+
from kfinance.tool_calling.get_cusip_from_ticker import GetCusipFromTicker, GetCusipFromTickerArgs
|
|
33
|
+
from kfinance.tool_calling.get_financial_line_item_from_identifier import (
|
|
34
|
+
GetFinancialLineItemFromIdentifierArgs,
|
|
35
|
+
)
|
|
36
|
+
from kfinance.tool_calling.get_financial_statement_from_identifier import (
|
|
37
|
+
GetFinancialStatementFromIdentifierArgs,
|
|
38
|
+
)
|
|
39
|
+
from kfinance.tool_calling.get_isin_from_ticker import GetIsinFromTickerArgs
|
|
40
|
+
from kfinance.tool_calling.get_latest import GetLatestArgs
|
|
41
|
+
from kfinance.tool_calling.get_n_quarters_ago import GetNQuartersAgoArgs
|
|
42
|
+
from kfinance.tool_calling.get_prices_from_identifier import GetPricesFromIdentifierArgs
|
|
43
|
+
from kfinance.tool_calling.shared_models import ToolArgsWithIdentifier
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
SPGI_COMPANY_ID = 21719
|
|
47
|
+
SPGI_SECURITY_ID = 2629107
|
|
48
|
+
SPGI_TRADING_ITEM_ID = 2629108
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@pytest.fixture
|
|
52
|
+
def mock_client(requests_mock: Mocker) -> Client:
|
|
53
|
+
"""Create a KFinanceApiClient with a mock response for the SPGI id triple."""
|
|
54
|
+
|
|
55
|
+
client = Client(refresh_token="foo")
|
|
56
|
+
# Set access token so that the client doesn't try to fetch it.
|
|
57
|
+
client.kfinance_api_client._access_token = "foo" # noqa: SLF001
|
|
58
|
+
client.kfinance_api_client._access_token_expiry = datetime(2100, 1, 1).timestamp() # noqa: SLF001
|
|
59
|
+
|
|
60
|
+
# Create a mock for the SPGI id triple.
|
|
61
|
+
requests_mock.get(
|
|
62
|
+
url="https://kfinance.kensho.com/api/v1/id/SPGI",
|
|
63
|
+
json={
|
|
64
|
+
"trading_item_id": SPGI_TRADING_ITEM_ID,
|
|
65
|
+
"security_id": SPGI_SECURITY_ID,
|
|
66
|
+
"company_id": SPGI_COMPANY_ID,
|
|
67
|
+
},
|
|
68
|
+
)
|
|
69
|
+
return client
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class TestGetBusinessRelationshipFromIdentifier:
|
|
73
|
+
def test_get_business_relationship_from_identifier(
|
|
74
|
+
self, requests_mock: Mocker, mock_client: Client
|
|
75
|
+
):
|
|
76
|
+
"""
|
|
77
|
+
GIVEN the GetBusinessRelationshipFromIdentifier tool
|
|
78
|
+
WHEN we request SPGI suppliers
|
|
79
|
+
THEN we get back the SPGI suppliers
|
|
80
|
+
"""
|
|
81
|
+
supplier_resp = {"current": [883103], "previous": [472898, 8182358]}
|
|
82
|
+
|
|
83
|
+
requests_mock.get(
|
|
84
|
+
url=f"https://kfinance.kensho.com/api/v1/relationship/{SPGI_COMPANY_ID}/supplier",
|
|
85
|
+
json=supplier_resp,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
tool = GetBusinessRelationshipFromIdentifier(kfinance_client=mock_client)
|
|
89
|
+
args = GetBusinessRelationshipFromIdentifierArgs(
|
|
90
|
+
identifier="SPGI", business_relationship=BusinessRelationshipType.supplier
|
|
91
|
+
)
|
|
92
|
+
resp = tool.run(args.model_dump(mode="json"))
|
|
93
|
+
# Companies is a set, so we have to sort the result
|
|
94
|
+
resp["previous"].sort()
|
|
95
|
+
assert resp == supplier_resp
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class TestGetCapitalizationFromIdentifier:
|
|
99
|
+
def test_get_capitalization_from_identifier(self, requests_mock: Mocker, mock_client: Client):
|
|
100
|
+
"""
|
|
101
|
+
GIVEN the GetCapitalizationFromIdentifier tool
|
|
102
|
+
WHEN we request the SPGI market cap
|
|
103
|
+
THEN we get back the SPGI market cap
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
requests_mock.get(
|
|
107
|
+
url=f"https://kfinance.kensho.com/api/v1/market_cap/{SPGI_COMPANY_ID}/none/none",
|
|
108
|
+
json={
|
|
109
|
+
"market_caps": [
|
|
110
|
+
{
|
|
111
|
+
"date": "2024-04-10",
|
|
112
|
+
"market_cap": "132766738270.000000",
|
|
113
|
+
"tev": "147455738270.000000",
|
|
114
|
+
"shares_outstanding": 313099562,
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
"date": "2024-04-11",
|
|
118
|
+
"market_cap": "132416066761.000000",
|
|
119
|
+
"tev": "147105066761.000000",
|
|
120
|
+
"shares_outstanding": 313099562,
|
|
121
|
+
},
|
|
122
|
+
]
|
|
123
|
+
},
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
expected_response = "| date | market_cap |\n|:-----------|-------------:|\n| 2024-04-10 | 1.32767e+11 |\n| 2024-04-11 | 1.32416e+11 |"
|
|
127
|
+
|
|
128
|
+
tool = GetCapitalizationFromIdentifier(kfinance_client=mock_client)
|
|
129
|
+
args = GetCapitalizationFromIdentifierArgs(
|
|
130
|
+
identifier="SPGI", capitalization=Capitalization.market_cap
|
|
131
|
+
)
|
|
132
|
+
response = tool.run(args.model_dump(mode="json"))
|
|
133
|
+
assert response == expected_response
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class TestGetCompanyIdFromIdentifier:
|
|
137
|
+
def test_get_company_id_from_identifier(self, mock_client: Client):
|
|
138
|
+
"""
|
|
139
|
+
GIVEN the GetCompanyIdFromIdentifier tool
|
|
140
|
+
WHEN request the company id for SPGI
|
|
141
|
+
THEN we get back the SPGI company id
|
|
142
|
+
"""
|
|
143
|
+
tool = GetCompanyIdFromIdentifier(kfinance_client=mock_client)
|
|
144
|
+
resp = tool.run(ToolArgsWithIdentifier(identifier="SPGI").model_dump(mode="json"))
|
|
145
|
+
assert resp == SPGI_COMPANY_ID
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class TestGetCusipFromTicker:
|
|
149
|
+
def test_get_cusip_from_ticker(self, requests_mock: Mocker, mock_client: Client):
|
|
150
|
+
"""
|
|
151
|
+
GIVEN the GetCusipFromTicker tool
|
|
152
|
+
WHEN we pass args with the SPGI ticker
|
|
153
|
+
THEN we get back the SPGI cusip
|
|
154
|
+
"""
|
|
155
|
+
|
|
156
|
+
spgi_cusip = "78409V104"
|
|
157
|
+
requests_mock.get(
|
|
158
|
+
url=f"https://kfinance.kensho.com/api/v1/cusip/{SPGI_SECURITY_ID}",
|
|
159
|
+
json={"cusip": spgi_cusip},
|
|
160
|
+
)
|
|
161
|
+
tool = GetCusipFromTicker(kfinance_client=mock_client)
|
|
162
|
+
resp = tool.run(GetCusipFromTickerArgs(ticker_str="SPGI").model_dump(mode="json"))
|
|
163
|
+
assert resp == spgi_cusip
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class TestGetEarningsCallDatetimesFromTicker:
|
|
167
|
+
def test_get_earnings_call_datetimes_from_ticker(
|
|
168
|
+
self, requests_mock: Mocker, mock_client: Client
|
|
169
|
+
):
|
|
170
|
+
"""
|
|
171
|
+
GIVEN the GetEarningsCallDatetimesFromIdentifier tool
|
|
172
|
+
WHEN we request earnings call datetimes for SPGI
|
|
173
|
+
THEN we get back the expected SPGI earnings call datetimes
|
|
174
|
+
"""
|
|
175
|
+
|
|
176
|
+
requests_mock.get(
|
|
177
|
+
url=f"https://kfinance.kensho.com/api/v1/earnings/{SPGI_COMPANY_ID}/dates",
|
|
178
|
+
json={"earnings": ["2025-04-29T12:30:00", "2025-02-11T13:30:00"]},
|
|
179
|
+
)
|
|
180
|
+
expected_response = '["2025-04-29T12:30:00+00:00", "2025-02-11T13:30:00+00:00"]'
|
|
181
|
+
|
|
182
|
+
tool = GetEarningsCallDatetimesFromIdentifier(kfinance_client=mock_client)
|
|
183
|
+
response = tool.run(ToolArgsWithIdentifier(identifier="SPGI").model_dump(mode="json"))
|
|
184
|
+
assert response == expected_response
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
class TestGetFinancialLineItemFromIdentifier:
|
|
188
|
+
def test_get_financial_line_item_from_identifier(
|
|
189
|
+
self, mock_client: Client, requests_mock: Mocker
|
|
190
|
+
):
|
|
191
|
+
"""
|
|
192
|
+
GIVEN the GetFinancialLineItemFromIdentifier tool
|
|
193
|
+
WHEN we request SPGI revenue
|
|
194
|
+
THEN we get back the SPGI revenue
|
|
195
|
+
"""
|
|
196
|
+
|
|
197
|
+
requests_mock.get(
|
|
198
|
+
url=f"https://kfinance.kensho.com/api/v1/line_item/{SPGI_COMPANY_ID}/revenue/none/none/none/none/none",
|
|
199
|
+
json={
|
|
200
|
+
"line_item": {
|
|
201
|
+
"2020": "7442000000.000000",
|
|
202
|
+
"2021": "8297000000.000000",
|
|
203
|
+
"2022": "11181000000.000000",
|
|
204
|
+
"2023": "12497000000.000000",
|
|
205
|
+
"2024": "14208000000.000000",
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
)
|
|
209
|
+
expected_response = "| | 2020 | 2021 | 2022 | 2023 | 2024 |\n|:--------|----------:|----------:|-----------:|-----------:|-----------:|\n| revenue | 7.442e+09 | 8.297e+09 | 1.1181e+10 | 1.2497e+10 | 1.4208e+10 |"
|
|
210
|
+
|
|
211
|
+
tool = GetFinancialLineItemFromIdentifier(kfinance_client=mock_client)
|
|
212
|
+
args = GetFinancialLineItemFromIdentifierArgs(identifier="SPGI", line_item="revenue")
|
|
213
|
+
response = tool.run(args.model_dump(mode="json"))
|
|
214
|
+
assert response == expected_response
|
|
215
|
+
|
|
216
|
+
def test_line_items_and_aliases_included_in_schema(self, mock_client: Client):
|
|
217
|
+
"""
|
|
218
|
+
GIVEN a GetFinancialLineItemFromIdentifier tool
|
|
219
|
+
WHEN we generate an openai schema from the tool
|
|
220
|
+
THEN all line items and aliases are included in the line item enum
|
|
221
|
+
"""
|
|
222
|
+
tool = GetFinancialLineItemFromIdentifier(kfinance_client=mock_client)
|
|
223
|
+
oai_schema = convert_to_openai_tool(tool)
|
|
224
|
+
line_items = oai_schema["function"]["parameters"]["properties"]["line_item"]["enum"]
|
|
225
|
+
# revenue is a line item
|
|
226
|
+
assert "revenue" in line_items
|
|
227
|
+
# normal_revenue is an alias for revenue
|
|
228
|
+
assert "normal_revenue" in line_items
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
class TestGetFinancialStatementFromIdentifier:
|
|
232
|
+
def test_get_financial_statement_from_identifier(
|
|
233
|
+
self, mock_client: Client, requests_mock: Mocker
|
|
234
|
+
):
|
|
235
|
+
"""
|
|
236
|
+
GIVEN the GetFinancialLineItemFromIdentifier tool
|
|
237
|
+
WHEN we request the SPGI income statement
|
|
238
|
+
THEN we get back the SPGI income statement
|
|
239
|
+
"""
|
|
240
|
+
|
|
241
|
+
requests_mock.get(
|
|
242
|
+
url=f"https://kfinance.kensho.com/api/v1/statements/{SPGI_COMPANY_ID}/income_statement/none/none/none/none/none",
|
|
243
|
+
# truncated from the original API response
|
|
244
|
+
json={
|
|
245
|
+
"statements": {
|
|
246
|
+
"2020": {"Revenues": "7442000000.000000", "Total Revenues": "7442000000.000000"}
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
)
|
|
250
|
+
expected_response = "| | 2020 |\n|:---------------|----------:|\n| Revenues | 7.442e+09 |\n| Total Revenues | 7.442e+09 |"
|
|
251
|
+
|
|
252
|
+
tool = GetFinancialStatementFromIdentifier(kfinance_client=mock_client)
|
|
253
|
+
args = GetFinancialStatementFromIdentifierArgs(
|
|
254
|
+
identifier="SPGI", statement=StatementType.income_statement
|
|
255
|
+
)
|
|
256
|
+
response = tool.run(args.model_dump(mode="json"))
|
|
257
|
+
assert response == expected_response
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
class TestGetHistoryMetadataFromIdentifier:
|
|
261
|
+
def test_get_history_metadata_from_identifier(self, mock_client: Client, requests_mock: Mocker):
|
|
262
|
+
"""
|
|
263
|
+
GIVEN the GetHistoryMetadataFromIdentifier tool
|
|
264
|
+
WHEN request history metadata for SPGI
|
|
265
|
+
THEN we get back the SPGI history metadata
|
|
266
|
+
"""
|
|
267
|
+
|
|
268
|
+
metadata_resp = {
|
|
269
|
+
"currency": "USD",
|
|
270
|
+
"exchange_name": "NYSE",
|
|
271
|
+
"first_trade_date": "1968-01-02",
|
|
272
|
+
"instrument_type": "Equity",
|
|
273
|
+
"symbol": "SPGI",
|
|
274
|
+
}
|
|
275
|
+
expected_resp = {
|
|
276
|
+
"currency": "USD",
|
|
277
|
+
"exchange_name": "NYSE",
|
|
278
|
+
"first_trade_date": date(1968, 1, 2),
|
|
279
|
+
"instrument_type": "Equity",
|
|
280
|
+
"symbol": "SPGI",
|
|
281
|
+
}
|
|
282
|
+
requests_mock.get(
|
|
283
|
+
url=f"https://kfinance.kensho.com/api/v1/pricing/{SPGI_TRADING_ITEM_ID}/metadata",
|
|
284
|
+
json=metadata_resp,
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
tool = GetHistoryMetadataFromIdentifier(kfinance_client=mock_client)
|
|
288
|
+
resp = tool.run(ToolArgsWithIdentifier(identifier="SPGI").model_dump(mode="json"))
|
|
289
|
+
assert resp == expected_resp
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
class TestGetInfoFromIdentifier:
|
|
293
|
+
def test_get_info_from_identifier(self, mock_client: Client, requests_mock: Mocker):
|
|
294
|
+
"""
|
|
295
|
+
GIVEN the GetInfoFromIdentifier tool
|
|
296
|
+
WHEN request info for SPGI
|
|
297
|
+
THEN we get back info for SPGI
|
|
298
|
+
"""
|
|
299
|
+
|
|
300
|
+
# truncated from the original
|
|
301
|
+
info_resp = {"name": "S&P Global Inc.", "status": "Operating"}
|
|
302
|
+
requests_mock.get(
|
|
303
|
+
url=f"https://kfinance.kensho.com/api/v1/info/{SPGI_COMPANY_ID}",
|
|
304
|
+
json=info_resp,
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
tool = GetInfoFromIdentifier(kfinance_client=mock_client)
|
|
308
|
+
resp = tool.run(ToolArgsWithIdentifier(identifier="SPGI").model_dump(mode="json"))
|
|
309
|
+
assert resp == str(info_resp)
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
class TestGetIsinFromTicker:
|
|
313
|
+
def test_get_isin_from_ticker(self, requests_mock: Mocker, mock_client: Client):
|
|
314
|
+
"""
|
|
315
|
+
GIVEN the GetIsinFromTicker tool
|
|
316
|
+
WHEN we pass args with the SPGI ticker
|
|
317
|
+
THEN we get back the SPGI isin
|
|
318
|
+
"""
|
|
319
|
+
|
|
320
|
+
spgi_isin = "US78409V1044"
|
|
321
|
+
requests_mock.get(
|
|
322
|
+
url=f"https://kfinance.kensho.com/api/v1/isin/{SPGI_SECURITY_ID}",
|
|
323
|
+
json={"isin": spgi_isin},
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
tool = GetIsinFromTicker(kfinance_client=mock_client)
|
|
327
|
+
resp = tool.run(GetIsinFromTickerArgs(ticker_str="SPGI").model_dump(mode="json"))
|
|
328
|
+
assert resp == spgi_isin
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
class TestGetLatest:
|
|
332
|
+
@time_machine.travel(datetime(2025, 1, 1, 12, tzinfo=datetime.now().astimezone().tzinfo))
|
|
333
|
+
def test_get_latest(self, mock_client: Client):
|
|
334
|
+
"""
|
|
335
|
+
GIVEN the GetLatest tool
|
|
336
|
+
WHEN request latest info
|
|
337
|
+
THEN we get back latest info
|
|
338
|
+
"""
|
|
339
|
+
|
|
340
|
+
expected_resp = {
|
|
341
|
+
"annual": {"latest_year": 2024},
|
|
342
|
+
"now": {
|
|
343
|
+
"current_date": "2025-01-01",
|
|
344
|
+
"current_month": 1,
|
|
345
|
+
"current_quarter": 1,
|
|
346
|
+
"current_year": 2025,
|
|
347
|
+
},
|
|
348
|
+
"quarterly": {"latest_quarter": 4, "latest_year": 2024},
|
|
349
|
+
}
|
|
350
|
+
tool = GetLatest(kfinance_client=mock_client)
|
|
351
|
+
resp = tool.run(GetLatestArgs().model_dump(mode="json"))
|
|
352
|
+
assert resp == expected_resp
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
class TestGetNQuartersAgo:
|
|
356
|
+
@time_machine.travel(datetime(2025, 1, 1, 12, tzinfo=datetime.now().astimezone().tzinfo))
|
|
357
|
+
def test_get_n_quarters_ago(self, mock_client: Client):
|
|
358
|
+
"""
|
|
359
|
+
GIVEN the GetNQuartersAgo tool
|
|
360
|
+
WHEN we request 3 quarters ago
|
|
361
|
+
THEN we get back 3 quarters ago
|
|
362
|
+
"""
|
|
363
|
+
|
|
364
|
+
expected_resp = {"quarter": 2, "year": 2024}
|
|
365
|
+
tool = GetNQuartersAgo(kfinance_client=mock_client)
|
|
366
|
+
resp = tool.run(GetNQuartersAgoArgs(n=3).model_dump(mode="json"))
|
|
367
|
+
assert resp == expected_resp
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
class TestPricesFromIdentifier:
|
|
371
|
+
def test_get_prices_from_identifier(self, mock_client: Client, requests_mock: Mocker):
|
|
372
|
+
"""
|
|
373
|
+
GIVEN the GetPricesFromIdentifier tool
|
|
374
|
+
WHEN we request prices for SPGI
|
|
375
|
+
THEN we get back prices for SPGI
|
|
376
|
+
"""
|
|
377
|
+
|
|
378
|
+
requests_mock.get(
|
|
379
|
+
url=f"https://kfinance.kensho.com/api/v1/pricing/{SPGI_TRADING_ITEM_ID}/none/none/day/adjusted",
|
|
380
|
+
# truncated response
|
|
381
|
+
json={
|
|
382
|
+
"prices": [
|
|
383
|
+
{
|
|
384
|
+
"date": "2024-04-11",
|
|
385
|
+
"open": "424.260000",
|
|
386
|
+
"high": "425.990000",
|
|
387
|
+
"low": "422.040000",
|
|
388
|
+
"close": "422.920000",
|
|
389
|
+
"volume": "1129158",
|
|
390
|
+
},
|
|
391
|
+
{
|
|
392
|
+
"date": "2024-04-12",
|
|
393
|
+
"open": "419.230000",
|
|
394
|
+
"high": "421.940000",
|
|
395
|
+
"low": "416.450000",
|
|
396
|
+
"close": "417.810000",
|
|
397
|
+
"volume": "1182229",
|
|
398
|
+
},
|
|
399
|
+
]
|
|
400
|
+
},
|
|
401
|
+
)
|
|
402
|
+
expected_response = "| date | open | high | low | close | volume |\n|:-----------|-------:|-------:|-------:|--------:|------------:|\n| 2024-04-11 | 424.26 | 425.99 | 422.04 | 422.92 | 1.12916e+06 |\n| 2024-04-12 | 419.23 | 421.94 | 416.45 | 417.81 | 1.18223e+06 |"
|
|
403
|
+
|
|
404
|
+
tool = GetPricesFromIdentifier(kfinance_client=mock_client)
|
|
405
|
+
response = tool.run(GetPricesFromIdentifierArgs(identifier="SPGI").model_dump(mode="json"))
|
|
406
|
+
assert response == expected_response
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
class TestGetSecurityIdFromIdentifier:
|
|
410
|
+
def test_get_security_id_from_identifier(self, mock_client: Client):
|
|
411
|
+
"""
|
|
412
|
+
GIVEN the GetSecurityIdFromIdentifier tool
|
|
413
|
+
WHEN we request the security id for SPGI
|
|
414
|
+
THEN we get back the SPGI primary security id
|
|
415
|
+
"""
|
|
416
|
+
tool = GetSecurityIdFromIdentifier(kfinance_client=mock_client)
|
|
417
|
+
resp = tool.run(ToolArgsWithIdentifier(identifier="SPGI").model_dump(mode="json"))
|
|
418
|
+
assert resp == SPGI_SECURITY_ID
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
class TestGetTradingItemIdFromIdentifier:
|
|
422
|
+
def test_get_security_id_from_identifier(self, mock_client: Client):
|
|
423
|
+
"""
|
|
424
|
+
GIVEN the GetTradingItemIdFromIdentifier tool
|
|
425
|
+
WHEN we request the trading item id for SPGI
|
|
426
|
+
THEN we get back the SPGI primary trading item id
|
|
427
|
+
"""
|
|
428
|
+
tool = GetTradingItemIdFromIdentifier(kfinance_client=mock_client)
|
|
429
|
+
resp = tool.run(ToolArgsWithIdentifier(identifier="SPGI").model_dump(mode="json"))
|
|
430
|
+
assert resp == SPGI_TRADING_ITEM_ID
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Tool Calling
|
|
2
|
+
|
|
3
|
+
The tools defined in this directory are intended for tool calling.
|
|
4
|
+
|
|
5
|
+
Each tool is a subclass of [KfinanceTool](shared_models.py), which is
|
|
6
|
+
turn a subclass of `BaseTool` from langchain. We use langchain to convert these tools
|
|
7
|
+
into LLM-specific tool descriptions.
|
|
8
|
+
|
|
9
|
+
### KfinanceTool
|
|
10
|
+
Each `KfinanceTool` requires the following attributes to be defined:
|
|
11
|
+
- name: the function name
|
|
12
|
+
- description: the description of the function passed to an LLM. This should not include a description
|
|
13
|
+
of the arguments.
|
|
14
|
+
- args_schema: A pydantic model defining the input schema for the function (more on that below)
|
|
15
|
+
- _run: the source code for the tool.
|
|
16
|
+
|
|
17
|
+
All new tools have to be added to the `ALL_TOOLS` [list](__init__.py).
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
When initializing a `KfinanceTool`, it's always required to pass in an initialized Kfinance
|
|
21
|
+
`Client`, which can be accessed from within the tool as `self.kfinance_client`. This allows us to
|
|
22
|
+
make kfinance api calls without needing to get a kfinance client passed in with every call.
|
|
23
|
+
|
|
24
|
+
If tools are called from langchain, then langchain handles the deserialization of arguments before
|
|
25
|
+
calling the tools. If tools are called without langchain, then we have to handle that
|
|
26
|
+
deserialization ourselves. The deserialization step is handled by
|
|
27
|
+
`KfinanceTool.run_without_langchain`.
|
|
28
|
+
|
|
29
|
+
### args_schema Pydantic Model
|
|
30
|
+
Each `KfinanceTool` has a corresponding pydantic model that defines the call arguments for the tool.
|
|
31
|
+
We use langchain to convert these pydantic models into llm-specific argument schemas.
|
|
32
|
+
- Each field should contain a `description` and, where feasible, a `default`.
|
|
33
|
+
- Use of python objects is preferred over string types. For example enums, dates, and datetimes all
|
|
34
|
+
work.
|
|
35
|
+
- The order, type, and default of arguments in the `tool_args` has to match the order, type, and
|
|
36
|
+
default of arguments in the `_run` function.
|
|
37
|
+
- If your tool takes an `identifier` as its first argument, subclass `ToolArgsWithIdentifier` to
|
|
38
|
+
ensure that the description of the identifier remains consistent across tools.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from typing import Type
|
|
2
|
+
|
|
3
|
+
from kfinance.tool_calling.get_business_relationship_from_identifier import (
|
|
4
|
+
GetBusinessRelationshipFromIdentifier,
|
|
5
|
+
)
|
|
6
|
+
from kfinance.tool_calling.get_capitalization_from_identifier import GetCapitalizationFromIdentifier
|
|
7
|
+
from kfinance.tool_calling.get_company_id_from_identifier import GetCompanyIdFromIdentifier
|
|
8
|
+
from kfinance.tool_calling.get_cusip_from_ticker import GetCusipFromTicker
|
|
9
|
+
from kfinance.tool_calling.get_earnings_call_datetimes_from_identifier import (
|
|
10
|
+
GetEarningsCallDatetimesFromIdentifier,
|
|
11
|
+
)
|
|
12
|
+
from kfinance.tool_calling.get_financial_line_item_from_identifier import (
|
|
13
|
+
GetFinancialLineItemFromIdentifier,
|
|
14
|
+
)
|
|
15
|
+
from kfinance.tool_calling.get_financial_statement_from_identifier import (
|
|
16
|
+
GetFinancialStatementFromIdentifier,
|
|
17
|
+
)
|
|
18
|
+
from kfinance.tool_calling.get_history_metadata_from_identifier import (
|
|
19
|
+
GetHistoryMetadataFromIdentifier,
|
|
20
|
+
)
|
|
21
|
+
from kfinance.tool_calling.get_info_from_identifier import GetInfoFromIdentifier
|
|
22
|
+
from kfinance.tool_calling.get_isin_from_ticker import GetIsinFromTicker
|
|
23
|
+
from kfinance.tool_calling.get_latest import GetLatest
|
|
24
|
+
from kfinance.tool_calling.get_n_quarters_ago import GetNQuartersAgo
|
|
25
|
+
from kfinance.tool_calling.get_prices_from_identifier import GetPricesFromIdentifier
|
|
26
|
+
from kfinance.tool_calling.get_security_id_from_identifier import GetSecurityIdFromIdentifier
|
|
27
|
+
from kfinance.tool_calling.get_trading_item_id_from_identifier import GetTradingItemIdFromIdentifier
|
|
28
|
+
from kfinance.tool_calling.shared_models import KfinanceTool
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
ALL_TOOLS: list[Type[KfinanceTool]] = [
|
|
32
|
+
GetLatest,
|
|
33
|
+
GetNQuartersAgo,
|
|
34
|
+
GetCompanyIdFromIdentifier,
|
|
35
|
+
GetSecurityIdFromIdentifier,
|
|
36
|
+
GetTradingItemIdFromIdentifier,
|
|
37
|
+
GetIsinFromTicker,
|
|
38
|
+
GetCusipFromTicker,
|
|
39
|
+
GetInfoFromIdentifier,
|
|
40
|
+
GetEarningsCallDatetimesFromIdentifier,
|
|
41
|
+
GetHistoryMetadataFromIdentifier,
|
|
42
|
+
GetPricesFromIdentifier,
|
|
43
|
+
GetCapitalizationFromIdentifier,
|
|
44
|
+
GetFinancialStatementFromIdentifier,
|
|
45
|
+
GetFinancialLineItemFromIdentifier,
|
|
46
|
+
GetBusinessRelationshipFromIdentifier,
|
|
47
|
+
]
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from typing import Type
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
|
|
5
|
+
from kfinance.constants import BusinessRelationshipType
|
|
6
|
+
from kfinance.kfinance import BusinessRelationships
|
|
7
|
+
from kfinance.tool_calling.shared_models import KfinanceTool, ToolArgsWithIdentifier
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class GetBusinessRelationshipFromIdentifierArgs(ToolArgsWithIdentifier):
|
|
11
|
+
# no description because the description for enum fields comes from the enum docstring.
|
|
12
|
+
business_relationship: BusinessRelationshipType
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class GetBusinessRelationshipFromIdentifier(KfinanceTool):
|
|
16
|
+
name: str = "get_business_relationship_from_identifier"
|
|
17
|
+
description: str = 'Get the current and previous company IDs that are relationship_type of a given identifier. For example, "What are the current distributors of SPGI?" or "What are the previous borrowers of JPM?"'
|
|
18
|
+
args_schema: Type[BaseModel] = GetBusinessRelationshipFromIdentifierArgs
|
|
19
|
+
|
|
20
|
+
def _run(self, identifier: str, business_relationship: BusinessRelationshipType) -> dict:
|
|
21
|
+
ticker = self.kfinance_client.ticker(identifier)
|
|
22
|
+
business_relationship_obj: BusinessRelationships = getattr(
|
|
23
|
+
ticker, business_relationship.value
|
|
24
|
+
)
|
|
25
|
+
return {
|
|
26
|
+
"current": [company.company_id for company in business_relationship_obj.current],
|
|
27
|
+
"previous": [company.company_id for company in business_relationship_obj.previous],
|
|
28
|
+
}
|