dfm-python 0.6.13__py3-none-any.whl → 0.7.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.
Files changed (48) hide show
  1. dfm_python/__init__.py +11 -2
  2. dfm_python/config/__init__.py +6 -6
  3. dfm_python/config/constants.py +90 -0
  4. dfm_python/config/schema/__init__.py +6 -6
  5. dfm_python/config/schema/model.py +403 -1
  6. dfm_python/config/schema/params.py +218 -0
  7. dfm_python/config/schema/results.py +231 -84
  8. dfm_python/dataset/__init__.py +4 -0
  9. dfm_python/dataset/afm_dataset.py +119 -0
  10. dfm_python/dataset/ddfm_dataset.py +21 -0
  11. dfm_python/dataset/ivdfm_dataset.py +427 -0
  12. dfm_python/functional/krylov.py +2 -61
  13. dfm_python/layer/mixing.py +37 -0
  14. dfm_python/layer/mlp.py +38 -19
  15. dfm_python/layer/pca.py +46 -0
  16. dfm_python/layer/regime.py +53 -0
  17. dfm_python/logger/__init__.py +8 -0
  18. dfm_python/logger/ivdfm_logger.py +92 -0
  19. dfm_python/models/__init__.py +10 -0
  20. dfm_python/models/afm/__init__.py +23 -0
  21. dfm_python/models/afm/afm.py +385 -0
  22. dfm_python/models/afm/baselines.py +72 -0
  23. dfm_python/models/afm/factors.py +192 -0
  24. dfm_python/models/afm/longconv.py +97 -0
  25. dfm_python/models/base.py +15 -0
  26. dfm_python/models/ddfm/ddfm.py +17 -8
  27. dfm_python/models/ivdfm/__init__.py +18 -0
  28. dfm_python/models/ivdfm/decoder.py +85 -0
  29. dfm_python/models/ivdfm/encoder.py +198 -0
  30. dfm_python/models/ivdfm/ivdfm.py +1526 -396
  31. dfm_python/models/ivdfm/optuna.py +647 -0
  32. dfm_python/models/ivdfm/prior.py +633 -0
  33. dfm_python/numeric/builder.py +247 -0
  34. dfm_python/numeric/estimator.py +78 -9
  35. dfm_python/numeric/validator.py +5 -0
  36. dfm_python/ssm/__init__.py +6 -0
  37. dfm_python/ssm/base.py +208 -0
  38. dfm_python/ssm/companion.py +500 -0
  39. dfm_python/ssm/kalman.py +100 -3
  40. dfm_python/ssm/residual.py +0 -0
  41. dfm_python/utils/loss.py +153 -159
  42. dfm_python-0.7.0.dist-info/METADATA +186 -0
  43. dfm_python-0.7.0.dist-info/RECORD +75 -0
  44. {dfm_python-0.6.13.dist-info → dfm_python-0.7.0.dist-info}/WHEEL +1 -1
  45. dfm_python-0.7.0.dist-info/licenses/LICENSE +21 -0
  46. dfm_python/functional/cauchy.py +0 -236
  47. dfm_python-0.6.13.dist-info/METADATA +0 -56
  48. dfm_python-0.6.13.dist-info/RECORD +0 -61
