Thursday, 30 January 2025

Code

from datetime import datetime, timedelta

from collections import deque



class MutualFundCGTCalculator:

    def __init__(self):

        self.pool_units = 0  # Total units in the pool

        self.pool_cost = 0.0  # Total cost in the pool

        self.same_day_buys = deque()  # Track same-day buys

        self.thirty_day_buys = deque()  # Track 30-day buys

        self.total_gains = 0.0

        self.total_losses = 0.0

        self.total_distributions = 0.0  # Distributions paid out

        self.annual_exempt_amount = 3000  # CGT allowance (2024-25)

        self.transactions = []  # To log all transactions


    def add_buy(self, date, units, price_per_unit, fees=0.0):

        total_cost = (units * price_per_unit) + fees

        self.pool_units += units

        self.pool_cost += total_cost

        self.transactions.append((date, "BUY", units, price_per_unit, fees, total_cost))

        self.same_day_buys.append((date, units, price_per_unit))

        self.thirty_day_buys.append((date, units, price_per_unit))


    def add_distribution(self, date, amount, reinvested=False):

        """

        Handles distributions such as dividends or interest.

        - If reinvested → Increases cost basis.

        - If paid out → Taxable as income but not part of CGT.

        """

        if reinvested:

            self.pool_cost += amount  # Increase cost basis

            print(f"Reinvested distribution of £{amount:.2f} added to cost basis.")

        else:

            self.total_distributions += amount  # Separate taxable income

            print(f"Cash distribution of £{amount:.2f} received (taxable as income).")


    def add_sell(self, date, units, price_per_unit, fees=0.0):

        proceeds = (units * price_per_unit) - fees

        cost = self.match_units(date, units)

        gain_or_loss = proceeds - cost

        if gain_or_loss >= 0:

            self.total_gains += gain_or_loss

        else:

            self.total_losses += abs(gain_or_loss)

        self.transactions.append((date, "SELL", units, price_per_unit, fees, proceeds, cost, gain_or_loss))

        return gain_or_loss


    def match_units(self, sell_date, units):

        cost = 0.0


        # Step 1: Same-Day Rule

        while units > 0 and self.same_day_buys:

            buy_date, buy_units, buy_price = self.same_day_buys[0]

            if buy_date == sell_date:

                matched_units = min(units, buy_units)

                cost += matched_units * buy_price

                units -= matched_units

                if matched_units == buy_units:

                    self.same_day_buys.popleft()

                else:

                    self.same_day_buys[0] = (buy_date, buy_units - matched_units, buy_price)

            else:

                break


        # Step 2: 30-Day Rule

        while units > 0 and self.thirty_day_buys:

            buy_date, buy_units, buy_price = self.thirty_day_buys[0]

            days_difference = (sell_date - buy_date).days

            if 0 < days_difference <= 30:

                matched_units = min(units, buy_units)

                cost += matched_units * buy_price

                units -= matched_units

                if matched_units == buy_units:

                    self.thirty_day_buys.popleft()

                else:

                    self.thirty_day_buys[0] = (buy_date, buy_units - matched_units, buy_price)

            else:

                break


        # Step 3: Section 104 Pool

        if units > 0:

            average_cost_per_unit = self.pool_cost / self.pool_units

            cost += units * average_cost_per_unit

            self.pool_units -= units

            self.pool_cost -= units * average_cost_per_unit


        return cost


    def calculate_taxable_gain(self):

        taxable_gain = max(0, self.total_gains - self.annual_exempt_amount)

        return taxable_gain


    def print_summary(self):

        print("\nTransaction Summary:")

        for t in self.transactions:

            print(t)

        print(f"\nTotal Gains: £{self.total_gains:.2f}")

        print(f"Total Losses: £{self.total_losses:.2f}")

        print(f"Total Distributions (Taxable as income): £{self.total_distributions:.2f}")

        taxable_gain = self.calculate_taxable_gain()

        print(f"Taxable Gains (after allowance): £{taxable_gain:.2f}")


    def reset(self):

        self.pool_units = 0

        self.pool_cost = 0.0

        self.same_day_buys.clear()

        self.thirty_day_buys.clear()

        self.total_gains = 0.0

        self.total_losses = 0.0

        self.total_distributions = 0.0

        self.transactions = []



# Example Usage

if __name__ == "__main__":

    calculator = MutualFundCGTCalculator()


    # Example buys

    calculator.add_buy(datetime(2025, 1, 1), 1000, 10)  # Buy 1000 units @ £10

    calculator.add_buy(datetime(2025, 1, 10), 500, 12, fees=10)  # Buy 500 units @ £12 + £10 fees

    calculator.add_buy(datetime(2025, 2, 15), 1000, 11)  # Buy 1000 units @ £11


    # Distributions

    calculator.add_distribution(datetime(2025, 3, 1), 300, reinvested=True)  # Reinvested distribution

    calculator.add_distribution(datetime(2025, 6, 1), 200, reinvested=False)  # Cash distribution


    # Example sells

    gain1 = calculator.add_sell(datetime(2025, 4, 1), 1200, 15)  # Sell 1200 units @ £15

    print(f"Capital Gain for first sale: £{gain1:.2f}")


    gain2 = calculator.add_sell(datetime(2025, 4, 20), 800, 16, fees=15)  # Sell 800 units @ £16 + £15 fees

    print(f"Capital Gain for second sale: £{gain2:.2f}")


    # Print summary

    calculator.print_summary()