diff --git a/pyproject.toml b/pyproject.toml index 796e390..2818f19 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -96,3 +96,7 @@ exclude_also = [ minversion = "6.0" testpaths = ["tests"] addopts = "--cov --no-header" +markers = [ + """network: mark a test that requires an Internet \ + connection (deselect with '-m "not network"')""" +] diff --git a/src/investir/cli.py b/src/investir/cli.py index d167f6c..4abf7d6 100644 --- a/src/investir/cli.py +++ b/src/investir/cli.py @@ -18,6 +18,7 @@ ) from investir.logging import configure_logger from investir.parser import ParserFactory +from investir.prettytable import OutputFormat from investir.taxcalculator import TaxCalculator from investir.transaction import Acquisition, Disposal, Transaction from investir.trhistory import TrHistory @@ -72,6 +73,12 @@ def __init__(self, opt1: str, opt2: str) -> None: ] +OutputFormatOpt = Annotated[ + OutputFormat, + typer.Option("--output", "-o", help="Output format."), +] + + def abort(message: str) -> None: logger.critical(message) raise typer.Exit(code=1) @@ -230,7 +237,7 @@ def main_callback( # noqa: PLR0913 @app.command("orders") -def orders_command( +def orders_command( # noqa: PLR0913 files: FilesArg, tax_year: TaxYearOpt = None, ticker: TickerOpt = None, @@ -240,6 +247,7 @@ def orders_command( disposals_only: Annotated[ bool, typer.Option("--disposals", help="Show only disposals.") ] = False, + format: OutputFormatOpt = OutputFormat.TEXT, ) -> None: """ Show share buy/sell orders. @@ -257,7 +265,7 @@ def orders_command( filters = create_filters(tax_year=tax_year, ticker=ticker, tr_type=tr_type) if table := tr_hist.get_orders_table(filters): - print(table.to_string(leading_nl=config.logging_enabled)) + print(table.to_string(format, leading_nl=config.logging_enabled)) @app.command("dividends") @@ -265,6 +273,7 @@ def dividends_command( files: FilesArg, tax_year: TaxYearOpt = None, ticker: TickerOpt = None, + format: OutputFormatOpt = OutputFormat.TEXT, ) -> None: """ Show share dividends paid out. @@ -272,7 +281,7 @@ def dividends_command( tr_hist, _ = parse(files) filters = create_filters(tax_year=tax_year, ticker=ticker) if table := tr_hist.get_dividends_table(filters): - print(table.to_string(leading_nl=config.logging_enabled)) + print(table.to_string(format, leading_nl=config.logging_enabled)) @app.command("transfers") @@ -285,6 +294,7 @@ def transfers_command( withdrawals_only: Annotated[ bool, typer.Option("--withdrawals", help="Show only withdrawals.") ] = False, + format: OutputFormatOpt = OutputFormat.TEXT, ) -> None: """ Show cash deposits and cash withdrawals. @@ -303,22 +313,26 @@ def transfers_command( filters = create_filters(tax_year=tax_year, amount_op=amount_op) if table := tr_hist.get_transfers_table(filters): - print(table.to_string(leading_nl=config.logging_enabled)) + print(table.to_string(format, leading_nl=config.logging_enabled)) @app.command("interest") -def interest_command(files: FilesArg, tax_year: TaxYearOpt = None) -> None: +def interest_command( + files: FilesArg, + tax_year: TaxYearOpt = None, + format: OutputFormatOpt = OutputFormat.TEXT, +) -> None: """ Show interest earned on cash. """ tr_hist, _ = parse(files) filters = create_filters(tax_year=tax_year) if table := tr_hist.get_interest_table(filters): - print(table.to_string(leading_nl=config.logging_enabled)) + print(table.to_string(format, leading_nl=config.logging_enabled)) @app.command("capital-gains") -def capital_gains_command( +def capital_gains_command( # noqa: PLR0913 files: FilesArg, gains_only: Annotated[ bool, typer.Option("--gains", help="Show only capital gains.") @@ -328,6 +342,7 @@ def capital_gains_command( ] = False, tax_year: TaxYearOpt = None, ticker: TickerOpt = None, + format: OutputFormatOpt = OutputFormat.TEXT, ) -> None: """ Show capital gains report. @@ -335,6 +350,11 @@ def capital_gains_command( if gains_only and losses_only: raise MutuallyExclusiveOption("--gains", "--losses") + if format != OutputFormat.TEXT and tax_year is None: + raise click.exceptions.UsageError( + f"The {format.value} format requires the option --tax-year to be used" + ) + _, tax_calculator = parse(files) tax_year = Year(tax_year) if tax_year else None ticker = Ticker(ticker) if ticker else None @@ -354,10 +374,16 @@ def capital_gains_command( if table: print(end="\n" if tax_year_idx == 0 and config.logging_enabled else "") - print(boldify(f"Capital Gains Tax Report {tax_year_short_date(tax_year)}")) - print(tax_year_full_date(tax_year)) - print(table.to_string(leading_nl=True)) - print(summary) + + if format == OutputFormat.TEXT: + print( + boldify(f"Capital Gains Tax Report {tax_year_short_date(tax_year)}") + ) + print(tax_year_full_date(tax_year)) + print(table.to_string(format)) + print(summary) + else: + print(table.to_string(format, leading_nl=False)) @app.command("holdings") @@ -367,6 +393,7 @@ def holdings_command( show_gain_loss: Annotated[ bool, typer.Option("--show-gain-loss", help="Show unrealised gain/loss.") ] = False, + format: OutputFormatOpt = OutputFormat.TEXT, ) -> None: """ Show current holdings. @@ -374,7 +401,7 @@ def holdings_command( _, tax_calculator = parse(files) ticker = Ticker(ticker) if ticker else None if table := tax_calculator.get_holdings_table(ticker, show_gain_loss): - print(table.to_string(leading_nl=config.logging_enabled)) + print(table.to_string(format, leading_nl=config.logging_enabled)) def main() -> None: diff --git a/src/investir/prettytable.py b/src/investir/prettytable.py index 445529b..3fa1c83 100644 --- a/src/investir/prettytable.py +++ b/src/investir/prettytable.py @@ -1,6 +1,7 @@ -from collections.abc import Callable +from collections.abc import Callable, Sequence, Set from datetime import date from decimal import Decimal +from enum import Enum from typing import Any import prettytable @@ -8,6 +9,13 @@ from investir.utils import boldify, unboldify +class OutputFormat(str, Enum): + TEXT = "text" + CSV = "csv" + JSON = "json" + HTML = "html" + + def date_format(format: str) -> Callable[[str, Any], str]: def _date_format(_field, val) -> str: if isinstance(val, date): @@ -30,13 +38,76 @@ def _decimal_format(_field, val) -> str: class PrettyTable(prettytable.PrettyTable): - def __init__(self, *args, **kwargs) -> None: - kwargs["field_names"] = list(map(lambda f: boldify(f), kwargs["field_names"])) + def __init__( + self, + field_names: Sequence[str], + hidden_fields: Sequence[str] | None = None, + show_total_fields: Sequence[str] | None = None, + **kwargs, + ) -> None: + super().__init__(field_names, **kwargs) + + self.hrules = prettytable.HEADER + self.vrules = prettytable.NONE + self._hidden_fields: Set[str] = frozenset(hidden_fields or []) + self._show_total_fields: Set[str] = frozenset(show_total_fields or []) + + def __bool__(self) -> bool: + return len(self.rows) > 0 + + def to_string(self, format: OutputFormat, leading_nl: bool = True) -> str: + if ( + self.rows + and self._show_total_fields + and format in (OutputFormat.TEXT, OutputFormat.HTML) + ): + self._insert_totals_row() + + self._apply_formatting(bold_titles=format == OutputFormat.TEXT) + + start_nl = "\n" if leading_nl else "" + end_nl = "\n" if format == OutputFormat.TEXT else "" + + fields = [ + f for f in self.field_names if unboldify(f) not in self._hidden_fields + ] + + kwargs: dict[str, Any] = {"fields": fields} + if format == OutputFormat.JSON: + kwargs["default"] = str + + table_str = self.get_formatted_string(format, **kwargs) + + if format == OutputFormat.CSV: + table_str = table_str.rstrip() - super().__init__(*args, **kwargs) + return f"{start_nl}{table_str}{end_nl}" + + def _insert_totals_row(self) -> None: + totals_row = [] + + for i, f in enumerate(self.field_names): + if unboldify(f) in self._show_total_fields: + total = sum( + ( + round(row[i], 2) + for row in self.rows + if row[i] and row[i] != "n/a" + ), + Decimal("0.0"), + ) + totals_row.append(total) + else: + totals_row.append("") + + self.add_row(totals_row) + + def _apply_formatting(self, bold_titles: bool) -> None: + if bold_titles: + self.field_names = list(map(lambda f: boldify(f), self.field_names)) for f in self.field_names: - plain_f = unboldify(f) + plain_f = unboldify(f) if bold_titles else f match plain_f.split()[0].strip(), plain_f.split()[-1]: case ("Date", _) | (_, "Date"): @@ -52,22 +123,3 @@ def __init__(self, *args, **kwargs) -> None: case _: self.align[f] = "l" - - self.hrules = prettytable.HEADER - self.vrules = prettytable.NONE - self.invisible_fields: set[str] = set() - - def hide_field(self, field_name: str) -> None: - self.invisible_fields.add(field_name) - - def to_string(self, leading_nl: bool = True) -> str: - nl = "\n" if leading_nl else "" - - fields = [ - f for f in self.field_names if unboldify(f) not in self.invisible_fields - ] - - return f"{nl}{self.get_string(fields=fields)}\n" - - def __bool__(self) -> bool: - return len(self.rows) > 0 diff --git a/src/investir/taxcalculator.py b/src/investir/taxcalculator.py index cc0a8f6..ea53b56 100644 --- a/src/investir/taxcalculator.py +++ b/src/investir/taxcalculator.py @@ -208,7 +208,7 @@ def get_holdings_table( self._calculate_capital_gains() table = PrettyTable( - field_names=[ + field_names=( "Security Name", "ISIN", "Cost (£)", @@ -216,14 +216,23 @@ def get_holdings_table( "Current Value (£)", "Gain/Loss (£)", "Weight (%)", - ] + ), + hidden_fields=( + "Current Value (£)", + "Gain/Loss (£)", + "Weight (%)", + ) + if not show_gain_loss + else (), + show_total_fields=( + "Current Value (£)", + "Gain/Loss (£)", + "Weight (%)", + ) + if show_gain_loss + else (), ) - if not show_gain_loss: - table.hide_field("Current Value (£)") - table.hide_field("Gain/Loss (£)") - table.hide_field("Weight (%)") - holdings = [] if ticker_filter is None: @@ -250,7 +259,6 @@ def get_holdings_table( ) portfolio_value = sum(val for val in holding2value.values()) - total_gain_loss = Decimal("0.0") last_idx = len(holdings) - 1 for idx, (isin, holding) in enumerate(holdings): @@ -260,7 +268,6 @@ def get_holdings_table( if holding_value := holding2value.get(isin): gain_loss = holding.cost - holding_value weight = holding_value / portfolio_value * 100 - total_gain_loss += gain_loss table.add_row( [ @@ -275,11 +282,6 @@ def get_holdings_table( divider=idx == last_idx, ) - if table.rows and show_gain_loss: - table.add_row( - ["", "", "", "", portfolio_value, total_gain_loss, Decimal("100.0")] - ) - return table def disposal_years(self) -> Sequence[Year]: diff --git a/src/investir/trhistory.py b/src/investir/trhistory.py index d45789c..a796676 100644 --- a/src/investir/trhistory.py +++ b/src/investir/trhistory.py @@ -1,5 +1,4 @@ from collections.abc import Callable, Mapping, Sequence, ValuesView -from decimal import Decimal from typing import NamedTuple, TypeVar from investir.exceptions import AmbiguousTickerError @@ -92,22 +91,24 @@ def get_orders_table( "Quantity", "Price (£)", "Fees (£)", - ) + ), + show_total_fields=( + "Total Cost (£)", + "Net Proceeds (£)", + "Fees (£)", + ), ) transactions = list(multifilter(filters, self._orders)) last_idx = len(transactions) - 1 - total_total_cost = total_net_proceeds = total_fees = Decimal("0.0") for idx, tr in enumerate(transactions): net_proceeds = None total_cost = None if isinstance(tr, Acquisition): total_cost = tr.total_cost - total_total_cost += round(tr.total_cost, 2) else: net_proceeds = tr.net_proceeds - total_net_proceeds += round(tr.net_proceeds, 2) divider = ( idx == last_idx or tr.tax_year() != transactions[idx + 1].tax_year() @@ -128,23 +129,6 @@ def get_orders_table( divider=divider, ) - total_fees += tr.fees - - if table.rows: - table.add_row( - [ - "", - "", - "", - "", - total_total_cost, - total_net_proceeds, - "", - "", - total_fees, - ] - ) - return table def get_dividends_table( @@ -158,17 +142,17 @@ def get_dividends_table( "Ticker", "Net Amount (£)", "Widthheld Amount (£)", - ) + ), + show_total_fields=( + "Net Amount (£)", + "Widthheld Amount (£)", + ), ) transactions = list(multifilter(filters, self._dividends)) last_idx = len(transactions) - 1 - total_paid = total_withheld = Decimal("0.0") for idx, tr in enumerate(transactions): - if tr.withheld is not None: - total_withheld += round(tr.withheld, 2) - divider = ( idx == last_idx or tr.tax_year() != transactions[idx + 1].tax_year() ) @@ -178,31 +162,26 @@ def get_dividends_table( divider=divider, ) - total_paid += tr.amount - - if table.rows: - table.add_row(["", "", "", "", total_paid, total_withheld]) - return table def get_transfers_table( self, filters: Sequence[Callable] | None = None ) -> PrettyTable: - table = PrettyTable(field_names=("Date", "Deposit (£)", "Withdrawal (£)")) + table = PrettyTable( + field_names=("Date", "Deposit (£)", "Withdrawal (£)"), + show_total_fields=("Deposit (£)", "Withdrawal (£)"), + ) transactions = list(multifilter(filters, self._transfers)) last_idx = len(transactions) - 1 - total_deposited = total_withdrew = Decimal("0.0") for idx, tr in enumerate(transactions): if tr.amount > 0: deposited = tr.amount widthdrew = "" - total_deposited += tr.amount else: deposited = "" widthdrew = abs(tr.amount) - total_withdrew += abs(tr.amount) divider = ( idx == last_idx or tr.tax_year() != transactions[idx + 1].tax_year() @@ -210,19 +189,17 @@ def get_transfers_table( table.add_row([tr.date, deposited, widthdrew], divider=divider) - if table.rows: - table.add_row(["", total_deposited, total_withdrew]) - return table def get_interest_table( self, filters: Sequence[Callable] | None = None ) -> PrettyTable: - table = PrettyTable(field_names=("Date", "Amount (£)")) + table = PrettyTable( + field_names=("Date", "Amount (£)"), show_total_fields=("Amount (£)",) + ) transactions = list(multifilter(filters, self._interest)) last_idx = len(transactions) - 1 - total_interest = Decimal("0.0") for idx, tr in enumerate(transactions): divider = ( @@ -231,11 +208,6 @@ def get_interest_table( table.add_row([tr.date, tr.amount], divider=divider) - total_interest += tr.amount - - if table.rows: - table.add_row(["", total_interest]) - return table def _securities_map(self) -> Mapping[ISIN, Security]: diff --git a/tests/test_cli.py b/tests/test_cli.py index bbcf8d2..56436f2 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -109,6 +109,31 @@ def _execute( # Holdings ("holdings", "holdings"), ("holdings --ticker SWKS", "holdings_swks"), + # Output formats + ("orders --output text", "orders"), + ("orders --output csv", "orders_csv"), + ("orders --output json", "orders_json"), + ("orders --output html", "orders_html"), + ("dividends --output text", "dividends"), + ("dividends --output csv", "dividends_csv"), + ("dividends --output json", "dividends_json"), + ("dividends --output html", "dividends_html"), + ("interest --output text", "interest"), + ("interest --output csv", "interest_csv"), + ("interest --output json", "interest_json"), + ("interest --output html", "interest_html"), + ("transfers --output text", "transfers"), + ("transfers --output csv", "transfers_csv"), + ("transfers --output json", "transfers_json"), + ("transfers --output html", "transfers_html"), + ("capital-gains --output text", "capital_gains"), + ("capital-gains --tax-year 2022 --output csv", "capital_gains_csv"), + ("capital-gains --tax-year 2022 --output json", "capital_gains_json"), + ("capital-gains --tax-year 2022 --output html", "capital_gains_html"), + ("holdings --output text", "holdings"), + ("holdings --output csv", "holdings_csv"), + ("holdings --output json", "holdings_json"), + ("holdings --output html", "holdings_html"), ] @@ -124,7 +149,10 @@ def test_cli_output(request, execute, cmd, expected): assert result.exit_code == EX_OK assert expected_output.exists() - assert result.stdout == expected_output.read_text(encoding="utf-8") + assert ( + result.stdout.splitlines() + == expected_output.read_text(encoding="utf-8").splitlines() + ) def test_capital_gains_multiple_input_files(execute): @@ -210,6 +238,13 @@ def test_capital_gains_command_mutually_exclusive_filters(execute): assert result.exit_code != EX_OK +def test_capital_gains_command_tax_year_required(execute): + result = execute(["capital-gains", "--output", "json", DATA_FILE]) + assert not result.stdout + assert "Usage:" in result.stderr + assert result.exit_code != EX_OK + + def test_invocation_without_any_argument(execute): result = execute([], global_opts=[]) assert not result.stderr @@ -262,6 +297,7 @@ def test_default_verbosity(execute): assert result.exit_code == EX_OK +@pytest.mark.network @pytest.mark.skipif( sys.version_info < (3, 13), reason="Skipping test to avoid hitting API limits for Yahoo Finance", @@ -282,6 +318,7 @@ def test_capital_gains_with_splits_downloaded_from_internet(execute): assert result.exit_code == EX_OK +@pytest.mark.network @pytest.mark.skipif( sys.version_info < (3, 13), reason="Skipping test to avoid hitting API limits for Yahoo Finance", diff --git a/tests/test_cli/capital_gains_csv b/tests/test_cli/capital_gains_csv new file mode 100644 index 0000000..9e0d5b5 --- /dev/null +++ b/tests/test_cli/capital_gains_csv @@ -0,0 +1,5 @@ +Disposal Date,Identification,Security Name,ISIN,Quantity,Cost (£),Proceeds (£),Gain/loss (£) +2022-09-20,Section 104,Amazon,US0231351067,48.31896981,3493.12,4941.53,1448.41 +2022-10-14,Section 104,Microsoft,US5949181045,1.32642,324.7926690016643403072609590,319.76,-5.0326690016643403072609590 +2022-12-16,Section 104,Skyworks,US83088M1027,8.30000000,1094.155192117222311059989321,979.69,-114.465192117222311059989321 +2023-03-03,Section 104,Skyworks,US83088M1027,2.10000000,277.2021570417068497862623581,312.95,35.7478429582931502137376419 diff --git a/tests/test_cli/capital_gains_html b/tests/test_cli/capital_gains_html new file mode 100644 index 0000000..8fd1ea0 --- /dev/null +++ b/tests/test_cli/capital_gains_html @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Disposal DateIdentificationSecurity NameISINQuantityCost (£)Proceeds (£)Gain/loss (£)
20/09/2022Section 104AmazonUS023135106748.318969813493.124941.531448.41
14/10/2022Section 104MicrosoftUS59491810451.32642000324.79319.76-5.03
16/12/2022Section 104SkyworksUS83088M10278.300000001094.16979.69-114.47
03/03/2023Section 104SkyworksUS83088M10272.10000000277.20312.9535.75
diff --git a/tests/test_cli/capital_gains_json b/tests/test_cli/capital_gains_json new file mode 100644 index 0000000..686eaae --- /dev/null +++ b/tests/test_cli/capital_gains_json @@ -0,0 +1,52 @@ +[ + [ + "Disposal Date", + "Identification", + "Security Name", + "ISIN", + "Quantity", + "Cost (\u00a3)", + "Proceeds (\u00a3)", + "Gain/loss (\u00a3)" + ], + { + "Cost (\u00a3)": "3493.12", + "Disposal Date": "2022-09-20", + "Gain/loss (\u00a3)": "1448.41", + "ISIN": "US0231351067", + "Identification": "Section 104", + "Proceeds (\u00a3)": "4941.53", + "Quantity": "48.31896981", + "Security Name": "Amazon" + }, + { + "Cost (\u00a3)": "324.7926690016643403072609590", + "Disposal Date": "2022-10-14", + "Gain/loss (\u00a3)": "-5.0326690016643403072609590", + "ISIN": "US5949181045", + "Identification": "Section 104", + "Proceeds (\u00a3)": "319.76", + "Quantity": "1.32642", + "Security Name": "Microsoft" + }, + { + "Cost (\u00a3)": "1094.155192117222311059989321", + "Disposal Date": "2022-12-16", + "Gain/loss (\u00a3)": "-114.465192117222311059989321", + "ISIN": "US83088M1027", + "Identification": "Section 104", + "Proceeds (\u00a3)": "979.69", + "Quantity": "8.30000000", + "Security Name": "Skyworks" + }, + { + "Cost (\u00a3)": "277.2021570417068497862623581", + "Disposal Date": "2023-03-03", + "Gain/loss (\u00a3)": "35.7478429582931502137376419", + "ISIN": "US83088M1027", + "Identification": "Section 104", + "Proceeds (\u00a3)": "312.95", + "Quantity": "2.10000000", + "Security Name": "Skyworks" + } +] diff --git a/tests/test_cli/dividends_csv b/tests/test_cli/dividends_csv new file mode 100644 index 0000000..6c16e18 --- /dev/null +++ b/tests/test_cli/dividends_csv @@ -0,0 +1,6 @@ +Date,Security Name,ISIN,Ticker,Net Amount (£),Widthheld Amount (£) +2022-02-12,Apple,US0378331005,AAPL,1.37,0.2391180000 +2022-03-09,Microsoft,US5949181045,MSFT,3.44,0.6073200000 +2022-03-15,Skyworks,US83088M1027,SWKS,2.50,0.4430620000 +2022-05-13,Microsoft,US5949181045,MSFT,4.12,0.7230000000 +2022-06-08,Apple,US0378331005,AAPL,1.64,0.2825940000 diff --git a/tests/test_cli/dividends_html b/tests/test_cli/dividends_html new file mode 100644 index 0000000..ce7eedd --- /dev/null +++ b/tests/test_cli/dividends_html @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
DateSecurity NameISINTickerNet Amount (£)Widthheld Amount (£)
12/02/2022AppleUS0378331005AAPL1.370.24
09/03/2022MicrosoftUS5949181045MSFT3.440.61
15/03/2022SkyworksUS83088M1027SWKS2.500.44
13/05/2022MicrosoftUS5949181045MSFT4.120.72
08/06/2022AppleUS0378331005AAPL1.640.28
13.072.29
diff --git a/tests/test_cli/dividends_json b/tests/test_cli/dividends_json new file mode 100644 index 0000000..1e5a0cb --- /dev/null +++ b/tests/test_cli/dividends_json @@ -0,0 +1,50 @@ +[ + [ + "Date", + "Security Name", + "ISIN", + "Ticker", + "Net Amount (\u00a3)", + "Widthheld Amount (\u00a3)" + ], + { + "Date": "2022-02-12", + "ISIN": "US0378331005", + "Net Amount (\u00a3)": "1.37", + "Security Name": "Apple", + "Ticker": "AAPL", + "Widthheld Amount (\u00a3)": "0.2391180000" + }, + { + "Date": "2022-03-09", + "ISIN": "US5949181045", + "Net Amount (\u00a3)": "3.44", + "Security Name": "Microsoft", + "Ticker": "MSFT", + "Widthheld Amount (\u00a3)": "0.6073200000" + }, + { + "Date": "2022-03-15", + "ISIN": "US83088M1027", + "Net Amount (\u00a3)": "2.50", + "Security Name": "Skyworks", + "Ticker": "SWKS", + "Widthheld Amount (\u00a3)": "0.4430620000" + }, + { + "Date": "2022-05-13", + "ISIN": "US5949181045", + "Net Amount (\u00a3)": "4.12", + "Security Name": "Microsoft", + "Ticker": "MSFT", + "Widthheld Amount (\u00a3)": "0.7230000000" + }, + { + "Date": "2022-06-08", + "ISIN": "US0378331005", + "Net Amount (\u00a3)": "1.64", + "Security Name": "Apple", + "Ticker": "AAPL", + "Widthheld Amount (\u00a3)": "0.2825940000" + } +] diff --git a/tests/test_cli/holdings_csv b/tests/test_cli/holdings_csv new file mode 100644 index 0000000..f68eee7 --- /dev/null +++ b/tests/test_cli/holdings_csv @@ -0,0 +1,6 @@ +Security Name,ISIN,Cost (£),Quantity,Current Value (£),Gain/Loss (£),Weight (%) +SMT,GB00BLDYK618,2196.812045454545454545454546,162.00000000,,n/a,n/a +Microsoft,US5949181045,882.3464985164979278272490710,3.62439184,,n/a,n/a +Apple,US0378331005,301.699341785252128650422842,3.00000000,,n/a,n/a +Skyworks,US83088M1027,301.0770951964634031790966809,2.295943596,,n/a,n/a +PayPal,US70450Y1038,249.59,4.13171759,,n/a,n/a diff --git a/tests/test_cli/holdings_html b/tests/test_cli/holdings_html new file mode 100644 index 0000000..10c5123 --- /dev/null +++ b/tests/test_cli/holdings_html @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Security NameISINCost (£)Quantity
SMTGB00BLDYK6182196.81162.00000000
MicrosoftUS5949181045882.353.62439184
AppleUS0378331005301.703.00000000
SkyworksUS83088M1027301.082.29594360
PayPalUS70450Y1038249.594.13171759
diff --git a/tests/test_cli/holdings_json b/tests/test_cli/holdings_json new file mode 100644 index 0000000..daa3368 --- /dev/null +++ b/tests/test_cli/holdings_json @@ -0,0 +1,56 @@ +[ + [ + "Security Name", + "ISIN", + "Cost (\u00a3)", + "Quantity", + "Current Value (\u00a3)", + "Gain/Loss (\u00a3)", + "Weight (%)" + ], + { + "Cost (\u00a3)": "2196.812045454545454545454546", + "Current Value (\u00a3)": null, + "Gain/Loss (\u00a3)": "n/a", + "ISIN": "GB00BLDYK618", + "Quantity": "162.00000000", + "Security Name": "SMT", + "Weight (%)": "n/a" + }, + { + "Cost (\u00a3)": "882.3464985164979278272490710", + "Current Value (\u00a3)": null, + "Gain/Loss (\u00a3)": "n/a", + "ISIN": "US5949181045", + "Quantity": "3.62439184", + "Security Name": "Microsoft", + "Weight (%)": "n/a" + }, + { + "Cost (\u00a3)": "301.699341785252128650422842", + "Current Value (\u00a3)": null, + "Gain/Loss (\u00a3)": "n/a", + "ISIN": "US0378331005", + "Quantity": "3.00000000", + "Security Name": "Apple", + "Weight (%)": "n/a" + }, + { + "Cost (\u00a3)": "301.0770951964634031790966809", + "Current Value (\u00a3)": null, + "Gain/Loss (\u00a3)": "n/a", + "ISIN": "US83088M1027", + "Quantity": "2.295943596", + "Security Name": "Skyworks", + "Weight (%)": "n/a" + }, + { + "Cost (\u00a3)": "249.59", + "Current Value (\u00a3)": null, + "Gain/Loss (\u00a3)": "n/a", + "ISIN": "US70450Y1038", + "Quantity": "4.13171759", + "Security Name": "PayPal", + "Weight (%)": "n/a" + } +] diff --git a/tests/test_cli/interest_csv b/tests/test_cli/interest_csv new file mode 100644 index 0000000..463fa83 --- /dev/null +++ b/tests/test_cli/interest_csv @@ -0,0 +1,6 @@ +Date,Amount (£) +2021-10-15,1.70 +2022-01-15,1.59 +2022-04-15,1.61 +2022-07-15,1.58 +2023-01-15,1.81 diff --git a/tests/test_cli/interest_html b/tests/test_cli/interest_html new file mode 100644 index 0000000..d162844 --- /dev/null +++ b/tests/test_cli/interest_html @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
DateAmount (£)
15/10/20211.70
15/01/20221.59
15/04/20221.61
15/07/20221.58
15/01/20231.81
8.29
diff --git a/tests/test_cli/interest_json b/tests/test_cli/interest_json new file mode 100644 index 0000000..152222a --- /dev/null +++ b/tests/test_cli/interest_json @@ -0,0 +1,26 @@ +[ + [ + "Date", + "Amount (\u00a3)" + ], + { + "Amount (\u00a3)": "1.70", + "Date": "2021-10-15" + }, + { + "Amount (\u00a3)": "1.59", + "Date": "2022-01-15" + }, + { + "Amount (\u00a3)": "1.61", + "Date": "2022-04-15" + }, + { + "Amount (\u00a3)": "1.58", + "Date": "2022-07-15" + }, + { + "Amount (\u00a3)": "1.81", + "Date": "2023-01-15" + } +] diff --git a/tests/test_cli/orders_csv b/tests/test_cli/orders_csv new file mode 100644 index 0000000..687d432 --- /dev/null +++ b/tests/test_cli/orders_csv @@ -0,0 +1,21 @@ +Date,Security Name,ISIN,Ticker,Total Cost (£),Net Proceeds (£),Quantity,Price (£),Fees (£) +2021-04-07,Apple,US0378331005,AAPL,1940.99,,20.13713692,96.15219917767733984300683794,4.76 +2021-06-05,Apple,US0378331005,AAPL,998.48,,9.09199546,109.3280352341926928326908777,4.47 +2021-06-11,Microsoft,US5949181045,MSFT,2715.44,,15.00246544,180.1863840854146970126264793,12.20 +2021-08-12,Skyworks,US83088M1027,SWKS,1497.76,,11.21263989,132.9793888529135666373389612,6.71 +2021-08-25,Microsoft,US5949181045,MSFT,,2939.35,13.00246544,227.0707823546424284900848773,13.13 +2021-08-26,SMT,GB00BLDYK618,SMT,2386.66,,176.00000000,13.49295454545454545454545455,11.90 +2021-09-01,Skyworks,US83088M1027,SWKS,,152.47,1.245848877,122.9763921037751996946255625,0.74 +2021-09-13,SMT,GB00BLDYK618,SMT,,1228.97,88.00000000,13.96556818181818181818181818,0.00 +2021-09-13,SMT,GB00BLDYK618,SMT,443.65,,34.00000000,13.02558823529411764705882353,0.78 +2021-09-26,SMT,GB00BLDYK618,SMT,482.67,,40.00000000,12.05275,0.56 +2021-11-02,Apple,US0378331005,AAPL,,3643.81,26.22913238,139.2859644410395857706979174,9.54 +2022-01-27,Skyworks,US83088M1027,SWKS,,177.34,1.245848877,142.9788181283563495960032077,0.79 +2022-04-10,PayPal,US70450Y1038,PYPL,249.59,,4.13171759,60.05492742305264866856497808,1.46 +2022-05-12,Microsoft,US5949181045,MSFT,843.26,,2.95081184,284.0946984949064051471340172,4.95 +2022-07-09,Amazon,US0231351067,AMZN,3464.02,,48.31896981,71.26931748630341918293476961,20.36 +2022-09-20,Amazon,US0231351067,AMZN,,4912.43,48.31896981,102.2689436349967578085663660,29.10 +2022-09-28,Skyworks,US83088M1027,SWKS,499.95,,3.97500146,125.0339163397439355909066760,2.94 +2022-10-14,Microsoft,US5949181045,MSFT,,317.88,1.32642,241.0699476787141327784562959,1.88 +2022-12-16,Skyworks,US83088M1027,SWKS,,973.95,8.30000000,118.0349397590361445783132530,5.74 +2023-03-03,Skyworks,US83088M1027,SWKS,,311.13,2.10000000,149.0238095238095238095238095,1.82 diff --git a/tests/test_cli/orders_html b/tests/test_cli/orders_html new file mode 100644 index 0000000..1f60656 --- /dev/null +++ b/tests/test_cli/orders_html @@ -0,0 +1,248 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
DateSecurity NameISINTickerTotal Cost (£)Net Proceeds (£)QuantityPrice (£)Fees (£)
07/04/2021AppleUS0378331005AAPL1940.9920.1371369296.154.76
05/06/2021AppleUS0378331005AAPL998.489.09199546109.334.47
11/06/2021MicrosoftUS5949181045MSFT2715.4415.00246544180.1912.20
12/08/2021SkyworksUS83088M1027SWKS1497.7611.21263989132.986.71
25/08/2021MicrosoftUS5949181045MSFT2939.3513.00246544227.0713.13
26/08/2021SMTGB00BLDYK618SMT2386.66176.0000000013.4911.90
01/09/2021SkyworksUS83088M1027SWKS152.471.24584888122.980.74
13/09/2021SMTGB00BLDYK618SMT1228.9788.0000000013.970.00
13/09/2021SMTGB00BLDYK618SMT443.6534.0000000013.030.78
26/09/2021SMTGB00BLDYK618SMT482.6740.0000000012.050.56
02/11/2021AppleUS0378331005AAPL3643.8126.22913238139.299.54
27/01/2022SkyworksUS83088M1027SWKS177.341.24584888142.980.79
10/04/2022PayPalUS70450Y1038PYPL249.594.1317175960.051.46
12/05/2022MicrosoftUS5949181045MSFT843.262.95081184284.094.95
09/07/2022AmazonUS0231351067AMZN3464.0248.3189698171.2720.36
20/09/2022AmazonUS0231351067AMZN4912.4348.31896981102.2729.10
28/09/2022SkyworksUS83088M1027SWKS499.953.97500146125.032.94
14/10/2022MicrosoftUS5949181045MSFT317.881.32642000241.071.88
16/12/2022SkyworksUS83088M1027SWKS973.958.30000000118.035.74
03/03/2023SkyworksUS83088M1027SWKS311.132.10000000149.021.82
15522.4714657.33133.83
diff --git a/tests/test_cli/orders_json b/tests/test_cli/orders_json new file mode 100644 index 0000000..52f462b --- /dev/null +++ b/tests/test_cli/orders_json @@ -0,0 +1,233 @@ +[ + [ + "Date", + "Security Name", + "ISIN", + "Ticker", + "Total Cost (\u00a3)", + "Net Proceeds (\u00a3)", + "Quantity", + "Price (\u00a3)", + "Fees (\u00a3)" + ], + { + "Date": "2021-04-07", + "Fees (\u00a3)": "4.76", + "ISIN": "US0378331005", + "Net Proceeds (\u00a3)": null, + "Price (\u00a3)": "96.15219917767733984300683794", + "Quantity": "20.13713692", + "Security Name": "Apple", + "Ticker": "AAPL", + "Total Cost (\u00a3)": "1940.99" + }, + { + "Date": "2021-06-05", + "Fees (\u00a3)": "4.47", + "ISIN": "US0378331005", + "Net Proceeds (\u00a3)": null, + "Price (\u00a3)": "109.3280352341926928326908777", + "Quantity": "9.09199546", + "Security Name": "Apple", + "Ticker": "AAPL", + "Total Cost (\u00a3)": "998.48" + }, + { + "Date": "2021-06-11", + "Fees (\u00a3)": "12.20", + "ISIN": "US5949181045", + "Net Proceeds (\u00a3)": null, + "Price (\u00a3)": "180.1863840854146970126264793", + "Quantity": "15.00246544", + "Security Name": "Microsoft", + "Ticker": "MSFT", + "Total Cost (\u00a3)": "2715.44" + }, + { + "Date": "2021-08-12", + "Fees (\u00a3)": "6.71", + "ISIN": "US83088M1027", + "Net Proceeds (\u00a3)": null, + "Price (\u00a3)": "132.9793888529135666373389612", + "Quantity": "11.21263989", + "Security Name": "Skyworks", + "Ticker": "SWKS", + "Total Cost (\u00a3)": "1497.76" + }, + { + "Date": "2021-08-25", + "Fees (\u00a3)": "13.13", + "ISIN": "US5949181045", + "Net Proceeds (\u00a3)": "2939.35", + "Price (\u00a3)": "227.0707823546424284900848773", + "Quantity": "13.00246544", + "Security Name": "Microsoft", + "Ticker": "MSFT", + "Total Cost (\u00a3)": null + }, + { + "Date": "2021-08-26", + "Fees (\u00a3)": "11.90", + "ISIN": "GB00BLDYK618", + "Net Proceeds (\u00a3)": null, + "Price (\u00a3)": "13.49295454545454545454545455", + "Quantity": "176.00000000", + "Security Name": "SMT", + "Ticker": "SMT", + "Total Cost (\u00a3)": "2386.66" + }, + { + "Date": "2021-09-01", + "Fees (\u00a3)": "0.74", + "ISIN": "US83088M1027", + "Net Proceeds (\u00a3)": "152.47", + "Price (\u00a3)": "122.9763921037751996946255625", + "Quantity": "1.245848877", + "Security Name": "Skyworks", + "Ticker": "SWKS", + "Total Cost (\u00a3)": null + }, + { + "Date": "2021-09-13", + "Fees (\u00a3)": "0.00", + "ISIN": "GB00BLDYK618", + "Net Proceeds (\u00a3)": "1228.97", + "Price (\u00a3)": "13.96556818181818181818181818", + "Quantity": "88.00000000", + "Security Name": "SMT", + "Ticker": "SMT", + "Total Cost (\u00a3)": null + }, + { + "Date": "2021-09-13", + "Fees (\u00a3)": "0.78", + "ISIN": "GB00BLDYK618", + "Net Proceeds (\u00a3)": null, + "Price (\u00a3)": "13.02558823529411764705882353", + "Quantity": "34.00000000", + "Security Name": "SMT", + "Ticker": "SMT", + "Total Cost (\u00a3)": "443.65" + }, + { + "Date": "2021-09-26", + "Fees (\u00a3)": "0.56", + "ISIN": "GB00BLDYK618", + "Net Proceeds (\u00a3)": null, + "Price (\u00a3)": "12.05275", + "Quantity": "40.00000000", + "Security Name": "SMT", + "Ticker": "SMT", + "Total Cost (\u00a3)": "482.67" + }, + { + "Date": "2021-11-02", + "Fees (\u00a3)": "9.54", + "ISIN": "US0378331005", + "Net Proceeds (\u00a3)": "3643.81", + "Price (\u00a3)": "139.2859644410395857706979174", + "Quantity": "26.22913238", + "Security Name": "Apple", + "Ticker": "AAPL", + "Total Cost (\u00a3)": null + }, + { + "Date": "2022-01-27", + "Fees (\u00a3)": "0.79", + "ISIN": "US83088M1027", + "Net Proceeds (\u00a3)": "177.34", + "Price (\u00a3)": "142.9788181283563495960032077", + "Quantity": "1.245848877", + "Security Name": "Skyworks", + "Ticker": "SWKS", + "Total Cost (\u00a3)": null + }, + { + "Date": "2022-04-10", + "Fees (\u00a3)": "1.46", + "ISIN": "US70450Y1038", + "Net Proceeds (\u00a3)": null, + "Price (\u00a3)": "60.05492742305264866856497808", + "Quantity": "4.13171759", + "Security Name": "PayPal", + "Ticker": "PYPL", + "Total Cost (\u00a3)": "249.59" + }, + { + "Date": "2022-05-12", + "Fees (\u00a3)": "4.95", + "ISIN": "US5949181045", + "Net Proceeds (\u00a3)": null, + "Price (\u00a3)": "284.0946984949064051471340172", + "Quantity": "2.95081184", + "Security Name": "Microsoft", + "Ticker": "MSFT", + "Total Cost (\u00a3)": "843.26" + }, + { + "Date": "2022-07-09", + "Fees (\u00a3)": "20.36", + "ISIN": "US0231351067", + "Net Proceeds (\u00a3)": null, + "Price (\u00a3)": "71.26931748630341918293476961", + "Quantity": "48.31896981", + "Security Name": "Amazon", + "Ticker": "AMZN", + "Total Cost (\u00a3)": "3464.02" + }, + { + "Date": "2022-09-20", + "Fees (\u00a3)": "29.10", + "ISIN": "US0231351067", + "Net Proceeds (\u00a3)": "4912.43", + "Price (\u00a3)": "102.2689436349967578085663660", + "Quantity": "48.31896981", + "Security Name": "Amazon", + "Ticker": "AMZN", + "Total Cost (\u00a3)": null + }, + { + "Date": "2022-09-28", + "Fees (\u00a3)": "2.94", + "ISIN": "US83088M1027", + "Net Proceeds (\u00a3)": null, + "Price (\u00a3)": "125.0339163397439355909066760", + "Quantity": "3.97500146", + "Security Name": "Skyworks", + "Ticker": "SWKS", + "Total Cost (\u00a3)": "499.95" + }, + { + "Date": "2022-10-14", + "Fees (\u00a3)": "1.88", + "ISIN": "US5949181045", + "Net Proceeds (\u00a3)": "317.88", + "Price (\u00a3)": "241.0699476787141327784562959", + "Quantity": "1.32642", + "Security Name": "Microsoft", + "Ticker": "MSFT", + "Total Cost (\u00a3)": null + }, + { + "Date": "2022-12-16", + "Fees (\u00a3)": "5.74", + "ISIN": "US83088M1027", + "Net Proceeds (\u00a3)": "973.95", + "Price (\u00a3)": "118.0349397590361445783132530", + "Quantity": "8.30000000", + "Security Name": "Skyworks", + "Ticker": "SWKS", + "Total Cost (\u00a3)": null + }, + { + "Date": "2023-03-03", + "Fees (\u00a3)": "1.82", + "ISIN": "US83088M1027", + "Net Proceeds (\u00a3)": "311.13", + "Price (\u00a3)": "149.0238095238095238095238095", + "Quantity": "2.10000000", + "Security Name": "Skyworks", + "Ticker": "SWKS", + "Total Cost (\u00a3)": null + } +] diff --git a/tests/test_cli/transfers_csv b/tests/test_cli/transfers_csv new file mode 100644 index 0000000..f394430 --- /dev/null +++ b/tests/test_cli/transfers_csv @@ -0,0 +1,6 @@ +Date,Deposit (£),Withdrawal (£) +2021-03-02,6000.00, +2021-07-28,4000.00, +2022-02-24,,400.00 +2022-04-08,,100.00 +2023-01-03,2000.00, diff --git a/tests/test_cli/transfers_html b/tests/test_cli/transfers_html new file mode 100644 index 0000000..6635d11 --- /dev/null +++ b/tests/test_cli/transfers_html @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
DateDeposit (£)Withdrawal (£)
02/03/20216000.00
28/07/20214000.00
24/02/2022400.00
08/04/2022100.00
03/01/20232000.00
12000.00500.00
diff --git a/tests/test_cli/transfers_json b/tests/test_cli/transfers_json new file mode 100644 index 0000000..95603de --- /dev/null +++ b/tests/test_cli/transfers_json @@ -0,0 +1,32 @@ +[ + [ + "Date", + "Deposit (\u00a3)", + "Withdrawal (\u00a3)" + ], + { + "Date": "2021-03-02", + "Deposit (\u00a3)": "6000.00", + "Withdrawal (\u00a3)": "" + }, + { + "Date": "2021-07-28", + "Deposit (\u00a3)": "4000.00", + "Withdrawal (\u00a3)": "" + }, + { + "Date": "2022-02-24", + "Deposit (\u00a3)": "", + "Withdrawal (\u00a3)": "400.00" + }, + { + "Date": "2022-04-08", + "Deposit (\u00a3)": "", + "Withdrawal (\u00a3)": "100.00" + }, + { + "Date": "2023-01-03", + "Deposit (\u00a3)": "2000.00", + "Withdrawal (\u00a3)": "" + } +]