#!/usr/bin/env python3
"""annahmen-ableiten.py — erzeugt die OEFFENTLICHEN Anbieter-Dateien annahmen/daten/<id>.json
aus den PRIVATEN Rohdaten annahmen/quellen/<id>.json.

Warum (Entscheid des Betreibers, 25.10.2026): Die vollstaendigen Annahmen der Vermoegensverwalter,
vor allem die Korrelationsmatrizen, werden nicht weiterverbreitet, solange keine Erlaubnis
vorliegt (Nutzungsbedingungen der Anbieter, EU-Datenbankschutz fuer vacatum.de).
Oeffentlich ist nur:
  * was die Seiten mit Quelle zitieren (Rendite, Volatilitaet einzelner Anlagen, Szenarien),
  * vorab gerechnete Kennzahlen der Beispielportfolios, aus denen sich die Matrix nicht
    zurueckrechnen laesst (je Portfolio-Variante a_rest, v_rest, k_rest; siehe
    kennzahlenVariante im Rechenkern).
annahmen/quellen/ wird von tools/veroeffentlichen.sh nicht ausgeliefert.

Die Portfolios sind HIER definiert — gleiche Gewichte wie auf den Seiten (annahmen/jpm-2027.html,
annahmen/jpm-chf.html, annahmen/index.html); tools/ltcma-pruefen.py vergleicht die abgeleiteten
Werte mit der Rechnung auf der vollen Matrix.

Nachrechnen (oeffentlich unter https://vacatum.ch/annahmen/annahmen-ableiten.py):
    Die Rohdaten liegen nicht bei, weil wir die vollstaendigen Datensaetze der Anbieter nicht
    weiterverbreiten. Wer nachrechnen will, laedt die Originaldaten beim Anbieter (Links auf der
    jeweiligen Seite unter «Quellen») und legt sie als annahmen/quellen/<id>.json ab; welche Felder
    gelesen werden, steht in den Funktionen unten (z. B. rendite_geometrisch, volatilitaet,
    korrelation_untere_dreiecksmatrix in Prozent bzw. als Zahl). Das Ergebnis muss den Dateien unter
    /annahmen/daten/ entsprechen.

Aufruf:
    python3 tools/annahmen-ableiten.py            schreibt annahmen/daten/<id>.json
    python3 tools/annahmen-ableiten.py --pruefen  vergleicht nur (Exit 1, wenn eine Datei veraltet ist)
"""
from __future__ import annotations

import json
import math
import sys
from pathlib import Path

BASE = Path(__file__).resolve().parent.parent
QUELLEN = BASE / "annahmen" / "quellen"
DATEN = BASE / "annahmen" / "daten"
STUFEN = [0, 0.02, 0.05, 0.10]       # Bitcoin-Anteile auf den Seiten
NK = 10                               # Nachkommastellen der abgeleiteten Groessen


def schluessel(b):
    """Wie String(b) in JavaScript: 0 -> "0", 0.1 -> "0.1"."""
    return "%g" % b


def lade(id_):
    return json.loads((QUELLEN / f"{id_}.json").read_text(encoding="utf-8"))


class Matrix:
    def __init__(self, blk):
        self.ids = blk["korrelation_reihenfolge"]
        u = blk["korrelation_untere_dreiecksmatrix"]
        self.k = {(a, b): (u[i][j] if i >= j else u[j][i]) for i, a in enumerate(self.ids) for j, b in enumerate(self.ids)}
        self.a = {i: (v["rendite_arithmetisch"] / 100, v["volatilitaet"] / 100) for i, v in blk["anlagen"].items()}

    def var(self, w):
        return sum(w[i] * w[j] * self.a[i][1] * self.a[j][1] * self.k[(i, j)] for i in w for j in w)

    def arith(self, w):
        return sum(x * self.a[i][0] for i, x in w.items())

    def geo_vol(self, w):
        a_, v_ = self.arith(w), math.sqrt(self.var(w))
        return (1 + a_) / math.sqrt(1 + v_ * v_ / (1 + a_) ** 2) - 1, v_