dfm_python/__init__.py CHANGED
@@ -63,7 +63,7 @@ Note: DFMConfig uses frequency dict to specify series (column names -> frequenci
63
63
  For detailed documentation, see the README.md file and the tutorial notebooks/scripts.
64
64
  """
65
65
 
66
- __version__ = "0.6.13"
66
+ __version__ = "0.7.0"
67
67
 
68
68
  # ============================================================================
69
69
  # PUBLIC API DEFINITION
@@ -98,6 +98,7 @@ from .utils.metric import calculate_rmse
98
98
  # Users can import these directly from dfm_python
99
99
  from .dataset.dfm_dataset import DFMDataset
100
100
  from .dataset.ddfm_dataset import DDFMDataset
101
+ from .dataset.afm_dataset import AFMDataset
101
102
 
102
103
  # Model implementations
103
104
  from .models.base import BaseFactorModel
@@ -109,6 +110,12 @@ try:
109
110
  except ImportError:
110
111
  DDFM = None
111
112
 
113
+ # AFM high-level API (Attention Factor Model; PyTorch is mandatory)
114
+ try:
115
+ from .models.afm.afm import AFM
116
+ except ImportError:
117
+ AFM = None
118
+
112
119
  __all__ = [
113
120
  # Core classes
114
121
  'DFM',
@@ -121,14 +128,16 @@ __all__ = [
121
128
  'BaseResult', 'DFMResult', 'DDFMResult', 'calculate_rmse',
122
129
  ]
123
130
 
124
- # DDFM high-level API (PyTorch is mandatory)
131
+ # DDFM / AFM high-level API (PyTorch is mandatory)
125
132
  __all__.extend([
126
133
  'DDFM', # High-level API class
134
+ 'AFM', # Attention Factor Model
127
135
  ])
128
136
 
129
137
  # Dataset classes
130
138
  __all__.extend([
131
139
  'DFMDataset', # DFM Dataset class
132
140
  'DDFMDataset', # DDFM Dataset class
141
+ 'AFMDataset', # AFM Dataset class
133
142
  ])
134
143
 
@@ -8,9 +8,9 @@ Note: Series are specified via frequency dict mapping column names to frequencie
8
8
  """
9
9
 
10
10
  from .schema import (
11
- BaseModelConfig, DFMConfig, DDFMConfig,
12
- BaseResult, DFMResult, DDFMResult,
13
- DFMStateSpaceParams, DDFMStateSpaceParams,
11
+ BaseModelConfig, DFMConfig, DDFMConfig, AFMConfig,
12
+ BaseResult, DFMResult, DDFMResult, AFMResult,
13
+ DFMStateSpaceParams, DDFMStateSpaceParams, AFMModelState,
14
14
  )
15
15
  # DEFAULT_BLOCK_NAME is imported lazily where needed to avoid circular imports
16
16
  from .constants import (
@@ -97,11 +97,11 @@ __all__ = [
97
97
  'BaseModelConfig', 'BaseResult',
98
98
  # 'DEFAULT_BLOCK_NAME', # Removed to avoid circular import
99
99
  # Model-specific configs (from schema.py)
100
- 'DFMConfig', 'DDFMConfig',
100
+ 'DFMConfig', 'DDFMConfig', 'AFMConfig',
101
101
  # State-space parameters
102
- 'DFMStateSpaceParams', 'DDFMStateSpaceParams',
102
+ 'DFMStateSpaceParams', 'DDFMStateSpaceParams', 'AFMModelState',
103
103
  # Model-specific results (from results.py)
104
- 'DFMResult', 'DDFMResult',
104
+ 'DFMResult', 'DDFMResult', 'AFMResult',
105
105
  # Utilities
106
106
  'validate_frequency',
107
107
  # IO
@@ -344,6 +344,53 @@ COMPUTATION_ERROR_TYPES = (RuntimeError, ValueError, TypeError, AttributeError,
344
344
  DEFAULT_KDFM_AR_ORDER = 1 # Default AR order (VAR lag order p) for KDFM
345
345
  DEFAULT_KDFM_MA_ORDER = 0 # Default MA order (MA lag order q) for KDFM (0 = pure VAR)
346
346
 
347
+ # ============================================================================
348
+ # iVDFM Defaults
349
+ # ============================================================================
350
+
351
+ # iVDFM dimension defaults
352
+ DEFAULT_IVDFM_LATENT_DIM = 3 # Default number of factors
353
+ DEFAULT_IVDFM_AUX_DIM = 1 # Default auxiliary variable dimension
354
+
355
+ # iVDFM regime (K=1 = baseline iVDFM, no regime structure)
356
+ DEFAULT_IVDFM_NUM_REGIMES = 1 # Number of discrete regimes; 1 = baseline iVDFM
357
+ DEFAULT_IVDFM_REGIME_TEMPERATURE = 1.0 # Softmax temperature for π; τ < 1 sharpens (local commitment)
358
+ DEFAULT_IVDFM_MIXING = False # If True, apply global post-factor mixing f -> M f (identity init) before decode
359
+
360
+ # iVDFM network architecture defaults
361
+ DEFAULT_IVDFM_ENCODER_HIDDEN_DIM = 200 # Default encoder hidden dimension
362
+ DEFAULT_IVDFM_ENCODER_N_HIDDEN_LAYERS = 2 # Default encoder hidden layers
363
+ DEFAULT_IVDFM_DECODER_TYPE = "mlp" # Default: 'linear', 'residual', or 'mlp' (mlp = current behavior)
364
+ DEFAULT_IVDFM_DECODER_HIDDEN_DIM = 200 # Default decoder hidden dimension
365
+ DEFAULT_IVDFM_DECODER_N_HIDDEN_LAYERS = 2 # Default decoder hidden layers
366
+ DEFAULT_IVDFM_PRIOR_HIDDEN_DIM = 100 # Default prior network hidden dimension
367
+ DEFAULT_IVDFM_PRIOR_N_HIDDEN_LAYERS = 1 # Default prior network hidden layers
368
+
369
+ # iVDFM dynamics and distribution defaults
370
+ DEFAULT_IVDFM_FACTOR_ORDER = 1 # Default AR order for factors
371
+ DEFAULT_IVDFM_INNOVATION_DIST = 'laplace' # Default innovation distribution
372
+ DEFAULT_IVDFM_DECODER_VAR = 0.01 # Default decoder variance
373
+ DEFAULT_IVDFM_ACTIVATION = 'lrelu' # Default activation function
374
+ DEFAULT_IVDFM_SLOPE = 0.1 # Default leaky ReLU slope
375
+
376
+ # iVDFM training defaults
377
+ DEFAULT_IVDFM_BATCH_SIZE = 32 # Default batch size
378
+ DEFAULT_IVDFM_MAX_EPOCHS = 100 # Default maximum epochs
379
+ DEFAULT_IVDFM_AUX_VARIABLE_TYPE = 'time' # Default auxiliary variable type
380
+
381
+ # iVDFM optimizer defaults
382
+ DEFAULT_IVDFM_OPTIMIZER_WEIGHT_DECAY = 0.0 # Default weight decay for optimizer
383
+ DEFAULT_IVDFM_OPTIMIZER_MOMENTUM = 0.9 # Default momentum for SGD
384
+
385
+ # iVDFM scheduler defaults
386
+ DEFAULT_IVDFM_SCHEDULER_TYPE = 'step' # Default scheduler type: 'step', 'plateau', 'cosine', 'exponential', None
387
+ DEFAULT_IVDFM_SCHEDULER_STEP_SIZE = None # Default step_size for StepLR (auto: max_epochs // 3)
388
+ DEFAULT_IVDFM_SCHEDULER_GAMMA = 0.5 # Default gamma for StepLR/ExponentialLR
389
+ DEFAULT_IVDFM_SCHEDULER_PATIENCE = 10 # Default patience for ReduceLROnPlateau
390
+ DEFAULT_IVDFM_SCHEDULER_FACTOR = 0.1 # Default factor for ReduceLROnPlateau
391
+ DEFAULT_IVDFM_SCHEDULER_MIN_LR = 0.0 # Default min_lr for ReduceLROnPlateau
392
+ DEFAULT_IVDFM_EARLY_STOP_PATIENCE = 20 # Default patience for early stopping (epochs without improvement)
393
+
347
394
  # ============================================================================
348
395
  # Tutorial Defaults
349
396
  # ============================================================================
@@ -410,11 +457,35 @@ FREQUENCY_HIERARCHY: Dict[str, int] = {
410
457
  # For frequency gaps larger than this, the missing data approach is used instead
411
458
  MAX_TENT_SIZE: int = 12
412
459
 
460
+ # ============================================================================
461
+ # AFM (Attention Factor Model) defaults
462
+ # ============================================================================
463
+ DEFAULT_AFM_NUM_FACTORS = 5 # K latent factors
464
+ DEFAULT_AFM_FACTOR_MODEL = 'attention' # 'attention' | 'pca'
465
+ DEFAULT_AFM_TRADING = 'longconv' # 'longconv' | 'ou'
466
+ DEFAULT_AFM_EMBED_DIM = 32 # characteristic embedding dimension d
467
+ DEFAULT_AFM_HIST_LEN = 20 # residual-history length s for the filter
468
+ DEFAULT_AFM_N_KERNELS = 32 # number of long convolutions
469
+ DEFAULT_AFM_RIDGE = 1e-2 # ridge penalty for closed-form loadings
470
+ DEFAULT_AFM_SQUASH_LAMBDA = 1e-3 # LongConv Squash kernel regularizer strength
471
+ DEFAULT_AFM_PCA_WINDOW = 252 # trailing window for the PCA benchmark
472
+ DEFAULT_AFM_REESTIM = 21 # PCA re-estimation stride
473
+ DEFAULT_AFM_LAMBDA_VAR = 0.1 # explained-variance regularizer weight
474
+ DEFAULT_AFM_TURNOVER_COST = 5e-4 # 5bps per unit turnover (paper)
475
+ DEFAULT_AFM_SHORT_COST = 1e-4 # 1bp shorting cost (paper)
476
+ DEFAULT_AFM_MAX_EPOCHS = 50 # training epochs
477
+
413
478
  # ============================================================================
414
479
  # Export all constants
415
480
  # ============================================================================
416
481
 
417
482
  __all__ = [
483
+ # AFM
484
+ 'DEFAULT_AFM_NUM_FACTORS', 'DEFAULT_AFM_FACTOR_MODEL', 'DEFAULT_AFM_TRADING',
485
+ 'DEFAULT_AFM_EMBED_DIM', 'DEFAULT_AFM_HIST_LEN', 'DEFAULT_AFM_N_KERNELS',
486
+ 'DEFAULT_AFM_RIDGE', 'DEFAULT_AFM_SQUASH_LAMBDA', 'DEFAULT_AFM_PCA_WINDOW',
487
+ 'DEFAULT_AFM_REESTIM', 'DEFAULT_AFM_LAMBDA_VAR', 'DEFAULT_AFM_TURNOVER_COST',
488
+ 'DEFAULT_AFM_SHORT_COST', 'DEFAULT_AFM_MAX_EPOCHS',
418
489
  # Convergence
419
490
  'DEFAULT_CONVERGENCE_THRESHOLD',
420
491
  'DEFAULT_EM_THRESHOLD',
@@ -564,6 +635,25 @@ __all__ = [
564
635
  # KDFM defaults
565
636
  'DEFAULT_KDFM_AR_ORDER',
566
637
  'DEFAULT_KDFM_MA_ORDER',
638
+ # iVDFM defaults
639
+ 'DEFAULT_IVDFM_LATENT_DIM',
640
+ 'DEFAULT_IVDFM_AUX_DIM',
641
+ 'DEFAULT_IVDFM_ENCODER_HIDDEN_DIM',
642
+ 'DEFAULT_IVDFM_ENCODER_N_HIDDEN_LAYERS',
643
+ 'DEFAULT_IVDFM_NUM_REGIMES',
644
+ 'DEFAULT_IVDFM_DECODER_TYPE',
645
+ 'DEFAULT_IVDFM_DECODER_HIDDEN_DIM',
646
+ 'DEFAULT_IVDFM_DECODER_N_HIDDEN_LAYERS',
647
+ 'DEFAULT_IVDFM_PRIOR_HIDDEN_DIM',
648
+ 'DEFAULT_IVDFM_PRIOR_N_HIDDEN_LAYERS',
649
+ 'DEFAULT_IVDFM_FACTOR_ORDER',
650
+ 'DEFAULT_IVDFM_INNOVATION_DIST',
651
+ 'DEFAULT_IVDFM_DECODER_VAR',
652
+ 'DEFAULT_IVDFM_ACTIVATION',
653
+ 'DEFAULT_IVDFM_SLOPE',
654
+ 'DEFAULT_IVDFM_BATCH_SIZE',
655
+ 'DEFAULT_IVDFM_MAX_EPOCHS',
656
+ 'DEFAULT_IVDFM_AUX_VARIABLE_TYPE',
567
657
  # Tutorial defaults
568
658
  'TUTORIAL_MAX_PERIODS',
569
659
  'TUTORIAL_MAX_EPOCHS',
@@ -7,13 +7,13 @@ This package contains:
7
7
  Note: Series are specified via frequency dict mapping column names to frequencies.
8
8
  """
9
9
 
10
- from .model import BaseModelConfig, DFMConfig, DDFMConfig
11
- from .results import BaseResult, DFMResult, DDFMResult
12
- from .params import DFMStateSpaceParams, DDFMStateSpaceParams
10
+ from .model import BaseModelConfig, DFMConfig, DDFMConfig, iVDFMConfig, AFMConfig
11
+ from .results import BaseResult, DFMResult, DDFMResult, iVDFMResult, AFMResult
12
+ from .params import DFMStateSpaceParams, DDFMStateSpaceParams, iVDFMModelState, AFMModelState
13
13
 
14
14
  __all__ = [
15
- 'BaseModelConfig', 'DFMConfig', 'DDFMConfig',
16
- 'BaseResult', 'DFMResult', 'DDFMResult',
17
- 'DFMStateSpaceParams', 'DDFMStateSpaceParams',
15
+ 'BaseModelConfig', 'DFMConfig', 'DDFMConfig', 'iVDFMConfig', 'AFMConfig',
16
+ 'BaseResult', 'DFMResult', 'DDFMResult', 'iVDFMResult', 'AFMResult',
17
+ 'DFMStateSpaceParams', 'DDFMStateSpaceParams', 'iVDFMModelState', 'AFMModelState',
18
18
  ]
19
19
 
@@ -48,6 +48,15 @@ from ..constants import (
48
48
  DEFAULT_BATCH_SIZE,
49
49
  DEFAULT_DDFM_WINDOW_SIZE,
50
50
  DEFAULT_GRAD_CLIP_VAL,
51
+ DEFAULT_IVDFM_OPTIMIZER_WEIGHT_DECAY,
52
+ DEFAULT_IVDFM_OPTIMIZER_MOMENTUM,
53
+ DEFAULT_IVDFM_SCHEDULER_TYPE,
54
+ DEFAULT_IVDFM_SCHEDULER_STEP_SIZE,
55
+ DEFAULT_IVDFM_SCHEDULER_GAMMA,
56
+ DEFAULT_IVDFM_SCHEDULER_PATIENCE,
57
+ DEFAULT_IVDFM_SCHEDULER_FACTOR,
58
+ DEFAULT_IVDFM_SCHEDULER_MIN_LR,
59
+ DEFAULT_IVDFM_EARLY_STOP_PATIENCE,
51
60
  DEFAULT_REGULARIZATION_SCALE,
52
61
  DEFAULT_STRUCTURAL_REG_WEIGHT,
53
62
  DEFAULT_CONVERGENCE_THRESHOLD,
@@ -68,8 +77,42 @@ from ..constants import (
68
77
  DEFAULT_CLOCK_FREQUENCY,
69
78
  DEFAULT_KDFM_AR_ORDER,
70
79
  DEFAULT_KDFM_MA_ORDER,
80
+ DEFAULT_AFM_NUM_FACTORS,
81
+ DEFAULT_AFM_FACTOR_MODEL,
82
+ DEFAULT_AFM_TRADING,
83
+ DEFAULT_AFM_EMBED_DIM,
84
+ DEFAULT_AFM_HIST_LEN,
85
+ DEFAULT_AFM_N_KERNELS,
86
+ DEFAULT_AFM_RIDGE,
87
+ DEFAULT_AFM_SQUASH_LAMBDA,
88
+ DEFAULT_AFM_PCA_WINDOW,
89
+ DEFAULT_AFM_REESTIM,
90
+ DEFAULT_AFM_LAMBDA_VAR,
91
+ DEFAULT_AFM_TURNOVER_COST,
92
+ DEFAULT_AFM_SHORT_COST,
93
+ DEFAULT_AFM_MAX_EPOCHS,
71
94
  FREQUENCY_HIERARCHY,
72
95
  DEFAULT_HIERARCHY_VALUE,
96
+ DEFAULT_IVDFM_LATENT_DIM,
97
+ DEFAULT_IVDFM_AUX_DIM,
98
+ DEFAULT_IVDFM_ENCODER_HIDDEN_DIM,
99
+ DEFAULT_IVDFM_ENCODER_N_HIDDEN_LAYERS,
100
+ DEFAULT_IVDFM_NUM_REGIMES,
101
+ DEFAULT_IVDFM_REGIME_TEMPERATURE,
102
+ DEFAULT_IVDFM_MIXING,
103
+ DEFAULT_IVDFM_DECODER_TYPE,
104
+ DEFAULT_IVDFM_DECODER_HIDDEN_DIM,
105
+ DEFAULT_IVDFM_DECODER_N_HIDDEN_LAYERS,
106
+ DEFAULT_IVDFM_PRIOR_HIDDEN_DIM,
107
+ DEFAULT_IVDFM_PRIOR_N_HIDDEN_LAYERS,
108
+ DEFAULT_IVDFM_FACTOR_ORDER,
109
+ DEFAULT_IVDFM_INNOVATION_DIST,
110
+ DEFAULT_IVDFM_DECODER_VAR,
111
+ DEFAULT_IVDFM_ACTIVATION,
112
+ DEFAULT_IVDFM_SLOPE,
113
+ DEFAULT_IVDFM_BATCH_SIZE,
114
+ DEFAULT_IVDFM_MAX_EPOCHS,
115
+ DEFAULT_IVDFM_AUX_VARIABLE_TYPE,
73
116
  )
74
117
 
75
118
 
@@ -136,6 +179,7 @@ class BaseModelConfig:
136
179
  # ========================================================================
137
180
  clock: str = 'm' # Required: Base frequency for latent factors (global clock): 'd', 'w', 'm', 'q', 'sa', 'a' (defaults to 'm' for monthly)
138
181
  target_scaler: Optional[ScalerType] = None # Fitted sklearn scaler instance (StandardScaler, RobustScaler, etc.) for target series only. Must be a fitted scaler object (call .fit() on target data first). Pass scaler object directly, not string. Feature series are assumed to be manually preprocessed. If None, target series are assumed to be already in the desired scale.
182
+ scaler: Optional[str] = None # Scaler type as string: 'standard', 'robust', 'minmax', 'maxabs', 'quantile', or null. Applied to targets only (not context/auxiliary variables). For iVDFM, this is the preferred way to specify scaler (like DDFM pattern).
139
183
 
140
184
  def __post_init__(self):
141
185
  """Validate basic model structure.
@@ -626,6 +670,291 @@ class DDFMConfig(BaseModelConfig):
626
670
  )
627
671
 
628
672
 
673
+ @dataclass
674
+ class iVDFMConfig(BaseModelConfig):
675
+ """Identifiable Variational Dynamic Factor Model configuration.
676
+
677
+ Uses 'scaler' (string) pattern like DDFM, not 'target_scaler' (instance).
678
+ Scaler is applied to targets only, not to context/auxiliary variables.
679
+ """
680
+ """Identifiable Variational Dynamic Factor Model configuration.
681
+
682
+ This configuration class extends BaseModelConfig with parameters specific
683
+ to iVDFM models trained using variational inference with identifiable
684
+ innovation priors.
685
+
686
+ Note: iVDFM does NOT use block structure. Use num_factors directly to specify
687
+ the number of factors.
688
+
689
+ The configuration can be built from:
690
+ - Main settings (training parameters) from config files
691
+ - Series definitions via frequency dict (column names -> frequencies)
692
+ """
693
+ # ========================================================================
694
+ # Model Structure
695
+ # ========================================================================
696
+ num_factors: Optional[int] = None # Number of factors (inferred from config if None)
697
+ window: Optional[int] = None # Sliding window length for training. If None, uses full T (full series as one sequence). Default: None.
698
+ stride: int = 1 # Step size for sliding windows. stride=1 means overlapping windows, stride=window means non-overlapping.
699
+ context: Optional[Union[List[str], List[int]]] = None # Column names (DataFrame) or indices (array) for exogenous context variables. These are concatenated with time features.
700
+ time_context: int = 1 # Dimension of time-based context features. Always included. time_context=1: time step only, time_context>1: adds periodic sine features.
701
+
702
+ # ========================================================================
703
+ # Neural Network Architecture
704
+ # ========================================================================
705
+ encoder_hidden_dim: Union[int, List[int]] = DEFAULT_IVDFM_ENCODER_HIDDEN_DIM # Encoder architecture
706
+ encoder_n_hidden_layers: int = DEFAULT_IVDFM_ENCODER_N_HIDDEN_LAYERS # Number of encoder hidden layers
707
+ decoder_type: str = DEFAULT_IVDFM_DECODER_TYPE # 'linear', 'residual', or 'mlp'
708
+ decoder_hidden_dim: Union[int, List[int]] = DEFAULT_IVDFM_DECODER_HIDDEN_DIM # Decoder MLP (residual/mlp)
709
+ decoder_n_hidden_layers: int = DEFAULT_IVDFM_DECODER_N_HIDDEN_LAYERS # Decoder hidden layers (residual/mlp)
710
+ prior_hidden_dim: Union[int, List[int]] = DEFAULT_IVDFM_PRIOR_HIDDEN_DIM # Prior network architecture
711
+ prior_n_hidden_layers: int = DEFAULT_IVDFM_PRIOR_N_HIDDEN_LAYERS # Number of prior network hidden layers
712
+ activation: str = DEFAULT_IVDFM_ACTIVATION # Activation function
713
+ slope: float = DEFAULT_IVDFM_SLOPE # Leaky ReLU slope
714
+
715
+ # ========================================================================
716
+ # Dynamics and Distribution Parameters
717
+ # ========================================================================
718
+ num_regimes: int = DEFAULT_IVDFM_NUM_REGIMES # K; 1 = baseline iVDFM (one global dynamics, one decoder)
719
+ regime_temperature: float = DEFAULT_IVDFM_REGIME_TEMPERATURE # Softmax τ for π; τ < 1 = sharper (local commitment)
720
+ factor_order: int = DEFAULT_IVDFM_FACTOR_ORDER # AR order for factors
721
+ innovation_distribution: str = DEFAULT_IVDFM_INNOVATION_DIST # Innovation distribution type
722
+ decoder_var: float = DEFAULT_IVDFM_DECODER_VAR # Decoder variance
723
+ beta_kl: float = 1.0 # Weight for KL term in ELBO: ELBO = recon_loss + beta_kl * kl_loss. β<1 reduces KL pressure.
724
+ use_layer_norm: bool = False # Whether to use layer normalization in encoder/decoder networks
725
+ mixing: bool = DEFAULT_IVDFM_MIXING # If True, global post-factor mixing f -> M f before decode (identity init)
726
+
727
+ # ========================================================================
728
+ # Training Parameters
729
+ # ========================================================================
730
+ learning_rate: float = DEFAULT_LEARNING_RATE # Learning rate
731
+ optimizer: str = 'Adam' # Optimizer type
732
+ optimizer_weight_decay: float = DEFAULT_IVDFM_OPTIMIZER_WEIGHT_DECAY # Weight decay (L2 regularization)
733
+ optimizer_momentum: float = DEFAULT_IVDFM_OPTIMIZER_MOMENTUM # Momentum for SGD
734
+ batch_size: int = DEFAULT_IVDFM_BATCH_SIZE # Batch size
735
+ max_epochs: int = DEFAULT_IVDFM_MAX_EPOCHS # Maximum epochs
736
+ tolerance: float = DEFAULT_TOLERANCE # Convergence tolerance
737
+ patience: Optional[int] = DEFAULT_IVDFM_EARLY_STOP_PATIENCE # Early stopping patience (epochs without improvement). None to disable.
738
+ seed: Optional[int] = None # Random seed
739
+
740
+ # ========================================================================
741
+ # Scheduler Parameters
742
+ # ========================================================================
743
+ scheduler_type: Optional[str] = DEFAULT_IVDFM_SCHEDULER_TYPE # Scheduler type: 'step', 'plateau', 'cosine', 'exponential', None
744
+ scheduler_step_size: Optional[int] = DEFAULT_IVDFM_SCHEDULER_STEP_SIZE # Step size for StepLR (None = auto: max_epochs // 3)
745
+ scheduler_gamma: float = DEFAULT_IVDFM_SCHEDULER_GAMMA # Gamma for StepLR/ExponentialLR
746
+ scheduler_patience: int = DEFAULT_IVDFM_SCHEDULER_PATIENCE # Patience for ReduceLROnPlateau
747
+ scheduler_factor: float = DEFAULT_IVDFM_SCHEDULER_FACTOR # Factor for ReduceLROnPlateau
748
+ scheduler_min_lr: float = DEFAULT_IVDFM_SCHEDULER_MIN_LR # Min learning rate for ReduceLROnPlateau
749
+
750
+ def __post_init__(self):
751
+ """Validate iVDFM configuration."""
752
+ super().__post_init__() # Validate base config
753
+
754
+ from ...utils.errors import ConfigurationError
755
+
756
+ # Validate window
757
+ if self.window is not None and self.window < 1:
758
+ raise ConfigurationError(
759
+ f"window must be >= 1, got {self.window}"
760
+ )
761
+
762
+ # Validate time_context
763
+ if self.time_context < 1:
764
+ raise ConfigurationError(
765
+ f"time_context must be >= 1, got {self.time_context}"
766
+ )
767
+
768
+ # Validate patience if provided
769
+ if self.patience is not None and self.patience < 1:
770
+ raise ConfigurationError(
771
+ f"patience must be >= 1 or None (to disable), got {self.patience}"
772
+ )
773
+
774
+ # Validate num_factors if provided
775
+ if self.num_factors is not None and self.num_factors < 1:
776
+ raise ConfigurationError(
777
+ f"num_factors must be >= 1, got {self.num_factors}"
778
+ )
779
+
780
+ # Validate factor_order
781
+ if self.factor_order < 1:
782
+ raise ConfigurationError(
783
+ f"factor_order must be >= 1, got {self.factor_order}"
784
+ )
785
+
786
+ # Validate innovation_distribution
787
+ valid_dists = {'laplace', 'gaussian', 'student_t', 'gamma', 'beta', 'exponential'}
788
+ if self.innovation_distribution not in valid_dists:
789
+ raise ConfigurationError(
790
+ f"innovation_distribution must be one of {valid_dists}, got '{self.innovation_distribution}'"
791
+ )
792
+
793
+ # Validate num_regimes
794
+ if self.num_regimes < 1:
795
+ raise ConfigurationError(
796
+ f"num_regimes must be >= 1, got {self.num_regimes}"
797
+ )
798
+ # Validate regime_temperature
799
+ if self.regime_temperature <= 0.0:
800
+ raise ConfigurationError(
801
+ f"regime_temperature must be > 0, got {self.regime_temperature}"
802
+ )
803
+ # Validate decoder_type
804
+ if self.decoder_type not in ("linear", "residual", "mlp"):
805
+ raise ConfigurationError(
806
+ f"decoder_type must be 'linear', 'residual', or 'mlp', got '{self.decoder_type}'"
807
+ )
808
+
809
+ # Validate activation
810
+ valid_activations = {'relu', 'lrelu', 'tanh', 'sigmoid'}
811
+ if self.activation not in valid_activations:
812
+ raise ConfigurationError(
813
+ f"activation must be one of {valid_activations}, got '{self.activation}'"
814
+ )
815
+
816
+ # Validate beta_kl
817
+ if self.beta_kl < 0:
818
+ raise ConfigurationError(
819
+ f"beta_kl must be >= 0, got {self.beta_kl}"
820
+ )
821
+
822
+ # Validate optimizer
823
+ from ..constants import VALID_OPTIMIZERS
824
+ if self.optimizer not in VALID_OPTIMIZERS:
825
+ raise ConfigurationError(
826
+ f"optimizer must be one of {VALID_OPTIMIZERS}, got '{self.optimizer}'"
827
+ )
828
+
829
+ # Validate scheduler_type
830
+ valid_schedulers = {'step', 'plateau', 'cosine', 'exponential', None}
831
+ if self.scheduler_type not in valid_schedulers:
832
+ raise ConfigurationError(
833
+ f"scheduler_type must be one of {valid_schedulers}, got '{self.scheduler_type}'"
834
+ )
835
+
836
+ # Validate scheduler parameters
837
+ if self.scheduler_gamma <= 0 or self.scheduler_gamma > 1:
838
+ raise ConfigurationError(
839
+ f"scheduler_gamma must be in (0, 1], got {self.scheduler_gamma}"
840
+ )
841
+
842
+ if self.scheduler_patience < 0:
843
+ raise ConfigurationError(
844
+ f"scheduler_patience must be >= 0, got {self.scheduler_patience}"
845
+ )
846
+
847
+ if self.scheduler_factor <= 0 or self.scheduler_factor > 1:
848
+ raise ConfigurationError(
849
+ f"scheduler_factor must be in (0, 1], got {self.scheduler_factor}"
850
+ )
851
+
852
+ if self.scheduler_min_lr < 0:
853
+ raise ConfigurationError(
854
+ f"scheduler_min_lr must be >= 0, got {self.scheduler_min_lr}"
855
+ )
856
+
857
+ @classmethod
858
+ def _extract_ivdfm(cls, data: Dict[str, Any]) -> Dict[str, Any]:
859
+ """Extract iVDFM-specific parameters from config dict."""
860
+ from ..constants import (
861
+ DEFAULT_IVDFM_LATENT_DIM,
862
+ DEFAULT_IVDFM_AUX_DIM,
863
+ DEFAULT_IVDFM_ENCODER_HIDDEN_DIM,
864
+ DEFAULT_IVDFM_ENCODER_N_HIDDEN_LAYERS,
865
+ DEFAULT_IVDFM_NUM_REGIMES,
866
+ DEFAULT_IVDFM_REGIME_TEMPERATURE,
867
+ DEFAULT_IVDFM_MIXING,
868
+ DEFAULT_IVDFM_DECODER_TYPE,
869
+ DEFAULT_IVDFM_DECODER_HIDDEN_DIM,
870
+ DEFAULT_IVDFM_DECODER_N_HIDDEN_LAYERS,
871
+ DEFAULT_IVDFM_PRIOR_HIDDEN_DIM,
872
+ DEFAULT_IVDFM_PRIOR_N_HIDDEN_LAYERS,
873
+ DEFAULT_IVDFM_FACTOR_ORDER,
874
+ DEFAULT_IVDFM_INNOVATION_DIST,
875
+ DEFAULT_IVDFM_DECODER_VAR,
876
+ DEFAULT_IVDFM_ACTIVATION,
877
+ DEFAULT_IVDFM_SLOPE,
878
+ DEFAULT_IVDFM_BATCH_SIZE,
879
+ DEFAULT_IVDFM_MAX_EPOCHS,
880
+ DEFAULT_IVDFM_AUX_VARIABLE_TYPE,
881
+ DEFAULT_IVDFM_OPTIMIZER_WEIGHT_DECAY,
882
+ DEFAULT_IVDFM_OPTIMIZER_MOMENTUM,
883
+ DEFAULT_IVDFM_SCHEDULER_TYPE,
884
+ DEFAULT_IVDFM_SCHEDULER_STEP_SIZE,
885
+ DEFAULT_IVDFM_SCHEDULER_GAMMA,
886
+ DEFAULT_IVDFM_SCHEDULER_PATIENCE,
887
+ DEFAULT_IVDFM_SCHEDULER_FACTOR,
888
+ DEFAULT_IVDFM_SCHEDULER_MIN_LR,
889
+ )
890
+
891
+ ivdfm_params = cls._extract_params(data, {
892
+ 'num_factors': None,
893
+ 'window': None, # Default: None (uses full T - full series as one sequence)
894
+ 'stride': 1, # Default: stride=1 (overlapping windows)
895
+ 'context': None,
896
+ 'time_context': 1, # Default: time step only
897
+ 'encoder_hidden_dim': DEFAULT_IVDFM_ENCODER_HIDDEN_DIM,
898
+ 'encoder_n_hidden_layers': DEFAULT_IVDFM_ENCODER_N_HIDDEN_LAYERS,
899
+ 'num_regimes': DEFAULT_IVDFM_NUM_REGIMES,
900
+ 'regime_temperature': DEFAULT_IVDFM_REGIME_TEMPERATURE,
901
+ 'decoder_type': DEFAULT_IVDFM_DECODER_TYPE,
902
+ 'decoder_hidden_dim': DEFAULT_IVDFM_DECODER_HIDDEN_DIM,
903
+ 'decoder_n_hidden_layers': DEFAULT_IVDFM_DECODER_N_HIDDEN_LAYERS,
904
+ 'prior_hidden_dim': DEFAULT_IVDFM_PRIOR_HIDDEN_DIM,
905
+ 'prior_n_hidden_layers': DEFAULT_IVDFM_PRIOR_N_HIDDEN_LAYERS,
906
+ 'activation': DEFAULT_IVDFM_ACTIVATION,
907
+ 'slope': DEFAULT_IVDFM_SLOPE,
908
+ 'factor_order': DEFAULT_IVDFM_FACTOR_ORDER,
909
+ 'innovation_distribution': DEFAULT_IVDFM_INNOVATION_DIST,
910
+ 'decoder_var': DEFAULT_IVDFM_DECODER_VAR,
911
+ 'beta_kl': 1.0,
912
+ 'use_layer_norm': False,
913
+ 'mixing': DEFAULT_IVDFM_MIXING,
914
+ 'learning_rate': DEFAULT_LEARNING_RATE,
915
+ 'optimizer': 'Adam',
916
+ 'optimizer_weight_decay': DEFAULT_IVDFM_OPTIMIZER_WEIGHT_DECAY,
917
+ 'optimizer_momentum': DEFAULT_IVDFM_OPTIMIZER_MOMENTUM,
918
+ 'batch_size': DEFAULT_IVDFM_BATCH_SIZE,
919
+ 'max_epochs': DEFAULT_IVDFM_MAX_EPOCHS,
920
+ 'tolerance': DEFAULT_TOLERANCE,
921
+ 'patience': DEFAULT_IVDFM_EARLY_STOP_PATIENCE,
922
+ 'seed': None,
923
+ 'scheduler_type': DEFAULT_IVDFM_SCHEDULER_TYPE,
924
+ 'scheduler_step_size': DEFAULT_IVDFM_SCHEDULER_STEP_SIZE,
925
+ 'scheduler_gamma': DEFAULT_IVDFM_SCHEDULER_GAMMA,
926
+ 'scheduler_patience': DEFAULT_IVDFM_SCHEDULER_PATIENCE,
927
+ 'scheduler_factor': DEFAULT_IVDFM_SCHEDULER_FACTOR,
928
+ 'scheduler_min_lr': DEFAULT_IVDFM_SCHEDULER_MIN_LR,
929
+ 'scaler': None, # Scaler string: 'standard', 'robust', 'minmax', 'maxabs', 'quantile', or null
930
+ })
931
+
932
+ return ivdfm_params
933
+
934
+ @classmethod
935
+ def from_dict(cls, data: Dict[str, Any]) -> 'iVDFMConfig':
936
+ """Create iVDFMConfig from dictionary.
937
+
938
+ Parameters
939
+ ----------
940
+ data : Dict[str, Any]
941
+ Configuration dictionary
942
+
943
+ Returns
944
+ -------
945
+ iVDFMConfig
946
+ iVDFM configuration instance
947
+ """
948
+ # Extract base params
949
+ base_params = cls._extract_base(data)
950
+
951
+ # Extract iVDFM-specific params
952
+ ivdfm_params = cls._extract_ivdfm(data)
953
+
954
+ # Combine and create config
955
+ return cls(**base_params, **ivdfm_params)
956
+
957
+
629
958
  # ============================================================================
630
959
  # Validation Functions
631
960
  # ============================================================================
@@ -662,6 +991,79 @@ def validate_frequency(frequency: str) -> str:
662
991
  f"Common frequencies: 'd' (daily), 'w' (weekly), 'm' (monthly), "
663
992
  f"'q' (quarterly), 'sa' (semi-annual), 'a' (annual)."
664
993
  )
665
-
994
+
666
995
  return frequency
667
996
 
997
+
998
+ @dataclass
999
+ class AFMConfig(BaseModelConfig):
1000
+ """Attention Factor Model configuration (Epstein et al., 2025).
1001
+
1002
+ Cross-sectional conditional factor model + residual trading policy. Like
1003
+ iVDFM, AFM does not use block structure and is constructed directly via
1004
+ ``AFMConfig.from_dict(OmegaConf.to_container(cfg))`` rather than through the
1005
+ block-requiring YAML adapter.
1006
+ """
1007
+ num_factors: Optional[int] = None # K latent factors
1008
+ factor_model: str = DEFAULT_AFM_FACTOR_MODEL # 'attention' (learned) | 'pca'
1009
+ trading: str = DEFAULT_AFM_TRADING # 'longconv' (learned) | 'ou' (Avellaneda-Lee)
1010
+ embed_dim: int = DEFAULT_AFM_EMBED_DIM # characteristic embedding dim d
1011
+ hist_len: int = DEFAULT_AFM_HIST_LEN # residual-history length s for the filter
1012
+ n_kernels: int = DEFAULT_AFM_N_KERNELS # number of long convolutions
1013
+ ridge: float = DEFAULT_AFM_RIDGE # ridge penalty for closed-form loadings
1014
+ squash_lambda: float = DEFAULT_AFM_SQUASH_LAMBDA # LongConv Squash regularizer
1015
+ pca_window: int = DEFAULT_AFM_PCA_WINDOW # trailing window for the PCA benchmark
1016
+ reestim: int = DEFAULT_AFM_REESTIM # PCA re-estimation stride
1017
+ lambda_var: float = DEFAULT_AFM_LAMBDA_VAR # explained-variance regularizer weight
1018
+ turnover_cost: float = DEFAULT_AFM_TURNOVER_COST # 5bps per unit turnover (paper)
1019
+ short_cost: float = DEFAULT_AFM_SHORT_COST # 1bp shorting cost (paper)
1020
+ risk_free: float = 0.0
1021
+ learning_rate: float = DEFAULT_LEARNING_RATE
1022
+ max_epochs: int = DEFAULT_AFM_MAX_EPOCHS
1023
+ patience: Optional[int] = None
1024
+ seed: Optional[int] = None
1025
+
1026
+ def __post_init__(self):
1027
+ super().__post_init__()
1028
+ from ...utils.errors import ConfigurationError
1029
+ if self.num_factors is not None and self.num_factors < 1:
1030
+ raise ConfigurationError(f"num_factors must be >= 1, got {self.num_factors}")
1031
+ if self.factor_model not in ('attention', 'pca'):
1032
+ raise ConfigurationError(
1033
+ f"factor_model must be 'attention' or 'pca', got '{self.factor_model}'")
1034
+ if self.trading not in ('longconv', 'ou'):
1035
+ raise ConfigurationError(
1036
+ f"trading must be 'longconv' or 'ou', got '{self.trading}'")
1037
+ if self.hist_len < 1:
1038
+ raise ConfigurationError(f"hist_len must be >= 1, got {self.hist_len}")
1039
+ if self.embed_dim < 1:
1040
+ raise ConfigurationError(f"embed_dim must be >= 1, got {self.embed_dim}")
1041
+
1042
+ @classmethod
1043
+ def _extract_afm(cls, data: Dict[str, Any]) -> Dict[str, Any]:
1044
+ """Extract AFM-specific parameters from a config dict."""
1045
+ return cls._extract_params(data, {
1046
+ 'num_factors': None,
1047
+ 'factor_model': DEFAULT_AFM_FACTOR_MODEL,
1048
+ 'trading': DEFAULT_AFM_TRADING,
1049
+ 'embed_dim': DEFAULT_AFM_EMBED_DIM,
1050
+ 'hist_len': DEFAULT_AFM_HIST_LEN,
1051
+ 'n_kernels': DEFAULT_AFM_N_KERNELS,
1052
+ 'ridge': DEFAULT_AFM_RIDGE,
1053
+ 'squash_lambda': DEFAULT_AFM_SQUASH_LAMBDA,
1054
+ 'pca_window': DEFAULT_AFM_PCA_WINDOW,
1055
+ 'reestim': DEFAULT_AFM_REESTIM,
1056
+ 'lambda_var': DEFAULT_AFM_LAMBDA_VAR,
1057
+ 'turnover_cost': DEFAULT_AFM_TURNOVER_COST,
1058
+ 'short_cost': DEFAULT_AFM_SHORT_COST,
1059
+ 'risk_free': 0.0, 'learning_rate': DEFAULT_LEARNING_RATE,
1060
+ 'max_epochs': DEFAULT_AFM_MAX_EPOCHS, 'patience': None, 'seed': None,
1061
+ })
1062
+
1063
+ @classmethod
1064
+ def from_dict(cls, data: Dict[str, Any]) -> 'AFMConfig':
1065
+ """Create AFMConfig from a dictionary."""
1066
+ base_params = cls._extract_base(data)
1067
+ afm_params = cls._extract_afm(data)
1068
+ return cls(**base_params, **afm_params)
1069
+