deriweave-python 0.3.0__cp312-cp312-win_amd64.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.
- deriweave_py/__init__.py +112 -0
- deriweave_py/_core.cp312-win_amd64.pyd +0 -0
- deriweave_py/_core.pyi +108 -0
- deriweave_py/composition.py +560 -0
- deriweave_py/licenses/COPYING.BSD +26 -0
- deriweave_py/licenses/COPYING.MINPACK +51 -0
- deriweave_py/licenses/COPYING.MPL2 +373 -0
- deriweave_py/licenses/COPYING.README +18 -0
- deriweave_py/licenses/DeriWeave-LICENSE +21 -0
- deriweave_py/py.typed +0 -0
- deriweave_py/structured.py +165 -0
- deriweave_python-0.3.0.dist-info/METADATA +211 -0
- deriweave_python-0.3.0.dist-info/RECORD +15 -0
- deriweave_python-0.3.0.dist-info/WHEEL +5 -0
- deriweave_python-0.3.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,560 @@
|
|
|
1
|
+
"""Composable contract descriptions for the native DeriWeave pricing engine.
|
|
2
|
+
|
|
3
|
+
Objects here are *recipes*, not wrappers around live C++ pointers. They can be
|
|
4
|
+
reused in as many products as needed. On price(), the recipes are converted to
|
|
5
|
+
fresh, exclusively owned DeriWeave C++ expressions and products.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from datetime import date
|
|
11
|
+
from math import isfinite
|
|
12
|
+
from numbers import Real
|
|
13
|
+
from enum import Enum
|
|
14
|
+
from types import MappingProxyType
|
|
15
|
+
from typing import TYPE_CHECKING
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from ._core import PricingResult, SimulationSettings, MarketPath, Cashflow
|
|
19
|
+
from typing import Any, Mapping, Sequence
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _number(value: float, name: str) -> float:
|
|
23
|
+
if isinstance(value, bool) or not isinstance(value, Real):
|
|
24
|
+
raise TypeError(f"{name} must be a real number")
|
|
25
|
+
v = float(value)
|
|
26
|
+
if not isfinite(v):
|
|
27
|
+
raise ValueError(f"{name} must be finite")
|
|
28
|
+
return v
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class _ExerciseTime(Enum):
|
|
32
|
+
CURRENT = "exercise"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
ExerciseTime = _ExerciseTime.CURRENT
|
|
36
|
+
"""Symbolic decision date for Spot/Variance inside an American contract."""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _time(value: float, name: str) -> float:
|
|
40
|
+
result = _number(value, name)
|
|
41
|
+
if result < 0:
|
|
42
|
+
raise ValueError(f"{name} must be non-negative")
|
|
43
|
+
return result
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _observation_time(value: float | _ExerciseTime) -> float | str:
|
|
47
|
+
return "exercise" if value is ExerciseTime else _time(value, "observation date")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _freeze(obj: Any) -> Any:
|
|
51
|
+
if isinstance(obj, Mapping):
|
|
52
|
+
return MappingProxyType({k: _freeze(v) for k, v in obj.items()})
|
|
53
|
+
if isinstance(obj, (list, tuple)):
|
|
54
|
+
return tuple(_freeze(v) for v in obj)
|
|
55
|
+
return obj
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass(frozen=True)
|
|
59
|
+
class Asset:
|
|
60
|
+
identifier: str
|
|
61
|
+
|
|
62
|
+
def __post_init__(self) -> None:
|
|
63
|
+
if not isinstance(self.identifier, str) or not self.identifier:
|
|
64
|
+
raise ValueError("Asset identifier must be a non-empty string")
|
|
65
|
+
|
|
66
|
+
def __str__(self) -> str:
|
|
67
|
+
return self.identifier
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _asset_name(asset: Asset | str) -> str:
|
|
71
|
+
if isinstance(asset, Asset):
|
|
72
|
+
return asset.identifier
|
|
73
|
+
if not isinstance(asset, str) or not asset:
|
|
74
|
+
raise TypeError("asset must be an Asset or a non-empty string")
|
|
75
|
+
return asset
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass(frozen=True)
|
|
79
|
+
class ValueExpression:
|
|
80
|
+
"""Declarative description of a DeriWeave ValueExpression."""
|
|
81
|
+
|
|
82
|
+
kind: str
|
|
83
|
+
fields: Mapping[str, Any]
|
|
84
|
+
|
|
85
|
+
def __post_init__(self) -> None:
|
|
86
|
+
object.__setattr__(self, "fields", _freeze(self.fields))
|
|
87
|
+
|
|
88
|
+
def to_spec(self) -> dict[str, Any]:
|
|
89
|
+
return {"kind": self.kind, **_recursive_copy(self.fields)}
|
|
90
|
+
|
|
91
|
+
def __mul__(self, factor: float) -> ValueExpression:
|
|
92
|
+
return Scaled(self, factor)
|
|
93
|
+
|
|
94
|
+
def __rmul__(self, factor: float) -> ValueExpression:
|
|
95
|
+
return Scaled(self, factor)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _recursive_copy(obj: Any) -> Any:
|
|
99
|
+
if isinstance(obj, Mapping):
|
|
100
|
+
return {k: _recursive_copy(v) for k, v in obj.items()}
|
|
101
|
+
if isinstance(obj, (list, tuple)):
|
|
102
|
+
return [_recursive_copy(v) for v in obj]
|
|
103
|
+
return obj
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _expr(value: ValueExpression) -> dict[str, Any]:
|
|
107
|
+
if not isinstance(value, ValueExpression):
|
|
108
|
+
raise TypeError("Expected ValueExpression (use Constant(number) for a constant)")
|
|
109
|
+
return value.to_spec()
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def Spot(asset: Asset | str, at: float | _ExerciseTime) -> ValueExpression:
|
|
113
|
+
return ValueExpression("spot", {"asset": _asset_name(asset), "at": _observation_time(at)})
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def Variance(asset: Asset | str, at: float | _ExerciseTime) -> ValueExpression:
|
|
117
|
+
"""Heston's instantaneous variance V_t, *not* its volatility."""
|
|
118
|
+
return ValueExpression("variance", {"asset": _asset_name(asset), "at": _observation_time(at)})
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def Constant(value: float) -> ValueExpression:
|
|
122
|
+
return ValueExpression("constant", {"value": _number(value, "value")})
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def Scaled(value: ValueExpression, factor: float) -> ValueExpression:
|
|
126
|
+
return ValueExpression("scaled", {"value": _expr(value), "factor": _number(factor, "factor")})
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _strike(strike: float | ValueExpression) -> float | dict[str, Any]:
|
|
130
|
+
if isinstance(strike, ValueExpression):
|
|
131
|
+
return _expr(strike)
|
|
132
|
+
value = _number(strike, "strike")
|
|
133
|
+
if value < 0:
|
|
134
|
+
raise ValueError("Fixed strike must be non-negative; use Constant for a floating expression")
|
|
135
|
+
return value
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def Call(value: ValueExpression, strike: float | ValueExpression) -> ValueExpression:
|
|
139
|
+
return ValueExpression("call", {"value": _expr(value), "strike": _strike(strike)})
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def Put(value: ValueExpression, strike: float | ValueExpression) -> ValueExpression:
|
|
143
|
+
return ValueExpression("put", {"value": _expr(value), "strike": _strike(strike)})
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def Digital(value: ValueExpression, threshold: float | ValueExpression,
|
|
147
|
+
amount: float = 1.0, comparison: str = ">=") -> ValueExpression:
|
|
148
|
+
if comparison not in ("<", "<=", ">", ">="):
|
|
149
|
+
raise ValueError("comparison must be one of <, <=, >, >=")
|
|
150
|
+
if _number(amount, "amount") < 0:
|
|
151
|
+
raise ValueError("Digital amount must be non-negative")
|
|
152
|
+
return ValueExpression("digital", {
|
|
153
|
+
"value": _expr(value), "threshold": (_expr(threshold) if isinstance(threshold, ValueExpression) else _number(threshold, "threshold")),
|
|
154
|
+
"amount": _number(amount, "amount"), "comparison": comparison,
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def Basket(items: Sequence[ValueExpression], weights: Sequence[float]) -> ValueExpression:
|
|
159
|
+
parts = [_expr(item) for item in items]
|
|
160
|
+
w = [_number(weight, "weight") for weight in weights]
|
|
161
|
+
if any(weight < 0 for weight in w) or (w and not any(weight > 0 for weight in w)):
|
|
162
|
+
raise ValueError("Basket weights must be non-negative with at least one positive weight")
|
|
163
|
+
if not parts or len(parts) != len(w):
|
|
164
|
+
raise ValueError("Basket items and weights must have the same nonzero length")
|
|
165
|
+
return ValueExpression("basket", {"items": parts, "weights": w})
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def Average(items: Sequence[ValueExpression] | Asset | str, *,
|
|
169
|
+
dates: Sequence[float] | None = None, method: str = "arithmetic") -> ValueExpression:
|
|
170
|
+
"""Average a list of expressions, or one asset observed at multiple dates."""
|
|
171
|
+
if isinstance(items, (Asset, str)):
|
|
172
|
+
if dates is None:
|
|
173
|
+
raise ValueError("dates are required when Average receives an asset")
|
|
174
|
+
values = [Spot(items, t) for t in dates]
|
|
175
|
+
else:
|
|
176
|
+
if dates is not None:
|
|
177
|
+
raise ValueError("dates are only valid when Average receives an asset")
|
|
178
|
+
values = list(items)
|
|
179
|
+
if not values:
|
|
180
|
+
raise ValueError("Average requires at least one observation")
|
|
181
|
+
if method not in ("arithmetic", "geometric"):
|
|
182
|
+
raise ValueError("Average method must be arithmetic or geometric")
|
|
183
|
+
return ValueExpression("average", {"items": [_expr(v) for v in values], "method": method})
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def WorstOf(items: Sequence[ValueExpression]) -> ValueExpression:
|
|
187
|
+
parts = [_expr(item) for item in items]
|
|
188
|
+
if not parts:
|
|
189
|
+
raise ValueError("WorstOf requires at least one expression")
|
|
190
|
+
return ValueExpression("worst_of", {"items": parts})
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def BestOf(items: Sequence[ValueExpression]) -> ValueExpression:
|
|
194
|
+
parts = [_expr(item) for item in items]
|
|
195
|
+
if not parts:
|
|
196
|
+
raise ValueError("BestOf requires at least one expression")
|
|
197
|
+
return ValueExpression("best_of", {"items": parts})
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def Conditional(condition: ValueExpression, if_true: ValueExpression,
|
|
201
|
+
if_false: ValueExpression) -> ValueExpression:
|
|
202
|
+
"""Condition must evaluate to exactly 0 or 1 (typically a Digital)."""
|
|
203
|
+
return ValueExpression("conditional", {
|
|
204
|
+
"condition": _expr(condition), "if_true": _expr(if_true), "if_false": _expr(if_false),
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
@dataclass(frozen=True)
|
|
209
|
+
class CashflowRule:
|
|
210
|
+
payment_date: float
|
|
211
|
+
amount: ValueExpression
|
|
212
|
+
|
|
213
|
+
def __post_init__(self) -> None:
|
|
214
|
+
object.__setattr__(self, "payment_date", _time(self.payment_date, "payment_date"))
|
|
215
|
+
_require_expr(self.amount)
|
|
216
|
+
|
|
217
|
+
def to_spec(self) -> dict[str, Any]:
|
|
218
|
+
return {"payment_date": _number(self.payment_date, "payment_date"),
|
|
219
|
+
"amount": _expr(self.amount)}
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
@dataclass(frozen=True, init=False)
|
|
223
|
+
class ComposedProduct:
|
|
224
|
+
rules: tuple[CashflowRule, ...]
|
|
225
|
+
|
|
226
|
+
def __init__(self, rules: Sequence[CashflowRule]):
|
|
227
|
+
items = tuple(rules)
|
|
228
|
+
if not items or any(not isinstance(r, CashflowRule) for r in items):
|
|
229
|
+
raise ValueError("ComposedProduct needs one or more CashflowRule objects")
|
|
230
|
+
object.__setattr__(self, "rules", items)
|
|
231
|
+
|
|
232
|
+
def to_spec(self) -> dict[str, Any]:
|
|
233
|
+
return {"kind": "composed", "rules": [rule.to_spec() for rule in self.rules]}
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def Product(payoff: ValueExpression | Sequence[CashflowRule], *,
|
|
237
|
+
payment_date: float | None = None) -> ComposedProduct:
|
|
238
|
+
"""Convenience: Product(Call(...), payment_date=1.0) or Product([rules...])."""
|
|
239
|
+
if isinstance(payoff, ValueExpression):
|
|
240
|
+
if payment_date is None:
|
|
241
|
+
raise ValueError("payment_date is mandatory for a single payoff")
|
|
242
|
+
return ComposedProduct([CashflowRule(payment_date, payoff)])
|
|
243
|
+
if payment_date is not None:
|
|
244
|
+
raise ValueError("Do not provide payment_date when passing CashflowRule objects")
|
|
245
|
+
return ComposedProduct(payoff)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
@dataclass(frozen=True, init=False)
|
|
249
|
+
class DiscreteBarrier:
|
|
250
|
+
"""Discrete knock-in/out wrapper; no rebate.
|
|
251
|
+
|
|
252
|
+
Monitor either one underlying on monitoring_dates, or arbitrary expressions
|
|
253
|
+
with observations=[(date, expression), ...]. These modes are exclusive.
|
|
254
|
+
"""
|
|
255
|
+
product: ComposedProduct | DiscreteBarrier
|
|
256
|
+
level: float
|
|
257
|
+
direction: str
|
|
258
|
+
activation: str
|
|
259
|
+
underlying: str | None
|
|
260
|
+
monitoring_dates: tuple[float, ...] | None
|
|
261
|
+
observations: tuple[tuple[float, ValueExpression], ...] | None
|
|
262
|
+
|
|
263
|
+
def __init__(self, product: ComposedProduct | DiscreteBarrier, *, level: float,
|
|
264
|
+
direction: str = "down", activation: str = "out",
|
|
265
|
+
underlying: Asset | str | None = None,
|
|
266
|
+
monitoring_dates: Sequence[float] | None = None,
|
|
267
|
+
observations: Sequence[tuple[float, ValueExpression]] | None = None):
|
|
268
|
+
_product_spec(product)
|
|
269
|
+
if direction not in ("down", "up") or activation not in ("in", "out"):
|
|
270
|
+
raise ValueError("direction must be down/up and activation must be in/out")
|
|
271
|
+
direct = underlying is not None and monitoring_dates is not None and observations is None
|
|
272
|
+
custom = observations is not None and underlying is None and monitoring_dates is None
|
|
273
|
+
if not (direct or custom):
|
|
274
|
+
raise ValueError("Provide underlying+monitoring_dates OR observations, not both")
|
|
275
|
+
obs = None if observations is None else tuple((
|
|
276
|
+
_time(t, "observation date"), _require_expr(v)) for t, v in observations)
|
|
277
|
+
dates = None if monitoring_dates is None else tuple(_time(t, "monitoring date") for t in monitoring_dates)
|
|
278
|
+
if (obs is not None and not obs) or (dates is not None and not dates):
|
|
279
|
+
raise ValueError("Barrier needs at least one monitoring observation")
|
|
280
|
+
object.__setattr__(self, "product", product)
|
|
281
|
+
object.__setattr__(self, "level", _number(level, "level"))
|
|
282
|
+
object.__setattr__(self, "direction", direction)
|
|
283
|
+
object.__setattr__(self, "activation", activation)
|
|
284
|
+
object.__setattr__(self, "underlying", None if underlying is None else _asset_name(underlying))
|
|
285
|
+
object.__setattr__(self, "monitoring_dates", dates)
|
|
286
|
+
object.__setattr__(self, "observations", obs)
|
|
287
|
+
|
|
288
|
+
def to_spec(self) -> dict[str, Any]:
|
|
289
|
+
result = {"kind": "barrier", "product": self.product.to_spec(),
|
|
290
|
+
"level": self.level, "direction": self.direction, "activation": self.activation}
|
|
291
|
+
if self.observations is not None:
|
|
292
|
+
result["observations"] = [
|
|
293
|
+
{"date": t, "value": _expr(expr)} for t, expr in self.observations]
|
|
294
|
+
else:
|
|
295
|
+
result["asset"] = self.underlying
|
|
296
|
+
result["monitoring_dates"] = list(self.monitoring_dates or ())
|
|
297
|
+
return result
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _require_expr(value: ValueExpression) -> ValueExpression:
|
|
301
|
+
_expr(value)
|
|
302
|
+
return value
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
@dataclass(frozen=True)
|
|
306
|
+
class SpotControl:
|
|
307
|
+
asset: Asset | str
|
|
308
|
+
at: float
|
|
309
|
+
|
|
310
|
+
def to_spec(self) -> dict[str, Any]:
|
|
311
|
+
return {"kind": "spot", "asset": _asset_name(self.asset), "at": _number(self.at, "at")}
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
@dataclass(frozen=True)
|
|
315
|
+
class Market:
|
|
316
|
+
"""Quotes mapped by asset name. Insertion order is the model's asset order."""
|
|
317
|
+
spots: Mapping[Asset | str, float]
|
|
318
|
+
rate: float
|
|
319
|
+
dividend_yields: Mapping[Asset | str, float] | None = None
|
|
320
|
+
valuation_date: date | None = None
|
|
321
|
+
|
|
322
|
+
def __post_init__(self) -> None:
|
|
323
|
+
if not isinstance(self.spots, Mapping):
|
|
324
|
+
raise TypeError("spots must map asset names to quotes")
|
|
325
|
+
def snapshot(values: Mapping, label: str) -> Mapping:
|
|
326
|
+
if not isinstance(values, Mapping):
|
|
327
|
+
raise TypeError(f"{label} must map asset names to values")
|
|
328
|
+
result: dict[str, float] = {}
|
|
329
|
+
for key, value in values.items():
|
|
330
|
+
name = _asset_name(key)
|
|
331
|
+
if name in result:
|
|
332
|
+
raise ValueError(f"Duplicate asset name in {label}: {name}")
|
|
333
|
+
result[name] = _number(value, label)
|
|
334
|
+
return MappingProxyType(result)
|
|
335
|
+
object.__setattr__(self, "spots", snapshot(self.spots, "spots"))
|
|
336
|
+
if self.dividend_yields is not None:
|
|
337
|
+
object.__setattr__(self, "dividend_yields", snapshot(self.dividend_yields, "dividend_yields"))
|
|
338
|
+
|
|
339
|
+
def to_spec(self) -> dict[str, Any]:
|
|
340
|
+
if not self.spots:
|
|
341
|
+
raise ValueError("Market requires at least one asset")
|
|
342
|
+
names = [_asset_name(name) for name in self.spots]
|
|
343
|
+
if len(set(names)) != len(names):
|
|
344
|
+
raise ValueError("Duplicate asset names")
|
|
345
|
+
divs = {} if self.dividend_yields is None else self.dividend_yields
|
|
346
|
+
if set(divs) - set(names):
|
|
347
|
+
raise ValueError("dividend_yields contains an unknown asset")
|
|
348
|
+
d = self.valuation_date if self.valuation_date is not None else date.today()
|
|
349
|
+
if not isinstance(d, date):
|
|
350
|
+
raise TypeError("valuation_date must be datetime.date or None")
|
|
351
|
+
return {"assets": names,
|
|
352
|
+
"spots": [_number(v, "spot") for v in self.spots.values()],
|
|
353
|
+
"dividend_yields": [_number(divs.get(name, 0.0), "dividend yield") for name in names],
|
|
354
|
+
"rate": _number(self.rate, "rate"),
|
|
355
|
+
"year": d.year, "month": d.month, "day": d.day}
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def _vector(values: float | Sequence[float] | Mapping[str, float],
|
|
359
|
+
assets: Sequence[str], label: str) -> list[float]:
|
|
360
|
+
if isinstance(values, Mapping):
|
|
361
|
+
if set(values) != set(assets):
|
|
362
|
+
raise ValueError(f"{label} keys must match market assets")
|
|
363
|
+
return [_number(values[name], label) for name in assets]
|
|
364
|
+
if isinstance(values, Real):
|
|
365
|
+
if len(assets) != 1:
|
|
366
|
+
raise ValueError(f"Scalar {label} only valid for one asset")
|
|
367
|
+
return [_number(values, label)]
|
|
368
|
+
result = [_number(v, label) for v in values]
|
|
369
|
+
if len(result) != len(assets):
|
|
370
|
+
raise ValueError(f"{label} length must equal the market asset count")
|
|
371
|
+
return result
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def _matrix(correlation: Sequence[Sequence[float]] | None, size: int) -> list[list[float]]:
|
|
375
|
+
if correlation is None:
|
|
376
|
+
return [[float(i == j) for j in range(size)] for i in range(size)]
|
|
377
|
+
result = [[_number(x, "correlation") for x in row] for row in correlation]
|
|
378
|
+
if len(result) != size or any(len(row) != size for row in result):
|
|
379
|
+
raise ValueError(f"Correlation matrix must be {size}x{size}")
|
|
380
|
+
return result
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def _params(values: Mapping[str, float] | Sequence[Mapping[str, float]] |
|
|
384
|
+
Mapping[str, Mapping[str, float]], assets: Sequence[str],
|
|
385
|
+
fields: Sequence[str]) -> list[list[float]]:
|
|
386
|
+
if isinstance(values, Mapping) and set(values) == set(fields) and len(assets) == 1:
|
|
387
|
+
rows = [values]
|
|
388
|
+
elif isinstance(values, Mapping):
|
|
389
|
+
if set(values) != set(assets):
|
|
390
|
+
raise ValueError("Model parameter asset names must match market")
|
|
391
|
+
rows = [values[name] for name in assets]
|
|
392
|
+
else:
|
|
393
|
+
rows = list(values)
|
|
394
|
+
if len(rows) != len(assets):
|
|
395
|
+
raise ValueError("Model parameter count must match market assets")
|
|
396
|
+
result = []
|
|
397
|
+
for row in rows:
|
|
398
|
+
if not isinstance(row, Mapping) or set(row) != set(fields):
|
|
399
|
+
raise ValueError(f"Model parameters must have fields {tuple(fields)}")
|
|
400
|
+
result.append([_number(row[field], field) for field in fields])
|
|
401
|
+
return result
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
@dataclass(frozen=True)
|
|
405
|
+
class BlackScholes:
|
|
406
|
+
volatilities: float | Sequence[float] | Mapping[str, float]
|
|
407
|
+
correlation: Sequence[Sequence[float]] | None = None
|
|
408
|
+
|
|
409
|
+
def to_spec(self, assets: Sequence[str]) -> dict[str, Any]:
|
|
410
|
+
return {"kind": "black_scholes", "volatilities": _vector(self.volatilities, assets, "volatility"),
|
|
411
|
+
"correlation": _matrix(self.correlation, len(assets))}
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
@dataclass(frozen=True)
|
|
415
|
+
class Heston:
|
|
416
|
+
parameters: Mapping | Sequence[Mapping]
|
|
417
|
+
rho: float | None = None
|
|
418
|
+
correlation: Sequence[Sequence[float]] | None = None
|
|
419
|
+
|
|
420
|
+
def to_spec(self, assets: Sequence[str]) -> dict[str, Any]:
|
|
421
|
+
spec = {"kind": "heston", "parameters": _params(self.parameters, assets, ("v0", "kappa", "theta", "xi"))}
|
|
422
|
+
if self.rho is not None:
|
|
423
|
+
if self.correlation is not None:
|
|
424
|
+
raise ValueError("Provide rho or a full correlation matrix, not both")
|
|
425
|
+
if len(assets) != 1:
|
|
426
|
+
raise ValueError("rho is only valid for single-asset Heston")
|
|
427
|
+
spec["rho"] = _number(self.rho, "rho")
|
|
428
|
+
else:
|
|
429
|
+
if self.correlation is None:
|
|
430
|
+
raise ValueError("Heston requires rho (single asset) or a full 2N x 2N correlation")
|
|
431
|
+
spec["correlation"] = _matrix(self.correlation, 2 * len(assets))
|
|
432
|
+
return spec
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
@dataclass(frozen=True)
|
|
436
|
+
class Merton:
|
|
437
|
+
parameters: Mapping | Sequence[Mapping]
|
|
438
|
+
correlation: Sequence[Sequence[float]] | None = None
|
|
439
|
+
|
|
440
|
+
def to_spec(self, assets: Sequence[str]) -> dict[str, Any]:
|
|
441
|
+
return {"kind": "merton", "parameters": _params(self.parameters, assets, ("sigma", "lambda", "mu", "delta")),
|
|
442
|
+
"correlation": _matrix(self.correlation, len(assets))}
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
@dataclass(frozen=True)
|
|
446
|
+
class Kou:
|
|
447
|
+
parameters: Mapping | Sequence[Mapping]
|
|
448
|
+
correlation: Sequence[Sequence[float]] | None = None
|
|
449
|
+
|
|
450
|
+
def to_spec(self, assets: Sequence[str]) -> dict[str, Any]:
|
|
451
|
+
return {"kind": "kou", "parameters": _params(self.parameters, assets, ("sigma", "lambda", "p", "eta_plus", "eta_minus")),
|
|
452
|
+
"correlation": _matrix(self.correlation, len(assets))}
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
@dataclass(frozen=True)
|
|
456
|
+
class VarianceGamma:
|
|
457
|
+
sigmas: float | Sequence[float] | Mapping[str, float]
|
|
458
|
+
thetas: float | Sequence[float] | Mapping[str, float]
|
|
459
|
+
nu: float
|
|
460
|
+
correlation: Sequence[Sequence[float]] | None = None
|
|
461
|
+
|
|
462
|
+
def to_spec(self, assets: Sequence[str]) -> dict[str, Any]:
|
|
463
|
+
return {"kind": "variance_gamma", "sigmas": _vector(self.sigmas, assets, "sigma"),
|
|
464
|
+
"thetas": _vector(self.thetas, assets, "theta"), "nu": _number(self.nu, "nu"),
|
|
465
|
+
"correlation": _matrix(self.correlation, len(assets))}
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
@dataclass(frozen=True)
|
|
469
|
+
class NIG:
|
|
470
|
+
sigmas: float | Sequence[float] | Mapping[str, float]
|
|
471
|
+
thetas: float | Sequence[float] | Mapping[str, float]
|
|
472
|
+
nu: float
|
|
473
|
+
correlation: Sequence[Sequence[float]] | None = None
|
|
474
|
+
|
|
475
|
+
def to_spec(self, assets: Sequence[str]) -> dict[str, Any]:
|
|
476
|
+
return {"kind": "nig", "sigmas": _vector(self.sigmas, assets, "sigma"),
|
|
477
|
+
"thetas": _vector(self.thetas, assets, "theta"), "nu": _number(self.nu, "nu"),
|
|
478
|
+
"correlation": _matrix(self.correlation, len(assets))}
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
def _product_spec(product: Any) -> dict[str, Any]:
|
|
482
|
+
from .structured import AutocallProduct
|
|
483
|
+
if not isinstance(product, (ComposedProduct, DiscreteBarrier, AutocallProduct)):
|
|
484
|
+
raise TypeError("Expected ComposedProduct, DiscreteBarrier or AutocallProduct")
|
|
485
|
+
return product.to_spec()
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
def _settings(settings: SimulationSettings | None) -> SimulationSettings:
|
|
489
|
+
from . import _core
|
|
490
|
+
if settings is not None and not isinstance(settings, _core.SimulationSettings):
|
|
491
|
+
raise TypeError("settings must be SimulationSettings or None")
|
|
492
|
+
return settings if settings is not None else _core.SimulationSettings()
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def _market_model(market: Market, model: Any) -> tuple[dict[str, Any], dict[str, Any]]:
|
|
496
|
+
if not isinstance(market, Market):
|
|
497
|
+
raise TypeError("market must be Market")
|
|
498
|
+
if not isinstance(model, (BlackScholes, Heston, Merton, Kou, VarianceGamma, NIG)):
|
|
499
|
+
raise TypeError("Unknown model type")
|
|
500
|
+
ms = market.to_spec()
|
|
501
|
+
return ms, model.to_spec(ms["assets"])
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def price(product: Any, *, market: Market,
|
|
505
|
+
model: BlackScholes | Heston | Merton | Kou | VarianceGamma | NIG,
|
|
506
|
+
settings: SimulationSettings | None = None,
|
|
507
|
+
control: SpotControl | None = None) -> PricingResult:
|
|
508
|
+
"""Price a composed contract or evaluate a frozen exercise policy in C++.
|
|
509
|
+
|
|
510
|
+
n_simulations counts observations (two paths per antithetic observation).
|
|
511
|
+
A SpotControl requires settings.use_control_variate=True. Training a policy
|
|
512
|
+
is a separate explicit operation; this function never learns a policy.
|
|
513
|
+
"""
|
|
514
|
+
from . import _core
|
|
515
|
+
if control is not None and not isinstance(control, SpotControl):
|
|
516
|
+
raise TypeError("control must be SpotControl or None")
|
|
517
|
+
mc = _settings(settings)
|
|
518
|
+
ms, model_spec = _market_model(market, model)
|
|
519
|
+
ctrl = None if control is None else control.to_spec()
|
|
520
|
+
if isinstance(product, _core.ExercisePolicy):
|
|
521
|
+
return _core.price_exercise_policy(product, ms, model_spec, mc, ctrl)
|
|
522
|
+
return _core.price_composed(_product_spec(product), ms, model_spec, mc, ctrl)
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
def simulate_path(*, market: Market, model: Any, dates: Sequence[float],
|
|
526
|
+
seed: int = 42) -> MarketPath:
|
|
527
|
+
"""Generate one native path on exact dates starting at zero; requires NumPy."""
|
|
528
|
+
from . import _core
|
|
529
|
+
ms, model_spec = _market_model(market, model)
|
|
530
|
+
return _core.simulate_path(ms, model_spec, list(dates), seed)
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
def evaluate(product: Any, path: MarketPath) -> list[Cashflow]:
|
|
534
|
+
"""Return undiscounted native Cashflow objects on a supplied MarketPath."""
|
|
535
|
+
from . import _core
|
|
536
|
+
return _core.evaluate(_product_spec(product), path)
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
def evaluate_expression(value: ValueExpression, path: MarketPath) -> float:
|
|
540
|
+
"""Evaluate an expression on a native MarketPath without simulation."""
|
|
541
|
+
from . import _core
|
|
542
|
+
return _core.evaluate_expression(_expr(value), path)
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
def simulation_dates(product: Any, *, settings: SimulationSettings | None = None,
|
|
546
|
+
control: SpotControl | None = None) -> list[float]:
|
|
547
|
+
"""Inspect the native numerical date union, independently of payment dates."""
|
|
548
|
+
from . import _core
|
|
549
|
+
if control is not None and not isinstance(control, SpotControl):
|
|
550
|
+
raise TypeError("control must be SpotControl or None")
|
|
551
|
+
return _core.simulation_dates(_product_spec(product), _settings(settings),
|
|
552
|
+
None if control is None else control.to_spec())
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
def has_finite_spot_moment(*, market: Market, model: Kou | VarianceGamma | NIG,
|
|
556
|
+
asset: Asset | str, order: float = 2) -> bool:
|
|
557
|
+
"""Use the engine's moment-domain check (Kou, VG and NIG only)."""
|
|
558
|
+
from . import _core
|
|
559
|
+
ms, model_spec = _market_model(market, model)
|
|
560
|
+
return _core.has_finite_spot_moment(model_spec, ms, _asset_name(asset), _number(order, "order"))
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright (c) 2011, Intel Corporation. All rights reserved.
|
|
3
|
+
|
|
4
|
+
Redistribution and use in source and binary forms, with or without modification,
|
|
5
|
+
are permitted provided that the following conditions are met:
|
|
6
|
+
|
|
7
|
+
* Redistributions of source code must retain the above copyright notice, this
|
|
8
|
+
list of conditions and the following disclaimer.
|
|
9
|
+
* Redistributions in binary form must reproduce the above copyright notice,
|
|
10
|
+
this list of conditions and the following disclaimer in the documentation
|
|
11
|
+
and/or other materials provided with the distribution.
|
|
12
|
+
* Neither the name of Intel Corporation nor the names of its contributors may
|
|
13
|
+
be used to endorse or promote products derived from this software without
|
|
14
|
+
specific prior written permission.
|
|
15
|
+
|
|
16
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
17
|
+
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
18
|
+
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
19
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
|
20
|
+
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
21
|
+
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
22
|
+
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
|
23
|
+
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
24
|
+
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
25
|
+
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
26
|
+
*/
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
Minpack Copyright Notice (1999) University of Chicago. All rights reserved
|
|
2
|
+
|
|
3
|
+
Redistribution and use in source and binary forms, with or
|
|
4
|
+
without modification, are permitted provided that the
|
|
5
|
+
following conditions are met:
|
|
6
|
+
|
|
7
|
+
1. Redistributions of source code must retain the above
|
|
8
|
+
copyright notice, this list of conditions and the following
|
|
9
|
+
disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above
|
|
12
|
+
copyright notice, this list of conditions and the following
|
|
13
|
+
disclaimer in the documentation and/or other materials
|
|
14
|
+
provided with the distribution.
|
|
15
|
+
|
|
16
|
+
3. The end-user documentation included with the
|
|
17
|
+
redistribution, if any, must include the following
|
|
18
|
+
acknowledgment:
|
|
19
|
+
|
|
20
|
+
"This product includes software developed by the
|
|
21
|
+
University of Chicago, as Operator of Argonne National
|
|
22
|
+
Laboratory.
|
|
23
|
+
|
|
24
|
+
Alternately, this acknowledgment may appear in the software
|
|
25
|
+
itself, if and wherever such third-party acknowledgments
|
|
26
|
+
normally appear.
|
|
27
|
+
|
|
28
|
+
4. WARRANTY DISCLAIMER. THE SOFTWARE IS SUPPLIED "AS IS"
|
|
29
|
+
WITHOUT WARRANTY OF ANY KIND. THE COPYRIGHT HOLDER, THE
|
|
30
|
+
UNITED STATES, THE UNITED STATES DEPARTMENT OF ENERGY, AND
|
|
31
|
+
THEIR EMPLOYEES: (1) DISCLAIM ANY WARRANTIES, EXPRESS OR
|
|
32
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO ANY IMPLIED WARRANTIES
|
|
33
|
+
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE
|
|
34
|
+
OR NON-INFRINGEMENT, (2) DO NOT ASSUME ANY LEGAL LIABILITY
|
|
35
|
+
OR RESPONSIBILITY FOR THE ACCURACY, COMPLETENESS, OR
|
|
36
|
+
USEFULNESS OF THE SOFTWARE, (3) DO NOT REPRESENT THAT USE OF
|
|
37
|
+
THE SOFTWARE WOULD NOT INFRINGE PRIVATELY OWNED RIGHTS, (4)
|
|
38
|
+
DO NOT WARRANT THAT THE SOFTWARE WILL FUNCTION
|
|
39
|
+
UNINTERRUPTED, THAT IT IS ERROR-FREE OR THAT ANY ERRORS WILL
|
|
40
|
+
BE CORRECTED.
|
|
41
|
+
|
|
42
|
+
5. LIMITATION OF LIABILITY. IN NO EVENT WILL THE COPYRIGHT
|
|
43
|
+
HOLDER, THE UNITED STATES, THE UNITED STATES DEPARTMENT OF
|
|
44
|
+
ENERGY, OR THEIR EMPLOYEES: BE LIABLE FOR ANY INDIRECT,
|
|
45
|
+
INCIDENTAL, CONSEQUENTIAL, SPECIAL OR PUNITIVE DAMAGES OF
|
|
46
|
+
ANY KIND OR NATURE, INCLUDING BUT NOT LIMITED TO LOSS OF
|
|
47
|
+
PROFITS OR LOSS OF DATA, FOR ANY REASON WHATSOEVER, WHETHER
|
|
48
|
+
SUCH LIABILITY IS ASSERTED ON THE BASIS OF CONTRACT, TORT
|
|
49
|
+
(INCLUDING NEGLIGENCE OR STRICT LIABILITY), OR OTHERWISE,
|
|
50
|
+
EVEN IF ANY OF SAID PARTIES HAS BEEN WARNED OF THE
|
|
51
|
+
POSSIBILITY OF SUCH LOSS OR DAMAGES.
|