dfm-python 0.6.14__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 (40) hide show
  1. dfm_python/__init__.py +11 -2
  2. dfm_python/config/__init__.py +6 -6
  3. dfm_python/config/constants.py +40 -9
  4. dfm_python/config/schema/__init__.py +6 -6
  5. dfm_python/config/schema/model.py +165 -27
  6. dfm_python/config/schema/params.py +35 -0
  7. dfm_python/config/schema/results.py +58 -0
  8. dfm_python/dataset/__init__.py +2 -0
  9. dfm_python/dataset/afm_dataset.py +119 -0
  10. dfm_python/dataset/ivdfm_dataset.py +87 -28
  11. dfm_python/layer/mixing.py +37 -0
  12. dfm_python/layer/mlp.py +34 -17
  13. dfm_python/layer/pca.py +46 -0
  14. dfm_python/layer/regime.py +53 -0
  15. dfm_python/models/__init__.py +10 -0
  16. dfm_python/models/afm/__init__.py +23 -0
  17. dfm_python/models/afm/afm.py +385 -0
  18. dfm_python/models/afm/baselines.py +72 -0
  19. dfm_python/models/afm/factors.py +192 -0
  20. dfm_python/models/afm/longconv.py +97 -0
  21. dfm_python/models/base.py +15 -0
  22. dfm_python/models/ivdfm/__init__.py +2 -0
  23. dfm_python/models/ivdfm/decoder.py +51 -103
  24. dfm_python/models/ivdfm/encoder.py +11 -6
  25. dfm_python/models/ivdfm/ivdfm.py +939 -372
  26. dfm_python/models/ivdfm/optuna.py +647 -0
  27. dfm_python/models/ivdfm/prior.py +77 -4
  28. dfm_python/numeric/builder.py +39 -1
  29. dfm_python/numeric/estimator.py +78 -9
  30. dfm_python/numeric/validator.py +5 -0
  31. dfm_python/ssm/companion.py +210 -2
  32. dfm_python/ssm/kalman.py +100 -3
  33. dfm_python/ssm/residual.py +0 -0
  34. dfm_python/utils/loss.py +53 -121
  35. dfm_python-0.7.0.dist-info/METADATA +186 -0
  36. dfm_python-0.7.0.dist-info/RECORD +75 -0
  37. {dfm_python-0.6.14.dist-info → dfm_python-0.7.0.dist-info}/WHEEL +1 -1
  38. dfm_python-0.7.0.dist-info/licenses/LICENSE +21 -0
  39. dfm_python-0.6.14.dist-info/METADATA +0 -56
  40. dfm_python-0.6.14.dist-info/RECORD +0 -64
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
@@ -348,18 +348,23 @@ DEFAULT_KDFM_MA_ORDER = 0 # Default MA order (MA lag order q) for KDFM (0 = pur
348
348
  # iVDFM Defaults
349
349
  # ============================================================================
350
350
 
351
- # iVDFM sequence and dimension defaults
352
- DEFAULT_IVDFM_SEQUENCE_LENGTH = 100 # Default sequence length for sliding windows
351
+ # iVDFM dimension defaults
353
352
  DEFAULT_IVDFM_LATENT_DIM = 3 # Default number of factors
354
353
  DEFAULT_IVDFM_AUX_DIM = 1 # Default auxiliary variable dimension
355
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
+
356
360
  # iVDFM network architecture defaults
357
361
  DEFAULT_IVDFM_ENCODER_HIDDEN_DIM = 200 # Default encoder hidden dimension
358
- DEFAULT_IVDFM_ENCODER_N_LAYERS = 3 # Default encoder layers
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)
359
364
  DEFAULT_IVDFM_DECODER_HIDDEN_DIM = 200 # Default decoder hidden dimension
360
- DEFAULT_IVDFM_DECODER_N_LAYERS = 3 # Default decoder layers
365
+ DEFAULT_IVDFM_DECODER_N_HIDDEN_LAYERS = 2 # Default decoder hidden layers
361
366
  DEFAULT_IVDFM_PRIOR_HIDDEN_DIM = 100 # Default prior network hidden dimension
362
- DEFAULT_IVDFM_PRIOR_N_LAYERS = 2 # Default prior network layers
367
+ DEFAULT_IVDFM_PRIOR_N_HIDDEN_LAYERS = 1 # Default prior network hidden layers
363
368
 
364
369
  # iVDFM dynamics and distribution defaults