def mit_bitcoin(basis, b, regel):
    """Wie mitBitcoin im Rechenkern (Gold zuerst bzw. anteilig)."""
    w = dict(basis)
    rest = b
    if b > 0:
        if regel == "gold" and w.get("gold"):
            g = min(w["gold"], rest)
            w["gold"] -= g
            rest -= g
        if rest > 0:
            s = sum(w.values())
            w = {i: x - rest * x / s for i, x in w.items()}
    return {i: x for i, x in w.items() if x > 1e-15}, b


def variante(m, basis, b, regel, kor_btc, vol_von):
    """a_rest, v_rest, k_rest der uebrigen Anlagen fuer Bitcoin-Anteil b."""
    w, b = mit_bitcoin(basis, b, regel)
    return {"b": b, "a_rest": round(m.arith(w), NK), "v_rest": round(m.var(w), NK),
            "k_rest": round(sum(x * vol_von(i) * kor_btc(i) for i, x in w.items()), NK)}


def runde(x, n=NK):
    return round(x, n)


# --- J.P. Morgan 2027 (USD) ------------------------------------------------------

def jpm_2027_usd():
    q = lade("jpm-2027-usd")
    m = Matrix(q)
    p6040 = {"acwi": 0.6, "us_agg": 0.4}
    a6, v6 = m.arith(p6040), math.sqrt(m.var(p6040))
    kor60 = {i: runde(sum(p6040[j] * m.a[j][1] * m.a[i][1] * m.k[(j, i)] for j in p6040) / (v6 * m.a[i][1])) for i in m.ids}
    portfolios = {"p1": ({"acwi": 0.6, "us_agg": 0.4}, "anteilig"),
                  "p2": ({"acwi": 0.55, "us_agg": 0.35, "gold": 0.10}, "gold")}
    varianten = {k: {schluessel(b): variante(m, w, b, r, lambda i: m.k[(i, "bitcoin")], lambda i: m.a[i][1]) for b in STUFEN}
                 for k, (w, r) in portfolios.items()}
    g6, _ = m.geo_vol(p6040)
    aus = {f: q[f] for f in ("id", "anbieter", "kurz", "ausgabe", "waehrung", "horizont", "stand", "erschienen",
                             "quelle_name", "quelle_url", "quelle_seite_url", "anlagen_quelle",
                             "bitcoin_szenarien", "portfolios", "inflation", "chf")}
    aus["hinweis"] = ("Oeffentlicher Auszug: zitierte Werte aus dem Report (Seitenangaben in den Feldern) und daraus "
                      "abgeleitete Kennzahlen der Beispielportfolios. Die vollstaendige Korrelationsmatrix wird nicht "
                      "veroeffentlicht. Erzeugt von tools/annahmen-ableiten.py.")
    aus["anlagen"] = {i: {"name": v["name"], "rendite_geometrisch": v["rendite_geometrisch"],
                          "rendite_arithmetisch": v["rendite_arithmetisch"], "volatilitaet": v["volatilitaet"],
                          "korrelation_bitcoin": m.k[(i, "bitcoin")]}
                      for i, v in q["anlagen"].items()}
    aus["abgeleitet"] = {
        "hinweis": "Vorab gerechnet aus der privaten Matrix: Kennzahlen des 60/40 (60 % ACWI, 40 % US Aggregate), "
                   "Korrelation jeder Anlage zum 60/40 und je Portfolio-Variante a_rest, v_rest, k_rest "
                   "(siehe kennzahlenVariante im Rechenkern). Werte dezimal.",
        "basis_60_40": {"arith": runde(a6), "vol": runde(v6)},
        "korrelation_zu_60_40": kor60,
        "varianten": varianten,
        "vergleich_60_40": {"geo": runde(g6), "vol": runde(v6), "methode": "schwankung",
                            "beschreibung": "60 % Aktien Welt (ACWI), 40 % US Aggregate"},
    }
    aus["rohdaten_oeffentlich"] = False
    return aus


# --- J.P. Morgan 2026 (CHF) ------------------------------------------------------

