import datetime import statistics from typing import Literal from fincal.core import date_parser from .fincal import TimeSeries from .utils import _interval_to_years @date_parser(3, 4) def sharpe_ratio( time_series_data: TimeSeries, risk_free_data: TimeSeries = None, risk_free_rate: float = None, from_date: str | datetime.datetime = None, to_date: str | datetime.datetime = None, frequency: Literal["D", "W", "M", "Q", "H", "Y"] = None, return_period_unit: Literal["years", "months", "days"] = "years", return_period_value: int = 1, as_on_match: str = "closest", prior_match: str = "closest", closest: Literal["previous", "next"] = "previous", date_format: str = None, ) -> float: """Calculate the Sharpe ratio of any time series Sharpe ratio is a measure of returns per unit of risk, where risk is measured by the standard deviation of the returns. The formula for Sharpe ratio is: (average asset return - risk free rate)/volatility of asset returns Parameters ---------- time_series_data: The time series for which Sharpe ratio needs to be calculated risk_free_data: Risk free rates as time series data. This should be the time series of risk free returns, and not the underlying asset value. risk_free_rate: Risk free rate to be used. Either risk_free_data or risk_free_rate needs to be provided. If both are provided, the time series data will be used. from_date: Start date from which returns should be calculated. Defaults to the first date of the series. to_date: End date till which returns should be calculated. Defaults to the last date of the series. frequency: The frequency at which returns should be calculated. return_period_unit : 'years', 'months', 'days' The type of time period to use for return calculation. return_period_value : int The value of the specified interval type over which returns needs to be calculated. as_on_match : str, optional The mode of matching the as_on_date. Refer closest. prior_match : str, optional The mode of matching the prior_date. Refer closest. closest : str, optional The mode of matching the closest date. Valid values are 'exact', 'previous', 'next' and next. The date format to use for this operation. Should be passed as a datetime library compatible string. Sets the date format only for this operation. To set it globally, use FincalOptions.date_format Returns ------- Value of Sharpe ratio as a float. Raises ------ ValueError If risk free data or risk free rate is not provided. """ interval_days = int(_interval_to_years(return_period_unit, return_period_value) * 365 + 1) if from_date is None: from_date = time_series_data.start_date + datetime.timedelta(days=interval_days) if to_date is None: to_date = time_series_data.end_date if risk_free_data is None and risk_free_rate is None: raise ValueError("At least one of risk_free_data or risk_free rate is required") elif risk_free_data is not None: risk_free_rate = risk_free_data.mean() common_params = { "from_date": from_date, "to_date": to_date, "frequency": frequency, "return_period_unit": return_period_unit, "return_period_value": return_period_value, "as_on_match": as_on_match, "prior_match": prior_match, "closest": closest, "date_format": date_format, } average_rr = time_series_data.average_rolling_return(**common_params, annual_compounded_returns=True) excess_returns = average_rr - risk_free_rate sd = time_series_data.volatility( **common_params, annualize_volatility=True, ) sharpe_ratio_value = excess_returns / sd return sharpe_ratio_value @date_parser(2, 3) def beta( asset_data: TimeSeries, market_data: TimeSeries, from_date: str | datetime.datetime = None, to_date: str | datetime.datetime = None, frequency: Literal["D", "W", "M", "Q", "H", "Y"] = None, return_period_unit: Literal["years", "months", "days"] = "years", return_period_value: int = 1, as_on_match: str = "closest", prior_match: str = "closest", closest: Literal["previous", "next"] = "previous", date_format: str = None, ) -> float: """Beta is a measure of sensitivity of asset returns to market returns The formula for beta is: Parameters ---------- asset_data : TimeSeries The time series data of the asset market_data : TimeSeries The time series data of the relevant market index from_date: Start date from which returns should be calculated. Defaults to the first date of the series. to_date: End date till which returns should be calculated. Defaults to the last date of the series. frequency: The frequency at which returns should be calculated. return_period_unit : 'years', 'months', 'days' The type of time period to use for return calculation. return_period_value : int The value of the specified interval type over which returns needs to be calculated. as_on_match : str, optional The mode of matching the as_on_date. Refer closest. prior_match : str, optional The mode of matching the prior_date. Refer closest. closest : str, optional The mode of matching the closest date. Valid values are 'exact', 'previous', 'next' and next. The date format to use for this operation. Should be passed as a datetime library compatible string. Sets the date format only for this operation. To set it globally, use FincalOptions.date_format Returns ------- The value of beta as a float. """ interval_years = _interval_to_years(return_period_unit, return_period_value) interval_days = int(interval_years * 365 + 1) annual_compounded_returns = True if interval_years > 1 else False if from_date is None: from_date = asset_data.start_date + datetime.timedelta(days=interval_days) if to_date is None: to_date = asset_data.end_date common_params = { "from_date": from_date, "to_date": to_date, "frequency": frequency, "return_period_unit": return_period_unit, "return_period_value": return_period_value, "as_on_match": as_on_match, "prior_match": prior_match, "closest": closest, "date_format": date_format, "annual_compounded_returns": annual_compounded_returns, } asset_rr = asset_data.calculate_rolling_returns(**common_params) market_rr = market_data.calculate_rolling_returns(**common_params) cov = statistics.covariance(asset_rr.values, market_rr.values) market_var = statistics.variance(market_rr.values) beta = cov / market_var return beta def jensens_alpha( asset_data: TimeSeries, market_data: TimeSeries, risk_free_data: TimeSeries = None, risk_free_rate: float = None, from_date: str | datetime.datetime = None, to_date: str | datetime.datetime = None, frequency: Literal["D", "W", "M", "Q", "H", "Y"] = None, return_period_unit: Literal["years", "months", "days"] = "years", return_period_value: int = 1, as_on_match: str = "closest", prior_match: str = "closest", closest: Literal["previous", "next"] = "previous", date_format: str = None, ) -> float: """ This function calculates the Jensen's alpha for a time series. The formula for Jensen's alpha is: Ri - Rf + B x (Rm - Rf) where: Ri = Realized return of the portfolio or investment Rf = The risk free rate during the return time frame B = Beta of the portfolio or investment Rm = Realized return of the market index """ interval_years = _interval_to_years(return_period_unit, return_period_value) interval_days = int(interval_years * 365 + 1) if from_date is None: from_date = asset_data.start_date + datetime.timedelta(days=interval_days) if to_date is None: to_date = asset_data.end_date common_params = { "from_date": from_date, "to_date": to_date, "frequency": frequency, "return_period_unit": return_period_unit, "return_period_value": return_period_value, "as_on_match": as_on_match, "prior_match": prior_match, "closest": closest, "date_format": date_format, } num_days = (to_date - from_date).days compound_realised_returns = True if num_days > 365 else False realized_return = asset_data.calculate_returns( as_on=to_date, return_period_unit="days", return_period_value=num_days, annual_compounded_returns=compound_realised_returns, as_on_match=as_on_match, prior_match=prior_match, closest=closest, date_format=date_format, ) market_return = market_data.calculate_returns( as_on=to_date, return_period_unit="days", return_period_value=num_days, annual_compounded_returns=compound_realised_returns, as_on_match=as_on_match, prior_match=prior_match, closest=closest, date_format=date_format, ) beta_value = beta(asset_data=asset_data, market_data=market_data, **common_params) if risk_free_data is None and risk_free_rate is None: raise ValueError("At least one of risk_free_data or risk_free rate is required") elif risk_free_data is not None: risk_free_rate = risk_free_data.mean() jensens_alpha = realized_return[1] - risk_free_rate + beta_value * (market_return[1] - risk_free_rate) return jensens_alpha