365
370
  DEFAULT_IVDFM_FACTOR_ORDER = 1 # Default AR order for factors
@@ -384,6 +389,7 @@ DEFAULT_IVDFM_SCHEDULER_GAMMA = 0.5 # Default gamma for StepLR/ExponentialLR
384
389
  DEFAULT_IVDFM_SCHEDULER_PATIENCE = 10 # Default patience for ReduceLROnPlateau
385
390
  DEFAULT_IVDFM_SCHEDULER_FACTOR = 0.1 # Default factor for ReduceLROnPlateau
386
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)
387
393
 
388
394
  # ============================================================================
389
395
  # Tutorial Defaults
@@ -451,11 +457,35 @@ FREQUENCY_HIERARCHY: Dict[str, int] = {
451
457
  # For frequency gaps larger than this, the missing data approach is used instead
452
458
  MAX_TENT_SIZE: int = 12
453
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
+
454
478
  # ============================================================================
455
479
  # Export all constants
456
480
  # ============================================================================
457
481
 
458
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',
459
489
  # Convergence
460
490
  'DEFAULT_CONVERGENCE_THRESHOLD',
461
491
  'DEFAULT_EM_THRESHOLD',
@@ -606,15 +636,16 @@ __all__ = [
606
636
  'DEFAULT_KDFM_AR_ORDER',
607
637
  'DEFAULT_KDFM_MA_ORDER',
608
638
  # iVDFM defaults
609
- 'DEFAULT_IVDFM_SEQUENCE_LENGTH',
610
639
  'DEFAULT_IVDFM_LATENT_DIM',
611
640
  'DEFAULT_IVDFM_AUX_DIM',
612
641
  'DEFAULT_IVDFM_ENCODER_HIDDEN_DIM',
613
- 'DEFAULT_IVDFM_ENCODER_N_LAYERS',
642
+ 'DEFAULT_IVDFM_ENCODER_N_HIDDEN_LAYERS',
643
+ 'DEFAULT_IVDFM_NUM_REGIMES',
644
+ 'DEFAULT_IVDFM_DECODER_TYPE',
614
645
  'DEFAULT_IVDFM_DECODER_HIDDEN_DIM',
615
- 'DEFAULT_IVDFM_DECODER_N_LAYERS',
646
+ 'DEFAULT_IVDFM_DECODER_N_HIDDEN_LAYERS',
616
647
  'DEFAULT_IVDFM_PRIOR_HIDDEN_DIM',
617
- 'DEFAULT_IVDFM_PRIOR_N_LAYERS',
648
+ 'DEFAULT_IVDFM_PRIOR_N_HIDDEN_LAYERS',
618
649
  'DEFAULT_IVDFM_FACTOR_ORDER',
619
650
  'DEFAULT_IVDFM_INNOVATION_DIST',
620
651
  'DEFAULT_IVDFM_DECODER_VAR',
@@ -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, iVDFMConfig
11
- from .results import BaseResult, DFMResult, DDFMResult, iVDFMResult
12
- from .params import DFMStateSpaceParams, DDFMStateSpaceParams, iVDFMModelState
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', 'iVDFMConfig',
16
- 'BaseResult', 'DFMResult', 'DDFMResult', 'iVDFMResult',
17
- 'DFMStateSpaceParams', 'DDFMStateSpaceParams', 'iVDFMModelState',
15
+ 'BaseModelConfig', 'DFMConfig', 'DDFMConfig', 'iVDFMConfig', 'AFMConfig',
16
+ 'BaseResult', 'DFMResult', 'DDFMResult', 'iVDFMResult', 'AFMResult',
17
+ 'DFMStateSpaceParams', 'DDFMStateSpaceParams', 'iVDFMModelState', 'AFMModelState',
18
18
  ]
19
19
 
@@ -56,6 +56,7 @@ from ..constants import (
56
56
  DEFAULT_IVDFM_SCHEDULER_PATIENCE,
57
57
  DEFAULT_IVDFM_SCHEDULER_FACTOR,
58
58
  DEFAULT_IVDFM_SCHEDULER_MIN_LR,
59
+ DEFAULT_IVDFM_EARLY_STOP_PATIENCE,
59
60
  DEFAULT_REGULARIZATION_SCALE,
60
61
  DEFAULT_STRUCTURAL_REG_WEIGHT,
61
62
  DEFAULT_CONVERGENCE_THRESHOLD,
@@ -76,17 +77,34 @@ from ..constants import (
76
77
  DEFAULT_CLOCK_FREQUENCY,
77
78
  DEFAULT_KDFM_AR_ORDER,
78
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,
79
94
  FREQUENCY_HIERARCHY,
80
95
  DEFAULT_HIERARCHY_VALUE,
81
- DEFAULT_IVDFM_SEQUENCE_LENGTH,
82
96
  DEFAULT_IVDFM_LATENT_DIM,
83
97
  DEFAULT_IVDFM_AUX_DIM,
84
98
  DEFAULT_IVDFM_ENCODER_HIDDEN_DIM,
85
- DEFAULT_IVDFM_ENCODER_N_LAYERS,
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,
86
104
  DEFAULT_IVDFM_DECODER_HIDDEN_DIM,
87
- DEFAULT_IVDFM_DECODER_N_LAYERS,
105
+ DEFAULT_IVDFM_DECODER_N_HIDDEN_LAYERS,
88
106
  DEFAULT_IVDFM_PRIOR_HIDDEN_DIM,
89
- DEFAULT_IVDFM_PRIOR_N_LAYERS,
107
+ DEFAULT_IVDFM_PRIOR_N_HIDDEN_LAYERS,
90
108
  DEFAULT_IVDFM_FACTOR_ORDER,
91
109
  DEFAULT_IVDFM_INNOVATION_DIST,
92
110
  DEFAULT_IVDFM_DECODER_VAR,
@@ -676,28 +694,35 @@ class iVDFMConfig(BaseModelConfig):
676
694
  # Model Structure
677
695
  # ========================================================================
678
696
  num_factors: Optional[int] = None # Number of factors (inferred from config if None)
679
- sequence_length: int = DEFAULT_IVDFM_SEQUENCE_LENGTH # Sequence length for training
680
- context: Optional[Union[List[str], List[int]]] = None # Column names (DataFrame) or indices (array) for context variables. If None, context_dim is used to generate time-based context.
681
- context_dim: int = DEFAULT_IVDFM_AUX_DIM # Dimension of context. Used when context is None (generates time-based context) or when context is multivariate.
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.
682
701
 
683
702
  # ========================================================================
684
703
  # Neural Network Architecture
685
704
  # ========================================================================
686
705
  encoder_hidden_dim: Union[int, List[int]] = DEFAULT_IVDFM_ENCODER_HIDDEN_DIM # Encoder architecture
687
- encoder_n_layers: int = DEFAULT_IVDFM_ENCODER_N_LAYERS # Number of encoder layers
688
- decoder_hidden_dim: Union[int, List[int]] = DEFAULT_IVDFM_DECODER_HIDDEN_DIM # Decoder architecture
689
- decoder_n_layers: int = DEFAULT_IVDFM_DECODER_N_LAYERS # Number of decoder layers
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)
690
710
  prior_hidden_dim: Union[int, List[int]] = DEFAULT_IVDFM_PRIOR_HIDDEN_DIM # Prior network architecture
691
- prior_n_layers: int = DEFAULT_IVDFM_PRIOR_N_LAYERS # Number of prior network layers
711
+ prior_n_hidden_layers: int = DEFAULT_IVDFM_PRIOR_N_HIDDEN_LAYERS # Number of prior network hidden layers
692
712
  activation: str = DEFAULT_IVDFM_ACTIVATION # Activation function
693
713
  slope: float = DEFAULT_IVDFM_SLOPE # Leaky ReLU slope
694
714
 
695
715
  # ========================================================================
696
716
  # Dynamics and Distribution Parameters
697
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)
698
720
  factor_order: int = DEFAULT_IVDFM_FACTOR_ORDER # AR order for factors
699
721
  innovation_distribution: str = DEFAULT_IVDFM_INNOVATION_DIST # Innovation distribution type
700
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)
701
726
 
