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/__init__.py +75 -0
- nirvana/domains/__init__.py +41 -0
- nirvana/domains/banking.py +77 -0
- nirvana/domains/consumer.py +132 -0
- nirvana/domains/cybersecurity.py +58 -0
- nirvana/domains/substitute.py +79 -0
- nirvana/domains/supply_chain.py +56 -0
- nirvana/experimental/__init__.py +22 -0
- nirvana/experimental/multiverse_invariance.py +269 -0
- nirvana/kalman.py +282 -0
- nirvana/manifold.py +184 -0
- nirvana/multiplex.py +75 -0
- nirvana/multiverse.py +217 -0
- nirvana/network.py +675 -0
- nirvana/proof.py +958 -0
- nirvana/stata.py +497 -0
- nirvana/static.py +774 -0
- nirvana/tree.py +497 -0
- nirvana/volatility.py +121 -0
- nirvana_quant-1.0.0.dist-info/METADATA +168 -0
- nirvana_quant-1.0.0.dist-info/RECORD +24 -0
- nirvana_quant-1.0.0.dist-info/WHEEL +5 -0
- nirvana_quant-1.0.0.dist-info/licenses/LICENSE +239 -0
- nirvana_quant-1.0.0.dist-info/top_level.txt +2 -0
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
|