Selbermachen oder in Auftrag geben? Fifo Buchhaltung

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Benutzeravatar
snafu
User
Beiträge: 6731
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Was soll eigentlich passieren, wenn z.B. 0,8 BTC durch die Käufe vorhanden sind und der nächste Sell 0,3 BTC vorgibt? Also was passiert mit der Differenz von 0,5 BTC, die dabei entsteht? Ist die einfach weg oder bleibt die trotz Verkauf irgendwie doch vorhanden und kann für den nächsten Verkauf mitgenutzt werden? Ich denke mal eher letzteres, also dass der letzte Einkauf nur zum Teil verkauft wird, oder?

Ich habe hier etwas gebastelt, wie ich an die Sache herangehen würde:
https://gist.github.com/seblin/bbf61cb8 ... 226ea972e8

Allerdings ist die Verrechnung der Käufe noch nicht korrekt implementiert, da Einkäufe zum Teil schon als Gewinn verbucht werden, obwohl sie noch nicht bzw noch nicht komplett durch einen Verkauf gedeckt wurden.
Fiffio
User
Beiträge: 33
Registriert: Donnerstag 21. Februar 2019, 21:20

snafu hat geschrieben: Dienstag 26. März 2019, 21:50 Was soll eigentlich passieren, wenn z.B. 0,8 BTC durch die Käufe vorhanden sind und der nächste Sell 0,3 BTC vorgibt? Also was passiert mit der Differenz von 0,5 BTC, die dabei entsteht? Ist die einfach weg oder bleibt die trotz Verkauf irgendwie doch vorhanden und kann für den nächsten Verkauf mitgenutzt werden? Ich denke mal eher letzteres, also dass der letzte Einkauf nur zum Teil verkauft wird, oder?
Ja genau. bords0 der das Script gemacht hat hat das hier beschrieben.
bords0 hat geschrieben: Freitag 22. März 2019, 21:47 ...Die eigentliche Arbeit passiert in fifo. Dort wird jeder Verkauf angeschaut, und so lange werden Einkäufe damit verrechnet, bis es reicht. Von dem Einkauf muss man sich halt den Rest merken.
Dann kommt der nächste Einkauf dran. ...
Das muss wohl in diesem Teil des Script sein? Wo der übrig geblieben Teil von z.B. 0,5 BTC gespeichert wird. Ichhabs noch nicht kapiert. :)

Code: Alles auswählen

while amount_sell > 1e-10:  # Falls es am Schluss genau aufgeht, kann die Rechengenauigkeit sonst ein Problem machen
            if amount_buy == 0.0:  # nächster Kauf wird benötigt
                i_buy, amount_buy = next(iter_buys)
            amount = min(amount_buy, amount_sell)
            amount_sell -= amount
            amount_buy -= amount
An der Börse kann das so aussehen:
Im Depo sind z.B. die Anzahl von 0,8 BTC durch Kauftrades vorhanden. Sagen wir gekauft bei Kurs zu 2000.- EUR. Dann geht der Kurs weiter hoch zu 2500.- EUR. Man möchte verkaufen und stellt eine Limitorder mit Verkaufspreis z.B. bei 2520.- ein. Doch der Kurs geht nicht höher und es wird nur ein Teil (Sell 0,3 BTC) bei dem Kurs von 2520.- verkauft. Der Rest 0,5 BTC bleibt als Limitorder stehen und der Kurs fällt wieder nach unten. Die 0,5 BTC müssen dann also zu einem anderen Zeitpunkt (und Kurs) verkauft werden. Usw.
Fiffio
User
Beiträge: 33
Registriert: Donnerstag 21. Februar 2019, 21:20

snafu hat geschrieben: Dienstag 26. März 2019, 21:50 Ich habe hier etwas gebastelt, wie ich an die Sache herangehen würde:
https://gist.github.com/seblin/bbf61cb8 ... 226ea972e8
Hab mal getestet ob dein Script meckert, wenn man von den Käufen etwas wegnimmt.
Summ der Käufe und Verkäufe ist ja:
buy 1.80000010
sell -1.80000010
also ist das Depo am Ende bei null BTC.

Das Script müsste schon hier meckern. Bsp:
2017-01-08 11:43:00,buy,1.00000001,210.55 (manipuliert)
2017-01-08 11:43:00,buy,1.00000002,210.55 (original)

Code: Alles auswählen

----- Verkauf #1 -------------------------
Verbleibende Bitcoins: 1.0000000100000002
Gesamtsaldo (€):       88.92

Verbleibende Käufe:
             DateTime Type  Amount    Rate
3 2017-01-11 18:43:00  buy     0.3  300.88
4 2017-01-12 18:55:00  buy     0.4  311.22

