Chaotischer Pandas DataFrame was mache ich falsch?

mit matplotlib, NumPy, pandas, SciPy, SymPy und weiteren mathematischen Programmbibliotheken.
Antworten
TechHippie420
User
Beiträge: 11
Registriert: Montag 22. Februar 2021, 10:03

Moin zusammen,

ich habe mir einige Datensätze von Yahoo finance runtergeladen und will diese nun auf bestimmte Bedingungen überprüfen und wenn diese Bedingungen erfüllt (True) sind, die betreffenden Unternehmen (ticker) als Dataframe anzeigen lassen.

hier mal die entsprechende Funktion:

Code: Alles auswählen

def createPortfolioLocal(self, parent, controller, lbl_PortfolioOverview):

        starttime = time.time()
        path = 'Data/HistoricalData'
        exportlist = pd.DataFrame(columns=['Stock', 'RS_Rating', '50 Day MA', '150 Day MA', '200 Day MA', '52 Week Low', '52 Week High'])
        
        for filename in os.listdir(path):

            try:
                df_rs_rating = pd.read_csv(os.path.join(path, filename))
                df_rs_rating = pd.DataFrame(df_rs_rating)
                df_rs_rating['Diff Vortag'] = df_rs_rating['Adj Close'].diff()
                delta = df_rs_rating['Adj Close'].diff()
                up = delta.clip(lower=0)
                down = -1*delta.clip(upper=0)
                ema_up = up.ewm(com=13, adjust=False).mean()
                ema_down = down.ewm(com=13, adjust=False).mean()
                rs = ema_up/ema_down
                df_rs_rating['RSI'] = 100 - (100/(1 + rs))
                
                df_rs_rating['ticker'] = filename[:-4]
                stock = df_rs_rating['ticker']
                RS_Rating = df_rs_rating['RSI']
                df_rs_rating.reset_index(inplace=False)
                df_rs_rating.set_index('Date', inplace=True)

                # Skip first 14 days to have real values
                df_rs_rating = df_rs_rating.iloc[14:]
            except Exception:
                print('sth. went wrong')
                print(Exception.with_traceback())

            try:

                smaUsed = [50, 150, 200]

                for x in smaUsed:
                    df_rs_rating['SMA_' + str(x)] = round(df_rs_rating.iloc[:,5].rolling(window=x).mean(), 2)
                            
                currentClose = df_rs_rating['Adj Close'][-1]
                moving_average_50 = df_rs_rating['SMA_50'][-1]
                moving_average_150 = df_rs_rating['SMA_150'][-1]
                moving_average_200 = df_rs_rating['SMA_200'][-1]

                low_of_52week = min(df_rs_rating['Adj Close'][-260:])
                high_of_52week = max(df_rs_rating['Adj Close'][-260:])
                    
                try:
                    moving_average_200_20past = df_rs_rating['SMA_200'][-20]
                except Exception:
                    moving_average_200_20past = 0

                print('Checking ' + filename + ' ...')
                print(df_rs_rating.tail())
            
                # Condition 1: Current price > 150 SMA and > 200 SMA
                if (currentClose > moving_average_150 and currentClose > moving_average_200):
                    cond_1 = True
                    # print(currentClose)
                    # print(moving_average_150)
                    # print(moving_average_200)
                    print('cond_1 is True')
                else:
                    cond_1 = False
                # print(currentClose)
                # print(moving_average_150)
                # print(moving_average_200)
                    print('cond_1 is False')

                # Condition 2: 150 SMA > 200 SMA
                if (moving_average_150 > moving_average_200):
                    cond_2 = True
                    # print('cond_2 is True')
                else:
                    cond_2 = False
                    # print('cond_2 is False')

                # # Condition3: 200 SMA trending up for at least 1 month (ideally 4-5 months)
                # if (moving_average_200 > moving_average_200_20past):
                #     cond_3 = True
                #     # print('cond_3 is True')
                # else:
                #     cond_3 = False
                #     # print('cond_3 is False')

                # # Condition 4: 50 SMA > 150 SMA and 50 SMA > 200 SMA
                # if (moving_average_50 > moving_average_150 and moving_average_50 > moving_average_200):
                #     cond_4 = True
                #     # print('cond_4 is True')
                # else:
                #     cond_4 = False
                #     # print('cond_4 is False')

                # # Condition 5: Current price > 50 SMA
                # if (currentClose > moving_average_50):
                #     cond_5 = True
                #     # print('cond_5 is True')
                # else:
                #     cond_5 = False
                #     # print('cond_5 is False')

                # # Condition 6: Current price is at least 30% above 52 weeks low (many of the best are up to 100 - 300% above)
                # if (currentClose >= (1.3*low_of_52week)):
                #     cond_6 = True
                #     # print('cond_6 is True')
                # else:
                #     cond_6 = False
                #     # print('cond_6 is False')

                # # Condition 7: Current price is within 25% of 52 week high
                # if (currentClose >= (.75*high_of_52week)):
                #     cond_7 = True
                #     # print('cond_7 is True')
                # else:
                #     cond_7 = False
                #     # print('cond_7 is False')

                # # Condition 8: (IBD) RS rating > 70 and the higher the better
                # if (RS_Rating > 70):
                #     cond_8 = True
                #     # print('cond_8 is True')
                # else:
                #     cond_8 = False
                #     # print('cond_8 is False')

                # if(cond_1 and cond_2 and cond_3 and cond_4 and cond_5 and cond_6 and cond_7 and cond_8):
                #     exportlist = exportlist.append({'Stock': stock, 'RS_Rating': RS_Rating,'50 Day MA': moving_average_50, '150 Day MA': moving_average_150, '200 Day MA': moving_average_200, '52 Week Low': low_of_52week, '52 Week High': high_of_52week}, ignore_index=True)

                if(cond_1 and cond_2):
                    exportlist = exportlist.append({'Stock': stock, 'RS_Rating': RS_Rating,'50 Day MA': moving_average_50, '150 Day MA': moving_average_150, '200 Day MA': moving_average_200, '52 Week Low': low_of_52week, '52 Week High': high_of_52week}, ignore_index=True)

            except Exception:
                print('No Data on ' + stock)
                print(Exception.with_traceback())

        exportlist.to_csv('exportlist.csv')
        print(exportlist.head())
        print(len(exportlist))
        self.lbl_PortfolioOverview.configure(text = exportlist)
        endtime = time.time()
        print(endtime - starttime)