def jpm_2026_chf():
    q = lade("jpm-2026-chf")
    m = Matrix(q)
    q27 = lade("jpm-2027-usd")
    m27 = Matrix(q27)
    # Bitcoin fehlt in der CHF-Matrix: Korrelationen aus JPM 2027 (USD), Cash = US-Geldmarkt (Annahme, auf der Seite genannt)
    kor_btc = {"acwi": m27.k[("acwi", "bitcoin")], "gold": m27.k[("gold", "bitcoin")], "ch_cash": m27.k[("us_cash", "bitcoin")]}
    mischungen = [
        ("60 % Aktien Welt, 40 % Schweizer Staatsanleihen", {"acwi": 0.6, "ch_staatsanleihen": 0.4}),
        ("40 % Aktien Welt, 20 % Schweizer Aktien, 40 % Staatsanleihen", {"acwi": 0.4, "ch_aktien": 0.2, "ch_staatsanleihen": 0.4}),
        ("60 % Aktien Welt, 30 % Staatsanleihen, 10 % Gold", {"acwi": 0.6, "ch_staatsanleihen": 0.3, "gold": 0.1}),
        ("60 % Aktien Welt, 40 % Franken-Geldmarkt", {"acwi": 0.6, "ch_cash": 0.4}),
        ("60 % Aktien Welt, 40 % US-Anleihen abgesichert", {"acwi": 0.6, "us_agg_abgesichert": 0.4}),
    ]

    def ergebnis(w):
        g, v = m.geo_vol(w)
        return {"geo": runde(g), "vol": runde(v)}
    portfolios = {"ch1": ({"acwi": 0.6, "ch_cash": 0.4}, "anteilig"),
                  "ch2": ({"acwi": 0.6, "ch_cash": 0.3, "gold": 0.1}, "gold")}
    aus = {f: q[f] for f in ("id", "anbieter", "kurz", "ausgabe", "waehrung", "horizont", "stand", "quelle_name", "quelle_url")}
    aus["hinweis"] = ("Oeffentlicher Auszug: zitierte Werte (Rendite, Volatilitaet) von 9 Anlagen aus JPMs CHF-Matrix, "
                      "Ausgabe 2026 (eine CHF-Fassung 2027 gibt es nicht), und daraus abgeleitete Ergebnisse. Die "
                      "Korrelationsmatrix wird nicht veroeffentlicht. Erzeugt von tools/annahmen-ableiten.py.")
    aus["anlagen"] = {i: {"name": v["name"], "jpm_name": v["jpm_name"], "rendite_geometrisch": v["rendite_geometrisch"],
                          "rendite_arithmetisch": v["rendite_arithmetisch"], "volatilitaet": v["volatilitaet"]}
                      for i, v in q["anlagen"].items()}
    aus["abgeleitet"] = {
        "hinweis": "Vorab gerechnet aus der privaten Matrix (geometrische Rendite und Volatilitaet dezimal, jaehrlich "
                   "rebalanciert). Entnahme-Varianten: a_rest, v_rest, k_rest je Bitcoin-Anteil; Bitcoin-Korrelationen "
                   "aus JPM 2027 (USD), zum Franken-Geldmarkt die des US-Geldmarkts.",
        "mischungen": [dict(name=n, gewichte=w, **ergebnis(w)) for n, w in mischungen],
        "festung": dict(gewichte={"acwi": round(475 / 550, NK), "ch_cash": round(75 / 550, NK)},
                        **ergebnis({"acwi": 475 / 550, "ch_cash": 75 / 550})),
        "allwetter_teil": dict(gewichte={"acwi": round(60 / 70, NK), "ch_cash": round(10 / 70, NK)},
                               **ergebnis({"acwi": 60 / 70, "ch_cash": 10 / 70})),
        "entnahme_varianten": {k: {schluessel(b): variante(m, w, b, r, lambda i: kor_btc.get(i, 0.0), lambda i: m.a[i][1])
                                   for b in STUFEN} for k, (w, r) in portfolios.items()},
        "vergleich_60_40": dict(beschreibung="60 % Aktien Welt (ACWI), 40 % Schweizer Staatsanleihen", methode="schwankung",
                                **ergebnis({"acwi": 0.6, "ch_staatsanleihen": 0.4})),
        "bausteine_chf": {b: {"wert": q["anlagen"][i]["rendite_geometrisch"], "name": q["anlagen"][i]["jpm_name"]}
                          for b, i in (("aktien_welt", "acwi"), ("aktien_schweiz", "ch_aktien"),
                                       ("staatsanleihen_ch", "ch_staatsanleihen"), ("geldmarkt_chf", "ch_cash"),
                                       ("gold_chf", "gold"), ("teuerung_ch", "ch_inflation"))},
    }
    aus["rohdaten_oeffentlich"] = False
    return aus