----- Verkauf #2 -------------------------
Verbleibende Bitcoins: 0.40000004
Gesamtsaldo (€):       132.12

Verbleibende Käufe:
             DateTime Type  Amount    Rate
4 2017-01-12 18:55:00  buy     0.4  311.22

----- Verkauf #3 -------------------------
Verbleibende Bitcoins: 0.700000079
Gesamtsaldo (€):       229.94

Verbleibende Käufe:
Keine
Es meckert nicht.

Es müsste "Need" 0.00000001 heißen.

Erst wenn man den buy Datensatz von 1.00000002 bis auf 0.50000000 minimiert
2017-01-08 11:43:00,buy,0.50000000,210.55
heißt es:

Code: Alles auswählen

ValueError: Need 0.10000000099999999 BTC, have 0.30000006 BTC
Die Zahlen stimmen irgendwie nicht? Es müsste nach meiner Rechnung Need -0.50000002 lauten.
Benutzeravatar
snafu
User
Beiträge: 6731
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Ich schau da morgen nochmal drüber. Habe ja angemerkt, dass es noch fehlerhaft ist.

Mein Code geht u.a. davon aus, dass für jeden Verkauf mindestens ein gekauftes Paket genutzt wird. Das ist natürlich falsch, da manchmal genug Reserven aus den Resten eines zuvor gekauften und nicht komplett ausgeschöpften Pakets vorhanden sind, um den Verkauf vollständig abwickeln zu können. Umgekehrt können Verkäufe aus Mangel an Bitcoins ab und zu auch gar nicht stattfinden.

Abläufe aus dem Finanzwesen nachzuprogrammieren und halt komplex und fehleranfällig. Ich an deiner Stelle hätte da schon längst einen Experten beauftragt - auch wenn der ein paar Taler kostet, aber das soll jeder selbst entscheiden...
bords0
User
Beiträge: 234
Registriert: Mittwoch 4. Juli 2007, 20:40

Wie könnte diese Fehlermeldung abgefangen werden?

RuntimeError: generator raised StopIteration

Das passiert, wenn mehr verkauft werden soll als vorhanden ist. Beispiel:

Code: Alles auswählen

raw = io.StringIO("""DateTime,           Type,  Amount,  Rate
2017-01-08 11:43:00,buy,0.10000001,200.11
2017-01-08 11:43:00,buy,1.00000001,210.55                   # statt 1.00000002
2017-01-10 16:11:44,sell,0.10000002,290.44
2017-01-11 18:43:00,buy,0.30000003,300.88
2017-01-12 18:55:00,buy,0.40000004,311.22
2017-01-14 22:22:11,sell,0.90000000,444.88
2018-02-15 09:22:55,sell,0.80000008,555.77                  # Achtung es sind nur 0.80000007 zum Verkauf da!
""")
StopIteration ist eine Exception. Die tritt hier auf, weil mit

Code: Alles auswählen

i_buy, amount_buy = next(iter_buys)
versucht wird, noch einen Kauf aus iter_buys herauszubekommen. Alles von iter_buys ist aber schon verbraucht, und deshalb wirft iter_buys (ein generator) diese Exception: "Stop jetzt, ich kann nicht mehr iterieren".
Abfangen kann man das mit try: except:, mit dem man Ausnahmen (Exceptions) abfangen kann.

Das habe ich gemacht im folgenden Code. Der hat jetzt alle Berechnungen außerhalb der Funktion fifo (die eigentlich eine generator function ist), in der Schleife, in der du auch deinen eigenen Berechnungen und Ausgaben eingebaut hast. (Das rettet wenigstens ein wenig die Struktur, die inzwischen nicht so schön geworden ist.)
Im Fall, dass nicht genug Käufe vorhanden sind, gibt fifo jetzt None für die Nummer des Kaufs zurück, und die Ausgabe in der Schleife habe ich entsprechend angepasst.
Außerdem habe ich einen zusätzlichen Verkauf eingebaut, damit man sieht, was passiert, wenn mehrere Verkäufe nicht bedient werden können.

Wenn deine Daten nicht konsistent sind, musst du evtl. auch aufpassen, dass keine Verkäufe mit Käufen verrechnet werden, die erst später kommen - aber dazu habe ich nichts eingebaut.

Code: Alles auswählen

import pandas as pd
import io
from datetime import timedelta, datetime
week = timedelta(weeks=1)

raw = io.StringIO("""DateTime,           Type,  Amount,  Rate
2017-01-08 11:43:00,buy,0.10000001,200.11
2017-01-08 11:43:00,buy,1.00000001,210.55
2017-01-10 16:11:44,sell,0.10000002,290.44
2017-01-11 18:43:00,buy,0.30000003,300.88
2017-01-12 18:55:00,buy,0.40000004,311.22
2017-01-14 22:22:11,sell,0.90000000,444.88
2018-02-15 09:22:55,sell,0.80000008,555.77
2018-02-16 09:22:55,sell,1.00000008,445.55
""")
file = r'bitstamp1.csv'
daten = pd.read_csv(raw, parse_dates=[0], 
                    skipinitialspace=True)  # damit die header vernünftig sind

