kensho-kfinance 1.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.

kfinance/kfinance.py ADDED
@@ -0,0 +1,1224 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import date, datetime, timezone
4
+ from functools import cached_property
5
+ from io import BytesIO
6
+ import logging
7
+ import re
8
+ from sys import stdout
9
+ from typing import Iterable, NamedTuple, Optional
10
+ from urllib.parse import urljoin
11
+ import webbrowser
12
+
13
+ import numpy as np
14
+ import pandas as pd
15
+ from PIL.Image import Image, open as image_open
16
+
17
+ from .constants import HistoryMetadata, IdentificationTriple, LatestPeriods, YearAndQuarter
18
+ from .fetch import (
19
+ DEFAULT_API_HOST,
20
+ DEFAULT_API_VERSION,
21
+ DEFAULT_OKTA_AUTH_SERVER,
22
+ DEFAULT_OKTA_HOST,
23
+ KFinanceApiClient,
24
+ )
25
+ from .llm_tools import (
26
+ _llm_tools,
27
+ anthropic_tool_descriptions,
28
+ gemini_tool_descriptions,
29
+ get_latest,
30
+ get_n_quarters_ago,
31
+ langchain_tools,
32
+ openai_tool_descriptions,
33
+ )
34
+ from .meta_classes import CompanyFunctionsMetaClass, DelegatedCompanyFunctionsMetaClass
35
+ from .prompt import PROMPT
36
+ from .server_thread import ServerThread
37
+
38
+
39
+ logger = logging.getLogger(__name__)
40
+
41
+
42
+ class TradingItem:
43
+ """Trading Class
44
+
45
+ :param kfinance_api_client: The KFinanceApiClient used to fetch data
46
+ :type kfinance_api_client: KFinanceApiClient
47
+ :param trading_item_id: The S&P CIQ Trading Item ID
48
+ :type trading_item_id: int
49
+ """
50
+
51
+ def __init__(
52
+ self,
53
+ kfinance_api_client: KFinanceApiClient,
54
+ trading_item_id: int,
55
+ ):
56
+ """Initialize the trading item object
57
+
58
+ :param kfinance_api_client: The KFinanceApiClient used to fetch data
59
+ :type kfinance_api_client: KFinanceApiClient
60
+ :param trading_item_id: The S&P CIQ Trading Item ID
61
+ :type trading_item_id: int
62
+ """
63
+ self.kfinance_api_client = kfinance_api_client
64
+ self.trading_item_id = trading_item_id
65
+ self.ticker: Optional[str] = None
66
+ self.exchange_code: Optional[str] = None
67
+
68
+ def __str__(self) -> str:
69
+ """String representation for the company object"""
70
+ return f"{type(self).__module__}.{type(self).__qualname__} of {self.trading_item_id}"
71
+
72
+ @staticmethod
73
+ def from_ticker(
74
+ kfinance_api_client: KFinanceApiClient, ticker: str, exchange_code: Optional[str] = None
75
+ ) -> "TradingItem":
76
+ """Return TradingItem object from ticker
77
+
78
+ :param kfinance_api_client: The KFinanceApiClient used to fetch data
79
+ :type kfinance_api_client: KFinanceApiClient
80
+ :param ticker: the ticker symbol
81
+ :type ticker: str
82
+ :param exchange_code: The exchange code identifying which exchange the ticker is on.
83
+ :type exchange_code: str, optional
84
+ """
85
+ trading_item_id = kfinance_api_client.fetch_id_triple(ticker, exchange_code)[
86
+ "trading_item_id"
87
+ ]
88
+ trading_item = TradingItem(kfinance_api_client, trading_item_id)
89
+ trading_item.ticker = ticker
90
+ trading_item.exchange_code = exchange_code
91
+ return trading_item
92
+
93
+ @cached_property
94
+ def trading_item_id(self) -> int:
95
+ """Get the trading item id for the object
96
+
97
+ :return: the CIQ trading item id
98
+ :rtype: int
99
+ """
100
+ return self.trading_item_id
101
+
102
+ @cached_property
103
+ def history_metadata(self) -> HistoryMetadata:
104
+ """Get information about exchange and quotation
105
+
106
+ :return: A dict containing data about the currency, symbol, exchange, type of instrument, and the first trading date
107
+ :rtype: HistoryMetadata
108
+ """
109
+ metadata = self.kfinance_api_client.fetch_history_metadata(self.trading_item_id)
110
+ if self.exchange_code is None:
111
+ self.exchange_code = metadata["exchange_name"]
112
+ return {
113
+ "currency": metadata["currency"],
114
+ "symbol": metadata["symbol"],
115
+ "exchange_name": metadata["exchange_name"],
116
+ "instrument_type": metadata["instrument_type"],
117
+ "first_trade_date": datetime.strptime(metadata["first_trade_date"], "%Y-%m-%d").date(),
118
+ }
119
+
120
+ def history(
121
+ self,
122
+ periodicity: str = "day",
123
+ adjusted: bool = True,
124
+ start_date: Optional[str] = None,
125
+ end_date: Optional[str] = None,
126
+ ) -> pd.DataFrame:
127
+ """Retrieves the historical price data for a given asset over a specified date range.
128
+
129
+ :param str periodicity: Determines the frequency of the historical data returned. Options are "day", "week", "month" and "year". This default to "day"
130
+ :param Optional[bool] adjusted: Whether to retrieve adjusted prices that account for corporate actions such as dividends and splits, it defaults True
131
+ :param Optional[str] start_date: The start date for historical price retrieval in format "YYYY-MM-DD", default to None
132
+ :param Optional[str] end_date: The end date for historical price retrieval in format "YYYY-MM-DD", default to None
133
+ :return: A pd.DataFrame containing historical price data with columns corresponding to the specified periodicity, with Date as the index, and columns "open", "high", "low", "close", "volume" in type decimal. The Date index is a string that depends on the periodicity. If periodicity="day", the Date index is the day in format "YYYY-MM-DD", eg "2024-05-13" If periodicity="week", the Date index is the week number of the year in format "YYYY Week ##", eg "2024 Week 2" If periodicity="month", the Date index is the month name of the year in format "<Month> YYYY", eg "January 2024". If periodicity="year", the Date index is the year in format "YYYY", eg "2024".
134
+ :rtype: pd.DataFrame
135
+ """
136
+ if periodicity not in {"day", "week", "month", "year"}:
137
+ raise RuntimeError(f"Periodicity type {periodicity} is not valid.")
138
+
139
+ if start_date and end_date:
140
+ if (
141
+ datetime.strptime(start_date, "%Y-%m-%d").date()
142
+ > datetime.strptime(end_date, "%Y-%m-%d").date()
143
+ ):
144
+ return pd.DataFrame()
145
+
146
+ return (
147
+ pd.DataFrame(
148
+ self.kfinance_api_client.fetch_history(
149
+ trading_item_id=self.trading_item_id,
150
+ is_adjusted=adjusted,
151
+ start_date=start_date,
152
+ end_date=end_date,
153
+ periodicity=periodicity,
154
+ )["prices"]
155
+ )
156
+ .set_index("date")
157
+ .apply(pd.to_numeric)
158
+ .replace(np.nan, None)
159
+ )
160
+
161
+ def price_chart(
162
+ self,
163
+ periodicity: str = "day",
164
+ adjusted: bool = True,
165
+ start_date: Optional[str] = None,
166
+ end_date: Optional[str] = None,
167
+ ) -> Image:
168
+ """Get the price chart.
169
+
170
+ :param str periodicity: Determines the frequency of the historical data returned. Options are "day", "week", "month" and "year". This default to "day"
171
+ :param Optional[bool] adjusted: Whether to retrieve adjusted prices that account for corporate actions such as dividends and splits, it defaults True
172
+ :param Optional[str] start_date: The start date for historical price retrieval in format "YYYY-MM-DD", default to None
173
+ :param Optional[str] end_date: The end date for historical price retrieval in format "YYYY-MM-DD", default to None
174
+ :return: An image showing the price chart of the trading item
175
+ :rtype: Image
176
+ """
177
+
178
+ if periodicity not in {"day", "week", "month", "year"}:
179
+ raise RuntimeError(f"Periodicity type {periodicity} is not valid.")
180
+
181
+ if start_date and end_date:
182
+ if (
183
+ datetime.strptime(start_date, "%Y-%m-%d").date()
184
+ > datetime.strptime(end_date, "%Y-%m-%d").date()
185
+ ):
186
+ return pd.DataFrame()
187
+
188
+ content = self.kfinance_api_client.fetch_price_chart(
189
+ trading_item_id=self.trading_item_id,
190
+ is_adjusted=adjusted,
191
+ start_date=start_date,
192
+ end_date=end_date,
193
+ periodicity=periodicity,
194
+ )
195
+ image = image_open(BytesIO(content))
196
+ return image
197
+
198
+
199
+ class Company(CompanyFunctionsMetaClass):
200
+ """Company class
201
+
202
+ :param KFinanceApiClient kfinance_api_client: The KFinanceApiClient used to fetch data
203
+ :type kfinance_api_client: KFinanceApiClient
204
+ :param company_id: The S&P Global CIQ Company Id
205
+ :type company_id: int
206
+ """
207
+
208
+ def __init__(self, kfinance_api_client: KFinanceApiClient, company_id: int):
209
+ """Initialize the Company object
210
+
211
+ :param kfinance_api_client: The KFinanceApiClient used to fetch data
212
+ :type kfinance_api_client: KFinanceApiClient
213
+ :param company_id: The S&P Global CIQ Company Id
214
+ :type company_id: int
215
+ """
216
+ super().__init__()
217
+ self.kfinance_api_client = kfinance_api_client
218
+ self.company_id = company_id
219
+
220
+ def __str__(self) -> str:
221
+ """String representation for the company object"""
222
+ return f"{type(self).__module__}.{type(self).__qualname__} of {self.company_id}"
223
+
224
+ @cached_property
225
+ def primary_security(self) -> Security:
226
+ """Return the primary security item for the Company object
227
+
228
+ :return: a Security object of the primary security of company_id
229
+ :rtype: Security
230
+ """
231
+ primary_security_id = self.kfinance_api_client.fetch_primary_security(self.company_id)[
232
+ "primary_security"
233
+ ]
234
+ self.primary_security = Security(
235
+ kfinance_api_client=self.kfinance_api_client, security_id=primary_security_id
236
+ )
237
+ return self.primary_security
238
+
239
+ @cached_property
240
+ def securities(self) -> Securities:
241
+ """Return the security items for the Company object
242
+
243
+ :return: a Securities object containing the list of securities of company_id
244
+ :rtype: Securities
245
+ """
246
+ security_ids = self.kfinance_api_client.fetch_securities(self.company_id)["securities"]
247
+ self.securities = Securities(
248
+ kfinance_api_client=self.kfinance_api_client, security_ids=security_ids
249
+ )
250
+ return self.securities
251
+
252
+ @cached_property
253
+ def latest_earnings_call(self) -> None:
254
+ """Set and return the latest earnings call item for the object
255
+
256
+ :raises NotImplementedError: This function is not yet implemented
257
+ """
258
+ raise NotImplementedError(
259
+ "The latest earnings call property of company class not implemented yet"
260
+ )
261
+
262
+ @cached_property
263
+ def info(self) -> dict:
264
+ """Get the company info
265
+
266
+ :return: a dict with containing: name, status, type, simple industry, number of employees, founding date, webpage, address, city, zip code, state, country, & iso_country
267
+ :rtype: dict
268
+ """
269
+ self.info = self.kfinance_api_client.fetch_info(self.company_id)
270
+ return self.info
271
+
272
+ @property
273
+ def name(self) -> str:
274
+ """Get the company name
275
+
276
+ :return: The company name
277
+ :rtype: str
278
+ """
279
+ return self.info["name"]
280
+
281
+ @property
282
+ def status(self) -> str:
283
+ """Get the company status
284
+
285
+ :return: The company status
286
+ :rtype: str
287
+ """
288
+ return self.info["status"]
289
+
290
+ @property
291
+ def type(self) -> str:
292
+ """Get the type of company
293
+
294
+ :return: The company type
295
+ :rtype: str
296
+ """
297
+ return self.info["type"]
298
+
299
+ @property
300
+ def simple_industry(self) -> str:
301
+ """Get the simple industry for the company
302
+
303
+ :return: The company's simple_industry
304
+ :rtype: str
305
+ """
306
+ return self.info["simple_industry"]
307
+
308
+ @property
309
+ def number_of_employees(self) -> str:
310
+ """Get the number of employees the company has
311
+
312
+ :return: how many employees the company has
313
+ :rtype: str
314
+ """
315
+ return self.info["number_of_employees"]
316
+
317
+ @property
318
+ def founding_date(self) -> date:
319
+ """Get the founding date for the company
320
+
321
+ :return: founding date for the company
322
+ :rtype: date
323
+ """
324
+ return datetime.strptime(self.info["founding_date"], "%Y-%m-%d").date()
325
+
326
+ @property
327
+ def webpage(self) -> str:
328
+ """Get the webpage for the company
329
+
330
+ :return: webpage for the company
331
+ :rtype: str
332
+ """
333
+ return self.info["webpage"]
334
+
335
+ @property
336
+ def address(self) -> str:
337
+ """Get the address of the company's HQ
338
+
339
+ :return: address of the company's HQ
340
+ :rtype: str
341
+ """
342
+ return self.info["address"]
343
+
344
+ @property
345
+ def city(self) -> str:
346
+ """Get the city of the company's HQ
347
+
348
+ :return: city of the company's HQ
349
+ :rtype: str
350
+ """
351
+ return self.info["city"]
352
+
353
+ @property
354
+ def zip_code(self) -> str:
355
+ """Get the zip code of the company's HQ
356
+
357
+ :return: zip code of the company's HQ
358
+ :rtype: str
359
+ """
360
+ return self.info["zip_code"]
361
+
362
+ @property
363
+ def state(self) -> str:
364
+ """Get the state of the company's HQ
365
+
366
+ :return: state of the company's HQ
367
+ :rtype: str
368
+ """
369
+ return self.info["state"]
370
+
371
+ @property
372
+ def country(self) -> str:
373
+ """Get the country of the company's HQ
374
+
375
+ :return: country of the company's HQ
376
+ :rtype: str
377
+ """
378
+ return self.info["country"]
379
+
380
+ @property
381
+ def iso_country(self) -> str:
382
+ """Get the ISO code for the country of the company's HQ
383
+
384
+ :return: iso code for the country of the company's HQ
385
+ :rtype: str
386
+ """
387
+ return self.info["iso_country"]
388
+
389
+ @cached_property
390
+ def earnings_call_datetimes(self) -> list[datetime]:
391
+ """Get the datetimes of the companies earnings calls
392
+
393
+ :return: a list of datetimes for the companies earnings calls
394
+ :rtype: list[datetime]
395
+ """
396
+ return [
397
+ datetime.fromisoformat(earnings_call).replace(tzinfo=timezone.utc)
398
+ for earnings_call in self.kfinance_api_client.fetch_earnings_dates(self.company_id)[
399
+ "earnings"
400
+ ]
401
+ ]
402
+
403
+
404
+ class Security:
405
+ """Security class
406
+
407
+ :param kfinance_api_client: The KFinanceApiClient used to fetch data
408
+ :type kfinance_api_client: KFinanceApiClient
409
+ :param security_id: The S&P CIQ security id
410
+ :type security_id: int
411
+ """
412
+
413
+ def __init__(
414
+ self,
415
+ kfinance_api_client: KFinanceApiClient,
416
+ security_id: int,
417
+ ):
418
+ """Initialize the Security object.
419
+
420
+ :param KFinanceApiClient kfinance_api_client: The KFinanceApiClient used to fetch data
421
+ :type kfinance_api_client: KFinanceApiClient
422
+ :param int security_id: The CIQ security id
423
+ :type security_id: int
424
+ """
425
+ self.kfinance_api_client = kfinance_api_client
426
+ self.security_id = security_id
427
+
428
+ def __str__(self) -> str:
429
+ """String representation for the security object"""
430
+ return f"{type(self).__module__}.{type(self).__qualname__} of {self.security_id}"
431
+
432
+ @cached_property
433
+ def isin(self) -> str:
434
+ """Get the ISIN for the object
435
+
436
+ :return: The ISIN
437
+ :rtype: str
438
+ """
439
+ return self.kfinance_api_client.fetch_isin(self.security_id)["isin"]
440
+
441
+ @cached_property
442
+ def cusip(self) -> str:
443
+ """Get the CUSIP for the object
444
+
445
+ :return: The CUSIP
446
+ :rtype: str
447
+ """
448
+ return self.kfinance_api_client.fetch_cusip(self.security_id)["cusip"]
449
+
450
+ @cached_property
451
+ def primary_trading_item(self) -> TradingItem:
452
+ """Return the primary trading item for the Security object
453
+
454
+ :return: a TradingItem object of the primary trading item of security_id
455
+ :rtype: TradingItem
456
+ """
457
+ primary_trading_item_id = self.kfinance_api_client.fetch_primary_trading_item(
458
+ self.security_id
459
+ )["primary_trading_item"]
460
+ self.primary_trading_item = TradingItem(
461
+ kfinance_api_client=self.kfinance_api_client, trading_item_id=primary_trading_item_id
462
+ )
463
+ return self.primary_trading_item
464
+
465
+ @cached_property
466
+ def trading_items(self) -> TradingItems:
467
+ """Return the trading items for the Security object
468
+
469
+ :return: a TradingItems object containing the list of trading items of security_id
470
+ :rtype: TradingItems
471
+ """
472
+ trading_item_ids = self.kfinance_api_client.fetch_trading_items(self.security_id)[
473
+ "trading_items"
474
+ ]
475
+ self.trading_items = TradingItems(
476
+ kfinance_api_client=self.kfinance_api_client, trading_item_ids=trading_item_ids
477
+ )
478
+ return self.trading_items
479
+
480
+
481
+ class Ticker(DelegatedCompanyFunctionsMetaClass):
482
+ """Base Ticker class for accessing data on company
483
+
484
+ :param kfinance_api_client: The KFinanceApiClient used to fetch data
485
+ :type kfinance_api_client: KFinanceApiClient
486
+ :param exchange_code: The exchange code identifying which exchange the ticker is on
487
+ :type exchange_code: str, optional
488
+ """
489
+
490
+ def __init__(
491
+ self,
492
+ kfinance_api_client: KFinanceApiClient,
493
+ identifier: Optional[str] = None,
494
+ exchange_code: Optional[str] = None,
495
+ company_id: Optional[int] = None,
496
+ security_id: Optional[int] = None,
497
+ trading_item_id: Optional[int] = None,
498
+ ) -> None:
499
+ """Initialize the Ticker object. [identifier] can be a ticker, ISIN, or CUSIP. Identifier is prioritized over identification triple (company_id, security_id, & trading_item_id)
500
+
501
+ :param kfinance_api_client: The KFinanceApiClient used to fetch data
502
+ :type kfinance_api_client: KFinanceApiClient
503
+ :param identifier: The ticker symbol, ISIN, or CUSIP, default None
504
+ :type identifier: str, optional
505
+ :param exchange_code: The exchange code identifying which exchange the ticker is on. It is only needed if symbol is passed in and default None
506
+ :type exchange_code: str, optional
507
+ :param company_id: The S&P Global CIQ Company Id, defaults None
508
+ :type company_id: int, optional
509
+ :param security_id: The S&P Global CIQ Security Id, default None
510
+ :type security_id: int, optional
511
+ :param trading_item_id: The S&P Global CIQ Trading Item Id, default None
512
+ :type trading_item_id: int, optional
513
+ """
514
+ super().__init__()
515
+ self._identifier = identifier
516
+ self.kfinance_api_client = kfinance_api_client
517
+ self._ticker: Optional[str] = None
518
+ self.exchange_code: Optional[str] = exchange_code
519
+ self._isin: Optional[str] = None
520
+ self._cusip: Optional[str] = None
521
+ self._company_id: Optional[int] = None
522
+ self._security_id: Optional[int] = None
523
+ self._trading_item_id: Optional[int] = None
524
+ if self._identifier is not None:
525
+ if re.match("^[a-zA-Z]{2}[a-zA-Z0-9]{9}[0-9]{1}$", self._identifier): # Regex for ISIN
526
+ self._isin = self._identifier
527
+ elif re.match("^[a-zA-Z0-9]{9}$", self._identifier): # Regex for CUSIP
528
+ self._cusip = self._identifier
529
+ else:
530
+ self._ticker = self._identifier
531
+ elif company_id is not None and security_id is not None and trading_item_id is not None:
532
+ self._company_id = company_id
533
+ self._security_id = security_id
534
+ self._trading_item_id = trading_item_id
535
+ else:
536
+ raise RuntimeError(
537
+ "Neither an identifier nor an identification triple (company id, security id, & trading item id) were passed in"
538
+ )
539
+
540
+ def __str__(self) -> str:
541
+ """String representation for the ticker object"""
542
+ str_attributes = []
543
+ if self._ticker:
544
+ str_attributes.append(
545
+ f"{self.exchange_code + ':' if self.exchange_code else ''}{self.ticker}"
546
+ )
547
+ if self._isin:
548
+ str_attributes.append(self._isin)
549
+ if self._cusip:
550
+ str_attributes.append(str(self._cusip))
551
+ if self._company_id and self._security_id and self._trading_item_id:
552
+ str_attributes.append(
553
+ f"identification triple ({self._company_id}/{self._security_id}/{self._trading_item_id})"
554
+ )
555
+
556
+ return f"{type(self).__module__}.{type(self).__qualname__} of {', '.join(str_attributes)}"
557
+
558
+ def set_identification_triple(self) -> None:
559
+ """Get & set company_id, security_id, & trading_item_id for ticker with an exchange"""
560
+ if self._identifier is None:
561
+ raise RuntimeError(
562
+ "Ticker.set_identification_triple was called with a identifier set to None"
563
+ )
564
+ else:
565
+ id_triple = self.kfinance_api_client.fetch_id_triple(
566
+ self._identifier, self.exchange_code
567
+ )
568
+ self.company_id = id_triple["company_id"]
569
+ self.security_id = id_triple["security_id"]
570
+ self.trading_item_id = id_triple["trading_item_id"]
571
+
572
+ def set_company_id(self) -> int:
573
+ """Set the company id for the object
574
+
575
+ :return: the CIQ company id
576
+ :rtype: int
577
+ """
578
+ self.set_identification_triple()
579
+ return self.company_id
580
+
581
+ def set_security_id(self) -> int:
582
+ """Set the security id for the object
583
+
584
+ :return: the CIQ security id
585
+ :rtype: int
586
+ """
587
+ self.set_identification_triple()
588
+ return self.security_id
589
+
590
+ def set_trading_item_id(self) -> int:
591
+ """Set the trading item id for the object
592
+
593
+ :return: the CIQ trading item id
594
+ :rtype: int
595
+ """
596
+ self.set_identification_triple()
597
+ return self.trading_item_id
598
+
599
+ @cached_property
600
+ def company_id(self) -> int:
601
+ """Get the company id for the object
602
+
603
+ :return: the CIQ company id
604
+ :rtype: int
605
+ """
606
+ if self._company_id:
607
+ return self._company_id
608
+ return self.set_company_id()
609
+
610
+ @cached_property
611
+ def security_id(self) -> int:
612
+ """Get the CIQ security id for the object
613
+
614
+ :return: the CIQ security id
615
+ :rtype: int
616
+ """
617
+ if self._security_id:
618
+ return self._security_id
619
+ return self.set_security_id()
620
+
621
+ @cached_property
622
+ def trading_item_id(self) -> int:
623
+ """Get the CIQ trading item id for the object
624
+
625
+ :return: the CIQ trading item id
626
+ :rtype: int
627
+ """
628
+ if self._trading_item_id:
629
+ return self._trading_item_id
630
+ return self.set_trading_item_id()
631
+
632
+ @cached_property
633
+ def primary_security(self) -> Security:
634
+ """Set and return the primary security for the object
635
+
636
+ :return: The primary security as a Security object
637
+ :rtype: Security
638
+ """
639
+
640
+ self.primary_security = Security(
641
+ kfinance_api_client=self.kfinance_api_client, security_id=self.security_id
642
+ )
643
+ return self.primary_security
644
+
645
+ @cached_property
646
+ def company(self) -> Company:
647
+ """Set and return the company for the object
648
+
649
+ :return: The company returned as Company object
650
+ :rtype: Company
651
+ """
652
+ self.company = Company(
653
+ kfinance_api_client=self.kfinance_api_client, company_id=self.company_id
654
+ )
655
+ return self.company
656
+
657
+ @cached_property
658
+ def primary_trading_item(self) -> TradingItem:
659
+ """Set and return the trading item for the object
660
+
661
+ :return: The trading item returned as TradingItem object
662
+ :rtype: TradingItem
663
+ """
664
+ self.primary_trading_item = TradingItem(
665
+ kfinance_api_client=self.kfinance_api_client, trading_item_id=self.trading_item_id
666
+ )
667
+ return self.primary_trading_item
668
+
669
+ @cached_property
670
+ def isin(self) -> str:
671
+ """Get the ISIN for the object
672
+
673
+ :return: The ISIN
674
+ :rtype: str
675
+ """
676
+ if self._isin:
677
+ return self._isin
678
+ isin = self.primary_security.isin
679
+ self._isin = isin
680
+ return isin
681
+
682
+ @cached_property
683
+ def cusip(self) -> str:
684
+ """Get the CUSIP for the object
685
+
686
+ :return: The CUSIP
687
+ :rtype: str
688
+ """
689
+ if self._cusip:
690
+ return self._cusip
691
+ cusip = self.primary_security.cusip
692
+ self._cusip = cusip
693
+ return cusip
694
+
695
+ @cached_property
696
+ def info(self) -> dict:
697
+ """Get the company info for the ticker
698
+
699
+ :return: a dict with containing: name, status, type, simple industry, number of employees, founding date, webpage, address, city, zip code, state, country, & iso_country
700
+ :rtype: dict
701
+ """
702
+ return self.company.info
703
+
704
+ @property
705
+ def name(self) -> str:
706
+ """Get the company name
707
+
708
+ :return: The company name
709
+ :rtype: str
710
+ """
711
+ return self.company.name
712
+
713
+ @property
714
+ def status(self) -> str:
715
+ """Get the company status
716
+
717
+ :return: The company status
718
+ :rtype: str
719
+ """
720
+ return self.company.status
721
+
722
+ @property
723
+ def type(self) -> str:
724
+ """Get the type of company
725
+
726
+ :return: The company type
727
+ :rtype: str
728
+ """
729
+ return self.company.type
730
+
731
+ @property
732
+ def simple_industry(self) -> str:
733
+ """Get the simple industry for the company
734
+
735
+ :return: The company's simple_industry
736
+ :rtype: str
737
+ """
738
+ return self.company.simple_industry
739
+
740
+ @property
741
+ def number_of_employees(self) -> str:
742
+ """Get the number of employees the company has
743
+
744
+ :return: how many employees the company has
745
+ :rtype: str
746
+ """
747
+ return self.company.number_of_employees
748
+
749
+ @property
750
+ def founding_date(self) -> date:
751
+ """Get the founding date for the company
752
+
753
+ :return: founding date for the company
754
+ :rtype: date
755
+ """
756
+ return self.company.founding_date
757
+
758
+ @property
759
+ def webpage(self) -> str:
760
+ """Get the webpage for the company
761
+
762
+ :return: webpage for the company
763
+ :rtype: str
764
+ """
765
+ return self.company.webpage
766
+
767
+ @property
768
+ def address(self) -> str:
769
+ """Get the address of the company's HQ
770
+
771
+ :return: address of the company's HQ
772
+ :rtype: str
773
+ """
774
+ return self.company.address
775
+
776
+ @property
777
+ def city(self) -> str:
778
+ """Get the city of the company's HQ
779
+
780
+ :return: city of the company's HQ
781
+ :rtype: str
782
+ """
783
+ return self.company.city
784
+
785
+ @property
786
+ def zip_code(self) -> str:
787
+ """Get the zip code of the company's HQ
788
+
789
+ :return: zip code of the company's HQ
790
+ :rtype: str
791
+ """
792
+ return self.company.zip_code
793
+
794
+ @property
795
+ def state(self) -> str:
796
+ """Get the state of the company's HQ
797
+
798
+ :return: state of the company's HQ
799
+ :rtype: str
800
+ """
801
+ return self.company.state
802
+
803
+ @property
804
+ def country(self) -> str:
805
+ """Get the country of the company's HQ
806
+
807
+ :return: country of the company's HQ
808
+ :rtype: str
809
+ """
810
+ return self.company.country
811
+
812
+ @property
813
+ def iso_country(self) -> str:
814
+ """Get the ISO code for the country of the company's HQ
815
+
816
+ :return: iso code for the country of the company's HQ
817
+ :rtype: str
818
+ """
819
+ return self.company.iso_country
820
+
821
+ @cached_property
822
+ def earnings_call_datetimes(self) -> list[datetime]:
823
+ """Get the datetimes of the companies earnings calls
824
+
825
+ :return: a list of datetimes for the companies earnings calls
826
+ :rtype: list[datetime]
827
+ """
828
+ return self.company.earnings_call_datetimes
829
+
830
+ @cached_property
831
+ def history_metadata(self) -> HistoryMetadata:
832
+ """Get information about exchange and quotation
833
+
834
+ :return: A dict containing data about the currency, symbol, exchange, type of instrument, and the first trading date
835
+ :rtype: HistoryMetadata
836
+ """
837
+ metadata = self.primary_trading_item.history_metadata
838
+ if self.exchange_code is None:
839
+ self.exchange_code = metadata["exchange_name"]
840
+ if self._ticker is None:
841
+ self._ticker = metadata["symbol"]
842
+ return metadata
843
+
844
+ @cached_property
845
+ def ticker(self) -> str:
846
+ """Get the ticker if it isn't available from initialization"""
847
+ if self._ticker is not None:
848
+ return self._ticker
849
+ return self.history_metadata["symbol"]
850
+
851
+ def history(
852
+ self,
853
+ periodicity: str = "day",
854
+ adjusted: bool = True,
855
+ start_date: Optional[str] = None,
856
+ end_date: Optional[str] = None,
857
+ ) -> pd.DataFrame:
858
+ """Retrieves the historical price data for a given asset over a specified date range.
859
+
860
+ :param periodicity: Determines the frequency of the historical data returned. Options are "day", "week", "month" and "year". This default to "day"
861
+ :type periodicity: str
862
+ :param adjusted: Whether to retrieve adjusted prices that account for corporate actions such as dividends and splits, it defaults True
863
+ :type adjusted: bool, optional
864
+ :param start_date: The start date for historical price retrieval in format "YYYY-MM-DD", default to None
865
+ :type start_date: str, optional
866
+ :param end_date: The end date for historical price retrieval in format "YYYY-MM-DD", default to None
867
+ :type end_date: str, optional
868
+ :return: A pd.DataFrame containing historical price data with columns corresponding to the specified periodicity, with Date as the index, and columns "open", "high", "low", "close", "volume" in type decimal. The Date index is a string that depends on the periodicity. If periodicity="day", the Date index is the day in format "YYYY-MM-DD", eg "2024-05-13" If periodicity="week", the Date index is the week number of the year in format "YYYY Week ##", eg "2024 Week 2" If periodicity="month", the Date index is the month name of the year in format "<Month> YYYY", eg "January 2024". If periodicity="year", the Date index is the year in format "YYYY", eg "2024".
869
+ :rtype: pd.DataFrame
870
+ """
871
+ return self.primary_trading_item.history(
872
+ periodicity,
873
+ adjusted,
874
+ start_date,
875
+ end_date,
876
+ )
877
+
878
+ def price_chart(
879
+ self,
880
+ periodicity: str = "day",
881
+ adjusted: bool = True,
882
+ start_date: Optional[str] = None,
883
+ end_date: Optional[str] = None,
884
+ ) -> Image:
885
+ """Get the price chart.
886
+
887
+ :param periodicity: Determines the frequency of the historical data returned. Options are "day", "week", "month" and "year". This default to "day"
888
+ :type periodicity: str
889
+ :param adjusted: Whether to retrieve adjusted prices that account for corporate actions such as dividends and splits, it defaults True
890
+ :type adjusted: bool, optional
891
+ :param start_date: The start date for historical price retrieval in format "YYYY-MM-DD", default to None
892
+ :type start_date: str, optional
893
+ :param end_date: The end date for historical price retrieval in format "YYYY-MM-DD", default to None
894
+ :type end_date: str, optional
895
+ :return: An image showing the price chart of the trading item
896
+ :rtype: Image
897
+ """
898
+ return self.primary_trading_item.price_chart(periodicity, adjusted, start_date, end_date)
899
+
900
+
901
+ class BusinessRelationships(NamedTuple):
902
+ """Business relationships object that represents the current and previous companies of a given Company object.
903
+
904
+ :param current: A Companies set that represents the current company_ids.
905
+ :param previous: A Companies set that represents the previous company_ids.
906
+ """
907
+
908
+ current: Companies
909
+ previous: Companies
910
+
911
+ def __str__(self) -> str:
912
+ """String representation for the BusinessRelationships object"""
913
+ dictionary = {
914
+ "current": [company.company_id for company in self.current],
915
+ "previous": [company.company_id for company in self.previous],
916
+ }
917
+ return f"{type(self).__module__}.{type(self).__qualname__} of {str(dictionary)}"
918
+
919
+
920
+ class Companies(set):
921
+ """Base class for representing a set of Companies"""
922
+
923
+ def __init__(self, kfinance_api_client: KFinanceApiClient, company_ids: Iterable[int]) -> None:
924
+ """Initialize the Companies object
925
+
926
+ :param kfinance_api_client: The KFinanceApiClient used to fetch data
927
+ :type kfinance_api_client: KFinanceApiClient
928
+ :param company_ids: An iterable of S&P CIQ Company ids
929
+ :type company_ids: Iterable[int]
930
+ """
931
+ super().__init__(Company(kfinance_api_client, company_id) for company_id in company_ids)
932
+
933
+
934
+ class Securities(set):
935
+ """Base class for representing a set of Securities"""
936
+
937
+ def __init__(self, kfinance_api_client: KFinanceApiClient, security_ids: Iterable[int]) -> None:
938
+ """Initialize the Securities
939
+
940
+ :param kfinance_api_client: The KFinanceApiClient used to fetch data
941
+ :type kfinance_api_client: KFinanceApiClient
942
+ :param security_ids: An iterable of S&P CIQ Security ids
943
+ :type security_ids: Iterable[int]
944
+ """
945
+ super().__init__(Security(kfinance_api_client, security_id) for security_id in security_ids)
946
+
947
+
948
+ class TradingItems(set):
949
+ """Base class for representing a set of Trading Items"""
950
+
951
+ def __init__(
952
+ self, kfinance_api_client: KFinanceApiClient, trading_item_ids: Iterable[int]
953
+ ) -> None:
954
+ """Initialize the Trading Items
955
+
956
+ :param kfinance_api_client: The KFinanceApiClient used to fetch data
957
+ :type kfinance_api_client: KFinanceApiClient
958
+ :param company_ids: An iterable of S&P CIQ Company ids
959
+ :type company_ids: Iterable[int]
960
+ """
961
+ super().__init__(
962
+ TradingItem(kfinance_api_client, trading_item_id)
963
+ for trading_item_id in trading_item_ids
964
+ )
965
+
966
+
967
+ class Tickers(set):
968
+ """Base TickerSet class for representing a set of Tickers"""
969
+
970
+ def __init__(
971
+ self,
972
+ kfinance_api_client: KFinanceApiClient,
973
+ id_triples: Iterable[IdentificationTriple],
974
+ ) -> None:
975
+ """Initialize the Ticker Set
976
+
977
+ :param kfinance_api_client: The KFinanceApiClient used to fetch data
978
+ :type kfinance_api_client: KFinanceApiClient
979
+ :param id_triples: An Iterable of IdentificationTriples that will become the ticker objects making up the tickers object
980
+ :type id_triples: Iterable[IdentificationTriple]
981
+ """
982
+ self.kfinance_api_client = kfinance_api_client
983
+ super().__init__(
984
+ Ticker(
985
+ kfinance_api_client=kfinance_api_client,
986
+ company_id=id_triple["company_id"],
987
+ security_id=id_triple["security_id"],
988
+ trading_item_id=id_triple["trading_item_id"],
989
+ )
990
+ for id_triple in id_triples
991
+ )
992
+
993
+ def companies(self) -> Companies:
994
+ """Build a group of company objects from the group of tickers
995
+
996
+ :return: The Companies corresponding to the Tickers
997
+ :rtype: Companies
998
+ """
999
+ return Companies(
1000
+ self.kfinance_api_client, (ticker.company_id for ticker in self.__iter__())
1001
+ )
1002
+
1003
+ def securities(self) -> Securities:
1004
+ """Build a group of security objects from the group of tickers
1005
+
1006
+ :return: The Securities corresponding to the Tickers
1007
+ :rtype: Securities
1008
+ """
1009
+ return Securities(
1010
+ self.kfinance_api_client, (ticker.security_id for ticker in self.__iter__())
1011
+ )
1012
+
1013
+ def trading_items(self) -> TradingItems:
1014
+ """Build a group of trading item objects from the group of ticker
1015
+
1016
+ :return: The TradingItems corresponding to the Tickers
1017
+ :rtype: TradingItems
1018
+ """
1019
+ return TradingItems(
1020
+ self.kfinance_api_client, (ticker.trading_item_id for ticker in self.__iter__())
1021
+ )
1022
+
1023
+
1024
+ class Client:
1025
+ """Client class with LLM tools and a pre-credentialed Ticker object
1026
+
1027
+ :param tools: A dictionary mapping function names to functions, where each function is an llm tool with the Client already passed in if applicable
1028
+ :type tools: dict[str, Callable]
1029
+ :param anthropic_tool_descriptions: A list of dictionaries, where each dictionary is an Anthropic tool definition
1030
+ :type anthropic_tool_descriptions: list[dict]
1031
+ :param gemini_tool_descriptions: A dictionary mapping "function_declarations" to a list of dictionaries, where each dictionary is a Gemini tool definition
1032
+ :type gemini_tool_descriptions: dict[list[dict]]
1033
+ :param openai_tool_descriptions: A list of dictionaries, where each dictionary is an OpenAI tool definition
1034
+ :type openai_tool_descriptions: list[dict]
1035
+ """
1036
+
1037
+ prompt = PROMPT
1038
+
1039
+ def __init__(
1040
+ self,
1041
+ refresh_token: Optional[str] = None,
1042
+ client_id: Optional[str] = None,
1043
+ private_key: Optional[str] = None,
1044
+ api_host: str = DEFAULT_API_HOST,
1045
+ api_version: int = DEFAULT_API_VERSION,
1046
+ okta_host: str = DEFAULT_OKTA_HOST,
1047
+ okta_auth_server: str = DEFAULT_OKTA_AUTH_SERVER,
1048
+ ):
1049
+ """Initialization of the client.
1050
+
1051
+ :param refresh_token: users refresh token
1052
+ :type refresh_token: str, Optional
1053
+ :param client_id: users client id will be provided by support@kensho.com
1054
+ :type client_id: str, Optional
1055
+ :param private_key: users private key that corresponds to the registered public sent to support@kensho.com
1056
+ :type private_key: str, Optional
1057
+ :param api_host: the api host URL
1058
+ :type api_host: str
1059
+ :param api_version: the api version number
1060
+ :type api_version: int
1061
+ :param okta_host: the okta host URL
1062
+ :type okta_host: str
1063
+ :param okta_auth_server: the okta route for authentication
1064
+ :type okta_auth_server: str
1065
+ """
1066
+
1067
+ # method 1 refresh token
1068
+ if refresh_token is not None:
1069
+ self.kfinance_api_client = KFinanceApiClient(
1070
+ refresh_token=refresh_token,
1071
+ api_host=api_host,
1072
+ api_version=api_version,
1073
+ okta_host=okta_host,
1074
+ )
1075
+ # method 2 keypair
1076
+ elif client_id is not None and private_key is not None:
1077
+ self.kfinance_api_client = KFinanceApiClient(
1078
+ client_id=client_id,
1079
+ private_key=private_key,
1080
+ api_host=api_host,
1081
+ api_version=api_version,
1082
+ okta_host=okta_host,
1083
+ okta_auth_server=okta_auth_server,
1084
+ )
1085
+ # method 3 automatic login getting a refresh token
1086
+ else:
1087
+ server_thread = ServerThread()
1088
+ stdout.write("Please login with your credentials.\n")
1089
+ server_thread.start()
1090
+ webbrowser.open(
1091
+ urljoin(
1092
+ api_host if api_host else DEFAULT_API_HOST,
1093
+ f"automated_login?port={server_thread.server_port}",
1094
+ )
1095
+ )
1096
+ server_thread.join()
1097
+ self.kfinance_api_client = KFinanceApiClient(
1098
+ refresh_token=server_thread.refresh_token,
1099
+ api_host=api_host,
1100
+ api_version=api_version,
1101
+ okta_host=okta_host,
1102
+ )
1103
+ stdout.write("Login credentials received.\n")
1104
+
1105
+ self.tools = _llm_tools(self)
1106
+ self.langchain_tools = langchain_tools(self.tools)
1107
+ self.anthropic_tool_descriptions = anthropic_tool_descriptions
1108
+ self.gemini_tool_descriptions = gemini_tool_descriptions
1109
+ self.openai_tool_descriptions = openai_tool_descriptions
1110
+
1111
+ @property
1112
+ def access_token(self) -> str:
1113
+ """Returns the client access token.
1114
+
1115
+ :return: A valid access token for use in API
1116
+ :rtype: str
1117
+ """
1118
+ return self.kfinance_api_client.access_token
1119
+
1120
+ def ticker(
1121
+ self,
1122
+ identifier: int | str,
1123
+ exchange_code: Optional[str] = None,
1124
+ function_called: Optional[bool] = False,
1125
+ ) -> Ticker:
1126
+ """Generate Ticker object from [identifier] that is a ticker, ISIN, or CUSIP.
1127
+
1128
+ :param identifier: the ticker symbol, ISIN, or CUSIP
1129
+ :type identifier: str
1130
+ :param exchange_code: The code representing the equity exchange the ticker is listed on.
1131
+ :type exchange_code: str, optional
1132
+ :param function_called: Flag for use in signaling function calling
1133
+ :type function_called: bool, optional
1134
+ :return: Ticker object from that corresponds to the identifier
1135
+ :rtype: Ticker
1136
+ """
1137
+ if function_called:
1138
+ self.kfinance_api_client.user_agent_source = "function_calling"
1139
+ return Ticker(self.kfinance_api_client, str(identifier), exchange_code)
1140
+
1141
+ def tickers(
1142
+ self,
1143
+ country_iso_code: Optional[str] = None,
1144
+ state_iso_code: Optional[str] = None,
1145
+ simple_industry: Optional[str] = None,
1146
+ exchange_code: Optional[str] = None,
1147
+ ) -> Tickers:
1148
+ """Generate tickers object representing the collection of Tickers that meet all the supplied parameters
1149
+
1150
+ One of country_iso_code, simple_industry, or exchange_code must be supplied. A parameter set to None is not used to filter on
1151
+
1152
+ :param country_iso_code: The ISO 3166-1 Alpha-2 or Alpha-3 code that represent the primary country the firm is based in. It default None
1153
+ :type country_iso_code: str, optional
1154
+ :param state_iso_code: The ISO 3166-2 Alpha-2 code that represents the primary subdivision of the country the firm the based in. Not all ISO 3166-2 codes are supported as S&P doesn't maintain the full list but a feature request for the full list is submitted to S&P product. Requires country_iso_code also to have a value other then None. It default None
1155
+ :type state_iso_code: str, optional
1156
+ :param simple_industry: The S&P CIQ Simple Industry defined in ciqSimpleIndustry in XPF. It default None
1157
+ :type simple_industry: str, optional
1158
+ :param exchange_code: The exchange code for the primary equity listing exchange of the firm. It default None
1159
+ :type exchange_code: str, optional
1160
+ :return: A tickers object that is the group of Ticker objects meeting all the supplied parameters
1161
+ :rtype: Tickers
1162
+ """
1163
+ return Tickers(
1164
+ kfinance_api_client=self.kfinance_api_client,
1165
+ id_triples=self.kfinance_api_client.fetch_ticker_combined(
1166
+ country_iso_code=country_iso_code,
1167
+ state_iso_code=state_iso_code,
1168
+ simple_industry=simple_industry,
1169
+ exchange_code=exchange_code,
1170
+ )["tickers"],
1171
+ )
1172
+
1173
+ def company(self, company_id: int) -> Company:
1174
+ """Generate the Company object from company_id
1175
+
1176
+ :param company_id: CIQ company id
1177
+ :type company_id: int
1178
+ :return: The Company specified by the the company id
1179
+ :rtype: Company
1180
+ """
1181
+ return Company(kfinance_api_client=self.kfinance_api_client, company_id=company_id)
1182
+
1183
+ def security(self, security_id: int) -> Security:
1184
+ """Generate Security object from security_id
1185
+
1186
+ :param security_id: CIQ security id
1187
+ :type security_id: int
1188
+ :return: The Security specified by the the security id
1189
+ :rtype: Security
1190
+ """
1191
+ return Security(kfinance_api_client=self.kfinance_api_client, security_id=security_id)
1192
+
1193
+ def trading_item(self, trading_item_id: int) -> TradingItem:
1194
+ """Generate TradingItem object from trading_item_id
1195
+
1196
+ :param trading_item_id: CIQ trading item id
1197
+ :type trading_item_id: int
1198
+ :return: The trading item specified by the the trading item id
1199
+ :rtype: TradingItem
1200
+ """
1201
+ return TradingItem(
1202
+ kfinance_api_client=self.kfinance_api_client, trading_item_id=trading_item_id
1203
+ )
1204
+
1205
+ @staticmethod
1206
+ def get_latest() -> LatestPeriods:
1207
+ """Get the latest annual reporting year, latest quarterly reporting quarter and year, and current date.
1208
+
1209
+ :return: A dict in the form of {"annual": {"latest_year": int}, "quarterly": {"latest_quarter": int, "latest_year": int}, "now": {"current_year": int, "current_quarter": int, "current_month": int, "current_date": str of Y-m-d}}
1210
+ :rtype: Latest
1211
+ """
1212
+
1213
+ return get_latest()
1214
+
1215
+ @staticmethod
1216
+ def get_n_quarters_ago(n: int) -> YearAndQuarter:
1217
+ """Get the year and quarter corresponding to [n] quarters before the current quarter
1218
+
1219
+ :param int n: the number of quarters before the current quarter
1220
+ :return: A dict in the form of {"year": int, "quarter": int}
1221
+ :rtype: YearAndQuarter
1222
+ """
1223
+
1224
+ return get_n_quarters_ago(n)