110 lines
4.5 KiB
Python
110 lines
4.5 KiB
Python
|
|
#!/usr/bin/env python3
|
||
|
|
|
||
|
|
# KKT_Nachweis.py
|
||
|
|
"""
|
||
|
|
Kapitel QP/NLP: Die KKT-Bedingungen numerisch nachpruefen.
|
||
|
|
|
||
|
|
Loest ein QP mit CVXPY, liest die Dualwerte aus und prueft alle vier
|
||
|
|
KKT-Bedingungen einzeln nach. Das ist zugleich eine Vorlage fuer die
|
||
|
|
Qualitaetssicherung eigener Modelle.
|
||
|
|
"""
|
||
|
|
|
||
|
|
import numpy as np
|
||
|
|
import cvxpy as cp
|
||
|
|
|
||
|
|
|
||
|
|
def baue_gueltige_kovarianz(vola, korrelationen, seed=0):
|
||
|
|
"""
|
||
|
|
Baut Sigma = D * C * D aus Volatilitaeten und einer Korrelationsmatrix.
|
||
|
|
Dieses Vorgehen ist konstruktionsbedingt positiv semidefinit - im
|
||
|
|
Gegensatz zum nachtraeglichen Ueberschreiben der Diagonalen, das die
|
||
|
|
positive Semidefinitheit zerstoeren kann.
|
||
|
|
"""
|
||
|
|
C = np.array(korrelationen, dtype=float)
|
||
|
|
assert np.allclose(C, C.T), "Korrelationsmatrix muss symmetrisch sein."
|
||
|
|
assert np.allclose(np.diag(C), 1.0), "Diagonale der Korrelationsmatrix muss 1 sein."
|
||
|
|
eigen = np.linalg.eigvalsh(C)
|
||
|
|
assert eigen.min() > -1e-10, (
|
||
|
|
f"Korrelationsmatrix ist nicht positiv semidefinit "
|
||
|
|
f"(kleinster Eigenwert {eigen.min():.4f}). Solche Korrelationen sind unmoeglich.")
|
||
|
|
D = np.diag(vola)
|
||
|
|
return D @ C @ D
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
# --- Ein kleines Portfolio-QP ----------------------------------------
|
||
|
|
vola = np.array([0.20, 0.14, 0.30]) # Volatilitaeten
|
||
|
|
korr = [[1.00, 0.30, 0.10],
|
||
|
|
[0.30, 1.00, 0.25],
|
||
|
|
[0.10, 0.25, 1.00]]
|
||
|
|
Sigma = baue_gueltige_kovarianz(vola, korr)
|
||
|
|
mu = np.array([0.09, 0.05, 0.13]) # erwartete Renditen
|
||
|
|
lam = 3.0 # Risikoaversion
|
||
|
|
|
||
|
|
eigenwerte = np.linalg.eigvalsh(Sigma)
|
||
|
|
print("=" * 74)
|
||
|
|
print(" KKT-BEDINGUNGEN AM PORTFOLIO-QP")
|
||
|
|
print("=" * 74)
|
||
|
|
print(f"Eigenwerte von Sigma: {np.round(eigenwerte, 6)}")
|
||
|
|
print(f" -> positiv definit: {bool(eigenwerte.min() > 0)} "
|
||
|
|
f"(Problem ist streng konvex, Loesung eindeutig)")
|
||
|
|
print(f" -> Konditionszahl: {eigenwerte.max() / eigenwerte.min():.2f}")
|
||
|
|
|
||
|
|
# --- Modell: min lam/2 * w'Sigma w - mu'w u.d.N. sum(w)=1, w>=0 ----
|
||
|
|
n = len(mu)
|
||
|
|
w = cp.Variable(n)
|
||
|
|
ziel = cp.Minimize(0.5 * lam * cp.quad_form(w, Sigma) - mu @ w)
|
||
|
|
budget = cp.sum(w) == 1
|
||
|
|
nichtnegativ = w >= 0
|
||
|
|
problem = cp.Problem(ziel, [budget, nichtnegativ])
|
||
|
|
problem.solve()
|
||
|
|
|
||
|
|
w_opt = w.value
|
||
|
|
nu = budget.dual_value # Multiplikator der Gleichung
|
||
|
|
lam_i = nichtnegativ.dual_value # Multiplikatoren der Ungleichungen
|
||
|
|
|
||
|
|
print(f"\nStatus: {problem.status}")
|
||
|
|
print(f"Optimale Gewichte: {np.round(w_opt, 6)}")
|
||
|
|
print(f"Zielwert: {problem.value:.6f}")
|
||
|
|
print(f"Multiplikator der Budgetgleichung (nu): {nu:.6f}")
|
||
|
|
print(f"Multiplikatoren der w>=0-Bedingungen: {np.round(lam_i, 6)}")
|
||
|
|
|
||
|
|
# --- KKT-Bedingungen einzeln pruefen ---------------------------------
|
||
|
|
print("\n--- Pruefung der vier KKT-Bedingungen ---")
|
||
|
|
|
||
|
|
# 1. Stationaritaet: grad f - lambda + nu*1 = 0
|
||
|
|
# f(w) = lam/2 w'Sigma w - mu'w -> grad f = lam*Sigma w - mu
|
||
|
|
# g_i(w) = -w_i <= 0 -> grad g_i = -e_i
|
||
|
|
# h(w) = sum(w) - 1 = 0 -> grad h = 1
|
||
|
|
grad_f = lam * (Sigma @ w_opt) - mu
|
||
|
|
stationaritaet = grad_f - lam_i + nu * np.ones(n)
|
||
|
|
print(f"1. Stationaritaet : max|Residuum| = {np.abs(stationaritaet).max():.2e}")
|
||
|
|
|
||
|
|
# 2. Primale Zulaessigkeit
|
||
|
|
print(f"2. Primal zulaessig : sum(w)-1 = {w_opt.sum()-1:.2e}, "
|
||
|
|
f"min(w) = {w_opt.min():.2e}")
|
||
|
|
|
||
|
|
# 3. Duale Zulaessigkeit
|
||
|
|
print(f"3. Dual zulaessig : min(lambda) = {lam_i.min():.2e} (muss >= 0 sein)")
|
||
|
|
|
||
|
|
# 4. Komplementaerer Schlupf: lambda_i * w_i = 0
|
||
|
|
print(f"4. Kompl. Schlupf : max|lambda_i * w_i| = "
|
||
|
|
f"{np.abs(lam_i * w_opt).max():.2e}")
|
||
|
|
|
||
|
|
alle_ok = (np.abs(stationaritaet).max() < 1e-6
|
||
|
|
and abs(w_opt.sum() - 1) < 1e-8
|
||
|
|
and w_opt.min() > -1e-8
|
||
|
|
and lam_i.min() > -1e-8
|
||
|
|
and np.abs(lam_i * w_opt).max() < 1e-6)
|
||
|
|
print(f"\nAlle vier KKT-Bedingungen erfuellt: {alle_ok}")
|
||
|
|
|
||
|
|
# --- Interpretation von nu -------------------------------------------
|
||
|
|
print("\n--- Was bedeutet nu? ---")
|
||
|
|
print("nu ist der Schattenpreis des Budgets: Um so viel aendert sich der")
|
||
|
|
print("Zielwert, wenn man statt 100 % nur 99 % investieren duerfte.")
|
||
|
|
problem2 = cp.Problem(cp.Minimize(0.5 * lam * cp.quad_form(w, Sigma) - mu @ w),
|
||
|
|
[cp.sum(w) == 1.01, w >= 0])
|
||
|
|
problem2.solve()
|
||
|
|
print(f" Vorhergesagt (nu * 0.01): {nu * 0.01:+.6f}")
|
||
|
|
print(f" Tatsaechlich gemessen: {problem2.value - problem.value:+.6f}")
|
||
|
|
print("=" * 74)
|