Wie Python hilft, Finanzberater zu ersetzen

Published on August 12, 2018

Wie Python hilft, Finanzberater zu ersetzen

  • Tutorial
In Fortsetzung des Artikels über die Gefahren einer übermäßigen Diversifizierung werden wir nützliche Instrumente für die Auswahl von Aktien erstellen. Danach werden wir ein einfaches Rebalancing durchführen und eindeutige Bedingungen für technische Indikatoren hinzufügen, die so häufig in populären Diensten fehlen. Anschließend vergleichen wir die Rentabilität einzelner Vermögenswerte und verschiedener Portfolios.

Dabei verwenden wir Pandas und minimieren die Anzahl der Zyklen. Wir gruppieren die Zeitreihen und zeichnen Diagramme. Machen wir uns mit Multi-Index und deren Verhalten vertraut. Und das alles in Jupyter in Python 3.6.

Wenn Sie etwas gut machen wollen, machen Sie es selbst.
Ferdinand Porsche

Mit dem beschriebenen Tool können Sie die optimalen Vermögenswerte für das Portfolio auswählen und von Beratern auferlegte Instrumente ausschließen. Wir werden jedoch nur ein allgemeines Bild sehen - ohne Berücksichtigung der Liquidität, der Zeit für die Einstellung von Positionen, der Maklergebühren und der Kosten für eine Aktie. Bei monatlicher oder jährlicher Anpassung großer Broker sind dies im Allgemeinen nur geringfügige Kosten. Bevor Sie jedoch die gewählte Strategie anwenden, sollten Sie den ereignisgesteuerten Back-Tester, z. B. Quantopian (QP), überprüfen, um mögliche Fehler zu beseitigen.

Warum nicht gleich in QP? Zeit Dort dauert der einfachste Test ca. 5 Minuten. Mit der aktuellen Lösung können Sie in einer Minute Hunderte verschiedener Strategien mit einzigartigen Bedingungen überprüfen.

Rohdaten werden geladen


Verwenden Sie die in diesem Artikel beschriebene Methode, um die Daten herunterzuladen . Ich verwende PostgreSQL, um Tagespreise zu speichern, aber jetzt gibt es viele kostenlose Quellen, aus denen Sie den erforderlichen DataFrame generieren können.

Der Code zum Herunterladen der Preishistorie aus der Datenbank ist im Repository verfügbar. Der Link befindet sich am Ende des Artikels.

DataFrame-Struktur


Wenn Sie mit der Preishistorie arbeiten, ist es für eine bequeme Gruppierung und den Zugriff auf alle Daten die beste Lösung, einen Multi-Index (MultiIndex) mit Datum und Tickern zu verwenden.

df = df.set_index(['dt', 'symbol'], drop=False).sort_index()
df.tail(len(df.index.levels[1]) * 2)


image

Mit einem Multi-Index können wir einfach auf die gesamte Preishistorie für alle Assets zugreifen und das Array nach Datum und Assets getrennt gruppieren. Wir können auch eine Preishistorie für einen Vermögenswert erhalten.

Hier ist ein Beispiel, wie Sie den Verlauf einfach nach Woche, Monat und Jahr gruppieren können. Und das alles zeigt die Grafik von Pandas:

# Правила обработки колонок при группировке
agg_rules = {
    'dt': 'last', 'symbol': 'last',
    'open': 'first', 'high': 'max', 'low': 'min', 'close': 'last',
    'volume': 'sum', 'adj': 'last'
}
level_values = df.index.get_level_values
# Графики
fig = plt.figure(figsize=(15, 3), facecolor='white')
df.groupby([pd.Grouper(freq='W', level=0)] + [level_values(i) for i in [1]]).agg(
    agg_rules).set_index(['dt', 'symbol'], drop=False
    ).close.unstack(1).plot(ax=fig.add_subplot(131), title="Weekly")
df.groupby([pd.Grouper(freq='M', level=0)] + [level_values(i) for i in [1]]).agg(
    agg_rules).set_index(['dt', 'symbol'], drop=False
    ).close.unstack(1).plot(ax=fig.add_subplot(132), title="Monthly")
df.groupby([pd.Grouper(freq='Y', level=0)] + [level_values(i) for i in [1]]).agg(
    agg_rules).set_index(['dt', 'symbol'], drop=False
    ).close.unstack(1).plot(ax=fig.add_subplot(133), title="Yearly")
plt.show()


image

Um den Bereich mit der Diagrammlegende korrekt anzuzeigen, verschieben wir die Indexebene mit Tickern mit dem Befehl Series () auf die zweite Ebene über den Spalten. Mit DataFrame () funktioniert diese Nummer nicht, es gibt jedoch eine Lösung.

