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
|
@@ -0,0 +1,823 @@
|
|
|
1
|
+
"""What an object can answer, computed rather than declared.
|
|
2
|
+
|
|
3
|
+
The app used to carry two hand-written tables saying what each kind cannot do
|
|
4
|
+
(``NA_TABS_BY_KIND`` and ``NA_MORE_BY_KIND`` in ``web/src/main.js``). The
|
|
5
|
+
library now knows the same thing properly, through
|
|
6
|
+
:func:`aggregate.exhibits.available_exhibits` and
|
|
7
|
+
:func:`aggregate.charts.available_charts`, and
|
|
8
|
+
``aggregate/dev/exhibits-and-charts.md`` states the invariant: capability is
|
|
9
|
+
derived, never declared twice. This module is the api's side of that, and the
|
|
10
|
+
build response carries its output so the navigation can paint immediately
|
|
11
|
+
rather than after a second round trip.
|
|
12
|
+
|
|
13
|
+
Three kinds of leaf, and the distinction decides where a future one goes. An
|
|
14
|
+
**exhibit leaf** lights from :func:`exhibits_for` and needs no app change to
|
|
15
|
+
appear when the library registers a new exhibit. A **chart leaf** lights from
|
|
16
|
+
:func:`charts_for`. An **app leaf** (the Narrative pane, the pricing forms) is
|
|
17
|
+
gated on a flag, because it is a piece of app behavior rather than a library
|
|
18
|
+
document.
|
|
19
|
+
|
|
20
|
+
Notes
|
|
21
|
+
-----
|
|
22
|
+
**A flag earns its place or it is not here.** Anything the exhibit or chart
|
|
23
|
+
list already says must not be repeated as a flag: that is the second
|
|
24
|
+
declaration this module exists to delete. ``is_tower`` was dropped from the
|
|
25
|
+
plan's draft list for exactly that reason, since ``economic_waterfall`` is
|
|
26
|
+
registered against a tower predicate and a single group P&L therefore already
|
|
27
|
+
drops it from :func:`exhibits_for`. Each flag below names the consumer that
|
|
28
|
+
cannot get its answer any other way.
|
|
29
|
+
|
|
30
|
+
``kind`` and ``has_reins`` are not repeated here either. Both already ride on
|
|
31
|
+
the build response (``has_reins`` through ``_summary_fields``, which the cache
|
|
32
|
+
hit path shares), and one field per fact is the whole point.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
from __future__ import annotations
|
|
36
|
+
|
|
37
|
+
import inspect
|
|
38
|
+
from typing import Any
|
|
39
|
+
|
|
40
|
+
import numpy as np
|
|
41
|
+
from aggregate import Aggregate, PnL, Portfolio
|
|
42
|
+
from aggregate import charts as agg_charts
|
|
43
|
+
from aggregate import exhibits as agg_exhibits
|
|
44
|
+
|
|
45
|
+
#: Namespace prefix the library stamps on the sharpen verdict it merges into a
|
|
46
|
+
#: program's ``note{}`` (``aggregate._program._SHARPEN_NOTE``). The note body is
|
|
47
|
+
#: ``;``-separated and the author's own prose never carries the prefix, which is
|
|
48
|
+
#: what makes this a safe test rather than a substring guess.
|
|
49
|
+
SHARPEN_NOTE_PREFIX = "sharpen: "
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def exhibits_for(obj: Any) -> list[dict]:
|
|
53
|
+
"""The exhibits this object can serve, with titles and perspectives.
|
|
54
|
+
|
|
55
|
+
A passthrough of :func:`aggregate.exhibits.available_exhibits`, shaped the
|
|
56
|
+
same way ``GET /v1/objects/{id}/exhibits`` shapes it so the inline payload
|
|
57
|
+
and the standalone route cannot drift.
|
|
58
|
+
|
|
59
|
+
Parameters
|
|
60
|
+
----------
|
|
61
|
+
obj : Any
|
|
62
|
+
A built object; an unregistered type yields an empty list.
|
|
63
|
+
|
|
64
|
+
Returns
|
|
65
|
+
-------
|
|
66
|
+
list of dict
|
|
67
|
+
``{"name", "title", "perspectives"}`` in registry order.
|
|
68
|
+
"""
|
|
69
|
+
return [
|
|
70
|
+
{
|
|
71
|
+
"name": name,
|
|
72
|
+
"title": agg_exhibits.EXHIBITS[name][0].title,
|
|
73
|
+
"perspectives": [p.value for p in perspectives],
|
|
74
|
+
}
|
|
75
|
+
for name, perspectives in agg_exhibits.available_exhibits(obj)
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def charts_for(obj: Any) -> list[str]:
|
|
80
|
+
"""The chart names this object can serve.
|
|
81
|
+
|
|
82
|
+
A passthrough of :func:`aggregate.charts.available_charts`. Complete as of
|
|
83
|
+
library a244: every first-class kind publishes its own chart, and the app
|
|
84
|
+
draws nothing it does not find here.
|
|
85
|
+
|
|
86
|
+
Parameters
|
|
87
|
+
----------
|
|
88
|
+
obj : Any
|
|
89
|
+
|
|
90
|
+
Returns
|
|
91
|
+
-------
|
|
92
|
+
list of str
|
|
93
|
+
Registry names, in registration order.
|
|
94
|
+
"""
|
|
95
|
+
return list(agg_charts.available_charts(obj))
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def primary_chart_for(obj: Any) -> str | None:
|
|
99
|
+
"""The chart that is this object's own picture, or None.
|
|
100
|
+
|
|
101
|
+
:func:`charts_for` answers what *can* be drawn, which for a reinsured
|
|
102
|
+
aggregate is two things. This answers which one to draw when nothing else
|
|
103
|
+
has been asked for, which is exactly the Overview Plot leaf's question and
|
|
104
|
+
the one it used to answer with a per-kind table in the browser. A
|
|
105
|
+
passthrough of :func:`aggregate.charts.primary_chart`, so the answer moves
|
|
106
|
+
when the library's registrations move and never when this file does.
|
|
107
|
+
|
|
108
|
+
Parameters
|
|
109
|
+
----------
|
|
110
|
+
obj : Any
|
|
111
|
+
|
|
112
|
+
Returns
|
|
113
|
+
-------
|
|
114
|
+
str or None
|
|
115
|
+
None where no registered chart claims the object, which is the app's
|
|
116
|
+
cue to say the picture does not exist yet rather than to approximate
|
|
117
|
+
one.
|
|
118
|
+
"""
|
|
119
|
+
return agg_charts.primary_chart(obj)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def has_premium(obj: Any) -> bool:
|
|
123
|
+
"""Does this object's exposure state a premium?
|
|
124
|
+
|
|
125
|
+
Consumer: the PnL button's form. :meth:`Aggregate.pnl_program` writes
|
|
126
|
+
``derive premium`` when there is one (the technical premium grossed up
|
|
127
|
+
for the expense clause; upstream since ``aggregate`` 1.0.0a270) and sizes
|
|
128
|
+
it from a loss ratio when there is not, so this decides whether the app
|
|
129
|
+
offers a loss-ratio input at all rather than showing one that will be
|
|
130
|
+
ignored.
|
|
131
|
+
|
|
132
|
+
Mirrors the library's own test in ``aggregate._program._pnl_consideration``:
|
|
133
|
+
the sum of ``exp_premium``, which is an array on a portfolio and a scalar
|
|
134
|
+
on an aggregate.
|
|
135
|
+
|
|
136
|
+
Parameters
|
|
137
|
+
----------
|
|
138
|
+
obj : Any
|
|
139
|
+
|
|
140
|
+
Returns
|
|
141
|
+
-------
|
|
142
|
+
bool
|
|
143
|
+
"""
|
|
144
|
+
return premium_for(obj) is not None
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def premium_for(obj: Any) -> float | None:
|
|
148
|
+
"""This object's own premium, or None when its exposure states none.
|
|
149
|
+
|
|
150
|
+
Consumer: the Pricing group's Evaluate form, which prefills the premium box
|
|
151
|
+
from it. ``needs_premium`` says whether the reader **must** type one; this
|
|
152
|
+
says what to put in the box when they need not, so an exposure that states a
|
|
153
|
+
consideration is evaluated as it stands with the number visible rather than
|
|
154
|
+
with an empty field that silently means "use your own".
|
|
155
|
+
|
|
156
|
+
Parameters
|
|
157
|
+
----------
|
|
158
|
+
obj : Any
|
|
159
|
+
|
|
160
|
+
Returns
|
|
161
|
+
-------
|
|
162
|
+
float or None
|
|
163
|
+
The total, summed over units on a portfolio. None where there is no
|
|
164
|
+
``exp_premium`` at all (a ``PnL`` keeps its premium in its ledger) and
|
|
165
|
+
where the total is zero, which is the library's own reading of an
|
|
166
|
+
exposure written without a consideration.
|
|
167
|
+
"""
|
|
168
|
+
premium = getattr(obj, "exp_premium", None)
|
|
169
|
+
if premium is None:
|
|
170
|
+
return None
|
|
171
|
+
try:
|
|
172
|
+
total = float(np.sum(np.asarray(premium, dtype=float)))
|
|
173
|
+
except (TypeError, ValueError):
|
|
174
|
+
return None
|
|
175
|
+
return total if total > 0.0 else None
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def can_sharpen(obj: Any) -> bool:
|
|
179
|
+
"""Is a grid audit worth offering for this object?
|
|
180
|
+
|
|
181
|
+
Consumer: the Sharpen button, which has no exhibit or chart of its own.
|
|
182
|
+
|
|
183
|
+
Two conditions. The object has to have the method at all, which is
|
|
184
|
+
``Aggregate`` and ``Portfolio`` and nothing else, and it is read off the
|
|
185
|
+
object rather than from a kind list so a future host needs no edit here.
|
|
186
|
+
And the program must not already carry a sharpen verdict, since a second
|
|
187
|
+
audit of a grid the probe just confirmed is a slow no-op.
|
|
188
|
+
|
|
189
|
+
The verdict test is one-sided on purpose. A probe that **moved** the grid
|
|
190
|
+
writes ``hints{}`` and clears its note, so Sharpen stays live: probing again
|
|
191
|
+
from the new center can find more, and the library says as much when a walk
|
|
192
|
+
runs out of its bucket limit while still improving. A probe that
|
|
193
|
+
**confirmed** the grid writes the note and no hints, and that is the case
|
|
194
|
+
worth refusing.
|
|
195
|
+
|
|
196
|
+
Parameters
|
|
197
|
+
----------
|
|
198
|
+
obj : Any
|
|
199
|
+
|
|
200
|
+
Returns
|
|
201
|
+
-------
|
|
202
|
+
bool
|
|
203
|
+
"""
|
|
204
|
+
if not hasattr(obj, "sharpen"):
|
|
205
|
+
return False
|
|
206
|
+
note = str(getattr(obj, "note", "") or "")
|
|
207
|
+
verdicts = (chunk.strip() for chunk in note.split(";"))
|
|
208
|
+
return not any(v.startswith(SHARPEN_NOTE_PREFIX) for v in verdicts)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def has_sharpen(obj: Any) -> bool:
|
|
212
|
+
"""Has a grid audit actually run on this object, leaving a table to read?
|
|
213
|
+
|
|
214
|
+
Consumer: the More group's Sharpen leaf, which shows what the probe tried.
|
|
215
|
+
|
|
216
|
+
**Not the negation of** :func:`can_sharpen`, and the two are not two views of
|
|
217
|
+
one fact. ``can_sharpen`` asks whether running the probe is worth offering;
|
|
218
|
+
this asks whether one has already run. An object that has never been
|
|
219
|
+
sharpened answers False to this and True to that; an object whose probe
|
|
220
|
+
*moved* the grid answers True to both, because the library says a walk that
|
|
221
|
+
ran out of its bucket limit while still improving is worth resuming.
|
|
222
|
+
|
|
223
|
+
Read off ``sharpen_df``, which the library leaves at ``None`` until
|
|
224
|
+
``sharpen()`` runs (``_aggregate.py:814``, ``_portfolio.py:1041``), so this
|
|
225
|
+
is the frame's own account of whether it exists rather than a guess from the
|
|
226
|
+
note.
|
|
227
|
+
|
|
228
|
+
Parameters
|
|
229
|
+
----------
|
|
230
|
+
obj : Any
|
|
231
|
+
|
|
232
|
+
Returns
|
|
233
|
+
-------
|
|
234
|
+
bool
|
|
235
|
+
"""
|
|
236
|
+
df = getattr(obj, "sharpen_df", None)
|
|
237
|
+
return df is not None and not df.empty
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
#: The whole program views, in the order the two pricing forms draw them.
|
|
241
|
+
#:
|
|
242
|
+
#: A subset of ``Aggregate.reins_views``, which also carries ``ceded`` and
|
|
243
|
+
#: ``ceded occ``: those are a cession read as a position in its own right, which
|
|
244
|
+
#: is the seller's question, and both forms here ask the insurer's. The library
|
|
245
|
+
#: serves the ceded rows in the ``pricing.stand_alone`` RAW reading, which is
|
|
246
|
+
#: where that perspective belongs until there is a REINSURER one.
|
|
247
|
+
_CALIBRATION_BASES = ("gross", "net occ", "net")
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def reins_bases_for(obj: Any) -> list[str]:
|
|
251
|
+
"""Which reinsurance bases this object can be calibrated on.
|
|
252
|
+
|
|
253
|
+
Consumers: the Pricing group's "calibrate on" row, and the Evaluate form's
|
|
254
|
+
"premium is" row. Both offered all three of gross, net occ and net to any
|
|
255
|
+
reinsured object before a52, and on most objects at least one of the three
|
|
256
|
+
was a button that either 400'd or repeated a column already on screen.
|
|
257
|
+
|
|
258
|
+
Read straight off ``obj.reins_views`` since a85, filtered to the whole
|
|
259
|
+
program views and put in the order the forms draw them. That property is the
|
|
260
|
+
library's own answer to what distributions a program has, so ``net occ``
|
|
261
|
+
appears exactly on the two-stage programs that have a distinct one, and the
|
|
262
|
+
app no longer has to know why. It replaces a local list of
|
|
263
|
+
``reins_density_df`` column names, which knew three views where the library
|
|
264
|
+
knows five and had to reason about the stages itself.
|
|
265
|
+
|
|
266
|
+
Empty for an object carrying no cession, which is what makes both rows grey
|
|
267
|
+
out as a whole rather than vanish.
|
|
268
|
+
|
|
269
|
+
**A reinsured Portfolio is locked to net** (author's ruling, 1.0.0a100).
|
|
270
|
+
Reinsurance is placed at the unit level: a book has no cession of its own to
|
|
271
|
+
choose and takes whatever its units produce, so there is no book-wide
|
|
272
|
+
decision for the row to offer. ``Portfolio.reins_views`` has reported
|
|
273
|
+
``['gross', 'ceded', 'net']`` since library a223 and both this filter's
|
|
274
|
+
survivors were live here through a99, which was wrong in a way a reader
|
|
275
|
+
could not see: ``CalibrationResult.pricing_df`` allocates ``density_df``,
|
|
276
|
+
the net book, whatever view was asked for, so a gross calibration arrived
|
|
277
|
+
beside a net allocation from one press, labeled as one calculation. Measured
|
|
278
|
+
on a two unit book, the two allocations agreed to the last digit while the
|
|
279
|
+
calibrations differed. Net is the only basis a book answers end to end.
|
|
280
|
+
|
|
281
|
+
An Aggregate is unaffected: a cession is placed on it directly, and all
|
|
282
|
+
three views are its own.
|
|
283
|
+
|
|
284
|
+
Parameters
|
|
285
|
+
----------
|
|
286
|
+
obj : Any
|
|
287
|
+
|
|
288
|
+
Returns
|
|
289
|
+
-------
|
|
290
|
+
list of str
|
|
291
|
+
"""
|
|
292
|
+
try:
|
|
293
|
+
views = set(obj.reins_views or ())
|
|
294
|
+
except Exception: # noqa: BLE001 -- an object that cannot answer offers none
|
|
295
|
+
return []
|
|
296
|
+
live = [name for name in _CALIBRATION_BASES if name in views]
|
|
297
|
+
if live and isinstance(obj, Portfolio):
|
|
298
|
+
return ["net"] if "net" in live else []
|
|
299
|
+
return live
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def can_price(obj: Any) -> bool:
|
|
303
|
+
"""Can this object answer the pricing forms?
|
|
304
|
+
|
|
305
|
+
Consumer: the Pricing group's Calibrate and Allocate leaves, which are app
|
|
306
|
+
behavior rather than a library document, so no exhibit or chart says this.
|
|
307
|
+
They gate together because one press fills both.
|
|
308
|
+
|
|
309
|
+
Read off the object as the method the pricing path actually calls, which is
|
|
310
|
+
the same test :func:`aggregate_api.pricing.run_calibration` makes before
|
|
311
|
+
it raises. A kind list would have been a second declaration of a fact the
|
|
312
|
+
object already carries, and the app had exactly that in
|
|
313
|
+
``NA_TABS_BY_KIND``.
|
|
314
|
+
|
|
315
|
+
Parameters
|
|
316
|
+
----------
|
|
317
|
+
obj : Any
|
|
318
|
+
|
|
319
|
+
Returns
|
|
320
|
+
-------
|
|
321
|
+
bool
|
|
322
|
+
"""
|
|
323
|
+
return hasattr(obj, "price_pentagon")
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def can_evaluate(obj: Any) -> bool:
|
|
327
|
+
"""Can this object answer the acceptability panel?
|
|
328
|
+
|
|
329
|
+
Consumer: the Pricing group's Evaluate leaf. A separate flag from
|
|
330
|
+
:func:`can_price` and not a synonym for it: evaluation reaches a ``PnL``,
|
|
331
|
+
which has no ``price_pentagon`` and is the kind the panel says the most
|
|
332
|
+
about, since it evaluates every margin row of the ledger rather than one
|
|
333
|
+
position.
|
|
334
|
+
|
|
335
|
+
Read off ``evaluate`` for the same reason the others are read off their
|
|
336
|
+
methods: the object is the authority on what it can do.
|
|
337
|
+
|
|
338
|
+
Parameters
|
|
339
|
+
----------
|
|
340
|
+
obj : Any
|
|
341
|
+
|
|
342
|
+
Returns
|
|
343
|
+
-------
|
|
344
|
+
bool
|
|
345
|
+
"""
|
|
346
|
+
return hasattr(obj, "evaluate")
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def can_pnl(obj: Any) -> bool:
|
|
350
|
+
"""Can this object be wrapped in a P&L?
|
|
351
|
+
|
|
352
|
+
Consumer: the action row's PnL button.
|
|
353
|
+
|
|
354
|
+
Read off ``pnl_program``, which the library puts only on the two classes
|
|
355
|
+
that can honestly answer, so "does this object have it" is the whole test.
|
|
356
|
+
Deliberately not folded into :func:`can_price`, even though both are true
|
|
357
|
+
for exactly an ``Aggregate`` and a ``Portfolio`` today: they are two
|
|
358
|
+
different questions and a shared flag would tie a future change in one to
|
|
359
|
+
the other.
|
|
360
|
+
|
|
361
|
+
Parameters
|
|
362
|
+
----------
|
|
363
|
+
obj : Any
|
|
364
|
+
|
|
365
|
+
Returns
|
|
366
|
+
-------
|
|
367
|
+
bool
|
|
368
|
+
"""
|
|
369
|
+
return hasattr(obj, "pnl_program")
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
#: Does the installed library's ``pnl_program`` accept ``premium_style``?
|
|
373
|
+
#:
|
|
374
|
+
#: Feature-detected once at import, off the public method's signature, because
|
|
375
|
+
#: the keyword is an upstream ask (``dev/plan-a182-pricing-pnl-forms.md``, the
|
|
376
|
+
#: upstream section) and the api must be honest before it ships: while this is
|
|
377
|
+
#: False the route refuses ``premium_style='rate'`` with a 400 and the two rate
|
|
378
|
+
#: menu items grey through :func:`can_pnl_rate`. No version dance: the next
|
|
379
|
+
#: editable-checkout sync flips it.
|
|
380
|
+
PNL_PREMIUM_STYLE_SUPPORTED = (
|
|
381
|
+
"premium_style" in inspect.signature(Aggregate.pnl_program).parameters)
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def can_pnl_rate(obj: Any) -> bool:
|
|
385
|
+
"""Can this object be wrapped with ceded premiums written as rates?
|
|
386
|
+
|
|
387
|
+
Consumer: the action row's two rate menu items, ``PnL (rate)`` and
|
|
388
|
+
``xPnL (rate)``.
|
|
389
|
+
|
|
390
|
+
:func:`can_pnl` plus the installed library accepting
|
|
391
|
+
``pnl_program(premium_style=)``. The second condition is the honest one:
|
|
392
|
+
the keyword is an upstream ask, so a fresh install can carry a library
|
|
393
|
+
that cannot write a ``rate`` clause yet, and the items grey with a why
|
|
394
|
+
rather than the press producing a 400.
|
|
395
|
+
|
|
396
|
+
Parameters
|
|
397
|
+
----------
|
|
398
|
+
obj : Any
|
|
399
|
+
|
|
400
|
+
Returns
|
|
401
|
+
-------
|
|
402
|
+
bool
|
|
403
|
+
"""
|
|
404
|
+
return PNL_PREMIUM_STYLE_SUPPORTED and can_pnl(obj)
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
#: Does the installed library's ``PnL`` serve ``pentagon_df``?
|
|
408
|
+
#:
|
|
409
|
+
#: Feature-detected once at import, for the same reason
|
|
410
|
+
#: :data:`PNL_PREMIUM_STYLE_SUPPORTED` is: the frame is an upstream ask that
|
|
411
|
+
#: landed in ``aggregate`` 1.0.0a393, so a fresh checkout can carry a library
|
|
412
|
+
#: without it. While this is False the Pentagon leaf greys with a why and the
|
|
413
|
+
#: route refuses with a 400, rather than the press producing a traceback.
|
|
414
|
+
PNL_PENTAGON_SUPPORTED = hasattr(PnL, "pentagon_df")
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def can_pnl_pentagon(obj: Any) -> bool:
|
|
418
|
+
"""Can this object answer the Pentagon figure?
|
|
419
|
+
|
|
420
|
+
Consumer: the ``PnL / Pentagon`` leaf, which is a picture this app draws
|
|
421
|
+
rather than a library document, so it gates on a flag rather than on the
|
|
422
|
+
exhibit list. That is the third of the three gating kinds this module's
|
|
423
|
+
header names.
|
|
424
|
+
|
|
425
|
+
Two conditions. The object is a :class:`~aggregate.PnL`, which is the only
|
|
426
|
+
kind carrying a ledger to read as a pentagon; and the installed library
|
|
427
|
+
serves the frame at all (:data:`PNL_PENTAGON_SUPPORTED`). The second is
|
|
428
|
+
what earns the flag its place beside ``kind``, which already rides on the
|
|
429
|
+
build response and would otherwise say the first half on its own.
|
|
430
|
+
|
|
431
|
+
A single-group P&L answers. It has no cession, so the figure draws one
|
|
432
|
+
pentagon with nothing left over, which is the honest picture of a book with
|
|
433
|
+
no program rather than a reason to grey the leaf. A hand-built kernel P&L
|
|
434
|
+
answers too: its rows carry distributions, so the capital is a real
|
|
435
|
+
quantile, and the frame asks nothing of the stochastic engine it lacks.
|
|
436
|
+
|
|
437
|
+
Parameters
|
|
438
|
+
----------
|
|
439
|
+
obj : Any
|
|
440
|
+
|
|
441
|
+
Returns
|
|
442
|
+
-------
|
|
443
|
+
bool
|
|
444
|
+
"""
|
|
445
|
+
return PNL_PENTAGON_SUPPORTED and isinstance(obj, PnL)
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def can_xpnl(obj: Any) -> bool:
|
|
449
|
+
"""Can this object end up exploded, layer by layer, from one press?
|
|
450
|
+
|
|
451
|
+
Consumer: the action row's ``xPnL`` menu item (and, with
|
|
452
|
+
:func:`can_pnl_rate`, the ``xPnL (rate)`` one).
|
|
453
|
+
|
|
454
|
+
Two shapes answer, because the item means "get me the exploded P&L of
|
|
455
|
+
what is in the box" wherever it starts. A non-P&L wraps and explodes in
|
|
456
|
+
one request, which needs :func:`can_pnl` and a single ``Aggregate``: the
|
|
457
|
+
library refuses to explode a portfolio because the portfolio total hides
|
|
458
|
+
its units. A P&L has only the explode left to do, so it answers through
|
|
459
|
+
the existing :func:`can_explode`, which stays, unchanged, as the explode
|
|
460
|
+
route's own gate.
|
|
461
|
+
|
|
462
|
+
Parameters
|
|
463
|
+
----------
|
|
464
|
+
obj : Any
|
|
465
|
+
|
|
466
|
+
Returns
|
|
467
|
+
-------
|
|
468
|
+
bool
|
|
469
|
+
"""
|
|
470
|
+
if type(obj).__name__ == "PnL":
|
|
471
|
+
return can_explode(obj)
|
|
472
|
+
return can_pnl(obj) and isinstance(obj, Aggregate)
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
def can_explode(obj: Any) -> bool:
|
|
476
|
+
"""Can this P&L be broken out layer by layer?
|
|
477
|
+
|
|
478
|
+
Consumer: the action row's PnL button in its second state, where it reads
|
|
479
|
+
``Explode`` and posts to ``/objects/{id}/explode``.
|
|
480
|
+
|
|
481
|
+
True for a ``PnL`` built by ``pnl`` whose engine is a single aggregate.
|
|
482
|
+
False for one already built by ``xpnl``, which has nothing left to do, and
|
|
483
|
+
false for a portfolio engine, which the library refuses to explode because
|
|
484
|
+
the portfolio total hides the units the walk would step through.
|
|
485
|
+
|
|
486
|
+
Read off ``PnL.program``, the text the object was built from, because the
|
|
487
|
+
two keywords produce the same class and the source is the only thing that
|
|
488
|
+
tells them apart. The leading ``x`` is the whole test: an object of this
|
|
489
|
+
class leads with one keyword or the other.
|
|
490
|
+
|
|
491
|
+
Parameters
|
|
492
|
+
----------
|
|
493
|
+
obj : Any
|
|
494
|
+
|
|
495
|
+
Returns
|
|
496
|
+
-------
|
|
497
|
+
bool
|
|
498
|
+
"""
|
|
499
|
+
if type(obj).__name__ != "PnL":
|
|
500
|
+
return False
|
|
501
|
+
if type(getattr(obj, "engine", None)).__name__ == "Portfolio":
|
|
502
|
+
return False
|
|
503
|
+
return str(getattr(obj, "program", "") or "").lstrip().startswith("pnl")
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
def can_hints(obj: Any) -> bool:
|
|
507
|
+
"""Can this object pin its realized grid into a ``hints{}`` clause?
|
|
508
|
+
|
|
509
|
+
Consumer: the action row's Hints button.
|
|
510
|
+
|
|
511
|
+
Read off ``with_hints``, which ``aggregate`` 1.0.0a291 put on ``Aggregate``
|
|
512
|
+
and ``Portfolio``, so "does this object have it" is the whole test.
|
|
513
|
+
|
|
514
|
+
Deliberately not folded into :func:`can_pnl`, even though both are true for
|
|
515
|
+
exactly those two classes today. They are two different questions, and one
|
|
516
|
+
flag would tie a future change in either to the other. The same argument
|
|
517
|
+
``can_pnl`` makes about ``can_price``.
|
|
518
|
+
|
|
519
|
+
Parameters
|
|
520
|
+
----------
|
|
521
|
+
obj : Any
|
|
522
|
+
|
|
523
|
+
Returns
|
|
524
|
+
-------
|
|
525
|
+
bool
|
|
526
|
+
"""
|
|
527
|
+
return hasattr(obj, "with_hints")
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
def can_reins(obj: Any) -> bool:
|
|
531
|
+
"""Can a cession be added to this object?
|
|
532
|
+
|
|
533
|
+
Consumer: the Reinsurance group's entry box, and the group pill above it.
|
|
534
|
+
This is the flag that makes the group live for an aggregate carrying **no**
|
|
535
|
+
cession, which is the case the entry box exists for: every table and chart
|
|
536
|
+
in there is dark until there is a program to describe, so without this the
|
|
537
|
+
group would grey out exactly when you wanted to add cover.
|
|
538
|
+
|
|
539
|
+
Read off ``reins_program``, which the library puts on ``Aggregate`` alone.
|
|
540
|
+
A portfolio cedes through its units rather than as a whole.
|
|
541
|
+
|
|
542
|
+
Parameters
|
|
543
|
+
----------
|
|
544
|
+
obj : Any
|
|
545
|
+
|
|
546
|
+
Returns
|
|
547
|
+
-------
|
|
548
|
+
bool
|
|
549
|
+
"""
|
|
550
|
+
return hasattr(obj, "reins_program")
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
def can_views(obj: Any) -> bool:
|
|
554
|
+
"""Can this object be re-read as a gross / ceded / net view pair?
|
|
555
|
+
|
|
556
|
+
Consumer: the action row's GCN control, which prepends ``grossceded``,
|
|
557
|
+
``grossnet`` or ``netceded`` to the program and rebuilds. The result is a
|
|
558
|
+
``BivariateAggregate`` of the named pair.
|
|
559
|
+
|
|
560
|
+
Two conditions, and the second is the one that is easy to get wrong. The
|
|
561
|
+
grammar's three view prefixes take an ``agg_out``, so this is an
|
|
562
|
+
``Aggregate`` question and a portfolio cannot answer it. And they build the
|
|
563
|
+
joint **per-occurrence** aggregate of the pair, so what they need is an
|
|
564
|
+
**occurrence** cession specifically: ``has_reins`` is the weaker test and
|
|
565
|
+
would light the control for a program carrying only an aggregate cession,
|
|
566
|
+
which then fails on submit. The house rule is that things grey with a
|
|
567
|
+
reason rather than fail when pressed, so the narrower test is the right one.
|
|
568
|
+
|
|
569
|
+
Read off ``occ_reins``, the cession spec itself, which is what
|
|
570
|
+
``routes.objects._has_reinsurance`` reads for the same reason: it costs
|
|
571
|
+
nothing, where materializing ``reins_summary_df`` on every build would.
|
|
572
|
+
|
|
573
|
+
Parameters
|
|
574
|
+
----------
|
|
575
|
+
obj : Any
|
|
576
|
+
|
|
577
|
+
Returns
|
|
578
|
+
-------
|
|
579
|
+
bool
|
|
580
|
+
"""
|
|
581
|
+
if not hasattr(obj, "reins_program"):
|
|
582
|
+
return False
|
|
583
|
+
return getattr(obj, "occ_reins", None) is not None
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
def can_bounds(obj: Any) -> bool:
|
|
587
|
+
"""Can pricing bounds be computed for this object?
|
|
588
|
+
|
|
589
|
+
Consumers: the Bounds group's envelope and PricingBounds leaves.
|
|
590
|
+
|
|
591
|
+
An ``isinstance`` rather than a ``hasattr``, unlike the flags above, and
|
|
592
|
+
the difference is honest: those name a method the object either has or does
|
|
593
|
+
not, while ``aggregate.bounds.Bounds`` declares the types it accepts and a
|
|
594
|
+
duck-typed near miss would fail somewhere deep instead of at the door. This
|
|
595
|
+
is the library's own accepted set, restricted to the two members the api
|
|
596
|
+
can hold, so it is still the library deciding. A P&L qualifies through its
|
|
597
|
+
engine: the library unwraps ``obj.engine`` itself (aggregate 1.0.0a375,
|
|
598
|
+
[Bounds-PnL-Engine]), so this see-through mirrors the constructor and the
|
|
599
|
+
flag and the constructor cannot disagree.
|
|
600
|
+
|
|
601
|
+
Parameters
|
|
602
|
+
----------
|
|
603
|
+
obj : Any
|
|
604
|
+
|
|
605
|
+
Returns
|
|
606
|
+
-------
|
|
607
|
+
bool
|
|
608
|
+
"""
|
|
609
|
+
return isinstance(obj, (Aggregate, Portfolio)) or \
|
|
610
|
+
isinstance(getattr(obj, "engine", None), (Aggregate, Portfolio))
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
def can_allocate(obj: Any) -> bool:
|
|
614
|
+
"""Can allocation bounds be computed?
|
|
615
|
+
|
|
616
|
+
Consumer: the Bounds group's AllocationBounds leaf.
|
|
617
|
+
|
|
618
|
+
A portfolio and nothing else. The calculation reads the ``exeqa_*`` columns
|
|
619
|
+
that a portfolio's density frame carries, which is the conditional
|
|
620
|
+
expectation of each unit given the total, and a single aggregate has no
|
|
621
|
+
analogue: there is one unit, so there is nothing to allocate.
|
|
622
|
+
|
|
623
|
+
Parameters
|
|
624
|
+
----------
|
|
625
|
+
obj : Any
|
|
626
|
+
|
|
627
|
+
Returns
|
|
628
|
+
-------
|
|
629
|
+
bool
|
|
630
|
+
"""
|
|
631
|
+
return isinstance(obj, Portfolio)
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
def can_natural_allocation(obj: Any) -> bool:
|
|
635
|
+
"""Does this object have parts to split one premium across?
|
|
636
|
+
|
|
637
|
+
Consumer: the Pricing group's Allocate leaf, and the route behind it. An
|
|
638
|
+
app leaf rather than an exhibit one, because ``pricing.allocate`` dispatches
|
|
639
|
+
on a ``CalibrationResult`` and never appears in :func:`exhibits_for`, so the
|
|
640
|
+
exhibit list cannot light this pill.
|
|
641
|
+
|
|
642
|
+
Two shapes qualify. A ``Portfolio`` allocates the book's premium across its
|
|
643
|
+
units, which is what ``analyze_distortions`` has always done. An
|
|
644
|
+
``Aggregate`` carrying an occurrence program allocates the gross premium
|
|
645
|
+
across the two halves of that program, ceded and net, off the joint
|
|
646
|
+
distribution.
|
|
647
|
+
|
|
648
|
+
**Not** :func:`can_allocate`, which is the Bounds group's per-unit range and
|
|
649
|
+
is a ``Portfolio`` alone. The names are close because the questions are
|
|
650
|
+
cousins; the flags are separate because one lights a pricing subtab and the
|
|
651
|
+
other a bounds leaf, and tying them would tie a future change in one to the
|
|
652
|
+
other.
|
|
653
|
+
|
|
654
|
+
Parameters
|
|
655
|
+
----------
|
|
656
|
+
obj : Any
|
|
657
|
+
|
|
658
|
+
Returns
|
|
659
|
+
-------
|
|
660
|
+
bool
|
|
661
|
+
|
|
662
|
+
Notes
|
|
663
|
+
-----
|
|
664
|
+
An aggregate cession is deliberately not enough. The natural allocation
|
|
665
|
+
reads the kappa curve off the joint distribution of gross and ceded, which
|
|
666
|
+
the library builds per occurrence; a program that only cedes in the
|
|
667
|
+
aggregate has no such joint to condition on. ``occ_reins`` is the library's
|
|
668
|
+
own public record of whether an occurrence stage was placed, so this asks it
|
|
669
|
+
rather than reasoning about the program text.
|
|
670
|
+
|
|
671
|
+
The basis is a second gate and it is not here: the allocation splits a
|
|
672
|
+
**gross** premium, so a fit struck on net has nothing to split. That is a
|
|
673
|
+
property of the calibration rather than of the object, which is why it lives
|
|
674
|
+
on the route and in the library's own availability predicate.
|
|
675
|
+
"""
|
|
676
|
+
if isinstance(obj, Portfolio):
|
|
677
|
+
return True
|
|
678
|
+
return isinstance(obj, Aggregate) and obj.occ_reins is not None
|
|
679
|
+
|
|
680
|
+
|
|
681
|
+
def needs_premium(obj: Any) -> bool:
|
|
682
|
+
"""Must the Evaluate form ask for a premium before it can run?
|
|
683
|
+
|
|
684
|
+
Consumer: the Pricing group's Evaluate leaf, which otherwise shows a form
|
|
685
|
+
with nothing in it and posts straight into a 400.
|
|
686
|
+
|
|
687
|
+
Three conditions, and none of them is a kind test. The object can be
|
|
688
|
+
evaluated at all. It is the sort of thing that carries its own
|
|
689
|
+
consideration, which is what ``exp_premium`` being present means: a ``PnL``
|
|
690
|
+
has no such attribute, because its premium lives in its ledger and there is
|
|
691
|
+
nothing to ask for. And it does not actually carry one.
|
|
692
|
+
|
|
693
|
+
Parameters
|
|
694
|
+
----------
|
|
695
|
+
obj : Any
|
|
696
|
+
|
|
697
|
+
Returns
|
|
698
|
+
-------
|
|
699
|
+
bool
|
|
700
|
+
"""
|
|
701
|
+
return (can_evaluate(obj)
|
|
702
|
+
and hasattr(obj, "exp_premium")
|
|
703
|
+
and not has_premium(obj))
|
|
704
|
+
|
|
705
|
+
|
|
706
|
+
#: Text fields are found by suffix rather than listed, so a narrative the
|
|
707
|
+
#: library adds appears with no edit here. Leading-underscore names are the
|
|
708
|
+
#: private working copies behind the public properties and would print twice.
|
|
709
|
+
NARRATIVE_SUFFIXES = ("_description", "_explanation")
|
|
710
|
+
|
|
711
|
+
#: Reading order for the Narrative pane, by attribute stem.
|
|
712
|
+
#:
|
|
713
|
+
#: The sections used to come out sorted by stem, which is alphabetical order over
|
|
714
|
+
#: names the reader never sees and has no reason to care about: `bs` before
|
|
715
|
+
#: `info` before `reins` before `sharpen` before `tail` before `validation`. That
|
|
716
|
+
#: is not an order, it is the absence of one.
|
|
717
|
+
#:
|
|
718
|
+
#: This is the order the questions actually get asked in. What was built, what
|
|
719
|
+
#: grid it was built on, whether that grid is trustworthy, how it behaves out in
|
|
720
|
+
#: the tail, what was ceded off it, and finally what the grid audit found, which
|
|
721
|
+
#: is the most specialist of the six and the one most often absent.
|
|
722
|
+
#:
|
|
723
|
+
#: Anything the library adds later is appended, alphabetically among itself, so a
|
|
724
|
+
#: new stem still appears without an edit here. That is the same contract the
|
|
725
|
+
#: suffix rule above keeps: derived by default, ordered by hand only where the
|
|
726
|
+
#: order carries meaning.
|
|
727
|
+
#:
|
|
728
|
+
#: ``info`` never matches a stem, since the object's ``info`` block is a plain
|
|
729
|
+
#: string served alongside these rather than a ``*_description`` pair. It leads
|
|
730
|
+
#: the list because that is where the pane puts it, and naming it here is what
|
|
731
|
+
#: makes this tuple the whole reading order rather than most of it.
|
|
732
|
+
NARRATIVE_ORDER = ("info", "bs", "validation", "tail", "reins", "sharpen")
|
|
733
|
+
|
|
734
|
+
|
|
735
|
+
def narrative_for(obj: Any) -> list[dict]:
|
|
736
|
+
"""Every text field this object carries, in one list.
|
|
737
|
+
|
|
738
|
+
Consumer: the More group's Narrative leaf, which is the pane that collects
|
|
739
|
+
what the object says about itself in prose rather than in numbers.
|
|
740
|
+
|
|
741
|
+
Derived by suffix, in the same spirit as the exhibit list: a new
|
|
742
|
+
``*_description`` upstream shows up here on its own. Sections are grouped by
|
|
743
|
+
stem, so ``validation_description`` and ``validation_explanation`` arrive as
|
|
744
|
+
one heading with a short form and a long one, which is what they are.
|
|
745
|
+
|
|
746
|
+
Empty strings are dropped rather than rendered as blank headings. Most of
|
|
747
|
+
these are empty most of the time: ``sharpen_*`` before a probe has run,
|
|
748
|
+
``reins_*`` with no cession.
|
|
749
|
+
|
|
750
|
+
Parameters
|
|
751
|
+
----------
|
|
752
|
+
obj : Any
|
|
753
|
+
|
|
754
|
+
Returns
|
|
755
|
+
-------
|
|
756
|
+
list of dict
|
|
757
|
+
``{"name", "description", "explanation"}``, in ``NARRATIVE_ORDER``, with
|
|
758
|
+
anything unlisted appended alphabetically.
|
|
759
|
+
"""
|
|
760
|
+
sections: dict[str, dict] = {}
|
|
761
|
+
for attr in dir(obj):
|
|
762
|
+
if attr.startswith("_"):
|
|
763
|
+
continue
|
|
764
|
+
for suffix in NARRATIVE_SUFFIXES:
|
|
765
|
+
if not attr.endswith(suffix):
|
|
766
|
+
continue
|
|
767
|
+
try:
|
|
768
|
+
text = getattr(obj, attr)
|
|
769
|
+
except Exception: # noqa: BLE001 -- a field that raises is absent
|
|
770
|
+
continue
|
|
771
|
+
text = str(text or "").strip()
|
|
772
|
+
if not text:
|
|
773
|
+
continue
|
|
774
|
+
stem = attr[: -len(suffix)]
|
|
775
|
+
section = sections.setdefault(
|
|
776
|
+
stem, {"name": stem, "description": "", "explanation": ""})
|
|
777
|
+
section[suffix.lstrip("_")] = text
|
|
778
|
+
|
|
779
|
+
# Listed stems first in their declared order, then whatever else the object
|
|
780
|
+
# carried, alphabetically among themselves so the tail of the list is at
|
|
781
|
+
# least stable.
|
|
782
|
+
rank = {name: i for i, name in enumerate(NARRATIVE_ORDER)}
|
|
783
|
+
order = sorted(sections, key=lambda n: (rank.get(n, len(rank)), n))
|
|
784
|
+
return [sections[name] for name in order]
|
|
785
|
+
|
|
786
|
+
|
|
787
|
+
def capability_for(obj: Any) -> dict:
|
|
788
|
+
"""The whole capability block for one object.
|
|
789
|
+
|
|
790
|
+
Parameters
|
|
791
|
+
----------
|
|
792
|
+
obj : Any
|
|
793
|
+
A built object.
|
|
794
|
+
|
|
795
|
+
Returns
|
|
796
|
+
-------
|
|
797
|
+
dict
|
|
798
|
+
The whole block, matching :class:`aggregate_api.models.Capability`.
|
|
799
|
+
"""
|
|
800
|
+
return {
|
|
801
|
+
"exhibits": exhibits_for(obj),
|
|
802
|
+
"charts": charts_for(obj),
|
|
803
|
+
"primary_chart": primary_chart_for(obj),
|
|
804
|
+
"has_premium": has_premium(obj),
|
|
805
|
+
"premium": premium_for(obj),
|
|
806
|
+
"can_sharpen": can_sharpen(obj),
|
|
807
|
+
"has_sharpen": has_sharpen(obj),
|
|
808
|
+
"can_pnl": can_pnl(obj),
|
|
809
|
+
"can_pnl_rate": can_pnl_rate(obj),
|
|
810
|
+
"can_pnl_pentagon": can_pnl_pentagon(obj),
|
|
811
|
+
"can_xpnl": can_xpnl(obj),
|
|
812
|
+
"can_explode": can_explode(obj),
|
|
813
|
+
"can_hints": can_hints(obj),
|
|
814
|
+
"can_reins": can_reins(obj),
|
|
815
|
+
"can_views": can_views(obj),
|
|
816
|
+
"reins_bases": reins_bases_for(obj),
|
|
817
|
+
"can_price": can_price(obj),
|
|
818
|
+
"can_evaluate": can_evaluate(obj),
|
|
819
|
+
"can_bounds": can_bounds(obj),
|
|
820
|
+
"can_allocate": can_allocate(obj),
|
|
821
|
+
"can_natural_allocation": can_natural_allocation(obj),
|
|
822
|
+
"needs_premium": needs_premium(obj),
|
|
823
|
+
}
|