Skip to content

Commit

Permalink
Fix: add _approx_equal_price_rounding to allow price in accepable range
Browse files Browse the repository at this point in the history
  • Loading branch information
hermanho committed Dec 24, 2024
1 parent 2aa2964 commit e28dbd2
Show file tree
Hide file tree
Showing 5 changed files with 134 additions and 2 deletions.
63 changes: 61 additions & 2 deletions cgt_calc/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
from .model import (
ActionType,
BrokerTransaction,
CalcuationType,
CalculationEntry,
CalculationLog,
CapitalGainsReport,
Expand All @@ -57,6 +58,52 @@ def get_amount_or_fail(transaction: BrokerTransaction) -> Decimal:
return amount


# Amount difference can be caused by rounding errors in the price.
# Schwab rounds down the price to 4 decimal places
# so that the error in amount can be more than $0.01.
# Fox example:
# 500 shares of FOO sold at $100.00016 with $1.23 fees.
# "01/01/2024,"Sell","FOO","FOO","500","$100.0001","$1.23","$49,998.85"
# calculated_amount = 500 * 100.0001 - 1.23 = 49998.82
# amount_on_record = 49998.85 vs calculated_amount = 49998.82
def _approx_equal_price_rounding(
amount_on_record: Decimal,
quantity_on_record: Decimal,
price_on_record: Decimal,
fees_on_record: Decimal,
calcuationType: CalcuationType,
) -> bool:
calculated_amount = Decimal(0)
calculated_price = Decimal(0)
if calcuationType is CalcuationType.ACQUISITION:
calculated_amount = Decimal(-1) * (
quantity_on_record * price_on_record + fees_on_record
)
calculated_price = (
Decimal(-1) * amount_on_record - fees_on_record
) / quantity_on_record
elif calcuationType is CalcuationType.DISPOSAL:
calculated_amount = quantity_on_record * price_on_record - fees_on_record
calculated_price = (amount_on_record + fees_on_record) / quantity_on_record
in_acceptable_range = abs(calculated_price - price_on_record) < Decimal("0.0001")
LOGGER.debug(
"Price calculated_price %.6f vs price_on_record %s in %s",
calculated_price,
price_on_record,
"acceptable range" if in_acceptable_range else "error",
)
if in_acceptable_range:
return True
accptable_amount = _approx_equal(amount_on_record, calculated_amount)
LOGGER.debug(
"Amount amount_on_record %.6f vs calculated_amount %s in %s",
amount_on_record,
calculated_amount,
"acceptable range" if accptable_amount else "error",
)
return accptable_amount


# It is not clear how Schwab or other brokers round the dollar value,
# so assume the values are equal if they are within $0.01.
def _approx_equal(val_a: Decimal, val_b: Decimal) -> bool:
Expand Down Expand Up @@ -131,7 +178,13 @@ def add_acquisition(

amount = get_amount_or_fail(transaction)
calculated_amount = quantity * price + transaction.fees
if not _approx_equal(amount, -calculated_amount):
if not _approx_equal_price_rounding(
amount,
quantity,
price,
transaction.fees,
CalcuationType.ACQUISITION,
):
raise CalculatedAmountDiscrepancyError(transaction, -calculated_amount)
amount = -amount

Expand Down Expand Up @@ -265,7 +318,13 @@ def add_disposal(
if price is None:
raise PriceMissingError(transaction)
calculated_amount = quantity * price - transaction.fees
if not _approx_equal(amount, calculated_amount):
if not _approx_equal_price_rounding(
amount,
quantity,
price,
transaction.fees,
CalcuationType.DISPOSAL,
):
raise CalculatedAmountDiscrepancyError(transaction, calculated_amount)
add_to_list(
self.disposal_list,
Expand Down
7 changes: 7 additions & 0 deletions cgt_calc/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,13 @@ class ActionType(Enum):
CASH_MERGER = 16


class CalcuationType(Enum):
"""Calculation type enumeration."""

ACQUISITION = 1
DISPOSAL = 2


@dataclass
class BrokerTransaction:
"""Broken transaction data."""
Expand Down
29 changes: 29 additions & 0 deletions tests/test_data/schwab/expected_output.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
WARNING: No schwab award file provided
INFO: No schwab Equity Award JSON file provided
INFO: No trading212 folder provided
INFO: No mssb folder provided
INFO: No sharesight file provided
INFO: No raw file provided
First pass completed
Final portfolio:
Final balance:
Charles Schwab: 1019826.86 (USD)
Dividends: £0.00
Dividend taxes: £0.00
Interest: £0.00
Disposal proceeds: £248079.51


Second pass completed
Portfolio at the end of 2023/2024 tax year:
For tax year 2023/2024:
Number of disposals: 3
Disposal proceeds: £248079.51
Allowable costs: £232737.33
Capital gain: £15342.18
Capital loss: £0.00
Total capital gain: £15342.18
Taxable capital gain: £9342.18

Generate calculations report
All done!
8 changes: 8 additions & 0 deletions tests/test_data/schwab/schwab_transactions-2023.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
"Date","Action","Symbol","Description","Quantity","Price","Fees & Comm","Amount"
"03/01/2022","MoneyLink Transfer","","Tfr BANK","","","","$1000000.00"
"10/09/2023","Buy","BAR","BAR CORP ROUNDING CASE","300","$135.0075","$1.34","-$40503.61"
"10/24/2023","Sell","BAR","BAR CORP","300","$140.35","$1.34","$42103.66"
"12/01/2023","Buy","FOO","FOO CORP","500","$90.1234","","-$45061.70"
"01/08/2024","Sell","FOO","FOO CORP ROUNDING CASE","500","$100.0001","$1.23","$49998.85"
"02/06/2024","Buy","QUX","QUX CORP ROUNDING CASE","1300","$160.1275","$1.12","-$208166.93"
"02/26/2024","Sell","QUX","QUX CORP ROUNDING CASE","1300","$170.3521","$1.2","$221456.59"
29 changes: 29 additions & 0 deletions tests/test_schwab_2023.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"""Test Schwab."""

from pathlib import Path
import subprocess
import sys


def test_run_with_schwab_example_files() -> None:
"""Runs the script and verifies it doesn't fail."""
cmd = [
sys.executable,
"-m",
"cgt_calc.main",
"--year",
"2023",
"--schwab",
"tests/test_data/schwab/schwab_transactions-2023.csv",
"--no-pdflatex",
]
result = subprocess.run(cmd, check=True, capture_output=True)
assert result.stderr == b"", "Run with example files generated errors"
expected_file = Path("tests") / "test_data" / "schwab" / "expected_output.txt"
expected = expected_file.read_text()
cmd_str = " ".join([param if param else "''" for param in cmd])
assert result.stdout.decode("utf-8") == expected, (
"Run with example files generated unexpected outputs, "
"if you added new features update the test with:\n"
f"{cmd_str} > {expected_file}"
)

0 comments on commit e28dbd2

Please sign in to comment.