Bei der Gruppierung nach Standardperioden verwendet Pandas das neueste Kalenderdatum der Gruppe im Index, das sich häufig von den tatsächlichen Daten unterscheidet. Aktualisieren Sie den Index, um dies zu beheben.

monthly = df.groupby([pd.Grouper(freq='M', level=0), level_values(1)]).agg(agg_rules) \
          .set_index(['dt', 'symbol'], drop=False)

Ein Beispiel für das Abrufen des Preisverlaufs eines bestimmten Vermögenswerts (wir nehmen alle Daten, den Ticker QQQ und alle Spalten):

monthly.loc[(slice(None), ['QQQ']), :]  # история одно актива

Monatliche Vermögensvolatilität


Jetzt können wir in ein paar Zeilen auf dem Diagramm die Änderung des Preises jedes Vermögenswerts für den für uns interessanten Zeitraum sehen. Dazu erhalten wir die prozentuale Änderung des Preises, indem wir den Datenrahmen nach der Multi-Index-Ebene mit dem Asset-Ticker gruppieren.

monthly = df.groupby([pd.Grouper(freq='M', level=0), level_values(1)]).agg(
              agg_rules).set_index(['dt', 'symbol'], drop=False)
# Ежемесячные изменения цены в процентах. Первое значение обнулим.
monthly['pct_close'] = monthly.groupby(level=1)['close'].pct_change().fillna(0)
# График
ax = monthly.pct_close.unstack(1).plot(title="Monthly", figsize=(15, 4))
ax.axhline(0, color='k', linestyle='--', lw=0.5)
plt.show()

image

Vergleichen Sie die Kapitalrendite


Nun verwenden wir die Methode Series (). Rolling () und leiten die Rendite für einen bestimmten Zeitraum ab:

Python-Code
rolling_prod = lambda x: x.rolling(len(x), min_periods=1).apply(np.prod)  # кумулятивный доход
monthly = df.groupby([pd.Grouper(freq='M', level=0), level_values(1)]).agg(
              agg_rules).set_index(['dt', 'symbol'], drop=False)
# Ежемесячные изменения цены в процентах. Первое значение обнулим. И прибавим 1.
monthly['pct_close'] = monthly.groupby(level=1)['close'].pct_change().fillna(0) + 1
# Новый DataFrame без данных старше 2007 года
fltr = monthly.dt >= '2007-01-01'
test = monthly[fltr].copy().set_index(['dt', 'symbol'], drop=False)  # обрежем dataframe и обновим индекс
test.loc[test.index.levels[0][0], 'pct_close'] = 1  # устанавливаем первое значение 1
# Получаем кумулятивный доход
test['performance'] = test.groupby(level=1)['pct_close'].transform(rolling_prod) - 1
# График
ax = test.performance.unstack(1).plot(title="Performance (Monthly) from 2007-01-01", figsize=(15, 4))
ax.axhline(0, color='k', linestyle='--', lw=0.5)
plt.show()
# Доходность каждого инструмента в последний момент
test.tail(len(test.index.levels[1])).sort_values('performance', ascending=False)


image

Portfolio-Rebalance-Methoden


Also kamen wir zu den leckersten. In den Beispielen werden wir die Ergebnisse des Portfolios bei der Aufteilung des Kapitals auf bestimmte Aktien auf mehrere Vermögenswerte untersuchen. Außerdem werden wir einmalige Bedingungen hinzufügen, unter denen wir zum Zeitpunkt der Kapitalausschüttung einige Vermögenswerte ablehnen. Wenn es keine geeigneten Assets gibt, gehen wir davon aus, dass das Kapital beim Broker im Cache liegt.

Um die Pandas-Methoden beim Neuausgleich verwenden zu können, müssen die Distributionsfreigaben und die Neuausgleichsbedingungen mit gruppierten Daten im DataFrame gespeichert werden. Betrachten Sie nun die Neuausgleichsfunktionen, die an die DataFrame (). Apply () -Methode übergeben werden:

Python-Code
def rebalance_simple(x):
    # Простая ребалансировка по долям
    data = x.unstack(1)
    return (data.pct_close * data['size']).sum() / data['size'].sum()
def rebalance_sma(x):
    # Ребалансировка по активам, у которых SMA50 > SMA200
    data = x.unstack(1)
    fltr = data['sma50'] > data['sma200']
    if not data[fltr]['size'].sum():
        return 1  # Баланс без изменений, если нет подходящих
    return (data[fltr].pct_close * data[fltr]['size']).sum() / data[fltr]['size'].sum()
