quantred 2.4.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.
- quantred/__init__.py +24 -0
- quantred/alphas/__init__.py +53 -0
- quantred/alphas/base.py +221 -0
- quantred/alphas/combine.py +260 -0
- quantred/alphas/horizon.py +153 -0
- quantred/alphas/refine.py +300 -0
- quantred/alphas/scorers.py +87 -0
- quantred/analytics/attribution.py +354 -0
- quantred/analytics/bootstrap.py +353 -0
- quantred/analytics/charts.py +393 -0
- quantred/analytics/excursion.py +185 -0
- quantred/analytics/execution_quality.py +180 -0
- quantred/analytics/exposure.py +167 -0
- quantred/analytics/htmlreport.py +1208 -0
- quantred/analytics/information.py +197 -0
- quantred/analytics/metrics.py +502 -0
- quantred/analytics/performance.py +639 -0
- quantred/analytics/reporting.py +1672 -0
- quantred/analytics/screening.py +190 -0
- quantred/analytics/series_comparison.py +303 -0
- quantred/analytics/trade_analytics.py +353 -0
- quantred/brokers/alpaca/broker.py +415 -0
- quantred/brokers/alpaca/factory.py +33 -0
- quantred/brokers/alpaca/market_data.py +166 -0
- quantred/brokers/base.py +220 -0
- quantred/brokers/dryrun.py +164 -0
- quantred/brokers/errors.py +70 -0
- quantred/cli.py +8619 -0
- quantred/costs/__init__.py +11 -0
- quantred/costs/base.py +126 -0
- quantred/costs/parametric.py +131 -0
- quantred/data/__init__.py +29 -0
- quantred/data/compute.py +332 -0
- quantred/data/edges.py +81 -0
- quantred/data/features.py +120 -0
- quantred/data/panel.py +61 -0
- quantred/data/scan.py +85 -0
- quantred/data/store.py +269 -0
- quantred/demo/__init__.py +22 -0
- quantred/demo/scanners.py +104 -0
- quantred/demo/strategies.py +125 -0
- quantred/engine/backtest.py +1206 -0
- quantred/engine/barcheck.py +228 -0
- quantred/engine/live.py +539 -0
- quantred/execution/decision.py +192 -0
- quantred/execution/flatten.py +302 -0
- quantred/execution/halt.py +165 -0
- quantred/execution/ledger.py +758 -0
- quantred/execution/live_trader.py +587 -0
- quantred/execution/order_id.py +83 -0
- quantred/execution/sizing.py +114 -0
- quantred/indicators/indicators.py +158 -0
- quantred/marketdata/base.py +62 -0
- quantred/marketdata/client.py +59 -0
- quantred/marketdata/session.py +112 -0
- quantred/marketdata/synthetic.py +74 -0
- quantred/marketdata/timeframe.py +125 -0
- quantred/mcp/__init__.py +6 -0
- quantred/mcp/selftest.py +139 -0
- quantred/mcp/server.py +2419 -0
- quantred/optimization/causality.py +607 -0
- quantred/optimization/config_store.py +300 -0
- quantred/optimization/optimizer.py +431 -0
- quantred/optimization/parallel.py +345 -0
- quantred/optimization/param_space.py +309 -0
- quantred/optimization/walk_forward.py +1254 -0
- quantred/portfolio/allocator.py +135 -0
- quantred/portfolio/benchmark.py +109 -0
- quantred/portfolio/optimizer.py +786 -0
- quantred/portfolio/policy.py +335 -0
- quantred/portfolio/posterior.py +208 -0
- quantred/research/__init__.py +30 -0
- quantred/research/agent.py +868 -0
- quantred/research/demo_proposals.py +222 -0
- quantred/research/llm.py +311 -0
- quantred/research/proposer.py +234 -0
- quantred/research/provider_errors.py +242 -0
- quantred/research/sandbox.py +340 -0
- quantred/risk/__init__.py +60 -0
- quantred/risk/base.py +218 -0
- quantred/risk/conditional.py +474 -0
- quantred/risk/exposures.py +88 -0
- quantred/risk/factor.py +119 -0
- quantred/risk/sample.py +89 -0
- quantred/risk/streaming.py +214 -0
- quantred/scanners/base.py +141 -0
- quantred/scanners/symbol_scanner.py +148 -0
- quantred/services/__init__.py +51 -0
- quantred/services/analysis.py +6082 -0
- quantred/services/audit.py +574 -0
- quantred/services/campaign.py +381 -0
- quantred/services/configs.py +282 -0
- quantred/services/data.py +172 -0
- quantred/services/dryrun.py +206 -0
- quantred/services/glossary.py +130 -0
- quantred/services/maintenance.py +314 -0
- quantred/services/registry.py +351 -0
- quantred/services/setup.py +1023 -0
- quantred/services/sizing.py +150 -0
- quantred/services/smallreal.py +450 -0
- quantred/settings.py +571 -0
- quantred/store/__init__.py +0 -0
- quantred/store/bars.py +660 -0
- quantred/store/trials.py +1804 -0
- quantred/strategies/base.py +643 -0
- quantred/strategies/signals.py +28 -0
- quantred/utils/errors.py +48 -0
- quantred/utils/extras.py +330 -0
- quantred/utils/logging_config.py +33 -0
- quantred/utils/numeric.py +46 -0
- quantred/utils/streaming.py +474 -0
- quantred/utils/timeutils.py +148 -0
- quantred-2.4.0.dist-info/METADATA +632 -0
- quantred-2.4.0.dist-info/RECORD +117 -0
- quantred-2.4.0.dist-info/WHEEL +4 -0
- quantred-2.4.0.dist-info/entry_points.txt +8 -0
- quantred-2.4.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
"""Bootstrap skill inference: simulate the null instead of assuming it.
|
|
2
|
+
|
|
3
|
+
The rest of the stack's skill-vs-luck machinery is parametric: PSR/DSR
|
|
4
|
+
(:mod:`quantred.analytics.metrics`) assume the Sharpe estimator's asymptotic
|
|
5
|
+
distribution (with skew/kurtosis corrections) and, for DSR, an *assumed* effective
|
|
6
|
+
trial count and trial-Sharpe variance. Those are good, cheap, always-on defaults —
|
|
7
|
+
this module is the heavier, definitive check behind ``--bootstrap-skill``, not a
|
|
8
|
+
replacement.
|
|
9
|
+
|
|
10
|
+
Two questions, two functions:
|
|
11
|
+
|
|
12
|
+
1. **Is this one track record skill?** :func:`bootstrap_null` imposes the null
|
|
13
|
+
(demean by the estimated alpha), block-resamples the residual with the
|
|
14
|
+
Politis-Romano stationary bootstrap (never i.i.d. - active returns are
|
|
15
|
+
autocorrelated), and reports the realized IR's rank in the empirical null
|
|
16
|
+
distribution: an *own* p-value with no assumption about the return
|
|
17
|
+
distribution's shape.
|
|
18
|
+
2. **Is the best of the K configs actually tried skill?** :func:`reality_check`
|
|
19
|
+
(White 2000) resamples the same block indices across every trial's column at
|
|
20
|
+
once, so the cross-trial correlation structure is preserved by construction
|
|
21
|
+
(DSR has to *assume* a trial-Sharpe variance to approximate this; this replays
|
|
22
|
+
the actual trials from the trial store) - the null distribution of "the best
|
|
23
|
+
IR among K correlated tries," compared against the actual observed best.
|
|
24
|
+
|
|
25
|
+
Both report a Monte-Carlo standard error on the p-value (``sqrt(p(1-p)/B)``) and a
|
|
26
|
+
block-length sensitivity check (p at L/2 and 2L) - a p-value that flips across that
|
|
27
|
+
range is not a result.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
import math
|
|
31
|
+
from typing import Any, Dict, Optional, Sequence, Union
|
|
32
|
+
|
|
33
|
+
import numpy as np
|
|
34
|
+
|
|
35
|
+
from quantred.analytics.metrics import TRADING_DAYS_PER_YEAR
|
|
36
|
+
|
|
37
|
+
Numbers = Union[Sequence[float], np.ndarray]
|
|
38
|
+
|
|
39
|
+
__all__ = [
|
|
40
|
+
"politis_white_block_length",
|
|
41
|
+
"stationary_bootstrap_indices",
|
|
42
|
+
"bootstrap_null",
|
|
43
|
+
"reality_check",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _as_array(values: Numbers) -> np.ndarray:
|
|
48
|
+
return np.asarray(values, dtype=float)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _annualized_ir(x: np.ndarray, periods_per_year: float, axis: int = -1) -> np.ndarray:
|
|
52
|
+
"""``mean/std * sqrt(periods_per_year)`` along ``axis``; zero-std slices get IR
|
|
53
|
+
0 (never inf/nan - a null distribution must stay finite to rank against)."""
|
|
54
|
+
mean = x.mean(axis=axis)
|
|
55
|
+
std = x.std(axis=axis, ddof=1)
|
|
56
|
+
with np.errstate(invalid="ignore", divide="ignore"):
|
|
57
|
+
ir = np.where(std > 0, mean / std * math.sqrt(periods_per_year), 0.0)
|
|
58
|
+
return np.asarray(ir, dtype=float)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# --------------------------------------------------------------------------- #
|
|
62
|
+
# Block-length selection (Politis & White 2004 / Patton, Politis & White 2009)
|
|
63
|
+
# --------------------------------------------------------------------------- #
|
|
64
|
+
def politis_white_block_length(returns: Numbers, *, max_lag: Optional[int] = None) -> float:
|
|
65
|
+
"""The automatic expected block length for the stationary bootstrap.
|
|
66
|
+
|
|
67
|
+
Estimates the series' own decay time from its correlogram (a flat-top lag
|
|
68
|
+
window over the sample autocorrelations), then sets the MSE-optimal average
|
|
69
|
+
block length for the stationary bootstrap (Politis & White 2004 eq. 4.1-4.2,
|
|
70
|
+
the ``D_SB = 2*Ghat**2`` variant). Too short a block leaks autocorrelation into
|
|
71
|
+
the resample and makes the p-value anti-conservative - the dangerous direction
|
|
72
|
+
- so this is the *reported*, always-overridable
|
|
73
|
+
default, never a silent assumption.
|
|
74
|
+
|
|
75
|
+
This is an engineering-faithful implementation of the published rule (the same
|
|
76
|
+
flat-top kernel, the same "insignificant for K_n consecutive lags" bandwidth
|
|
77
|
+
rule) - not a byte-for-byte port of any particular reference implementation.
|
|
78
|
+
No test in this codebase depends on matching one exactly, only on the
|
|
79
|
+
calibration/power properties the block length is supposed to buy (see
|
|
80
|
+
``tests/test_bootstrap_skill.py``).
|
|
81
|
+
"""
|
|
82
|
+
r = _as_array(returns)
|
|
83
|
+
n = len(r)
|
|
84
|
+
if n < 20:
|
|
85
|
+
# Too short to estimate a correlogram meaningfully; fall back to a quarter
|
|
86
|
+
# of the sample, floored at 1 - a conservative (long) block for tiny n.
|
|
87
|
+
return max(float(n) / 4.0, 1.0)
|
|
88
|
+
|
|
89
|
+
r = r - r.mean()
|
|
90
|
+
var = float(np.dot(r, r) / n)
|
|
91
|
+
if var <= 0:
|
|
92
|
+
return 1.0
|
|
93
|
+
|
|
94
|
+
k_max = max_lag or min(n // 2, 100)
|
|
95
|
+
rho = np.ones(k_max + 1, dtype=float)
|
|
96
|
+
for k in range(1, k_max + 1):
|
|
97
|
+
rho[k] = float(np.dot(r[: n - k], r[k:]) / n) / var
|
|
98
|
+
|
|
99
|
+
# Bandwidth m: the first lag beyond which the correlogram is "insignificant"
|
|
100
|
+
# for K_n consecutive lags (Politis & White's own significance band, not a
|
|
101
|
+
# generic 95% CI).
|
|
102
|
+
k_n = max(5, int(math.ceil(math.sqrt(math.log10(n)))))
|
|
103
|
+
band = 2.0 * math.sqrt(math.log10(n) / n)
|
|
104
|
+
m = k_max
|
|
105
|
+
for k in range(1, max(k_max - k_n + 1, 2)):
|
|
106
|
+
if np.all(np.abs(rho[k : k + k_n]) < band):
|
|
107
|
+
m = k
|
|
108
|
+
break
|
|
109
|
+
bandwidth = int(np.clip(2 * m, 1, k_max))
|
|
110
|
+
|
|
111
|
+
def _flat_top(x: float) -> float:
|
|
112
|
+
ax = abs(x)
|
|
113
|
+
if ax <= 0.5:
|
|
114
|
+
return 1.0
|
|
115
|
+
if ax <= 1.0:
|
|
116
|
+
return 2.0 * (1.0 - ax)
|
|
117
|
+
return 0.0
|
|
118
|
+
|
|
119
|
+
g_hat = 0.0
|
|
120
|
+
cap_g_hat = rho[0]
|
|
121
|
+
for k in range(1, bandwidth + 1):
|
|
122
|
+
w = _flat_top(k / bandwidth)
|
|
123
|
+
g_hat += 2.0 * w * k * rho[k]
|
|
124
|
+
cap_g_hat += 2.0 * w * rho[k]
|
|
125
|
+
|
|
126
|
+
if cap_g_hat <= 0:
|
|
127
|
+
return float(max(bandwidth, 1))
|
|
128
|
+
d_sb = 2.0 * cap_g_hat**2
|
|
129
|
+
if d_sb <= 0:
|
|
130
|
+
return float(max(bandwidth, 1))
|
|
131
|
+
b = ((2.0 * g_hat**2) / d_sb) ** (1.0 / 3.0) * n ** (1.0 / 3.0)
|
|
132
|
+
return float(np.clip(b, 1.0, max(n // 4, 1)))
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
# --------------------------------------------------------------------------- #
|
|
136
|
+
# Stationary (Politis-Romano 1994) block bootstrap
|
|
137
|
+
# --------------------------------------------------------------------------- #
|
|
138
|
+
def stationary_bootstrap_indices(t: int, block_length: float, b: int, rng: np.random.Generator) -> np.ndarray:
|
|
139
|
+
"""``B`` rows of ``T`` circular resample indices with geometric block lengths
|
|
140
|
+
(expected length ``block_length``), Politis & Romano's (1994) stationary
|
|
141
|
+
bootstrap.
|
|
142
|
+
|
|
143
|
+
Implemented as its equivalent recurrence: at each step, "restart" to a fresh
|
|
144
|
+
uniform index with probability ``p = 1/block_length``, else continue the
|
|
145
|
+
previous index + 1 (mod T) - a geometric run length with mean
|
|
146
|
+
``block_length``. Vectorized over ``B`` (one restart draw per time step, not
|
|
147
|
+
per ``(round, step)`` pair): the only large array actually materialized is the
|
|
148
|
+
``(B, T)`` index gather this feeds, plus a ``T``-length loop of ``B``-wide
|
|
149
|
+
numpy ops.
|
|
150
|
+
"""
|
|
151
|
+
if t <= 0:
|
|
152
|
+
return np.empty((b, 0), dtype=np.int64)
|
|
153
|
+
p = 1.0 / max(float(block_length), 1.0)
|
|
154
|
+
idx = np.empty((t, b), dtype=np.int64)
|
|
155
|
+
idx[0] = rng.integers(0, t, size=b)
|
|
156
|
+
if t > 1:
|
|
157
|
+
restarts = rng.random((t - 1, b)) < p
|
|
158
|
+
fresh = rng.integers(0, t, size=(t - 1, b))
|
|
159
|
+
for step in range(1, t):
|
|
160
|
+
prev = idx[step - 1]
|
|
161
|
+
idx[step] = np.where(restarts[step - 1], fresh[step - 1], (prev + 1) % t)
|
|
162
|
+
return idx.T # (B, T)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _p_value(null: np.ndarray, observed: float, b: int) -> "tuple[float, float]":
|
|
166
|
+
"""One-sided p = P(null >= observed), with the standard +1 continuity
|
|
167
|
+
correction (never reports p=0 from a finite B) and its Monte-Carlo SE."""
|
|
168
|
+
p = float((1 + np.sum(null >= observed)) / (b + 1))
|
|
169
|
+
se = float(math.sqrt(max(p * (1.0 - p), 0.0) / b))
|
|
170
|
+
return p, se
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _flips_significance(p: float, p_half: float, p_double: float, level: float = 0.05) -> bool:
|
|
174
|
+
sig = p < level
|
|
175
|
+
return (sig != (p_half < level)) or (sig != (p_double < level))
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
# --------------------------------------------------------------------------- #
|
|
179
|
+
# 1. Single track record: zero-alpha bootstrap
|
|
180
|
+
# --------------------------------------------------------------------------- #
|
|
181
|
+
def bootstrap_null(
|
|
182
|
+
returns: Numbers,
|
|
183
|
+
*,
|
|
184
|
+
B: int = 2000,
|
|
185
|
+
block_length: Optional[float] = None,
|
|
186
|
+
seed: int = 0,
|
|
187
|
+
periods_per_year: float = TRADING_DAYS_PER_YEAR,
|
|
188
|
+
alpha: Optional[float] = None,
|
|
189
|
+
) -> Dict[str, Any]:
|
|
190
|
+
"""Zero-alpha stationary block bootstrap for one active-return track record.
|
|
191
|
+
|
|
192
|
+
Demeans by the full-sample estimated alpha (Fama-French: ``r_tilde = r_a -
|
|
193
|
+
alpha_hat``; ``alpha_hat`` defaults to the sample mean - pass ``alpha=`` for a
|
|
194
|
+
regression-estimated intercept instead) to impose H0: no skill, block-resamples
|
|
195
|
+
the residual ``B`` times (stationary bootstrap; never the equity curve, never
|
|
196
|
+
an i.i.d. shuffle - active returns are autocorrelated), recomputes the
|
|
197
|
+
annualized IR each round -> the empirical null distribution. The p-value is
|
|
198
|
+
one-sided (``P(null IR >= observed IR)``): the hypothesis under test is
|
|
199
|
+
"skill", so a negative track record correctly returns a high p (no evidence of
|
|
200
|
+
skill needed to explain a loss).
|
|
201
|
+
|
|
202
|
+
Reports the Monte-Carlo SE of p, the block length used (default:
|
|
203
|
+
:func:`politis_white_block_length`, always overridable and always reported),
|
|
204
|
+
and block-length sensitivity at L/2 and 2L (a p whose
|
|
205
|
+
significance flips across that range is not a result, per
|
|
206
|
+
``block_sensitivity_flag``).
|
|
207
|
+
"""
|
|
208
|
+
r = _as_array(returns)
|
|
209
|
+
t = len(r)
|
|
210
|
+
if t < 8:
|
|
211
|
+
return {
|
|
212
|
+
"ir_observed": 0.0,
|
|
213
|
+
"p_value": 1.0,
|
|
214
|
+
"p_se": 0.0,
|
|
215
|
+
"B": 0,
|
|
216
|
+
"block_length": 0.0,
|
|
217
|
+
"seed": seed,
|
|
218
|
+
"n_obs": t,
|
|
219
|
+
"block_sensitivity": {},
|
|
220
|
+
"block_sensitivity_flag": False,
|
|
221
|
+
"insufficient_data": True,
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
a_hat = float(r.mean()) if alpha is None else float(alpha)
|
|
225
|
+
ir_observed = float(_annualized_ir(r, periods_per_year))
|
|
226
|
+
l_default = float(block_length) if block_length is not None else politis_white_block_length(r)
|
|
227
|
+
resid = r - a_hat
|
|
228
|
+
|
|
229
|
+
def _run(length: float):
|
|
230
|
+
rng = np.random.default_rng(seed)
|
|
231
|
+
idx = stationary_bootstrap_indices(t, length, B, rng)
|
|
232
|
+
resampled = resid[idx] # (B, T)
|
|
233
|
+
null_ir = _annualized_ir(resampled, periods_per_year, axis=-1)
|
|
234
|
+
p, se = _p_value(null_ir, ir_observed, B)
|
|
235
|
+
return p, se, null_ir
|
|
236
|
+
|
|
237
|
+
p_value, p_se, null_ir = _run(l_default)
|
|
238
|
+
p_half, _, _ = _run(max(l_default / 2.0, 1.0))
|
|
239
|
+
p_double, _, _ = _run(l_default * 2.0)
|
|
240
|
+
|
|
241
|
+
return {
|
|
242
|
+
"ir_observed": ir_observed,
|
|
243
|
+
"p_value": p_value,
|
|
244
|
+
"p_se": p_se,
|
|
245
|
+
"B": B,
|
|
246
|
+
"block_length": l_default,
|
|
247
|
+
"seed": seed,
|
|
248
|
+
"n_obs": t,
|
|
249
|
+
"alpha_hat": a_hat,
|
|
250
|
+
"null_ir_mean": float(np.mean(null_ir)),
|
|
251
|
+
"null_ir_std": float(np.std(null_ir, ddof=1)) if B > 1 else 0.0,
|
|
252
|
+
"block_sensitivity": {"half": p_half, "double": p_double},
|
|
253
|
+
"block_sensitivity_flag": _flips_significance(p_value, p_half, p_double),
|
|
254
|
+
"insufficient_data": False,
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
# --------------------------------------------------------------------------- #
|
|
259
|
+
# 2. Best-of-K: White's Reality Check
|
|
260
|
+
# --------------------------------------------------------------------------- #
|
|
261
|
+
def reality_check(
|
|
262
|
+
trial_returns_matrix: Any,
|
|
263
|
+
*,
|
|
264
|
+
B: int = 2000,
|
|
265
|
+
block_length: Optional[float] = None,
|
|
266
|
+
seed: int = 0,
|
|
267
|
+
periods_per_year: float = TRADING_DAYS_PER_YEAR,
|
|
268
|
+
trial_ids: Optional[Sequence[Any]] = None,
|
|
269
|
+
) -> Dict[str, Any]:
|
|
270
|
+
"""White's (2000) Reality Check over the T x K joint panel of trials' OOS
|
|
271
|
+
return series.
|
|
272
|
+
|
|
273
|
+
Every trial column is demeaned by its OWN mean (impose H0 independently per
|
|
274
|
+
trial - each trial's own null is "no skill for that config"), then resampled
|
|
275
|
+
with ONE set of block indices per round applied to *every* column at once -
|
|
276
|
+
cross-trial correlation is preserved by construction, which is the entire
|
|
277
|
+
point: DSR has to *assume* a trial-Sharpe variance to approximate this: this
|
|
278
|
+
replays the actual trials. The per-round max IR across K columns is the null
|
|
279
|
+
distribution of "the best IR among K correlated tries"; the family p-value is
|
|
280
|
+
the rank of the actual observed max REAL (non-resampled) IR in it - the
|
|
281
|
+
question a researcher who tried K configs and kept the best actually faces.
|
|
282
|
+
|
|
283
|
+
Reports which trial achieved the observed max (``selected_trial``), every
|
|
284
|
+
trial's own real IR, the Monte-Carlo SE of the family p, and the same
|
|
285
|
+
block-length sensitivity check as :func:`bootstrap_null`.
|
|
286
|
+
"""
|
|
287
|
+
m = np.asarray(trial_returns_matrix, dtype=float)
|
|
288
|
+
if m.ndim != 2 or m.shape[0] == 0 or m.shape[1] == 0:
|
|
289
|
+
return {
|
|
290
|
+
"family_p": 1.0,
|
|
291
|
+
"family_p_se": 0.0,
|
|
292
|
+
"B": 0,
|
|
293
|
+
"k_trials": 0 if m.ndim != 2 else int(m.shape[1]),
|
|
294
|
+
"block_sensitivity": {},
|
|
295
|
+
"block_sensitivity_flag": False,
|
|
296
|
+
"insufficient_data": True,
|
|
297
|
+
}
|
|
298
|
+
t, k = m.shape
|
|
299
|
+
if t < 8:
|
|
300
|
+
return {
|
|
301
|
+
"family_p": 1.0,
|
|
302
|
+
"family_p_se": 0.0,
|
|
303
|
+
"B": 0,
|
|
304
|
+
"k_trials": k,
|
|
305
|
+
"n_obs": t,
|
|
306
|
+
"block_sensitivity": {},
|
|
307
|
+
"block_sensitivity_flag": False,
|
|
308
|
+
"insufficient_data": True,
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
real_ir = _annualized_ir(m, periods_per_year, axis=0) # (K,)
|
|
312
|
+
best_idx = int(np.argmax(real_ir))
|
|
313
|
+
observed_max_ir = float(real_ir[best_idx])
|
|
314
|
+
ids = list(trial_ids) if trial_ids is not None else list(range(k))
|
|
315
|
+
|
|
316
|
+
resid = m - m.mean(axis=0, keepdims=True)
|
|
317
|
+
l_default = (
|
|
318
|
+
float(block_length) if block_length is not None else politis_white_block_length(m[:, best_idx])
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
def _run(length: float):
|
|
322
|
+
rng = np.random.default_rng(seed)
|
|
323
|
+
idx = stationary_bootstrap_indices(t, length, B, rng) # (B, T)
|
|
324
|
+
resampled = resid[
|
|
325
|
+
idx
|
|
326
|
+
] # (B, T, K) - one gather, one set of block indices/round, shared by every column
|
|
327
|
+
ir = _annualized_ir(resampled, periods_per_year, axis=1) # (B, K)
|
|
328
|
+
null_max = ir.max(axis=1) # (B,)
|
|
329
|
+
p, se = _p_value(null_max, observed_max_ir, B)
|
|
330
|
+
return p, se, null_max
|
|
331
|
+
|
|
332
|
+
family_p, family_p_se, null_max = _run(l_default)
|
|
333
|
+
p_half, _, _ = _run(max(l_default / 2.0, 1.0))
|
|
334
|
+
p_double, _, _ = _run(l_default * 2.0)
|
|
335
|
+
|
|
336
|
+
return {
|
|
337
|
+
"family_p": family_p,
|
|
338
|
+
"family_p_se": family_p_se,
|
|
339
|
+
"B": B,
|
|
340
|
+
"block_length": l_default,
|
|
341
|
+
"seed": seed,
|
|
342
|
+
"k_trials": k,
|
|
343
|
+
"n_obs": t,
|
|
344
|
+
"observed_max_ir": observed_max_ir,
|
|
345
|
+
"selected_trial": ids[best_idx],
|
|
346
|
+
"selected_trial_index": best_idx,
|
|
347
|
+
"per_trial_real_ir": {ids[i]: float(real_ir[i]) for i in range(k)},
|
|
348
|
+
"null_max_ir_mean": float(np.mean(null_max)),
|
|
349
|
+
"null_max_ir_std": float(np.std(null_max, ddof=1)) if B > 1 else 0.0,
|
|
350
|
+
"block_sensitivity": {"half": p_half, "double": p_double},
|
|
351
|
+
"block_sensitivity_flag": _flips_significance(family_p, p_half, p_double),
|
|
352
|
+
"insufficient_data": False,
|
|
353
|
+
}
|