package name.abuchen.portfolio.snapshot; import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.assertThat; import java.math.BigDecimal; import java.time.LocalDate; import java.time.Month; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import org.junit.Test; import name.abuchen.portfolio.TestCurrencyConverter; import name.abuchen.portfolio.model.Client; import name.abuchen.portfolio.model.Portfolio; import name.abuchen.portfolio.model.PortfolioTransaction; import name.abuchen.portfolio.model.PortfolioTransaction.Type; import name.abuchen.portfolio.model.Security; import name.abuchen.portfolio.model.SecurityPrice; import name.abuchen.portfolio.model.Transaction.Unit; import name.abuchen.portfolio.money.CurrencyConverter; import name.abuchen.portfolio.money.CurrencyUnit; import name.abuchen.portfolio.money.Money; import name.abuchen.portfolio.money.Values; import name.abuchen.portfolio.snapshot.security.SecurityPerformanceRecord; import name.abuchen.portfolio.snapshot.security.SecurityPerformanceSnapshot; @SuppressWarnings("nls") public class SecurityPositionTest { @Test public void testFIFOPurchasePrice() { List<PortfolioTransaction> tx = new ArrayList<PortfolioTransaction>(); tx.add(new PortfolioTransaction(LocalDate.now(), CurrencyUnit.EUR, 100000, null, 100 * Values.Share.factor(), Type.BUY, 0, 0)); tx.add(new PortfolioTransaction(LocalDate.now(), CurrencyUnit.EUR, 50000, null, 50 * Values.Share.factor(), Type.SELL, 0, 0)); SecurityPosition position = new SecurityPosition(new Security(), new TestCurrencyConverter(), new SecurityPrice(), tx); assertThat(position.getShares(), is(50L * Values.Share.factor())); assertThat(position.getFIFOPurchasePrice(), is(Money.of(CurrencyUnit.EUR, 10_00))); } @Test public void testPurchasePriceWithMultipleBuyTransactions() { List<PortfolioTransaction> tx = new ArrayList<PortfolioTransaction>(); tx.add(new PortfolioTransaction(LocalDate.now(), CurrencyUnit.EUR, 25000, null, 25 * Values.Share.factor(), Type.BUY, 0, 0)); tx.add(new PortfolioTransaction(LocalDate.now(), CurrencyUnit.EUR, 150000, null, 75 * Values.Share.factor(), Type.BUY, 0, 0)); tx.add(new PortfolioTransaction(LocalDate.now(), CurrencyUnit.EUR, 100000, null, 50 * Values.Share.factor(), Type.SELL, 0, 0)); SecurityPosition position = new SecurityPosition(new Security(), new TestCurrencyConverter(), new SecurityPrice(), tx); assertThat(position.getShares(), is(50L * Values.Share.factor())); assertThat(position.getFIFOPurchasePrice(), is(Money.of(CurrencyUnit.EUR, 20_00))); } @Test public void testPurchasePriceWithMultipleBuyTransactionsMiddlePrice() { List<PortfolioTransaction> tx = new ArrayList<PortfolioTransaction>(); tx.add(new PortfolioTransaction(LocalDate.now(), CurrencyUnit.EUR, 75000, null, 75 * Values.Share.factor(), Type.BUY, 0, 0)); tx.add(new PortfolioTransaction(LocalDate.now(), CurrencyUnit.EUR, 50000, null, 25 * Values.Share.factor(), Type.BUY, 0, 0)); tx.add(new PortfolioTransaction(LocalDate.now(), CurrencyUnit.EUR, 100000, null, 50 * Values.Share.factor(), Type.SELL, 0, 0)); SecurityPosition position = new SecurityPosition(new Security(), new TestCurrencyConverter(), new SecurityPrice(), tx); assertThat(position.getShares(), is(50L * Values.Share.factor())); assertThat(position.getFIFOPurchasePrice(), is(Money.of(CurrencyUnit.EUR, 15_00))); } @Test public void testPurchasePriceNaNIfOnlySellTransactions() { SecurityPosition position = new SecurityPosition(new Security(), new TestCurrencyConverter(), new SecurityPrice(), Arrays.asList( // new PortfolioTransaction(LocalDate.now(), CurrencyUnit.EUR, 500_00, null, 50 * Values.Share.factor(), Type.SELL, 0, 0))); assertThat(position.getShares(), is(-50L * Values.Share.factor())); assertThat(position.getFIFOPurchasePrice(), is(Money.of(CurrencyUnit.EUR, 0))); } @Test public void testThatTransferInCountsIfTransferOutIsMissing() { SecurityPrice price = new SecurityPrice(LocalDate.of(2012, Month.DECEMBER, 2), Values.Quote.factorize(20)); List<PortfolioTransaction> tx = new ArrayList<PortfolioTransaction>(); tx.add(new PortfolioTransaction("2012-01-01", CurrencyUnit.EUR, 50000, null, 50 * Values.Share.factor(), Type.TRANSFER_IN, 0, 0)); SecurityPosition position = new SecurityPosition(new Security(), new TestCurrencyConverter(), price, tx); assertThat(position.getShares(), is(50L * Values.Share.factor())); assertThat(position.getFIFOPurchasePrice(), is(Money.of(CurrencyUnit.EUR, 10_00))); assertThat(position.getFIFOPurchaseValue(), is(Money.of(CurrencyUnit.EUR, 500_00))); assertThat(position.calculateValue(), is(Money.of(CurrencyUnit.EUR, 1000_00))); assertThat(position.getProfitLoss(), is(Money.of(CurrencyUnit.EUR, 500_00))); } @Test public void testThatTransferInCountsIfTransferOutIsMissingPlusBuyTransaction() { SecurityPrice price = new SecurityPrice(LocalDate.of(2012, Month.DECEMBER, 2), Values.Quote.factorize(20)); List<PortfolioTransaction> tx = new ArrayList<PortfolioTransaction>(); tx.add(new PortfolioTransaction("2012-01-01", CurrencyUnit.EUR, 50000, null, 50 * Values.Share.factor(), Type.BUY, 0, 0)); tx.add(new PortfolioTransaction("2012-02-01", CurrencyUnit.EUR, 55000, null, 50 * Values.Share.factor(), Type.TRANSFER_IN, 0, 0)); SecurityPosition position = new SecurityPosition(new Security(), new TestCurrencyConverter(), price, tx); assertThat(position.getShares(), is(100L * Values.Share.factor())); assertThat(position.getFIFOPurchasePrice(), is(Money.of(CurrencyUnit.EUR, 10_50))); assertThat(position.getFIFOPurchaseValue(), is(Money.of(CurrencyUnit.EUR, 1050_00))); assertThat(position.calculateValue(), is(Money.of(CurrencyUnit.EUR, 2000_00))); assertThat(position.getProfitLoss(), is(Money.of(CurrencyUnit.EUR, 950_00))); } @Test public void testThatTransferInDoesNotCountIfMatchingTransferOutIsIncluded() { SecurityPrice price = new SecurityPrice(LocalDate.of(2012, Month.DECEMBER, 2), Values.Quote.factorize(20)); List<PortfolioTransaction> tx = new ArrayList<PortfolioTransaction>(); tx.add(new PortfolioTransaction("2012-01-01", CurrencyUnit.EUR, 50000, null, 50 * Values.Share.factor(), Type.BUY, 0, 0)); tx.add(new PortfolioTransaction("2012-02-01", CurrencyUnit.EUR, 55000, null, 50 * Values.Share.factor(), Type.TRANSFER_OUT, 0, 0)); tx.add(new PortfolioTransaction("2012-02-01", CurrencyUnit.EUR, 55000, null, 50 * Values.Share.factor(), Type.TRANSFER_IN, 0, 0)); SecurityPosition position = new SecurityPosition(new Security(), new TestCurrencyConverter(), price, tx); assertThat(position.getShares(), is(50L * Values.Share.factor())); assertThat(position.getFIFOPurchasePrice(), is(Money.of(CurrencyUnit.EUR, 10_00))); assertThat(position.getFIFOPurchaseValue(), is(Money.of(CurrencyUnit.EUR, 500_00))); assertThat(position.calculateValue(), is(Money.of(CurrencyUnit.EUR, 1000_00))); assertThat(position.getProfitLoss(), is(Money.of(CurrencyUnit.EUR, 500_00))); } @Test public void testThatOnlyMatchingTransfersAreRemoved_InRemains() { SecurityPrice price = new SecurityPrice(LocalDate.of(2012, Month.DECEMBER, 2), Values.Quote.factorize(20)); List<PortfolioTransaction> tx = new ArrayList<PortfolioTransaction>(); tx.add(new PortfolioTransaction("2012-01-01", CurrencyUnit.EUR, 50000, null, 50 * Values.Share.factor(), Type.BUY, 0, 0)); tx.add(new PortfolioTransaction("2012-02-01", CurrencyUnit.EUR, 55000, null, 50 * Values.Share.factor(), Type.TRANSFER_OUT, 0, 0)); tx.add(new PortfolioTransaction("2012-02-01", CurrencyUnit.EUR, 55000, null, 50 * Values.Share.factor(), Type.TRANSFER_IN, 0, 0)); tx.add(new PortfolioTransaction("2012-02-02", CurrencyUnit.EUR, 55000, null, 50 * Values.Share.factor(), Type.TRANSFER_IN, 0, 0)); SecurityPosition position = new SecurityPosition(new Security(), new TestCurrencyConverter(), price, tx); assertThat(position.getShares(), is(100L * Values.Share.factor())); assertThat(position.getFIFOPurchasePrice(), is(Money.of(CurrencyUnit.EUR, 10_50))); assertThat(position.getFIFOPurchaseValue(), is(Money.of(CurrencyUnit.EUR, 1050_00))); assertThat(position.calculateValue(), is(Money.of(CurrencyUnit.EUR, 2000_00))); assertThat(position.getProfitLoss(), is(Money.of(CurrencyUnit.EUR, 950_00))); } @Test public void testThatOnlyMatchingTransfersAreRemoved_OutRemains() { SecurityPrice price = new SecurityPrice(LocalDate.of(2012, Month.DECEMBER, 2), Values.Quote.factorize(20)); List<PortfolioTransaction> tx = new ArrayList<PortfolioTransaction>(); tx.add(new PortfolioTransaction("2012-01-01", CurrencyUnit.EUR, 50000, null, 50 * Values.Share.factor(), Type.BUY, 0, 0)); tx.add(new PortfolioTransaction("2012-02-01", CurrencyUnit.EUR, 55000, null, 50 * Values.Share.factor(), Type.TRANSFER_OUT, 0, 0)); tx.add(new PortfolioTransaction("2012-02-01", CurrencyUnit.EUR, 55000, null, 50 * Values.Share.factor(), Type.TRANSFER_IN, 0, 0)); tx.add(new PortfolioTransaction("2012-02-02", CurrencyUnit.EUR, 55000, null, 25 * Values.Share.factor(), Type.TRANSFER_OUT, 0, 0)); SecurityPosition position = new SecurityPosition(new Security(), new TestCurrencyConverter(), price, tx); assertThat(position.getShares(), is(25L * Values.Share.factor())); assertThat(position.getFIFOPurchasePrice(), is(Money.of(CurrencyUnit.EUR, 10_00))); assertThat(position.getFIFOPurchaseValue(), is(Money.of(CurrencyUnit.EUR, 250_00))); assertThat(position.calculateValue(), is(Money.of(CurrencyUnit.EUR, 500_00))); assertThat(position.getProfitLoss(), is(Money.of(CurrencyUnit.EUR, 250_00))); } @Test public void testPurchasePriceIfSharesArePartiallyTransferredOut() { SecurityPrice price = new SecurityPrice(LocalDate.of(2012, Month.DECEMBER, 2), Values.Quote.factorize(20)); List<PortfolioTransaction> tx = new ArrayList<PortfolioTransaction>(); tx.add(new PortfolioTransaction("2012-01-01", CurrencyUnit.EUR, 50000, null, 50 * Values.Share.factor(), Type.BUY, 0, 0)); tx.add(new PortfolioTransaction("2012-02-01", CurrencyUnit.EUR, 55000, null, 25 * Values.Share.factor(), Type.TRANSFER_OUT, 0, 0)); SecurityPosition position = new SecurityPosition(new Security(), new TestCurrencyConverter(), price, tx); assertThat(position.getShares(), is(25L * Values.Share.factor())); assertThat(position.getFIFOPurchasePrice(), is(Money.of(CurrencyUnit.EUR, 10_00))); assertThat(position.getFIFOPurchaseValue(), is(Money.of(CurrencyUnit.EUR, 250_00))); assertThat(position.calculateValue(), is(Money.of(CurrencyUnit.EUR, 500_00))); assertThat(position.getProfitLoss(), is(Money.of(CurrencyUnit.EUR, 250_00))); } @Test public void testFIFOPurchasePriceWithForex() { CurrencyConverter currencyConverter = new TestCurrencyConverter().with(CurrencyUnit.USD); Security security = new Security("", CurrencyUnit.USD); PortfolioTransaction t = new PortfolioTransaction(); t.setType(PortfolioTransaction.Type.DELIVERY_INBOUND); t.setDate(LocalDate.parse("2017-01-25")); t.setShares(Values.Share.factorize(13)); t.setSecurity(security); t.setMonetaryAmount(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(9659.24))); t.addUnit(new Unit(Unit.Type.GROSS_VALUE, Money.of(CurrencyUnit.EUR, Values.Amount.factorize(9644.24)), Money.of(CurrencyUnit.USD, Values.Amount.factorize(10287.13)), BigDecimal.valueOf(0.937470704040499))); t.addUnit(new Unit(Unit.Type.FEE, Money.of(CurrencyUnit.EUR, Values.Amount.factorize(15)), Money.of(CurrencyUnit.USD, Values.Amount.factorize(16)), BigDecimal.valueOf(0.937470704040499))); List<PortfolioTransaction> tx = new ArrayList<>(); tx.add(t); SecurityPosition position = new SecurityPosition(security, currencyConverter, new SecurityPrice(), tx); assertThat(position.getShares(), is(13L * Values.Share.factor())); // 10287.13 / 13 = 791.32 assertThat(position.getFIFOPurchasePrice(), is(Money.of(CurrencyUnit.USD, Values.Amount.factorize(791.32)))); // 9659.24 EUR x ( 1 / 0.937470704040499) = 10303,51 assertThat(position.getFIFOPurchaseValue(), is(Money.of(CurrencyUnit.USD, Values.Amount.factorize(9659.24 * (1 / 0.937470704040499))))); Client client = new Client(); client.addSecurity(security); Portfolio p = new Portfolio(); p.addTransaction(t); client.addPortfolio(p); SecurityPerformanceSnapshot snapshot = SecurityPerformanceSnapshot.create(client, currencyConverter, new ReportingPeriod.FromXtoY(LocalDate.parse("2016-12-31"), LocalDate.parse("2017-02-01"))); assertThat(snapshot.getRecords().size(), is(1)); SecurityPerformanceRecord record = snapshot.getRecords().get(0); assertThat(record.getSecurity(), is(security)); assertThat(record.getFifoCost(), is(position.getFIFOPurchaseValue())); assertThat(record.getFifoCostPerSharesHeld().toMoney(), is(position.getFIFOPurchasePrice())); } @Test public void testSplittingPositionsWithForexGrossValue() { Security security = new Security("", CurrencyUnit.EUR); SecurityPrice price = new SecurityPrice(LocalDate.of(2016, Month.DECEMBER, 2), Values.Quote.factorize(10.19)); PortfolioTransaction inbound_delivery = new PortfolioTransaction(); inbound_delivery.setType(PortfolioTransaction.Type.DELIVERY_INBOUND); inbound_delivery.setDate(LocalDate.parse("2016-01-01")); inbound_delivery.setSecurity(security); inbound_delivery.setMonetaryAmount(Money.of(CurrencyUnit.USD, Values.Amount.factorize(27409.55))); inbound_delivery.setShares(Values.Share.factorize(2415.794)); Unit grossValue = new Unit(Unit.Type.GROSS_VALUE, // Money.of(CurrencyUnit.USD, Values.Amount.factorize(27409.55)), Money.of(CurrencyUnit.EUR, Values.Amount.factorize(24616.95)), BigDecimal.valueOf(1.1134421608)); inbound_delivery.addUnit(grossValue); SecurityPosition position = new SecurityPosition(security, new TestCurrencyConverter(), price, Arrays.asList(inbound_delivery)); SecurityPosition third = SecurityPosition.split(position, 20 * Values.Weight.factor()); // 20% // 24616.95 EUR * 0.2 = 4923,39 EUR assertThat(third.getFIFOPurchaseValue(), is(Money.of(CurrencyUnit.EUR, Values.Amount.factorize(4923.39)))); assertThat(third.getFIFOPurchaseValue(), is(Money.of(CurrencyUnit.EUR, Math.round(position.getFIFOPurchaseValue().getAmount() * 0.2)))); } }