buys = daten[daten.Type == "buy"]
sells = daten[daten.Type == "sell"]

def fifo(buys, sells):
    iter_buys = buys.Amount.iteritems()  # enumerate(buys)
    i_buy, amount_buy = None, 0.0
    for i_sell, amount_sell in sells.Amount.iteritems():
        while amount_sell > 1e-10:  # Falls es am Schluss genau aufgeht, kann die Rechengenauigkeit sonst ein Problem machen
            if amount_buy == 0.0:  # nächster Kauf wird benötigt
                try:
                    i_buy, amount_buy = next(iter_buys)
                except StopIteration:
                    yield i_sell, None, amount_sell
                    break
            amount = min(amount_buy, amount_sell)
            amount_sell -= amount
            amount_buy -= amount
            yield i_sell, i_buy, amount


for i_sell, i_buy, amount in fifo(buys, sells):
    sell_t = sells.at[i_sell, "DateTime"]
    sell_r = sells.at[i_sell, "Rate"]
    if i_buy is None:
        print(f"\nSELL id {i_sell} {sell_t} (amount {amount:0.8f} BTC) {sell_r} EUR")
        print("no matching buy found")
        continue
    
    buy_t = buys.at[i_buy, "DateTime"]
    buy_r = buys.at[i_buy, "Rate"]
    rate_diff = sell_r - buy_r
    time_diff = sell_t - buy_t
    print(f"\nSELL id {i_sell} {sell_t} (amount {amount:0.8f} BTC) {sell_r} EUR")
    print(f" BUY id {i_buy} {buy_t}         {amount:0.8f} BTC  {buy_r} EUR")
    print(f"              Rate Diff  {rate_diff:0.2f} EUR * {amount:0.8f} BTC = {rate_diff * amount:0.2f} EUR")
    if time_diff.days >= 365:
        print(f"              Haltedauer > 1 Jahr: OK ({time_diff.days} days)")
    else:
        print(f" Haltedauer > 1 Jahr: Nein ({time_diff.days} days)")
Ausgabe:

Code: Alles auswählen

SELL id 2 2017-01-10 16:11:44 (amount 0.10000001 BTC) 290.44 EUR
 BUY id 0 2017-01-08 11:43:00         0.10000001 BTC  200.11 EUR
              Rate Diff  90.33 EUR * 0.10000001 BTC = 9.03 EUR
 Haltedauer > 1 Jahr: Nein (2 days)

SELL id 2 2017-01-10 16:11:44 (amount 0.00000001 BTC) 290.44 EUR
 BUY id 1 2017-01-08 11:43:00         0.00000001 BTC  210.55 EUR
              Rate Diff  79.89 EUR * 0.00000001 BTC = 0.00 EUR
 Haltedauer > 1 Jahr: Nein (2 days)

SELL id 5 2017-01-14 22:22:11 (amount 0.90000000 BTC) 444.88 EUR
 BUY id 1 2017-01-08 11:43:00         0.90000000 BTC  210.55 EUR
              Rate Diff  234.33 EUR * 0.90000000 BTC = 210.90 EUR
 Haltedauer > 1 Jahr: Nein (6 days)

SELL id 6 2018-02-15 09:22:55 (amount 0.10000000 BTC) 555.77 EUR
 BUY id 1 2017-01-08 11:43:00         0.10000000 BTC  210.55 EUR
              Rate Diff  345.22 EUR * 0.10000000 BTC = 34.52 EUR
              Haltedauer > 1 Jahr: OK (402 days)

SELL id 6 2018-02-15 09:22:55 (amount 0.30000003 BTC) 555.77 EUR
 BUY id 3 2017-01-11 18:43:00         0.30000003 BTC  300.88 EUR
              Rate Diff  254.89 EUR * 0.30000003 BTC = 76.47 EUR
              Haltedauer > 1 Jahr: OK (399 days)

SELL id 6 2018-02-15 09:22:55 (amount 0.40000004 BTC) 555.77 EUR
 BUY id 4 2017-01-12 18:55:00         0.40000004 BTC  311.22 EUR
              Rate Diff  244.55 EUR * 0.40000004 BTC = 97.82 EUR
              Haltedauer > 1 Jahr: OK (398 days)

SELL id 6 2018-02-15 09:22:55 (amount 0.00000001 BTC) 555.77 EUR
no matching buy found

SELL id 7 2018-02-16 09:22:55 (amount 1.00000008 BTC) 445.55 EUR
no matching buy found
Fiffio
User
Beiträge: 33
Registriert: Donnerstag 21. Februar 2019, 21:20

