Simulering av allvädersportfölj

Jag fick lite feeling och fastnade en stund vid datorn. Jag uppdaterade min tidigare kod med dataserier för en enkel allvädersportfölj.

Not: Tickers i diagrammet är de som @Zino använt i första inlägget i sin tråd om allvädersportföljer:

Jag har hämtat data från Yahoo Finance och där används andra tickers, se nedan.

WEBN=WEBG.DE
Captor Iris Bond (B)=0P0001H70E.ST
EN4C=EN4C.DE
GLDA=GOLD.MI

Python-koden
import yfinance as yf
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
from datetime import datetime, timedelta

# Datumintervall – senaste 365 dagar
end_date = datetime.today()
start_date = end_date - timedelta(days=365)


# Hämta data: WEBG, EN4C, samt GOLD i EUR, Captor Iris i SEK, och EUR/SEK-växelkurs
webg = yf.download("WEBG.DE", start=start_date, end=end_date, auto_adjust=False)
print("WEBG loaded:", not webg.empty)

en4c = yf.download("EN4C.DE", start=start_date, end=end_date, auto_adjust=False)
print("EN4C loaded:", not en4c.empty)

gold = yf.download("GOLD.MI", start=start_date, end=end_date, auto_adjust=False)
print("GOLD loaded:", not gold.empty)

eursek = yf.download("EURSEK=X", start=start_date, end=end_date, auto_adjust=False)  # EUR/SEK
print("EUR/SEK loaded:", not eursek.empty)

captor_iris = yf.download("0P0001H70E.ST", start=start_date, end=end_date, auto_adjust=False)  # ISIN + .ST ibland funkar
print("Captor Iris loaded:", not captor_iris.empty)

# Debug
"""
# Testa: skriv ut första 5 rader av varje
print("WEBG.DE:\n", webg.head(), "\n")
print("EN4C.DE:\n", en4c.head(), "\n")
print("GOLD.MI:\n", gold.head(), "\n")
print("EURSEK=X:\n", eursek.head(), "\n")
print("Captor Iris (SEK):\n", captor_iris.head(), "\n")
"""

# Säker extraktion av 'Close' även om vi får en DataFrame med MultiIndex (efter nya yfinance-versioner)
def extract_close(df, name):
    if isinstance(df.columns, pd.MultiIndex):
        return df.loc[:, ("Close", name)]
    else:
        return df["Close"]


# Byt namn på serierna
webg_close = extract_close(webg, "WEBG.DE")
webg_close.name = "WEBG_EUR"

en4c_close = extract_close(en4c, "EN4C.DE")
en4c_close.name = "EN4C_EUR"

gold_close = extract_close(gold, "GOLD.MI")
gold_close.name = "GOLD_EUR"

eursek_close = extract_close(eursek, "EURSEK=X")
eursek_close.name = "EURSEK"

captor_close = extract_close(captor_iris, "0P0001H70E.ST")
captor_close.name = "Captor_SEK"

# Kombinera dem i en gemensam DataFrame
df = pd.concat([webg_close, en4c_close, gold_close, eursek_close, captor_close], axis=1)
df.dropna(inplace=True)

# Ta bort alla rader där någon av dem saknas (t.ex. helgdagar eller stängd börs)
df.dropna(inplace=True)

# Kontroll om DataFrame blev tom efter sammanslagningen
if df.empty:
    raise ValueError("Inga gemensamma datum – datan kan vara tom eller sakna överlapp.")

# Omvandla till SEK
df["WEBG_SEK"] = df["WEBG_EUR"] * df["EURSEK"]
df["EN4C_SEK"] = df["EN4C_EUR"] * df["EURSEK"]
df["GOLD_SEK"] = df["GOLD_EUR"] * df["EURSEK"]

# Indexera utvecklingen (börja från 100)
for col in ["WEBG_EUR", "WEBG_SEK", "EN4C_EUR", "EN4C_SEK", "GOLD_EUR", "GOLD_SEK", "Captor_SEK", "EURSEK"]:
    df[f"{col}_pct"] = (df[col] / df[col].iloc[0]) * 100

# Beräkna dagliga logaritmiska avkastningar
cols_for_vol = ["WEBG_EUR", "WEBG_SEK", "EN4C_EUR", "EN4C_SEK", "GOLD_EUR", "GOLD_SEK", "Captor_SEK", "EURSEK"]
log_returns = np.log(df[cols_for_vol] / df[cols_for_vol].shift(1))

# Beräkna årlig volatilitet (standardavvikelse * sqrt(252))
volatility = log_returns.std() * np.sqrt(252)

# Konvertera till procent, t.ex. 0.25 → 25.0%
volatility_pct = (volatility * 100).round(2)


# Plot
plt.figure(figsize=(14, 7))

plot_config = [
    ("WEBG_EUR_pct", "Amundi ACWI (WEBN) i EUR", "blue", ":"),
    ("WEBG_SEK_pct", "Amundi ACWI (WEBN) i SEK", "blue", "-"),
    ("EN4C_EUR_pct", "Commodities (EN4C) i EUR", "red", ":"),
    ("EN4C_SEK_pct", "Commodities (EN4C) i SEK", "red", "-"),
    ("GOLD_EUR_pct", "Guld ETC (GLDA) i EUR", "orange", ":"),
    ("GOLD_SEK_pct", "Guld ETC (GLDA) i SEK", "orange", "-"),
    ("Captor_SEK_pct", "Captor Iris Bond B i SEK", "green", "-"),
    ("EURSEK_pct", "EUR/SEK", "black", "--")
]

for col, label, color, style in plot_config:
    plt.plot(df.index, df[col], label=label, color=color, linestyle=style)

# Volatilitets-etiketter
last_date = df.index[-1]
for col, label, color, _ in plot_config:
    vol = volatility_pct.get(col.replace("_pct", ""), None)
    if vol is not None:
        plt.text(last_date, df[col].iloc[-1], f"Vol: {vol}%", color=color, fontsize=8)

plt.title("Enkel allvädersportfölj - Indexerad utveckling & volatilitet (senaste 12 mån)")
plt.xlabel("Datum")
plt.ylabel("Index (start = 100)")
plt.legend()
plt.grid(True)
plt.tight_layout()
plt.show()
2 gillningar