Das Programm läuft auch ohne Fehler durch, allerdings sieht mein DataFrame nicht aus wie ich mir das vorgestellt habe.

hier mal ein Auszug der Ausgabe:

Code: Alles auswählen

                                             Stock                                          RS_Rating  ...  52 Week Low  52 Week High0  0       1COV.DE
1       1COV.DE
2       1COV.D...  0             NaN
1        0.000000
2        0...  ...    22.177183     61.900002
1  0       A
1       A
2       A
3       A
4     ...  0             NaN
1        0.000000
2        0...  ...    62.860069    130.119995
2  0       AAL
1       AAL
2       AAL
3       AA...  0              NaN
1       100.000000
2       ...  ...     9.040000     30.090000
3  0       AAP
1       AAP
2       AAP
3       AA...  0              NaN
1       100.000000
2       ...  ...    74.666817    176.669998
4  0       AAPL
1       AAPL
2       AAPL
3      ...  0             NaN
1        0.000000
2        1...  ...    55.661041    142.946396
Ich denke, irgendwas mit dem Index ist falsch.
Die Berechnungen klappen. die Ausgabe von 'print(df_rs_rating.tail())' sieht aus wie erwartet.
Nur der DataFrame 'exportlist' sieht halt aus wie er aussieht.

Besten Dank im Voraus und noch einen schönen Tag zusammen

TechHippie420
Benutzeravatar
__blackjack__
User
Beiträge: 12984
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@TechHippie420: Die Methode bekommt `lbl_PortfolioOverview` als Argument, verwendet das aber nirgends. Was auch sinnvoll ist, da es das anscheinend als Attribut auf dem Objekt gibt. Die Argumente `parent` und `controller` werden auch nirgends verwendet. Bleibt noch `self` übrig, allerdings wird das nur an einer einizigen Stelle verwendet um eben auf das Label zuzugreifen. Der ganze Code hat aber ansonsten gar nichts mit GUI zu tun, also ist die Frage warum das überhaupt eine Methode ist und nicht einfach nur eine Funktion.

Namen werden in Python klein_mit_unterstrichen geschrieben. Ausnahmen sind Konstanten (KOMPLETT_GROSS) und Klassen (PascalCase).

Für Zeitmessungen verwendet man besser `time.monotonic()`.

Für Pfadoperationen besser `pathlib`.

Von einem beliebigen Dateinamen einfach die letzten vier Zeichen wegzuschneiden ist keine robuste Art ein Dateinamenserweiterung zu entfernen. In `os.path` gibt es dafür eine Funktion zum trennen von Name und Erweiterung und `filepath.Path`-Objekte machen es noch einfacher: die haben ein Attribut was nur den Dateinamen ohne Erweiterung liefert.