snafu hat geschrieben: Dienstag 26. März 2019, 23:20 ...Abläufe aus dem Finanzwesen nachzuprogrammieren und halt komplex und fehleranfällig. Ich an deiner Stelle hätte da schon längst einen Experten beauftragt - auch wenn der ein paar Taler kostet, aber das soll jeder selbst entscheiden...
Das hatte ich ja auch vor "einen Experten beauftragt", aber mir fehlt es halt u.a. an "Delegations"- und sonstigen Erfahrung. ;) Und nun war ich ganz mit dem Script von User bords0 beschäftigt, weil das schon ziemlich nach dem aussah, was mir so vorschwebte.
Fiffio
User
Beiträge: 33
Registriert: Donnerstag 21. Februar 2019, 21:20

Bords0 und snafu,

was meint ihr, welche Möglichkeiten sind sinnvoll weiter vorzugehen?
Ich selber bin „Laie“ und mache das ganze hier nach der Methode „Learning by Doing“.
Mir wäre es natürlich lieb, wenn der Weg nicht mehr all zu lang ist, aber falsche Hoffnungen will ich mir auch nicht machen.
snafu hat geschrieben: Dienstag 26. März 2019, 23:20 ...
Abläufe aus dem Finanzwesen nachzuprogrammieren sind halt komplex und fehleranfällig. Ich an deiner Stelle hätte da schon längst einen Experten beauftragt - auch wenn der ein paar Taler kostet,...
Wie gesagt, den Experten, der mir alle Probleme bei dem Fifo Ding abnimmt, hätte ich auch gerne beauftragt. __deets__ hatte ja leider keine Zeit. ;)
__deets__ hat geschrieben: Donnerstag 21. März 2019, 18:24...belegt.
Kenst du noch einen?

Snafu, willst du an deinem zweiten Script weitermachen? Bist du einfach an dem Thema interessiert? Macht das Sinn zwei Scripte? Zumindest könnte man die berechneten Zahlen dann vergleichen. Aber wenn ich dir alles ein zweites mal erklären muss kostet das Zeit. ;) Das ist eher das Problem, als ein paar Taler springen lassen.

Bords0 würde es Sinn machen dein Script mit Git (jetzt) online zu stellen? Oder ist das nur Mehraufwand? Ich könnte z.B. in Bitcoinforen einen Beitrag posten um das zum testen anzubieten. Dann könnten Leute an Hand ihrer Daten Feedback geben. Oder es hat doch der ein oder andere was privat in die Richtung gemacht und will es dann beisteuern, was er bisher nicht öffentlich gemacht hat. Aber ich habe keine Ahnung von Git (was hochladen würde ich hinkriegen) und weiß nicht ob solche Aktionen (Mehraufwand) jetzt Sinn machen. Das wohl doch ziemlich „idealistisch“.
bords0 hat geschrieben: Dienstag 26. März 2019, 23:26 Wenn deine Daten nicht konsistent sind, musst du evtl. auch aufpassen, dass keine Verkäufe mit Käufen verrechnet werden, die erst später kommen - aber dazu habe ich nichts eingebaut.
Ja in diesem Sinn können Daten inkonsistent sein.

Was meinst du wie ich vorgehen soll? Was denkst du von deiner Seite was das Script noch bräuchte, außer dass es getestet werden muss? Kannst du das noch einbauen, „Sperre für ungewollte Verrechnung von früheren Verkäufen mit späteren Käufen“?
Ich erwarte nicht dich ewig kostenlos verpflichten zu können.

Ich habe angefangen von den Auswertungen etwas in eine Datei schreiben zu lassen.

Code: Alles auswählen

output_pl.write(f"{sell_t}\t{rate_diff * amount:0.2f}\t{holdingNO}{holdingOK}\n")
Das klappt mit der Berechnung der Zeiten noch nicht. (Haltedauer heißt jetzt holdingOK/ holdingNO)
Wie bekommt man ({time_diff.days} days) von if /else in die for Schleife bzw. in die output-Datei?
Jetzt sieht es so aus:

Code: Alles auswählen

2017-01-10 16:11:44	-48.69	{6}{398}
2017-01-10 16:11:44	0.00	{2}{398}
2017-01-14 22:22:11	210.90	{2}{398}
2018-02-15 09:22:55	34.52	{6}{398}
2018-02-15 09:22:55	76.47	{6}{402}
2018-02-15 09:22:55	97.82	{6}{399}
Der ersten buy hat jetzt einen höheren Preis/Rate, so ergibt die erste Berechnung ein Minusbetrag. Das muss auch dabei sein.

Code: Alles auswählen

