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.
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: 6744
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: 6744
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: 13117
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.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Benutzeravatar
snafu
User
Beiträge: 6744
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