# --- Gemeinsam fuer die Anbieter mit Franken-Bausteinen (seit 25.10.2026) ---------------
# Drei Schweizer Mischungen, fuer jeden Anbieter gleich definiert. «schwankung» = aus Rendite,
# Volatilitaet und Korrelation gerechnet (jaehrlich rebalanciert, log-normal, wie bei JPM);
# «durchschnitt» = gewichteter Durchschnitt der Einzelrenditen, wo der Anbieter keine
# Schwankung oder keine passende Korrelation veroeffentlicht.
MISCHUNGEN_CHF = [
    ("m1", "60 % Aktien Welt, 40 % Staatsanleihen Schweiz", "aktien_welt", "staatsanleihen_ch"),
    ("m2", "60 % Aktien Welt, 40 % Geldmarkt Franken", "aktien_welt", "geldmarkt_chf"),
    ("m3", "60 % Aktien Schweiz, 40 % Staatsanleihen Schweiz", "aktien_schweiz", "staatsanleihen_ch"),
]
BAUSTEINE = ["aktien_welt", "aktien_schweiz", "staatsanleihen_ch", "geldmarkt_chf", "gold_chf", "teuerung_ch"]


def geo_zu_arith(g, s):
    """Wie geoZuArith im Rechenkern: (1+g) = (1+a)/sqrt(1+s^2/(1+a)^2) nach a aufgeloest."""
    G = (1 + g) ** 2
    return math.sqrt((G + math.sqrt(G * G + 4 * G * s * s)) / 2) - 1


def arith_zu_geo(a, s):
    return (1 + a) / math.sqrt(1 + s * s / (1 + a) ** 2) - 1


def mischung_durchschnitt(key, name, gewichte, renditen):
    geo = sum(w * renditen[i] for i, w in gewichte.items())
    return {"key": key, "name": name, "gewichte": gewichte, "geo": runde(geo), "vol": None, "arith": None,
            "methode": "durchschnitt"}


def mischung_schwankung(key, name, gewichte, geo, vol, kor):
    """geo/vol dezimal je Anlage, kor(i, j) Korrelation."""
    a = sum(w * geo_zu_arith(geo[i], vol[i]) for i, w in gewichte.items())
    v = math.sqrt(max(sum(gewichte[i] * gewichte[j] * vol[i] * vol[j] * kor(i, j) for i in gewichte for j in gewichte), 0))
    return {"key": key, "name": name, "gewichte": gewichte, "geo": runde(arith_zu_geo(a, v)), "vol": runde(v),
            "arith": runde(a), "methode": "schwankung"}


def kopf(q, felder=("id", "anbieter", "kurz", "ausgabe", "waehrung", "horizont", "stand", "erschienen",
                    "quelle_name", "quelle_url", "quelle_seite_url")):
    return {f: q[f] for f in felder if f in q}


def abschluss(aus, mischungen, bausteine, hinweis):
    m1 = next((m for m in mischungen if m["key"] == "m1" and m.get("geo") is not None), None)
    aus["abgeleitet"] = {
        "hinweis": hinweis,
        "mischungen": mischungen,
        "bausteine_chf": bausteine,
    }
    if m1:
        aus["abgeleitet"]["vergleich_60_40"] = {"geo": m1["geo"], "vol": m1["vol"], "methode": m1["methode"],
                                                "beschreibung": m1["beschreibung"]}
    aus["rohdaten_oeffentlich"] = False
    return aus


# --- BlackRock 2026 (CHF) --------------------------------------------------------------