Die Ausnahmebehandlung ist schlecht bis kaputt. Beim ersten ``try`` ”behandelst” Du einfach *alle* Ausnahmen durch die Ausgabe "sth. went wrong", der Code macht dann aber einfach weiter als wäre nichts passiert, was entweder im ersten Schleifendurchlauf zu einer Ausnahme führt weil in dem ``try``-Block dann ja nicht alle Namen/Werte definiert wurden, oder aber es wird nach dem ``try``/``except`` mit Werten aus dem vorherigen Schleigendurchlauf weitergearbeitet. Damit werden dann also falsche Ergebnisse produziert.

Wenn Du einen Abbrucht der Schleife durch beliebige Probleme verhindern möchtest, so das solche Dateien einfach übersprungen werden, dann muss der entsprechende ``try``/``except``-Block den gesamten Schleifenkörper umfassen und der zweite ``try``/``except``-Block dieser Art wird überflüssig.

Im zweiten wird `stock` ausgegeben — eine Information die man aus dem Dateinamen bereits ganz am Anfang extrahieren könnte ohne Gefahr von Ausnahmen.

Die Ausnhamebehandlung wird 100%ig eine weitere Ausnahme zur Folge haben, denn ``print(Exception.with_traceback())`` macht so gar nicht was Du vielleicht denkst was es tut. Die `with_traceback()`-Methode kann man a) nicht auf der Klasse aufrufen, sondern muss das auf einem konkreten Objekt tun, und b) erwartet die ein Traceback-Objekt als Argument. Wenn Du an der Stelle einen Traceback ausgeben lassen wolltest: das geht am besten mit Logging. Da kann/sollte man dann auch gleich die anderen `print()`-Ausgaben drauf umstellen.

Etwas mit `pandas.read_csv()` als `DataFrame` zu lesen und dann gleich als nächstes in einen `DataFrame` zu packen ist unsinnig. Das ist bereits ein `DateFrame`.

``df_rs_rating["Adj Close"].diff()`` wird unnötigerweise zweimal gemacht.

Die Zeile ``df_rs_rating.reset_index(inplace=False)`` ist sinnfrei weil mit dem Ergebnis überhaupt nichts gemacht wird.

`x` ist kein guter Name für eine Anzahl/Fenstergrösse.

Das zusammenstückeln von Zeichenketten und Werten mittels ``+`` und `str()` ist eher BASIC als Python. Dafür gibt es die `format()`-Methode auf Zeichenketten und f-Zeichenkettenliterale.

Es wird eigentlich immer über Spaltennamen auf die Spalten zugegriffen und auch neue Spalten hinzugefügt, für die "SMA_*"-Werte wird dann aber plötzlich über einen magischen numerischen Spaltenindex (5) auf eine Spalte zugegriffen. Es wäre robuster und für den Leser verständlicher auch dort den Namen zu verwenden.

Wenn es eine Methode auf einem Pandas- oder Numpy-Objekt gibt, die das gleiche macht wie eine Funktion ist es in der Regel lesbarer die Methode zu verwenden statt Methoden- und Funktionsaufrufe zu vermischen. Insbesondere wenn die Funktionen dann auch noch zusätzliche Argumente entgegennehmen. Zudem sind die Methoden in der Regel effizienter als allgemeine Funktionen die nicht speziell auf Pandas/Numpy-Objekte ausgerichtet sind. Vergleich:

Code: Alles auswählen

                df_rs_rating[f"SMA_{window_size}"] = round(
                    df_rs_rating.iloc[:, 5].rolling(window=window_size).mean(),
                    2,
                )
                
                # vs.
                
                df_rs_rating[f"SMA_{window_size}"] = (
                    df_rs_rating.iloc[:, 5]
                    .rolling(window=window_size)
                    .mean()
                    .round(2)
                )
In der zweiten Variante ist IMHO leichter zu sehen was in welcher Reihenfolge auf die Daten angewandt wird, und das die 2 zum `round()` gehört.

Funktionieren die ``df_rs_rating["Adj Close"][-1]``-Ausdrücke eigentlich? Dürften sie im Grunde nicht, es sei denn die "Date"-Spalte die als Index gesetzt wurde hat sehr, ähm, überraschende Werte‽ Denn die -1 wird ja im Index gesucht und ist kein normaler fortlaufender Index wie bei Liste und negative Werte haben da auch keine besondere Bedeutung wie bei Listen. Das verwirrt mich jetzt gerade leicht bis mittelschwer.

