aggregate_api 1.0.0__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.
- aggregate_api/__init__.py +41 -0
- aggregate_api/__main__.py +154 -0
- aggregate_api/app.py +206 -0
- aggregate_api/audit.py +395 -0
- aggregate_api/bounds.py +331 -0
- aggregate_api/cache.py +319 -0
- aggregate_api/capability.py +823 -0
- aggregate_api/completion.py +219 -0
- aggregate_api/config.py +363 -0
- aggregate_api/cors.py +61 -0
- aggregate_api/examples.py +620 -0
- aggregate_api/layer_pricing.py +840 -0
- aggregate_api/library.py +94 -0
- aggregate_api/library_notes.py +96 -0
- aggregate_api/models.py +1407 -0
- aggregate_api/net.py +281 -0
- aggregate_api/pnl.py +101 -0
- aggregate_api/pricing.py +778 -0
- aggregate_api/resources.py +257 -0
- aggregate_api/routes/__init__.py +8 -0
- aggregate_api/routes/decl.py +327 -0
- aggregate_api/routes/examples.py +82 -0
- aggregate_api/routes/meta.py +282 -0
- aggregate_api/routes/objects.py +4119 -0
- aggregate_api/routes/status.py +466 -0
- aggregate_api/serializers.py +565 -0
- aggregate_api/sessions.py +353 -0
- aggregate_api/static/aggregate-api-logo-512.png +0 -0
- aggregate_api/static/aggregate-api-logo.png +0 -0
- aggregate_api/static/aggregate-api-trim.png +0 -0
- aggregate_api/static/android-chrome-192x192.png +0 -0
- aggregate_api/static/android-chrome-512x512.png +0 -0
- aggregate_api/static/apple-touch-icon.png +0 -0
- aggregate_api/static/assets/bootstrap-icons-BeopsB42.woff +0 -0
- aggregate_api/static/assets/bootstrap-icons-mSm7cUeB.woff2 +0 -0
- aggregate_api/static/assets/bootstrap-ohb1VZ53.js +5 -0
- aggregate_api/static/assets/codemirror-h62DHGGa.js +14 -0
- aggregate_api/static/assets/csv-grid.worker-DKzHGXac.js +4 -0
- aggregate_api/static/assets/echarts-B7o9sc00.js +40 -0
- aggregate_api/static/assets/echarts-gl-DG1Uf6wE.js +4282 -0
- aggregate_api/static/assets/lite-CUlcD8p4.css +1 -0
- aggregate_api/static/assets/lite-Dd2TnT4M.js +1 -0
- aggregate_api/static/assets/main-Bxhxa55v.css +9 -0
- aggregate_api/static/assets/main-CmoEiPit.js +9 -0
- aggregate_api/static/assets/tables-BHCF7qIF.js +8 -0
- aggregate_api/static/assets/tables-CxvajLr7.css +1 -0
- aggregate_api/static/favicon-16x16.png +0 -0
- aggregate_api/static/favicon-32x32.png +0 -0
- aggregate_api/static/favicon.ico +0 -0
- aggregate_api/static/index.html +912 -0
- aggregate_api/static/lite.html +83 -0
- aggregate_api/static/logo.png +0 -0
- aggregate_api/static/site.webmanifest +14 -0
- aggregate_api/static/sw.js +78 -0
- aggregate_api/status.py +536 -0
- aggregate_api/status_page.html +546 -0
- aggregate_api/tables.py +316 -0
- aggregate_api-1.0.0.dist-info/METADATA +187 -0
- aggregate_api-1.0.0.dist-info/RECORD +63 -0
- aggregate_api-1.0.0.dist-info/WHEEL +5 -0
- aggregate_api-1.0.0.dist-info/entry_points.txt +2 -0
- aggregate_api-1.0.0.dist-info/licenses/LICENSE +28 -0
- aggregate_api-1.0.0.dist-info/top_level.txt +1 -0
aggregate_api/pricing.py
ADDED
|
@@ -0,0 +1,778 @@
|
|
|
1
|
+
"""The Pricing group: validate a form, call the method, serve the exhibits.
|
|
2
|
+
|
|
3
|
+
Five runners, one shape each. ``run_pricing_preview`` completes the pentagon
|
|
4
|
+
and answers with scalars, because a preview line prints numbers.
|
|
5
|
+
``run_calibration``, ``run_natural_allocation`` and ``run_evaluation`` hand back
|
|
6
|
+
library exhibit envelopes, because everything else on this pane is a table the
|
|
7
|
+
library owns. ``run_ruin`` serves the Pr Ruin pill: the ``ruin`` chart document
|
|
8
|
+
and the ``ruin`` exhibit envelopes from one press, both built by the library on
|
|
9
|
+
the same seed (``dev/plan-pk-tab.md``).
|
|
10
|
+
|
|
11
|
+
Two of them calibrate and differ only in what they then ask for.
|
|
12
|
+
``run_calibration`` serves the receipt and the parts priced on their own
|
|
13
|
+
(``pricing.calibrate``, ``pricing.stand_alone``); ``run_natural_allocation``
|
|
14
|
+
serves the one premium split across those parts (``pricing.allocate``). They are
|
|
15
|
+
two presses rather than one because the allocation is real work on an
|
|
16
|
+
occurrence program, where it builds the joint distribution of gross and ceded,
|
|
17
|
+
and a reader who wants a calibration should not pay for one they did not ask
|
|
18
|
+
for.
|
|
19
|
+
|
|
20
|
+
There is no pandas in this file, and that is the point of it. Through 1.0.0a84
|
|
21
|
+
it held the other half of the pane: a ``_BasisView`` duck type presenting one
|
|
22
|
+
column of ``reins_density_df`` to an unbound ``Aggregate.calibrate_distortions``,
|
|
23
|
+
a pentagon completed row by row, a subtraction producing the difference rows, and
|
|
24
|
+
four ``tables.FORMATS`` entries saying how the results should print. All of it
|
|
25
|
+
was this repo deciding what a price means.
|
|
26
|
+
|
|
27
|
+
The library owns that now. ``calibrate_distortions`` and ``evaluate`` return
|
|
28
|
+
``CalibrationResult`` and ``EvaluationResult``; the registry dispatches
|
|
29
|
+
``pricing.calibrate``, ``pricing.stand_alone``, ``pricing.allocate`` and
|
|
30
|
+
``pricing.evaluate`` on those, with the frames materialized on the result, the
|
|
31
|
+
captions written upstream and the formats resolved into the document.
|
|
32
|
+
``calibrate_distortions(reins_view=...)`` does what the shim did and more, since
|
|
33
|
+
the library knows five views where the shim knew three.
|
|
34
|
+
|
|
35
|
+
See ``dev/plan-pricing-exhibits.md``, and for the shipped contract this codes
|
|
36
|
+
against, ``aggregate_REFACTOR/dev/plan-pricing-exhibits-LIB.md`` section 4.
|
|
37
|
+
The stand-alone / allocate split and the allocation route are
|
|
38
|
+
``dev/plan-pricing-natural-allocation.md`` phase B1.
|
|
39
|
+
|
|
40
|
+
Notes
|
|
41
|
+
-----
|
|
42
|
+
Every ``ValueError`` raised on this path is written to be read by a person, so
|
|
43
|
+
each route turns one into an HTTP 400 whose ``detail`` is the message verbatim.
|
|
44
|
+
Three reach the app: the unbounded anchor guard on ``p = 1``, a loss-ratio target
|
|
45
|
+
implying a premium above the assets, and the "exactly one of" validations.
|
|
46
|
+
|
|
47
|
+
**The standing rule that makes this safe on a shared object.** Pricing writes
|
|
48
|
+
result attributes onto the object it prices (``distortions``,
|
|
49
|
+
``distortion_df``, ``calibration_df``), and since a110 the object cache is
|
|
50
|
+
deliberately shared while the recipe base is not, so two sessions can be pricing
|
|
51
|
+
one object at once. That residue is accepted, on measured grounds: nothing in
|
|
52
|
+
this repo reads any of those attributes back, and every request recomputes from
|
|
53
|
+
its own form. The one condition attached to that acceptance is a rule for this
|
|
54
|
+
file, so it is written here rather than left as a habit.
|
|
55
|
+
|
|
56
|
+
**Expenses enter only through the premium leg, and the arithmetic lives here,
|
|
57
|
+
once.** ``octet.premium`` everywhere in this group is a technical premium, net
|
|
58
|
+
of expenses, and a reader pricing against a real quote types a gross one. With
|
|
59
|
+
an ``expense_ratio`` a premium target is read as gross and the engine is fed
|
|
60
|
+
``premium * (1 - expense_ratio)``; a CoC or LR target calibrates exactly as
|
|
61
|
+
before, the ratio only translating the resolved technical premium into a gross
|
|
62
|
+
reading on the preview. The flat-ratio identity ``gross = net / (1 - e)``
|
|
63
|
+
matches ``pnl_program``'s own ``expense_ratio`` meaning (gross expense as a
|
|
64
|
+
fraction of premium). If the expense model ever grows past a flat ratio, the
|
|
65
|
+
arithmetic moves upstream; it does not grow a second home here or in the SPA,
|
|
66
|
+
whose forms hold no arithmetic.
|
|
67
|
+
|
|
68
|
+
**Every call from here passes its own target and its own distortions
|
|
69
|
+
explicitly. Never lean on a library default that reads ``self.distortions``.**
|
|
70
|
+
A method that falls back to the stored fit would serve this request an answer
|
|
71
|
+
struck for whoever priced the object last, silently and plausibly.
|
|
72
|
+
``Portfolio.price`` raises when ``distortions`` is unset, which is the guard
|
|
73
|
+
that would otherwise have hidden the problem by making it look like a bug in
|
|
74
|
+
the first caller rather than in the second. ``tests/test_sessions.py`` pins the
|
|
75
|
+
residue directly: two sessions calibrating one shared object at different
|
|
76
|
+
targets get the answers their own forms imply.
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
from __future__ import annotations
|
|
80
|
+
|
|
81
|
+
from typing import Any
|
|
82
|
+
|
|
83
|
+
from .capability import can_natural_allocation
|
|
84
|
+
from .library_notes import library_warnings
|
|
85
|
+
|
|
86
|
+
#: Both perspectives travel in every response. The frames are tiny, and bundling
|
|
87
|
+
#: is what lets the app's RAW / INSURER toggle flip with no recompute and no
|
|
88
|
+
#: server-side result cache: a ``CalibrationResult`` is not in the object cache,
|
|
89
|
+
#: so a second request would have to calibrate again to answer the other reading.
|
|
90
|
+
_PERSPECTIVES = ("raw", "insurer")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _one_target(coc: float | None, lr: float | None,
|
|
94
|
+
premium: float | None = None) -> dict:
|
|
95
|
+
"""Exactly one pricing target, in the caller's own vocabulary.
|
|
96
|
+
|
|
97
|
+
Three members since 1.0.0a100. ``price_pentagon`` has always accepted a
|
|
98
|
+
premium target (``P=``) beside ``ROE`` and ``LR``, so the app's three forms
|
|
99
|
+
are one form over that signature and this is where the third spelling
|
|
100
|
+
enters. :func:`_pentagon_target` translates to the pentagon's vocabulary;
|
|
101
|
+
``calibrate_distortions`` speaks the caller's, which is why the two
|
|
102
|
+
spellings stay separate rather than one being translated at the call site.
|
|
103
|
+
"""
|
|
104
|
+
named = [name for name, value in
|
|
105
|
+
(("coc", coc), ("lr", lr), ("premium", premium))
|
|
106
|
+
if value is not None]
|
|
107
|
+
if len(named) != 1:
|
|
108
|
+
raise ValueError(
|
|
109
|
+
"pass exactly one of coc (CoC/ROE), lr (loss ratio) "
|
|
110
|
+
"or premium (P)")
|
|
111
|
+
return {named[0]: {"coc": coc, "lr": lr, "premium": premium}[named[0]]}
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
#: The caller's spelling of a pricing target, to the pentagon's own. The
|
|
115
|
+
#: pentagon's keywords are the canonical stat names, which is why they are
|
|
116
|
+
#: capitalized and the caller's are not.
|
|
117
|
+
_PENTAGON_TARGET = {"coc": "ROE", "lr": "LR", "premium": "P"}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _pentagon_target(target: dict) -> dict:
|
|
121
|
+
"""One caller-spelled target as the keyword ``price_pentagon`` takes."""
|
|
122
|
+
(name, value), = target.items()
|
|
123
|
+
return {_PENTAGON_TARGET[name]: value}
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _one_anchor(p: float | None, a: float | None) -> dict:
|
|
127
|
+
"""Exactly one capital anchor, as the keyword the library takes."""
|
|
128
|
+
if (p is None) == (a is None):
|
|
129
|
+
raise ValueError("pass exactly one of p (VaR probability) or a (assets)")
|
|
130
|
+
return {"p": p} if p is not None else {"a": a}
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _technical_target(target: dict, expense_ratio: float | None) -> dict:
|
|
134
|
+
"""A premium target read as gross, scaled to the technical premium.
|
|
135
|
+
|
|
136
|
+
Parameters
|
|
137
|
+
----------
|
|
138
|
+
target : dict
|
|
139
|
+
The single validated target, from :func:`_one_target`.
|
|
140
|
+
expense_ratio : float or None
|
|
141
|
+
Gross expense as a fraction of premium, in ``[0, 1)``, or None.
|
|
142
|
+
|
|
143
|
+
Returns
|
|
144
|
+
-------
|
|
145
|
+
dict
|
|
146
|
+
The target the engine should see: ``premium * (1 - e)`` for a premium
|
|
147
|
+
target with a non-zero ratio, otherwise the target unchanged. A CoC or
|
|
148
|
+
LR target never moves: the ratio only changes how the resolved premium
|
|
149
|
+
is *reported*, which is the caller's job on the way out.
|
|
150
|
+
"""
|
|
151
|
+
if expense_ratio and "premium" in target:
|
|
152
|
+
return {"premium": target["premium"] * (1.0 - expense_ratio)}
|
|
153
|
+
return target
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _envelopes(result: Any, name: str) -> dict[str, dict]:
|
|
157
|
+
"""One exhibit on one result object, under both perspectives.
|
|
158
|
+
|
|
159
|
+
Parameters
|
|
160
|
+
----------
|
|
161
|
+
result : CalibrationResult | EvaluationResult
|
|
162
|
+
What the pricing call returned. The exhibit registry dispatches on its
|
|
163
|
+
type, which is why these methods return objects rather than frames.
|
|
164
|
+
name : str
|
|
165
|
+
Registry name, e.g. ``'pricing.allocate'``. The dot is part of the name
|
|
166
|
+
and not a namespace this repo has to take apart.
|
|
167
|
+
|
|
168
|
+
Returns
|
|
169
|
+
-------
|
|
170
|
+
dict
|
|
171
|
+
Perspective to envelope payload, the same shape
|
|
172
|
+
``GET /objects/{id}/exhibit/{name}`` serves. That route hands back
|
|
173
|
+
deterministic bytes because its ETag hashes them; these ride inside a
|
|
174
|
+
response model, so the parsed payload is the useful form.
|
|
175
|
+
"""
|
|
176
|
+
from aggregate import exhibits as agg_exhibits
|
|
177
|
+
|
|
178
|
+
from .tables import MAX_ROWS
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
perspective: agg_exhibits.build_exhibit(
|
|
182
|
+
result, name, perspective, max_rows=MAX_ROWS).to_payload()
|
|
183
|
+
for perspective in _PERSPECTIVES
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _kind_of(obj: Any) -> str:
|
|
188
|
+
"""The wire name for what was priced."""
|
|
189
|
+
name = type(obj).__name__
|
|
190
|
+
return {"Portfolio": "port", "PnL": "pnl"}.get(name, "agg")
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _coc_for_premium(obj: Any, anchor: dict, premium: float,
|
|
194
|
+
basis: str | None) -> float:
|
|
195
|
+
"""The cost of capital a premium implies, at one capital anchor.
|
|
196
|
+
|
|
197
|
+
Parameters
|
|
198
|
+
----------
|
|
199
|
+
obj : Aggregate | Portfolio
|
|
200
|
+
The live object.
|
|
201
|
+
anchor : dict
|
|
202
|
+
The single capital anchor, already validated, as ``price_pentagon``
|
|
203
|
+
takes it.
|
|
204
|
+
premium : float
|
|
205
|
+
The premium target.
|
|
206
|
+
basis : str or None
|
|
207
|
+
The reinsurance view, so both legs resolve on the distribution the
|
|
208
|
+
calibration is about to be struck on.
|
|
209
|
+
|
|
210
|
+
Returns
|
|
211
|
+
-------
|
|
212
|
+
float
|
|
213
|
+
|
|
214
|
+
Notes
|
|
215
|
+
-----
|
|
216
|
+
The bridge between the two vocabularies. ``price_pentagon`` accepts a
|
|
217
|
+
premium target and ``calibrate_distortions`` does not, so a premium becomes
|
|
218
|
+
the cost of capital it implies before the fit. The pentagon identity is
|
|
219
|
+
never written here: the library solves ``{L, a, P}`` and reports ``ROE``,
|
|
220
|
+
and this reads it off. Used by :func:`run_calibration` and by
|
|
221
|
+
``bounds._calibrate_for_envelope``, which asked the same question with its
|
|
222
|
+
own arithmetic through 1.0.0a100.
|
|
223
|
+
|
|
224
|
+
A refusal on the way (the unbounded anchor guard, a premium above the
|
|
225
|
+
assets) is the library's own ``ValueError`` and travels to the caller
|
|
226
|
+
unchanged, which is what puts its sentence in the preview line.
|
|
227
|
+
"""
|
|
228
|
+
row = obj.price_pentagon(**anchor, P=premium, reins_view=basis).iloc[0]
|
|
229
|
+
return _scalar(row["ROE"])
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def run_pricing_preview(
|
|
233
|
+
obj: Any,
|
|
234
|
+
*,
|
|
235
|
+
p: float | None = None,
|
|
236
|
+
a: float | None = None,
|
|
237
|
+
coc: float | None = None,
|
|
238
|
+
lr: float | None = None,
|
|
239
|
+
premium: float | None = None,
|
|
240
|
+
basis: str | None = None,
|
|
241
|
+
expense_ratio: float | None = None,
|
|
242
|
+
) -> dict:
|
|
243
|
+
"""Complete the pentagon and report it as scalars, with no calibration.
|
|
244
|
+
|
|
245
|
+
Parameters
|
|
246
|
+
----------
|
|
247
|
+
obj : Aggregate | Portfolio
|
|
248
|
+
The live object.
|
|
249
|
+
p, a : float, optional
|
|
250
|
+
Exactly one capital anchor: a VaR probability, or an asset level the
|
|
251
|
+
library snaps to its grid.
|
|
252
|
+
coc, lr, premium : float, optional
|
|
253
|
+
Exactly one pricing target.
|
|
254
|
+
basis : str, optional
|
|
255
|
+
Which reinsurance view to answer on, passed through as ``reins_view``.
|
|
256
|
+
Both legs of the anchor come off the named view, so the reading is the
|
|
257
|
+
one a reader who chose that calibration basis is about to get.
|
|
258
|
+
expense_ratio : float, optional
|
|
259
|
+
Gross expense as a fraction of premium, in ``[0, 1)``. A premium
|
|
260
|
+
target is then read as gross and the pentagon is fed
|
|
261
|
+
``premium * (1 - e)``; whatever the target, the response reports
|
|
262
|
+
``gross_premium``, the resolved technical premium grossed back up.
|
|
263
|
+
Refused for a P&L, whose ledger states its own expenses.
|
|
264
|
+
|
|
265
|
+
Returns
|
|
266
|
+
-------
|
|
267
|
+
dict
|
|
268
|
+
Matches :class:`aggregate_api.models.PricingPreviewResponse`: the
|
|
269
|
+
pentagon octet under wire names, plus the probability the caller named.
|
|
270
|
+
``gross_premium`` rides when a ratio was sent. On a P&L it is the
|
|
271
|
+
ledger's own stated gross, off ``economic_ratios_df``'s gross (first)
|
|
272
|
+
block, and ``net_premium`` and ``net_of_expense_premium`` are ``P`` and
|
|
273
|
+
``P - E`` off the **closing** (last) block, which is the position the
|
|
274
|
+
whole program has worked.
|
|
275
|
+
|
|
276
|
+
Notes
|
|
277
|
+
-----
|
|
278
|
+
This feeds the Calibrate form's live preview line, so it is deliberately the
|
|
279
|
+
cheapest question in the group: one completion of five numbers and three
|
|
280
|
+
ratios, no distortion fitting, no allocation. It is also where the library's
|
|
281
|
+
unbounded anchor guard reaches the reader as a sentence, since a preview that
|
|
282
|
+
refuses says why in the place they are already looking rather than after they
|
|
283
|
+
press the button.
|
|
284
|
+
|
|
285
|
+
``p`` is echoed, not resolved. A caller who anchored on assets gets null, and
|
|
286
|
+
the asset level is the answer they asked for; resolving the probability back
|
|
287
|
+
would mean reaching for a grid distribution on a named view, which is a
|
|
288
|
+
private surface and buys a number the line does not print.
|
|
289
|
+
|
|
290
|
+
A P&L resolves to its wrapped engine, mirroring the Bounds group's own
|
|
291
|
+
see-through (aggregate 1.0.0a375, [Bounds-PnL-Engine]): the Bounds form
|
|
292
|
+
resolves its premium and asset pair through this preview before it sweeps,
|
|
293
|
+
and the sweep answers on the engine's loss distribution, so the pentagon
|
|
294
|
+
has to be the engine's too. Preview only: ``run_calibration`` and the
|
|
295
|
+
other runners still refuse a P&L, whose ledger states a premium on every
|
|
296
|
+
row and has no single pentagon of its own to calibrate.
|
|
297
|
+
"""
|
|
298
|
+
target = _one_target(coc, lr, premium)
|
|
299
|
+
anchor = _one_anchor(p, a)
|
|
300
|
+
is_pnl = _kind_of(obj) == "pnl"
|
|
301
|
+
if is_pnl and expense_ratio is not None:
|
|
302
|
+
raise ValueError("a P&L states its own expenses in the ledger; "
|
|
303
|
+
"drop the expense ratio")
|
|
304
|
+
target = _technical_target(target, expense_ratio)
|
|
305
|
+
source = obj
|
|
306
|
+
if not hasattr(obj, "price_pentagon"):
|
|
307
|
+
engine = getattr(obj, "engine", None)
|
|
308
|
+
if hasattr(engine, "price_pentagon"):
|
|
309
|
+
obj = engine
|
|
310
|
+
else:
|
|
311
|
+
raise ValueError("pricing requires an Aggregate or a Portfolio")
|
|
312
|
+
|
|
313
|
+
row = obj.price_pentagon(**anchor, **_pentagon_target(target),
|
|
314
|
+
reins_view=basis).iloc[0]
|
|
315
|
+
out = {
|
|
316
|
+
"p": p,
|
|
317
|
+
"assets": _scalar(row["a"]),
|
|
318
|
+
"loss": _scalar(row["L"]),
|
|
319
|
+
"margin": _scalar(row["M"]),
|
|
320
|
+
"premium": _scalar(row["P"]),
|
|
321
|
+
"capital": _scalar(row["Q"]),
|
|
322
|
+
"lr": _scalar(row["LR"]),
|
|
323
|
+
"pq": _scalar(row["PQ"]),
|
|
324
|
+
"coc": _scalar(row["ROE"]),
|
|
325
|
+
}
|
|
326
|
+
# `premium` stays technical, as documented; the gross reading is a second
|
|
327
|
+
# field rather than a relabeling. For a premium target the gross is the
|
|
328
|
+
# number the caller typed, recovered exactly by the flat-ratio identity.
|
|
329
|
+
if expense_ratio is not None and out["premium"] is not None:
|
|
330
|
+
out["gross_premium"] = out["premium"] / (1.0 - expense_ratio)
|
|
331
|
+
if is_pnl:
|
|
332
|
+
# The ledger's own figures, not an assumed ratio. `economic_ratios_df`'s
|
|
333
|
+
# amounts satisfy M == P - L - E on every block, so a block's `P - E` is
|
|
334
|
+
# that block's premium net of its expenses.
|
|
335
|
+
#
|
|
336
|
+
# **Two blocks, not one.** The gross (first) block is the book before
|
|
337
|
+
# any cession and is what `gross_premium` has always meant. The pair the
|
|
338
|
+
# form needs is the closing (last) block: the position once every tier's
|
|
339
|
+
# program has worked, which is what a reader looking at a peeled P&L is
|
|
340
|
+
# pricing. Through a198 this read the gross block for both, so the
|
|
341
|
+
# Bounds form on `Capstone.PnL` opened at 21,000 where the net position
|
|
342
|
+
# is 4,881, and then swept a range the book does not occupy. On a
|
|
343
|
+
# single-group ledger the closing block *is* the gross block and all
|
|
344
|
+
# three figures agree, which is why the error was invisible until a
|
|
345
|
+
# program with a cession reached the form.
|
|
346
|
+
blocks = source.economic_ratios_df
|
|
347
|
+
gross = _scalar(blocks.iloc[0]["P"])
|
|
348
|
+
closing = blocks.iloc[-1]
|
|
349
|
+
net = _scalar(closing["P"])
|
|
350
|
+
expense = _scalar(closing["E"])
|
|
351
|
+
out["gross_premium"] = gross
|
|
352
|
+
out["net_premium"] = net
|
|
353
|
+
if net is not None and expense is not None:
|
|
354
|
+
out["net_of_expense_premium"] = net - expense
|
|
355
|
+
return out
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def run_calibration(
|
|
359
|
+
obj: Any,
|
|
360
|
+
*,
|
|
361
|
+
p: float | None = None,
|
|
362
|
+
a: float | None = None,
|
|
363
|
+
coc: float | None = None,
|
|
364
|
+
lr: float | None = None,
|
|
365
|
+
premium: float | None = None,
|
|
366
|
+
basis: str | None = None,
|
|
367
|
+
expense_ratio: float | None = None,
|
|
368
|
+
) -> dict:
|
|
369
|
+
"""Fit the standard distortion set, and serve what it says.
|
|
370
|
+
|
|
371
|
+
Parameters
|
|
372
|
+
----------
|
|
373
|
+
obj : Aggregate | Portfolio
|
|
374
|
+
The live object.
|
|
375
|
+
p, a : float, optional
|
|
376
|
+
Exactly one capital anchor.
|
|
377
|
+
coc, lr, premium : float, optional
|
|
378
|
+
Exactly one pricing target. The library owns the loss-ratio conversion
|
|
379
|
+
since 1.0.0a262, including its refusal: a loss ratio can imply a premium
|
|
380
|
+
above the assets, which is a position with negative capital, and it says
|
|
381
|
+
so rather than reporting a receipt of garbage.
|
|
382
|
+
basis : str, optional
|
|
383
|
+
Calibration basis for a reinsured Aggregate, passed as ``reins_view``.
|
|
384
|
+
expense_ratio : float, optional
|
|
385
|
+
Gross expense as a fraction of premium, in ``[0, 1)``. A premium
|
|
386
|
+
target is read as gross and the fit runs on ``premium * (1 - e)``;
|
|
387
|
+
with a CoC or LR target the ratio changes nothing here, only the
|
|
388
|
+
preview's gross reading. See the module docstring.
|
|
389
|
+
|
|
390
|
+
Returns
|
|
391
|
+
-------
|
|
392
|
+
dict
|
|
393
|
+
Matches :class:`aggregate_api.models.PricingExhibitsResponse`: the
|
|
394
|
+
``pricing.calibrate`` and ``pricing.stand_alone`` envelopes, each under
|
|
395
|
+
both perspectives, and any warnings the library raised on the way.
|
|
396
|
+
|
|
397
|
+
Notes
|
|
398
|
+
-----
|
|
399
|
+
Two exhibits from one call because the pane draws two subtabs from one press.
|
|
400
|
+
``pricing.stand_alone``'s frames are materialized lazily off the result, so
|
|
401
|
+
the parts are priced here, inside the warning capture, rather than on the
|
|
402
|
+
reader's next click.
|
|
403
|
+
|
|
404
|
+
**The second exhibit is the parts priced alone, not the whole split across
|
|
405
|
+
them.** Through 1.0.0a102 this bundle carried ``pricing.allocate``, and for a
|
|
406
|
+
book that was the ``analyze_distortions`` sweep, which is the more expensive
|
|
407
|
+
of the two questions and the one a Calibrate press does not ask. The split
|
|
408
|
+
moved that sweep behind its own press, :func:`run_natural_allocation`, and
|
|
409
|
+
put the stand-alone reading here: per-part ``Distortion.price`` calls on
|
|
410
|
+
families that are already fitted.
|
|
411
|
+
|
|
412
|
+
A distortion the library declines to price (the mass distortion on an
|
|
413
|
+
unbounded book) is a warning, not a failure: the exhibit carries the rows
|
|
414
|
+
that did answer and the warning says which did not. That is why the capture
|
|
415
|
+
wraps the envelope building and not just the calibration.
|
|
416
|
+
|
|
417
|
+
**A premium target costs one extra library call.**
|
|
418
|
+
``calibrate_distortions`` takes a cost of capital or a loss ratio and not a
|
|
419
|
+
premium, so :func:`_coc_for_premium` completes the pentagon first and hands
|
|
420
|
+
over the cost of capital it reports. Two library calls chained, with no
|
|
421
|
+
arithmetic here: the pentagon identity stays upstream where it belongs. The
|
|
422
|
+
tidier form is ``calibrate_distortions(P=...)``, which is an open ask.
|
|
423
|
+
"""
|
|
424
|
+
target = _one_target(coc, lr, premium)
|
|
425
|
+
anchor = _one_anchor(p, a)
|
|
426
|
+
if not hasattr(obj, "calibrate_distortions"):
|
|
427
|
+
raise ValueError("calibration requires an Aggregate or a Portfolio")
|
|
428
|
+
target = _technical_target(target, expense_ratio)
|
|
429
|
+
if "premium" in target:
|
|
430
|
+
target = {"coc": _coc_for_premium(obj, anchor, target["premium"], basis)}
|
|
431
|
+
|
|
432
|
+
warns: list[str] = []
|
|
433
|
+
with library_warnings() as caught:
|
|
434
|
+
result = obj.calibrate_distortions(**target, **anchor, reins_view=basis)
|
|
435
|
+
exhibits = {name: _envelopes(result, name)
|
|
436
|
+
for name in ("pricing.calibrate", "pricing.stand_alone")}
|
|
437
|
+
warns.extend(caught)
|
|
438
|
+
return {"kind": _kind_of(obj), "exhibits": exhibits, "warnings": warns}
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def _allocation_basis(obj: Any, basis: str | None) -> str | None:
|
|
442
|
+
"""The one reinsurance view an allocation can be struck on, or a refusal.
|
|
443
|
+
|
|
444
|
+
Parameters
|
|
445
|
+
----------
|
|
446
|
+
obj : Aggregate | Portfolio
|
|
447
|
+
The live object, already known to have parts to allocate across.
|
|
448
|
+
basis : str or None
|
|
449
|
+
What the form sent, which is ``None`` where the object offers no basis
|
|
450
|
+
row at all.
|
|
451
|
+
|
|
452
|
+
Returns
|
|
453
|
+
-------
|
|
454
|
+
str or None
|
|
455
|
+
The view to pass as ``reins_view``.
|
|
456
|
+
|
|
457
|
+
Notes
|
|
458
|
+
-----
|
|
459
|
+
Structural rather than a preference, in both directions. An occurrence
|
|
460
|
+
program's allocation splits the **gross** premium into its ceded and net
|
|
461
|
+
parts, so a fit struck on net has no gross premium to split and there is
|
|
462
|
+
nothing for the tab to say; the library's own availability predicate makes
|
|
463
|
+
the same test on the result. A book is locked to net by the 1.0.0a100 ruling
|
|
464
|
+
that governs the Calibrate row beside this one: reinsurance is placed at the
|
|
465
|
+
unit level, so a book has no cession of its own to choose.
|
|
466
|
+
|
|
467
|
+
An ``Aggregate`` that states nothing is read as gross rather than as the
|
|
468
|
+
library's default. That default is the object's own distribution, which for
|
|
469
|
+
a reinsured aggregate is the net view, and answering a request for the
|
|
470
|
+
allocation by striking the one calibration that cannot produce it would be a
|
|
471
|
+
refusal wearing a 500. The app always states a basis here; this is for
|
|
472
|
+
everyone else.
|
|
473
|
+
"""
|
|
474
|
+
if _kind_of(obj) == "port":
|
|
475
|
+
if basis not in (None, "net"):
|
|
476
|
+
raise ValueError(
|
|
477
|
+
"a book is allocated on its net view: reinsurance is placed at "
|
|
478
|
+
"the unit level, so there is no book-wide basis to choose. Pass "
|
|
479
|
+
"net, or drop the basis")
|
|
480
|
+
return basis
|
|
481
|
+
if basis is None:
|
|
482
|
+
return "gross"
|
|
483
|
+
if basis != "gross":
|
|
484
|
+
raise ValueError(
|
|
485
|
+
"the natural allocation splits a gross premium; calibrate on gross")
|
|
486
|
+
return basis
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def run_natural_allocation(
|
|
490
|
+
obj: Any,
|
|
491
|
+
*,
|
|
492
|
+
p: float | None = None,
|
|
493
|
+
a: float | None = None,
|
|
494
|
+
coc: float | None = None,
|
|
495
|
+
lr: float | None = None,
|
|
496
|
+
premium: float | None = None,
|
|
497
|
+
basis: str | None = None,
|
|
498
|
+
expense_ratio: float | None = None,
|
|
499
|
+
) -> dict:
|
|
500
|
+
"""Split one calibrated premium across the parts, and serve the exhibit.
|
|
501
|
+
|
|
502
|
+
Parameters
|
|
503
|
+
----------
|
|
504
|
+
obj : Aggregate | Portfolio
|
|
505
|
+
The live object. A book, whose parts are its units, or an aggregate
|
|
506
|
+
carrying an occurrence program, whose parts are the ceded and net halves
|
|
507
|
+
of it.
|
|
508
|
+
p, a : float, optional
|
|
509
|
+
Exactly one capital anchor, as :func:`run_calibration` takes it.
|
|
510
|
+
coc, lr, premium : float, optional
|
|
511
|
+
Exactly one pricing target, as :func:`run_calibration` takes it.
|
|
512
|
+
basis : str, optional
|
|
513
|
+
The calibration basis, narrowed by :func:`_allocation_basis`.
|
|
514
|
+
expense_ratio : float, optional
|
|
515
|
+
As :func:`run_calibration` takes it: a premium target is read as
|
|
516
|
+
gross and the fit runs on ``premium * (1 - e)``.
|
|
517
|
+
|
|
518
|
+
Returns
|
|
519
|
+
-------
|
|
520
|
+
dict
|
|
521
|
+
Matches :class:`aggregate_api.models.PricingExhibitsResponse`, carrying
|
|
522
|
+
the ``pricing.allocate`` envelope under both perspectives.
|
|
523
|
+
|
|
524
|
+
Notes
|
|
525
|
+
-----
|
|
526
|
+
**Not** ``bounds.run_allocation``, which sweeps every distortion consistent
|
|
527
|
+
with a premium and reports each unit's range. The names are close because
|
|
528
|
+
both split a total across units; this one is the pricing pane's single
|
|
529
|
+
answer under fitted families, that one is the Bounds group's interval. They
|
|
530
|
+
are imported into the same route module, so the longer name here is what
|
|
531
|
+
keeps the shorter one meaning what it always has.
|
|
532
|
+
|
|
533
|
+
**Stateless, like its siblings.** The calibration is struck again here rather
|
|
534
|
+
than read off the one a Calibrate press already made. Nothing about a
|
|
535
|
+
``CalibrationResult`` is cached server side, so holding one would mean a
|
|
536
|
+
result cache keyed on a form body, and a press that recomputes is the same
|
|
537
|
+
decision the Evaluate route made.
|
|
538
|
+
|
|
539
|
+
Two different questions arrive at one registry name, which is the point of
|
|
540
|
+
the name. For a book it is ``analyze_distortions``, the per-unit premium
|
|
541
|
+
allocation the library has always served. For an occurrence program it is
|
|
542
|
+
the natural allocation off the joint distribution of gross and ceded, where
|
|
543
|
+
each family's distorted view of the gross sets the weights and ceded plus
|
|
544
|
+
net foot to gross exactly. The app draws whichever it is served and holds no
|
|
545
|
+
opinion about which arrived.
|
|
546
|
+
|
|
547
|
+
The occurrence path is the expensive one on this pane: it builds a joint
|
|
548
|
+
distribution rather than reading a density frame. That is why it sits behind
|
|
549
|
+
its own press.
|
|
550
|
+
"""
|
|
551
|
+
target = _one_target(coc, lr, premium)
|
|
552
|
+
anchor = _one_anchor(p, a)
|
|
553
|
+
if not hasattr(obj, "calibrate_distortions"):
|
|
554
|
+
raise ValueError("allocation requires an Aggregate or a Portfolio")
|
|
555
|
+
if not can_natural_allocation(obj):
|
|
556
|
+
raise ValueError(
|
|
557
|
+
"there is nothing to allocate across: the natural allocation splits "
|
|
558
|
+
"one premium among parts, which means the units of a book or the "
|
|
559
|
+
"halves of an occurrence cession")
|
|
560
|
+
basis = _allocation_basis(obj, basis)
|
|
561
|
+
target = _technical_target(target, expense_ratio)
|
|
562
|
+
if "premium" in target:
|
|
563
|
+
target = {"coc": _coc_for_premium(obj, anchor, target["premium"], basis)}
|
|
564
|
+
|
|
565
|
+
warns: list[str] = []
|
|
566
|
+
with library_warnings() as caught:
|
|
567
|
+
result = obj.calibrate_distortions(**target, **anchor, reins_view=basis)
|
|
568
|
+
exhibits = {"pricing.allocate": _envelopes(result, "pricing.allocate")}
|
|
569
|
+
warns.extend(caught)
|
|
570
|
+
return {"kind": _kind_of(obj), "exhibits": exhibits, "warnings": warns}
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
def run_evaluation(
|
|
574
|
+
obj: Any,
|
|
575
|
+
*,
|
|
576
|
+
premium: float | None = None,
|
|
577
|
+
basis: str | None = None,
|
|
578
|
+
p: float | None = None,
|
|
579
|
+
a: float | None = None,
|
|
580
|
+
expense_ratio: float | None = None,
|
|
581
|
+
) -> dict:
|
|
582
|
+
"""The breakeven acceptability panel, as the library's own exhibit.
|
|
583
|
+
|
|
584
|
+
Parameters
|
|
585
|
+
----------
|
|
586
|
+
obj : Aggregate | Portfolio | PnL
|
|
587
|
+
The live object; all three expose ``evaluate``.
|
|
588
|
+
premium : float, optional
|
|
589
|
+
The consideration to measure against. Only an ``Aggregate`` or a
|
|
590
|
+
``Portfolio`` takes one, and only when its own exposure states none.
|
|
591
|
+
basis : str, optional
|
|
592
|
+
Which premium is being input, for a reinsured ``Aggregate``: the gross
|
|
593
|
+
one, the one net of the occurrence program, or the net. Passed as
|
|
594
|
+
``reins_view``.
|
|
595
|
+
p, a : float, optional
|
|
596
|
+
At most one asset anchor. The acceptability solve is a function of it
|
|
597
|
+
since 1.0.0a261, so evaluating at the anchor a calibration was struck at
|
|
598
|
+
closes the round trip and recovers that calibration's parameters. Omit
|
|
599
|
+
both for the unlimited reading, which is the library's default and
|
|
600
|
+
reports four families rather than five: ``ccoc`` needs an asset level.
|
|
601
|
+
expense_ratio : float, optional
|
|
602
|
+
Gross expense as a fraction of premium, in ``[0, 1)``. A typed
|
|
603
|
+
premium is read as gross and evaluated at ``premium * (1 - e)``;
|
|
604
|
+
with no typed premium the ratio has nothing to scale and is ignored.
|
|
605
|
+
Refused for a P&L, whose ledger states its own expenses.
|
|
606
|
+
|
|
607
|
+
Returns
|
|
608
|
+
-------
|
|
609
|
+
dict
|
|
610
|
+
Matches :class:`aggregate_api.models.PricingExhibitsResponse`, carrying
|
|
611
|
+
the ``pricing.evaluate`` envelope under both perspectives.
|
|
612
|
+
|
|
613
|
+
Notes
|
|
614
|
+
-----
|
|
615
|
+
``DegenerateEvaluationWarning`` is not an error and is not swallowed. It
|
|
616
|
+
fires when no breakeven level exists, either because the premium does not
|
|
617
|
+
cover the expected loss or because the position cannot lose. Both report
|
|
618
|
+
``NaN`` in the panel, so the warning is what tells the reader which of the
|
|
619
|
+
two they are looking at.
|
|
620
|
+
"""
|
|
621
|
+
if not hasattr(obj, "evaluate"):
|
|
622
|
+
raise ValueError(
|
|
623
|
+
"evaluation requires an Aggregate, a Portfolio or a P&L")
|
|
624
|
+
|
|
625
|
+
is_pnl = type(obj).__name__ == "PnL"
|
|
626
|
+
if is_pnl:
|
|
627
|
+
# Every ledger row is its own position with its own consideration, so
|
|
628
|
+
# there is no single premium to state and no single asset level to
|
|
629
|
+
# anchor on. The anchor question is genuinely open upstream rather than
|
|
630
|
+
# merely unimplemented; see the plan's decision 4.
|
|
631
|
+
if premium is not None:
|
|
632
|
+
raise ValueError(
|
|
633
|
+
"a P&L carries its own premium in the ledger; drop the premium "
|
|
634
|
+
"argument and evaluate it as it stands")
|
|
635
|
+
if expense_ratio is not None:
|
|
636
|
+
raise ValueError(
|
|
637
|
+
"a P&L states its own expenses in the ledger; drop the "
|
|
638
|
+
"expense ratio")
|
|
639
|
+
if basis is not None or p is not None or a is not None:
|
|
640
|
+
raise ValueError(
|
|
641
|
+
"a P&L evaluates every row of its ledger on that row's own "
|
|
642
|
+
"terms; drop the basis and the asset anchor")
|
|
643
|
+
|
|
644
|
+
# The typed premium is read as gross when a ratio rides with it; the
|
|
645
|
+
# library sees the technical number, matching the calibrate runners.
|
|
646
|
+
if premium is not None and expense_ratio:
|
|
647
|
+
premium = premium * (1.0 - expense_ratio)
|
|
648
|
+
|
|
649
|
+
warns: list[str] = []
|
|
650
|
+
with library_warnings() as caught:
|
|
651
|
+
result = (obj.evaluate() if is_pnl
|
|
652
|
+
else obj.evaluate(premium, p=p, a=a, reins_view=basis))
|
|
653
|
+
exhibits = {"pricing.evaluate": _envelopes(result, "pricing.evaluate")}
|
|
654
|
+
warns.extend(caught)
|
|
655
|
+
return {"kind": _kind_of(obj), "exhibits": exhibits, "warnings": warns}
|
|
656
|
+
|
|
657
|
+
|
|
658
|
+
def run_ruin(
|
|
659
|
+
obj: Any,
|
|
660
|
+
*,
|
|
661
|
+
p: float | None = None,
|
|
662
|
+
a: float | None = None,
|
|
663
|
+
coc: float | None = None,
|
|
664
|
+
lr: float | None = None,
|
|
665
|
+
premium: float | None = None,
|
|
666
|
+
ruin_p: float | None = None,
|
|
667
|
+
u: float | None = None,
|
|
668
|
+
seed: int | None = None,
|
|
669
|
+
sample: bool = False,
|
|
670
|
+
n_plot: int | None = None,
|
|
671
|
+
detail: int | None = None,
|
|
672
|
+
) -> dict:
|
|
673
|
+
"""The eventual-ruin reading: the two-panel chart and its stats strip.
|
|
674
|
+
|
|
675
|
+
Parameters
|
|
676
|
+
----------
|
|
677
|
+
obj : Aggregate
|
|
678
|
+
The live object. Must serve the ``ruin`` chart, which the library's
|
|
679
|
+
own predicate limits to an updated aggregate with a Poisson or
|
|
680
|
+
renewal frequency.
|
|
681
|
+
p, a : float, optional
|
|
682
|
+
Exactly one capital anchor, the Calibrate form's.
|
|
683
|
+
coc, lr, premium : float, optional
|
|
684
|
+
Exactly one pricing target. The ruin engine takes a loss ratio, so
|
|
685
|
+
``lr`` passes through and the other two are completed to one through
|
|
686
|
+
:meth:`price_pentagon`, the same bridge :func:`run_calibration` uses
|
|
687
|
+
for a premium target: the pentagon identity stays upstream.
|
|
688
|
+
ruin_p, u : float, optional
|
|
689
|
+
At most one capital level: a probability of eventual default the
|
|
690
|
+
library resolves through the ruin function's capital lookup, or the
|
|
691
|
+
initial surplus directly. With neither, the library's teaching
|
|
692
|
+
default applies.
|
|
693
|
+
seed : int, optional
|
|
694
|
+
rng seed for the simulated paths; omitted, the library's fixed
|
|
695
|
+
teaching seed keeps the document deterministic.
|
|
696
|
+
sample : bool
|
|
697
|
+
The Sample action. Draws one fresh integer seed here so the chart
|
|
698
|
+
and the exhibit describe the same draw; the chart's ``meta.seed``
|
|
699
|
+
reports it.
|
|
700
|
+
n_plot, detail : int, optional
|
|
701
|
+
Paths drawn, and the per-path display budget, passed only when set.
|
|
702
|
+
|
|
703
|
+
Returns
|
|
704
|
+
-------
|
|
705
|
+
dict
|
|
706
|
+
Matches :class:`aggregate_api.models.RuinResponse`: the ``ruin``
|
|
707
|
+
chart document parsed from its canonical JSON, and the ``ruin``
|
|
708
|
+
exhibit envelopes under both perspectives, built on the
|
|
709
|
+
:class:`~aggregate.results.RuinResult` the same inputs produce.
|
|
710
|
+
|
|
711
|
+
Notes
|
|
712
|
+
-----
|
|
713
|
+
One POST answers with both halves because both move together under the
|
|
714
|
+
debounced form, and because neither can travel the generic GETs: the
|
|
715
|
+
chart needs options those routes do not carry, and the exhibit registers
|
|
716
|
+
on the result object rather than on the cached ``obj``. The exhibit and
|
|
717
|
+
the chart run the same capped simulation twice, deliberately: sharing
|
|
718
|
+
one seed makes the strip describe the drawn paths, and lifting the
|
|
719
|
+
simulation out to share the arrays would be this service reaching past
|
|
720
|
+
the library's public surface.
|
|
721
|
+
"""
|
|
722
|
+
from aggregate import charts as agg_charts
|
|
723
|
+
|
|
724
|
+
target = _one_target(coc, lr, premium)
|
|
725
|
+
if "lr" not in target:
|
|
726
|
+
anchor = _one_anchor(p, a)
|
|
727
|
+
if not hasattr(obj, "price_pentagon"):
|
|
728
|
+
raise ValueError("the ruin reading requires an Aggregate")
|
|
729
|
+
row = obj.price_pentagon(**anchor,
|
|
730
|
+
**_pentagon_target(target)).iloc[0]
|
|
731
|
+
target = {"lr": _scalar(row["LR"])}
|
|
732
|
+
if "ruin" not in agg_charts.available_charts(obj):
|
|
733
|
+
raise ValueError(
|
|
734
|
+
"no ruin reading for this object: it needs an updated aggregate "
|
|
735
|
+
"with a Poisson or renewal (wait) frequency")
|
|
736
|
+
if ruin_p is not None and u is not None:
|
|
737
|
+
raise ValueError("pass at most one of ruin_p (a default probability) "
|
|
738
|
+
"or u (an initial surplus)")
|
|
739
|
+
if sample:
|
|
740
|
+
import numpy as np
|
|
741
|
+
seed = int(np.random.default_rng().integers(1, 2 ** 31 - 1))
|
|
742
|
+
|
|
743
|
+
options: dict = {"lr": target["lr"]}
|
|
744
|
+
if ruin_p is not None:
|
|
745
|
+
options["p"] = ruin_p
|
|
746
|
+
if u is not None:
|
|
747
|
+
options["u"] = u
|
|
748
|
+
if seed is not None:
|
|
749
|
+
options["seed"] = seed
|
|
750
|
+
warns: list[str] = []
|
|
751
|
+
with library_warnings() as caught:
|
|
752
|
+
chart_options = dict(options)
|
|
753
|
+
if n_plot is not None:
|
|
754
|
+
chart_options["n_plot"] = n_plot
|
|
755
|
+
if detail is not None:
|
|
756
|
+
chart_options["detail"] = detail
|
|
757
|
+
doc = agg_charts.build_chart_doc(obj, "ruin", **chart_options)
|
|
758
|
+
result = obj.eventual_ruin(**options)
|
|
759
|
+
exhibits = {"ruin": _envelopes(result, "ruin")}
|
|
760
|
+
warns.extend(caught)
|
|
761
|
+
import json
|
|
762
|
+
chart = json.loads(agg_charts.canonical_json(doc))
|
|
763
|
+
return {"kind": _kind_of(obj), "chart": chart, "exhibits": exhibits,
|
|
764
|
+
"warnings": warns}
|
|
765
|
+
|
|
766
|
+
|
|
767
|
+
def _scalar(value):
|
|
768
|
+
"""Coerce numpy scalars and non-finite floats for JSON."""
|
|
769
|
+
import math
|
|
770
|
+
import numpy as np
|
|
771
|
+
|
|
772
|
+
if value is None:
|
|
773
|
+
return None
|
|
774
|
+
if isinstance(value, (np.integer, np.floating)):
|
|
775
|
+
value = value.item()
|
|
776
|
+
if isinstance(value, float) and not math.isfinite(value):
|
|
777
|
+
return None
|
|
778
|
+
return value
|