From 8b4a40dcddfef1d4de76fba161039826a144c761 Mon Sep 17 00:00:00 2001 From: Sergey Volnov Date: Mon, 26 Jun 2023 11:12:44 +0100 Subject: [PATCH] Update Morgan Stanley parser (#399) - Handle GOOG stock split correctly - Treat "Staged" releases similarly to "Completed" --- cgt_calc/parsers/mssb.py | 60 ++++++++++++++++--- tests/test_data/mssb/Releases Report.csv | 3 +- tests/test_data/mssb/Withdrawals Report.csv | 5 +- .../test_run_with_example_files_output.txt | 12 ++-- 4 files changed, 65 insertions(+), 15 deletions(-) diff --git a/cgt_calc/parsers/mssb.py b/cgt_calc/parsers/mssb.py index 173c6d5b..eb10a150 100644 --- a/cgt_calc/parsers/mssb.py +++ b/cgt_calc/parsers/mssb.py @@ -6,7 +6,8 @@ from __future__ import annotations import csv -from datetime import datetime +from dataclasses import dataclass +import datetime from decimal import Decimal from pathlib import Path from typing import Final @@ -48,6 +49,20 @@ } +@dataclass +class StockSplit: + """Info about stock split.""" + + symbol: str + date: datetime.date + factor: int + + +STOCK_SPLIT_INFO = [ + StockSplit(symbol="GOOG", date=datetime.datetime(2022, 6, 15).date(), factor=20), +] + + def _hacky_parse_decimal(decimal: str) -> Decimal: return Decimal(decimal.replace(",", "")) @@ -60,7 +75,7 @@ def _init_from_release_report(row_raw: list[str], filename: str) -> BrokerTransa if row["Type"] != "Release": raise ParsingError(filename, f"Unknown type: {row_raw[3]}") - if row["Status"] != "Complete": + if row["Status"] != "Complete" and row["Status"] != "Staged": raise ParsingError(filename, f"Unknown status: {row_raw[5]}") if row["Price"][0] != "$": @@ -79,7 +94,7 @@ def _init_from_release_report(row_raw: list[str], filename: str) -> BrokerTransa symbol = TICKER_RENAMES.get(symbol, symbol) return BrokerTransaction( - date=datetime.strptime(row["Vest Date"], "%d-%b-%Y").date(), + date=datetime.datetime.strptime(row["Vest Date"], "%d-%b-%Y").date(), action=ActionType.STOCK_ACTIVITY, symbol=symbol, description=row["Plan"], @@ -92,9 +107,38 @@ def _init_from_release_report(row_raw: list[str], filename: str) -> BrokerTransa ) +# Morgan Stanley decided to put a notice in the end of the withdrawal report that looks +# like that: +# "Please note that any Alphabet share sales, transfers, or deposits that occurred on +# or prior to the July 15, 2022 stock split are reflected in pre-split. Any sales, +# transfers, or deposits that occurred after July 15, 2022 are in post-split values. +# For GSU vests, your activity is displayed in post-split values." +# It makes sense, but it totally breaks the CSV parsing +def _is_notice(row: list[str]) -> bool: + return row[0][:11] == "Please note" + + +def _handle_stock_split(transaction: BrokerTransaction) -> BrokerTransaction: + for split in STOCK_SPLIT_INFO: + if ( + transaction.symbol == split.symbol + and transaction.action == ActionType.SELL + and transaction.date < split.date + ): + if transaction.quantity: + transaction.quantity *= split.factor + if transaction.price: + transaction.price /= split.factor + + return transaction + + def _init_from_withdrawal_report( row_raw: list[str], filename: str -) -> BrokerTransaction: +) -> BrokerTransaction | None: + if _is_notice(row_raw): + return None + if len(COLUMNS_WITHDRAWAL) != len(row_raw): raise UnexpectedColumnCountError(row_raw, len(COLUMNS_WITHDRAWAL), filename) row = {col: row_raw[i] for i, col in enumerate(COLUMNS_WITHDRAWAL)} @@ -122,8 +166,8 @@ def _init_from_withdrawal_report( else: action = ActionType.SELL - return BrokerTransaction( - date=datetime.strptime(row["Date"], "%d-%b-%Y").date(), + transaction = BrokerTransaction( + date=datetime.datetime.strptime(row["Date"], "%d-%b-%Y").date(), action=action, symbol=KNOWN_SYMBOL_DICT[row["Plan"]], description=row["Plan"], @@ -135,6 +179,8 @@ def _init_from_withdrawal_report( broker="Morgan Stanley", ) + return _handle_stock_split(transaction) + def _validate_header( header: list[str], golden_header: list[str], filename: str @@ -172,4 +218,4 @@ def read_mssb_transactions(transactions_folder: str) -> list[BrokerTransaction]: _init_from_release_report(row, str(file)) for row in lines ] - return transactions + return [transaction for transaction in transactions if transaction] diff --git a/tests/test_data/mssb/Releases Report.csv b/tests/test_data/mssb/Releases Report.csv index 50f74e9b..232b9bd6 100644 --- a/tests/test_data/mssb/Releases Report.csv +++ b/tests/test_data/mssb/Releases Report.csv @@ -1,2 +1,3 @@ Vest Date,Order Number,Plan,Type,Status,Price,Quantity,Net Cash Proceeds,Net Share Proceeds,Tax Payment Method -25-Mar-2021,ORDER_NUMBER_HIDDEN,GSU Class C,Release,Complete,"$2,045.06",20.000,$0.00,10.60,Fractional Shares +25-Mar-2021,ORDER_NUMBER_HIDDEN,GSU Class C,Release,Complete,$102.25,400.000,$0.00,212.0,Fractional Shares +25-Mar-2023,ORDER_NUMBER_HIDDEN,GSU Class C,Release,Complete,$121.64,40.000,$0.00,21.201,Fractional Shares diff --git a/tests/test_data/mssb/Withdrawals Report.csv b/tests/test_data/mssb/Withdrawals Report.csv index 1b806d5a..c6a3c3f5 100644 --- a/tests/test_data/mssb/Withdrawals Report.csv +++ b/tests/test_data/mssb/Withdrawals Report.csv @@ -1,3 +1,6 @@ Date,Order Number,Plan,Type,Order Status,Price,Quantity,Net Amount,Net Share Proceeds,Tax Payment Method 01-Apr-2021,ORDER_NUMBER_HIDDEN,GSU Class C,Sale,Complete,"$2,110.00",-2,"$4,219.95",0,N/A -02-Apr-2023,ORDER_NUMBER_HIDDEN,Cash,Sale,Complete,$1.00,"-4,218.95","$4,218.95",0,N/A +02-Apr-2021,ORDER_NUMBER_HIDDEN,Cash,Sale,Complete,$1.00,"-4,218.95","$4,218.95",0,N/A +09-Feb-2023,ORDER_NUMBER_HIDDEN,Cash,Sale,Complete,$1.00,"-3,170.93","$3,170.93",0,N/A +07-Feb-2023,ORDER_NUMBER_HIDDEN,GSU Class C,Sale,Complete,$105.70,-30.00,"$3,170.93",0,N/A +Please note that any Alphabet share sales, transfers, or deposits that occurred on or prior to the July 15, 2022 stock split are reflected in pre-split. Any sales, transfers, or deposits that occurred after July 15, 2022 are in post-split values. For GSU vests, your activity is displayed in post-split values. diff --git a/tests/test_data/test_run_with_example_files_output.txt b/tests/test_data/test_run_with_example_files_output.txt index 5e2ad988..dab7befd 100644 --- a/tests/test_data/test_run_with_example_files_output.txt +++ b/tests/test_data/test_run_with_example_files_output.txt @@ -6,7 +6,7 @@ First pass completed Final portfolio: GE: 1.00 NVDA: 1.00 - GOOG: 8.60 + GOOG: 163.20 FOO: 30.50 Final balance: Charles Schwab: 9477.90 (USD) @@ -24,15 +24,15 @@ Portfolio at the end of 2020/2021 tax year: GE: 1.00, £8.07 NVDA: 1.00, £401.01 FOO: 30.50, £543.97 - GOOG: 8.60, £12516.92 + GOOG: 172.00, £12516.55 For tax year 2020/2021: Number of disposals: 4 Disposal proceeds: £42769.56 -Allowable costs: £17656.17 -Capital gain: £25113.39 +Allowable costs: £17656.08 +Capital gain: £25113.48 Capital loss: £0.00 -Total capital gain: £25113.39 -Taxable capital gain: £12813.39 +Total capital gain: £25113.48 +Taxable capital gain: £12813.48 Generate calculations report All done!