ifieryarrows commited on
Commit
bb3e88a
·
verified ·
1 Parent(s): 0e165f2

Sync from GitHub (tests passed)

Browse files
deep_learning/calibration/conformal.py CHANGED
@@ -33,6 +33,15 @@ def compute_nonconformity(actual: np.ndarray, lower: np.ndarray, upper: np.ndarr
33
  return np.maximum(lower - actual, actual - upper)
34
 
35
 
 
 
 
 
 
 
 
 
 
36
  def rolling_conformal_adjustment(
37
  actual: np.ndarray,
38
  lower: np.ndarray,
 
33
  return np.maximum(lower - actual, actual - upper)
34
 
35
 
36
+ def interval_coverage(actual: np.ndarray, lower: np.ndarray, upper: np.ndarray) -> float:
37
+ actual_np = np.asarray(actual, dtype=np.float64)
38
+ lower_np = np.asarray(lower, dtype=np.float64)
39
+ upper_np = np.asarray(upper, dtype=np.float64)
40
+ if actual_np.size == 0:
41
+ return 0.0
42
+ return float(np.mean((actual_np >= lower_np) & (actual_np <= upper_np)))
43
+
44
+
45
  def rolling_conformal_adjustment(
46
  actual: np.ndarray,
47
  lower: np.ndarray,
deep_learning/config.py CHANGED
@@ -141,9 +141,11 @@ class WeeklyLossConfig:
141
  lambda_dispersion: float = 0.35
142
  lambda_magnitude: float = 0.55
143
  lambda_naive: float = 0.40
144
- lambda_bias: float = 0.17
145
  lambda_directional: float = 0.06
146
  lambda_saturation: float = 0.25
 
 
147
  weekly_median_cap_abs_median_multiple: float = 2.0
148
  weekly_median_cap_mean_abs_multiple: float = 1.6
149
  weekly_median_cap_std_multiple: float = 1.2
 
141
  lambda_dispersion: float = 0.35
142
  lambda_magnitude: float = 0.55
143
  lambda_naive: float = 0.40
144
+ lambda_bias: float = 0.19
145
  lambda_directional: float = 0.06
146
  lambda_saturation: float = 0.25
147
+ lambda_positive_rate: float = 0.20
148
+ lambda_interval: float = 0.15
149
  weekly_median_cap_abs_median_multiple: float = 2.0
150
  weekly_median_cap_mean_abs_multiple: float = 1.6
151
  weekly_median_cap_std_multiple: float = 1.2
deep_learning/models/tft_copper.py CHANGED
@@ -135,7 +135,17 @@ def _weekly_scale_losses(
135
  model_mae = torch.mean(torch.abs(pred_weekly_median - actual_weekly))
136
  zero_mae = actual_abs_mean
137
  naive_relative_loss = torch.relu((model_mae / zero_mae) - 1.0)
138
- bias_loss = torch.abs((pred_weekly_median.mean() - actual_weekly.mean()) / zero_mae)
 
 
 
 
 
 
 
 
 
 
139
 
140
  return {
141
  "dispersion_loss": dispersion_loss,
@@ -147,6 +157,54 @@ def _weekly_scale_losses(
147
  }
148
 
149
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
150
  # ---------------------------------------------------------------------------
151
  # Module-level ASRO loss class (must be at module level for pickle / checkpoint)
152
  # ---------------------------------------------------------------------------
@@ -259,6 +317,8 @@ try:
259
  lambda_naive: float = 0.40,
260
  lambda_bias: float = 0.25,
261
  lambda_saturation: float = 0.25,
 
 
262
  weekly_median_cap: Optional[float] = None,
263
  sharpe_eps: float = 1e-8,
264
  debug_mode: bool = False,
@@ -272,6 +332,8 @@ try:
272
  self.lambda_naive = lambda_naive
273
  self.lambda_bias = lambda_bias
274
  self.lambda_saturation = lambda_saturation
 
 
275
  self.weekly_median_cap = weekly_median_cap
276
  self.sharpe_eps = sharpe_eps
277
  self.debug_mode = debug_mode
@@ -287,6 +349,8 @@ try:
287
  "naive": 0.0,
288
  "bias": 0.0,
289
  "saturation": 0.0,
 
 
290
  "directional": 0.0,
291
  "total": 0.0,
292
  }
@@ -301,6 +365,8 @@ try:
301
  naive_relative_loss: torch.Tensor,
302
  bias_loss: torch.Tensor,
303
  saturation_loss: torch.Tensor,
 
 
304
  directional_loss: torch.Tensor,
305
  total_loss: torch.Tensor,
306
  ) -> None:
@@ -311,6 +377,8 @@ try:
311
  self._component_sums["naive"] += float(naive_relative_loss.detach().mean().cpu())
312
  self._component_sums["bias"] += float(bias_loss.detach().mean().cpu())
313
  self._component_sums["saturation"] += float(saturation_loss.detach().mean().cpu())
 
 
314
  self._component_sums["directional"] += float(directional_loss.detach().mean().cpu())
315
  self._component_sums["total"] += float(total_loss.detach().mean().cpu())
316
  self._component_batches += 1
@@ -327,6 +395,8 @@ try:
327
  "naive_loss_mean": 0.0,
328
  "bias_loss_mean": 0.0,
329
  "saturation_loss_mean": 0.0,
 
 
330
  "directional_loss_mean": 0.0,
331
  "total_loss_mean": 0.0,
332
  "dominant_component": None,
@@ -340,6 +410,8 @@ try:
340
  "naive": self._component_sums["naive"],
341
  "bias": self._component_sums["bias"],
342
  "saturation": self._component_sums["saturation"],
 
 
343
  "directional": self._component_sums["directional"],
344
  }
345
  return {
@@ -351,6 +423,8 @@ try:
351
  "naive_loss_mean": self._component_sums["naive"] / n_batches,
352
  "bias_loss_mean": self._component_sums["bias"] / n_batches,
353
  "saturation_loss_mean": self._component_sums["saturation"] / n_batches,
 
 
354
  "directional_loss_mean": self._component_sums["directional"] / n_batches,
355
  "total_loss_mean": self._component_sums["total"] / n_batches,
356
  "dominant_component": max(components, key=components.get),
@@ -420,6 +494,17 @@ try:
420
  magnitude_loss = scale_losses["magnitude_loss"]
421
  naive_relative_loss = scale_losses["naive_relative_loss"]
422
  bias_loss = scale_losses["bias_loss"]
 
 
 
 
 
 
 
 
 
 
 
423
 
424
  weekly_pred_direction = torch.tanh(pred_weekly_median * 10.0)
425
  weekly_actual_direction = torch.sign(actual_weekly)
@@ -440,6 +525,8 @@ try:
440
  naive_relative_loss = _to_scalar(naive_relative_loss)
441
  bias_loss = _to_scalar(bias_loss)
442
  saturation_loss = _to_scalar(saturation_loss)
 
 
443
  directional_loss = _to_scalar(directional_loss)
444
 
445
  total_loss = (
@@ -450,6 +537,8 @@ try:
450
  + self.lambda_naive * _to_scalar(naive_relative_loss)
451
  + self.lambda_bias * _to_scalar(bias_loss)
452
  + self.lambda_saturation * _to_scalar(saturation_loss)
 
 
453
  + self.lambda_directional * _to_scalar(directional_loss)
454
  )
455
 
@@ -461,6 +550,8 @@ try:
461
  naive_relative_loss,
462
  bias_loss,
463
  saturation_loss,
 
 
464
  directional_loss,
465
  total_loss,
466
  )
@@ -506,11 +597,14 @@ def create_tft_model(
506
  lambda_bias=cfg.weekly_loss.lambda_bias,
507
  lambda_directional=cfg.weekly_loss.lambda_directional,
508
  lambda_saturation=cfg.weekly_loss.lambda_saturation,
 
 
509
  weekly_median_cap=cfg.weekly_loss.weekly_median_cap,
510
  )
511
  logger.info(
512
  "Using weekly ASRO loss | weekly_q=%.2f t1_q=%.2f dispersion=%.2f "
513
  "magnitude=%.2f naive=%.2f bias=%.2f dir=%.2f saturation=%.2f "
 
514
  "median_cap=%s "
515
  "monotonic_transform=true gap_scale=%.3f",
516
  cfg.weekly_loss.lambda_weekly_quantile,
@@ -521,6 +615,8 @@ def create_tft_model(
521
  cfg.weekly_loss.lambda_bias,
522
  cfg.weekly_loss.lambda_directional,
523
  cfg.weekly_loss.lambda_saturation,
 
 
524
  (
525
  f"{cfg.weekly_loss.weekly_median_cap:.6f}"
526
  if cfg.weekly_loss.weekly_median_cap is not None
 
135
  model_mae = torch.mean(torch.abs(pred_weekly_median - actual_weekly))
136
  zero_mae = actual_abs_mean
137
  naive_relative_loss = torch.relu((model_mae / zero_mae) - 1.0)
138
+ mean_gap = (pred_weekly_median.mean() - actual_weekly.mean()) / zero_mae
139
+ median_gap = (
140
+ (pred_weekly_median.median() - actual_weekly.median())
141
+ / actual_abs_median
142
+ )
143
+ bias_loss = (
144
+ torch.abs(mean_gap)
145
+ + 0.50 * torch.abs(median_gap)
146
+ + 0.75 * torch.relu(mean_gap)
147
+ + 0.50 * torch.relu(median_gap)
148
+ )
149
 
150
  return {
151
  "dispersion_loss": dispersion_loss,
 
157
  }
158
 
159
 
160
+ def _weekly_positive_rate_loss(
161
+ pred_weekly_median: torch.Tensor,
162
+ actual_weekly: torch.Tensor,
163
+ temperature: float = 0.01,
164
+ eps: float = 1e-8,
165
+ ) -> torch.Tensor:
166
+ """Penalize batch-level weekly sign-balance drift with a smooth sign proxy."""
167
+ temp = max(float(temperature), eps)
168
+ pred_positive_rate = torch.sigmoid(pred_weekly_median / temp).mean()
169
+ actual_positive_rate = (actual_weekly > 0).float().mean()
170
+ return torch.abs(pred_positive_rate - actual_positive_rate)
171
+
172
+
173
+ def _weekly_interval_undercoverage_loss(
174
+ pred_weekly_quantiles: torch.Tensor,
175
+ actual_weekly: torch.Tensor,
176
+ quantiles: Sequence[float],
177
+ *,
178
+ target_width_ratio: float = 0.70,
179
+ eps: float = 1e-8,
180
+ ) -> torch.Tensor:
181
+ """Penalize weekly PI80 misses and intervals that are too narrow for actual scale."""
182
+ q = list(quantiles)
183
+ q10_idx = q.index(0.10) if 0.10 in q else 1
184
+ q90_idx = q.index(0.90) if 0.90 in q else len(q) - 2
185
+ lower = pred_weekly_quantiles[:, q10_idx]
186
+ upper = pred_weekly_quantiles[:, q90_idx]
187
+ width = torch.clamp(upper - lower, min=0.0)
188
+
189
+ actual_scale = actual_weekly.abs().mean().clamp_min(eps)
190
+ miss_loss = (
191
+ torch.relu(lower - actual_weekly)
192
+ + torch.relu(actual_weekly - upper)
193
+ ).mean() / actual_scale
194
+
195
+ actual_std = actual_weekly.std(unbiased=False).clamp_min(eps)
196
+ width_ratio = width.mean() / (2.56 * actual_std + eps)
197
+ under_width_loss = torch.relu(
198
+ torch.as_tensor(
199
+ float(target_width_ratio),
200
+ device=pred_weekly_quantiles.device,
201
+ dtype=pred_weekly_quantiles.dtype,
202
+ )
203
+ - width_ratio
204
+ ).pow(2)
205
+ return miss_loss + under_width_loss
206
+
207
+
208
  # ---------------------------------------------------------------------------
209
  # Module-level ASRO loss class (must be at module level for pickle / checkpoint)
210
  # ---------------------------------------------------------------------------
 
317
  lambda_naive: float = 0.40,
318
  lambda_bias: float = 0.25,
319
  lambda_saturation: float = 0.25,
320
+ lambda_positive_rate: float = 0.20,
321
+ lambda_interval: float = 0.15,
322
  weekly_median_cap: Optional[float] = None,
323
  sharpe_eps: float = 1e-8,
324
  debug_mode: bool = False,
 
332
  self.lambda_naive = lambda_naive
333
  self.lambda_bias = lambda_bias
334
  self.lambda_saturation = lambda_saturation
335
+ self.lambda_positive_rate = lambda_positive_rate
336
+ self.lambda_interval = lambda_interval
337
  self.weekly_median_cap = weekly_median_cap
338
  self.sharpe_eps = sharpe_eps
339
  self.debug_mode = debug_mode
 
349
  "naive": 0.0,
350
  "bias": 0.0,
351
  "saturation": 0.0,
352
+ "positive_rate": 0.0,
353
+ "interval": 0.0,
354
  "directional": 0.0,
355
  "total": 0.0,
356
  }
 
365
  naive_relative_loss: torch.Tensor,
366
  bias_loss: torch.Tensor,
367
  saturation_loss: torch.Tensor,
368
+ positive_rate_loss: torch.Tensor,
369
+ interval_loss: torch.Tensor,
370
  directional_loss: torch.Tensor,
371
  total_loss: torch.Tensor,
372
  ) -> None:
 
377
  self._component_sums["naive"] += float(naive_relative_loss.detach().mean().cpu())
378
  self._component_sums["bias"] += float(bias_loss.detach().mean().cpu())
379
  self._component_sums["saturation"] += float(saturation_loss.detach().mean().cpu())
380
+ self._component_sums["positive_rate"] += float(positive_rate_loss.detach().mean().cpu())
381
+ self._component_sums["interval"] += float(interval_loss.detach().mean().cpu())
382
  self._component_sums["directional"] += float(directional_loss.detach().mean().cpu())
383
  self._component_sums["total"] += float(total_loss.detach().mean().cpu())
384
  self._component_batches += 1
 
395
  "naive_loss_mean": 0.0,
396
  "bias_loss_mean": 0.0,
397
  "saturation_loss_mean": 0.0,
398
+ "positive_rate_loss_mean": 0.0,
399
+ "interval_loss_mean": 0.0,
400
  "directional_loss_mean": 0.0,
401
  "total_loss_mean": 0.0,
402
  "dominant_component": None,
 
410
  "naive": self._component_sums["naive"],
411
  "bias": self._component_sums["bias"],
412
  "saturation": self._component_sums["saturation"],
413
+ "positive_rate": self._component_sums["positive_rate"],
414
+ "interval": self._component_sums["interval"],
415
  "directional": self._component_sums["directional"],
416
  }
417
  return {
 
423
  "naive_loss_mean": self._component_sums["naive"] / n_batches,
424
  "bias_loss_mean": self._component_sums["bias"] / n_batches,
425
  "saturation_loss_mean": self._component_sums["saturation"] / n_batches,
426
+ "positive_rate_loss_mean": self._component_sums["positive_rate"] / n_batches,
427
+ "interval_loss_mean": self._component_sums["interval"] / n_batches,
428
  "directional_loss_mean": self._component_sums["directional"] / n_batches,
429
  "total_loss_mean": self._component_sums["total"] / n_batches,
430
  "dominant_component": max(components, key=components.get),
 
494
  magnitude_loss = scale_losses["magnitude_loss"]
495
  naive_relative_loss = scale_losses["naive_relative_loss"]
496
  bias_loss = scale_losses["bias_loss"]
497
+ positive_rate_loss = _weekly_positive_rate_loss(
498
+ pred_weekly_median,
499
+ actual_weekly,
500
+ eps=eps,
501
+ )
502
+ interval_loss = _weekly_interval_undercoverage_loss(
503
+ pred_weekly_quantiles,
504
+ actual_weekly,
505
+ self.quantiles,
506
+ eps=eps,
507
+ )
508
 
509
  weekly_pred_direction = torch.tanh(pred_weekly_median * 10.0)
510
  weekly_actual_direction = torch.sign(actual_weekly)
 
525
  naive_relative_loss = _to_scalar(naive_relative_loss)
526
  bias_loss = _to_scalar(bias_loss)
527
  saturation_loss = _to_scalar(saturation_loss)
528
+ positive_rate_loss = _to_scalar(positive_rate_loss)
529
+ interval_loss = _to_scalar(interval_loss)
530
  directional_loss = _to_scalar(directional_loss)
531
 
532
  total_loss = (
 
537
  + self.lambda_naive * _to_scalar(naive_relative_loss)
538
  + self.lambda_bias * _to_scalar(bias_loss)
539
  + self.lambda_saturation * _to_scalar(saturation_loss)
540
+ + self.lambda_positive_rate * _to_scalar(positive_rate_loss)
541
+ + self.lambda_interval * _to_scalar(interval_loss)
542
  + self.lambda_directional * _to_scalar(directional_loss)
543
  )
544
 
 
550
  naive_relative_loss,
551
  bias_loss,
552
  saturation_loss,
553
+ positive_rate_loss,
554
+ interval_loss,
555
  directional_loss,
556
  total_loss,
557
  )
 
597
  lambda_bias=cfg.weekly_loss.lambda_bias,
598
  lambda_directional=cfg.weekly_loss.lambda_directional,
599
  lambda_saturation=cfg.weekly_loss.lambda_saturation,
600
+ lambda_positive_rate=cfg.weekly_loss.lambda_positive_rate,
601
+ lambda_interval=cfg.weekly_loss.lambda_interval,
602
  weekly_median_cap=cfg.weekly_loss.weekly_median_cap,
603
  )
604
  logger.info(
605
  "Using weekly ASRO loss | weekly_q=%.2f t1_q=%.2f dispersion=%.2f "
606
  "magnitude=%.2f naive=%.2f bias=%.2f dir=%.2f saturation=%.2f "
607
+ "positive_rate=%.2f interval=%.2f "
608
  "median_cap=%s "
609
  "monotonic_transform=true gap_scale=%.3f",
610
  cfg.weekly_loss.lambda_weekly_quantile,
 
615
  cfg.weekly_loss.lambda_bias,
616
  cfg.weekly_loss.lambda_directional,
617
  cfg.weekly_loss.lambda_saturation,
618
+ cfg.weekly_loss.lambda_positive_rate,
619
+ cfg.weekly_loss.lambda_interval,
620
  (
621
  f"{cfg.weekly_loss.weekly_median_cap:.6f}"
622
  if cfg.weekly_loss.weekly_median_cap is not None
deep_learning/training/hyperopt.py CHANGED
@@ -71,8 +71,10 @@ KNOWN_GOOD_TRIAL_PARAMS = {
71
  "lambda_dispersion": 0.35,
72
  "lambda_magnitude": 0.55,
73
  "lambda_naive": 0.40,
74
- "lambda_bias": 0.17,
75
  "lambda_directional": 0.06,
 
 
76
  "batch_size": 32,
77
  }
78
 
@@ -419,6 +421,8 @@ def create_trial_config(trial, base_cfg: TFTASROConfig) -> TFTASROConfig:
419
  "lambda_directional",
420
  [0.05, 0.06, 0.07],
421
  ),
 
 
422
  )
423
 
424
  training_cfg = TrainingConfig(
 
71
  "lambda_dispersion": 0.35,
72
  "lambda_magnitude": 0.55,
73
  "lambda_naive": 0.40,
74
+ "lambda_bias": 0.19,
75
  "lambda_directional": 0.06,
76
+ "lambda_positive_rate": 0.20,
77
+ "lambda_interval": 0.15,
78
  "batch_size": 32,
79
  }
80
 
 
421
  "lambda_directional",
422
  [0.05, 0.06, 0.07],
423
  ),