import pandas as pd
import io
from datetime import timedelta, datetime
week = timedelta(weeks=1)

raw = io.StringIO("""DateTime,           Type,  Amount,  Rate
2017-01-08 11:43:00,buy,0.10000001,777.33
2017-01-08 11:43:00,buy,1.00000002,210.55
2017-01-10 16:11:44,sell,0.10000002,290.44
2017-01-11 18:43:00,buy,0.30000003,300.88
2017-01-12 18:55:00,buy,0.40000004,311.22
2017-01-14 22:22:11,sell,0.90000000,444.88
2018-02-15 09:22:55,sell,0.80000008,555.77
""")
file = r'datafile.csv'
daten = pd.read_csv(raw, parse_dates=[0], 
                    skipinitialspace=True)  # damit die header vernünftig sind
output_pl = open("output_pl.txt","w")
output = open("output.txt","w")
buys = daten[daten.Type == "buy"]
sells = daten[daten.Type == "sell"]

def fifo(buys, sells):
    iter_buys = buys.Amount.iteritems()  # enumerate(buys)
    i_buy, amount_buy = None, 0.0
    for i_sell, amount_sell in sells.Amount.iteritems():
        while amount_sell > 1e-10:  # Falls es am Schluss genau aufgeht, kann die Rechengenauigkeit sonst ein Problem machen
            if amount_buy == 0.0:  # nächster Kauf wird benötigt
                i_buy, amount_buy = next(iter_buys)
            amount = min(amount_buy, amount_sell)
            amount_sell -= amount
            amount_buy -= amount
            rate_diff = sells.at[i_sell, "Rate"] - buys.at[i_buy, "Rate"]
            time_diff = sells.at[i_sell, "DateTime"] - buys.at[i_buy, "DateTime"]
            yield i_sell, i_buy, amount, rate_diff, time_diff
                            
for i_sell, i_buy, amount, rate_diff, time_diff in fifo(buys, sells):
    sell_t = sells.at[i_sell, "DateTime"]
    buy_t = buys.at[i_buy, "DateTime"]
    sell_r = sells.at[i_sell, "Rate"]
    buy_r = buys.at[i_buy, "Rate"]
    output_pl.write(f"{sell_t}\t{rate_diff * amount:0.2f}\t{holdingNO}{holdingOK}\n")
    output.write(f"\nSELL id {i_sell} {sell_t} (Amount -{amount:0.8f} BTC) {sell_r} EUR\n"
                        f" BUY id {i_buy} {buy_t}          {amount:0.8f} BTC  {buy_r} EUR\n"
                        f"               Rate Diff  {rate_diff:0.2f} EUR * {amount:0.8f} BTC = " 
                        f"{rate_diff * amount:0.2f} EUR\n")
                        
    print(f"\nSELL id {i_sell} {sell_t} (Amount -{amount:0.8f} BTC) {sell_r} EUR")
    print(f" BUY id {i_buy} {buy_t}          {amount:0.8f} BTC  {buy_r} EUR")
    print(f"               Rate Diff  {rate_diff:0.2f} EUR * {amount:0.8f} BTC = {rate_diff * amount:0.2f} EUR")
    if time_diff.days >= 365:
        print(f"              holding period > 1 year: OK ({time_diff.days} days)")
        holdingOK = {time_diff.days}
    else:
        print(f" holding period > 1 year: NO ({time_diff.days} days)")
        holdingNO = {time_diff.days}
bords0 hat geschrieben: Dienstag 26. März 2019, 23:26(Das rettet wenigstens ein wenig die Struktur, die inzwischen nicht so schön geworden ist.)
Tut mir Leid. :D
bords0
User
Beiträge: 234
Registriert: Mittwoch 4. Juli 2007, 20:40

Fiffio hat geschrieben: Mittwoch 27. März 2019, 10:23 Bords0 und snafu,

was meint ihr, welche Möglichkeiten sind sinnvoll weiter vorzugehen?
Also ich glaube, dass du es eigentich so gut wie fertig hast... Aber das hängt natürlich ab, was du noch alles brauchst. Es war zum Beispiel ein Überraschung für mich, dass deine Daten nicht "sauber" sind.
Fiffio hat geschrieben: Mittwoch 27. März 2019, 10:23 Bords0 würde es Sinn machen dein Script mit Git (jetzt) online zu stellen?
Glaub ich nicht, man kann ja auf das Forum verweisen.
Fiffio hat geschrieben: Mittwoch 27. März 2019, 10:23 Was meinst du wie ich vorgehen soll? Was denkst du von deiner Seite was das Script noch bräuchte, außer dass es getestet werden muss? Kannst du das noch einbauen, „Sperre für ungewollte Verrechnung von früheren Verkäufen mit späteren Käufen“?
Ja, ich habs eingebaut, siehe unten. Ich rate aber dringend dazu, die Funktion fifo nur die Tupel (sell_id, buy_id, amount) ausgeben zu lassen, und nur in der Schleife im Hauptprogramm die Ausgabe zu machen.
Fiffio hat geschrieben: Mittwoch 27. März 2019, 10:23 Ich erwarte nicht dich ewig kostenlos verpflichten zu können.
Danke, das ist nett, und ich weiß das zu schätzen. Wenn du willst, spende einen Betrag, den du für angemessen hältst, z.B. an Spyder (www.spyder-ide.org), damit "arbeite" ich meistens.
Ich selbst möchte nichts.
Fiffio hat geschrieben: Mittwoch 27. März 2019, 10:23 Ich habe angefangen von den Auswertungen etwas in eine Datei schreiben zu lassen.

