diff --git a/IRRCalculationAfterTax.java b/IRRCalculationAfterTax.java
new file mode 100644
index 0000000000..4b78c8aeda
--- /dev/null
+++ b/IRRCalculationAfterTax.java
@@ -0,0 +1,85 @@
+package name.abuchen.portfolio.snapshot.security;
+
+import name.abuchen.portfolio.model.AccountTransaction;
+import name.abuchen.portfolio.model.PortfolioTransaction;
+import name.abuchen.portfolio.money.CurrencyConverter;
+import name.abuchen.portfolio.money.Values;
+
+/**
+ * Calculates the internal rate of return (IRR) of a security based on the
+ * actual, after-tax cash flows, i.e. unlike {@link IRRCalculation} taxes are
+ * NOT added back to reconstruct a value before taxes:
+ *
+ * - dividends are used at their net (post-withholding) amount
+ * - buys are used at the full amount actually paid (including taxes)
+ * - sells are used at the full amount actually received (after
+ * taxes)
+ * - standalone tax transactions and tax refunds linked to the security
+ * (i.e. not already part of a buy, sell or dividend) are included as real
+ * cash flows instead of being ignored
+ *
+ * Fees continue to be treated exactly as in {@link IRRCalculation}, i.e.
+ * always as real cash flows, because they are never added back in the gross
+ * calculation either.
+ */
+/* package */ class IRRCalculationAfterTax extends IRRCalculation
+{
+ @Override
+ public void visit(CurrencyConverter converter, CalculationLineItem.DividendPayment t)
+ {
+ dates.add(t.getDateTime().toLocalDate());
+
+ long amount = t.getValue().with(converter.at(t.getDateTime())).getAmount();
+
+ values.add(amount / Values.Amount.divider());
+ }
+
+ @Override
+ public void visit(CurrencyConverter converter, CalculationLineItem.TransactionItem item, AccountTransaction t)
+ {
+ switch (t.getType())
+ {
+ case TAXES:
+ // unlike the gross IRR, a standalone tax charge on the
+ // security is a real cash outflow and must be counted
+ dates.add(t.getDateTime().toLocalDate());
+ values.add(-converter.convert(t.getDateTime(), t.getMonetaryAmount()).getAmount()
+ / Values.Amount.divider());
+ break;
+ case TAX_REFUND:
+ dates.add(t.getDateTime().toLocalDate());
+ values.add(converter.convert(t.getDateTime(), t.getMonetaryAmount()).getAmount()
+ / Values.Amount.divider());
+ break;
+ default:
+ // FEES / FEES_REFUND are handled identically to the gross
+ // calculation; all other types are ignored, same as in the
+ // parent class
+ super.visit(converter, item, t);
+ }
+ }
+
+ @Override
+ public void visit(CurrencyConverter converter, CalculationLineItem.TransactionItem item, PortfolioTransaction t)
+ {
+ dates.add(t.getDateTime().toLocalDate());
+
+ long amount = t.getMonetaryAmount(converter).getAmount();
+
+ switch (t.getType())
+ {
+ case BUY:
+ case DELIVERY_INBOUND:
+ case TRANSFER_IN:
+ values.add(-amount / Values.Amount.divider());
+ break;
+ case SELL:
+ case DELIVERY_OUTBOUND:
+ case TRANSFER_OUT:
+ values.add(amount / Values.Amount.divider());
+ break;
+ default:
+ throw new UnsupportedOperationException();
+ }
+ }
+}
diff --git a/IRRCalculationAfterTaxTest.java b/IRRCalculationAfterTaxTest.java
new file mode 100644
index 0000000000..38e6adad83
--- /dev/null
+++ b/IRRCalculationAfterTaxTest.java
@@ -0,0 +1,114 @@
+package name.abuchen.portfolio.snapshot.security;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+import java.time.LocalDateTime;
+import java.time.Month;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.hamcrest.number.IsCloseTo;
+import org.junit.Test;
+
+import name.abuchen.portfolio.junit.TestCurrencyConverter;
+import name.abuchen.portfolio.model.Account;
+import name.abuchen.portfolio.model.AccountTransaction;
+import name.abuchen.portfolio.model.Portfolio;
+import name.abuchen.portfolio.model.PortfolioTransaction;
+import name.abuchen.portfolio.model.Security;
+import name.abuchen.portfolio.model.Transaction.Unit;
+import name.abuchen.portfolio.money.CurrencyUnit;
+import name.abuchen.portfolio.money.Money;
+import name.abuchen.portfolio.money.Values;
+
+@SuppressWarnings("nls")
+public class IRRCalculationAfterTaxTest
+{
+ @Test
+ public void testDividendPaymentsWithTaxes()
+ {
+ // same scenario as IRRCalculationTest#testDividendPaymentsWithTaxes,
+ // but this time the tax withheld on the dividend and the tax paid on
+ // the sale must NOT be added back
+
+ List tx = new ArrayList<>();
+
+ Portfolio portfolio = new Portfolio();
+ Security security = new Security();
+
+ tx.add(CalculationLineItem.of(portfolio,
+ new PortfolioTransaction(LocalDateTime.of(2015, Month.DECEMBER, 31, 0, 0), //
+ CurrencyUnit.EUR, Values.Amount.factorize(1000), //
+ security, Values.Share.factorize(10), PortfolioTransaction.Type.BUY, //
+ Values.Amount.factorize(10), 0)));
+
+ AccountTransaction t = new AccountTransaction();
+ t.setType(AccountTransaction.Type.DIVIDENDS);
+ t.setDateTime(LocalDateTime.parse("2016-06-01T00:00"));
+ t.setSecurity(security);
+ t.setMonetaryAmount(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(100)));
+ t.setShares(Values.Share.factorize(10));
+ t.addUnit(new Unit(Unit.Type.TAX, Money.of(CurrencyUnit.EUR, Values.Amount.factorize(50))));
+ tx.add(CalculationLineItem.of(new Account(), t));
+
+ tx.add(CalculationLineItem.of(portfolio,
+ new PortfolioTransaction(LocalDateTime.of(2016, Month.DECEMBER, 31, 0, 0), //
+ CurrencyUnit.EUR, Values.Amount.factorize(1200), //
+ security, Values.Share.factorize(10), PortfolioTransaction.Type.SELL, //
+ Values.Amount.factorize(10), Values.Amount.factorize(30))));
+
+ IRRCalculationAfterTax calculation = Calculation.perform(IRRCalculationAfterTax.class,
+ new TestCurrencyConverter(), security, tx);
+
+ // Excel verification
+ // 31.12.15 -1000 (unchanged: this buy has no taxes)
+ // 01.06.16 100 (net dividend, tax NOT added back)
+ // 31.12.16 1200 (net sale proceeds, tax NOT added back)
+ // =XINTZINSFUSS(B1:B3;A1:A3) = 0,316409186
+
+ assertThat(calculation.getIRR(), IsCloseTo.closeTo(0.316409186d, 0.000001d));
+ }
+
+ @Test
+ public void testStandaloneTaxTransactionIsIncludedAsCashFlow()
+ {
+ // a standalone TAXES transaction linked to the security (e.g. a tax
+ // adjustment not tied to a specific buy/sell/dividend) must be
+ // counted as a real cash outflow in the after-tax IRR, whereas the
+ // gross IRRCalculation always ignores it
+
+ List tx = new ArrayList<>();
+
+ Portfolio portfolio = new Portfolio();
+ Security security = new Security();
+
+ tx.add(CalculationLineItem.of(portfolio,
+ new PortfolioTransaction(LocalDateTime.of(2020, Month.JANUARY, 1, 0, 0), //
+ CurrencyUnit.EUR, Values.Amount.factorize(1000), //
+ security, Values.Share.factorize(10), PortfolioTransaction.Type.BUY, 0, 0)));
+
+ AccountTransaction standaloneTax = new AccountTransaction();
+ standaloneTax.setType(AccountTransaction.Type.TAXES);
+ standaloneTax.setDateTime(LocalDateTime.of(2020, Month.JULY, 1, 0, 0));
+ standaloneTax.setSecurity(security);
+ standaloneTax.setMonetaryAmount(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(20)));
+ tx.add(CalculationLineItem.of(new Account(), standaloneTax));
+
+ tx.add(CalculationLineItem.of(portfolio,
+ new PortfolioTransaction(LocalDateTime.of(2021, Month.JANUARY, 1, 0, 0), //
+ CurrencyUnit.EUR, Values.Amount.factorize(1100), //
+ security, Values.Share.factorize(10), PortfolioTransaction.Type.SELL, 0, 0)));
+
+ IRRCalculationAfterTax afterTax = Calculation.perform(IRRCalculationAfterTax.class,
+ new TestCurrencyConverter(), security, tx);
+ IRRCalculation gross = Calculation.perform(IRRCalculation.class, new TestCurrencyConverter(), security, tx);
+
+ // the gross IRR ignores the standalone tax charge entirely
+ assertThat(gross.getIRR(), IsCloseTo.closeTo(0.1d, 0.001d));
+
+ // the after-tax IRR must be lower because it counts the 20 EUR tax
+ // charge as a real cash outflow
+ assertTrue(afterTax.getIRR() < gross.getIRR());
+ }
+}
diff --git a/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/Messages.java b/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/Messages.java
index 41da24899c..1bf5db1859 100644
--- a/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/Messages.java
+++ b/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/Messages.java
@@ -229,6 +229,9 @@ public class Messages extends NLS
public static String ColumnInterval;
public static String ColumnIRR;
public static String ColumnIRR_MenuLabel;
+ public static String ColumnIRRAfterTax;
+ public static String ColumnIRRAfterTax_Description;
+ public static String ColumnIRRAfterTax_MenuLabel;
public static String ColumnIRRPerformance;
public static String ColumnIRRPerformanceOption;
public static String ColumnISIN;
diff --git a/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/messages.properties b/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/messages.properties
index e2f6cacefc..2444d64f12 100644
--- a/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/messages.properties
+++ b/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/messages.properties
@@ -500,6 +500,12 @@ ColumnHoldingPeriod = Holding period (days)
ColumnIRR = IRR
+ColumnIRRAfterTax = IRR (after tax)
+
+ColumnIRRAfterTax_Description = Internal rate of return based on the actual after-tax cash flows, i.e. taxes are not added back to reconstruct a gross value.
+
+ColumnIRRAfterTax_MenuLabel = Internal rate of return after taxes
+
ColumnIRRPerformance = IRR Performance
ColumnIRRPerformanceOption = IRR {0}
diff --git a/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/messages_it.properties b/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/messages_it.properties
index 912600c3ef..f7f437fdbe 100644
--- a/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/messages_it.properties
+++ b/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/messages_it.properties
@@ -493,6 +493,12 @@ ColumnHoldingPeriod = Periodo detenzione (gg)
ColumnIRR = IRR
+ColumnIRRAfterTax = IRR (al netto delle imposte)
+
+ColumnIRRAfterTax_Description = Tasso di rendimento interno calcolato sui flussi di cassa realmente al netto delle imposte, senza ricostruire il valore lordo.
+
+ColumnIRRAfterTax_MenuLabel = Tasso di rendimento interno al netto delle imposte
+
ColumnIRRPerformance = Performance IRR
ColumnIRRPerformanceOption = IRR {0}
diff --git a/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/views/SecuritiesPerformanceView.java b/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/views/SecuritiesPerformanceView.java
index 6ef5ce4801..a41fa69261 100644
--- a/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/views/SecuritiesPerformanceView.java
+++ b/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/views/SecuritiesPerformanceView.java
@@ -1309,6 +1309,17 @@ private void addPerformanceColumns()
column.setSorter(ColumnViewerSorter.create(e -> ((LazySecurityPerformanceRecord) e).getIrr()));
recordColumns.addColumn(column);
+ // internal rate of return after taxes
+ column = new Column("izf_after_tax", Messages.ColumnIRRAfterTax, SWT.RIGHT, 80); //$NON-NLS-1$
+ column.setGroupLabel(Messages.GroupLabelPerformance);
+ column.setMenuLabel(Messages.ColumnIRRAfterTax_MenuLabel);
+ column.setDescription(Messages.ColumnIRRAfterTax_Description);
+ column.setLabelProvider(new RowElementLabelProvider(new NumberColorLabelProvider<>(Values.Percent2,
+ r -> ((LazySecurityPerformanceRecord) r).getIrrAfterTax())));
+ column.setSorter(ColumnViewerSorter.create(e -> ((LazySecurityPerformanceRecord) e).getIrrAfterTax()));
+ column.setVisible(false);
+ recordColumns.addColumn(column);
+
column = new Column("capitalgains", Messages.ColumnCapitalGains, SWT.RIGHT, 80); //$NON-NLS-1$
column.setGroupLabel(Messages.GroupLabelPerformance);
column.setDescription(Messages.ColumnCapitalGains_Description);
diff --git a/name.abuchen.portfolio/src/name/abuchen/portfolio/snapshot/security/IRRCalculation.java b/name.abuchen.portfolio/src/name/abuchen/portfolio/snapshot/security/IRRCalculation.java
index f6e7452d61..109a0a0b5f 100644
--- a/name.abuchen.portfolio/src/name/abuchen/portfolio/snapshot/security/IRRCalculation.java
+++ b/name.abuchen.portfolio/src/name/abuchen/portfolio/snapshot/security/IRRCalculation.java
@@ -11,10 +11,21 @@
import name.abuchen.portfolio.money.CurrencyConverter;
import name.abuchen.portfolio.money.Values;
+/**
+ * Calculates the internal rate of return (IRR) of a security based on the
+ * gross cash flows, i.e. taxes withheld on dividends and paid on sales are
+ * added back to reconstruct the value before taxes. Fees, on the other
+ * hand, are always treated as real cash flows.
+ *
+ * @see IRRCalculationAfterTax for the after-tax variant of this calculation
+ */
/* package */class IRRCalculation extends Calculation
{
- private List dates = new ArrayList<>();
- private List values = new ArrayList<>();
+ // package-visible (not private) so that IRRCalculationAfterTax can reuse
+ // the ValuationAtStart/ValuationAtEnd handling and append its own,
+ // after-tax cash flows for dividends, buys/sells and taxes.
+ protected List dates = new ArrayList<>();
+ protected List values = new ArrayList<>();
@Override
public void visit(CurrencyConverter converter, CalculationLineItem.ValuationAtStart t)
diff --git a/name.abuchen.portfolio/src/name/abuchen/portfolio/snapshot/security/LazySecurityPerformanceRecord.java b/name.abuchen.portfolio/src/name/abuchen/portfolio/snapshot/security/LazySecurityPerformanceRecord.java
index a97b865a60..42b3398cba 100644
--- a/name.abuchen.portfolio/src/name/abuchen/portfolio/snapshot/security/LazySecurityPerformanceRecord.java
+++ b/name.abuchen.portfolio/src/name/abuchen/portfolio/snapshot/security/LazySecurityPerformanceRecord.java
@@ -68,6 +68,12 @@ public V get()
private final LazyValue irr = new LazyValue<>(
() -> Calculation.perform(IRRCalculation.class, converter, security, lineItems).getIRR());
+ /**
+ * internal rate of return of security after taxes {@link #calculateIRR()}
+ */
+ private final LazyValue irrAfterTax = new LazyValue<>(() -> Calculation
+ .perform(IRRCalculationAfterTax.class, converter, security, lineItems).getIRR());
+
/**
* weak reference to the performance index, because it can consume a lot of
* memory in particular for large intervals
@@ -183,11 +189,16 @@ private Money getCostMoney(CostMethod costMethod, TaxesAndFees taxesAndFees)
super(client, security, converter, interval);
}
- public Double getIrr()
+ public Double getIrr()
{
return irr.get();
}
+ public Double getIrrAfterTax()
+ {
+ return irrAfterTax.get();
+ }
+
public Double getTrueTimeWeightedRateOfReturn()
{
return twror.get();