package name.abuchen.portfolio.snapshot; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Objects; import name.abuchen.portfolio.model.Classification; import name.abuchen.portfolio.model.InvestmentVehicle; 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; import name.abuchen.portfolio.model.Transaction.Unit; import name.abuchen.portfolio.money.CurrencyConverter; import name.abuchen.portfolio.money.Money; import name.abuchen.portfolio.money.Values; public class SecurityPosition { private static class Record { private Money purchasePrice; private Money purchaseValue; public static Record calculate(CurrencyConverter converter, SecurityPosition position) { Record answer = new Record(); answer.calculatePurchaseValuePrice(converter, answer.filter(position.transactions)); return answer; } public Money getPurchasePrice() { return purchasePrice; } public Money getPurchaseValue() { return purchaseValue; } /** * Remove matching transfer_in / transfer_out transactions */ private List<PortfolioTransaction> filter(List<PortfolioTransaction> input) { List<PortfolioTransaction> inbound = new ArrayList<>(); for (PortfolioTransaction t : input) if (t.getType() == Type.TRANSFER_IN) inbound.add(t); if (inbound.isEmpty()) return input; List<PortfolioTransaction> output = new ArrayList<>(input.size()); TransactionLoop: for (PortfolioTransaction t : input) { if (t.getType() == Type.TRANSFER_IN) { continue; } else if (t.getType() == Type.TRANSFER_OUT) { Iterator<PortfolioTransaction> iter = inbound.iterator(); while (iter.hasNext()) { PortfolioTransaction t_inbound = iter.next(); if (t_inbound.getDate().equals(t.getDate()) && t_inbound.getShares() == t.getShares()) { iter.remove(); continue TransactionLoop; } } output.add(t); } else { output.add(t); } } output.addAll(inbound); return output; } private void calculatePurchaseValuePrice(CurrencyConverter converter, List<PortfolioTransaction> input) { Collections.sort(input, new Transaction.ByDate()); long sharesSold = 0; for (PortfolioTransaction t : input) { if (t.getType() == Type.TRANSFER_OUT || t.getType() == Type.SELL || t.getType() == Type.DELIVERY_OUTBOUND) sharesSold += t.getShares(); } long sharesBought = 0; long grossInvestment = 0; long netInvestment = 0; for (PortfolioTransaction t : input) { if (t.getType() == Type.TRANSFER_IN || t.getType() == Type.BUY || t.getType() == Type.DELIVERY_INBOUND) { long bought = t.getShares(); if (sharesSold > 0) { sharesSold -= bought; if (sharesSold < 0) bought = -sharesSold; else bought = 0; } if (bought > 0) { long grossAmount; long netAmount; grossAmount = t.getMonetaryAmount(converter).getAmount(); netAmount = t.getGrossValue(converter).getAmount(); sharesBought += bought; grossInvestment += grossAmount * bought / (double) t.getShares(); netInvestment += netAmount * bought / (double) t.getShares(); } } } this.purchasePrice = Money.of(converter.getTermCurrency(), sharesBought > 0 ? Math.round((netInvestment * Values.Share.factor()) / (double) sharesBought) : 0); this.purchaseValue = Money.of(converter.getTermCurrency(), grossInvestment); } } private final InvestmentVehicle investment; private final CurrencyConverter converter; private final SecurityPrice price; private final long shares; private final List<PortfolioTransaction> transactions; private transient Map<String, Record> currency2record = new HashMap<String, Record>() { private static final long serialVersionUID = 1L; @Override public Record get(Object key) { return super.computeIfAbsent((String) key, currency -> Record.calculate(converter.with(currency), SecurityPosition.this)); } }; private SecurityPosition(InvestmentVehicle investment, CurrencyConverter converter, SecurityPrice price, long shares, List<PortfolioTransaction> transactions) { this.investment = investment; this.converter = converter; this.price = price; this.shares = shares; this.transactions = transactions; } public SecurityPosition(AccountSnapshot snapshot) { Objects.requireNonNull(snapshot); Objects.requireNonNull(snapshot.getAccount()); this.investment = snapshot.getAccount(); this.converter = snapshot.getCurrencyConverter().with(investment.getCurrencyCode()); this.price = new SecurityPrice(snapshot.getTime(), snapshot.getUnconvertedFunds().getAmount() * Values.Quote.factorToMoney()); this.shares = Values.Share.factor(); this.transactions = new ArrayList<>(); } public SecurityPosition(Security security, CurrencyConverter converter, SecurityPrice price, List<PortfolioTransaction> transactions) { Objects.requireNonNull(security); Objects.requireNonNull(converter); Objects.requireNonNull(price); this.investment = security; this.converter = converter.with(investment.getCurrencyCode()); this.price = price; this.shares = transactions.stream().mapToLong(t -> { switch (t.getType()) { case BUY: case TRANSFER_IN: case DELIVERY_INBOUND: return t.getShares(); case SELL: case TRANSFER_OUT: case DELIVERY_OUTBOUND: return -t.getShares(); default: throw new UnsupportedOperationException(); } }).sum(); this.transactions = new ArrayList<>(transactions); } public Security getSecurity() { return investment instanceof Security ? (Security) investment : null; } public InvestmentVehicle getInvestmentVehicle() { return investment; } public SecurityPrice getPrice() { return price; } public long getShares() { return shares; } public Money calculateValue() { if (price == null) return Money.of(investment.getCurrencyCode(), 0); double marketValue = shares * price.getValue() / Values.Share.divider() / Values.Quote.dividerToMoney(); return Money.of(investment.getCurrencyCode(), Math.round(marketValue)); } public Money getFIFOPurchasePrice() { return currency2record.get(investment.getCurrencyCode()).getPurchasePrice(); } public Money getFIFOPurchaseValue() { return currency2record.get(investment.getCurrencyCode()).getPurchaseValue(); } public Money getFIFOPurchaseValue(String currencyCode) { return currency2record.get(currencyCode).getPurchaseValue(); } public Money getProfitLoss() { if (!(investment instanceof Security)) return Money.of(investment.getCurrencyCode(), 0); Record record = currency2record.get(investment.getCurrencyCode()); return calculateValue().subtract(record.getPurchaseValue()); } public static SecurityPosition split(SecurityPosition position, int weight) { List<PortfolioTransaction> splitTransactions = new ArrayList<>(position.transactions.size()); for (PortfolioTransaction t : position.transactions) { PortfolioTransaction t2 = new PortfolioTransaction(); t2.setDate(t.getDate()); t2.setSecurity(t.getSecurity()); t2.setType(t.getType()); t2.setCurrencyCode(t.getCurrencyCode()); t2.setAmount(Math.round(t.getAmount() * weight / (double) Classification.ONE_HUNDRED_PERCENT)); t2.setShares(Math.round(t.getShares() * weight / (double) Classification.ONE_HUNDRED_PERCENT)); t.getUnits().forEach(u -> { long splitAmount = Math.round( u.getAmount().getAmount() * weight / (double) Classification.ONE_HUNDRED_PERCENT); if (u.getForex() == null) { t2.addUnit(new Unit(u.getType(), // Money.of(u.getAmount().getCurrencyCode(), splitAmount))); } else { long splitForex = Math.round( u.getForex().getAmount() * weight / (double) Classification.ONE_HUNDRED_PERCENT); t2.addUnit(new Unit(u.getType(), // Money.of(u.getAmount().getCurrencyCode(), splitAmount), Money.of(u.getForex().getCurrencyCode(), splitForex), // u.getExchangeRate())); } }); splitTransactions.add(t2); } return new SecurityPosition(position.investment, position.converter, position.price, Math.round(position.shares * weight / (double) Classification.ONE_HUNDRED_PERCENT), splitTransactions); } }