package name.abuchen.portfolio.snapshot.security; import java.time.LocalDate; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.ResourceBundle; import name.abuchen.portfolio.math.Risk; import name.abuchen.portfolio.model.Adaptable; import name.abuchen.portfolio.model.Annotated; import name.abuchen.portfolio.model.Attributable; import name.abuchen.portfolio.model.Client; import name.abuchen.portfolio.model.InvestmentVehicle; import name.abuchen.portfolio.model.Named; import name.abuchen.portfolio.model.Security; import name.abuchen.portfolio.model.SecurityPrice; import name.abuchen.portfolio.model.Transaction; import name.abuchen.portfolio.money.CurrencyConverter; import name.abuchen.portfolio.money.Money; import name.abuchen.portfolio.money.MutableMoney; import name.abuchen.portfolio.money.Quote; import name.abuchen.portfolio.money.Values; import name.abuchen.portfolio.snapshot.PerformanceIndex; import name.abuchen.portfolio.snapshot.ReportingPeriod; public final class SecurityPerformanceRecord implements Adaptable { public enum Periodicity { UNKNOWN, NONE, INDEFINITE, ANNUAL, SEMIANNUAL, QUARTERLY, IRREGULAR; private static final ResourceBundle RESOURCES = ResourceBundle .getBundle("name.abuchen.portfolio.snapshot.labels"); //$NON-NLS-1$ @Override public String toString() { return RESOURCES.getString("dividends." + name()); //$NON-NLS-1$ } } private final Security security; private List<Transaction> transactions = new ArrayList<>(); /** * internal rate of return of security {@link #calculateIRR()} */ private double irr; /** * True time-weighted rate of return * {@link #calculatePerformance(ReportingPeriod)} */ private double twror; /** * Max Drawdown and Max Drawdown Duration * {@link #calculatePerformance(ReportingPeriod)} */ private Risk.Drawdown drawdown; /** * Volatility and semi-volatility * {@link #calculatePerformance(ReportingPeriod)} */ private Risk.Volatility volatility; /** * delta = market value + sells + dividends - purchase costs * {@link #calculateDelta()} */ private Money delta; /** * deltaPercent = delta / purchase costs + buy {@link #calculateDelta()} */ private double deltaPercent; /** * market value of holdings at end of period * {@link #addTransaction(Transaction)} */ private Money marketValue; /** * Latest quote */ private SecurityPrice quote; /** * fifo cost of shares held {@link #calculateFifoCosts()} */ private Money fifoCost; /** * fees paid */ private Money fees; /** * taxes paid */ private Money taxes; /** * shares held {@link #calculateFifoCosts()} */ private long sharesHeld; /** * cost per shares held {@link #calculateFifoCosts()} */ private Quote fifoCostPerSharesHeld; /** * sum of all dividend payments {@link #calculateDividends()} */ private Money sumOfDividends; /** * number of dividend events during reporting period * {@link #calculateDividends()} */ private int dividendEventCount; /** * last dividend payment in reporting period {@link #calculateDividends()} */ private LocalDate lastDividendPayment; /** * periodicity of dividend payments {@link #calculateDividends()} */ private Periodicity periodicity = Periodicity.UNKNOWN; /** * market value - fifo cost of shares held {@link #calculateFifoCosts()} */ private Money capitalGainsOnHoldings; /** * {@link capitalGainsOnHoldings} in percent */ private double capitalGainsOnHoldingsPercent; /* package */ SecurityPerformanceRecord(Security security) { this.security = security; } public Security getSecurity() { return security; } public String getSecurityName() { return getSecurity().getName(); } public String getNote() { return getSecurity().getNote(); } public double getIrr() { return irr; } public double getTrueTimeWeightedRateOfReturn() { return twror; } public double getMaxDrawdown() { return drawdown.getMaxDrawdown(); } public long getMaxDrawdownDuration() { return drawdown.getMaxDrawdownDuration().getDays(); } public double getVolatility() { return volatility.getStandardDeviation(); } public double getSemiVolatility() { return volatility.getSemiDeviation(); } public Money getDelta() { return delta; } public double getDeltaPercent() { return deltaPercent; } public Money getMarketValue() { return marketValue; } public Quote getQuote() { return Quote.of(security.getCurrencyCode(), quote.getValue()); } public SecurityPrice getLatestSecurityPrice() { return quote; } public Money getFifoCost() { return fifoCost; } public Money getCapitalGainsOnHoldings() { return capitalGainsOnHoldings; } public double getCapitalGainsOnHoldingsPercent() { return capitalGainsOnHoldingsPercent; } public Money getFees() { return fees; } public Money getTaxes() { return taxes; } public long getSharesHeld() { return sharesHeld; } public Quote getFifoCostPerSharesHeld() { return fifoCostPerSharesHeld; } public Money getSumOfDividends() { return sumOfDividends; } public int getDividendEventCount() { return dividendEventCount; } public LocalDate getLastDividendPayment() { return lastDividendPayment; } public Periodicity getPeriodicity() { return periodicity; } public int getPeriodicitySort() { return periodicity.ordinal(); } public double getTotalRateOfReturnDiv() { return sharesHeld > 0 ? (double) sumOfDividends.getAmount() / (double) fifoCost.getAmount() : 0; } public List<Transaction> getTransactions() { return transactions; } @Override public <T> T adapt(Class<T> type) { if (type == Security.class) return type.cast(security); else if (type == Attributable.class) return type.cast(security); else if (type == Named.class) return type.cast(security); else if (type == InvestmentVehicle.class) return type.cast(security); else if (type == Annotated.class) return type.cast(security); else return null; } /* package */ void addTransaction(Transaction t) { transactions.add(t); } /* package */ void calculate(Client client, CurrencyConverter converter, ReportingPeriod period) { Collections.sort(transactions, new TransactionComparator()); if (!transactions.isEmpty()) { calculateMarketValue(converter, period); calculateIRR(converter); calculateTTWROR(client, converter, period); calculateDelta(converter); calculateFifoCosts(converter); calculateDividends(converter); } } private void calculateMarketValue(CurrencyConverter converter, ReportingPeriod period) { MutableMoney mv = MutableMoney.of(converter.getTermCurrency()); for (Transaction t : transactions) if (t instanceof DividendFinalTransaction) mv.add(t.getMonetaryAmount().with(converter.at(t.getDate()))); this.marketValue = mv.toMoney(); this.quote = security.getSecurityPrice(period.getEndDate()); } private void calculateIRR(CurrencyConverter converter) { this.irr = Calculation.perform(IRRCalculation.class, converter, transactions).getIRR(); } private void calculateTTWROR(Client client, CurrencyConverter converter, ReportingPeriod period) { PerformanceIndex index = PerformanceIndex.forInvestment(client, converter, security, period, new ArrayList<Exception>()); this.twror = index.getFinalAccumulatedPercentage(); this.drawdown = index.getDrawdown(); this.volatility = index.getVolatility(); } private void calculateDelta(CurrencyConverter converter) { DeltaCalculation calculation = Calculation.perform(DeltaCalculation.class, converter, transactions); this.delta = calculation.getDelta(); this.deltaPercent = calculation.getDeltaPercent(); } private void calculateFifoCosts(CurrencyConverter converter) { CostCalculation cost = Calculation.perform(CostCalculation.class, converter, transactions); this.fifoCost = cost.getFifoCost(); this.sharesHeld = cost.getSharesHeld(); Money netFifoCost = cost.getNetFifoCost(); this.fifoCostPerSharesHeld = Quote.of(netFifoCost.getCurrencyCode(), Math.round(netFifoCost.getAmount() * Values.Share.factor() * Values.Quote.factorToMoney() / (double) sharesHeld)); this.fees = cost.getFees(); this.taxes = cost.getTaxes(); this.capitalGainsOnHoldings = marketValue.subtract(fifoCost); // avoid NaN for securities with no holdings if (marketValue.getAmount() == 0L && fifoCost.getAmount() == 0L) this.capitalGainsOnHoldingsPercent = 0d; else this.capitalGainsOnHoldingsPercent = ((double) marketValue.getAmount() / (double) fifoCost.getAmount()) - 1; } private void calculateDividends(CurrencyConverter converter) { DividendCalculation dividends = Calculation.perform(DividendCalculation.class, converter, transactions); this.sumOfDividends = dividends.getSum(); this.dividendEventCount = dividends.getNumOfEvents(); this.lastDividendPayment = dividends.getLastDividendPayment(); this.periodicity = dividends.getPeriodicity(); } }