diff --git a/.gitignore b/.gitignore index 7cbaf96540..a0207bbe20 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,9 @@ workspace .metadata/* .recommenders/* .DS_Store +**/.classpath +**/.metadata/** +.classpath ### IntelliJ IDEA ### .idea diff --git a/name.abuchen.portfolio.junit/src/name/abuchen/portfolio/junit/AccountBuilder.java b/name.abuchen.portfolio.junit/src/name/abuchen/portfolio/junit/AccountBuilder.java index 302436c6e8..63a315a3c9 100644 --- a/name.abuchen.portfolio.junit/src/name/abuchen/portfolio/junit/AccountBuilder.java +++ b/name.abuchen.portfolio.junit/src/name/abuchen/portfolio/junit/AccountBuilder.java @@ -16,6 +16,11 @@ import name.abuchen.portfolio.money.CurrencyUnit; import name.abuchen.portfolio.money.Money; +/** + * @deprecated this class is technical debt; please use + * {@link name.abuchen.portfolio.model.AccountTransaction.AccountTransactionBuilder} + */ +@Deprecated public class AccountBuilder { private Account account; diff --git a/name.abuchen.portfolio.junit/src/name/abuchen/portfolio/junit/TestCurrencyConverter.java b/name.abuchen.portfolio.junit/src/name/abuchen/portfolio/junit/TestCurrencyConverter.java index 123eb167a3..fbe822a963 100644 --- a/name.abuchen.portfolio.junit/src/name/abuchen/portfolio/junit/TestCurrencyConverter.java +++ b/name.abuchen.portfolio.junit/src/name/abuchen/portfolio/junit/TestCurrencyConverter.java @@ -15,6 +15,8 @@ public class TestCurrencyConverter implements CurrencyConverter { private static ExchangeRateTimeSeriesImpl EUR_USD = null; // NOSONAR private static InverseExchangeRateTimeSeries USD_EUR = null; // NOSONAR + private static ExchangeRateTimeSeriesImpl EUR_CHF = null; // NOSONAR + private static InverseExchangeRateTimeSeries CHF_EUR = null; // NOSONAR static { @@ -35,6 +37,25 @@ public class TestCurrencyConverter implements CurrencyConverter EUR_USD.addRate(new ExchangeRate(LocalDate.parse("2015-01-16"), BigDecimal.valueOf(1.1588).setScale(10))); USD_EUR = new InverseExchangeRateTimeSeries(EUR_USD); + + EUR_CHF = new ExchangeRateTimeSeriesImpl(null, CurrencyUnit.EUR, "CHF"); //$NON-NLS-1$ + EUR_CHF.addRate(new ExchangeRate(LocalDate.parse("2014-12-31"), new BigDecimal("1.2024"))); + EUR_CHF.addRate(new ExchangeRate(LocalDate.parse("2015-01-02"), new BigDecimal("1.2022"))); + + EUR_CHF.addRate(new ExchangeRate(LocalDate.parse("2015-01-05"), new BigDecimal("1.2016"))); + EUR_CHF.addRate(new ExchangeRate(LocalDate.parse("2015-01-06"), new BigDecimal("1.2014"))); + EUR_CHF.addRate(new ExchangeRate(LocalDate.parse("2015-01-07"), new BigDecimal("1.2011"))); + EUR_CHF.addRate(new ExchangeRate(LocalDate.parse("2015-01-08"), new BigDecimal("1.2010"))); + EUR_CHF.addRate(new ExchangeRate(LocalDate.parse("2015-01-09"), new BigDecimal("1.2010"))); + + EUR_CHF.addRate(new ExchangeRate(LocalDate.parse("2015-01-12"), new BigDecimal("1.2010"))); + EUR_CHF.addRate(new ExchangeRate(LocalDate.parse("2015-01-13"), new BigDecimal("1.2010"))); + EUR_CHF.addRate(new ExchangeRate(LocalDate.parse("2015-01-14"), new BigDecimal("1.2010"))); + // 'Francogeddon': the SNB abandons its EUR/CHF cap on 2015-01-15 + EUR_CHF.addRate(new ExchangeRate(LocalDate.parse("2015-01-15"), new BigDecimal("1.0280"))); + EUR_CHF.addRate(new ExchangeRate(LocalDate.parse("2015-01-16"), new BigDecimal("1.0128"))); + + CHF_EUR = new InverseExchangeRateTimeSeries(EUR_CHF); } private final String termCurrency; @@ -66,6 +87,10 @@ public ExchangeRate getRate(LocalDate date, String currencyCode) series = USD_EUR; else if (currencyCode.equals("EUR") && termCurrency.equals("USD")) series = EUR_USD; + else if (currencyCode.equals("CHF") && termCurrency.equals("EUR")) + series = CHF_EUR; + else if (currencyCode.equals("EUR") && termCurrency.equals("CHF")) + series = EUR_CHF; else // testing: any other currency will be converted 1:1 return new ExchangeRate(date, BigDecimal.ONE); @@ -80,7 +105,8 @@ public CurrencyConverter with(String currencyCode) return this; if (currencyCode.equals(CurrencyUnit.EUR) - || currencyCode.equals(CurrencyUnit.USD)) + || currencyCode.equals(CurrencyUnit.USD) + || currencyCode.equals("CHF")) return new TestCurrencyConverter(currencyCode); return null; diff --git a/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/snapshot/security/DividendCalculationTest.java b/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/snapshot/security/DividendCalculationTest.java index f3031095cd..9f87ae6e06 100644 --- a/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/snapshot/security/DividendCalculationTest.java +++ b/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/snapshot/security/DividendCalculationTest.java @@ -2,6 +2,7 @@ import static org.junit.Assert.assertEquals; +import java.time.LocalDate; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; @@ -12,8 +13,10 @@ import name.abuchen.portfolio.junit.TestCurrencyConverter; import name.abuchen.portfolio.model.Account; import name.abuchen.portfolio.model.AccountTransaction; +import name.abuchen.portfolio.model.AccountTransaction.AccountTransactionBuilder; import name.abuchen.portfolio.model.Portfolio; import name.abuchen.portfolio.model.PortfolioTransaction; +import name.abuchen.portfolio.model.PortfolioTransaction.PortfolioTransactionBuilder; import name.abuchen.portfolio.model.PortfolioTransaction.Type; import name.abuchen.portfolio.model.Security; import name.abuchen.portfolio.money.CurrencyConverter; @@ -196,4 +199,230 @@ public void rateOfReturnCalculationTest() assertEquals(0.1, dividends.getRateOfReturnPerYear(), 0.0); } + + @Test + public void testCalculateYieldOnCost_emptyPortfolioShouldNotCauseExceptions() + { + Account myWealthyAccount = new Account("myWealthyAccount"); + myWealthyAccount.setCurrencyCode("USD"); + Security apple = new Security("Apple Corp", "USD"); + + List transactions = new ArrayList<>(); + + @SuppressWarnings("unused") + CostCalculation cost = Calculation.perform(CostCalculation.class, converter, apple, transactions); + DividendCalculation dividends = Calculation.perform(DividendCalculation.class, converter, apple, transactions); + + assertEquals(0.0, dividends.getYieldOnCost(), 0.0); + } + + @Test + public void testCalculateYieldOnCost_noDividendsShouldNotCauseExceptions() + { + Account myWealthyAccount = new Account("myWealthyAccount"); + myWealthyAccount.setCurrencyCode("USD"); + Security apple = new Security("Apple Corp", "USD"); + + List transactions = new ArrayList<>(); + transactions.add(CalculationLineItem.of(new Portfolio(), + new PortfolioTransactionBuilder(Type.BUY).forSecurity(apple).numberOfShares(10) + .withAmountOf(1000).transactionAt(LocalDateTime.now()) + .withCurrency(apple.getCurrencyCode()).withCostsOf(8).build())); + + @SuppressWarnings("unused") + CostCalculation cost = Calculation.perform(CostCalculation.class, converter, apple, transactions); + DividendCalculation dividends = Calculation.perform(DividendCalculation.class, converter, apple, + transactions); + + assertEquals(0.0, dividends.getYieldOnCost(), 0.0); + } + + @Test + public void testCalculateYieldOnCost_oneDiviPaymentAndOneBuyTransation() + { + Security apple = new Security("Apple Corp", "USD"); + + Account myWealthyAccount = new Account("myWealthyAccount"); + myWealthyAccount.setCurrencyCode(apple.getCurrencyCode()); + Portfolio portfolio = new Portfolio(); + CurrencyConverter noConvertsNeeded = new TestCurrencyConverter(apple.getCurrencyCode()); + + List transactions = new ArrayList<>(); + transactions.add(CalculationLineItem.of(portfolio, + new PortfolioTransactionBuilder(Type.BUY).forSecurity(apple).numberOfShares(10) + .withAmountOf(1000).withCurrency(apple.getCurrencyCode()) + .transactionAt(LocalDateTime.of(2023, 1, 1, 0, 0)).withCostsOf(8).build())); + transactions.add(CalculationLineItem.of(myWealthyAccount, + new AccountTransactionBuilder(AccountTransaction.Type.DIVIDENDS).forSecurity(apple) + .withAmountOf(20).withCurrency(apple.getCurrencyCode()) + .transactionAt(LocalDateTime.of(2023, 1, 15, 0, 0)).build())); + + @SuppressWarnings("unused") + CostCalculation cost = Calculation.perform(CostCalculation.class, noConvertsNeeded, apple, transactions); + DividendCalculation dividends = Calculation.perform(DividendCalculation.class, noConvertsNeeded, apple, + transactions); + + assertEquals(2.0, dividends.calculateYieldOnCost(2, LocalDate.of(2023, 3, 30)), 0.0); + } + + @Test + public void testCalculateYieldOnCost_oneDiviPaymentAndTwoBuyTransations() + { + Security apple = new Security("Apple Corp", "USD"); + + Account myWealthyAccount = new Account("myWealthyAccount"); + myWealthyAccount.setCurrencyCode(apple.getCurrencyCode()); + Portfolio portfolio = new Portfolio(); + CurrencyConverter noConvertsNeeded = new TestCurrencyConverter(apple.getCurrencyCode()); + + List transactions = new ArrayList<>(); + // 2k for 30 shares = 66.66 $/share in avg + transactions.add(CalculationLineItem.of(portfolio, + new PortfolioTransactionBuilder(Type.BUY).forSecurity(apple).numberOfShares(20) + .withAmountOf(1000).withCurrency(apple.getCurrencyCode()) + .transactionAt(LocalDateTime.of(2022, 1, 1, 0, 0)).withCostsOf(8).build())); + transactions.add(CalculationLineItem.of(myWealthyAccount, + new AccountTransactionBuilder(AccountTransaction.Type.DIVIDENDS).forSecurity(apple) + .withAmountOf(20).withCurrency(apple.getCurrencyCode()) + .transactionAt(LocalDateTime.of(2022, 1, 15, 0, 0)).build())); + transactions.add(CalculationLineItem.of(portfolio, + new PortfolioTransactionBuilder(Type.BUY).forSecurity(apple).numberOfShares(10) + .withAmountOf(1000).withCurrency(apple.getCurrencyCode()) + .transactionAt(LocalDateTime.of(2023, 1, 1, 0, 0)).withCostsOf(8).build())); + // 20 * 1$ + 10 * 0.5$ + transactions.add(CalculationLineItem.of(myWealthyAccount, + new AccountTransactionBuilder(AccountTransaction.Type.DIVIDENDS).forSecurity(apple) + .withAmountOf(25).withCurrency(apple.getCurrencyCode()) + .transactionAt(LocalDateTime.of(2023, 1, 15, 0, 0)).build())); + + @SuppressWarnings("unused") + CostCalculation cost = Calculation.perform(CostCalculation.class, noConvertsNeeded, apple, transactions); + DividendCalculation dividends = Calculation.perform(DividendCalculation.class, noConvertsNeeded, apple, + transactions); + + assertEquals(1.25, dividends.calculateYieldOnCost(2, LocalDate.of(2023, 3, 30)), 0.0); + } + + @Test + public void testCalculateYieldOnCost_twoDiviPaymentsOnePerYear() + { + Security apple = new Security("Apple Corp", "USD"); + + Account myWealthyAccount = new Account("myWealthyAccount"); + myWealthyAccount.setCurrencyCode(apple.getCurrencyCode()); + Portfolio portfolio = new Portfolio(); + CurrencyConverter noConvertsNeeded = new TestCurrencyConverter(apple.getCurrencyCode()); + + List transactions = new ArrayList<>(); + transactions.add(CalculationLineItem.of(portfolio, + new PortfolioTransactionBuilder(Type.BUY).forSecurity(apple).numberOfShares(10) + .withAmountOf(1000).withCurrency(apple.getCurrencyCode()) + .transactionAt(LocalDateTime.of(2022, 1, 1, 0, 0)).withCostsOf(8).build())); + transactions.add(CalculationLineItem.of(myWealthyAccount, + new AccountTransactionBuilder(AccountTransaction.Type.DIVIDENDS).forSecurity(apple) + .withAmountOf(15).withCurrency(apple.getCurrencyCode()) + .transactionAt(LocalDateTime.of(2022, 1, 15, 0, 0)).build())); + transactions.add(CalculationLineItem.of(myWealthyAccount, + new AccountTransactionBuilder(AccountTransaction.Type.DIVIDENDS).forSecurity(apple) + .withAmountOf(20).withCurrency(apple.getCurrencyCode()) + .transactionAt(LocalDateTime.of(2023, 1, 15, 0, 0)).build())); + + @SuppressWarnings("unused") + CostCalculation cost = Calculation.perform(CostCalculation.class, noConvertsNeeded, apple, transactions); + DividendCalculation dividends = Calculation.perform(DividendCalculation.class, noConvertsNeeded, apple, + transactions); + + assertEquals(2.0, dividends.calculateYieldOnCost(2, LocalDate.of(2023, 3, 30)), 0.0); + } + + @Test + public void testCalculateYieldOnCost_fourDiviPaymentsHalfYearPeriod() + { + Security apple = new Security("Apple Corp", "USD"); + + Account myWealthyAccount = new Account("myWealthyAccount"); + myWealthyAccount.setCurrencyCode(apple.getCurrencyCode()); + Portfolio portfolio = new Portfolio(); + CurrencyConverter noConvertsNeeded = new TestCurrencyConverter(apple.getCurrencyCode()); + + List transactions = new ArrayList<>(); + transactions.add(CalculationLineItem.of(portfolio, + new PortfolioTransactionBuilder(Type.BUY).forSecurity(apple).numberOfShares(10) + .withAmountOf(1000).withCurrency(apple.getCurrencyCode()) + .transactionAt(LocalDateTime.of(2021, 3, 1, 0, 0)).withCostsOf(8).build())); + transactions.add(CalculationLineItem.of(myWealthyAccount, + new AccountTransactionBuilder(AccountTransaction.Type.DIVIDENDS).forSecurity(apple) + .withAmountOf(10).withCurrency(apple.getCurrencyCode()) + .transactionAt(LocalDateTime.of(2021, 7, 15, 0, 0)).build())); + transactions.add(CalculationLineItem.of(myWealthyAccount, + new AccountTransactionBuilder(AccountTransaction.Type.DIVIDENDS).forSecurity(apple) + .withAmountOf(15).withCurrency(apple.getCurrencyCode()) + .transactionAt(LocalDateTime.of(2022, 1, 15, 0, 0)).build())); + transactions.add(CalculationLineItem.of(myWealthyAccount, + new AccountTransactionBuilder(AccountTransaction.Type.DIVIDENDS).forSecurity(apple) + .withAmountOf(15).withCurrency(apple.getCurrencyCode()) + .transactionAt(LocalDateTime.of(2022, 7, 15, 0, 0)).build())); + transactions.add(CalculationLineItem.of(myWealthyAccount, + new AccountTransactionBuilder(AccountTransaction.Type.DIVIDENDS).forSecurity(apple) + .withAmountOf(20).withCurrency(apple.getCurrencyCode()) + .transactionAt(LocalDateTime.of(2023, 1, 15, 0, 0)).build())); + + @SuppressWarnings("unused") + CostCalculation cost = Calculation.perform(CostCalculation.class, noConvertsNeeded, apple, transactions); + DividendCalculation dividends = Calculation.perform(DividendCalculation.class, noConvertsNeeded, apple, + transactions); + + // only last 2 divi payments will be used by calculation + assertEquals(3.5, dividends.calculateYieldOnCost(2, LocalDate.of(2023, 3, 30)), 0.0); + } + + @Test + public void testCalculateYieldOnCost_sixDiviPaymentsQuarterPeriod() + { + Security apple = new Security("Apple Corp", "USD"); + + Account myWealthyAccount = new Account("myWealthyAccount"); + myWealthyAccount.setCurrencyCode(apple.getCurrencyCode()); + Portfolio portfolio = new Portfolio(); + CurrencyConverter noConvertsNeeded = new TestCurrencyConverter(apple.getCurrencyCode()); + + List transactions = new ArrayList<>(); + transactions.add(CalculationLineItem.of(portfolio, + new PortfolioTransactionBuilder(Type.BUY).forSecurity(apple).numberOfShares(10) + .withAmountOf(1000).withCurrency(apple.getCurrencyCode()) + .transactionAt(LocalDateTime.of(2021, 3, 1, 0, 0)).withCostsOf(8).build())); + transactions.add(CalculationLineItem.of(myWealthyAccount, + new AccountTransactionBuilder(AccountTransaction.Type.DIVIDENDS).forSecurity(apple) + .withAmountOf(10).withCurrency(apple.getCurrencyCode()) + .transactionAt(LocalDateTime.of(2021, 7, 15, 0, 0)).build())); + transactions.add(CalculationLineItem.of(myWealthyAccount, + new AccountTransactionBuilder(AccountTransaction.Type.DIVIDENDS).forSecurity(apple) + .withAmountOf(10).withCurrency(apple.getCurrencyCode()) + .transactionAt(LocalDateTime.of(2022, 1, 15, 0, 0)).build())); + // switch between payment period should have no effect + transactions.add(CalculationLineItem.of(myWealthyAccount, + new AccountTransactionBuilder(AccountTransaction.Type.DIVIDENDS).forSecurity(apple) + .withAmountOf(5).withCurrency(apple.getCurrencyCode()) + .transactionAt(LocalDateTime.of(2022, 4, 15, 0, 0)).build())); + transactions.add(CalculationLineItem.of(myWealthyAccount, + new AccountTransactionBuilder(AccountTransaction.Type.DIVIDENDS).forSecurity(apple) + .withAmountOf(5).withCurrency(apple.getCurrencyCode()) + .transactionAt(LocalDateTime.of(2022, 7, 15, 0, 0)).build())); + transactions.add(CalculationLineItem.of(myWealthyAccount, + new AccountTransactionBuilder(AccountTransaction.Type.DIVIDENDS).forSecurity(apple) + .withAmountOf(5).withCurrency(apple.getCurrencyCode()) + .transactionAt(LocalDateTime.of(2022, 10, 15, 0, 0)).build())); + transactions.add(CalculationLineItem.of(myWealthyAccount, + new AccountTransactionBuilder(AccountTransaction.Type.DIVIDENDS).forSecurity(apple) + .withAmountOf(5).withCurrency(apple.getCurrencyCode()) + .transactionAt(LocalDateTime.of(2023, 1, 15, 0, 0)).build())); + + @SuppressWarnings("unused") + CostCalculation cost = Calculation.perform(CostCalculation.class, noConvertsNeeded, apple, transactions); + DividendCalculation dividends = Calculation.perform(DividendCalculation.class, noConvertsNeeded, apple, + transactions); + + assertEquals(2.0, dividends.calculateYieldOnCost(2, LocalDate.of(2023, 3, 30)), 0.0); + } + } 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 6bbf7b582c..a786fdf0c1 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 @@ -186,6 +186,8 @@ public class Messages extends NLS public static String ColumnDividendMovingAverageTotalRateOfReturn; public static String ColumnDividendMovingAverageTotalRateOfReturn_Description; public static String ColumnDividendMovingAverageTotalRateOfReturn_MenuLabel; + public static String ColumnDividendYieldOnCost; + public static String ColumnDividendYieldOnCost_Description; public static String ColumnEarnings; public static String ColumnEarnings_Description; public static String ColumnEndDate; 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 f4c548b572..2b61321bba 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 @@ -398,7 +398,11 @@ ColumnDividendSum_MenuLabel = Sum of dividends ColumnDividendTotalRateOfReturn = Div% -ColumnDividendTotalRateOfReturn_Description = dividend rate of return = sum of dividend payments / purchase value based on FIFO\n\nAttention: if shares are sold after a dividend payment then dividend payment is not reduced. Therefore the rate of return might be over estimated. +ColumnDividendTotalRateOfReturn_Description = Dividend rate of return = sum of dividend payments / purchase value based on FIFO\n\nAttention: if shares are sold after a dividend payment then dividend payment is not reduced. Therefore the rate of return might be over estimated. + +ColumnDividendYieldOnCost = Div%YoC 1Y + +ColumnDividendYieldOnCost_Description = Dividend yield on cost (YoC) for the last 12 months. ColumnEarnings = Earnings diff --git a/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/messages_de.properties b/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/messages_de.properties index dd86ee2329..89ef1bb975 100644 --- a/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/messages_de.properties +++ b/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/messages_de.properties @@ -387,6 +387,10 @@ ColumnDividendTotalRateOfReturn = Div% ColumnDividendTotalRateOfReturn_Description = Dividendenrendite = Summe der Dividendenzahlungen / Einstand nach FIFO\n\nAchtung: wenn Verk\u00E4ufe nach einer Dividendenzahlung vorliegen, werden die momentan nicht abgezogen. D.h. die Rendite ist eventuell zu hoch. +ColumnDividendYieldOnCost = Div%YoC 1J + +ColumnDividendYieldOnCost_Description = Dividendenrendite nach Einstandskosten (YoC) f\u00FCr die letzten 12 Monate. + ColumnEarnings = Ertr\u00E4ge ColumnEarnings_Description = Dividenden + Zinsen diff --git a/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/util/viewers/ColumnViewerSorter.java b/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/util/viewers/ColumnViewerSorter.java index 1b70515eb4..950f725cfc 100644 --- a/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/util/viewers/ColumnViewerSorter.java +++ b/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/util/viewers/ColumnViewerSorter.java @@ -160,7 +160,7 @@ private Method determineReadMethod(Class clazz, String attribute) catch (NoSuchMethodException e1) { PortfolioPlugin.log(Arrays.asList(e, e1)); - throw new IllegalArgumentException(); + throw new IllegalArgumentException(e.getMessage()); } } } 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 ad728d36d7..9f5a06ddb2 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 @@ -886,6 +886,22 @@ public String getText(Object r) }); column.setSorter(ColumnViewerSorter.create(SecurityPerformanceRecord.class, "periodicitySort")); //$NON-NLS-1$ recordColumns.addColumn(column); + + // dividend yield on cost + column = new Column("yocdiv", Messages.ColumnDividendYieldOnCost, SWT.RIGHT, 80); //$NON-NLS-1$ + column.setGroupLabel(Messages.GroupLabelDividends); + column.setDescription(Messages.ColumnDividendYieldOnCost_Description); + column.setVisible(false); + column.setLabelProvider(new ColumnLabelProvider() + { + @Override + public String getText(Object r) + { + return Values.PercentPlain2.formatNonZero(((SecurityPerformanceRecord) r).getDividendYieldOnCost()); + } + }); + column.setSorter(ColumnViewerSorter.create(SecurityPerformanceRecord.class, "dividendYieldOnCost")); //$NON-NLS-1$ + recordColumns.addColumn(column); } private void createRiskColumns() diff --git a/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/views/StatementOfAssetsViewer.java b/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/views/StatementOfAssetsViewer.java index 5792ab0b44..d4ed206824 100644 --- a/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/views/StatementOfAssetsViewer.java +++ b/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/views/StatementOfAssetsViewer.java @@ -69,6 +69,7 @@ import name.abuchen.portfolio.snapshot.ClientSnapshot; import name.abuchen.portfolio.snapshot.GroupByTaxonomy; import name.abuchen.portfolio.snapshot.ReportingPeriod; +import name.abuchen.portfolio.snapshot.ReportingPeriod.LastXYears; import name.abuchen.portfolio.snapshot.SecurityPosition; import name.abuchen.portfolio.snapshot.filter.ClientFilter; import name.abuchen.portfolio.snapshot.filter.ReadOnlyAccount; @@ -746,6 +747,27 @@ private void addDividendColumns(List options) column.setSorter(ColumnViewerSorter.create(new ElementComparator(labelProvider))); column.setVisible(false); support.addColumn(column); + + // dividend yield on costs + column = new Column("yocdiv", Messages.ColumnDividendYieldOnCost, SWT.RIGHT, 80); //$NON-NLS-1$ + column.setOptions(new ReportingPeriodColumnOptions(Messages.ColumnDividendYieldOnCost + " {0}", // $NON-NLS-1$ //$NON-NLS-1$ + List.of(new LastXYears(1)))); + column.setGroupLabel(Messages.GroupLabelDividends); + column.setDescription(Messages.ColumnDividendYieldOnCost_Description); + column.setVisible(false); + Function valueProvider = new Function() + { + @Override + public Object apply(SecurityPerformanceRecord t) + { + return t.getDividendYieldOnCost() / 100.0;// labelProvider do + // *100% magic + } + }; + labelProvider = new ReportingPeriodLabelProvider(valueProvider, null, false); + column.setLabelProvider(labelProvider); + column.setSorter(ColumnViewerSorter.create(new ElementComparator(labelProvider))); + support.addColumn(column); } private void addAttributeColumns() diff --git a/name.abuchen.portfolio/src/name/abuchen/portfolio/model/AccountTransaction.java b/name.abuchen.portfolio/src/name/abuchen/portfolio/model/AccountTransaction.java index e937705883..c2078716bb 100644 --- a/name.abuchen.portfolio/src/name/abuchen/portfolio/model/AccountTransaction.java +++ b/name.abuchen.portfolio/src/name/abuchen/portfolio/model/AccountTransaction.java @@ -2,6 +2,7 @@ import java.time.Instant; import java.time.LocalDateTime; +import java.util.Objects; import java.util.ResourceBundle; import name.abuchen.portfolio.money.Money; @@ -45,6 +46,53 @@ public String toString() } } + public static class AccountTransactionBuilder + { + private Type txType; + private LocalDateTime dateTime; + private String currencyCode; + private long amount; + private Security security; + + public AccountTransactionBuilder(Type txType) + { + this.txType = txType; + } + + public AccountTransactionBuilder transactionAt(LocalDateTime dateTime) + { + this.dateTime = dateTime; + return this; + } + + public AccountTransactionBuilder withCurrency(String currencyCode) + { + this.currencyCode = currencyCode; + return this; + } + + public AccountTransactionBuilder withAmountOf(long amount) + { + this.amount = amount; + return this; + } + + public AccountTransactionBuilder forSecurity(Security security) + { + this.security = security; + return this; + } + + public AccountTransaction build() + { + Objects.requireNonNull(dateTime, "dateTime is a required field"); //$NON-NLS-1$ + Objects.requireNonNull(currencyCode, "currencyCode is a required field"); //$NON-NLS-1$ + Objects.requireNonNull(amount, "amount is a required field"); //$NON-NLS-1$ + Objects.requireNonNull(security, "security is a required field"); //$NON-NLS-1$ + return new AccountTransaction(dateTime, currencyCode, amount, security, txType); + } + } + private Type type; public AccountTransaction() diff --git a/name.abuchen.portfolio/src/name/abuchen/portfolio/model/PortfolioTransaction.java b/name.abuchen.portfolio/src/name/abuchen/portfolio/model/PortfolioTransaction.java index f967427446..426fc265e7 100644 --- a/name.abuchen.portfolio/src/name/abuchen/portfolio/model/PortfolioTransaction.java +++ b/name.abuchen.portfolio/src/name/abuchen/portfolio/model/PortfolioTransaction.java @@ -4,6 +4,7 @@ import java.math.RoundingMode; import java.time.Instant; import java.time.LocalDateTime; +import java.util.Objects; import java.util.Optional; import java.util.ResourceBundle; @@ -63,6 +64,75 @@ public String toString() } } + public static class PortfolioTransactionBuilder + { + private Type txType; + private LocalDateTime datetime; + private String currencyCode; + private long amount = 0; + private long shares = 0; + private Security security; + private long fees = 0; + private long taxes = 0; + + public PortfolioTransactionBuilder(Type txType) + { + this.txType = txType; + } + + public PortfolioTransactionBuilder transactionAt(LocalDateTime dateTime) + { + datetime = dateTime; + return this; + } + + public PortfolioTransactionBuilder withCurrency(String code) + { + currencyCode = code; + return this; + } + + public PortfolioTransactionBuilder withAmountOf(long amount) + { + this.amount = amount; + return this; + } + + public PortfolioTransactionBuilder numberOfShares(long shares) + { + this.shares = shares; + return this; + } + + public PortfolioTransactionBuilder forSecurity(Security security) + { + this.security = security; + return this; + } + + public PortfolioTransactionBuilder withCostsOf(long fees) + { + this.fees = fees; + return this; + } + + public PortfolioTransactionBuilder withTaxAmountOf(long taxes) + { + this.taxes = taxes; + return this; + } + + public PortfolioTransaction build() + { + Objects.requireNonNull(datetime, "datetime is a required field"); //$NON-NLS-1$ + Objects.requireNonNull(currencyCode, "currencyCode is a required field"); //$NON-NLS-1$ + Objects.requireNonNull(amount, "amount is a required field"); //$NON-NLS-1$ + Objects.requireNonNull(security, "security is a required field"); //$NON-NLS-1$ + Objects.requireNonNull(shares, "shares is a required field"); //$NON-NLS-1$ + return new PortfolioTransaction(datetime, currencyCode, amount, security, shares, txType, fees, taxes); + } + } + private Type type; @Deprecated diff --git a/name.abuchen.portfolio/src/name/abuchen/portfolio/money/Values.java b/name.abuchen.portfolio/src/name/abuchen/portfolio/money/Values.java index d6eae2ffcb..b4d4b14354 100644 --- a/name.abuchen.portfolio/src/name/abuchen/portfolio/money/Values.java +++ b/name.abuchen.portfolio/src/name/abuchen/portfolio/money/Values.java @@ -14,6 +14,10 @@ import name.abuchen.portfolio.util.FormatHelper; +/** + * @deprecated class do unexpected things + */ +@Deprecated public abstract class Values { public static final MathContext MC = new MathContext(10, RoundingMode.HALF_UP); @@ -373,6 +377,15 @@ public String format(Double percent) } }; + public static final Values PercentPlain2 = new Values("0.00", 0) //$NON-NLS-1$ + { + @Override + public String format(Double percent) + { + return String.format("%,.2f", percent) + "%"; //$NON-NLS-1$ //$NON-NLS-2$ + } + }; + public static final Values Weight = new Values("#,##0.00", 2) //$NON-NLS-1$ { @Override diff --git a/name.abuchen.portfolio/src/name/abuchen/portfolio/snapshot/ReportingPeriod.java b/name.abuchen.portfolio/src/name/abuchen/portfolio/snapshot/ReportingPeriod.java index 5fe26d471b..b1f481d39f 100644 --- a/name.abuchen.portfolio/src/name/abuchen/portfolio/snapshot/ReportingPeriod.java +++ b/name.abuchen.portfolio/src/name/abuchen/portfolio/snapshot/ReportingPeriod.java @@ -56,6 +56,7 @@ private Type(char code, Class implementation) this.code = code; this.implementation = implementation; } + } private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM); @@ -121,6 +122,15 @@ public String getCode() return buf.toString(); } + public static class LastXYears extends LastX + { + + public LastXYears(int years) + { + super(years, 0); + } + } + public static class LastX extends ReportingPeriod { private final int years; diff --git a/name.abuchen.portfolio/src/name/abuchen/portfolio/snapshot/security/DividendCalculation.java b/name.abuchen.portfolio/src/name/abuchen/portfolio/snapshot/security/DividendCalculation.java index 4e505ea681..88e99fa8ef 100644 --- a/name.abuchen.portfolio/src/name/abuchen/portfolio/snapshot/security/DividendCalculation.java +++ b/name.abuchen.portfolio/src/name/abuchen/portfolio/snapshot/security/DividendCalculation.java @@ -39,22 +39,25 @@ private static class Payment */ public final double rateOfReturn; + public final Money fifoCost; + /** * Constructs an instance. * * @param converter * currency converter - * @param t + * @param diviPayment * {@link DividendTransaction} * @param security * {@link Security} */ - public Payment(CurrencyConverter converter, CalculationLineItem.DividendPayment t, Security security) + public Payment(CurrencyConverter converter, CalculationLineItem.DividendPayment diviPayment, Security security) { - this.amount = t.getGrossValue().with(converter.at(t.getDateTime())); - LocalDateTime time = t.getDateTime(); + this.amount = diviPayment.getGrossValue().with(converter.at(diviPayment.getDateTime())); + LocalDateTime time = diviPayment.getDateTime(); this.year = time.getYear(); this.date = time.toLocalDate(); + fifoCost = diviPayment.getFifoCost(); // try to set rate of return, default is NaN double rr = Double.NaN; @@ -67,9 +70,9 @@ public Payment(CurrencyConverter converter, CalculationLineItem.DividendPayment // multiple accounts). The moving average cost is always the // total costs. - Money movingAverageCost = t.getMovingAverageCost(); + Money movingAverageCost = diviPayment.getMovingAverageCost(); if (movingAverageCost != null && !movingAverageCost.isZero()) - rr = t.getGrossValueAmount() / (double) movingAverageCost.getAmount(); + rr = diviPayment.getGrossValueAmount() / (double) movingAverageCost.getAmount(); // check if it is valid (non 0) if (rr == 0) @@ -83,7 +86,7 @@ public Payment(CurrencyConverter converter, CalculationLineItem.DividendPayment { double sharePriceAmount = ((double) pValue) / Values.Quote.factor() * Values.AmountFraction.factor(); - rr = t.getDividendPerShare() / sharePriceAmount; + rr = diviPayment.getDividendPerShare() / sharePriceAmount; } } } @@ -95,6 +98,7 @@ public Payment(CurrencyConverter converter, CalculationLineItem.DividendPayment private Periodicity periodicity; private MutableMoney sum; private double rateOfReturnPerYear; + public double yieldOnCostPerYear; @Override public void finish(CurrencyConverter converter, List lineItems) @@ -129,7 +133,6 @@ public void finish(CurrencyConverter converter, List lineIt } int years = 0; - // now walk through individual years for (int year = firstPayment.getYear(); year <= lastPayment.getYear(); year++) { @@ -182,6 +185,7 @@ public void finish(CurrencyConverter converter, List lineIt } this.rateOfReturnPerYear = sumRateOfReturn / years; + yieldOnCostPerYear = calculateYieldOnCost(years, LocalDate.now()); // determine periodicity? if (significantCount > 0) @@ -238,6 +242,11 @@ public double getRateOfReturnPerYear() return rateOfReturnPerYear; } + public double getYieldOnCost() + { + return yieldOnCostPerYear; + } + public Money getSum() { return sum.toMoney(); @@ -256,4 +265,42 @@ public void visit(CurrencyConverter converter, CalculationLineItem.DividendPayme // construct new payment and add it to the list payments.add(new Payment(converter, t, getSecurity())); } + + /** + * calculates the dividend yield on cost for one year. yield is based on the + * last dividends and not on an average dividend payment over time for this + * security. + * + * @param years + * between first and last dividend payments + * @return + */ + double calculateYieldOnCost(int years, LocalDate now) + { + long fifoCostAmount = 0L; + for (int i = payments.size() - 1; i >= 0; i--) + { + Payment p = payments.get(i); + if (p.fifoCost != null && p.fifoCost.isPositive()) + { + fifoCostAmount = p.fifoCost.getAmount(); + break; + } + } + if (fifoCostAmount > 0) + { + double fifoCosts = fifoCostAmount * Values.AmountFraction.factor(); + + LocalDate firstDividendPaymentAccepted = now.minusYears(1); + double diviSum = years > 1 ? payments.stream() // + .filter(p -> p.date.isAfter(firstDividendPaymentAccepted)) // + .mapToDouble(p -> p.amount.getAmount() * Values.AmountFraction.factor()) // + .sum() // + : sum.getAmount() * Values.AmountFraction.factor() / years; + + return diviSum * 100.0 / fifoCosts; + } + return 0.0; + } + } diff --git a/name.abuchen.portfolio/src/name/abuchen/portfolio/snapshot/security/SecurityPerformanceRecord.java b/name.abuchen.portfolio/src/name/abuchen/portfolio/snapshot/security/SecurityPerformanceRecord.java index 9419da05d8..2b3a35978a 100644 --- a/name.abuchen.portfolio/src/name/abuchen/portfolio/snapshot/security/SecurityPerformanceRecord.java +++ b/name.abuchen.portfolio/src/name/abuchen/portfolio/snapshot/security/SecurityPerformanceRecord.java @@ -205,7 +205,9 @@ public final SecurityPerformanceRecord build(Client client, CurrencyConverter co /** * rate of return per year {@link #calculateDividends()} */ - private double rateOfReturnPerYear; + private double dividendRateOfReturnPerYear; + + private double dividendYieldOnCost; /** * market value - fifo cost of shares held @@ -405,7 +407,7 @@ public int getPeriodicitySort() */ public double getRateOfReturnPerYear() { - return this.rateOfReturnPerYear; + return this.dividendRateOfReturnPerYear; } public double getTotalRateOfReturnDiv() @@ -418,6 +420,11 @@ public double getTotalRateOfReturnDivMovingAverage() return sharesHeld > 0 ? (double) sumOfDividends.getAmount() / (double) movingAverageCost.getAmount() : 0; } + public double getDividendYieldOnCost() + { + return dividendYieldOnCost; + } + public CapitalGainsRecord getRealizedCapitalGains() { return realizedCapitalGains; @@ -594,7 +601,8 @@ private void calculateDividends(CurrencyConverter converter) this.sumOfDividends = dividends.getSum(); this.dividendEventCount = dividends.getNumOfEvents(); this.lastDividendPayment = dividends.getLastDividendPayment(); - this.rateOfReturnPerYear = dividends.getRateOfReturnPerYear(); + this.dividendRateOfReturnPerYear = dividends.getRateOfReturnPerYear(); + this.dividendYieldOnCost = dividends.getYieldOnCost(); } private void calculatePeriodicity(Client client, CurrencyConverter converter)