def blackrock_2026_chf():
    q = lade("blackrock-2026-chf")
    A = q["anlagen"]
    geo = {i: v["rendite_geometrisch"] / 100 for i, v in A.items()}
    vol = {i: v["volatilitaet"] / 100 for i, v in A.items()}
    rolle = {"aktien_welt": "welt_ex_ch", "aktien_schweiz": "ch_aktien", "staatsanleihen_ch": "ch_staatsanleihen",
             "geldmarkt_chf": "ch_cash"}

    def kor(i, j):
        # Bekannt ist nur die Korrelation jeder Anlage zur Referenz-Aktie (MSCI World ex Switzerland)
        if i == j:
            return 1.0
        if "welt_ex_ch" in (i, j):
            andere = j if i == "welt_ex_ch" else i
            return A[andere]["korrelation_referenz_aktie"] if vol[andere] > 0 else 0.0
        if vol[i] == 0 or vol[j] == 0:
            return 0.0
        raise KeyError((i, j))
    mischungen = []
    for key, name, a, b in MISCHUNGEN_CHF:
        w = {rolle[a]: 0.6, rolle[b]: 0.4}
        try:
            m = mischung_schwankung(key, name, w, geo, vol, kor)
        except KeyError:
            m = mischung_durchschnitt(key, name, w, geo)
        m["beschreibung"] = name.replace("Aktien Welt", "Aktien Welt ohne Schweiz") + " (BlackRock: MSCI World ex Switzerland, SBI)"
        mischungen.append(m)
    aus = kopf(q)
    aus["hinweis"] = ("Oeffentlicher Auszug: zitierte Werte (Rendite 10 Jahre, Volatilitaet) aus BlackRocks Capital Market "
                      "Assumptions, Basiswaehrung CHF, und daraus abgeleitete Ergebnisse. Die Korrelationen werden nicht "
                      "veroeffentlicht. Erzeugt von tools/annahmen-ableiten.py.")
    aus["anlagen"] = {i: {"name": v["name"], "anbieter_name": v["anbieter_name"] + " (" + v["index"] + ")", "waehrung": "CHF",
                          "rendite": v["rendite_geometrisch"], "volatilitaet": v["volatilitaet"]} for i, v in A.items()}
    bausteine = {b: ({"wert": A[rolle[b]]["rendite_geometrisch"], "name": A[rolle[b]]["anbieter_name"]} if b in rolle else None)
                 for b in BAUSTEINE}
    bausteine["aktien_welt"]["hinweis"] = "ohne Schweiz"
    return abschluss(aus, mischungen, bausteine,
                     "Vorab gerechnet: m1 und m2 mit Volatilitaet und BlackRocks Korrelation zu MSCI World ex Switzerland "
                     "(jaehrlich rebalanciert, log-normal); m3 als gewichteter Durchschnitt, weil die Korrelation zwischen "
                     "Schweizer Aktien und Schweizer Staatsanleihen nicht veroeffentlicht ist. Werte dezimal.")


# --- Pictet Wealth Management Horizon 2025 (CHF) ----------------------------------------

def pictet_wm_2025_chf():
    q = lade("pictet-wm-2025-chf")
    A = q["anlagen"]
    nom = {i: v["rendite_nominal"] / 100 for i, v in A.items()}
    rolle = {"aktien_welt": "acwi", "aktien_schweiz": "ch_aktien", "staatsanleihen_ch": "ch_staatsanleihen_10j",
             "geldmarkt_chf": "ch_cash", "gold_chf": "gold", "teuerung_ch": "ch_inflation"}
    mischungen = []
    for key, name, a, b in MISCHUNGEN_CHF:
        m = mischung_durchschnitt(key, name, {rolle[a]: 0.6, rolle[b]: 0.4}, nom)
        m["beschreibung"] = name + " (Pictet: Equity all countries bzw. Switzerland, 10-year Swiss bonds, Cash CHF)"
        mischungen.append(m)
    aus = kopf(q)
    aus["hinweis"] = ("Oeffentlicher Auszug: zitierte Werte (10 Jahre, nominal und real, in CHF) aus Pictet Wealth Management "
                      "Horizon 2025, S. 4. Erzeugt von tools/annahmen-ableiten.py.")
    aus["anlagen"] = {i: {"name": v["name"], "anbieter_name": v["anbieter_name"], "waehrung": "CHF", "rendite": v["rendite_nominal"],
                          "rendite_real": v["rendite_real"]} for i, v in A.items()}
    bausteine = {b: {"wert": A[rolle[b]]["rendite_nominal"], "name": A[rolle[b]]["anbieter_name"]} for b in BAUSTEINE}
    bausteine["staatsanleihen_ch"]["hinweis"] = "10 Jahre Laufzeit"
    return abschluss(aus, mischungen, bausteine,
                     "Vorab gerechnet als gewichteter Durchschnitt der nominalen Einzelrenditen; Pictet veroeffentlicht "
                     "keine Volatilitaeten und keine Korrelationen. Werte dezimal.")