def rebalance_rsi(x):
    # Ребалансировка по активам, у которых RSI100 > 50
    data = x.unstack(1)
    fltr = data['rsi100'] > 50
    if not data[fltr]['size'].sum():
        return 1  # Баланс без изменений, если нет подходящих
    return (data[fltr].pct_close * data[fltr]['size']).sum() / data[fltr]['size'].sum()
def rebalance_custom(x, df=None):
    # Медленная ребалансировка с уникальными условиями и внешними данными
    data = x.unstack(1)
    for s in data.index:
        if data['dt'][s]:
            fltr_dt = df['dt'] < data['rebalance_dt'][s]  # исключим будущее
            values = df[fltr_dt].loc[(slice(None), [s]), 'close'].values
            data.loc[s, 'custom'] = 0  # обнулим значение фильтра
            if len(values) > len(values[np.isnan(values)]):                
                # Получим RSI за 100 дней
                data.loc[s, 'custom'] = talib.RSI(values, timeperiod=100)[-1]
    fltr = data['custom'] > 50
    if not data[fltr]['size'].sum():
        return 1  # Баланс без изменений, если нет подходящих
    return (data[fltr].pct_close * data[fltr]['size']).sum() / data[fltr]['size'].sum()
def drawdown(chg, is_max=False):
    # Максимальная просадка доходности
    total = len(chg.index)
    rolling_max = chg.rolling(total, min_periods=1).max()
    daily_drawdown = chg/rolling_max - 1.0
    if is_max:
        return daily_drawdown.rolling(total, min_periods=1).min()
    return daily_drawdown


In der Reihenfolge:

  • rebalance_simple ist die einfachste Funktion, mit der die Rentabilität jedes Vermögenswerts nach Aktien aufgeteilt wird.
  • rebalance_sma ist eine Funktion, die Kapital auf Vermögenswerte verteilt, deren gleitender 50-Tage-Durchschnitt den Wert von 200 Tagen zum Zeitpunkt des Neuausgleichs überschreitet.
  • rebalance_rsi ist eine Funktion, die Kapital auf Vermögenswerte verteilt, deren RSI-Indikatorwert für 100 Tage über 50 liegt.
  • rebalance_custom ist die langsamste und universellste Funktion, bei der wir die Indikatorwerte aus dem täglichen Preisverlauf des Vermögenswerts zum Zeitpunkt des Neuausgleichs berechnen. Hier können Sie beliebige Bedingungen und Daten verwenden. Sogar jedes Mal von externen Quellen herunterladen. Aber ohne einen Zyklus geht das nicht.
  • Drawdown - Hilfsfunktion, die den maximalen Drawdown des Portfolios anzeigt.

In den Neuausgleichsfunktionen benötigen wir ein Array aller Daten zum Datum in Bezug auf die Vermögenswerte. Die DataFrame (). Apply () -Methode, mit der die Ergebnisse der Portfolios berechnet werden, übergibt ein Array an unsere Funktion, wobei die Spalten den Zeilenindex darstellen. Und wenn wir einen Multi-Index erstellen, bei dem die Ticker die Null sind, kommt der Multi-Index zu uns. Wir können diesen Multi-Index zu einem zweidimensionalen Array erweitern und die Daten des entsprechenden Assets in jeder Zeile abrufen.

image

Portfolios neu ausbalancieren


Jetzt genügt es, die notwendigen Bedingungen vorzubereiten und für jedes Portfolio in einem Zyklus eine Berechnung durchzuführen. Berechnen wir zunächst die Indikatoren für die tägliche Preisentwicklung:

# Смещаем данные на 1 день вперед, чтобы не заглядывать в будущее
df['sma50'] = df.groupby(level=1)['close'].transform(lambda x: talib.SMA(x.values, timeperiod=50)).shift(1)
df['sma200'] = df.groupby(level=1)['close'].transform(lambda x: talib.SMA(x.values, timeperiod=200)).shift(1)
df['rsi100'] = df.groupby(level=1)['close'].transform(lambda x: talib.RSI(x.values, timeperiod=100)).shift(1)

Gruppieren Sie nun den Verlauf unter Verwendung der oben beschriebenen Methoden unter dem gewünschten Ausgleichszeitraum. Wir werden in diesem Fall die Werte der Indikatoren zu Beginn des Zeitraums heranziehen, um einen Blick in die Zukunft auszuschließen.

Wir beschreiben die Struktur der Portfolios und weisen auf die notwendige Neuausrichtung hin. Portfolios werden in einem Zyklus berechnet, da wir eindeutige Aktien und Bedingungen angeben müssen:

