Skip to content

Commit

Permalink
3237: add dividend yoc calculation and tests
Browse files Browse the repository at this point in the history
  • Loading branch information
Gerd Gühne (Marfir) committed Apr 6, 2023
1 parent 74ff6c5 commit f3fb7ef
Show file tree
Hide file tree
Showing 16 changed files with 521 additions and 14 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ workspace
.metadata/*
.recommenders/*
.DS_Store
**/.classpath
**/.metadata/**
.classpath

### IntelliJ IDEA ###
.idea
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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<CalculationLineItem> 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<CalculationLineItem> 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<CalculationLineItem> 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<CalculationLineItem> 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<CalculationLineItem> 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<CalculationLineItem> 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<CalculationLineItem> 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);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading

0 comments on commit f3fb7ef

Please sign in to comment.