# --- Luzerner Kantonalbank 2026 (CHF) -----------------------------------------------------

def lukb_2026_chf():
    q = lade("lukb-2026-chf")
    A = q["anlagen"]
    r_ = {i: v["rendite"] / 100 for i, v in A.items()}
    rolle = {"aktien_schweiz": "ch_aktien", "staatsanleihen_ch": "ch_staatsanleihen", "geldmarkt_chf": "ch_cash"}
    mischungen = []
    for key, name, a, b in MISCHUNGEN_CHF:
        if a in rolle and b in rolle:
            m = mischung_durchschnitt(key, name, {rolle[a]: 0.6, rolle[b]: 0.4}, r_)
        else:
            m = {"key": key, "name": name, "gewichte": None, "geo": None, "vol": None, "arith": None, "methode": None,
                 "grund": "keine Annahme für Aktien Welt"}
        m["beschreibung"] = name
        mischungen.append(m)
    aus = kopf(q)
    aus["hinweis"] = ("Oeffentlicher Auszug: zitierte Werte (etwa 5 Jahre, je in der angegebenen Waehrung) aus den "
                      "Investment Office News der LUKB vom 27.01.2026, S. 5. Erzeugt von tools/annahmen-ableiten.py.")
    aus["anlagen"] = {i: {"name": v["name"], "anbieter_name": v["anbieter_name"], "waehrung": v["waehrung"], "rendite": v["rendite"]}
                      for i, v in A.items()}
    bausteine = {b: ({"wert": A[rolle[b]]["rendite"], "name": A[rolle[b]]["anbieter_name"]} if b in rolle else None) for b in BAUSTEINE}
    pz = lambda x: ("%.1f" % x).replace("-", "\u2212")      # echtes Minuszeichen wie auf den Seiten
    bausteine["gold_chf"] = {"wert": None, "name": "Gold (USD)", "hinweis": "nur in USD: " + pz(A["gold_usd"]["rendite"]) +
                             " %; USD gegenüber Franken " + pz(A["usd_chf"]["rendite"]) + " % p.a."}
    return abschluss(aus, mischungen, bausteine,
                     "Vorab gerechnet als gewichteter Durchschnitt; die LUKB veroeffentlicht keine Volatilitaeten, keine "
                     "Korrelationen und keine Annahme fuer Aktien Welt. Werte dezimal.")


ERZEUGER = {"jpm-2027-usd": jpm_2027_usd, "jpm-2026-chf": jpm_2026_chf, "blackrock-2026-chf": blackrock_2026_chf,
            "pictet-wm-2025-chf": pictet_wm_2025_chf, "lukb-2026-chf": lukb_2026_chf}


def main() -> int:
    pruefen = "--pruefen" in sys.argv
    veraltet = 0
    verz = json.loads((DATEN / "verzeichnis.json").read_text(encoding="utf-8"))
    for id_ in [e["id"] for e in verz["eintraege"]]:
        f = ERZEUGER[id_]
        text = json.dumps(f(), indent=2, ensure_ascii=False) + "\n"
        ziel = DATEN / f"{id_}.json"
        if pruefen:
            if not ziel.exists() or ziel.read_text(encoding="utf-8") != text:
                print(f"VERALTET: {ziel.relative_to(BASE)} — tools/annahmen-ableiten.py laufen lassen")
                veraltet += 1
        else:
            ziel.write_text(text, encoding="utf-8")
            print(f"geschrieben: {ziel.relative_to(BASE)}")
    return 1 if veraltet else 0


if __name__ == "__main__":
    sys.exit(main())
