Coverage for src/scrilla/analysis/markets.py: 65%
83 statements
« prev ^ index » next coverage.py v6.4.2, created at 2022-07-18 18:14 +0000
« prev ^ index » next coverage.py v6.4.2, created at 2022-07-18 18:14 +0000
1# This file is part of scrilla: https://github.com/chinchalinchin/scrilla.
3# scrilla is free software: you can redistribute it and/or modify
4# it under the terms of the GNU General Public License version 3
5# as published by the Free Software Foundation.
7# scrilla is distributed in the hope that it will be useful,
8# but WITHOUT ANY WARRANTY; without even the implied warranty of
9# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10# GNU General Public License for more details.
12# You should have received a copy of the GNU General Public License
13# along with scrilla. If not, see <https://www.gnu.org/licenses/>
14# or <https://github.com/chinchalinchin/scrilla/blob/develop/main/LICENSE>.
15"""
16A module of functions that calculate financial statistics.
17"""
18import datetime
19from typing import Dict, Union
20from datetime import date
21from scrilla import settings, services, files, cache
22from scrilla.static import keys
23from scrilla.analysis.objects.cashflow import Cashflow
24import scrilla.analysis.models.geometric.statistics as statistics
25from scrilla.util import errors
26import scrilla.util.outputter as outputter
28logger = outputter.Logger('scrilla.analysis.markets', settings.LOG_LEVEL)
29profile_cache = cache.ProfileCache()
32def sharpe_ratio(ticker: str, start_date: Union[date, None] = None, end_date: Union[date, None] = None, risk_free_rate: Union[float, None] = None, ticker_profile: Union[dict, None] = None, method: str = settings.ESTIMATION_METHOD, cache_in: bool = True, cache_out: bool = True) -> float:
33 """
34 Calculates the sharpe ratio for the supplied ticker over the specified time range. If no start and end date are supplied, calculation will default to the last 100 days of prices. The risk free rate and ticker risk profile can be passed in to force calculations without historical data.
36 Parameters
37 ----------
38 1. **ticker**: ``str``
39 A string of the ticker symbol whose sharpe ratio will be computed.
40 2. **start_date**: ``datetime.date``
41 Start date of the time period for which the sharpe ratio will be computed.
42 3. **end_date**: ``datetime.date``
43 End_date of the time period for which the sharpe ratio will be computed.
44 4. **risk_free_rate**: ``float``
45 Risk free rate used to evaluate excess return. Defaults to settings.RISK_FREE_RATE.
46 5. **ticker_profile**: ``dict``
47 Risk-return profile for the supplied ticker. Formatted as follows: `{ 'annual_return': float, 'annual_volatility': float } `
48 6. **method** : ``str``
49 Estimation method used to calculate financial statistics. Defaults to the value set by `scrilla.settings.ESTIMATION_METHOD`. Allowable value are accessible through the `scrilla.keys.keys` dictionary.
50 7. **cache_in**: ``bool``
51 Flag to tell function to search cache defined by `scrilla.settings.CACHE_MODE` before computing sharpe ratio. Defaults to `True`.
52 8. **cache_out**: ``bool``
53 Flag to tell function to save sharpe ratio to the cache defined by `scrilla.settings.CACHE_MODE`. Defaults to `True`.
55 .. notes::
56 * if ``ticker_profile`` is provided, this function will skip both an external data service call and the calculation of the ticker's risk profile. The calculation will proceed as if the supplied profile were the true profile. If ``ticker_profile`` is not provided, all statistics will be estimated from historical data.
57 """
58 start_date, end_date = errors.validate_dates(start_date=start_date, end_date=end_date,
59 asset_type=keys.keys['ASSETS']['EQUITY'])
61 if cache_in and (ticker_profile is None or ticker_profile.get(keys.keys['STATISTICS']['RETURN']) is None 61 ↛ 63line 61 didn't jump to line 63, because the condition on line 61 was never true
62 or ticker_profile.get(keys.keys['STATISTICS']['VOLATILITY']) is None):
63 result = profile_cache.filter(
64 ticker=ticker, start_date=start_date, end_date=end_date, method=method)
66 if result is not None and result.get(keys.keys['STATISTICS']['SHARPE']) is not None:
67 return result[keys.keys['STATISTICS']['SHARPE']]
69 ticker_profile = statistics.calculate_risk_return(
70 ticker=ticker, start_date=start_date, end_date=end_date, method=method)
72 if risk_free_rate is None: 72 ↛ 75line 72 didn't jump to line 75, because the condition on line 72 was never false
73 risk_free_rate = services.get_risk_free_rate()
75 sh_ratio = (ticker_profile[keys.keys['STATISTICS']['RETURN']] -
76 risk_free_rate)/ticker_profile[keys.keys['STATISTICS']['VOLATILITY']]
78 if cache_out: 78 ↛ 82line 78 didn't jump to line 82, because the condition on line 78 was never false
79 profile_cache.save_or_update_row(ticker=ticker, start_date=start_date,
80 end_date=end_date, sharpe_ratio=sh_ratio, method=method)
82 return sh_ratio
85def market_premium(start_date: Union[date, None] = None, end_date: Union[date, None] = None, market_profile: Union[dict, None] = None, method: str = settings.ESTIMATION_METHOD) -> float:
86 """
87 Returns the excess of the market return defined by the environment variable `MARKET_PROXY` over the risk free rate defined by the `RISK_FREE` environment variable.
89 Parameters
90 ----------
91 1. **start_date**: ``datetime.date``
92 *Optional*. Start date of the time period for which the market premium will be computed. Defaults to 100 days ago.
93 2. **end_date**: ``datetime.date``
94 *Optional*. End_date of the time period for which the market premium will be computed. Defaults to today.
95 3. **market_profile**: ``dict``
96 *Optional*. Manually inputted market risk profile. Will override call to `scrilla.analysis.models.geometric.statistics.calculate_risk_return`.
97 4. **method** : ``str``
98 *Optional*. Estimation method used to calculate financial statistics. Defaults to the value set by `scrilla.settings.ESTIMATION_METHOD`. Allowable value are accessible through the `scrilla.keys.keys` dictionary.
100 """
101 start_date, end_date = errors.validate_dates(start_date=start_date, end_date=end_date,
102 asset_type=keys.keys['ASSETS']['EQUITY'])
104 if market_profile is None: 104 ↛ 108line 104 didn't jump to line 108, because the condition on line 104 was never false
105 market_profile = profile_cache.filter(
106 ticker=settings.MARKET_PROXY, start_date=start_date, end_date=end_date, method=method)
108 if market_profile is None or market_profile.get(keys.keys['STATISTICS']['RETURN']) is None:
109 market_profile = statistics.calculate_risk_return(
110 ticker=settings.MARKET_PROXY, start_date=start_date, end_date=end_date, method=method)
112 market_prem = (
113 market_profile[keys.keys['STATISTICS']['RETURN']] - services.get_risk_free_rate())
114 return market_prem
117def market_beta(ticker: str, start_date: Union[date, None] = None, end_date: Union[date, None] = None, market_profile: Union[dict, None] = None, market_correlation: Union[dict, None] = None, ticker_profile: Union[dict, None] = None, sample_prices: Union[dict, None] = None, method: str = settings.ESTIMATION_METHOD, cache_in: bool = True, cache_out: bool = True) -> float:
118 """
119 Returns the beta of an asset against the market return defined by the ticker symbol set `scrilla.settings.MARKET_PROXY`, which in turn is configured through the environment variable of the same name, `MARKET_PROXY`.
121 Parameters
122 ----------
123 1. **ticker**: ``str``
124 A string of the ticker symbol whose asset beta will be computed.
125 2. **start_date**: ``datetime.date``
126 Start date of the time period for which the asset beta will be computed.
127 3. **end_date**: ``datetime.date``
128 End_date of the time period for which the asset beta will be computed.
129 4. **method** : ``str``
130 Estimation method used to calculate financial statistics. Defaults to the value set by `scrilla.settings.ESTIMATION_METHOD`. Allowable value are accessible through the `scrilla.keys.keys` dictionary.
131 7. **cache_in**: ``bool``
132 Flag to tell function to search cache defined by `scrilla.settings.CACHE_MODE` before computing sharpe ratio. Defaults to `True`.
133 8. **cache_out**: ``bool``
134 Flag to tell function to save sharpe ratio to the cache defined by `scrilla.settings.CACHE_MODE`. Defaults to `True`.
136 .. notes::
137 * If not configured by an environment variable, `scrilla.settings.MARKET_PROXY` defaults to ``SPY``, the ETF tracking the *S&P500*.
138 """
139 start_date, end_date = errors.validate_dates(start_date=start_date, end_date=end_date,
140 asset_type=keys.keys['ASSETS']['EQUITY'])
141 if cache_in and ticker_profile is None:
142 ticker_profile = profile_cache.filter(
143 ticker=ticker, start_date=start_date, end_date=end_date, method=method)
145 if ticker_profile is not None and ticker_profile.get(keys.keys['STATISTICS']['BETA']) is not None:
146 return ticker_profile[keys.keys['STATISTICS']['BETA']]
148 if market_profile is None or market_profile.get(keys.keys['STATISTICS']['RETURN']) is None \ 148 ↛ 156line 148 didn't jump to line 156, because the condition on line 148 was never false
149 or market_profile.get(keys.keys['STATISTICS']['VOLATILITY']) is None:
150 if sample_prices is None: 150 ↛ 154line 150 didn't jump to line 154, because the condition on line 150 was never false
151 market_profile = statistics.calculate_risk_return(ticker=settings.MARKET_PROXY, start_date=start_date,
152 end_date=end_date, method=method)
153 else:
154 market_profile = statistics.calculate_risk_return(ticker=settings.MARKET_PROXY, method=method,
155 sample_prices=sample_prices[settings.MARKET_PROXY])
156 if ticker_profile is None or ticker_profile.get(keys.keys['STATISTICS']['RETURN']) is None \
157 or ticker_profile.get(keys.keys['STATISTICS']['VOLATILITY']) is None:
158 if sample_prices is None: 158 ↛ 162line 158 didn't jump to line 162, because the condition on line 158 was never false
159 ticker_profile = statistics.calculate_risk_return(ticker=ticker, start_date=start_date,
160 end_date=end_date, method=method)
161 else:
162 ticker_profile = statistics.calculate_risk_return(ticker=ticker, method=method,
163 sample_prices=sample_prices[ticker])
165 market_covariance = statistics.calculate_return_covariance(ticker_1=ticker, ticker_2=settings.MARKET_PROXY,
166 profile_1=ticker_profile, profile_2=market_profile,
167 correlation=market_correlation,
168 sample_prices=sample_prices,
169 start_date=start_date, end_date=end_date)
171 beta = market_covariance / (market_profile['annual_volatility']**2)
173 if cache_out: 173 ↛ 177line 173 didn't jump to line 177, because the condition on line 173 was never false
174 profile_cache.save_or_update_row(
175 ticker=ticker, start_date=start_date, end_date=end_date, asset_beta=beta, method=method)
177 return beta
180def cost_of_equity(ticker: str, start_date: Union[datetime.date, None] = None, end_date: Union[datetime.date, None] = None, market_profile: Union[Dict[str, float], None] = None, ticker_profile: Union[dict, None] = None, market_correlation: Union[Dict[str, float], None] = None, method=settings.ESTIMATION_METHOD, cache_in: bool = True, cache_out: bool = True) -> float:
181 """
182 Returns the cost of equity of an asset as estimated by the Capital Asset Pricing Model, i.e. the product of the market premium and asset beta increased by the risk free rate.
184 Parameters
185 ----------
186 1. **ticker**: ``str``
187 A string of the ticker symbol whose cost of equity ratio will be computed.
188 2. **start_date**: ``Union[datetime.date, None]``
189 *Optional*. Start date of the time period for which the cost of equity ratio will be computed
190 3. **end_date**: ``Union[datetime.date, None]``
191 *Optional.* End_date of the time period for which the cost of equity ratio will be computed.
192 4. **market_profile**: ``Union[Dict[str, float], None]``
193 *Optional*. Dictionary containing the assumed risk profile for the market proxy. Overrides calls to services and staistical methods, forcing the calculation fo the cost of equity with the inputted market profile. Format: ``{ 'annual_return': value, 'annual_volatility': value}``
194 5. **market_correlation**: ``Union[Dict[str, float], None]``
195 *Optional*. Dictionary containing the assumed correlation for the calculation. Overrides calls to services and statistical methods, forcing the calculation of the cost of equity with the inputted correlation. Format: ``{ 'correlation' : value }``
196 6. **method** : ``str``
197 *Optional*. Estimation method used to calculate financial statistics. Defaults to the value set by `scrilla.settings.ESTIMATION_METHOD`. Allowable value are accessible through the `scrilla.keys.keys` dictionary.
198 7. **cache_in**: ``bool``
199 Flag to tell function to search cache defined by `scrilla.settings.CACHE_MODE` before computing sharpe ratio. Defaults to `True`.
200 8. **cache_out**: ``bool``
201 Flag to tell function to save sharpe ratio to the cache defined by `scrilla.settings.CACHE_MODE`. Defaults to `True`.
202 """
203 start_date, end_date = errors.validate_dates(start_date=start_date, end_date=end_date,
204 asset_type=keys.keys['ASSETS']['EQUITY'])
206 if cache_in and ticker_profile is None:
207 ticker_profile = profile_cache.filter(
208 ticker=ticker, start_date=start_date, end_date=end_date, method=method)
210 if ticker_profile is not None and ticker_profile.get(keys.keys['STATISTICS']['EQUITY']) is not None:
211 return ticker_profile[keys.keys['STATISTICS']['EQUITY']]
213 beta = market_beta(ticker=ticker, start_date=start_date, end_date=end_date,
214 market_profile=market_profile, ticker_profile=ticker_profile,
215 market_correlation=market_correlation, method=method)
216 premium = market_premium(start_date=start_date, end_date=end_date,
217 market_profile=market_profile, method=method)
219 equity_cost = (premium*beta + services.get_risk_free_rate())
221 # TODO: only update a single column here...
223 if cache_out: 223 ↛ 227line 223 didn't jump to line 227, because the condition on line 223 was never false
224 profile_cache.save_or_update_row(
225 ticker=ticker, start_date=start_date, end_date=end_date, equity_cost=equity_cost, method=method)
227 return equity_cost
230def screen_for_discount(model: str = keys.keys['MODELS']['DDM'], discount_rate: float = None) -> Dict[str, Dict[str, float]]:
231 """
232 Screens the stocks saved under the user watchlist in the `scrilla.settings.COMMON_DIR` directory for discounts relative to the model inputted into the function.
234 Parameters
235 ----------
236 1. **model** : ``str``
237 *Optional*. Model used to evaluated the equities saved in the watchlist. If no model is specified, the function will default to the discount dividend model. Model constants are accessible through the the `scrilla.keys.keys` dictionary.
238 2. **discount_rate** : ``float``
239 *Optional*. Rate used to discount future cashflows to present. Defaults to an equity's CAPM cost of equity, as calculated by `scrilla.analysis.markets.cost_of_equity`.
241 Returns
242 -------
243 ``dict``
244 A list of tickers that trade at a discount relative to the model price, formatted as follows: `{ 'ticker' : { 'spot_price': value, 'model_price': value,'discount': value }, ... }`
245 """
247 equities = list(files.get_watchlist())
248 discounts = {}
249 user_discount_rate = discount_rate
251 for equity in equities:
252 spot_price = services.get_daily_price_latest(ticker=equity)
254 if user_discount_rate is None:
255 discount_rate = cost_of_equity(ticker=equity)
256 else:
257 discount_rate = user_discount_rate
259 if model == keys.keys['MODELS']['DDM']:
260 logger.debug(
261 'Using Discount Dividend Model to screen watchlisted equities for discounts.', 'screen_for_discount')
262 dividends = services.get_dividend_history(equity)
263 model_price = Cashflow(
264 sample=dividends, discount_rate=discount_rate).calculate_net_present_value()
265 discount = float(model_price) - float(spot_price)
267 if discount > 0:
268 discount_result = {'spot_price': spot_price,
269 'model_price': model_price, 'discount': discount}
270 discounts[equity] = discount_result
271 logger.debug(
272 f'Discount of {discount} found for {equity}', 'screen_for_discount')
274 return discounts