PineForge HPO 1.11.0
Native hyperparameter optimization for PineForge strategies
Loading...
Searching...
No Matches
dsr.py
Go to the documentation of this file.
1"""Deflated Sharpe ratio of one selected trial from a finished study.
2
3``dsr`` reads one complete result, rows JSONL file, parsed result dictionary or
4list of rows through the shared reader. The caller must name ``series`` (``bar``
5or ``monthly``) and ``selected`` (a trial id); neither has a default. This library
6function runs no trial, writes no file, keeps no state and changes no native result.
7
8Contract ``pineforge-hpo-dsr/v1``, algorithm ``deflated_sharpe_exact_v1`` uses the
9selected row's recorded excess ``sharpe_per_period``, ``count``, population skewness
10and raw population kurtosis (normal = 3). It never reads the objective as a Sharpe,
11annualises a ratio, or converts the recorded moment conventions.
12
13Only ``ok`` rows with all five requested return-statistic keys, a series status
14that is exactly the integer zero, finite numeric values and an integer count in
152..2^53 are eligible. The first exclusion reason decides: ``status_<status>``,
16``returns_not_recorded``, ``returns_status_nonzero``, then ``returns_unavailable``.
17Eligible rows alone feed the sample variance and the selected row's statistics.
18Default N counts every row read, including excluded trials and warm-start ancestors:
19this is the conservative choice because every recorded trial was still searched.
20A caller's ``n_trials`` must be an integer in 2..2^50 and at least the rows read.
21
22Before exclusions, a requested series entry of any row that cannot be written as
23key-sorted JSON refuses the call as ``dsr_input_invalid``. An object whose keys
24cannot be sorted in any of the five entries also refuses. Its ``ids`` is the union
25of the offending trial ids in ascending order, and ``names`` lists the offending entry
26keys in ascending alphabetical order. An ``ok`` row with all five keys, series
27status zero and an integer value whose float conversion overflows is excluded as
28``returns_unavailable``. An integer beyond the interpreter's integer-to-text
29limit in a value entry is not refused: its digest cell is
30``["other", "<unwritable>"]``. An integer beyond the interpreter's integer-to-text
31limit in the status entry is refused so the result's ``statuses`` can be written;
32a smaller large integer there is a status that is not ok.
33
34The sample variance V and the denominator ``1 - g3*SR + ((g4-1)/4)*SR^2`` are exact
35rationals over the binary64 inputs, each rounded once, ties to even. Both must be
36positive as rationals and after rounding. The same rounded denominator is reported
37and used for its square root. The remaining scalar binary64 operations follow a
38fixed order: float(N); division then subtraction for ``1 - 1/N``; multiplication,
39division then subtraction for ``1 - 1/(N*E)``; the two inverse normal calls; the
40weighted sum; sqrt(V) times that sum; sqrt(float(T)-1); sqrt(den); subtraction,
41multiplication then division for z; the normal cdf. G is 0.5772156649015329 and E is
422.718281828459045. Phi and its inverse come from ``statistics.NormalDist``. Every
43intermediate must be finite; a finite z whose cdf saturates to 0.0 or 1.0 is valid.
44
45The returned document names every eligible and excluded id in ascending order,
46with ascending reason keys and distinct nonzero series statuses ordered by integer
47value first, then compact JSON text. Its SHA-256 covers every row read through
48sorted per-row digests of the id, status and five typed return-statistic cells;
49it covers neither the selection nor N. Absent and null cells coincide, while
50integers, floats and booleans retain distinct representations.
51The admission figure is rows read + eligible rows, with no built-in cap.
52
53A refusal raises ``DsrError``; ``document()`` renders only the refusal. The first
54defect decides: parameters (series, selected, n_trials, max_work); source,
55context and series cells; eligible count (zero before one); selection (unknown
56before excluded); N against rows read; admission figure; variance then the
57remaining statistic.
58Under the paper's assumptions the DSR reads as the probability that the selected
59trial's true Sharpe exceeds the expected maximum of N unskilled trials. Tuned
60trials are correlated, so this is a deflated reading under a stated N, not a
61significance test. Recorded data is taken as given, no threshold is applied and
62no out-of-sample performance is claimed. v1 analyses finished studies; native
63in-loop walk-forward comes later.
64"""
65
66from __future__ import annotations
67
68import hashlib
69import json
70import math
71from fractions import Fraction
72from statistics import NormalDist, StatisticsError
73from typing import Any, Iterable
74
75from ._rows import RowsError, check_context, read_source
76
77
78
79CONTRACT = "pineforge-hpo-dsr/v1"
80ALGORITHM = "deflated_sharpe_exact_v1"
81CODES = (
82 "dsr_input_invalid",
83 "dsr_rows_incomplete",
84 "dsr_context_mismatch",
85 "dsr_parameter_invalid",
86 "dsr_selected_unknown",
87 "dsr_selected_not_eligible",
88 "dsr_no_eligible_rows",
89 "dsr_too_few_eligible_rows",
90 "dsr_trials_inconsistent",
91 "dsr_zero_sharpe_variance",
92 "dsr_statistic_undefined",
93 "dsr_resource_exceeded",
94)
95INTERPRETATION = (
96 "(a) The number is conditional on the trial count N and on the variance V, and "
97 "both are read from the finished rows of this one source: the default N is the "
98 "number of rows read, every trial of the record whatever its status, so it is "
99 "the conservative count of the trials this file holds, and V uses only the "
100 "eligible rows. If the selection was made over trials this source does not "
101 "hold — a sweep split over several studies, or a grid whose other arms were "
102 "written elsewhere — then N counts only the trials this file holds, and the "
103 "number is too favourable, unless the caller passes n_trials. "
104 "(b) The module assumes that the returns of the selected trial are the ones its "
105 "sharpe_per_period was computed on; it reads a recorded number and cannot check "
106 "what data any trial saw, so the recording is taken as given. "
107 "(c) The trials of a tuned study are not independent: the search correlates them, "
108 "the runs share a dataset and a window the module cannot see, and the N of the "
109 "paper is an effective number of independent trials, not a count of runs. The "
110 "number is therefore a deflated reading under a stated N, and it is not a "
111 "significance test of the strategy. "
112 "(d) No threshold is applied: 0.95 and every other level are the caller's to set, "
113 "and the module reports a number only. "
114 "(e) The number is in-sample: it says nothing about how the strategy would perform "
115 "on data the study did not see, and no out-of-sample performance is claimed. "
116 "(f) v1 analyses finished studies; native in-loop walk-forward comes later."
117)
118
119
120
121
122
123
124class DsrError(Exception):
125 """A refusal with a module-local code, detail, sorted ids and field names."""
126
128 self, code: str, detail: str, ids: Iterable = (), names: Iterable = ()
129 ) -> None:
130 if code not in CODES:
131 raise ValueError(f"unknown dsr refusal code: {code!r}")
132 super().__init__(f"{code}: {detail}")
133 self.code = code
134 self.detail = detail
135 self.ids = tuple(sorted(ids))
136 self.names = tuple(names)
137
138 def __reduce__(self):
139 return (type(self), (self.code, self.detail, self.ids, self.names))
140
141 def document(self) -> dict:
142 """Return the refused document, without any statistic."""
143 return {
144 "contract": CONTRACT,
145 "status": "refused",
146 "refusal": {
147 "code": self.code,
148 "detail": self.detail,
149 "ids": list(self.ids),
150 "names": list(self.names),
151 },
152 }
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329def dsr(
330 source: Any,
331 *,
332 series: str,
333 selected: int,
334 n_trials: int | None = None,
335 max_work: int | None = None,
336) -> dict:
337 """Return one selected trial's DSR document, or raise ``DsrError``.
338
339 N defaults to all rows read; a caller may only increase it. ``max_work`` bounds
340 rows read + eligible rows before exact arithmetic is performed.
341 """
342 _check_parameters(series, selected, n_trials, max_work)
343 try:
344 loaded = read_source(source)
345 check_context(loaded.rows)
346 except RowsError as error:
347 ids = [item[1] if isinstance(item, tuple) else item for item in error.ids]
348 code = {
349 "rows_incomplete": "dsr_rows_incomplete",
350 "context_mismatch": "dsr_context_mismatch",
351 }.get(error.kind, "dsr_input_invalid")
352 raise DsrError(code, error.detail, ids, error.names) from None
353 rows = sorted(loaded.rows, key=lambda row: row.trial_id)
354 names = tuple(f"returns.{series}.{field}" for field in _FIELDS)
355 invalid_ids = set()
356 invalid_names = set()
357 for name in names:
358 for row in rows:
359 if row.metrics is None or name not in row.metrics:
360 continue
361 value = row.metrics[name]
362 try:
363 _json(value)
364 except (TypeError, ValueError, OverflowError, RecursionError):
365 # Oversized integer values have a total digest representation.
366 if type(value) is int and name != names[-1]:
367 continue
368 invalid_ids.add(row.trial_id)
369 invalid_names.add(name)
370 if invalid_ids:
371 raise DsrError(
372 "dsr_input_invalid",
373 "a series entry cannot be written as JSON",
374 invalid_ids,
375 sorted(invalid_names),
376 )
377 eligible = {}
378 reasons = {}
379 status_values = []
380 for row in rows:
381 reason = None
382 if row.status != "ok":
383 reason = f"status_{row.status}"
384 elif row.metrics is None or any(name not in row.metrics for name in names):
385 reason = "returns_not_recorded"
386 else:
387 sr, count, skew, kurt, status = (row.metrics[name] for name in names)
388 if type(status) is not int or status != 0:
389 reason = "returns_status_nonzero"
390 status_values.append(status)
391 else:
392 sr, skew, kurt = _number(sr), _number(skew), _number(kurt)
393 if (
394 sr is None
395 or skew is None
396 or kurt is None
397 or type(count) is not int
398 or not 2 <= count <= 1 << 53
399 ):
400 reason = "returns_unavailable"
401 else:
402 eligible[row.trial_id] = (sr, count, skew, kurt)
403 if reason is not None:
404 reasons.setdefault(reason, []).append(row.trial_id)
405 if not eligible:
406 raise DsrError("dsr_no_eligible_rows", "no row remains after exclusions")
407 if len(eligible) == 1:
408 raise DsrError(
409 "dsr_too_few_eligible_rows",
410 "only one eligible row remains",
411 eligible.keys(),
412 )
413 if selected not in {row.trial_id for row in rows}:
414 raise DsrError(
415 "dsr_selected_unknown", "selected trial was not read", (selected,)
416 )
417 if selected not in eligible:
418 reason = next(reason for reason, ids in reasons.items() if selected in ids)
419 raise DsrError(
420 "dsr_selected_not_eligible",
421 "selected trial is excluded",
422 (selected,),
423 (reason,),
424 )
425 rows_read = len(rows)
426 if n_trials is not None and n_trials < rows_read:
427 raise DsrError(
428 "dsr_trials_inconsistent",
429 "n_trials is below the number of rows read",
430 names=("n_trials",),
431 )
432 work = rows_read + len(eligible)
433 if max_work is not None and work > max_work:
434 raise DsrError(
435 "dsr_resource_exceeded",
436 f"admission figure {work} is above max_work {max_work}",
437 )
438 n = rows_read if n_trials is None else n_trials
439 values = {identifier: stats[0] for identifier, stats in eligible.items()}
440 v, sr0, den, z, result = _statistic(values, eligible[selected], n, selected)
441 sr, count, skew, kurt = eligible[selected]
442 excluded = {}
443 for reason in sorted(reasons):
444 entry = {"count": len(reasons[reason]), "ids": reasons[reason]}
445 if reason == "returns_status_nonzero":
446 entry["statuses"] = _statuses(status_values)
447 excluded[reason] = entry
448 return {
449 "contract": CONTRACT,
450 "status": "ok",
451 "series": series,
452 "selected": selected,
453 "sharpe_per_period": sr,
454 "count": count,
455 "skew": skew,
456 "kurt_raw": kurt,
457 "n_trials": n,
458 "n_trials_source": "rows_read" if n_trials is None else "caller",
459 "variance_basis": "sample_n_minus_1",
460 "variance": v,
461 "sr0": sr0,
462 "den": den,
463 "z": z,
464 "dsr": result,
465 "selected_is_max": sr == max(values.values()),
466 "rows_read": rows_read,
467 "rows_eligible": len(eligible),
468 "rows_excluded": rows_read - len(eligible),
469 "eligible": list(eligible),
470 "excluded": excluded,
471 "algorithm": ALGORITHM,
472 "input_sha256": _input_digest(rows, names),
473 "interpretation": INTERPRETATION,
474 }
None __init__(self, str code, str detail, Iterable ids=(), Iterable names=())
Definition dsr.py:129
dict document(self)
Definition dsr.py:141