Bei der Definition von `moving_average_200_20past` wird eine viel zu generische Ausnahme verwendet. *Jede* Ausnahme führt zum Wert 0, auch welche die Du an der Stelle gar nicht erwartest und die Programmierfehler sein könnten, die man beheben sollte. Da es auch hier eigentlich ein Schlüsselzugriff via Index ist (bei dem eine -20 ein sehr komischer Wert ist) kann man sich das mit der Ausnhamebehandlung auch komplett sparen, weil man bei der `get()`-Methode einen Default-Wert angeben kann.

Wenn man anfängt Namen zu nummerieren, dann ist in der Regel was faul. Entweder will man sich bessere Namen ausdenken, oder aber gar keine einzelnen Namen sondern eine Datenstruktur. Oft eine Liste. Und das ist wohl auch hier mit den `cond_*`-Namen der Fall. Der ganze Code wird dann auch ziemlich drastisch kürzer.

Wiederholt in einer Schleife `append()` auf `DataFrame`-Objekte anzuwenden ist sehr ineffizient weil die ganzen Daten jedes mal in einen neuen DataFrame kopiert werden und die Datenmenge die da bewegt werden muss quadratisch ansteigt. Deshalb sammelt man die einzelnen `DateFrames` besser in einer Liste und setzt die am *Ende* *einmal* mit `pd.concat()` zusammen.

Zwischenstand (ungetestet):

Code: Alles auswählen

#!/usr/bin/env python3
import logging
import time
from pathlib import Path

import pandas as pd

HISTORICAL_DATA_PATH = Path("Data/HistoricalData")
DAYS_PER_WEEK = 5

LOG = logging.getLogger(__name__)


def create_portfolio_from_local_files():
    start_time = time.monotonic()

    stock_dataframes = list()
    for file_path in HISTORICAL_DATA_PATH.iterdir():
        stock = file_path.stem
        try:
            df_rs_rating = pd.read_csv(file_path)

            delta = df_rs_rating["Adj Close"].diff()
            df_rs_rating["Diff Vortag"] = delta

            ema_up = delta.clip(lower=0).ewm(com=13, adjust=False).mean()
            ema_down = (-delta.clip(upper=0)).ewm(com=13, adjust=False).mean()
            rs_rating = 100 * ema_up / (ema_up + ema_down)
            df_rs_rating["RSI"] = rs_rating

            df_rs_rating["ticker"] = stock

            df_rs_rating.set_index("Date", inplace=True)
            #
            # Skip first 14 days to have real values.
            #
            df_rs_rating = df_rs_rating.iloc[14:]

            for window_size in [50, 150, 200]:
                #
                # TODO Use column name instead of the magical 5 as column index.
                #
                df_rs_rating[f"SMA_{window_size}"] = (
                    df_rs_rating.iloc[:, 5]
                    .rolling(window=window_size)
                    .mean()
                    .round(2)
                )

            current_close = df_rs_rating["Adj Close"][-1]
            moving_average_50 = df_rs_rating["SMA_50"][-1]
            moving_average_150 = df_rs_rating["SMA_150"][-1]
            moving_average_200 = df_rs_rating["SMA_200"][-1]

            low_of_52week, high_of_52week = df_rs_rating["Adj Close"][
                -52 * DAYS_PER_WEEK
            ].agg(["min", "max"])

            moving_average_200_20past = df_rs_rating["SMA_200"].get(-20, 0)

            print("Checking", file_path.name, "...")
            print(df_rs_rating.tail())

            conditions = [
                current_close > max(moving_average_150, moving_average_200),
                moving_average_150 > moving_average_200,
                # moving_average_200 > moving_average_200_20past,
                # moving_average_50
                # > max(moving_average_150, moving_average_200),
                # current_close > moving_average_50,
                # current_close >= 1.3 * low_of_52week,
                # current_close >= 0.75 * high_of_52week,
                # rs_rating > 70,
            ]
            if all(conditions):
                stock_dataframes.append(
                    pd.DataFrame(
                        {
                            "Stock": stock,
                            "RS_Rating": rs_rating,
                            "50 Day MA": moving_average_50,
                            "150 Day MA": moving_average_150,
                            "200 Day MA": moving_average_200,
                            "52 Week Low": low_of_52week,
                            "52 Week High": high_of_52week,
                        }
                    )
                )

        except Exception:
            LOG.exception("No data on %s", stock)

    result = pd.concat(stock_dataframes, ignore_index=True)
    result.to_csv("exportlist.csv")
    print(result.head())
    print(len(result))
    print(time.monotonic() - start_time)
    return result
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
TechHippie420
User
Beiträge: 11
Registriert: Montag 22. Februar 2021, 10:03