702
727
  # ========================================================================
703
728
  # Training Parameters
@@ -709,6 +734,7 @@ class iVDFMConfig(BaseModelConfig):
709
734
  batch_size: int = DEFAULT_IVDFM_BATCH_SIZE # Batch size
710
735
  max_epochs: int = DEFAULT_IVDFM_MAX_EPOCHS # Maximum epochs
711
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.
712
738
  seed: Optional[int] = None # Random seed
713
739
 
714
740
  # ========================================================================
@@ -727,16 +753,22 @@ class iVDFMConfig(BaseModelConfig):
727
753
 
728
754
  from ...utils.errors import ConfigurationError
729
755
 
730
- # Validate sequence_length
731
- if self.sequence_length < 1:
756
+ # Validate window
757
+ if self.window is not None and self.window < 1:
732
758
  raise ConfigurationError(
733
- f"sequence_length must be >= 1, got {self.sequence_length}"
759
+ f"window must be >= 1, got {self.window}"
734
760
  )
735
761
 
736
- # Validate context_dim
737
- if self.context_dim < 1:
762
+ # Validate time_context
763
+ if self.time_context < 1:
738
764
  raise ConfigurationError(
739
- f"context_dim must be >= 1, got {self.context_dim}"
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}"
740
772
  )
741
773
 
