package scenarios;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import java.io.IOException;
import java.math.BigDecimal;
import java.time.LocalDate;
import org.hamcrest.number.IsCloseTo;
import org.junit.BeforeClass;
import org.junit.Test;
import name.abuchen.portfolio.TestCurrencyConverter;
import name.abuchen.portfolio.model.Account;
import name.abuchen.portfolio.model.Client;
import name.abuchen.portfolio.model.ClientFactory;
import name.abuchen.portfolio.model.Security;
import name.abuchen.portfolio.money.CurrencyUnit;
import name.abuchen.portfolio.money.Money;
import name.abuchen.portfolio.money.MoneyCollectors;
import name.abuchen.portfolio.money.Values;
import name.abuchen.portfolio.snapshot.AccountSnapshot;
import name.abuchen.portfolio.snapshot.AssetCategory;
import name.abuchen.portfolio.snapshot.AssetPosition;
import name.abuchen.portfolio.snapshot.ClientPerformanceSnapshot;
import name.abuchen.portfolio.snapshot.ClientPerformanceSnapshot.CategoryType;
import name.abuchen.portfolio.snapshot.ClientSnapshot;
import name.abuchen.portfolio.snapshot.GroupByTaxonomy;
import name.abuchen.portfolio.snapshot.ReportingPeriod;
import name.abuchen.portfolio.snapshot.security.SecurityPerformanceRecord;
import name.abuchen.portfolio.snapshot.security.SecurityPerformanceSnapshot;
@SuppressWarnings("nls")
public class CurrencyTestCase
{
private static TestCurrencyConverter converter = new TestCurrencyConverter();
private static Client client;
private static Security securityEUR;
private static Security securityUSD;
private static Account accountEUR;
private static Account accountUSD;
@BeforeClass
public static void prepare() throws IOException
{
client = ClientFactory.load(SecurityTestCase.class.getResourceAsStream("currency_sample.xml"));
securityEUR = client.getSecurities().stream().filter(s -> s.getName().equals("BASF")).findFirst().get();
securityUSD = client.getSecurities().stream().filter(s -> s.getName().equals("Apple")).findFirst().get();
accountEUR = client.getAccounts().stream().filter(s -> s.getName().equals("Account EUR")).findFirst().get();
accountUSD = client.getAccounts().stream().filter(s -> s.getName().equals("Account USD")).findFirst().get();
}
@Test
public void testClientSnapshot()
{
LocalDate requestedTime = LocalDate.parse("2015-01-16");
ClientSnapshot snapshot = ClientSnapshot.create(client, converter, requestedTime);
AccountSnapshot accountEURsnapshot = lookupAccountSnapshot(snapshot, accountEUR);
assertThat(accountEURsnapshot.getFunds(), is(Money.of(CurrencyUnit.EUR, 1000_00)));
assertThat(accountEURsnapshot.getUnconvertedFunds(), is(Money.of(CurrencyUnit.EUR, 1000_00)));
AccountSnapshot accountUSDsnapshot = lookupAccountSnapshot(snapshot, accountUSD);
assertThat(accountUSDsnapshot.getFunds(), is(Money.of(CurrencyUnit.EUR, Math.round(1000_00 * (1 / 1.1588)))));
assertThat(accountUSDsnapshot.getUnconvertedFunds(), is(Money.of("USD", 1000_00)));
GroupByTaxonomy grouping = snapshot.groupByTaxonomy(client.getTaxonomy("30314ba9-949f-4bf4-944e-6a30802f5190"));
testTotals(snapshot, grouping);
testAssetCategories(grouping);
testUSDAssetPosition(grouping);
}
private void testTotals(ClientSnapshot snapshot, GroupByTaxonomy grouping)
{
assertThat(snapshot.getMonetaryAssets(), is(grouping.getValuation()));
assertThat(grouping.getCategories().map(c -> c.getValuation()).collect(MoneyCollectors.sum(CurrencyUnit.EUR)),
is(grouping.getValuation()));
}
private void testAssetCategories(GroupByTaxonomy grouping)
{
AssetCategory cash = getAssetCategoryByName(grouping, "Barvermögen");
Money cashValuation = Money.of(CurrencyUnit.EUR, 1000_00 + Math.round(1000_00 * (1 / 1.1588)));
assertThat(cash.getValuation(), is(cashValuation));
AssetPosition positionEUR = getAssetPositionByName(grouping, accountEUR.getName());
assertThat(positionEUR.getValuation(), is(Money.of(CurrencyUnit.EUR, 1000_00)));
AssetPosition positionUSD = getAssetPositionByName(grouping, accountUSD.getName());
assertThat(positionUSD.getValuation(), is(Money.of(CurrencyUnit.EUR, Math.round(1000_00 * (1 / 1.1588)))));
Money equityEURvaluation = Money.of(CurrencyUnit.EUR, Math.round(20
* securityEUR.getSecurityPrice(grouping.getDate()).getValue() / Values.Quote.dividerToMoney()));
Money equityUSDvaluation = Money.of("USD", Math.round(10
* securityUSD.getSecurityPrice(grouping.getDate()).getValue() / Values.Quote.dividerToMoney()))
.with(converter.at(grouping.getDate()));
Money equityValuation = Money.of(CurrencyUnit.EUR,
equityEURvaluation.getAmount() + equityUSDvaluation.getAmount());
AssetCategory equity = getAssetCategoryByName(grouping, "Eigenkapital");
assertThat(equity.getValuation(), is(equityValuation));
AssetPosition equityEUR = getAssetPositionByName(grouping, securityEUR.getName());
assertThat(equityEUR.getValuation(), is(equityEURvaluation));
AssetPosition equityUSD = getAssetPositionByName(grouping, securityUSD.getName());
assertThat(equityUSD.getValuation(), is(equityUSDvaluation));
assertThat(grouping.getValuation(), is(cashValuation.add(equityValuation)));
}
private void testUSDAssetPosition(GroupByTaxonomy grouping)
{
AssetPosition equityUSD = getAssetPositionByName(grouping, securityUSD.getName());
assertThat(equityUSD.getPosition().getShares(), is(Values.Share.factorize(10)));
// purchase value must be sum of both purchases:
// the one in EUR account and the one in USD account
// must take the inverse of the exchange used within the transaction
BigDecimal rate = BigDecimal.ONE.divide(BigDecimal.valueOf(0.8237), 10, BigDecimal.ROUND_HALF_DOWN);
assertThat(equityUSD.getPosition().getFIFOPurchaseValue(),
is(Money.of("USD", Math.round(454_60 * rate.doubleValue()) + 571_90)));
// price per share is the total purchase price minus 20 USD fees and
// taxes divided by the number of shares
Money pricePerShare = equityUSD.getPosition().getFIFOPurchaseValue() //
.subtract(Money.of("USD", 20_00)).divide(10);
assertThat(equityUSD.getPosition().getFIFOPurchasePrice(), is(pricePerShare));
// profit loss w/o rounding differences
assertThat(equityUSD.getProfitLoss(), is(equityUSD.getValuation().subtract(equityUSD.getFIFOPurchaseValue())));
assertThat(equityUSD.getPosition().getProfitLoss(), is(equityUSD.getPosition().calculateValue()
.subtract(equityUSD.getPosition().getFIFOPurchaseValue())));
}
@Test
public void testClientPerformanceSnapshot()
{
ReportingPeriod period = new ReportingPeriod.FromXtoY(LocalDate.parse("2015-01-02"),
LocalDate.parse("2015-01-14"));
ClientPerformanceSnapshot performance = new ClientPerformanceSnapshot(client, converter, period);
// calculating the totals is tested with #testClientSnapshot
assertThat(performance.getValue(CategoryType.INITIAL_VALUE), is(Money.of(CurrencyUnit.EUR, 4131_99)));
assertThat(performance.getValue(CategoryType.FINAL_VALUE), is(Money.of(CurrencyUnit.EUR, 4187_94)));
assertThat(performance.getAbsoluteDelta(),
is(performance.getValue(CategoryType.FINAL_VALUE)
.subtract(performance.getValue(CategoryType.TRANSFERS))
.subtract(performance.getValue(CategoryType.INITIAL_VALUE))));
assertThat(performance.getValue(CategoryType.CAPITAL_GAINS)
.add(performance.getValue(CategoryType.CURRENCY_GAINS)),
is(performance.getValue(CategoryType.FINAL_VALUE)
.subtract(performance.getValue(CategoryType.INITIAL_VALUE))));
// compare with result calculated by Excel's XIRR function
assertThat(performance.getPerformanceIRR(), IsCloseTo.closeTo(0.505460984, 0.00000001));
}
@Test
public void testFIFOPurchasePriceWithForex()
{
ClientSnapshot snapshot = ClientSnapshot.create(client, converter, LocalDate.parse("2015-08-09"));
// 1.1. ........ -> 454.60 EUR
// 1.1. 571.90 $ -> 471.05 EUR (exchange rate: 1.2141)
// 3.8. 577.60 $ -> 498.45 EUR (exchange rate: 1.1588)
AssetPosition position = snapshot.getPositionsByVehicle().get(securityUSD);
assertThat(position.getPosition().getShares(), is(Values.Share.factorize(15)));
assertThat(position.getFIFOPurchaseValue(), is(Money.of(CurrencyUnit.EUR, 454_60 + 471_05 + 498_45)));
ReportingPeriod period = new ReportingPeriod.FromXtoY(LocalDate.parse("2014-12-31"),
LocalDate.parse("2015-08-10"));
SecurityPerformanceSnapshot performance = SecurityPerformanceSnapshot.create(client, converter, period);
SecurityPerformanceRecord record = performance.getRecords().stream().filter(r -> r.getSecurity() == securityUSD)
.findAny().get();
assertThat(record.getSharesHeld(), is(Values.Share.factorize(15)));
assertThat(record.getFifoCost(), is(Money.of(CurrencyUnit.EUR, 454_60 + 471_05 + 498_45)));
}
private AccountSnapshot lookupAccountSnapshot(ClientSnapshot snapshot, Account account)
{
for (AccountSnapshot as : snapshot.getAccounts())
if (account.equals(as.getAccount()))
return as;
return null;
}
private AssetCategory getAssetCategoryByName(GroupByTaxonomy grouping, String label)
{
return grouping.asList().stream().filter(category -> label.equals(category.getClassification().getName()))
.findFirst().get();
}
private AssetPosition getAssetPositionByName(GroupByTaxonomy grouping, String label)
{
return grouping.asList().stream().flatMap(category -> category.getPositions().stream())
.filter(position -> label.equals(position.getDescription())).findFirst().get();
}
}