moqua 0.2.2__tar.gz → 0.2.4__tar.gz
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.
- {moqua-0.2.2 → moqua-0.2.4}/PKG-INFO +1 -1
- {moqua-0.2.2 → moqua-0.2.4}/moqua/__init__.py +4 -1
- moqua-0.2.4/moqua/pricing/engine/__init__.py +1 -0
- moqua-0.2.4/moqua/pricing/engine/bachelier.py +83 -0
- moqua-0.2.4/moqua/pricing/engine/black76.py +95 -0
- moqua-0.2.4/moqua/pricing/engine/black_scholes.py +100 -0
- moqua-0.2.4/moqua/pricing/instruments/__init__.py +1 -0
- moqua-0.2.4/moqua/pricing/instruments/european_option.py +37 -0
- {moqua-0.2.2 → moqua-0.2.4}/moqua.egg-info/PKG-INFO +1 -1
- {moqua-0.2.2 → moqua-0.2.4}/moqua.egg-info/SOURCES.txt +6 -0
- {moqua-0.2.2 → moqua-0.2.4}/setup.py +1 -1
- {moqua-0.2.2 → moqua-0.2.4}/README.md +0 -0
- {moqua-0.2.2 → moqua-0.2.4}/moqua/core.py +0 -0
- {moqua-0.2.2 → moqua-0.2.4}/moqua/optimization.py +0 -0
- {moqua-0.2.2 → moqua-0.2.4}/moqua/outputs.py +0 -0
- {moqua-0.2.2 → moqua-0.2.4}/moqua/pricing/__init__.py +0 -0
- {moqua-0.2.2 → moqua-0.2.4}/moqua/pricing/contracts.py +0 -0
- {moqua-0.2.2 → moqua-0.2.4}/moqua.egg-info/dependency_links.txt +0 -0
- {moqua-0.2.2 → moqua-0.2.4}/moqua.egg-info/not-zip-safe +0 -0
- {moqua-0.2.2 → moqua-0.2.4}/moqua.egg-info/requires.txt +0 -0
- {moqua-0.2.2 → moqua-0.2.4}/moqua.egg-info/top_level.txt +0 -0
- {moqua-0.2.2 → moqua-0.2.4}/pyproject.toml +0 -0
- {moqua-0.2.2 → moqua-0.2.4}/setup.cfg +0 -0
- {moqua-0.2.2 → moqua-0.2.4}/src/erf_cody.cpp +0 -0
- {moqua-0.2.2 → moqua-0.2.4}/src/lets_be_rational.cpp +0 -0
- {moqua-0.2.2 → moqua-0.2.4}/src/normaldistribution.cpp +0 -0
- {moqua-0.2.2 → moqua-0.2.4}/src/optimization.cpp +0 -0
- {moqua-0.2.2 → moqua-0.2.4}/src/rationalcubic.cpp +0 -0
- {moqua-0.2.2 → moqua-0.2.4}/src/rolling_ols.cpp +0 -0
|
@@ -20,6 +20,9 @@ from .core import (
|
|
|
20
20
|
log_returns
|
|
21
21
|
)
|
|
22
22
|
|
|
23
|
+
# Explicitly expose subpackages for better discovery
|
|
24
|
+
from . import contracts, engine, instruments
|
|
25
|
+
|
|
23
26
|
# ==========================================
|
|
24
27
|
# 2. PRICING (Moteurs & Instruments)
|
|
25
28
|
# ==========================================
|
|
@@ -65,4 +68,4 @@ from .outputs import (
|
|
|
65
68
|
create_excel_report
|
|
66
69
|
)
|
|
67
70
|
|
|
68
|
-
__version__ = "0.2.
|
|
71
|
+
__version__ = "0.2.4"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Init file for engine module
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from moqua.core import pdf, cdf
|
|
3
|
+
from moqua.pricing.contracts import Calculator, Engine, Option, Market
|
|
4
|
+
from functools import cached_property
|
|
5
|
+
|
|
6
|
+
class BachelierCalculator(Calculator):
|
|
7
|
+
|
|
8
|
+
def __init__(self, F, K, T, r, sigma_N, w):
|
|
9
|
+
self.F = np.asarray(F, float)
|
|
10
|
+
self.K = np.asarray(K, float)
|
|
11
|
+
self.T = np.asarray(T, float)
|
|
12
|
+
self.r = np.asarray(r, float)
|
|
13
|
+
self.sigma_N = np.asarray(sigma_N, float)
|
|
14
|
+
self.w = np.asarray(w, float)
|
|
15
|
+
|
|
16
|
+
@cached_property
|
|
17
|
+
def discount(self):
|
|
18
|
+
return np.exp(-self.r * self.T)
|
|
19
|
+
|
|
20
|
+
@cached_property
|
|
21
|
+
def mask(self):
|
|
22
|
+
return (self.T <= 0.0) | (self.sigma_N <= 1e-10)
|
|
23
|
+
|
|
24
|
+
@cached_property
|
|
25
|
+
def safe_sqrt_T(self):
|
|
26
|
+
return np.where(~self.mask, np.sqrt(self.T), 1.0)
|
|
27
|
+
|
|
28
|
+
@cached_property
|
|
29
|
+
def d(self):
|
|
30
|
+
return (self.F - self.K) / (self.sigma_N * self.safe_sqrt_T)
|
|
31
|
+
|
|
32
|
+
@cached_property
|
|
33
|
+
def price(self):
|
|
34
|
+
val = self.discount * (
|
|
35
|
+
self.w * (self.F - self.K) * cdf(self.w * self.d) +
|
|
36
|
+
self.sigma_N * np.sqrt(np.maximum(self.T, 0.0)) * pdf(self.d)
|
|
37
|
+
)
|
|
38
|
+
payoff = np.maximum(0.0, self.w * (self.F - self.K))
|
|
39
|
+
return self._result(np.where(self.mask, payoff, val))
|
|
40
|
+
|
|
41
|
+
@cached_property
|
|
42
|
+
def delta(self):
|
|
43
|
+
val = self.w * self.discount * cdf(self.w * self.d)
|
|
44
|
+
payoff = np.where(self.w * (self.F - self.K) > 0, self.w * self.discount, 0.0)
|
|
45
|
+
return self._result(np.where(self.mask, payoff, val))
|
|
46
|
+
|
|
47
|
+
@cached_property
|
|
48
|
+
def gamma(self):
|
|
49
|
+
with np.errstate(divide='ignore', invalid='ignore'):
|
|
50
|
+
val = self.discount * pdf(self.d) / (self.sigma_N * self.safe_sqrt_T)
|
|
51
|
+
return self._result(np.where(self.mask, 0.0, val))
|
|
52
|
+
|
|
53
|
+
@cached_property
|
|
54
|
+
def vega(self):
|
|
55
|
+
val = self.discount * self.safe_sqrt_T * pdf(self.d)
|
|
56
|
+
return self._result(np.where(self.mask, 0.0, val))
|
|
57
|
+
|
|
58
|
+
@cached_property
|
|
59
|
+
def rho(self):
|
|
60
|
+
val = -self.T * self.price
|
|
61
|
+
return self._result(np.where(self.T <= 0, 0.0, val))
|
|
62
|
+
|
|
63
|
+
@cached_property
|
|
64
|
+
def theta(self):
|
|
65
|
+
with np.errstate(divide='ignore', invalid='ignore'):
|
|
66
|
+
term1 = -(self.discount * self.sigma_N * pdf(self.d)) / (2.0 * np.sqrt(np.maximum(self.T, 1e-10)))
|
|
67
|
+
|
|
68
|
+
term2 = self.r * self.price
|
|
69
|
+
val = (term1 + term2) / 365.0
|
|
70
|
+
return self._result(np.where(self.T <= 0, 0.0, val))
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class BachelierEngine(Engine):
|
|
74
|
+
def calculator(self, option: Option, market: Market):
|
|
75
|
+
if market.F is None and market.S is None:
|
|
76
|
+
raise ValueError("Bachelier requires 'forward' or 'spot'.")
|
|
77
|
+
|
|
78
|
+
F = market.F if market.F is not None else market.S * np.exp(market.r * option.T)
|
|
79
|
+
|
|
80
|
+
return BachelierCalculator(
|
|
81
|
+
F=F, K=option.K, # No np.maximum(F, 1e-10) because Bachelier handles negative F and K!
|
|
82
|
+
T=option.T, r=market.r, sigma_N=market.sigma, w=option.w
|
|
83
|
+
)
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from moqua.core import pdf, cdf
|
|
3
|
+
from moqua.pricing.contracts import Calculator, Engine, Option, Market
|
|
4
|
+
from functools import cached_property
|
|
5
|
+
|
|
6
|
+
class Black76Calculator(Calculator):
|
|
7
|
+
|
|
8
|
+
def __init__(self, F, K, T, r, sigma, w):
|
|
9
|
+
self.F = np.asarray(F, float)
|
|
10
|
+
self.K = np.asarray(K, float)
|
|
11
|
+
self.T = np.asarray(T, float)
|
|
12
|
+
self.r = np.asarray(r, float)
|
|
13
|
+
self.sigma = np.asarray(sigma, float)
|
|
14
|
+
self.w = np.asarray(w, float)
|
|
15
|
+
|
|
16
|
+
@cached_property
|
|
17
|
+
def discount(self):
|
|
18
|
+
return np.exp(-self.r * self.T)
|
|
19
|
+
|
|
20
|
+
@cached_property
|
|
21
|
+
def mask(self):
|
|
22
|
+
return (self.T <= 0.0) | (self.sigma <= 1e-10)
|
|
23
|
+
|
|
24
|
+
@cached_property
|
|
25
|
+
def vol_sqrt_T(self):
|
|
26
|
+
return self.sigma * np.sqrt(np.maximum(self.T, 0.0))
|
|
27
|
+
|
|
28
|
+
@cached_property
|
|
29
|
+
def safe_vol(self):
|
|
30
|
+
return np.where(~self.mask, self.vol_sqrt_T, 1.0)
|
|
31
|
+
|
|
32
|
+
@cached_property
|
|
33
|
+
def d1(self):
|
|
34
|
+
with np.errstate(divide='ignore', invalid='ignore'):
|
|
35
|
+
return (np.log(self.F / self.K) + (0.5 * self.sigma ** 2) * self.T) / self.safe_vol
|
|
36
|
+
|
|
37
|
+
@cached_property
|
|
38
|
+
def d2(self):
|
|
39
|
+
return self.d1 - self.vol_sqrt_T
|
|
40
|
+
|
|
41
|
+
@cached_property
|
|
42
|
+
def price(self):
|
|
43
|
+
val = self.w * self.discount * (self.F * cdf(self.w * self.d1) - self.K * cdf(self.w * self.d2))
|
|
44
|
+
payoff = np.maximum(0.0, self.w * (self.F - self.K))
|
|
45
|
+
|
|
46
|
+
return self._result(np.where(self.mask, payoff, val))
|
|
47
|
+
|
|
48
|
+
@cached_property
|
|
49
|
+
def delta(self):
|
|
50
|
+
|
|
51
|
+
val = self.w * self.discount * cdf(self.w * self.d1)
|
|
52
|
+
payoff = np.where(self.w * (self.F - self.K) > 0, self.w * self.discount, 0.0)
|
|
53
|
+
|
|
54
|
+
return self._result(np.where(self.mask, payoff, val))
|
|
55
|
+
|
|
56
|
+
@cached_property
|
|
57
|
+
def gamma(self):
|
|
58
|
+
|
|
59
|
+
with np.errstate(divide='ignore', invalid='ignore'):
|
|
60
|
+
val = self.discount * pdf(self.d1) / (self.F * self.vol_sqrt_T)
|
|
61
|
+
bad = self.mask | (self.F <= 0)
|
|
62
|
+
|
|
63
|
+
return self._result(np.where(bad, 0.0, val))
|
|
64
|
+
|
|
65
|
+
@cached_property
|
|
66
|
+
def vega(self):
|
|
67
|
+
val = self.F * self.discount * pdf(self.d1) * np.sqrt(np.maximum(self.T, 0.0))
|
|
68
|
+
return self._result(np.where(self.mask, 0.0, val))
|
|
69
|
+
|
|
70
|
+
@cached_property
|
|
71
|
+
def rho(self):
|
|
72
|
+
val = -self.T * self.price
|
|
73
|
+
return self._result(np.where(self.T <= 0, 0.0, val))
|
|
74
|
+
|
|
75
|
+
@cached_property
|
|
76
|
+
def theta(self):
|
|
77
|
+
with np.errstate(divide='ignore', invalid='ignore'):
|
|
78
|
+
term1 = -(self.F * self.discount * pdf(self.d1) * self.sigma) / (2.0 * np.sqrt(np.maximum(self.T, 1e-10)))
|
|
79
|
+
|
|
80
|
+
term2 = self.r * self.price
|
|
81
|
+
val = (term1 + term2) / 365.0
|
|
82
|
+
return self._result(np.where(self.T <= 0, 0.0, val))
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class Black76Engine(Engine):
|
|
86
|
+
def calculator(self, option: Option, market: Market):
|
|
87
|
+
if market.F is None and market.S is None:
|
|
88
|
+
raise ValueError("Black76 requires 'forward' or 'spot'.")
|
|
89
|
+
|
|
90
|
+
F = market.F if market.F is not None else market.S * np.exp(market.r * option.T)
|
|
91
|
+
|
|
92
|
+
return Black76Calculator(
|
|
93
|
+
F=np.maximum(F, 1e-10), K=option.K,
|
|
94
|
+
T=option.T, r=market.r, sigma=market.sigma, w=option.w
|
|
95
|
+
)
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from moqua.core import pdf, cdf
|
|
3
|
+
from moqua.pricing.contracts import Calculator, Engine, Option, Market
|
|
4
|
+
from functools import cached_property
|
|
5
|
+
|
|
6
|
+
class BlackScholesCalculator(Calculator):
|
|
7
|
+
|
|
8
|
+
def __init__(self, S, K, T, r, sigma, w):
|
|
9
|
+
self.S = np.asarray(S, float)
|
|
10
|
+
self.K = np.asarray(K, float)
|
|
11
|
+
self.T = np.asarray(T, float)
|
|
12
|
+
self.r = np.asarray(r, float)
|
|
13
|
+
self.sigma = np.asarray(sigma, float)
|
|
14
|
+
self.w = np.asarray(w, float)
|
|
15
|
+
|
|
16
|
+
# Private helpers
|
|
17
|
+
@cached_property
|
|
18
|
+
def discount(self):
|
|
19
|
+
return np.exp(-self.r * self.T)
|
|
20
|
+
|
|
21
|
+
@cached_property
|
|
22
|
+
def mask(self):
|
|
23
|
+
return (self.T <= 0) | (self.sigma <= 1e-10)
|
|
24
|
+
|
|
25
|
+
@cached_property
|
|
26
|
+
def vol_sqrt_T(self):
|
|
27
|
+
return self.sigma * np.sqrt(np.maximum(self.T, 0.0))
|
|
28
|
+
|
|
29
|
+
@cached_property
|
|
30
|
+
def safe_vol(self):
|
|
31
|
+
return np.where(~self.mask, self.vol_sqrt_T, 1.0)
|
|
32
|
+
|
|
33
|
+
@cached_property
|
|
34
|
+
def d1(self):
|
|
35
|
+
with np.errstate(divide='ignore', invalid='ignore'):
|
|
36
|
+
return (np.log(self.S / self.K) + (self.r + 0.5 * self.sigma**2) * self.T) / self.safe_vol
|
|
37
|
+
|
|
38
|
+
@cached_property
|
|
39
|
+
def d2(self):
|
|
40
|
+
return self.d1 - self.vol_sqrt_T
|
|
41
|
+
|
|
42
|
+
@cached_property
|
|
43
|
+
def price(self):
|
|
44
|
+
|
|
45
|
+
val = self.w * (self.S * cdf(self.w * self.d1) - self.K * self.discount * cdf(self.w * self.d2))
|
|
46
|
+
payoff = np.maximum(0.0, self.w * (self.S - self.K))
|
|
47
|
+
|
|
48
|
+
return self._result(np.where(self.mask, payoff, val))
|
|
49
|
+
|
|
50
|
+
@cached_property
|
|
51
|
+
def delta(self):
|
|
52
|
+
|
|
53
|
+
val = self.w * cdf(self.w * self.d1)
|
|
54
|
+
payoff = np.where(self.w * (self.S - self.K) > 0, self.w, 0.0) # ITM -> w, OTM -> 0
|
|
55
|
+
|
|
56
|
+
return self._result(np.where(self.mask, payoff, val))
|
|
57
|
+
|
|
58
|
+
@cached_property
|
|
59
|
+
def gamma(self):
|
|
60
|
+
|
|
61
|
+
with np.errstate(divide='ignore', invalid='ignore'):
|
|
62
|
+
val = pdf(self.d1) / (self.S * self.vol_sqrt_T)
|
|
63
|
+
bad = self.mask | (self.S <= 0)
|
|
64
|
+
|
|
65
|
+
return self._result(np.where(bad, 0.0, val))
|
|
66
|
+
|
|
67
|
+
@cached_property
|
|
68
|
+
def vega(self):
|
|
69
|
+
|
|
70
|
+
val = self.S * pdf(self.d1) * np.sqrt(np.maximum(self.T, 0.0))
|
|
71
|
+
|
|
72
|
+
return self._result(np.where(self.mask, 0.0, val))
|
|
73
|
+
|
|
74
|
+
@cached_property
|
|
75
|
+
def rho(self):
|
|
76
|
+
|
|
77
|
+
val = self.w * self.K * self.T * self.discount * cdf(self.w * self.d2)
|
|
78
|
+
|
|
79
|
+
return self._result(np.where(self.T <= 0, 0.0, val))
|
|
80
|
+
|
|
81
|
+
@cached_property
|
|
82
|
+
def theta(self):
|
|
83
|
+
|
|
84
|
+
with np.errstate(divide='ignore', invalid='ignore'):
|
|
85
|
+
term1 = -(self.S * pdf(self.d1) * self.sigma) / (2.0 * np.sqrt(np.maximum(self.T, 1e-10)))
|
|
86
|
+
|
|
87
|
+
term2 = -self.w * self.r * self.K * self.discount * cdf(self.w * self.d2)
|
|
88
|
+
val = (term1 + term2) / 365.0
|
|
89
|
+
|
|
90
|
+
return self._result(np.where(self.T <= 0, 0.0, val))
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class BlackScholesEngine(Engine):
|
|
94
|
+
def calculator(self, option: Option, market: Market):
|
|
95
|
+
if market.S is None: raise ValueError("Spot is missing")
|
|
96
|
+
return BlackScholesCalculator(
|
|
97
|
+
S=np.maximum(market.S, 1e-10), K=option.K,
|
|
98
|
+
T=option.T, r=market.r, sigma=market.sigma, w=option.w
|
|
99
|
+
)
|
|
100
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Init file for instruments module
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from moqua.pricing.contracts import Calculator, Engine, Option, Market
|
|
3
|
+
|
|
4
|
+
class EuropeanOptionPricer:
|
|
5
|
+
|
|
6
|
+
def __init__(self, engine: Engine, yield_curve=None, vol_surface=None):
|
|
7
|
+
self.engine = engine
|
|
8
|
+
self.yield_curve = yield_curve
|
|
9
|
+
self.vol_surface = vol_surface
|
|
10
|
+
|
|
11
|
+
def set_engine(self, new_engine: Engine):
|
|
12
|
+
self.engine = new_engine
|
|
13
|
+
|
|
14
|
+
def get_calculator(self, K, T, spot=None, forward=None, r=None, sigma=None, option_type='call') -> Calculator:
|
|
15
|
+
w = np.where(np.asarray(option_type) == 'call', 1.0, -1.0)
|
|
16
|
+
option = Option(K=K, T=T, w=w)
|
|
17
|
+
|
|
18
|
+
if r is not None:
|
|
19
|
+
r_val = r
|
|
20
|
+
elif self.yield_curve is not None:
|
|
21
|
+
r_val = self.yield_curve.get_rate(T)
|
|
22
|
+
else:
|
|
23
|
+
raise ValueError("You must provide 'r' directly or configure a 'yield_curve'.")
|
|
24
|
+
|
|
25
|
+
if sigma is not None:
|
|
26
|
+
sigma_val = sigma
|
|
27
|
+
elif self.vol_surface is not None:
|
|
28
|
+
sigma_val = self.vol_surface.get_vol(K, T)
|
|
29
|
+
else:
|
|
30
|
+
raise ValueError("You must provide 'sigma' directly or configure a 'vol_surface'.")
|
|
31
|
+
|
|
32
|
+
market = Market(
|
|
33
|
+
r=r_val,
|
|
34
|
+
sigma=sigma_val,
|
|
35
|
+
S=spot, F=forward
|
|
36
|
+
)
|
|
37
|
+
return self.engine.calculator(option, market)
|
|
@@ -13,6 +13,12 @@ moqua.egg-info/requires.txt
|
|
|
13
13
|
moqua.egg-info/top_level.txt
|
|
14
14
|
moqua/pricing/__init__.py
|
|
15
15
|
moqua/pricing/contracts.py
|
|
16
|
+
moqua/pricing/engine/__init__.py
|
|
17
|
+
moqua/pricing/engine/bachelier.py
|
|
18
|
+
moqua/pricing/engine/black76.py
|
|
19
|
+
moqua/pricing/engine/black_scholes.py
|
|
20
|
+
moqua/pricing/instruments/__init__.py
|
|
21
|
+
moqua/pricing/instruments/european_option.py
|
|
16
22
|
src/erf_cody.cpp
|
|
17
23
|
src/lets_be_rational.cpp
|
|
18
24
|
src/normaldistribution.cpp
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|