# ─── MLE Estimation ──────────────────────────────────────────────────────────
def estimate_rs_ou(data, dt, label=''):
"""Estimate 2-state RS-OU model via MLE with multiple restarts."""
# Smart initial values based on data split
mid = len(data) // 2
mean1, mean2 = np.mean(data[:mid]), np.mean(data[mid:])
std1, std2 = np.std(data[:mid]), np.std(data[mid:])
# Ensure regime 1 is the high-spread regime
if mean1 < mean2:
mean1, mean2 = mean2, mean1
std1, std2 = std2, std1
# Multiple starting points
starts = [
[1.0, mean1, std1*np.sqrt(12)*0.5, 2.0, mean2, std2*np.sqrt(12)*0.5, 0.95, 0.97],
[0.5, mean1*1.1, std1*np.sqrt(12)*0.3, 3.0, mean2*0.9, std2*np.sqrt(12)*0.3, 0.90, 0.95],
[2.0, mean1, std1*np.sqrt(12)*0.8, 1.0, mean2, std2*np.sqrt(12)*0.8, 0.97, 0.98],
[0.3, np.percentile(data, 75), 5.0, 5.0, np.percentile(data, 25), 2.0, 0.93, 0.96],
]
bounds = [
(0.01, 50), (0, 35), (0.01, 20), # κ1, θ1, σ1
(0.01, 50), (0, 35), (0.01, 20), # κ2, θ2, σ2
(0.5, 0.999), (0.5, 0.999) # p11, p22
]
best_nll = np.inf
best_result = None
for i, x0 in enumerate(starts):
try:
result = minimize(
rs_ou_neg_log_likelihood, x0, args=(data, dt),
method='L-BFGS-B', bounds=bounds,
options={'maxiter': 20000, 'ftol': 1e-12}
)
if result.fun < best_nll and result.success:
best_nll = result.fun
best_result = result
except Exception:
pass
if best_result is None:
print(f' WARNING: Optimization failed for {label}')
return None
params = best_result.x
kappa1, theta1, sigma1, kappa2, theta2, sigma2, p11, p22 = params
# Ensure regime 1 is high-spread, regime 2 is low-spread
if theta1 < theta2:
kappa1, kappa2 = kappa2, kappa1
theta1, theta2 = theta2, theta1
sigma1, sigma2 = sigma2, sigma1
p11, p22 = p22, p11
params = [kappa1, theta1, sigma1, kappa2, theta2, sigma2, p11, p22]
# Get filtered and smoothed probabilities
ll, filtered = rs_ou_hamilton_filter(params, data, dt)
smoothed = kim_smoother(filtered, p11, p22)
# Expected duration of each regime (in months)
duration1 = 1 / (1 - p11) if p11 < 1 else np.inf
duration2 = 1 / (1 - p22) if p22 < 1 else np.inf
# BIC for model comparison
n_params = 8
bic = n_params * np.log(len(data)) - 2 * ll
results = {
'kappa1': kappa1, 'theta1': theta1, 'sigma1': sigma1,
'kappa2': kappa2, 'theta2': theta2, 'sigma2': sigma2,
'p11': p11, 'p22': p22,
'duration1': duration1, 'duration2': duration2,
'log_likelihood': ll, 'bic': bic,
'filtered': filtered, 'smoothed': smoothed
}
if label:
print(f'\n══════════════════════════════════════════════════════════════')
print(f' RS-OU MLE Results — {label}')
print(f'══════════════════════════════════════════════════════════════')
print(f' Regime 1 (High-Spread):')
print(f' κ₁ = {kappa1:.4f}, θ₁ = {theta1:.4f}%, σ₁ = {sigma1:.4f}')
print(f' Expected duration: {duration1:.1f} months')
print(f' Regime 2 (Mature/Low-Spread):')
print(f' κ₂ = {kappa2:.4f}, θ₂ = {theta2:.4f}%, σ₂ = {sigma2:.4f}')
print(f' Expected duration: {duration2:.1f} months')
print(f' Transition: p₁₁ = {p11:.4f}, p₂₂ = {p22:.4f}')
print(f' Log-likelihood: {ll:.4f}')
print(f' BIC: {bic:.4f}')
print(f'══════════════════════════════════════════════════════════════')
return results
print('Estimating RS-OU model...')
rs_usd = estimate_rs_ou(S_usd, dt, 'USD Spread')
rs_khr = estimate_rs_ou(S_khr, dt, 'KHR Spread')