Code: Alles auswählen

output_pl.write(f"{sell_t}\t{rate_diff * amount:0.2f}\t{holdingNO}{holdingOK}\n")
Das klappt mit der Berechnung der Zeiten noch nicht. (Haltedauer heißt jetzt holdingOK/ holdingNO)
Ja, das sieht seltsam aus. Du willst sowohl holdingNO als auch holdingOK ausgeben, es wird pro Schleifendurchlauf aber nur einer der Werte berechnet, und auch erst, nachdem du den Wert ausgegeben hast.

Vielleicht möchtest du folgendes: Es wird einfach nur ausgegeben, ob es OK ist. Das könnte man in einer Variaben "is_holding_OK" speichern oder so.

Ich habe das Beispiel noch etwas erweitet, damit man die Verkäufe ohne passende Käufe besser sieht.

Code: Alles auswählen

import pandas as pd
import io
from datetime import timedelta, datetime
week = timedelta(weeks=1)

raw = io.StringIO("""DateTime,           Type,  Amount,  Rate
2017-01-08 11:43:00,buy,0.10000001,777.33
2017-01-08 11:43:00,buy,1.00000001,210.55
2017-01-10 16:11:44,sell,0.10000002,290.44
2017-01-11 18:43:00,buy,0.30000003,300.88
2017-01-12 18:55:00,buy,0.40000004,311.22
2017-01-14 22:22:11,sell,0.90000000,444.88
2018-02-15 09:22:55,sell,0.80000008,555.77
2018-02-16 09:22:55,sell,1.00000008,445.55
2018-02-15 18:55:00,buy,0.40000004,311.22
""")
file = r'bitstamp1.csv'
daten = pd.read_csv(raw, parse_dates=[0], 
                    skipinitialspace=True)  # damit die header vernünftig sind

buys = daten[daten.Type == "buy"]
sells = daten[daten.Type == "sell"]

def fifo(buys, sells):
    iter_buys = buys.Amount.iteritems()  # enumerate(buys)
    i_buy, amount_buy = None, 0.0
    for i_sell, amount_sell in sells.Amount.iteritems():
        while amount_sell > 1e-10:  # Falls es am Schluss genau aufgeht, kann die Rechengenauigkeit sonst ein Problem machen
            if amount_buy == 0.0:  # nächster Kauf wird benötigt
                # gibt es noch einen?
                try:
                    i_buy, amount_buy = next(iter_buys)
                except StopIteration:
                    yield i_sell, None, amount_sell
                    break
            # wenn der nicht in der Vergangenheit liegt,
            # dann kann er auch nicht verwendet werden.
            if buys.at[i_buy, "DateTime"] > sells.at[i_sell, "DateTime"]:
                yield i_sell, None, amount_sell
                break
            # verkaufen kann ich so viel, wie beim Kauf und beim Verkauf vorhanden ist    
            amount = min(amount_buy, amount_sell)
            amount_sell -= amount
            amount_buy -= amount
            yield i_sell, i_buy, amount