Moin __blackjack__ ,

erstmal vielen vielen Dank für Deine wirklich ausführliche Antwort.
Wie Du sicher schon bemerkt hast, bin ich nicht so der talentierteste coder und weiß bei vielem nicht wirklich was ich tue.
Ich versuche einfach meine Ideen umzusetzen und nehme dafür code aus tutorials (youtube etc) und versuche ihn dann für meine Zwecke anzupassen.
Das übergebene lbl_PortfolioOverview wird doch aber ganz unten in meinem code mit "self.lbl_PortfolioOverview.configure(text = exportlist)"
verwendet.

was Deine Lösung angeht habe ich diese mal ausprobiert und bis auf den Part mit:

Code: Alles auswählen

low_of_52week, high_of_52week = df_rs_rating["Adj Close"][
                 -52 * DAYS_PER_WEEK
                ].agg(["min", "max"])
wobei mir VSCode sagt irgendwas has no Attribute agg
hat es auch eigentlich ganz gut funktioniert.

Eigentlich, weil ich so einen Dataframe mit etwa 1,5 Millionen Zeilen bekomme. Also alle Zeilen von allen Aktien (stock) welche die Bedingungen erfüllen. - Vermutlich durch das concat?
Was ich aber eigentlich haben will ist für jede Aktie, welche die Bedingungen erfüllt nur eine Zeile (bisher etwa 350 von knapp 700 csv's, da auch die Bedingung 'rs_rating > 70' nicht funktioniert und mir 'The truth value of a Series is ambiguous. Use a.empty, a.bool(), a.item(), a.any() or a.all().' liefert - was ich mal so gar nicht verstehe, weil das ja alles truth oder false values sind.)

Nun ja, habe jetzt mal etwas mit Deinem Code weiter rumprobiert (Den Teil mit den Bedingungen fand ich echt richtig sexy und schön kurz) und bekomme jetzt in meiner alten abgeänderten Funktion 'createPortfolioLocal' einen DataFrame von Listen.

Anstelle jetzt hier Unmengen von Code zu posten, habe ich mal GitHub eingerichtet - sieht aus als hätte das auch geklappt.
Also hier mal der Link zum kompletten Repository falls Du Dir das Projekt mal in Gänze ansehen möchtest:

https://github.com/420DevStudio/Personal-Broker.

Warnung ist vermutlich etwas sehr chaotisch

Also nochmals besten Dank und einen schönen Tag

Beste Grüße

TechHippie420
Benutzeravatar
__blackjack__
User
Beiträge: 12984
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@TechHippie420: `lbl_PortfolioOverview` wird nicht verwendet, Du schreibst es ja selbst: ganz am Ende wird auf das Attribut `self.lbl_PortfolioOverview` zugegriffen, nicht auf das Argument `lbl_PortfolioOverview`.

Bei `low_of_52week` & Co fehlte ein Doppelpunkt bei dem Indexzugriff. Wobei ich immer noch verwundert bin, dass das überhaupt funktioniert, weil das ja eigentlich Schlüsselzugriffe sein müssten.

Die vielen Datensätze bekommst Du weil ich das `append()` falsch interpretiert habe, beziehungsweise Du da einen Fehler gemacht hast, der letztendlich für Dein ursprungsproblem verantwortlich ist. Du sagst Du willst da *einen* Datensatz anhängen, aber Du hast da als einzelne Zellenwerte immer komplette `Series`-Objekte mit allen Werten verwendet.

Ja bei `rs_rating > 70` kommt ein `Series`-Objekt mit lauter Wahrheitswerten bei heraus. Und das verwendest Du dann als *Bedingung* als *einen* Wahrheitswert, und *das* ist eben nicht eindeutig was mehrere Wahrheitswerte als *ein* Wahrheitswert bedeuten sollen. Da fehlt also noch Code der das auf *einen* Wert eindampft. Üblicherweise eine Entscheidung dazwischen ob das wahr sein soll wenn mindestens ein Wert wahr ist oder wenn alle Werte wahr sind. Manchmal auch ob mindestens eine bestimmte Anzahl wahr sind. Falls es beispielsweise alle sein sollen, dann ``(rs_rating > 70).all()``.
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
Antworten