package name.abuchen.portfolio.snapshot; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStreamWriter; import java.io.Writer; import java.nio.charset.StandardCharsets; import java.time.LocalDate; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.function.Predicate; import java.util.function.ToLongBiFunction; import org.apache.commons.csv.CSVPrinter; import org.apache.commons.csv.CSVStrategy; import name.abuchen.portfolio.Messages; import name.abuchen.portfolio.math.Risk.Drawdown; import name.abuchen.portfolio.math.Risk.Volatility; 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.Portfolio; import name.abuchen.portfolio.model.PortfolioTransaction; import name.abuchen.portfolio.model.Security; import name.abuchen.portfolio.money.CurrencyConverter; import name.abuchen.portfolio.money.Money; import name.abuchen.portfolio.money.Values; import name.abuchen.portfolio.snapshot.filter.PortfolioClientFilter; import name.abuchen.portfolio.util.Interval; import name.abuchen.portfolio.util.TradeCalendar; public class PerformanceIndex { private final Client client; private final CurrencyConverter converter; private final ReportingPeriod reportInterval; protected LocalDate[] dates; protected long[] totals; protected long[] transferals; protected long[] taxes; protected long[] dividends; protected long[] interest; protected long[] interestCharge; protected double[] accumulated; protected double[] delta; private Drawdown drawdown; private Volatility volatility; private ClientPerformanceSnapshot performanceSnapshot; /* package */ PerformanceIndex(Client client, CurrencyConverter converter, ReportingPeriod reportInterval) { this.client = client; this.converter = converter; this.reportInterval = reportInterval; } public static ClientIndex forClient(Client client, CurrencyConverter converter, ReportingPeriod reportInterval, List<Exception> warnings) { ClientIndex index = new ClientIndex(client, converter, reportInterval); index.calculate(warnings); return index; } public static PerformanceIndex forAccount(Client client, CurrencyConverter converter, Account account, ReportingPeriod reportInterval, List<Exception> warnings) { Client pseudoClient = new PortfolioClientFilter(Collections.emptyList(), Arrays.asList(account)).filter(client); return PerformanceIndex.forClient(pseudoClient, converter, reportInterval, warnings); } public static PerformanceIndex forPortfolio(Client client, CurrencyConverter converter, Portfolio portfolio, ReportingPeriod reportInterval, List<Exception> warnings) { Client pseudoClient = new PortfolioClientFilter(portfolio).filter(client); return PerformanceIndex.forClient(pseudoClient, converter, reportInterval, warnings); } public static PerformanceIndex forPortfolioPlusAccount(Client client, CurrencyConverter converter, Portfolio portfolio, ReportingPeriod reportInterval, List<Exception> warnings) { Client pseudoClient = new PortfolioClientFilter(portfolio, portfolio.getReferenceAccount()).filter(client); return PerformanceIndex.forClient(pseudoClient, converter, reportInterval, warnings); } public static PerformanceIndex forClassification(Client client, CurrencyConverter converter, Classification classification, ReportingPeriod reportInterval, List<Exception> warnings) { return ClassificationIndex.calculate(client, converter, classification, reportInterval, warnings); } public static PerformanceIndex forInvestment(Client client, CurrencyConverter converter, Security security, ReportingPeriod reportInterval, List<Exception> warnings) { Classification classification = new Classification(null, null); classification.addAssignment(new Assignment(security)); return forClassification(client, converter, classification, reportInterval, warnings); } public static PerformanceIndex forSecurity(PerformanceIndex clientIndex, Security security) { SecurityIndex index = new SecurityIndex(clientIndex.getClient(), clientIndex.getCurrencyConverter(), clientIndex.getReportInterval()); index.calculate(clientIndex, security); return index; } public static PerformanceIndex forConsumerPriceIndex(PerformanceIndex clientIndex) { CPIIndex index = new CPIIndex(clientIndex.getClient(), clientIndex.getCurrencyConverter(), clientIndex.getReportInterval()); index.calculate(clientIndex); return index; } /* package */ Client getClient() { return client; } public ReportingPeriod getReportInterval() { return reportInterval; } public CurrencyConverter getCurrencyConverter() { return converter; } /** * Returns the interval for which data exists. Might be different from * {@link #getReportInterval()} if the reporting interval extends into the * future. */ public Interval getActualInterval() { return Interval.of(dates[0], dates[dates.length - 1]); } public LocalDate[] getDates() { return dates; } public double[] getAccumulatedPercentage() { return accumulated; } /** * Returns the final accumulated performance value for this performance * reporting period. It is the last element of the array returned by * {@link #getAccumulatedPercentage}. */ public double getFinalAccumulatedPercentage() { return accumulated != null ? accumulated[accumulated.length - 1] : 0; } public double[] getDeltaPercentage() { return delta; } public long[] getTotals() { return totals; } public long[] getTransferals() { return transferals; } public Drawdown getDrawdown() { if (drawdown == null) drawdown = new Drawdown(accumulated, dates); return drawdown; } public Volatility getVolatility() { if (volatility == null) volatility = new Volatility(delta, filterReturnsForVolatilityCalculation()); return volatility; } /** * The volatility calculation must exclude returns * <ul> * <li>on first day (because on the first day the return is always zero as * there is no previous day to compare to)</li> * <li>on days where there are no holdings including the first day it was * bought (for example if the investment vehicle was bought in the middle of * the reporting period)</li> * <li>on weekends or public holidays</li> * </ul> */ private Predicate<Integer> filterReturnsForVolatilityCalculation() { TradeCalendar calendar = new TradeCalendar(); return index -> index > 0 && totals[index] != 0 && totals[index - 1] != 0 && !calendar.isHoliday(dates[index]); } public ClientPerformanceSnapshot getClientPerformanceSnapshot() { if (performanceSnapshot == null) performanceSnapshot = new ClientPerformanceSnapshot(client, converter, reportInterval); return performanceSnapshot; } public long[] getTaxes() { return taxes; } public long[] getDividends() { return dividends; } public long[] getInterest() { return interest; } public long[] getInterestCharge() { return interestCharge; } /** * Calculates the absolute invested capital, i.e. starting with the first * transaction recorded for the client. */ public long[] calculateAbsoluteInvestedCapital() { ToLongBiFunction<Money, LocalDate> convertIfNecessary = (amount, date) -> { if (amount.getCurrencyCode().equals(getCurrencyConverter().getTermCurrency())) return amount.getAmount(); else return getCurrencyConverter().convert(date, amount).getAmount(); }; long startValue = 0; Interval interval = getActualInterval(); for (Account account : getClient().getAccounts()) startValue += account.getTransactions() // .stream() // .filter(t -> t.getType() == AccountTransaction.Type.DEPOSIT || t.getType() == AccountTransaction.Type.REMOVAL) .filter(t -> t.getDate().isBefore(interval.getStart())) // .mapToLong(t -> { if (t.getType() == AccountTransaction.Type.DEPOSIT) return convertIfNecessary.applyAsLong(t.getMonetaryAmount(), t.getDate()); else if (t.getType() == AccountTransaction.Type.REMOVAL) return -convertIfNecessary.applyAsLong(t.getMonetaryAmount(), t.getDate()); else return 0; }).sum(); for (Portfolio portfolio : getClient().getPortfolios()) startValue += portfolio.getTransactions() // .stream() // .filter(t -> t.getType() == PortfolioTransaction.Type.DELIVERY_INBOUND || t.getType() == PortfolioTransaction.Type.DELIVERY_OUTBOUND) .filter(t -> t.getDate().isBefore(interval.getStart())) // .mapToLong(t -> { if (t.getType() == PortfolioTransaction.Type.DELIVERY_INBOUND) return convertIfNecessary.applyAsLong(t.getMonetaryAmount(), t.getDate()); else if (t.getType() == PortfolioTransaction.Type.DELIVERY_OUTBOUND) return -convertIfNecessary.applyAsLong(t.getMonetaryAmount(), t.getDate()); else return 0; }).sum(); return calculateInvestedCapital(startValue); } /** * Calculates the invested capital for the given reporting period. */ public long[] calculateInvestedCapital() { return calculateInvestedCapital(totals[0]); } private long[] calculateInvestedCapital(long startValue) { long[] investedCapital = new long[transferals.length]; investedCapital[0] = startValue; long current = startValue; for (int ii = 1; ii < investedCapital.length; ii++) current = investedCapital[ii] = current + transferals[ii]; return investedCapital; } /** * Calculates the delta as difference between the current valuation and the * invested capital since the first transaction. */ public long[] calculateAbsoluteDelta() { return calculateDelta(calculateAbsoluteInvestedCapital()); } /** * Calculates the delta as difference between the total valuation and the * invested capital for the given reporting period. */ public long[] calculateDelta() { return calculateDelta(calculateInvestedCapital()); } private long[] calculateDelta(long[] investedCapital) { long[] answer = investedCapital; for (int ii = 0; ii < answer.length; ii++) answer[ii] = totals[ii] - answer[ii]; return answer; } public Optional<LocalDate> getFirstDataPoint() { for (int ii = 0; ii < totals.length; ii++) { if (totals[ii] != 0) return Optional.of(dates[ii]); } return Optional.empty(); } public void exportTo(File file) throws IOException { exportTo(file, index -> true); } public void exportVolatilityData(File file) throws IOException { exportTo(file, filterReturnsForVolatilityCalculation()); } private void exportTo(File file, Predicate<Integer> filter) throws IOException { CSVStrategy strategy = new CSVStrategy(';', '"', CSVStrategy.COMMENTS_DISABLED, CSVStrategy.ESCAPE_DISABLED, false, false, false, false); try (Writer writer = new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8)) { CSVPrinter printer = new CSVPrinter(writer); printer.setStrategy(strategy); printer.println(new String[] { Messages.CSVColumn_Date, // Messages.CSVColumn_Value, // Messages.CSVColumn_Transferals, // Messages.CSVColumn_DeltaInPercent, // Messages.CSVColumn_CumulatedPerformanceInPercent }); for (int ii = 0; ii < totals.length; ii++) { if (!filter.test(ii)) continue; printer.print(dates[ii].toString()); printer.print(Values.Amount.format(totals[ii])); printer.print(Values.Amount.format(transferals[ii])); printer.print(Values.Percent.format(delta[ii])); printer.print(Values.Percent.format(accumulated[ii])); printer.println(); } } } }