424
+ lambda_positive_rate=0.20,
425
+ lambda_interval=0.15,
426
  )
427
 
428
  training_cfg = TrainingConfig(
deep_learning/training/trainer.py CHANGED
@@ -62,9 +62,11 @@ KNOWN_GOOD_CONFIG = {
62
  "lambda_dispersion": 0.35,
63
  "lambda_magnitude": 0.55,
64
  "lambda_naive": 0.40,
65
- "lambda_bias": 0.17,
66
  "lambda_directional": 0.06,
67
  "lambda_saturation": 0.25,
 
 
68
  "batch_size": 32,
69
  }
70
 
@@ -349,7 +351,8 @@ def train_tft_model(
349
  )
350
  logger.info(
351
  "Weekly loss | weekly_q=%.2f t1_q=%.2f dispersion=%.2f "
352
- "magnitude=%.2f naive=%.2f directional=%.2f saturation=%.2f "
 
353
  "median_cap=%.6f "
354
  "monotonic_transform=true",
355
  cfg.weekly_loss.lambda_weekly_quantile,
@@ -357,8 +360,11 @@ def train_tft_model(
357
  cfg.weekly_loss.lambda_dispersion,
358
  cfg.weekly_loss.lambda_magnitude,
359
  cfg.weekly_loss.lambda_naive,
 
360
  cfg.weekly_loss.lambda_directional,
361
  cfg.weekly_loss.lambda_saturation,
 
 
362
  cfg.weekly_loss.weekly_median_cap or 0.0,
363
  )
364
  else:
@@ -523,6 +529,8 @@ def train_tft_model(
523
  "lambda_bias": cfg.weekly_loss.lambda_bias,
524
  "lambda_directional": cfg.weekly_loss.lambda_directional,
525
  "lambda_saturation": cfg.weekly_loss.lambda_saturation,
 
 
526
  "weekly_median_cap_abs_median_multiple": (
527
  cfg.weekly_loss.weekly_median_cap_abs_median_multiple
528
  ),
@@ -614,7 +622,11 @@ def _write_conformal_calibration_artifact(
614
  try:
615
  import torch
616
 
617
- from deep_learning.calibration.conformal import rolling_conformal_adjustment
 
 
 
 
618
  from deep_learning.training.metrics import (
619
  apply_weekly_median_cap_np,
620
  cumulative_horizon,
@@ -655,26 +667,58 @@ def _write_conformal_calibration_artifact(
655
  q90_idx = q.index(0.90)
656
  raw_lower = weekly_quantiles[:, q10_idx]
657
  raw_upper = weekly_quantiles[:, q90_idx]
658
- validation_pi80_coverage = float(
659
- np.mean((weekly_actual >= raw_lower) & (weekly_actual <= raw_upper))
 
 
 
 
 
 
 
 
 
 
 
660
  )
661
 
662
  calibration_status = "fit"
663
  if validation_pi80_coverage >= 0.90:
664
- global_adj = 0.0
665
  calibration_status = "skipped_interval_already_overcovered"
666
  else:
667
- global_adj = rolling_conformal_adjustment(
668
  weekly_actual,
669
  raw_lower,
670
  raw_upper,
671
  alpha=0.20,
672
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
673
  artifact = {
674
  "generated_at": datetime.now(timezone.utc).isoformat(),
675
  "target": "weekly_5d_log_return",
676
  "alpha": 0.20,
677
  "global_adjustment": float(global_adj),
 
 
678
  "bucket_adjustments": {
679
  "neutral": float(global_adj),
680
  "risk_on": float(global_adj),
@@ -687,6 +731,12 @@ def _write_conformal_calibration_artifact(
687
  "fit_split": "validation",
688
  "test_split_used_for_fit": False,
689
  "validation_pi80_coverage": validation_pi80_coverage,
 
 
 
 
 
 
690
  "calibration_status": calibration_status,
691
  }
692
 
@@ -819,7 +869,8 @@ def _overlay_training_config(cfg: TFTASROConfig, params: dict) -> TFTASROConfig:
819
  k: params[k] for k in (
820
  "lambda_weekly_quantile", "lambda_t1_quantile", "lambda_directional",
821
  "lambda_dispersion", "lambda_magnitude", "lambda_naive", "lambda_bias",
822
- "lambda_saturation", "weekly_median_cap_abs_median_multiple",
 
823
  "weekly_median_cap_mean_abs_multiple", "weekly_median_cap_std_multiple",
824
  ) if k in params
825
  }
 
62
  "lambda_dispersion": 0.35,
63
  "lambda_magnitude": 0.55,
64
  "lambda_naive": 0.40,
65
+ "lambda_bias": 0.19,
66
  "lambda_directional": 0.06,
67
  "lambda_saturation": 0.25,
68
+ "lambda_positive_rate": 0.20,
69
+ "lambda_interval": 0.15,
70
  "batch_size": 32,
71
  }
72
 
 
351
  )
352
  logger.info(
353
  "Weekly loss | weekly_q=%.2f t1_q=%.2f dispersion=%.2f "
354
+ "magnitude=%.2f naive=%.2f bias=%.2f directional=%.2f saturation=%.2f "
355
+ "positive_rate=%.2f interval=%.2f "
356
  "median_cap=%.6f "
357
  "monotonic_transform=true",
358
  cfg.weekly_loss.lambda_weekly_quantile,
 
360
  cfg.weekly_loss.lambda_dispersion,
361
  cfg.weekly_loss.lambda_magnitude,
362
  cfg.weekly_loss.lambda_naive,
363
+ cfg.weekly_loss.lambda_bias,
364
  cfg.weekly_loss.lambda_directional,
365
  cfg.weekly_loss.lambda_saturation,
366
+ cfg.weekly_loss.lambda_positive_rate,
367
+ cfg.weekly_loss.lambda_interval,
368
  cfg.weekly_loss.weekly_median_cap or 0.0,
369
  )
370
  else:
 
529
  "lambda_bias": cfg.weekly_loss.lambda_bias,
530
  "lambda_directional": cfg.weekly_loss.lambda_directional,
531
  "lambda_saturation": cfg.weekly_loss.lambda_saturation,
532
+ "lambda_positive_rate": cfg.weekly_loss.lambda_positive_rate,
533
+ "lambda_interval": cfg.weekly_loss.lambda_interval,
534
  "weekly_median_cap_abs_median_multiple": (
535
  cfg.weekly_loss.weekly_median_cap_abs_median_multiple
536
  ),
 
622
  try:
623
  import torch
624
 
625
+ from deep_learning.calibration.conformal import (
626
+ apply_conformal_interval,
627
+ interval_coverage,
628
+ rolling_conformal_adjustment,
629
+ )
630
  from deep_learning.training.metrics import (
631
  apply_weekly_median_cap_np,
632
  cumulative_horizon,
 
667
  q90_idx = q.index(0.90)
668
  raw_lower = weekly_quantiles[:, q10_idx]
669
  raw_upper = weekly_quantiles[:, q90_idx]
670
+ validation_pi80_coverage = interval_coverage(weekly_actual, raw_lower, raw_upper)
671
+ validation_pi80_width = float(np.mean(raw_upper - raw_lower))
672
+ validation_weekly_std = float(np.std(weekly_actual))
673
+ target_min_pi80_width_ratio = 0.70
674
+ validation_pi80_width_ratio = (
675
+ validation_pi80_width / (2.56 * validation_weekly_std + 1e-8)
676
+ if validation_weekly_std > 1e-12
677
+ else 0.0
678
+ )
679
+ target_min_pi80_width = target_min_pi80_width_ratio * 2.56 * validation_weekly_std
680
+ width_floor_adjustment = max(
681
+ 0.0,
682
+ (target_min_pi80_width - validation_pi80_width) / 2.0,
683
  )
684
 
685
  calibration_status = "fit"
686
  if validation_pi80_coverage >= 0.90:
687
+ conformal_adj = 0.0
688
  calibration_status = "skipped_interval_already_overcovered"
689
  else:
690
+ conformal_adj = rolling_conformal_adjustment(
691
  weekly_actual,
692
  raw_lower,
693
  raw_upper,
694
  alpha=0.20,
695
  )
696
+ global_adj = max(float(conformal_adj), float(width_floor_adjustment))
697
+ if width_floor_adjustment > conformal_adj and width_floor_adjustment > 0.0:
698
+ calibration_status = "fit_width_floor"
699
+ calibrated_lower, calibrated_upper = apply_conformal_interval(
700
+ raw_lower,
701
+ raw_upper,
702
+ global_adj,
703
+ )
704
+ calibrated_validation_pi80_coverage = interval_coverage(
705
+ weekly_actual,
706
+ calibrated_lower,
707
+ calibrated_upper,
708
+ )
709
+ calibrated_validation_pi80_width = float(np.mean(calibrated_upper - calibrated_lower))
710
+ calibrated_validation_pi80_width_ratio = (
711
+ calibrated_validation_pi80_width / (2.56 * validation_weekly_std + 1e-8)
712
+ if validation_weekly_std > 1e-12
713
+ else 0.0
714
+ )
715
  artifact = {
716
  "generated_at": datetime.now(timezone.utc).isoformat(),
717
  "target": "weekly_5d_log_return",
718
  "alpha": 0.20,
719
  "global_adjustment": float(global_adj),
720
+ "base_conformal_adjustment": float(conformal_adj),
721
+ "width_floor_adjustment": float(width_floor_adjustment),
722
  "bucket_adjustments": {
723
  "neutral": float(global_adj),
724
  "risk_on": float(global_adj),
 
731
  "fit_split": "validation",
732
  "test_split_used_for_fit": False,
733
  "validation_pi80_coverage": validation_pi80_coverage,
734
+ "calibrated_validation_pi80_coverage": calibrated_validation_pi80_coverage,
735
+ "validation_pi80_width": validation_pi80_width,
736
+ "calibrated_validation_pi80_width": calibrated_validation_pi80_width,
737
+ "validation_pi80_width_ratio": validation_pi80_width_ratio,
738
+ "calibrated_validation_pi80_width_ratio": calibrated_validation_pi80_width_ratio,
739
+ "target_min_pi80_width_ratio": target_min_pi80_width_ratio,
740
  "calibration_status": calibration_status,
741
  }
742
 
 
869
  k: params[k] for k in (
870
  "lambda_weekly_quantile", "lambda_t1_quantile", "lambda_directional",
871
  "lambda_dispersion", "lambda_magnitude", "lambda_naive", "lambda_bias",
872
+ "lambda_saturation", "lambda_positive_rate", "lambda_interval",
873
+ "weekly_median_cap_abs_median_multiple",
874
  "weekly_median_cap_mean_abs_multiple", "weekly_median_cap_std_multiple",
875
  ) if k in params
876
  }