# Ausgabedateien öffnen - und dafür sorgen, dass sie automatisch wieder geschlossen werden
with open("output_pl.txt","w") as output_pl:
    with open("output.txt","w") as output:
        # Jetzt über alle Verkaufs/Kaufs-Paare iterieren
        for i_sell, i_buy, amount in fifo(buys, sells):
            # Verkaufsinfos bestimmen
            sell_t = sells.at[i_sell, "DateTime"]
            sell_r = sells.at[i_sell, "Rate"]
            # Ausgeben und diesen Schleifendurchlauf beenden, wenn kein Verkauf zustande kam
            if i_buy is None:
                print(f"\nSELL id {i_sell} {sell_t} (amount {amount:0.8f} BTC) {sell_r} EUR")
                print("no matching buy found")
                continue
            
            # Kaufinfos bestimmen
            buy_t = buys.at[i_buy, "DateTime"]
            buy_r = buys.at[i_buy, "Rate"]
            
            # Relevante berechnete Werte bestimmen
            rate_diff = sell_r - buy_r
            time_diff = sell_t - buy_t
            is_holding_OK = "OK" if time_diff.days >= 365 else "not OK"
            
            # Ausgabe auf Bildschirm
            print(f"\nSELL id {i_sell} {sell_t} (amount {amount:0.8f} BTC) {sell_r} EUR")
            print(f" BUY id {i_buy} {buy_t}         {amount:0.8f} BTC  {buy_r} EUR")
            print(f"              Rate Diff  {rate_diff:0.2f} EUR * {amount:0.8f} BTC = {rate_diff * amount:0.2f} EUR")
        
            # Ausgabe in Datei
            output_pl.write(f"{sell_t}\t{rate_diff * amount:0.2f}\tholding time is {is_holding_OK} ({time_diff.days} days)\n")
            output.write(f"\nSELL id {i_sell} {sell_t} (Amount -{amount:0.8f} BTC) {sell_r} EUR\n"
                         f" BUY id {i_buy} {buy_t}          {amount:0.8f} BTC  {buy_r} EUR\n"
                         f"               Rate Diff  {rate_diff:0.2f} EUR * {amount:0.8f} BTC = " 
                         f"{rate_diff * amount:0.2f} EUR\n")
                                
Anmerkung: Ich weiß nicht, ob das steuerlich wirklich i.O. mit den 365 Tagen. Wahrscheinlich eine sehr gute Näherung. Falls es Fälle gibt, wo es genau 365 Tage sind, müsste man das aber überprüfen.

Anmerkung 2: Der Code ist nicht schön. Ich wollte aber nicht zu viel ändern, du sollst ihn ja noch wiedererkennen...
Benutzeravatar
snafu
User
Beiträge: 6731
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Habe das grundsätzliche Vorgehen nochmal neugeschrieben und dabei weitere Features von Pandas verwendet:

Code: Alles auswählen

import io
import pandas as pd

TESTDATA = io.StringIO(
"""DateTime,           Type,  Amount,  Rate
2017-01-08 11:43:00,buy,0.10000001,777.33
2017-01-08 11:43:00,buy,1.00000001,210.55
2017-01-10 16:11:44,sell,0.10000002,290.44
2017-01-11 18:43:00,buy,0.30000003,300.88
2017-01-12 18:55:00,buy,0.40000004,311.22
2017-01-14 22:22:11,sell,0.90000000,444.88
2018-02-15 09:22:55,sell,0.80000008,555.77
2018-02-16 09:22:55,sell,1.00000008,445.55
2018-02-15 18:55:00,buy,0.40000004,311.22
""")

def parse_transactions(csv_stream):
    df = pd.read_csv(csv_stream, parse_dates=[0], skipinitialspace=True)
    return (df[df.Type == 'sell'], df[df.Type == 'buy'])

def pick_buys(sell, buys, balance=0):
    if balance >= sell.Amount:
        # Already have enough BTC
        return pd.DataFrame()
    if buys.empty:
        raise ValueError(f'Need more buys (sell ID #{sell.Index})')
    mask = buys.Amount.cumsum() + balance < sell.Amount
    picked = buys.loc[:mask.idxmin() + 1]
    if picked.Amount.sum() < sell.Amount:
        raise ValueError(f'Not enough BTC (sell ID #{sell.Index})')
    return picked

def run_fifo(sells, buys):
    balance = 0
    for sell in sells.itertuples():
        picked = pick_buys(sell, buys, balance)
        picked_amount = 0 if picked.empty else picked.Amount.sum()
        new_balance = balance + picked_amount - sell.Amount
        print_info(sell, picked, balance, new_balance)
        balance = new_balance
        buys = buys.drop(picked.index)

def print_info(sell, buys, balance, new_balance):
    print(f'Selling {sell.Amount} BTC  (ID #{sell.Index})')
    if not buys.empty:
        print(f'Using balance of {balance} BTC and buys:')
        for index, amount in buys.Amount.items():
            print(f'{amount} BTC  (ID #{index})')
    print(f'Balance after sale: {new_balance} BTC')
    print()

def main():
    sells, buys = parse_transactions(TESTDATA)
    try:
        run_fifo(sells, buys)
    except ValueError as e:
        print('STOPPED:', e)

if __name__ == '__main__':
    main()
Die Umrechnung in Euro ist noch nicht eingebaut, lässt sich aber bei Interesse nachholen. Es wäre auch gut zu wissen, welche Form der Ausgabe gewünscht ist. Ich habe da einfach mal ein mir sinnvoll erscheinendes Format genommen.
Benutzeravatar
snafu
User
Beiträge: 6731
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Die pick_buys() nochmal in anderer Form (und IMHO verständlicher) geschrieben:

Code: Alles auswählen

