package name.abuchen.portfolio.snapshot; import java.time.LocalDate; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.stream.Stream; import name.abuchen.portfolio.Messages; import name.abuchen.portfolio.model.Classification; import name.abuchen.portfolio.model.Classification.Assignment; import name.abuchen.portfolio.model.InvestmentVehicle; import name.abuchen.portfolio.model.Taxonomy; import name.abuchen.portfolio.money.CurrencyConverter; import name.abuchen.portfolio.money.Money; import name.abuchen.portfolio.money.MoneyCollectors; public final class GroupByTaxonomy { private static final class Item { private SecurityPosition position; private int weight; private Item(SecurityPosition position) { this.position = position; } } private final Taxonomy taxonomy; private final CurrencyConverter converter; private final LocalDate date; private final Money valuation; private final List<AssetCategory> categories = new ArrayList<>(); private GroupByTaxonomy(Taxonomy taxonomy, CurrencyConverter converter, LocalDate date, Money valuation) { this.taxonomy = taxonomy; this.converter = converter; this.date = date; this.valuation = valuation; } /* package */GroupByTaxonomy(Taxonomy taxonomy, ClientSnapshot snapshot) { this(taxonomy, snapshot.getCurrencyConverter(), snapshot.getTime(), snapshot.getMonetaryAssets()); Map<InvestmentVehicle, Item> vehicle2position = new HashMap<>(); // cash for (AccountSnapshot account : snapshot.getAccounts()) { if (account.getFunds().isZero()) continue; vehicle2position.put(account.getAccount(), new Item(new SecurityPosition(account))); } // portfolio if (snapshot.getJointPortfolio() != null) { for (SecurityPosition pos : snapshot.getJointPortfolio().getPositions()) vehicle2position.put(pos.getSecurity(), new Item(pos)); } doGrouping(vehicle2position); } /* package */public GroupByTaxonomy(Taxonomy taxonomy, PortfolioSnapshot snapshot) { this(taxonomy, snapshot.getCurrencyConverter(), snapshot.getTime(), snapshot.getValue()); Map<InvestmentVehicle, Item> vehicle2position = new HashMap<>(); for (SecurityPosition pos : snapshot.getPositions()) vehicle2position.put(pos.getSecurity(), new Item(pos)); doGrouping(vehicle2position); } private void doGrouping(final Map<InvestmentVehicle, Item> vehicle2position) { if (taxonomy != null) { createCategoriesAndAllocate(vehicle2position); sortCategoriesByRank(); } allocateLeftOvers(vehicle2position); } private void createCategoriesAndAllocate(final Map<InvestmentVehicle, Item> vehicle2position) { for (Classification classification : taxonomy.getRoot().getChildren()) { final Map<InvestmentVehicle, Item> vehicle2item = new HashMap<>(); // first: assign items to categories // item.weight records both the weight // (a) already assigned to any category // (b) assigned to this category classification.accept(new Taxonomy.Visitor() { @Override public void visit(Classification classification, Assignment assignment) { Item item = vehicle2position.get(assignment.getInvestmentVehicle()); if (item != null) { item.weight += assignment.getWeight(); // record (a) SecurityPosition position = item.position; if (assignment.getWeight() == Classification.ONE_HUNDRED_PERCENT) { vehicle2item.put(assignment.getInvestmentVehicle(), item); } else { Item other = vehicle2item.get(assignment.getInvestmentVehicle()); if (other == null) { other = new Item(position); vehicle2item.put(assignment.getInvestmentVehicle(), other); } // record (b) into the copy other.weight += assignment.getWeight(); } } } }); // second: create asset category and positions if (!vehicle2item.isEmpty()) { AssetCategory category = new AssetCategory(classification, converter, date, valuation); categories.add(category); for (Entry<InvestmentVehicle, Item> entry : vehicle2item.entrySet()) { Item item = entry.getValue(); SecurityPosition position = item.position; if (item.weight != Classification.ONE_HUNDRED_PERCENT) position = SecurityPosition.split(position, item.weight); category.addPosition(new AssetPosition(position, converter, date, getValuation())); } // sort positions by name Collections.sort(category.getPositions(), new AssetPosition.ByDescription()); } } } private void sortCategoriesByRank() { Collections.sort(categories, new Comparator<AssetCategory>() { @Override public int compare(AssetCategory o1, AssetCategory o2) { int rank1 = o1.getClassification().getRank(); int rank2 = o2.getClassification().getRank(); return rank1 < rank2 ? -1 : rank1 == rank2 ? 0 : 1; } }); } private void allocateLeftOvers(final Map<InvestmentVehicle, Item> vehicle2position) { Classification classification = new Classification(null, Classification.UNASSIGNED_ID, Messages.LabelWithoutClassification); AssetCategory unassigned = new AssetCategory(classification, converter, date, getValuation()); for (Entry<InvestmentVehicle, Item> entry : vehicle2position.entrySet()) { Item item = entry.getValue(); if (item.weight < Classification.ONE_HUNDRED_PERCENT) { SecurityPosition position = item.position; if (item.weight != 0) position = SecurityPosition.split(position, Classification.ONE_HUNDRED_PERCENT - item.weight); unassigned.addPosition(new AssetPosition(position, converter, date, getValuation())); } } if (!unassigned.getPositions().isEmpty()) categories.add(unassigned); } public LocalDate getDate() { return date; } public Money getValuation() { return valuation; } public Money getFIFOPurchaseValue() { return categories.stream().map(AssetCategory::getFIFOPurchaseValue) .collect(MoneyCollectors.sum(converter.getTermCurrency())); } public Money getProfitLoss() { return categories.stream().map(AssetCategory::getProfitLoss) .collect(MoneyCollectors.sum(converter.getTermCurrency())); } public List<AssetCategory> asList() { return categories; } public Stream<AssetCategory> getCategories() { return categories.stream(); } /* package */AssetCategory byClassification(Classification classification) { for (AssetCategory category : categories) { if (category.getClassification().equals(classification)) return category; } return null; } }