742
774
  # Validate num_factors if provided
@@ -758,6 +790,22 @@ class iVDFMConfig(BaseModelConfig):
758
790
  f"innovation_distribution must be one of {valid_dists}, got '{self.innovation_distribution}'"
759
791
  )
760
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
+
761
809
  # Validate activation
762
810
  valid_activations = {'relu', 'lrelu', 'tanh', 'sigmoid'}
763
811
  if self.activation not in valid_activations:
@@ -765,6 +813,12 @@ class iVDFMConfig(BaseModelConfig):
765
813
  f"activation must be one of {valid_activations}, got '{self.activation}'"
766
814
  )
767
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
+
768
822
  # Validate optimizer
769
823
  from ..constants import VALID_OPTIMIZERS
770
824
  if self.optimizer not in VALID_OPTIMIZERS:
@@ -804,15 +858,18 @@ class iVDFMConfig(BaseModelConfig):
804
858
  def _extract_ivdfm(cls, data: Dict[str, Any]) -> Dict[str, Any]:
805
859
  """Extract iVDFM-specific parameters from config dict."""
806
860
  from ..constants import (
807
- DEFAULT_IVDFM_SEQUENCE_LENGTH,
808
861
  DEFAULT_IVDFM_LATENT_DIM,
809
862
  DEFAULT_IVDFM_AUX_DIM,
810
863
  DEFAULT_IVDFM_ENCODER_HIDDEN_DIM,
811
- DEFAULT_IVDFM_ENCODER_N_LAYERS,
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,
812
869
  DEFAULT_IVDFM_DECODER_HIDDEN_DIM,
813
- DEFAULT_IVDFM_DECODER_N_LAYERS,
870
+ DEFAULT_IVDFM_DECODER_N_HIDDEN_LAYERS,
814
871
  DEFAULT_IVDFM_PRIOR_HIDDEN_DIM,
815
- DEFAULT_IVDFM_PRIOR_N_LAYERS,
872
+ DEFAULT_IVDFM_PRIOR_N_HIDDEN_LAYERS,
816
873
  DEFAULT_IVDFM_FACTOR_ORDER,
817
874
  DEFAULT_IVDFM_INNOVATION_DIST,
818
875
  DEFAULT_IVDFM_DECODER_VAR,
@@ -833,20 +890,27 @@ class iVDFMConfig(BaseModelConfig):
833
890
 
834
891
  ivdfm_params = cls._extract_params(data, {
835
892
  'num_factors': None,
836
- 'sequence_length': DEFAULT_IVDFM_SEQUENCE_LENGTH,
893
+ 'window': None, # Default: None (uses full T - full series as one sequence)
894
+ 'stride': 1, # Default: stride=1 (overlapping windows)
837
895
  'context': None,
838
- 'context_dim': DEFAULT_IVDFM_AUX_DIM,
896
+ 'time_context': 1, # Default: time step only
839
897
  'encoder_hidden_dim': DEFAULT_IVDFM_ENCODER_HIDDEN_DIM,
840
- 'encoder_n_layers': DEFAULT_IVDFM_ENCODER_N_LAYERS,
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,
841
902
  'decoder_hidden_dim': DEFAULT_IVDFM_DECODER_HIDDEN_DIM,
842
- 'decoder_n_layers': DEFAULT_IVDFM_DECODER_N_LAYERS,
903
+ 'decoder_n_hidden_layers': DEFAULT_IVDFM_DECODER_N_HIDDEN_LAYERS,
843
904
  'prior_hidden_dim': DEFAULT_IVDFM_PRIOR_HIDDEN_DIM,
844
- 'prior_n_layers': DEFAULT_IVDFM_PRIOR_N_LAYERS,
905
+ 'prior_n_hidden_layers': DEFAULT_IVDFM_PRIOR_N_HIDDEN_LAYERS,
845
906
  'activation': DEFAULT_IVDFM_ACTIVATION,
846
907
  'slope': DEFAULT_IVDFM_SLOPE,
847
908
  'factor_order': DEFAULT_IVDFM_FACTOR_ORDER,
848
909
  'innovation_distribution': DEFAULT_IVDFM_INNOVATION_DIST,
849
910
  'decoder_var': DEFAULT_IVDFM_DECODER_VAR,
911
+ 'beta_kl': 1.0,
912
+ 'use_layer_norm': False,
913
+ 'mixing': DEFAULT_IVDFM_MIXING,
850
914
  'learning_rate': DEFAULT_LEARNING_RATE,
851
915
  'optimizer': 'Adam',
852
916
  'optimizer_weight_decay': DEFAULT_IVDFM_OPTIMIZER_WEIGHT_DECAY,
@@ -854,6 +918,7 @@ class iVDFMConfig(BaseModelConfig):
854
918
  'batch_size': DEFAULT_IVDFM_BATCH_SIZE,
855
919
  'max_epochs': DEFAULT_IVDFM_MAX_EPOCHS,
856
920
  'tolerance': DEFAULT_TOLERANCE,
921
+ 'patience': DEFAULT_IVDFM_EARLY_STOP_PATIENCE,
857
922
  'seed': None,
858
923
  'scheduler_type': DEFAULT_IVDFM_SCHEDULER_TYPE,
859
924
  'scheduler_step_size': DEFAULT_IVDFM_SCHEDULER_STEP_SIZE,
@@ -926,6 +991,79 @@ def validate_frequency(frequency: str) -> str:
926
991
  f"Common frequencies: 'd' (daily), 'w' (weekly), 'm' (monthly), "
927
992
  f"'q' (quarterly), 'sa' (semi-annual), 'a' (annual)."
928
993
  )
929
-
994
+
930
995
  return frequency
931
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
+
@@ -514,3 +514,38 @@ DDFMStateSpaceParams = DDFMModelState
514
514
  # Backward compatibility alias
515
515
  DFMStateSpaceParams = DFMModelState
516
516
 
517
+
518
+ @dataclass
519
+ class AFMModelState:
520
+ """Attention Factor Model training state for checkpointing."""
521
+ num_iter: int = 0
522
+ converged: bool = False
523
+ loss_now: Optional[float] = None
524
+ sharpe: Optional[float] = None
525
+ explained_variance: Optional[float] = None
526
+ residuals: Optional[np.ndarray] = None # (B, T, N) traded residual portfolios
527
+ book_returns: Optional[np.ndarray] = None # (B, n_eval) after-cost book returns
528
+
529
+ @classmethod
530
+ def from_model(cls, model: Any) -> 'AFMModelState':
531
+ """Create state from a trained AFM instance."""
532
+ return cls(
533
+ num_iter=getattr(model, '_num_iter', 0),
534
+ converged=getattr(model, '_converged', False),
535
+ loss_now=getattr(model, 'loss_now', None),
536
+ sharpe=getattr(model, '_sharpe', None),
537
+ explained_variance=getattr(model, '_ev', None),
538
+ residuals=getattr(model, '_residuals', None),
539
+ book_returns=getattr(model, '_book_returns', None),
540
+ )
541
+
542
+ def apply_to_model(self, model: Any) -> None:
543
+ """Restore training diagnostics onto a model instance."""
544
+ model._num_iter = self.num_iter
545
+ model._converged = self.converged
546
+ model.loss_now = self.loss_now
547
+ model._sharpe = self.sharpe
548
+ model._ev = self.explained_variance
549
+ model._residuals = self.residuals
550
+ model._book_returns = self.book_returns
551
+
@@ -458,6 +458,13 @@ class iVDFMResult(BaseResult):
458
458
  reconstructions: Optional[np.ndarray] = None # decoded factors, (T, N) or (batch, T, N)
459
459
  full_state: Optional[np.ndarray] = None # Augmented state for companion form (T x r*p) for p>1, (T x r) for p=1
460
460
 
461
+ # Regime and mixing (when num_regimes > 1 or mixing=True)
462
+ num_regimes: Optional[int] = None # K; 1 = baseline (no regime structure)
463
+ regime_temperature: Optional[float] = None # Softmax τ for π (τ < 1 = sharper commitment)
464
+ regime_weights: Optional[np.ndarray] = None # (T, K) or (num_windows, K) π_t from regime_net; for Z = [factors, regime] in stability
465
+ mixing: Optional[bool] = None # Whether post-factor mixing f -> M f was used
466
+ mixing_matrix: Optional[np.ndarray] = None # (r, r) learned M when mixing=True (for inspection)
467
+
461
468
  # Training diagnostics
462
469
  training_elbo: Optional[float] = None
463
470
  training_loss: Optional[float] = None
@@ -498,6 +505,14 @@ class iVDFMResult(BaseResult):
498
505
  lines.append(f"Reconstructions shape: {getattr(self.reconstructions, 'shape', None)}")
499
506
  if self.full_state is not None:
500
507
  lines.append(f"Full state (augmented) shape: {getattr(self.full_state, 'shape', None)}")
508
+ if self.num_regimes is not None:
509
+ lines.append(f"Num regimes: {self.num_regimes}")
510
+ if self.regime_temperature is not None:
511
+ lines.append(f"Regime temperature: {self.regime_temperature}")
512
+ if self.mixing is not None:
513
+ lines.append(f"Mixing: {self.mixing}")
514
+ if self.mixing_matrix is not None:
515
+ lines.append(f"Mixing matrix shape: {getattr(self.mixing_matrix, 'shape', None)}")
501
516
  lines.append("")
502
517
  lines.append("Training:")
503
518
  lines.append(f" Converged: {self.converged}")
@@ -513,3 +528,46 @@ class iVDFMResult(BaseResult):
513
528
 
514
529
  # FitParams moved to config.schema.params as DFMParams
515
530
 
531
+
532
+ @dataclass
533
+ class AFMResult(BaseResult):
534
+ """Attention Factor Model results (statistical-arbitrage residual trading).
535
+
536
+ AFM is not a linear state-space model; its primary artifacts are the traded
537
+ residual portfolios, the after-cost book returns, and the objective values.
538
+ ``Z``/``x_sm`` alias the residuals so the common accessors still work.
539
+ """
540
+ residual_portfolios: Optional[np.ndarray] = None # (B, T, N) traded residuals
541
+ book_returns: Optional[np.ndarray] = None # (B, n_eval) after-cost returns
542
+ sharpe: Optional[float] = None # after-cost Sharpe (objective)
543
+ explained_variance: Optional[float] = None # EV regularizer value
544
+ factor_model: Optional[str] = None # 'attention' | 'pca'
545
+ trading: Optional[str] = None # 'longconv' | 'ou'
546
+ num_factors: Optional[int] = None
547
+
548
+ @property
549
+ def factors(self) -> Optional[np.ndarray]:
550
+ return self.Z
551
+
552
+ @property
553
+ def reconstructed(self) -> Optional[np.ndarray]:
554
+ return self.x_sm
555
+
556
+ @property
557
+ def x_hat(self) -> Optional[np.ndarray]:
558
+ return self.x_sm
559
+
560
+ def summary(self) -> str:
561
+ lines = ["=" * 80, "AFM Result Summary", "=" * 80, ""]
562
+ lines.append(f"Factor model: {self.factor_model}, trading: {self.trading}")
563
+ if self.num_factors is not None:
564
+ lines.append(f"Num factors: {self.num_factors}")
565
+ if self.sharpe is not None:
566
+ lines.append(f"After-cost Sharpe (objective): {self.sharpe:.4f}")
567
+ if self.explained_variance is not None:
568
+ lines.append(f"Explained variance: {self.explained_variance:.4f}")
569
+ if self.book_returns is not None:
570
+ lines.append(f"Book returns shape: {getattr(self.book_returns, 'shape', None)}")
571
+ lines += ["", "=" * 80]
572
+ return "\n".join(lines)
573
+
@@ -11,6 +11,7 @@ This module provides:
11
11
  from .ddfm_dataset import DDFMDataset
12
12
  from .dfm_dataset import DFMDataset
13
13
  from .ivdfm_dataset import iVDFMDataset
14
+ from .afm_dataset import AFMDataset
14
15
  from .time import TimeIndex
15
16
 
16
17
  __all__ = [
@@ -18,6 +19,7 @@ __all__ = [
18
19
  'DDFMDataset',
19
20
  'DFMDataset',
20
21
  'iVDFMDataset',
22
+ 'AFMDataset',
21
23
  # Time utilities
22
24
  'TimeIndex',
23
25
  ]