-
-
Notifications
You must be signed in to change notification settings - Fork 508
Expand file tree
/
Copy pathtoolkit_controller.py
More file actions
3843 lines (3266 loc) · 184 KB
/
toolkit_controller.py
File metadata and controls
3843 lines (3266 loc) · 184 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
"""Toolkit Module"""
__docformat__ = "google"
import re
import warnings
from collections import Counter
from datetime import datetime, timedelta
import pandas as pd
from financetoolkit import currencies_model, helpers
from financetoolkit.economics.economics_controller import Economics
from financetoolkit.fixedincome.fixedincome_controller import FixedIncome
from financetoolkit.fmp_model import (
get_analyst_estimates as _get_analyst_estimates,
get_dividend_calendar as _get_dividend_calendar,
get_earnings_calendar as _get_earnings_calendar,
get_esg_scores as _get_esg_scores,
get_financial_data as _get_financial_data,
get_profile as _get_profile,
get_quote as _get_quote,
get_rating as _get_rating,
get_revenue_segmentation as _get_revenue_segmentation,
)
from financetoolkit.fundamentals_model import collect_financial_statements
from financetoolkit.historical_model import (
convert_daily_to_other_period as _convert_daily_to_other_period,
get_historical_data as _get_historical_data,
get_historical_statistics as _get_historical_statistics,
)
from financetoolkit.models.models_controller import Models
from financetoolkit.normalization_model import (
copy_normalization_files as _copy_normalization_files,
initialize_statements_and_normalization as _initialize_statements_and_normalization,
)
from financetoolkit.options.options_controller import Options
from financetoolkit.performance.performance_controller import Performance
from financetoolkit.ratios.ratios_controller import Ratios
from financetoolkit.risk.risk_controller import Risk
from financetoolkit.technicals.technicals_controller import Technicals
from financetoolkit.utilities import cache_model, logger_model
# Set up logger, this is meant to display useful messages, warnings or errors when
# the Finance Toolkit runs into issues or does something that might not be entirely
# logical at first
logger_model.setup_logger()
logger = logger_model.get_logger()
# Runtime errors are ignored on purpose given the nature of the calculations
# sometimes leading to division by zero or other mathematical errors. This is however
# for financial analysis purposes not an issue and should not be considered as a bug.
warnings.filterwarnings("ignore", category=RuntimeWarning)
# pylint: disable=too-many-instance-attributes,too-many-lines,line-too-long,too-many-locals
# pylint: disable=too-many-function-args,too-many-public-methods
# ruff: noqa: E501
TICKER_LIMIT = 20
try:
from tqdm import tqdm
ENABLE_TQDM = True
except ImportError:
ENABLE_TQDM = False
class Toolkit:
"""
The Finance Toolkit is an open-source toolkit in which
all 150+ financial ratios, indicators and performance measurements
are written down in the most simplistic way allowing for complete transparency
of the calculation method. This allows you to not have to rely on metrics
from other providers and, given a financial statement, allow for efficient manual
calculations. This leads to one uniform method of calculation being applied that
is available and understood by everyone.
"""
def __init__(
self,
tickers: list | str | None = None,
api_key: str = "",
start_date: str | None = None,
end_date: str | None = None,
quarterly: bool = False,
use_cached_data: bool | str = False,
risk_free_rate: str = "10y",
benchmark_ticker: str | None = "SPY",
enforce_source: str | None = None,
historical: pd.DataFrame = pd.DataFrame(),
balance: pd.DataFrame = pd.DataFrame(),
income: pd.DataFrame = pd.DataFrame(),
cash: pd.DataFrame = pd.DataFrame(),
format_location: str = "",
convert_currency: bool | None = None,
reverse_dates: bool = True,
intraday_period: str | None = None,
rounding: int | None = 4,
remove_invalid_tickers: bool = False,
sleep_timer: bool | None = None,
progress_bar: bool = True,
):
"""
Initializes a Toolkit object with a ticker or a list of tickers. The way the Toolkit is initialized
will define how the data is collected. For example, if you enable the quarterly flag, you will
be able to collect quarterly data. Next to that, you can define the start and end date to specify
a specific range. Another option is to work with cached data. This is useful when you have collected
data before and want to use this data again. This can be done by setting the use_cached_data variable
to True. If you want to use a specific location to store the cached data, you can define this as a string,
e.g. "datasets".
It is good to note that the Finance Toolkit will always attempt to acquire data from Financial Modeling Prep
if an API key is set. If this isn't the case, the data comes from Yahoo Finance. In case you have an API key
set and the current plan doesn't allow for the data to be collected, the Toolkit will automatically switch to
Yahoo Finance. You can disable this behaviour by setting the enforce_source variable to "FinancialModelingPrep".
For more information on the capabilities of the Finance Toolkit see here: https://www.jeroenbouma.com/projects/financetoolkit
Args:
tickers (list | str | None): A string or a list of strings containing the company ticker(s). E.g. 'TSLA' or 'MSFT'.
Find tickers on various websites or via the FinanceDatabase: https://github.com/JerBouma/financedatabase. Defaults to None.
api_key (str): An API key from FinancialModelingPrep. Obtain one here: https://www.jeroenbouma.com/fmp. Defaults to "".
start_date (str | None): A string containing the start date of the data. Needs to be formatted as YYYY-MM-DD.
Defaults to 5 years/quarters back from today depending on the 'quarterly' flag.
end_date (str | None): A string containing the end date of the data. Needs to be formatted as YYYY-MM-DD.
Defaults to today.
quarterly (bool): A boolean indicating whether to collect quarterly data. Defaults to False (yearly).
Note that historical data can still be collected for any period and interval.
use_cached_data (bool | str): A boolean indicating whether to use cached data. If True, uses a 'cached' folder.
If a string is provided, uses that string as the path to the cache folder. Defaults to False.
risk_free_rate (str): The risk-free rate identifier ('13w', '5y', '10y', '30y'). Based on US Treasury Yields.
Used for calculations like Excess Returns. Defaults to "10y".
benchmark_ticker (str | None): The benchmark ticker (e.g., 'SPY' for S&P 500). Used for comparative analysis
(CAPM, Alpha, Beta). Defaults to "SPY". Set to None to disable benchmark comparison.
enforce_source (str | None): Enforce data source ('FinancialModelingPrep' or 'YahooFinance').
Defaults to None (uses FMP if api_key provided, otherwise YahooFinance, with fallback).
historical (pd.DataFrame): Custom historical price data. See notebook:
https://www.jeroenbouma.com/projects/financetoolkit/external-datasets. Defaults to an empty DataFrame.
balance (pd.DataFrame): Custom balance sheet data. See notebook link above. Defaults to an empty DataFrame.
income (pd.DataFrame): Custom income statement data. See notebook link above. Defaults to an empty DataFrame.
cash (pd.DataFrame): Custom cash flow statement data. See notebook link above. Defaults to an empty DataFrame.
format_location (str): Path to custom normalization files. Defaults to "".
convert_currency (bool | None): Convert financial statements currency to match historical data currency.
Important for cross-ticker comparison and calculations involving both data types.
Defaults to None (True if FMP plan is Premium, False if Free). Can be overridden.
reverse_dates (bool): Reverse the order of dates in financial statements (oldest first). Defaults to True.
intraday_period (str | None): Intraday data interval ('1min', '5min', '15min', '30min', '1hour').
Enables short-term analysis using Risk, Performance, and Technicals modules. Requires FMP Premium.
Defaults to None (no intraday data).
rounding (int | None): Number of decimal places for results. Defaults to 4.
remove_invalid_tickers (bool): Remove tickers that fail data retrieval. Defaults to False.
sleep_timer (bool | None): Enable sleep timer on FMP rate limit (requires Premium).
Defaults to None (determined by FMP plan: True for Premium, False for Free).
progress_bar (bool): Show progress bar for operations involving multiple tickers. Defaults to True.
As an example:
```python
from financetoolkit import Toolkit
# Simple example
toolkit = Toolkit(
tickers=["TSLA", "ASML"],
api_key="FINANCIAL_MODELING_PREP_KEY")
# Obtaining quarterly data
toolkit = Toolkit(
tickers=["AAPL", "GOOGL"],
quarterly=True,
api_key="FINANCIAL_MODELING_PREP_KEY")
# Enforce a specific source
toolkit = Toolkit(
tickers=["ASML", "BABA"],
quarterly=True,
enforce_source="YahooFinance")
# Including a start and end date
toolkit = Toolkit(
tickers=["MSFT", "MU"],
start_date="2020-01-01",
end_date="2023-01-01",
quarterly=True,
api_key="FINANCIAL_MODELING_PREP_KEY")
# Working with cached data
toolkit = Toolkit(
tickers=["WMT", "AAPL"],
quarterly=True,
api_key="FINANCIAL_MODELING_PREP_KEY",
use_cached_data=True)
# Changing the benchmark and risk free rate
toolkit = Toolkit(
tickers="AMZN",
benchmark_ticker="^DJI",
risk_free_rate="30y",
api_key="FINANCIAL_MODELING_PREP_KEY")
```
"""
self._api_key = api_key
self._risk_free_rate = risk_free_rate
self._rounding = rounding
self._remove_invalid_tickers = remove_invalid_tickers
self._invalid_tickers: list = []
self._use_cached_data = (
use_cached_data if isinstance(use_cached_data, bool) else True
)
self._cached_data_location = (
"cached" if isinstance(use_cached_data, bool) else use_cached_data
)
self._benchmark_ticker = benchmark_ticker
if start_date and re.match(r"^\d{4}-\d{2}-\d{2}$", start_date) is None:
raise ValueError(
"Please input a valid start date (%Y-%m-%d) like '2010-01-01'"
)
if end_date and re.match(r"^\d{4}-\d{2}-\d{2}$", end_date) is None:
raise ValueError(
"Please input a valid end date (%Y-%m-%d) like '2020-01-01'"
)
if start_date and end_date and start_date > end_date:
raise ValueError(
f"Please ensure the start date {start_date} is before the end date {end_date}"
)
if risk_free_rate not in [
"13w",
"5y",
"10y",
"30y",
]:
raise ValueError(
"Please select a valid risk free rate (13w, 5y, 10y or 30y)"
)
self._start_date = (
start_date
if start_date
else (
datetime.now() - timedelta(days=90 * 5 if quarterly else 365 * 5)
).strftime("%Y-%m-%d")
)
self._end_date = end_date if end_date else datetime.now().strftime("%Y-%m-%d")
self._quarterly = quarterly
if use_cached_data:
cached_configurations = cache_model.load_cached_data(
cached_data_location=self._cached_data_location,
file_name="configurations.pickle",
method="pickle",
return_empty_type={},
)
if cached_configurations: # Check if dictionary is not empty
cached_overwrites = []
# Map cache keys to tuples of (initial_value, attribute_name)
config_mapping = {
"start_date": (self._start_date, "_start_date"),
"end_date": (self._end_date, "_end_date"),
"quarterly": (self._quarterly, "_quarterly"),
"benchmark_ticker": (self._benchmark_ticker, "_benchmark_ticker"),
"risk_free_rate": (self._risk_free_rate, "_risk_free_rate"),
}
# Compare initial values with cached values, update instance if different
for key, (initial_value, attr_name) in config_mapping.items():
cached_value = cached_configurations.get(key)
# Check if cached value exists and is different from the initial value
if cached_value is not None and initial_value != cached_value:
setattr(
self, attr_name, cached_value
) # Update instance attribute
cached_overwrites.append(f"{key} ({cached_value})")
# Handle tickers separately: compare input tickers with cached tickers
cached_tickers = cached_configurations.get("tickers")
# Check if cached tickers exist and are different from the input tickers
if cached_tickers is not None and tickers != cached_tickers and tickers:
# Only log the change if the user actually provided tickers initially
cached_overwrites.append("tickers")
tickers = cached_tickers
if cached_overwrites:
folder = (
"cached"
if isinstance(use_cached_data, bool)
else use_cached_data
)
logger.info(
"The following variables are overwritten by the cached "
"configurations: %s\n"
"If this is undesirable, please set the use_cached_data variable "
"to False, delete the directory %s or select a new "
"location for the cached data by changing the use_cached_data "
"variable to a string.",
", ".join(cached_overwrites),
folder,
)
else:
# Save the current configuration if no cache exists
# Use the values as they are before potential overwrites from cache
cache_model.save_cached_data(
cached_data={
"tickers": tickers, # Use the initial tickers list/str
"start_date": self._start_date,
"end_date": self._end_date,
"quarterly": self._quarterly,
"benchmark_ticker": self._benchmark_ticker,
"risk_free_rate": self._risk_free_rate, # Use the initial risk_free_rate
},
cached_data_location=self._cached_data_location,
file_name="configurations.pickle",
method="pickle",
include_message=False,
)
if isinstance(tickers, str):
tickers = [tickers.upper()]
elif isinstance(tickers, list):
tickers = [
ticker.upper() if ticker != "Portfolio" else ticker
for ticker in tickers
]
elif tickers is None:
raise ValueError("Please input a ticker or a list of tickers.")
else:
raise TypeError("Tickers must be a string or a list of strings.")
self._tickers: list[str] = []
for ticker in tickers:
# Check whether the ticker is in ISIN format and if say so convert it to a ticker
self._tickers.append(helpers.convert_isin_to_ticker(ticker))
# Take out duplicate tickers if applicable
deduplicated_tickers = list(set(self._tickers))
if len(deduplicated_tickers) != len(self._tickers):
duplicate_tickers = [
ticker for ticker, count in Counter(self._tickers).items() if count > 1
]
logger.warning(
"Found duplicate tickers, duplicate entries of the following tickers are removed: %s",
", ".join(duplicate_tickers),
)
self._tickers = deduplicated_tickers
if self._benchmark_ticker in self._tickers:
logger.warning(
"Please note that the benchmark ticker (%s) is also "
"included in the tickers. Therefore, this ticker will be removed from the "
"tickers list. If this is not desired, please set the benchmark_ticker to None.",
self._benchmark_ticker,
)
self._tickers.remove(self._benchmark_ticker)
self._enforce_source: str | None = enforce_source
if self._enforce_source not in [None, "FinancialModelingPrep", "YahooFinance"]:
raise ValueError(
"Please select either FinancialModelingPrep or YahooFinance as the "
"enforced source."
)
if self._enforce_source == "FinancialModelingPrep" and not self._api_key:
raise ValueError(
"Please input an API key from FinancialModelingPrep if you wish to use "
"historical data from FinancialModelingPrep."
)
if sleep_timer is None:
# This tests the API key to determine the subscription plan. This is relevant for the sleep timer
# but also for other components of the Toolkit. This prevents wait timers from occurring while
# it wouldn't result to any other answer than a rate limit error.
determine_plan = _get_financial_data(
url=f"https://financialmodelingprep.com/stable/income-statement?symbol=AAPL&apikey={api_key}&limit=10",
sleep_timer=False,
user_subscription="Free",
)
self._fmp_plan = "Premium"
for option in [
"PREMIUM QUERY PARAMETER",
"EXCLUSIVE ENDPOINT",
"NO DATA",
"BANDWIDTH LIMIT REACH",
"INVALID API KEY",
"LIMIT REACH",
]:
if option in determine_plan:
if option == "INVALID API KEY" and api_key:
self._enforce_source = "YahooFinance"
logger.error(
"You have entered an invalid API key from Financial Modeling Prep. Obtain your API key for free "
"and get 15%% off the Premium plans by using the following affiliate link.\nThis also supports "
"the project: https://www.jeroenbouma.com/fmp. Using Yahoo Finance as data source instead."
)
self._fmp_plan = "Free"
break
else:
self._fmp_plan = "Premium"
self._sleep_timer = (
sleep_timer if sleep_timer is not None else self._fmp_plan != "Free"
)
self._progress_bar = progress_bar
if self._api_key or self._use_cached_data:
# Initialize attributes to empty DataFrames
self._profile: pd.DataFrame = pd.DataFrame()
self._quote: pd.DataFrame = pd.DataFrame()
self._rating: pd.DataFrame = pd.DataFrame()
self._analyst_estimates: pd.DataFrame = pd.DataFrame()
self._analyst_estimates_growth: pd.DataFrame = pd.DataFrame()
self._dividend_calendar: pd.DataFrame = pd.DataFrame()
self._earnings_calendar: pd.DataFrame = pd.DataFrame()
self._esg_scores: pd.DataFrame = pd.DataFrame()
self._revenue_geographic_segmentation: pd.DataFrame = pd.DataFrame()
self._revenue_product_segmentation: pd.DataFrame = pd.DataFrame()
self._revenue_geographic_segmentation_growth: pd.DataFrame = pd.DataFrame()
self._revenue_product_segmentation_growth: pd.DataFrame = pd.DataFrame()
# Define attributes and their corresponding cache file names
cached_attributes = {
"_profile": "profile.pickle",
"_quote": "quote.pickle",
"_rating": "rating.pickle",
"_analyst_estimates": "analyst_estimates.pickle",
"_analyst_estimates_growth": "analyst_estimates_growth.pickle",
"_dividend_calendar": "dividend_calendar.pickle",
"_earnings_calendar": "earnings_calendar.pickle",
"_esg_scores": "esg_scores.pickle",
"_revenue_geographic_segmentation": "revenue_geographic_segmentation.pickle",
"_revenue_product_segmentation": "revenue_product_segmentation.pickle",
}
# Initialize FinancialModelingPrep Variables
for attr_name, file_name in cached_attributes.items():
data = (
cache_model.load_cached_data(
cached_data_location=self._cached_data_location,
file_name=file_name,
)
if self._use_cached_data
else pd.DataFrame()
)
setattr(self, attr_name, data)
if intraday_period and intraday_period not in [
"1min",
"5min",
"15min",
"30min",
"1hour",
]:
raise ValueError(
"Please select a valid intraday period (1min, 5min, 15min, 30min or 1hour)"
)
self._intraday_period = intraday_period
# Load intraday data from cache if specified, otherwise initialize empty DataFrame
self._intraday_historical_data: pd.DataFrame = (
cache_model.load_cached_data(
cached_data_location=self._cached_data_location,
file_name="intraday_historical_data.pickle",
)
if self._use_cached_data
else pd.DataFrame()
)
# Use provided historical data if available, otherwise load daily data from cache or initialize empty DataFrame
self._historical = historical
self._daily_historical_data: pd.DataFrame = (
historical
if not historical.empty
else (
cache_model.load_cached_data(
cached_data_location=self._cached_data_location,
file_name="daily_historical_data.pickle",
)
if self._use_cached_data
else pd.DataFrame()
)
)
# Initialize other periods as empty DataFrames. They will be populated on demand.
self._weekly_historical_data: pd.DataFrame = pd.DataFrame()
self._monthly_historical_data: pd.DataFrame = pd.DataFrame()
self._quarterly_historical_data: pd.DataFrame = pd.DataFrame()
self._yearly_historical_data: pd.DataFrame = pd.DataFrame()
self._historical_statistics: pd.DataFrame = pd.DataFrame()
# Initialization of the Financial Statements and Normalization
self._reverse_dates = reverse_dates
(
self._balance_sheet_statement,
self._income_statement,
self._cash_flow_statement,
self._statistics_statement,
self._fmp_balance_sheet_statement_generic,
self._yf_balance_sheet_statement_generic,
self._fmp_income_statement_generic,
self._yf_income_statement_generic,
self._fmp_cash_flow_statement_generic,
self._yf_cash_flow_statement_generic,
self._fmp_statistics_statement_generic,
) = _initialize_statements_and_normalization(
balance=balance,
income=income,
cash=cash,
format_location=format_location,
reverse_dates=self._reverse_dates,
use_cached_data=use_cached_data,
cached_data_location=self._cached_data_location,
start_date=self._start_date,
end_date=self._end_date,
quarterly=self._quarterly,
)
self._balance_sheet_statement_growth: pd.DataFrame = pd.DataFrame()
self._income_statement_growth: pd.DataFrame = pd.DataFrame()
self._cash_flow_statement_growth: pd.DataFrame = pd.DataFrame()
self._currencies: list = []
self._statement_currencies: pd.Series = pd.Series()
self._convert_currency = (
convert_currency
if convert_currency is not None
else self._fmp_plan != "Free"
)
# Initialization of Risk Free Rate
self._daily_risk_free_rate: pd.DataFrame = pd.DataFrame()
self._weekly_risk_free_rate: pd.DataFrame = pd.DataFrame()
self._monthly_risk_free_rate: pd.DataFrame = pd.DataFrame()
self._quarterly_risk_free_rate: pd.DataFrame = pd.DataFrame()
self._yearly_risk_free_rate: pd.DataFrame = pd.DataFrame()
# Initialization of Treasury Variables
self._daily_treasury_data: pd.DataFrame = pd.DataFrame()
self._weekly_treasury_data: pd.DataFrame = pd.DataFrame()
self._monthly_treasury_data: pd.DataFrame = pd.DataFrame()
self._quarterly_treasury_data: pd.DataFrame = pd.DataFrame()
self._yearly_treasury_data: pd.DataFrame = pd.DataFrame()
# Initialization of the Exchange Rate Variables
self._daily_exchange_rate_data: pd.DataFrame = pd.DataFrame()
self._weekly_exchange_rate_data: pd.DataFrame = pd.DataFrame()
self._monthly_exchange_rate_data: pd.DataFrame = pd.DataFrame()
self._quarterly_exchange_rate_data: pd.DataFrame = pd.DataFrame()
self._yearly_exchange_rate_data: pd.DataFrame = pd.DataFrame()
# Initialization of the Portfolio Variables
self._portfolio_weights: dict | None = None
pd.set_option("display.float_format", str)
@property
def ratios(self) -> Ratios:
"""
The Ratios Module contains over 50+ ratios that can be used to analyse companies. These ratios
are divided into 5 categories which are efficiency, liquidity, profitability, solvency and
valuation. Each ratio is calculated using the data from the Toolkit module.
Some examples of ratios are the Current Ratio, Debt to Equity Ratio, Return on Assets (ROA),
Return on Equity (ROE), Return on Invested Capital (ROIC), Return on Capital Employed (ROCE),
Price to Earnings Ratio (P/E), Price to Book Ratio (P/B), Price to Sales Ratio (P/S), Price
to Cash Flow Ratio (P/CF), Price to Free Cash Flow Ratio (P/FCF), Dividend Yield and
Dividend Payout Ratio.
Next to that, it is also possible to define custom ratios.
See the following link for more information: https://www.jeroenbouma.com/projects/financetoolkit/docs/ratios
As an example:
```python
from financetoolkit import Toolkit
toolkit = Toolkit(["AAPL", "TSLA"], api_key="FINANCIAL_MODELING_PREP_KEY")
profitability_ratios = toolkit.ratios.collect_profitability_ratios()
profitability_ratios.loc['AAPL']
```
Which returns:
| | 2018 | 2019 | 2020 | 2021 | 2022 |
|:--------------------------------------------|---------:|---------:|---------:|---------:|---------:|
| Gross Margin | 0.383437 | 0.378178 | 0.382332 | 0.417794 | 0.433096 |
| Operating Margin | 0.26694 | 0.24572 | 0.241473 | 0.297824 | 0.302887 |
| Net Profit Margin | 0.224142 | 0.212381 | 0.209136 | 0.258818 | 0.253096 |
| Interest Burden Ratio | 1.02828 | 1.02827 | 1.01211 | 1.00237 | 0.997204 |
| Income Before Tax Profit Margin | 0.274489 | 0.252666 | 0.244398 | 0.298529 | 0.30204 |
| Effective Tax Rate | 0.183422 | 0.159438 | 0.144282 | 0.133023 | 0.162045 |
| Return on Assets (ROA) | 0.162775 | 0.16323 | 0.177256 | 0.269742 | 0.282924 |
| Return on Equity (ROE) | 0.555601 | 0.610645 | 0.878664 | 1.50071 | 1.96959 |
| Return on Invested Capital (ROIC) | 0.269858 | 0.293721 | 0.344126 | 0.503852 | 0.562645 |
| Return on Capital Employed (ROCE) | 0.305968 | 0.297739 | 0.320207 | 0.495972 | 0.613937 |
| Return on Tangible Assets | 0.555601 | 0.610645 | 0.878664 | 1.50071 | 1.96959 |
| Income Quality Ratio | 1.30073 | 1.25581 | 1.4052 | 1.09884 | 1.22392 |
| Net Income per EBT | 0.816578 | 0.840562 | 0.855718 | 0.866977 | 0.837955 |
| Free Cash Flow to Operating Cash Flow Ratio | 0.828073 | 0.848756 | 0.909401 | 0.893452 | 0.912338 |
| EBT to EBIT Ratio | 0.957448 | 0.948408 | 0.958936 | 0.976353 | 0.975982 |
| EBIT to Revenue | 0.286688 | 0.26641 | 0.254864 | 0.305759 | 0.309473 |
"""
empty_data: list = []
if (
not self._api_key
and (
self._balance_sheet_statement.empty
or self._income_statement.empty
or self._cash_flow_statement.empty
)
and self._enforce_source == "FinancialModelingPrep"
):
raise ValueError(
"The ratios class requires an API key from FinancialModelPrep if you wish to enforce the usage. "
"of Financial Modeling Prep. Get an API key here: https://www.jeroenbouma.com/fmp"
)
if self._balance_sheet_statement.empty:
empty_data.append("Balance Sheet Statement")
if self._income_statement.empty:
empty_data.append("Income Statement")
if self._cash_flow_statement.empty:
empty_data.append("Cash Flow Statement")
if empty_data:
empty_data_iterator = (
tqdm(empty_data, desc="Obtaining financial statements")
if ENABLE_TQDM & self._progress_bar
else empty_data
)
for statement in empty_data_iterator:
if statement == "Balance Sheet Statement":
self.get_balance_sheet_statement(progress_bar=False)
if statement == "Income Statement":
self.get_income_statement(progress_bar=False)
if statement == "Cash Flow Statement":
self.get_cash_flow_statement(progress_bar=False)
if (
self._balance_sheet_statement.empty
and self._income_statement.empty
and self._cash_flow_statement.empty
):
raise ValueError(
"The datasets could not be populated and therefore the Ratios class cannot be initialized. "
"This is usually because no tickers are equities, you have reached the API limit or "
"entered an invalid API key."
)
if not self._start_date:
self._start_date = (
f"{self._balance_sheet_statement.columns[0].year - 5}-01-01"
)
if not self._end_date:
self._end_date = (
f"{self._balance_sheet_statement.columns[-1].year + 5}-01-01"
)
if self._quarterly:
self.get_historical_data(period="quarterly")
else:
self.get_historical_data(period="yearly")
historical = {
"period": (
self._quarterly_historical_data
if self._quarterly
else self._yearly_historical_data
),
"daily": self._daily_historical_data,
}
tickers = (
self._balance_sheet_statement.index.get_level_values(0).unique().tolist()
)
ratios = Ratios(
tickers=(
tickers + ["Portfolio"] if "Portfolio" in self._tickers else tickers
),
historical=historical,
balance=self._balance_sheet_statement,
income=self._income_statement,
cash=self._cash_flow_statement,
quarterly=self._quarterly,
rounding=self._rounding,
)
if self._portfolio_weights:
ratios._portfolio_weights = self._portfolio_weights
return ratios
@property
def models(self) -> Models:
"""
Gives access to the Models module. The Models module is meant to execute well-known models
such as DUPONT and the Discounted Cash Flow (DCF) model. These models are also directly
related to the data retrieved from the Toolkit module.
See the following link for more information: https://www.jeroenbouma.com/projects/financetoolkit/docs/models
As an example:
```python
from financetoolkit import Toolkit
toolkit = Toolkit(["TSLA", "AMZN"], api_key="FINANCIAL_MODELING_PREP_KEY", quarterly=True, start_date='2022-12-31')
dupont_analysis = toolkit.models.get_extended_dupont_analysis()
dupont_analysis.loc['AMZN']
```
Which returns:
| | 2022Q2 | 2022Q3 | 2022Q4 | 2023Q1 | 2023Q2 |
|:------------------------|------------:|----------:|------------:|----------:|----------:|
| Interest Burden Ratio | -1.24465 | 0.858552 | -2.88409 | 1.20243 | 1.01681 |
| Tax Burden Ratio | -0.611396 | 1.13743 | 0.101571 | 0.640291 | 0.878792 |
| Operating Profit Margin | -0.0219823 | 0.0231391 | -0.00636042 | 0.0323498 | 0.0562125 |
| Asset Turnover | nan | 0.299735 | 0.3349 | 0.274759 | 0.285319 |
| Equity Multiplier | nan | 3.15403 | 3.14263 | 3.08433 | 2.91521 |
| Return on Equity | nan | 0.0213618 | 0.00196098 | 0.0211066 | 0.0417791 |
"""
empty_data: list = []
if not self._api_key and (
self._balance_sheet_statement.empty
or self._income_statement.empty
or self._cash_flow_statement.empty
):
raise ValueError(
"The models class requires an API key from FinancialModelPrep. "
"Get an API key here: https://www.jeroenbouma.com/fmp"
)
if self._balance_sheet_statement.empty:
empty_data.append("Balance Sheet Statement")
if self._income_statement.empty:
empty_data.append("Income Statement")
if self._cash_flow_statement.empty:
empty_data.append("Cash Flow Statement")
if empty_data:
empty_data_iterator = (
tqdm(empty_data, desc="Obtaining financial statements")
if ENABLE_TQDM & self._progress_bar
else empty_data
)
for statement in empty_data_iterator:
if statement == "Balance Sheet Statement":
self.get_balance_sheet_statement(progress_bar=False)
if statement == "Income Statement":
self.get_income_statement(progress_bar=False)
if statement == "Cash Flow Statement":
self.get_cash_flow_statement(progress_bar=False)
if (
self._balance_sheet_statement.empty
and self._income_statement.empty
and self._cash_flow_statement.empty
):
raise ValueError(
"The datasets could not be populated and therefore the Ratios class cannot be initialized. "
"This is usually because no tickers are equities, you have reached the API limit or "
"entered an invalid API key."
)
if not self._start_date:
self._start_date = (
f"{self._balance_sheet_statement.columns[0].year - 5}-01-01"
)
if not self._end_date:
self._end_date = (
f"{self._balance_sheet_statement.columns[-1].year + 5}-01-01"
)
for period in ["daily", "weekly", "monthly", "quarterly", "yearly"]:
self.get_historical_data(period=period)
historical_data = {
"daily": self._daily_historical_data,
"weekly": self._weekly_historical_data,
"monthly": self._monthly_historical_data,
"quarterly": self._quarterly_historical_data,
"yearly": self._yearly_historical_data,
}
risk_free_rate_data = {
"daily": self._daily_risk_free_rate,
"weekly": self._weekly_risk_free_rate,
"monthly": self._monthly_risk_free_rate,
"quarterly": self._quarterly_risk_free_rate,
"yearly": self._yearly_risk_free_rate,
}
tickers = (
self._balance_sheet_statement.index.get_level_values(0).unique().tolist()
)
return Models(
tickers=tickers,
historical_data=historical_data,
risk_free_rate_data=risk_free_rate_data,
balance=self._balance_sheet_statement,
income=self._income_statement,
cash=self._cash_flow_statement,
quarterly=self._quarterly,
rounding=self._rounding,
)
@property
def options(self) -> Options:
"""
This gives access to the Options module. The Options Module is meant to provide Options valuations
based on real market data. This includes the Black-Scholes model and in the future the Binomial model
and the Monte Carlo model. It also includes all available first-order, second-order and third-order
Greeks such as Delta, Gamma, Theta, Vega, Rho, Charm, Vanna, Vomma, Veta, Speed and Zomma.
It gives insights in the sensitivity of an option to changes in the underlying asset price, volatility,
years to maturity, dividend yilds and interest rates and several derivatives of these sensitivities.
See the following link for more information: https://www.jeroenbouma.com/projects/financetoolkit/docs/options
As an example:
```python
from financetoolkit import Toolkit
toolkit = Toolkit(["TSLA", "MU"], api_key="FINANCIAL_MODELING_PREP_KEY")
all_greeks = toolkit.options.collect_all_greeks(start_date='2024-01-03')
all_greeks.loc['TSLA', '2024-01-04']
```
Which returns:
| Strike Price | Delta | Dual Delta | Vega | Theta | Rho | Epsilon | Lambda | Gamma | Dual Gamma | Vanna | Charm | Vomma | Vera | Veta | PD | Speed | Zomma | Color | Ultima |
|---------------:|--------:|-------------:|-------:|--------:|-------:|----------:|---------:|--------:|-------------:|--------:|---------:|--------:|--------:|----------:|-------:|--------:|--------:|--------:|---------:|
| 180 | 1 | -0.9999 | 0 | -0.0193 | 0.0049 | -0.6533 | 0.0408 | 0 | 0 | -0 | 0 | 0 | -0 | 0 | 0 | -0 | 0 | 0 | 0 |
| 185 | 1 | -0.9999 | 0 | -0.0198 | 0.0051 | -0.6533 | 0.0446 | 0 | 0 | -0 | 0 | 0 | -0 | 0 | 0 | -0 | 0 | 0 | 0 |
| 190 | 1 | -0.9999 | 0 | -0.0204 | 0.0052 | -0.6533 | 0.0492 | 0 | 0 | -0 | 0 | 0 | -0 | 0 | 0 | -0 | 0 | 0 | 0 |
| 195 | 1 | -0.9999 | 0 | -0.0209 | 0.0053 | -0.6533 | 0.0549 | 0 | 0 | -0 | 0 | 0 | -0 | 0 | 0 | -0 | 0 | 0 | 0 |
| 200 | 1 | -0.9999 | 0 | -0.0214 | 0.0055 | -0.6533 | 0.062 | 0 | 0 | -0 | 0 | 0 | -0 | 0.0014 | 0 | -0 | 0 | 0 | 0 |
| 205 | 1 | -0.9999 | 0 | -0.022 | 0.0056 | -0.6533 | 0.0712 | 0 | 0 | -0 | 0.0005 | 0.0003 | -0 | 0.1236 | 0 | -0 | 0 | 0.0004 | 0.0001 |
| 210 | 1 | -0.9999 | 0 | -0.0226 | 0.0058 | -0.6533 | 0.0837 | 0 | 0 | -0.0002 | 0.0221 | 0.0119 | -0.0001 | 4.6313 | 0 | -0 | 0.0001 | 0.0132 | 0.0034 |
| 215 | 0.9998 | -0.9997 | 0.0001 | -0.0254 | 0.0059 | -0.6532 | 0.1016 | 0.0001 | 0.0001 | -0.0044 | 0.4426 | 0.1942 | -0.0029 | 77.6496 | 0.0001 | -0.0001 | 0.0021 | 0.209 | 0.0336 |
| 220 | 0.9973 | -0.9969 | 0.001 | -0.0526 | 0.006 | -0.6515 | 0.1287 | 0.0012 | 0.0014 | -0.0414 | 4.1955 | 1.4351 | -0.0273 | 600.92 | 0.0014 | -0.0005 | 0.0144 | 1.4569 | 0.1196 |
| 225 | 0.9777 | -0.976 | 0.0066 | -0.2079 | 0.006 | -0.6387 | 0.1723 | 0.0076 | 0.0086 | -0.1884 | 19.0888 | 4.7244 | -0.1249 | 2187.89 | 0.0086 | -0.0022 | 0.0407 | 4.1228 | 0.0829 |
| 230 | 0.8953 | -0.8898 | 0.0226 | -0.6528 | 0.0056 | -0.5849 | 0.2419 | 0.0261 | 0.028 | -0.3993 | 40.3564 | 6.2557 | -0.267 | 3816.31 | 0.028 | -0.0048 | 0.0253 | 2.5239 | -0.1641 |
| 235 | 0.6978 | -0.6874 | 0.0435 | -1.2304 | 0.0044 | -0.4558 | 0.3442 | 0.0502 | 0.0516 | -0.306 | 30.653 | 1.9785 | -0.2119 | 3623.7 | 0.0516 | -0.0039 | -0.0672 | -6.8719 | -0.0977 |
| 240 | 0.4192 | -0.4078 | 0.0488 | -1.3691 | 0.0027 | -0.2739 | 0.4789 | 0.0562 | 0.0555 | 0.1634 | -17.1438 | 0.4159 | 0.0934 | 3407.79 | 0.0555 | 0.0014 | -0.096 | -9.7512 | -0.0222 |
| 245 | 0.1812 | -0.1736 | 0.0329 | -0.9207 | 0.0012 | -0.1184 | 0.6396 | 0.0379 | 0.0359 | 0.4445 | -45.5549 | 5.0536 | 0.2814 | 4080.87 | 0.0359 | 0.0048 | -0.0098 | -0.9474 | -0.1945 |
| 250 | 0.0544 | -0.0513 | 0.0138 | -0.3848 | 0.0004 | -0.0355 | 0.8183 | 0.0159 | 0.0144 | 0.3232 | -33.01 | 6.468 | 0.2073 | 3328.37 | 0.0144 | 0.0036 | 0.0461 | 4.7176 | -0.0443 |
| 255 | 0.0112 | -0.0104 | 0.0037 | -0.1028 | 0.0001 | -0.0073 | 1.0084 | 0.0042 | 0.0037 | 0.1223 | -12.477 | 3.4845 | 0.0789 | 1542.52 | 0.0037 | 0.0014 | 0.0325 | 3.3216 | 0.1424 |
| 260 | 0.0016 | -0.0015 | 0.0006 | -0.018 | 0 | -0.001 | 1.205 | 0.0007 | 0.0006 | 0.0276 | -2.8148 | 1.0161 | 0.0179 | 421.028 | 0.0006 | 0.0003 | 0.0104 | 1.0578 | 0.1054 |
| 265 | 0.0002 | -0.0001 | 0.0001 | -0.0021 | 0 | -0.0001 | 1.4049 | 0.0001 | 0.0001 | 0.004 | -0.4041 | 0.1783 | 0.0026 | 71.3544 | 0.0001 | 0 | 0.0019 | 0.1933 | 0.0322 |
| 270 | 0 | -0 | 0 | -0.0002 | 0 | -0 | 1.6059 | 0 | 0 | 0.0004 | -0.0385 | 0.02 | 0.0002 | 7.8471 | 0 | 0 | 0.0002 | 0.0222 | 0.0054 |
| 275 | 0 | -0 | 0 | -0 | 0 | -0 | 1.8068 | 0 | 0 | 0 | -0.0025 | 0.0015 | 0 | 0.5804 | 0 | 0 | 0 | 0.0017 | 0.0006 |
| 280 | 0 | -0 | 0 | -0 | 0 | -0 | 2.0066 | 0 | 0 | 0 | -0.0001 | 0.0001 | 0 | 0.0297 | 0 | 0 | 0 | 0.0001 | 0 |
| 285 | 0 | -0 | 0 | -0 | 0 | -0 | 2.2048 | 0 | 0 | 0 | -0 | 0 | 0 | 0.0011 | 0 | 0 | 0 | 0 | 0 |
| 290 | 0 | -0 | 0 | -0 | 0 | -0 | 2.401 | 0 | 0 | 0 | -0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| 295 | 0 | -0 | 0 | -0 | 0 | -0 | 2.595 | 0 | 0 | 0 | -0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
"""
if not self._start_date:
self._start_date = (datetime.today() - timedelta(days=365 * 10)).strftime(
"%Y-%m-%d"
)
if not self._end_date:
self._end_date = datetime.today().strftime("%Y-%m-%d")
self.get_historical_data(period="daily")
self.get_historical_data(period="yearly")
return Options(
tickers=self._tickers,
daily_historical=self._daily_historical_data,
annual_historical=self._yearly_historical_data,
risk_free_rate=self._daily_risk_free_rate,
quarterly=self._quarterly,
rounding=self._rounding,
)
@property
def technicals(self) -> Technicals:
"""
This gives access to the Technicals module. The Technicals Module contains
nearly 50 Technical Indicators that can be used to analyse companies. These indicators are
divided into 3 categories: breadth, overlap and volatility. Each indicator is calculated using
the data from the Toolkit module.
Some examples of technical indicators are the Average Directional Index (ADX), the
Accumulation/Distribution Line (ADL), the Average True Range (ATR), the Bollinger Bands (BBANDS),
the Commodity Channel Index (CCI), the Chaikin Oscillator (CHO), the Chaikin Money Flow (CMF),
the Double Exponential Moving Average (DEMA), the Exponential Moving Average (EMA) and
the Moving Average Convergence Divergence (MACD).
See the following link for more information: https://www.jeroenbouma.com/projects/financetoolkit/docs/technicals
As an example:
```python
from financetoolkit import Toolkit
toolkit = Toolkit(["AAPL", "TSLA"], api_key="FINANCIAL_MODELING_PREP_KEY")
average_directional_index = toolkit.technicals.get_average_directional_index()
```
Which returns:
| Date | AAPL | MSFT |
|:-----------|--------:|--------:|
| 2023-08-21 | 62.8842 | 36.7468 |
| 2023-08-22 | 65.7063 | 36.5525 |
| 2023-08-23 | 67.3596 | 35.5149 |
| 2023-08-24 | 66.4527 | 35.4399 |
| 2023-08-25 | 63.4837 | 32.3323 |
"""
if not self._start_date:
self._start_date = (datetime.today() - timedelta(days=365 * 10)).strftime(
"%Y-%m-%d"
)
if not self._end_date:
self._end_date = datetime.today().strftime("%Y-%m-%d")
for period in ["daily", "weekly", "monthly", "quarterly", "yearly"]:
self.get_historical_data(period=period)
if self._intraday_period:
if self._intraday_period in ["1min", "5min", "15min", "30min", "1hour"]:
self.get_intraday_data(period=self._intraday_period)
else:
raise ValueError(
"The intraday period must be one of '1min', '5min', '15min', '30min' or '1hour'."
)
tickers = (
self._daily_historical_data.columns.get_level_values(1).unique().tolist()
)
historical_data = {
"intraday": self._intraday_historical_data,
"daily": self._daily_historical_data,
"weekly": self._weekly_historical_data,
"monthly": self._monthly_historical_data,
"quarterly": self._quarterly_historical_data,
"yearly": self._yearly_historical_data,
}
technicals = Technicals(
tickers=(
tickers + ["Portfolio"] if "Portfolio" in self._tickers else tickers
),
historical_data=historical_data,
rounding=self._rounding,
start_date=self._start_date,
end_date=self._end_date,
)
if self._portfolio_weights:
technicals._portfolio_weights = self._portfolio_weights
return technicals
@property
def performance(self) -> Performance:
"""
This gives access to the Performance module. The Performance Module is meant to calculate metrics related
to the risk-return relationship. These are things such as Beta, Sharpe Ratio, Sortino Ratio, CAPM,
Alpha and the Treynor Ratio.
It gives insights in the performance a stock has to e.g. a benchmark that is not easily identified by
looking at the raw data. This class is closely related to the Risk class which highlights things
such as Value at Risk (VaR) and Maximum Drawdown.
See the following link for more information: https://www.jeroenbouma.com/projects/financetoolkit/docs/performance
As an example:
```python