nirvana-quant 1.0.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.
nirvana/manifold.py ADDED
@@ -0,0 +1,184 @@
1
+ """
2
+ Base-12 Dual-Clock Manifold Module
3
+ ===================================
4
+ Maps linear discrete time onto a 2-Torus manifold (S1 x S1) using Base-12 harmonic octaves.
5
+ Synchronizes macro-economic calendar time (Quarterly BCTC cycle) with micro-information flow.
6
+ """
7
+
8
+ import math
9
+ from dataclasses import dataclass
10
+ from typing import Optional, List, Tuple, Union
11
+ import numpy as np
12
+ import pandas as pd
13
+
14
+
15
+ @dataclass
16
+ class DualClockCoordinates:
17
+ macro_sin_1: float
18
+ macro_cos_1: float
19
+ macro_sin_2: float
20
+ macro_cos_2: float
21
+ micro_sin_1: float
22
+ micro_cos_1: float
23
+ micro_sin_2: float
24
+ micro_cos_2: float
25
+ resonance_gamma: float
26
+
27
+ def to_vector(self) -> List[float]:
28
+ return [
29
+ self.macro_sin_1,
30
+ self.macro_cos_1,
31
+ self.macro_sin_2,
32
+ self.macro_cos_2,
33
+ self.micro_sin_1,
34
+ self.micro_cos_1,
35
+ self.micro_sin_2,
36
+ self.micro_cos_2,
37
+ self.resonance_gamma,
38
+ ]
39
+
40
+
41
+ class Base12Manifold:
42
+ """
43
+ Transforms arbitrary time series into Base-12 Dual-Clock Manifold.
44
+ - Macro Octave 1: Primary structural cycle (default 63 sessions / quarter)
45
+ - Macro Octave 2: Secondary macro cycle (default 252 sessions / year)
46
+ - Micro Octaves: Information / Volume quanta
47
+ Supports custom macro_periods for cross-domain adaptation (e.g. 24h & 168h for network traffic).
48
+ """
49
+
50
+ def __init__(
51
+ self,
52
+ volume_quantum: float = 1000.0,
53
+ macro_periods: Optional[Tuple[float, float]] = None,
54
+ ):
55
+ self.volume_quantum = max(volume_quantum, 1e-4)
56
+ self.macro_periods = (
57
+ (float(macro_periods[0]), float(macro_periods[1]))
58
+ if macro_periods is not None
59
+ else (63.0, 252.0)
60
+ )
61
+
62
+ @staticmethod
63
+ def estimate_dominant_periods(
64
+ signal: Union[np.ndarray, pd.Series], top_k: int = 2
65
+ ) -> List[float]:
66
+ """
67
+ Estimates dominant cyclical periods from a raw 1D time-series using FFT spectral power.
68
+ Useful for uncalibrated non-financial domains (IoT sensors, cyber packet rates, climate).
69
+ """
70
+ s = np.asarray(signal, dtype=float)
71
+ s = s[~np.isnan(s)]
72
+ if len(s) < 16:
73
+ return [63.0, 252.0]
74
+ # Linear detrending to remove baseline drift
75
+ s_detrend = s - np.linspace(s[0], s[-1], len(s))
76
+ fft_vals = np.fft.rfft(s_detrend)
77
+ freqs = np.fft.rfftfreq(len(s_detrend))
78
+ power = np.abs(fft_vals) ** 2
79
+ power[0] = 0.0 # Zero DC component
80
+
81
+ idx_sorted = np.argsort(power)[::-1]
82
+ periods = []
83
+ for idx in idx_sorted:
84
+ if freqs[idx] > 0:
85
+ p = 1.0 / freqs[idx]
86
+ if 2.0 <= p <= len(s):
87
+ periods.append(float(round(p, 1)))
88
+ if len(periods) == top_k:
89
+ break
90
+ return periods if len(periods) == top_k else [63.0, 252.0]
91
+
92
+ def transform(
93
+ self,
94
+ calendar_index: int,
95
+ cumulative_volume: float = 0.0,
96
+ ) -> DualClockCoordinates:
97
+ p1, p2 = self.macro_periods
98
+ theta_m1 = (2.0 * math.pi * (calendar_index % p1)) / p1
99
+ m_sin_1 = math.sin(theta_m1)
100
+ m_cos_1 = math.cos(theta_m1)
101
+
102
+ theta_m2 = (2.0 * math.pi * (calendar_index % p2)) / p2
103
+ m_sin_2 = math.sin(theta_m2)
104
+ m_cos_2 = math.cos(theta_m2)
105
+
106
+ # Volume/Information Time (Micro Base-12)
107
+ volume_bars = int(cumulative_volume / self.volume_quantum)
108
+ theta_v1 = (2.0 * math.pi * (volume_bars % 12)) / 12.0
109
+ v_sin_1 = math.sin(theta_v1)
110
+ v_cos_1 = math.cos(theta_v1)
111
+
112
+ theta_v2 = (2.0 * math.pi * (volume_bars % 144)) / 144.0
113
+ v_sin_2 = math.sin(theta_v2)
114
+ v_cos_2 = math.cos(theta_v2)
115
+
116
+ # Phase Resonance Index: gamma(t) = cos(theta_macro - theta_micro)
117
+ resonance_gamma = math.cos(theta_m1 - theta_v1)
118
+
119
+ return DualClockCoordinates(
120
+ macro_sin_1=m_sin_1,
121
+ macro_cos_1=m_cos_1,
122
+ macro_sin_2=m_sin_2,
123
+ macro_cos_2=m_cos_2,
124
+ micro_sin_1=v_sin_1,
125
+ micro_cos_1=v_cos_1,
126
+ micro_sin_2=v_sin_2,
127
+ micro_cos_2=v_cos_2,
128
+ resonance_gamma=resonance_gamma,
129
+ )
130
+
131
+ def batch_transform(
132
+ self,
133
+ calendar_indices: Union[np.ndarray, pd.Series, list, range, int],
134
+ cumulative_volumes: Optional[Union[np.ndarray, pd.Series, list]] = None,
135
+ dates: Optional[Union[pd.DatetimeIndex, list, np.ndarray]] = None,
136
+ ) -> np.ndarray:
137
+ """
138
+ Batch transformation for pandas Series / numpy arrays.
139
+ Supports actual calendar dates or empirical trading sessions.
140
+ Returns shape (N, 9).
141
+ """
142
+ cal_idx = np.atleast_1d(np.asarray(calendar_indices, dtype=int))
143
+ n = len(cal_idx)
144
+
145
+ if cumulative_volumes is None:
146
+ cum_vol = np.zeros(n, dtype=float)
147
+ else:
148
+ cum_vol = np.nan_to_num(np.asarray(cumulative_volumes, dtype=float), nan=0.0)
149
+ if len(cum_vol) != n:
150
+ cum_vol = np.resize(cum_vol, n)
151
+
152
+ p1, p2 = self.macro_periods
153
+ if dates is not None and self.macro_periods == (63.0, 252.0):
154
+ try:
155
+ dt_idx = pd.to_datetime(dates)
156
+ day_of_year = dt_idx.dayofyear.values
157
+ quarter_len = 365.25 / 4.0
158
+ theta_m1 = (2.0 * np.pi * (day_of_year % quarter_len)) / quarter_len
159
+ theta_m2 = (2.0 * np.pi * (day_of_year / 365.25))
160
+ except Exception:
161
+ theta_m1 = (2.0 * np.pi * (cal_idx % p1)) / p1
162
+ theta_m2 = (2.0 * np.pi * (cal_idx % p2)) / p2
163
+ else:
164
+ theta_m1 = (2.0 * np.pi * (cal_idx % p1)) / p1
165
+ theta_m2 = (2.0 * np.pi * (cal_idx % p2)) / p2
166
+
167
+ v_bars = np.maximum((cum_vol / self.volume_quantum).astype(int), 0)
168
+ theta_v1 = (2.0 * np.pi * (v_bars % 12)) / 12.0
169
+ theta_v2 = (2.0 * np.pi * (v_bars % 144)) / 144.0
170
+
171
+ gamma = np.cos(theta_m1 - theta_v1)
172
+
173
+ features = np.column_stack([
174
+ np.sin(theta_m1),
175
+ np.cos(theta_m1),
176
+ np.sin(theta_m2),
177
+ np.cos(theta_m2),
178
+ np.sin(theta_v1),
179
+ np.cos(theta_v1),
180
+ np.sin(theta_v2),
181
+ np.cos(theta_v2),
182
+ gamma,
183
+ ])
184
+ return features
nirvana/multiplex.py ADDED
@@ -0,0 +1,75 @@
1
+ """
2
+ Multiplex Network Fusion Engine
3
+ ================================
4
+ Couples Slow Fundamental Layer (Quarterly Financial Reports: CAMEL/NPL/CAR)
5
+ with Fast High-Frequency Layer (Daily Stock Return / Volatility).
6
+
7
+ Solves the multi-timescale mismatch in banking systemic risk modeling.
8
+ """
9
+
10
+ from typing import Dict, List, Optional, Union
11
+ import numpy as np
12
+ import pandas as pd
13
+
14
+ from .network import DEFAULT_ASSET_WEIGHTS
15
+
16
+
17
+ class MultiplexNetworkEngine:
18
+ """
19
+ Two-layer multiplex network for financial systems:
20
+ - Layer 1 (Fundamental Macro/Balance-sheet): Low-frequency (Quarterly)
21
+ - Layer 2 (Market Microstructure/Price): High-frequency (Daily)
22
+ """
23
+
24
+ def __init__(self, coupling_strength: float = 0.35):
25
+ self.coupling_strength = float(np.clip(coupling_strength, 0.0, 1.0))
26
+
27
+ def fuse_layers(
28
+ self,
29
+ market_connectivity: pd.DataFrame,
30
+ balance_sheet_weights: Optional[Union[Dict[str, float], bool, str]] = None,
31
+ interbank_exposures: Optional[pd.DataFrame] = None,
32
+ ) -> pd.DataFrame:
33
+ """
34
+ Fuses the market risk connectedness matrix with fundamental balance-sheet constraints.
35
+
36
+ Parameters:
37
+ - market_connectivity: (N, N) DataFrame of market spillovers.
38
+ - balance_sheet_weights: Dict of {Bank: systemic_weight} based on Total Assets, NPL, or CAR.
39
+ - interbank_exposures: (N, N) matrix of bilateral interbank deposits/lendings if available.
40
+ """
41
+ fused = market_connectivity.copy()
42
+ excluded = {"FROM_others", "TO_others", "NET_spillover"}
43
+ banks = [c for c in fused.columns if c in fused.index and c not in excluded]
44
+ fused_core = fused.loc[banks, banks].values.astype(float)
45
+
46
+ if balance_sheet_weights is True or balance_sheet_weights == "default":
47
+ balance_sheet_weights = DEFAULT_ASSET_WEIGHTS
48
+
49
+ # 1. Fundamental asset size / fragility modulation
50
+ if balance_sheet_weights:
51
+ weights = np.array([balance_sheet_weights.get(b, 1.0) for b in banks], dtype=float)
52
+ mean_w = np.mean(weights) if np.mean(weights) > 0 else 1.0
53
+ weights = weights / mean_w
54
+ # Modulate outgoing risk by sender bank's fundamental vulnerability (Column j = Transmitter)
55
+ fused_core = fused_core * weights[None, :]
56
+
57
+ # 2. Interbank bilateral direct exposure network injection
58
+ if interbank_exposures is not None:
59
+ ib_core = interbank_exposures.reindex(index=banks, columns=banks, fill_value=0.0).values.astype(float)
60
+ ib_row_sums = np.sum(ib_core, axis=1, keepdims=True)
61
+ has_ib = (ib_row_sums > 0)
62
+ ib_norm = np.where(has_ib, (ib_core / (ib_row_sums + 1e-8)) * 100.0, fused_core)
63
+ fused_core = np.where(
64
+ has_ib,
65
+ (1.0 - self.coupling_strength) * fused_core + self.coupling_strength * ib_norm,
66
+ fused_core,
67
+ )
68
+
69
+ # Re-normalize rows to 100%
70
+ row_sums = fused_core.sum(axis=1, keepdims=True)
71
+ row_sums[row_sums == 0] = 1.0
72
+ fused_core = (fused_core / row_sums) * 100.0
73
+
74
+ res_df = pd.DataFrame(np.round(fused_core, 2), index=banks, columns=banks)
75
+ return res_df
nirvana/multiverse.py ADDED
@@ -0,0 +1,217 @@
1
+ """
2
+ Multiverse Counterfactual Reconstruction Engine
3
+ ===============================================
4
+ Synthesizes unobserved latent market volatility for pre-IPO periods (2017 - 2020)
5
+ anchored on:
6
+ 1. Structural Peer-Group Dynamics (Big4, Tier-1 Retail, Tier-2 Commercial, Tier-3 Regional)
7
+ 2. Macroeconomic Market Factors (VN-Index volatility & co-movement)
8
+ 3. Base-12 Dual-Clock Manifold Harmonics (Calendar & Quarterly BCTC cycles)
9
+ 4. Multiverse Ensemble Simulation with Phase 2 Apoptosis Pruning
10
+
11
+ Eliminates the 'IPO jump shock' (nén lò xo) and enables 100% continuous,
12
+ academically rigorous systemic risk modeling across all 27 banks (2017 - 2025).
13
+ """
14
+
15
+ from typing import Dict, List, Optional, Tuple
16
+ import numpy as np
17
+ import pandas as pd
18
+
19
+ from .manifold import Base12Manifold
20
+
21
+
22
+ from .domains.banking import DEFAULT_BANKING_TIERS
23
+
24
+
25
+ class MultiverseReconstructor:
26
+ """
27
+ Multiverse Counterfactual Synthesis Engine.
28
+ """
29
+
30
+ def __init__(
31
+ self,
32
+ n_universes: int = 3,
33
+ boundary_window: int = 30,
34
+ apoptosis_threshold: float = 2.5,
35
+ reg_lambda: float = 0.5,
36
+ tier_mapping: Optional[Dict[str, List[str]]] = None,
37
+ volume_quantum: float = 1000.0,
38
+ ):
39
+ self.n_universes = n_universes
40
+ self.boundary_window = boundary_window
41
+ self.apoptosis_threshold = apoptosis_threshold
42
+ self.reg_lambda = reg_lambda
43
+ self.tier_mapping = tier_mapping or DEFAULT_BANKING_TIERS
44
+ self.volume_quantum = volume_quantum
45
+ self.manifold = Base12Manifold(volume_quantum=volume_quantum)
46
+
47
+ def _get_tier_for_bank(self, bank: str, active_tiers: Dict[str, List[str]]) -> str:
48
+ for tier_name, banks in active_tiers.items():
49
+ if bank in banks:
50
+ return tier_name
51
+ return list(active_tiers.keys())[0] if active_tiers else "TIER2_COMMERCIAL"
52
+
53
+ @staticmethod
54
+ def _deterministic_seed(text: str) -> int:
55
+ return sum((i + 1) * ord(c) * (31 ** (i % 7)) for i, c in enumerate(text)) % (2**31)
56
+
57
+ def reconstruct(
58
+ self,
59
+ bank_panel: pd.DataFrame,
60
+ market_index: Optional[pd.Series] = None,
61
+ reg_lambda: Optional[float] = None,
62
+ boundary_window: Optional[int] = None,
63
+ apoptosis_threshold: Optional[float] = None,
64
+ tier_mapping: Optional[Dict[str, List[str]]] = None,
65
+ ) -> pd.DataFrame:
66
+ """
67
+ Synthesizes latent volatility for unlisted historical dates across all banks.
68
+
69
+ Parameters:
70
+ - bank_panel: (T, N) DataFrame of daily Parkinson log-volatility (2017 - 2025).
71
+ - market_index: Optional Series of daily market index (e.g. VNINDEX).
72
+ """
73
+ active_lambda = self.reg_lambda if reg_lambda is None else reg_lambda
74
+ active_window = self.boundary_window if boundary_window is None else boundary_window
75
+ active_thresh = self.apoptosis_threshold if apoptosis_threshold is None else apoptosis_threshold
76
+ active_tiers = self.tier_mapping if tier_mapping is None else tier_mapping
77
+
78
+ df = bank_panel.copy()
79
+ dates = df.index
80
+ n_dates, n_banks = df.shape
81
+ calendar_idx = np.arange(n_dates)
82
+
83
+ # 1. Base-12 Manifold Harmonic Features (Quarterly & Annual cycles)
84
+ manifold_features = self.manifold.batch_transform(calendar_idx, dates=dates)
85
+ m_cos_1 = manifold_features[:, 1]
86
+ m_sin_1 = manifold_features[:, 0]
87
+ m_cos_2 = manifold_features[:, 3]
88
+ m_sin_2 = manifold_features[:, 2]
89
+
90
+ # 2. Market Baseline Factor (harmonized to annualized log-volatility scale)
91
+ if market_index is not None:
92
+ aligned_mkt = market_index.reindex(dates).ffill().bfill()
93
+ if float(np.nanmean(aligned_mkt)) > 10.0:
94
+ # Price index (e.g. VN-Index closing levels): compute annualized log-volatility percentage
95
+ mkt_ret = np.log(np.maximum(aligned_mkt / aligned_mkt.shift(1).bfill(), 1e-8)).values
96
+ mkt_vol = np.log(100.0 * np.sqrt(252.0 * np.maximum(mkt_ret ** 2, 1e-4)))
97
+ else:
98
+ mkt_vol = aligned_mkt.values
99
+ else:
100
+ # Fallback: cross-sectional average of core9 always-active banks
101
+ core_banks = [c for c in ["VCB", "CTG", "BID", "ACB", "MBB"] if c in df.columns]
102
+ mkt_vol = df[core_banks].mean(axis=1).values
103
+
104
+ # 3. Process each bank individually
105
+ reconstructed = df.copy()
106
+
107
+ for bank in df.columns:
108
+ series = df[bank].values
109
+ first_idx = df[bank].first_valid_index()
110
+ tier_name = self._get_tier_for_bank(bank, active_tiers)
111
+ peer_banks = [
112
+ b for b in active_tiers.get(tier_name, [])
113
+ if b in df.columns and b != bank
114
+ ]
115
+
116
+ # Compute historical peer average (using available peers at each t)
117
+ if len(peer_banks) > 0:
118
+ peer_vol = df[peer_banks].mean(axis=1).bfill().ffill().fillna(0.0).values
119
+ else:
120
+ peer_vol = df.mean(axis=1).bfill().ffill().fillna(0.0).values
121
+
122
+ system_vol = df.mean(axis=1).bfill().ffill().fillna(0.0).values
123
+
124
+ # If bank has zero observed data across the entire panel, synthesize from peer tier
125
+ if first_idx is None:
126
+ rng = np.random.default_rng(seed=self._deterministic_seed(bank))
127
+ y_synth_all = peer_vol + rng.normal(0, 0.2, n_dates)
128
+ reconstructed[bank] = y_synth_all
129
+ continue
130
+
131
+ first_pos = int(df.index.get_loc(first_idx))
132
+
133
+ # If bank was already listed from day 1, no synthesis needed
134
+ if first_pos == 0:
135
+ continue
136
+
137
+ # Target bank was unlisted for dates: [0 : first_pos]
138
+ # Training period: [first_pos : n_dates]
139
+
140
+ # Feature matrix: [mkt_vol, peer_vol, system_vol, m_cos_1, m_sin_1]
141
+ X = np.column_stack([
142
+ mkt_vol,
143
+ peer_vol,
144
+ system_vol,
145
+ m_cos_1,
146
+ m_sin_1,
147
+ np.ones(n_dates),
148
+ ])
149
+ np.nan_to_num(X, copy=False, nan=0.0)
150
+
151
+ # Observed training set
152
+ X_train = X[first_pos:]
153
+ y_train = series[first_pos:]
154
+ np.nan_to_num(y_train, copy=False, nan=0.0)
155
+
156
+ # Regularized least squares (Ridge regression) to learn fundamental sensitivity
157
+ reg_lambda_val = active_lambda
158
+ eye = np.eye(X.shape[1])
159
+ eye[-1, -1] = 0.0 # Do not regularize intercept
160
+ beta = np.linalg.pinv(X_train.T @ X_train + reg_lambda_val * eye) @ (X_train.T @ y_train)
161
+
162
+ # Calculate empirical residual standard deviation from observed period
163
+ residuals_train = y_train - (X_train @ beta)
164
+ sigma_noise = max(float(np.std(residuals_train)), 0.15)
165
+
166
+ # 4. Generate Multiverse Predictions with calibrated stochastic noise
167
+ # ponytail: deterministic invariant seed per bank for 100% cross-platform reproducibility
168
+ rng = np.random.default_rng(seed=self._deterministic_seed(bank))
169
+ X_unlisted = X[:first_pos]
170
+ y_pred_base = X_unlisted @ beta
171
+
172
+ # Universe 1: Core Fundamental Benchmark with real residual variance
173
+ u1 = y_pred_base + rng.normal(0, sigma_noise, first_pos)
174
+
175
+ # Universe 2: Liquidity Stress Regime (Correlated with active peers)
176
+ u2 = 0.6 * peer_vol[:first_pos] + 0.4 * y_pred_base + rng.normal(0, sigma_noise * 1.1, first_pos)
177
+
178
+ # Universe 3: Macroeconomic Market Anchor Regime
179
+ u3 = y_pred_base + 0.25 * (mkt_vol[:first_pos] - np.mean(mkt_vol)) + rng.normal(0, sigma_noise * 0.9, first_pos)
180
+
181
+ # Stack multiverse branches
182
+ multiverse_matrix = np.column_stack([u1, u2, u3])
183
+
184
+ # Meta-Ensemble with Apoptosis Pruning:
185
+ # Excise branches that diverge beyond apoptosis_threshold std devs from median
186
+ median_pred = np.median(multiverse_matrix, axis=1)
187
+ residuals = np.abs(multiverse_matrix - median_pred[:, None])
188
+ std_proxy = np.std(residuals) + 1e-6
189
+
190
+ weights = np.where(residuals / std_proxy > active_thresh, 0.0, 1.0)
191
+ weights_sum = weights.sum(axis=1, keepdims=True)
192
+ weights_sum[weights_sum == 0] = 1.0
193
+ norm_weights = weights / weights_sum
194
+
195
+ y_synthetic = np.sum(multiverse_matrix * norm_weights, axis=1)
196
+
197
+ # 5. Smooth Boundary Bridging (Hermite polynomial transition)
198
+ # Use post-IPO equilibrium median (first 5 sessions) to avoid bridging into ±40% first-day IPO spikes
199
+ post_ipo_eq = float(np.nanmedian(series[first_pos : first_pos + min(5, len(series) - first_pos)]))
200
+ if np.isnan(post_ipo_eq):
201
+ post_ipo_eq = float(series[first_pos])
202
+ window = min(active_window, first_pos)
203
+
204
+ if window > 0:
205
+ bridge_synthetic = y_synthetic[-window:]
206
+ # Smooth S-curve (cubic Hermite) transition weights from 0 to 1
207
+ t_norm = np.linspace(0.0, 1.0, window)
208
+ smooth_ramp = t_norm * t_norm * (3.0 - 2.0 * t_norm)
209
+ delta = post_ipo_eq - bridge_synthetic[-1]
210
+ y_synthetic[-window:] = bridge_synthetic + smooth_ramp * delta
211
+
212
+ # Inject synthesized values into panel
213
+ reconstructed.iloc[:first_pos, reconstructed.columns.get_loc(bank)] = y_synthetic
214
+
215
+ # Intermittent post-IPO trading halts / holiday suspensions fallback
216
+ reconstructed = reconstructed.ffill().bfill().fillna(0.0)
217
+ return reconstructed