package name.abuchen.portfolio.snapshot; import java.util.List; import name.abuchen.portfolio.model.Account; import name.abuchen.portfolio.model.AccountTransaction; import name.abuchen.portfolio.model.Classification; import name.abuchen.portfolio.model.Classification.Assignment; import name.abuchen.portfolio.model.Client; import name.abuchen.portfolio.model.InvestmentVehicle; import name.abuchen.portfolio.model.Portfolio; import name.abuchen.portfolio.model.PortfolioTransaction; import name.abuchen.portfolio.model.Security; import name.abuchen.portfolio.model.Taxonomy.Visitor; import name.abuchen.portfolio.model.Transaction.Unit; import name.abuchen.portfolio.money.CurrencyConverter; import name.abuchen.portfolio.money.Money; /* package */final class ClassificationIndex { private ClassificationIndex() {} /* package */static PerformanceIndex calculate(final Client client, CurrencyConverter converter, Classification classification, ReportingPeriod reportInterval, List<Exception> warnings) { final Client pseudoClient = new Client(); classification.accept(new Visitor() { @Override public void visit(Classification classification, Assignment assignment) { InvestmentVehicle vehicle = assignment.getInvestmentVehicle(); if (vehicle instanceof Security) addSecurity(pseudoClient, client, (Security) vehicle, assignment.getWeight()); else if (vehicle instanceof Account) addAccount(pseudoClient, (Account) vehicle, assignment.getWeight()); } }); return PerformanceIndex.forClient(pseudoClient, converter, reportInterval, warnings); } private static void addSecurity(Client pseudoClient, Client client, Security security, int weight) { // if a security has no currency code, it must be an index and must not // have transactions after all if (security.getCurrencyCode() == null) return; Account pseudoAccount = new Account(); pseudoAccount.setName(""); //$NON-NLS-1$ pseudoAccount.setCurrencyCode(security.getCurrencyCode()); pseudoClient.addAccount(pseudoAccount); Portfolio pseudoPortfolio = new Portfolio(); pseudoPortfolio.setReferenceAccount(pseudoAccount); pseudoClient.addPortfolio(pseudoPortfolio); pseudoClient.addSecurity(security); for (Portfolio p : client.getPortfolios()) { for (PortfolioTransaction t : p.getTransactions()) { if (!security.equals(t.getSecurity())) continue; PortfolioTransaction pseudo = new PortfolioTransaction(); pseudo.setDate(t.getDate()); pseudo.setCurrencyCode(t.getCurrencyCode()); pseudo.setSecurity(security); pseudo.setShares(value(t.getShares(), weight)); // convert type to the appropriate delivery type (either inbound // or outbound delivery) pseudo.setType(convertTypeToDelivery(t.getType())); // calculation is without taxes -> remove any taxes & adapt // total accordingly long taxes = value(t.getUnitSum(Unit.Type.TAX).getAmount(), weight); long amount = value(t.getAmount(), weight); pseudo.setAmount(pseudo.getType() == PortfolioTransaction.Type.DELIVERY_INBOUND ? amount - taxes : amount + taxes); // copy all units (except for taxes) over to the pseudo // transaction t.getUnits().filter(u -> u.getType() != Unit.Type.TAX).forEach(u -> pseudo.addUnit(value(u, weight))); pseudoPortfolio.addTransaction(pseudo); } } for (Account a : client.getAccounts()) { for (AccountTransaction t : a.getTransactions()) { if (!security.equals(t.getSecurity())) continue; switch (t.getType()) { case DIVIDENDS: long taxes = value(t.getUnitSum(Unit.Type.TAX).getAmount(), weight); long amount = value(t.getAmount(), weight); pseudoAccount.addTransaction(new AccountTransaction(t.getDate(), t.getCurrencyCode(), amount + taxes, t.getSecurity(), t.getType())); pseudoAccount.addTransaction(new AccountTransaction(t.getDate(), t.getCurrencyCode(), amount + taxes, t.getSecurity(), AccountTransaction.Type.REMOVAL)); break; case TAX_REFUND: // ignore taxes when calculating performance of // securities case BUY: case TRANSFER_IN: case SELL: case TRANSFER_OUT: case DEPOSIT: case REMOVAL: case INTEREST: case INTEREST_CHARGE: case TAXES: case FEES: case FEES_REFUND: // do nothing break; default: throw new UnsupportedOperationException(); } } } } private static PortfolioTransaction.Type convertTypeToDelivery(PortfolioTransaction.Type type) { switch (type) { case BUY: case TRANSFER_IN: case DELIVERY_INBOUND: return PortfolioTransaction.Type.DELIVERY_INBOUND; case SELL: case TRANSFER_OUT: case DELIVERY_OUTBOUND: return PortfolioTransaction.Type.DELIVERY_OUTBOUND; default: throw new UnsupportedOperationException(); } } private static void addAccount(Client pseudoClient, Account account, int weight) { Account pseudoAccount = new Account(); pseudoAccount.setCurrencyCode(account.getCurrencyCode()); pseudoAccount.setName(account.getName()); pseudoClient.addAccount(pseudoAccount); for (AccountTransaction t : account.getTransactions()) { long amount = value(t.getAmount(), weight); switch (t.getType()) { case SELL: case TRANSFER_IN: case DIVIDENDS: pseudoAccount.addTransaction(new AccountTransaction(t.getDate(), t.getCurrencyCode(), amount, null, AccountTransaction.Type.DEPOSIT)); break; case BUY: case TRANSFER_OUT: pseudoAccount.addTransaction(new AccountTransaction(t.getDate(), t.getCurrencyCode(), amount, null, AccountTransaction.Type.REMOVAL)); break; case TAX_REFUND: if (t.getSecurity() != null) { pseudoAccount.addTransaction(new AccountTransaction(t.getDate(), t.getCurrencyCode(), amount, null, AccountTransaction.Type.DEPOSIT)); break; } // fall through if tax refund applies to account case DEPOSIT: case REMOVAL: case INTEREST: case INTEREST_CHARGE: case TAXES: case FEES: case FEES_REFUND: if (weight != Classification.ONE_HUNDRED_PERCENT) pseudoAccount.addTransaction(new AccountTransaction(t.getDate(), t.getCurrencyCode(), amount, null, t.getType())); else pseudoAccount.addTransaction(t); break; default: throw new UnsupportedOperationException(); } } } private static Unit value(Unit unit, int weight) { if (weight == Classification.ONE_HUNDRED_PERCENT) return unit; else return new Unit(unit.getType(), Money.of(unit.getAmount().getCurrencyCode(), value(unit.getAmount().getAmount(), weight))); } private static long value(long value, int weight) { if (weight == Classification.ONE_HUNDRED_PERCENT) return value; else return Math.round(value * weight / (double) Classification.ONE_HUNDRED_PERCENT); } }