Python-Code
# Условия портфелей: доли активов, функция ребалансировки, название
portfolios = [
    {'symbols': [('SPY', 0.8), ('AGG', 0.2)], 'func': rebalance_sma, 'name': 'Portfolio 80/20 SMA50x200'},
    {'symbols': [('SPY', 0.8), ('AGG', 0.2)], 'func': rebalance_rsi, 'name': 'Portfolio 80/20 RSI100>50'},
    {'symbols': [('SPY', 0.8), ('AGG', 0.2)], 'func': partial(rebalance_custom, df=df), 'name': 'Portfolio 80/20 Custom'},
    {'symbols': [('SPY', 0.8), ('AGG', 0.2)], 'func': rebalance_simple, 'name': 'Portfolio 80/20'},
    {'symbols': [('SPY', 0.4), ('AGG', 0.6)], 'func': rebalance_simple, 'name': 'Portfolio 40/60'},
    {'symbols': [('SPY', 0.2), ('AGG', 0.8)], 'func': rebalance_simple, 'name': 'Portfolio 20/80'},
    {'symbols': [('DIA', 0.2), ('QQQ', 0.3), ('SPY', 0.2), ('IWM', 0.2), ('AGG', 0.1)], 
     'func': rebalance_simple, 'name': 'Portfolio DIA & QQQ & SPY & IWM & AGG'},
]
for p in portfolios:
    # Обнуляем размер долей
    rebalance['size'] = 0.
    for s, pct in p['symbols']:
        # Устанавливаем свои доли для каждого актива
        rebalance.loc[(slice(None), [s]), 'size'] = pct
    # Подготовим индекс для корректной ребалансировки и получим доходность за каждый период
    rebalance_perf = rebalance.stack().unstack([1, 2]).apply(p['func'], axis=1)
    # Кумулятивная доходность портфеля
    p['performance'] = (rebalance_perf.rolling(len(rebalance_perf), min_periods=1).apply(np.prod) - 1)
    # Максимальная просадка портфеля
    p['drawdown'] = drawdown(p['performance'] + 1, is_max=True)


Dieses Mal müssen wir den Trick mit den Indizes von Spalten und Zeilen drehen, um den gewünschten Multi-Index in der Neuausgleichsfunktion zu erhalten. Dies erreichen wir, indem wir die Methoden DataFrame (). Stack (). Unstack ([1, 2]) nacheinander aufrufen. Dieser Code überträgt die Spalten in den Multi-Index niedrigerer Ordnung und gibt den Multi-Index mit Tickern und Spalten in der gewünschten Reihenfolge zurück.

Fertige Portfolios für Grafiken


Jetzt bleibt noch alles zu zeichnen. Führen Sie dazu erneut einen Zyklus von Portfolios durch, in dem Daten in den Diagrammen angezeigt werden. Am Ende werden wir SPY als Vergleichsmaßstab ziehen.

Python-Code
fig = plt.figure(figsize=(15, 4), facecolor='white')
ax_perf = fig.add_subplot(121)
ax_dd = fig.add_subplot(122)
for p in portfolios:
    p['performance'].rename(p['name']).plot(ax=ax_perf, legend=True, title='Performance')
    p['drawdown'].rename(p['name']).plot(ax=ax_dd, legend=True, title='Max drawdown')
    # Вывод доходности и просадки перед графиками
    print(f"{p['name']}: {p['performance'][-1]*100:.2f}% / {p['drawdown'][-1]*100:.2f}%")
# SPY, как бенчмарк
rebalance.loc[(slice(None), ['SPY']), :].set_index('dt', drop=False).performance. \
    rename('SPY').plot(ax=ax_perf, legend=True)
drawdown(rebalance.loc[(slice(None), ['SPY']), :].set_index('dt', drop=False).performance + 1, 
         is_max=True).rename('SPY').plot(ax=ax_dd, legend=True)
ax_perf.axhline(0, color='k', linestyle='--', lw=0.5)
ax_dd.axhline(0, color='k', linestyle='--', lw=0.5)
plt.show()


image

Fazit


Mit dem berücksichtigten Code können Sie verschiedene Portfoliostrukturen und Ausgleichsbedingungen auswählen. Damit können Sie schnell prüfen, ob es sich beispielsweise lohnt, Gold (GLD) oder Emerging Markets (EEM) in einem Portfolio zu halten. Probieren Sie es aus, fügen Sie Ihre eigenen Indikatorbedingungen hinzu oder wählen Sie die bereits beschriebenen Parameter aus. (Beachten Sie jedoch den Fehler des Überlebenden und die Tatsache, dass die Anpassung an frühere Daten möglicherweise nicht den Erwartungen entspricht.) Und entscheiden Sie dann, wem Sie in Ihr Portfolio vertrauen - Python oder Finanzberater?

Repository: rebalance.portfolio