Skip to content

Commit

Permalink
Merge branch 'develop'
Browse files Browse the repository at this point in the history
  • Loading branch information
JerBouma committed May 23, 2024
2 parents 7644751 + 0e2d03f commit 2119a21
Show file tree
Hide file tree
Showing 14 changed files with 4,937 additions and 48 deletions.
221 changes: 221 additions & 0 deletions README.md

Large diffs are not rendered by default.

4,486 changes: 4,486 additions & 0 deletions examples/Finance Toolkit - 0. README Examples.ipynb

Large diffs are not rendered by default.

11 changes: 10 additions & 1 deletion financetoolkit/fundamentals_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,16 @@ def worker(ticker, analyst_estimates_dict):
try:
analyst_estimates = analyst_estimates.drop("symbol", axis=1)

# One day is deducted from the date because it could be that
# the date is reported as 2023-07-01 while the data is about the
# second quarter of 2023. This usually happens when companies
# have a different financial year than the calendar year. It doesn't
# matter for others that are correctly reporting since 2023-06-31
# minus one day is still 2023
analyst_estimates["date"] = pd.to_datetime(
analyst_estimates["date"]
) - pd.offsets.Day(1)

if quarter:
analyst_estimates["date"] = pd.to_datetime(
analyst_estimates["date"]
Expand Down Expand Up @@ -593,7 +603,6 @@ def worker(ticker, analyst_estimates_dict):
"issues when values are zero and is predominantly relevant for "
"ratio calculations."
)

analyst_estimates_total.columns = pd.PeriodIndex(
analyst_estimates_total.columns, freq="Q" if quarter else "Y"
)
Expand Down
69 changes: 68 additions & 1 deletion financetoolkit/technicals/overlap_model.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
"""Profitability Module"""
"""Overlap Module"""
__docformat__ = "google"

import numpy as np
import pandas as pd
from scipy.signal import argrelextrema


def get_moving_average(prices: pd.Series, window: int) -> pd.Series:
Expand Down Expand Up @@ -89,3 +91,68 @@ def get_triangular_moving_average(prices: pd.Series, window: int) -> pd.Series:
tri_ma = tri_sum / ((window + 1) / 2)

return tri_ma


def get_support_resistance_levels(
prices: pd.Series, window: int = 5, sensitivity: float = 0.05
):
"""
Calculate support and resistance levels from historical price data.
Parameters:
prices (pd.Series): A pandas Series of historical closing prices.
window (int): The window size to use for identifying local maxima and minima.
sensitivity (float): The sensitivity threshold for identifying levels.
Returns:
support_resistance_levels (pd.DataFrame): A DataFrame with support and resistance levels.
The DataFrame has two columns: "Resistance" and "Support".
The index of the DataFrame represents the dates, and the values represent the levels.
"""
# Identify local maxima and minima
local_maxima_indices = argrelextrema(prices.values, np.greater, order=window)[0]
local_minima_indices = argrelextrema(prices.values, np.less, order=window)[0]

local_maxima_prices = prices.iloc[local_maxima_indices]
local_minima_prices = prices.iloc[local_minima_indices]

# Initialize dictionaries for support and resistance levels
resistance_levels: dict[pd.PeriodIndex, float] = {}
support_levels: dict[pd.PeriodIndex, float] = {}

# Calculate resistance levels
for idx, price in zip(local_maxima_indices, local_maxima_prices):
if not resistance_levels:
resistance_levels[prices.index[idx]] = price
else:
close_to_existing = False
for date, level in resistance_levels.items():
if abs(price - level) / level < sensitivity:
resistance_levels[date] = (resistance_levels[date] + price) / 2
close_to_existing = True
break
if not close_to_existing:
resistance_levels[prices.index[idx]] = price

# Calculate support levels
for idx, price in zip(local_minima_indices, local_minima_prices):
if not support_levels:
support_levels[prices.index[idx]] = price
else:
close_to_existing = False
for date, level in support_levels.items():
if abs(price - level) / level < sensitivity:
support_levels[date] = (support_levels[date] + price) / 2
close_to_existing = True
break
if not close_to_existing:
support_levels[prices.index[idx]] = price