def pick_buys(sell, buys, balance=0):
    if balance >= sell.Amount:
        # Already have enough BTC
        return pd.DataFrame()
    if buys.empty:
        raise ValueError(f'Need more buys (sell ID #{sell.Index})')
    enough = buys.Amount.cumsum() + balance >= sell.Amount
    if not any(enough):
        raise ValueError(f'Not enough BTC (sell ID #{sell.Index})')
    # Stops slicing after the first index meeting the "enough" condition
    return buys.loc[:enough.idxmax() + 1]
Ob das Filtern nun mit Python-Boardmitteln oder mit höherem Pandas-Anteil performanter wird, müsste man noch testen.
Benutzeravatar
__blackjack__
User
Beiträge: 13004
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Also mit der vorherigen Version mit „weniger Pandas“ würde ich sagen das es mit noch weniger Pandas verständlicher wäre, nämlich wenn man da aus den einzelnen Datensätzen Objekte machen würde, statt immer mit der Indirektion über Indizes in den `DataFrame` zu arbeiten.
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
Benutzeravatar
snafu
User
Beiträge: 6731
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Mit etwas weniger Pandas und vorhergehender Berechnung des neuen Saldos:

Code: Alles auswählen

import io
import pandas as pd

TESTDATA = io.StringIO(
"""DateTime,           Type,  Amount,  Rate
2017-01-08 11:43:00,buy,0.10000001,777.33
2017-01-08 11:43:00,buy,1.00000001,210.55
2017-01-10 16:11:44,sell,0.10000002,290.44
2017-01-11 18:43:00,buy,0.30000003,300.88
2017-01-12 18:55:00,buy,0.40000004,311.22
2017-01-14 22:22:11,sell,0.90000000,444.88
2018-02-15 09:22:55,sell,0.80000008,555.77
2018-02-16 09:22:55,sell,1.00000008,445.55
2018-02-15 18:55:00,buy,0.40000004,311.22
""")

def parse_transactions(csv_stream):
    df = pd.read_csv(csv_stream, parse_dates=[0], skipinitialspace=True)
    return (df[df.Type == 'sell'], df[df.Type == 'buy'])

def sell_buys(sell, buys, balance=0):
    balance -= sell.Amount
    if balance >= 0:
        return balance, pd.DataFrame()
    for buy in buys.itertuples():
        balance += buy.Amount
        if balance >= 0:
            return balance, buys.loc[:buy.Index]
    raise ValueError(f'Not enough BTC (Sell ID #{sell.Index})')
    
def run_fifo(sells, buys):
    balance = 0
    for sell in sells.itertuples():
        new_balance, sold_buys = sell_buys(sell, buys, balance)
        print_info(sell, sold_buys, balance, new_balance)
        balance = new_balance
        buys = buys.drop(sold_buys.index)

def print_info(sell, buys, balance, new_balance):
    print(f'Selling {sell.Amount} BTC  (ID #{sell.Index})')
    if not buys.empty:
        print(f'Using balance of {balance} BTC and buys:')
        for index, amount in buys.Amount.items():
            print(f'{amount} BTC  (ID #{index})')
    print(f'Balance after sale: {new_balance} BTC')
    print()

def main():
    sells, buys = parse_transactions(TESTDATA)
    try:
        run_fifo(sells, buys)
    except ValueError as e:
        print('STOPPED:', e)

if __name__ == '__main__':
    main()
Fiffio
User
Beiträge: 33
Registriert: Donnerstag 21. Februar 2019, 21:20

bords0 hat geschrieben: Freitag 29. März 2019, 00:54 Danke, das ist nett, und ich weiß das zu schätzen. Wenn du willst, spende einen Betrag, den du für angemessen hältst, z.B. an Spyder (www.spyder-ide.org), damit "arbeite" ich meistens.
Ich selbst möchte nichts.
Gut dann spende ich etwas für freie Software. Bei Spyder ist das für mich aber etwas kompliziert https://opencollective.com/spyder/donate ich bin kein freund von Google Tracking und ReCaptcha etc. da müsste ich erst meine Surfgrundsätze über den Haufen werfen. Wenn du noch einen anderen Vorschlag hast wo man z.B. (unkompliziert) via Bitcoin spenden kann, dann teile das gerne mit.

Das Script hat mir nun geholfen bei einzelnen Jahren die Summen zu vergleichen.
Für 2017 bin ich zum Glück gerade noch fertig geworden.
Zu tun gibt es immer noch, aber ich brauche jetzt unbedingt eine Pause.

Snafu, habe dein Script kurz ausprobiert, müsste mich damit aber länger beschäftigen.

Vielen Dank an alle für die viele Hilfe! Wie gesagt, jetzt brauche ich zuerst eine richtige Pause. Melde mich wieder.
Antworten