quantmod 0.0.1__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.
@@ -0,0 +1,115 @@
1
+ import numpy as np
2
+ from pydantic import BaseModel, Field
3
+ from typing import Optional,Tuple
4
+ from .optioninputs import OptionInputs
5
+
6
+ # Monte Carlo Option Pricing Engine
7
+ class MonteCarloOptionPricing:
8
+ """
9
+ Class for Monte Carlo Option Pricing
10
+
11
+ Parameters
12
+ ----------
13
+ inputs : OptionInputs
14
+ Option inputs parameters
15
+ initialspot : float, optional
16
+ Initial stock price
17
+ nsims : int, optional
18
+ Number of simulations
19
+ timestep : int, optional
20
+ Timestep, by default 252
21
+ barrier : float, optional
22
+ Barrier, by default None
23
+ rebate : int, optional
24
+ Rebate, by default None
25
+
26
+ Returns
27
+ -------
28
+ attributes: float
29
+ call_vanilla, put_vanilla
30
+
31
+ call_asian, put_asian
32
+
33
+ upandoutcall
34
+ """
35
+
36
+ def __init__(self,
37
+ inputs: OptionInputs,
38
+ initialspot: float = Field(..., gt=0, description="Initial stock price"),
39
+ nsims: int = Field(..., gt=0, description="Number of simulations"),
40
+ timestep: int = 252,
41
+ barrier: Optional[float] = None,
42
+ rebate: Optional[int] = None
43
+ ) -> None:
44
+
45
+ self.inputs = inputs
46
+ self.initialspot = initialspot
47
+ self.nsims = nsims
48
+ self.timestep = timestep
49
+ self.barrier = barrier
50
+ self.rebate = rebate
51
+
52
+ self.call_vanilla, self.put_vanilla = self._vanillaoption()
53
+ self.call_asian, self.put_asian = self._asianoption()
54
+ self.upandoutcall = self._upandoutcall()
55
+
56
+ @property
57
+ def _discount_factor(self) -> float:
58
+ return np.exp(-self.inputs.rate * self.inputs.ttm)
59
+
60
+ @property
61
+ def _pseudorandomnumber(self) -> np.ndarray:
62
+ return np.random.standard_normal(self.nsims)
63
+
64
+ @property
65
+ def _simulatepath(self) -> np.ndarray:
66
+ """Simulate price path"""
67
+ np.random.seed(2024)
68
+
69
+ dt = self.inputs.ttm / self.timestep
70
+
71
+ S = np.zeros((self.timestep, self.nsims))
72
+ S[0] = self.initialspot
73
+
74
+ for i in range(0, self.timestep-1):
75
+ w = self._pseudorandomnumber
76
+ S[i+1] = S[i] * (1 + self.inputs.rate*dt + self.inputs.volatility*np.sqrt(dt)*w)
77
+
78
+ return S
79
+
80
+ def _vanillaoption(self) -> Tuple[float, float]:
81
+ """Calculate vanilla option payoff"""
82
+ S = self._simulatepath
83
+
84
+ vanilla_call = self._discount_factor * np.mean(np.maximum(0, S[-1] - self.inputs.strike))
85
+ vanilla_put = self._discount_factor * np.mean(np.maximum(0, self.inputs.strike - S[-1]))
86
+
87
+ return [vanilla_call, vanilla_put]
88
+
89
+ def _asianoption(self) -> Tuple[float]:
90
+ """Calculate asian option payoff"""
91
+ S = self._simulatepath
92
+
93
+ A = S.mean(axis=0)
94
+
95
+ asian_call = self._discount_factor * np.mean(np.maximum(0, A - self.inputs.strike))
96
+ asian_put = self._discount_factor * np.mean(np.maximum(0, self.inputs.strike - A))
97
+
98
+ return [asian_call, asian_put]
99
+
100
+ def _upandoutcall(self) -> float:
101
+ """Calculate up-and-out barrier call option payoff"""
102
+ S = self._simulatepath
103
+
104
+ # Barrier shift
105
+ barriershift = self.barrier * np.exp(0.5826 * self.inputs.volatility * np.sqrt(self.inputs.ttm/self.timestep))
106
+
107
+ value = 0
108
+ for i in range(self.nsims):
109
+ if S[:,i].max () < barriershift:
110
+ value += np.maximum(0, S[-1,i] - self.inputs.strike)
111
+ else:
112
+ value += self.rebate
113
+
114
+ return self._discount_factor * value / self.nsims
115
+
@@ -0,0 +1,44 @@
1
+ from pydantic import BaseModel, Field
2
+ from typing import Optional
3
+
4
+ class OptionInputs(BaseModel):
5
+ """
6
+ Option inputs parameters
7
+
8
+ Parameters
9
+ ----------
10
+ spot : float
11
+ Spot price of the underlying asset
12
+ strike : float
13
+ Strike price of the option
14
+ rate : float
15
+ Risk-free interest rate
16
+ ttm : float
17
+ Time to maturity in years
18
+ volatility : float
19
+ Volatility of the underlying asset
20
+ callprice : float | None
21
+ Default is None
22
+ Market price of the call option
23
+ putprice : float | None
24
+ Default is None
25
+ Market price of the put option
26
+
27
+ Returns
28
+ -------
29
+ OptionInputs
30
+ Option inputs parameters
31
+
32
+ Raises
33
+ ------
34
+ ValueError
35
+ If any of the input parameters are invalid
36
+ """
37
+
38
+ spot: float = Field(..., gt=0, description="Spot price of the underlying asset")
39
+ strike: float = Field(..., gt=0, description="Strike price of the option")
40
+ rate: float = Field(..., gt=0, le=1, description="Risk-free interest rate")
41
+ ttm: float = Field(..., gt=0, description="Time to maturity in years")
42
+ volatility: float = Field(..., gt=0, description="Volatility of the underlying asset")
43
+ callprice: Optional[float] = Field(default=None, ge=0, description="Market price of the call option")
44
+ putprice: Optional[float] = Field(default=None, ge=0, description="Market price of the put option")
@@ -0,0 +1,27 @@
1
+ # Quantmod Python Package
2
+ # https://kannansingaravelu.com/
3
+
4
+ # Copyright 2024 Kannan Singaravelu
5
+
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+
18
+ from .riskinputs import RiskInputs
19
+ from .var import ValueAtRisk, ConditionalVaR
20
+ from .varbacktester import VarBacktester
21
+
22
+ __all__ = [
23
+ "RiskInputs",
24
+ "ValueAtRisk",
25
+ "ConditionalVaR",
26
+ "VarBacktester",
27
+ ]
@@ -0,0 +1,36 @@
1
+ from pydantic import BaseModel, Field, field_validator
2
+ from typing import List
3
+
4
+ class RiskInputs(BaseModel):
5
+ """
6
+ RiskInputs parameters
7
+
8
+ Parameters
9
+ ----------
10
+ returns : List[float]
11
+ List of historical returns
12
+ confidence_level : float
13
+ The confidence level for the VaR calculation
14
+
15
+ Returns
16
+ -------
17
+ RiskInputs
18
+ RiskInputs parameters
19
+
20
+ Raises
21
+ ------
22
+ ValueError
23
+ If any of the input parameters are invalid
24
+
25
+ """
26
+ returns: List[float] = Field(..., description="List of historical returns")
27
+ confidence_level: float = Field(..., ge=0.0, le=1.0, description="The confidence level for the VaR calculation")
28
+
29
+ class Config:
30
+ arbitrary_types_allowed = True
31
+
32
+ @field_validator('returns')
33
+ def check_returns(cls, v):
34
+ if len(v) <= 252: # Assuming at least one year of daily data
35
+ raise ValueError("At least 252 returns are required for meaningful backtest")
36
+ return v
quantmod/risk/var.py ADDED
@@ -0,0 +1,103 @@
1
+ import pandas as pd
2
+ import numpy as np
3
+ from scipy import stats
4
+ from arch import arch_model
5
+ from .riskinputs import RiskInputs
6
+ from typing import Literal
7
+
8
+
9
+ class ValueAtRisk:
10
+ """
11
+ Class to calculate Value at Risk (VaR) based on returns and confidence level
12
+
13
+ Parameters
14
+ ----------
15
+ inputs : RiskInputs
16
+ An instance of Risk inputs containing returns and confidence level
17
+ method : Literal['historical', 'parametric', 'montecarlo']
18
+ The method to calculate VaR, by default 'historical'
19
+
20
+ Returns
21
+ -------
22
+ attributes: float
23
+ var
24
+ """
25
+
26
+ def __init__(self, inputs: RiskInputs, method: Literal['historical', 'parametric', 'montecarlo']) -> float:
27
+ self.inputs = inputs
28
+ self.returns = pd.Series(inputs.returns)
29
+ self.method = method
30
+ self.mean = np.mean(self.returns)
31
+ self.std = np.std(self.returns)
32
+ self.var = self._var()
33
+
34
+ def _pvar(self) -> float:
35
+ """
36
+ Calculate the Value at Risk (VaR) at the specified confidence level using variance - covariance approach
37
+ """
38
+ # return (self.mean + stats.norm.ppf(1-self.inputs.confidence_level) * self.std)
39
+ return np.round(stats.norm.ppf(1-self.inputs.confidence_level, self.mean, self.std),4)
40
+
41
+ def _hvar(self) -> float:
42
+ """
43
+ Calculate the Value at Risk (VaR) at the specified confidence level using historical returns
44
+ """
45
+ return np.round(np.percentile(self.returns, (1 - self.inputs.confidence_level) * 100),4)
46
+
47
+ def _mcvar(self) -> float:
48
+ """
49
+ Calculate the Value at Risk (VaR) at the specified confidence level using Monte Carlo simulation
50
+ """
51
+ simulated_returns = stats.norm.rvs(loc=self.mean, scale=self.std, size=5000)
52
+ return np.round(np.percentile(simulated_returns, (1 - self.inputs.confidence_level) * 100),4)
53
+
54
+ # def _garchvar(self) -> float:
55
+ # """
56
+ # Calculate the Value at Risk (VaR) at the specified confidence level using GARCH model
57
+ # """
58
+ # model = arch_model(self.returns*1000, vol='GARCH', p=1, q=1, dist="gaussian")
59
+ # fitted_model = model.fit(disp="off")
60
+ # conditional_volatilities = fitted_model.conditional_volatility
61
+ # return (stats.norm.ppf(1-self.inputs.confidence_level) * conditional_volatilities.iloc[-1]) / 1000
62
+
63
+
64
+ def _var(self) -> float:
65
+ if self.method == 'parametric':
66
+ return self._pvar()
67
+ elif self.method == 'historical':
68
+ return self._hvar()
69
+ elif self.method == 'montecarlo':
70
+ return self._mcvar()
71
+ # elif self.method == 'garch':
72
+ # return self._garchvar()
73
+ else:
74
+ raise ValueError("Invalid method")
75
+
76
+
77
+ class ConditionalVaR:
78
+ """
79
+ Class to calculate Conditional Value at Risk (CVaR) aka Expected Shortfall (ES) based on historical returns and confidence level
80
+
81
+ Parameters
82
+ ----------
83
+ inputs : RiskInputs
84
+ An instance of Risk inputs containing returns and confidence level
85
+
86
+ Returns
87
+ -------
88
+ attributes: float
89
+ CVaR
90
+ """
91
+
92
+ def __init__(self, inputs: RiskInputs) -> float:
93
+ self.inputs = inputs
94
+ self.returns = pd.Series(inputs.returns)
95
+ self.cvar = self._cvar()
96
+
97
+ def _cvar(self) -> float:
98
+ """
99
+ Calculate the Conditional Value at Risk (CVaR) at the specified confidence level
100
+ """
101
+ var = np.percentile(self.returns, (1 - self.inputs.confidence_level) * 100)
102
+ tail_returns = self.returns[self.returns <= var]
103
+ return np.round(tail_returns.mean() if len(tail_returns) > 0 else float('nan'),4)
@@ -0,0 +1,85 @@
1
+ import pandas as pd
2
+ import numpy as np
3
+ from scipy import stats
4
+ from tabulate import tabulate
5
+ from .riskinputs import RiskInputs
6
+
7
+
8
+ class VarBacktester:
9
+ """
10
+ Class to perform a backtest for Value at Risk (VaR) calculations with non-overlapping windows
11
+
12
+ Parameters
13
+ ----------
14
+ inputs : RiskInputs
15
+ An instance of Risk inputs containing returns and confidence level
16
+ window_volatility : int, optional
17
+ The window size for the rolling volatility calculation, by default 21
18
+ window_forward : int, optional
19
+ The window size for the rolling forward calculation, by default 10
20
+
21
+ Returns
22
+ -------
23
+ attributes: tabular output
24
+ run
25
+ """
26
+
27
+ def __init__(self, inputs: RiskInputs, window_volatility:int = 21, window_forward: int = 10) -> pd.DataFrame:
28
+ self.inputs = inputs
29
+ self.returns = pd.Series(inputs.returns)
30
+ self.window_volatility = window_volatility
31
+ self.window_forward = window_forward
32
+ self.run = self._runbacktest()
33
+
34
+
35
+ def _runbacktest(self) -> str:
36
+ """
37
+ Perform VaR backtest with non-overlapping windows
38
+
39
+ Returns
40
+ -------
41
+ str
42
+ A tabular output containing the results of the backtest
43
+ """
44
+ # Create a dataframe with returns
45
+ data = pd.DataFrame({'returns': self.returns})
46
+
47
+ # Calculate rolling VaR
48
+ data['VaR'] = data['returns'].rolling(self.window_volatility).std()*np.sqrt(self.window_forward)*stats.norm.ppf(1-self.inputs.confidence_level)
49
+
50
+ # Calculate forward returns
51
+ data['shifted_returns'] = data['returns'].shift(-2) # non overlapping window
52
+ data['forward_returns'] = data['shifted_returns'].rolling(window=self.window_forward).sum()
53
+ data['shifted_forward_returns'] = data['forward_returns'].shift(-self.window_forward+1)
54
+
55
+ # Identify breaches
56
+ data['breach'] = data['shifted_forward_returns'] < data['VaR']
57
+
58
+ # Calculate number and percentage of breaches
59
+ total_breaches = data['breach'].sum()
60
+ total_observations = len(data) - self.window_volatility - self.window_forward
61
+ percentage_breaches = (total_breaches / total_observations) * 100 if total_observations > 0 else np.nan
62
+
63
+ # Calculate continuous breaches
64
+ data['breach_shift'] = data['breach'].shift(1)
65
+ data['continuous_breaches'] = (data['breach'] & (data['breach'] == data['breach_shift'])).astype(int)
66
+ number_continuous_breaches = data['continuous_breaches'].sum()
67
+
68
+ # # Print data of breaches
69
+ # print(data[data['breach']])
70
+
71
+ # Conditional probability of breaches
72
+ non_nan_breaches = data['breach'].dropna()
73
+ conditional_probability = non_nan_breaches.mean() if len(non_nan_breaches) > 0 else np.nan
74
+
75
+ # Ouput results in tabular format
76
+ header = ['Backtest Results', 'Values']
77
+ table = [
78
+ ['Actual Breaches', total_breaches],
79
+ ['Percentage of Actual Breaches', percentage_breaches],
80
+ ['Expected Breaches', int(total_observations*(1-self.inputs.confidence_level))],
81
+ ['Percentage of Expected Breaches', (1-self.inputs.confidence_level)*100],
82
+ ['Number of Continuous Breaches', number_continuous_breaches],
83
+ ['Conditional Probability of Breaches', conditional_probability*100]]
84
+
85
+ return tabulate(table,headers=header, floatfmt=(".2f"))
@@ -0,0 +1,93 @@
1
+ # Quantmod Python Package
2
+ # https://kannansingaravelu.com/
3
+
4
+ # Copyright 2024 Kannan Singaravelu
5
+
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+
19
+
20
+ from .timeseries import (
21
+ seriesHi,
22
+ seriesLo,
23
+ Op,
24
+ Hi,
25
+ Lo,
26
+ Cl,
27
+ Vo,
28
+ Ad,
29
+ OpCl,
30
+ OpHi,
31
+ OpLo,
32
+ HiCl,
33
+ HiLo,
34
+ LoCl,
35
+ Gap,
36
+ lag,
37
+ lead,
38
+ last,
39
+ first,
40
+ trend_score,
41
+ )
42
+
43
+
44
+ from .performance import (
45
+ periodReturn,
46
+ dailyReturn,
47
+ weeklyReturn,
48
+ monthlyReturn,
49
+ quarterlyReturn,
50
+ annualReturn,
51
+ allReturn,
52
+ cagr,
53
+ volatility,
54
+ sharpe,
55
+ maxdd,
56
+ calmar
57
+ )
58
+
59
+
60
+ __all__ = [
61
+ "seriesHi",
62
+ "seriesLo",
63
+ "Op",
64
+ "Hi",
65
+ "Lo",
66
+ "Cl",
67
+ "Vo",
68
+ "Ad",
69
+ "OpCl",
70
+ "OpHi",
71
+ "OpLo",
72
+ "HiCl",
73
+ "HiLo",
74
+ "LoCl",
75
+ "Gap",
76
+ "lag",
77
+ "lead",
78
+ "last",
79
+ "first",
80
+ "trend_score",
81
+ "periodReturn",
82
+ "dailyReturn",
83
+ "weeklyReturn",
84
+ "monthlyReturn",
85
+ "quarterlyReturn",
86
+ "annualReturn",
87
+ "allReturn",
88
+ "cagr",
89
+ "volatility",
90
+ "sharpe",
91
+ "maxdd",
92
+ "calmar",
93
+ ]