support_resistance_levels = pd.DataFrame(
{
"Resistance": pd.Series(resistance_levels),
"Support": pd.Series(support_levels),
}
).sort_index()

return support_resistance_levels
103 changes: 103 additions & 0 deletions financetoolkit/technicals/technicals_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -3135,6 +3135,109 @@ def get_triangular_moving_average(

return triangular_moving_average.round(rounding if rounding else self._rounding)

@handle_errors
def get_support_resistance_levels(
self,
sensitivity: float = 0.05,
period: str = "daily",
close_column: str = "Adj Close",
window: int = 14,
rounding: int | None = None,
) -> pd.Series | pd.DataFrame:
"""
Retrieves the support and resistance levels for the specified period and assets.
The Support and Resistance Levels are price levels where the price tends to stop and reverse.
- Support Levels: These are the valleys where the price tends to stop going down and may start to go up.
Think of support levels as "floors" that the price has trouble falling below.
- Resistance Levels: These are the peaks where the price tends to stop going up and may start to go down.
Think of resistance levels as "ceilings" that the price has trouble breaking through.
It does so by:
- Looking for Peaks and Valleys: The function looks at the stock prices and finds the high points
(peaks) and low points (valleys) over time.
- Grouping Similar Peaks and Valleys: Sometimes, prices will stop at similar points multiple times.
The function groups these similar peaks and valleys together to identify key resistance and
support levels.
Args:
sensitivity (float, optional): The sensitivity parameter to determine the significance of the peaks
and valleys. A higher sensitivity value will result in fewer support and resistance levels
being identified. Defaults to 0.05.
period (str, optional): The time period to consider for historical data.
Can be "daily", "weekly", "quarterly", or "yearly". Defaults to "daily".
close_column (str, optional): The column name for closing prices in the historical data.
Defaults to "Adj Close".
window (int, optional): Number of periods for calculating support and resistance levels.
The number of periods (time intervals) over which to calculate the support and resistance levels.
Defaults to 14.
rounding (int | None, optional): The number of decimals to round the results to.
If None, the rounding value specified during the initialization of the Toolkit instance will be used.
Defaults to None.
Returns:
pd.DataFrame: The support and resistance levels for each asset.
Raises:
ValueError: If the specified `period` is not one of the valid options.
Notes:
- The method retrieves historical data based on the specified `period` and calculates the
support and resistance levels for each asset in the Toolkit instance.
As an example:
```python
from financetoolkit import Toolkit
toolkit = Toolkit(tickers=["AAPL", "MSFT"])
support_resistance_levels = toolkit.technicals.get_support_resistance_levels()
```
"""
if period not in [
"intraday",
"daily",
"weekly",
"monthly",
"quarterly",
"yearly",
]:
raise ValueError(
"Period must be intraday, daily, weekly, monthly, quarterly, or yearly."
)
if period == "intraday":
if self._historical_data[period].empty:
raise ValueError(
"Please define the 'intraday_period' parameter when initializing the Toolkit."
)
close_column = "Close"

historical_data = self._historical_data[period]

support_resistance_levels = {}

for ticker in historical_data[close_column].columns:
support_resistance_levels[
ticker
] = overlap_model.get_support_resistance_levels(
prices=historical_data[close_column][ticker],
window=window,
sensitivity=sensitivity,
)

support_resistance_levels_df = (
pd.concat(support_resistance_levels, axis=1)
.swaplevel(1, 0, axis=1)
.sort_index(axis=1)
)

return support_resistance_levels_df.round(
rounding if rounding else self._rounding
)

def collect_volatility_indicators(
self,
period: str = "daily",
Expand Down
13 changes: 13 additions & 0 deletions financetoolkit/toolkit_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -3029,8 +3029,21 @@ def get_income_statement(
income_statement = self._income_statement

if trailing:
# This is a special case where the trailing period does not make sense
# for the Weighted Average Shares and Weighted Average Shares Diluted.
weighted_average_shares = income_statement.loc[
:, ["Weighted Average Shares", "Weighted Average Shares Diluted"], :
]

# The rolling window is calculated for the rest of the income statement.
income_statement = self._income_statement.T.rolling(trailing).sum().T

# The Weighted Average Shares and Weighted Average Shares Diluted should
# not be summed up but rather kept equal to the current value.
income_statement.loc[
weighted_average_shares.index
] = weighted_average_shares

if growth:
self._income_statement_growth = _calculate_growth(
income_statement,
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "financetoolkit"
version = "1.9.0"
version = "1.9.1"
description = "Transparent and Efficient Financial Analysis"
license = "MIT"
authors = ["Jeroen Bouma"]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,5 @@ Date,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
2016,29.0,63.0,225.0,29.0,63.0,225.0,29.0,62.0,223.0,29.0,62.0,224.0,27.0,57.0,198.0,122345200,25579900,108998300,1.0,1.0,5.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,3.0,3.0,2.0
2017,43.0,86.0,269.0,43.0,86.0,269.0,42.0,86.0,267.0,42.0,86.0,267.0,40.0,80.0,241.0,103999600,18717400,96007400,1.0,2.0,5.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,4.0,4.0,2.0
2018,40.0,101.0,250.0,40.0,102.0,250.0,39.0,100.0,247.0,39.0,102.0,250.0,38.0,96.0,230.0,140014000,33173800,144299400,1.0,2.0,5.0,-0.0,0.0,-0.0,0.0,0.0,0.0,-0.0,0.0,-0.0,0.0,0.0,0.0,4.0,4.0,2.0
2019,72.0,157.0,321.0,73.0,158.0,322.0,72.0,156.0,320.0,73.0,158.0,322.0,71.0,152.0,301.0,100805600,18369400,57077300,1.0,2.0,6.0,1.0,1.0,0.0,0.0,0.0,0.0,1.0,1.0,0.0,0.0,0.0,0.0,7.0,7.0,3.0
2019,72.0,157.0,321.0,73.0,158.0,322.0,72.0,156.0,320.0,73.0,158.0,322.0,71.0,151.0,301.0,100805600,18369400,57077300,1.0,2.0,6.0,1.0,1.0,0.0,0.0,0.0,0.0,1.0,1.0,0.0,0.0,0.0,0.0,7.0,7.0,3.0
2020,136.0,225.0,372.0,136.0,226.0,373.0,133.0,221.0,372.0,134.0,222.0,372.0,131.0,215.0,355.0,96452100,20272300,49455300,1.0,2.0,6.0,1.0,0.0,0.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,0.0,0.0,13.0,10.0,4.0
Original file line number Diff line number Diff line change
Expand Up @@ -10,29 +10,3 @@
2020-08,0.4,-0.5,-0.5,-0.1,0.3,0.7,-0.2,-0.4,-0.1,-0.3
2020-09,0.8,-0.4,-0.5,0.2,0.1,0.9,-0.4,-0.6,0.3,-0.0
2020-10,0.8,-0.3,-0.4,0.3,0.3,0.9,-0.3,-0.4,0.1,-0.1
2020-11,0.5,-0.6,-0.6,-0.3,-0.3,0.7,-0.5,-0.5,-0.2,-0.5
2020-12,0.6,0.2,-0.1,0.1,0.4,0.2,-0.4,-0.4,-0.1,-0.1
2021-01,0.5,-0.3,-0.6,0.3,0.1,0.6,-0.6,-0.7,0.1,-0.3
2021-02,0.8,0.3,-0.6,-0.6,-0.5,0.6,0.1,-0.7,-0.5,-0.5
2021-03,0.8,0.1,-0.8,-0.7,-0.4,0.7,-0.1,-0.8,-0.6,-0.5
2021-04,0.7,-0.2,-0.3,-0.1,-0.1,0.7,-0.1,-0.3,-0.1,-0.1
2021-05,0.9,0.2,-0.4,-0.0,-0.2,0.9,0.2,-0.4,0.0,-0.3
2021-06,0.5,-0.1,-0.3,-0.1,0.0,0.6,-0.4,-0.6,-0.4,-0.3
2021-07,0.6,-0.3,0.2,0.5,0.5,0.6,-0.4,-0.0,0.3,0.1
2021-08,0.6,-0.2,-0.3,0.2,0.3,0.5,-0.1,-0.3,0.3,-0.1
2021-09,0.8,-0.0,-0.1,-0.3,0.2,0.8,-0.3,-0.3,-0.2,-0.2
2021-10,0.8,-0.2,-0.3,-0.3,-0.1,0.4,-0.4,-0.6,0.2,-0.6
2021-11,0.3,-0.1,-0.2,0.1,0.1,0.9,-0.1,-0.3,0.0,-0.3
2021-12,0.8,0.2,-0.5,-0.2,-0.2,0.8,0.2,-0.5,-0.2,-0.2
2022-01,0.9,0.0,-0.6,-0.5,-0.3,0.7,-0.2,-0.7,-0.3,-0.5
2022-02,0.9,0.1,-0.7,-0.2,-0.8,0.9,-0.0,-0.9,-0.4,-0.9
2022-03,0.9,0.0,-0.6,-0.3,-0.6,0.9,-0.0,-0.7,-0.1,-0.8
2022-04,0.9,-0.2,-0.6,-0.1,-0.4,0.8,-0.2,-0.6,-0.2,-0.4
2022-05,0.9,-0.1,-0.5,-0.4,-0.2,0.9,-0.0,-0.6,-0.5,-0.3
2022-06,0.9,-0.1,-0.5,-0.3,-0.3,0.9,-0.1,-0.6,-0.3,-0.4
2022-07,0.8,0.2,-0.6,-0.5,-0.7,0.8,-0.0,-0.6,-0.4,-0.7
2022-08,0.9,0.3,-0.5,-0.3,-0.6,0.9,0.2,-0.5,-0.3,-0.6
2022-09,0.7,-0.0,-0.5,-0.0,-0.4,0.9,0.1,-0.6,-0.1,-0.5
2022-10,0.9,-0.0,-0.5,-0.0,-0.4,0.8,-0.1,-0.5,-0.0,-0.5
2022-11,0.9,0.4,-0.8,-0.3,-0.8,0.9,0.3,-0.7,-0.2,-0.7
2022-12,0.9,0.3,-0.6,-0.3,-0.6,0.9,0.3,-0.6,-0.5,-0.7
Original file line number Diff line number Diff line change
Expand Up @@ -304,8 +304,8 @@ Date,,Mkt-RF,SMB,HML,RMW,CMA
2023,HML,-0.05,0.41,1.0,0.22,0.62
2023,RMW,-0.34,-0.4,0.22,1.0,0.31
2023,CMA,-0.45,0.08,0.62,0.31,1.0
2024,Mkt-RF,1.0,0.52,-0.11,-0.35,-0.42
2024,SMB,0.52,1.0,0.03,-0.55,0.05
2024,HML,-0.11,0.03,1.0,0.42,0.53
2024,RMW,-0.35,-0.55,0.42,1.0,0.17
2024,CMA,-0.42,0.05,0.53,0.17,1.0
2024,Mkt-RF,1.0,0.28,-0.19,-0.27,-0.52
2024,SMB,0.28,1.0,0.38,-0.59,0.36
2024,HML,-0.19,0.38,1.0,0.09,0.47
2024,RMW,-0.27,-0.59,0.09,1.0,-0.09
2024,CMA,-0.52,0.36,0.47,-0.09,1.0
Original file line number Diff line number Diff line change
Expand Up @@ -3627,10 +3627,20 @@ Date,,Mkt-RF,SMB,HML,RMW,CMA
2023-12,Mkt-RF,1.0,0.51,0.39,-0.66,-0.03
2023-12,SMB,0.51,1.0,0.86,-0.63,0.57
2023-12,HML,0.39,0.86,1.0,-0.34,0.55
2023-12,RMW,-0.66,-0.63,-0.34,1.0,-0.25
2023-12,CMA,-0.03,0.57,0.55,-0.25,1.0
2023-12,RMW,-0.66,-0.63,-0.34,1.0,-0.24
2023-12,CMA,-0.03,0.57,0.55,-0.24,1.0
2024-01,Mkt-RF,1.0,0.52,-0.11,-0.35,-0.42
2024-01,SMB,0.52,1.0,0.03,-0.55,0.05
2024-01,HML,-0.11,0.03,1.0,0.42,0.53
2024-01,RMW,-0.35,-0.55,0.42,1.0,0.17
2024-01,CMA,-0.42,0.05,0.53,0.17,1.0
2024-02,Mkt-RF,1.0,0.22,-0.27,-0.27,-0.57
2024-02,SMB,0.22,1.0,0.41,-0.72,0.41
2024-02,HML,-0.27,0.41,1.0,0.01,0.37
2024-02,RMW,-0.27,-0.72,0.01,1.0,-0.23
2024-02,CMA,-0.57,0.41,0.37,-0.23,1.0
2024-03,Mkt-RF,1.0,0.05,-0.2,-0.17,-0.57
2024-03,SMB,0.05,1.0,0.7,-0.43,0.52
2024-03,HML,-0.2,0.7,1.0,-0.2,0.55
2024-03,RMW,-0.17,-0.43,-0.2,1.0,-0.13
2024-03,CMA,-0.57,0.52,0.55,-0.13,1.0
Original file line number Diff line number Diff line change
Expand Up @@ -121,5 +121,5 @@ Date,,HML,Mkt-RF
2022,Mkt-RF,-0.56,1.0
2023,HML,1.0,-0.05
2023,Mkt-RF,-0.05,1.0
2024,HML,1.0,-0.11
2024,Mkt-RF,-0.11,1.0
2024,HML,1.0,-0.19
2024,Mkt-RF,-0.19,1.0
Original file line number Diff line number Diff line change
Expand Up @@ -365,9 +365,9 @@ Date,,Mkt-RF,SMB,HML,RMW,CMA,RF
2023,RMW,-0.34,-0.4,0.22,1.0,0.31,0.0
2023,CMA,-0.45,0.08,0.62,0.31,1.0,0.13
2023,RF,-0.01,0.06,0.22,0.0,0.13,1.0
2024,Mkt-RF,1.0,0.52,-0.11,-0.35,-0.42,
2024,SMB,0.52,1.0,0.03,-0.55,0.05,
2024,HML,-0.11,0.03,1.0,0.42,0.53,
2024,RMW,-0.35,-0.55,0.42,1.0,0.17,
2024,CMA,-0.42,0.05,0.53,0.17,1.0,
2024,RF,,,,,,
2024,Mkt-RF,1.0,0.28,-0.19,-0.27,-0.52,-0.1
2024,SMB,0.28,1.0,0.38,-0.59,0.36,-0.12
2024,HML,-0.19,0.38,1.0,0.09,0.47,-0.1
2024,RMW,-0.27,-0.59,0.09,1.0,-0.09,0.04
2024,CMA,-0.52,0.36,0.47,-0.09,1.0,-0.02
2024,RF,-0.1,-0.12,-0.1,0.04,-0.02,1.0
12 changes: 9 additions & 3 deletions tests/performance/test_performance_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,20 @@ def test_get_capital_asset_pricing_model(recorder):


def test_get_factor_asset_correlations(recorder):
recorder.capture(performance_module.get_factor_asset_correlations().round(1))
recorder.capture(
performance_module.get_factor_asset_correlations(period="monthly").round(1)
performance_module.get_factor_asset_correlations().round(1).iloc[:10]
)
recorder.capture(
performance_module.get_factor_asset_correlations(period="monthly")
.round(1)
.iloc[:10]
)
recorder.capture(
performance_module.get_factor_asset_correlations(
factors_to_calculate=["HML", "Mkt-RF"]
).round(1)
)
.round(1)
.iloc[:10]
)


Expand Down

0 comments on commit 2119a21

Please sign in to comment.