package name.abuchen.portfolio.model; import java.time.LocalDate; import java.util.ArrayList; import java.util.List; 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; import name.abuchen.portfolio.util.Dates; import name.abuchen.portfolio.util.TradeCalendar; public class InvestmentPlan implements Named, Adaptable { private String name; private String note; private Security security; private Portfolio portfolio; private Account account; private LocalDate start; private int interval = 1; private long amount; private long fees; private List<PortfolioTransaction> transactions = new ArrayList<>(); public InvestmentPlan() { // needed for xstream de-serialization } public InvestmentPlan(String name) { this.name = name; } @Override public String getName() { return name; } @Override public void setName(String name) { this.name = name; } @Override public String getNote() { return note; } @Override public void setNote(String note) { this.note = note; } public Security getSecurity() { return security; } public void setSecurity(Security security) { this.security = security; } public Portfolio getPortfolio() { return portfolio; } public void setPortfolio(Portfolio portfolio) { this.portfolio = portfolio; } public Account getAccount() { return account; } public void setAccount(Account account) { this.account = account; } public LocalDate getStart() { return start; } public void setStart(LocalDate start) { this.start = start; } public int getInterval() { return interval; } public void setInterval(int interval) { this.interval = interval; } public long getAmount() { return amount; } public void setAmount(long amount) { this.amount = amount; } public long getFees() { return fees; } public void setFees(long fees) { this.fees = fees; } public List<PortfolioTransaction> getTransactions() { return transactions; } public void removeTransaction(PortfolioTransaction transaction) { transactions.remove(transaction); } public String getCurrencyCode() { return account != null ? account.getCurrencyCode() : portfolio.getReferenceAccount().getCurrencyCode(); } @Override public <T> T adapt(Class<T> type) { if (type == Security.class) return type.cast(security); else if (type == Account.class) return type.cast(account); else if (type == Portfolio.class) return type.cast(portfolio); else return null; } /** * Returns the date of the last transaction generated */ private LocalDate getLastDate() { LocalDate last = null; for (PortfolioTransaction t : transactions) { LocalDate date = t.getDate(); if (last == null || last.isBefore(date)) last = date; } return last; } /** * Returns the date for the next transaction to be generated based on the * interval */ private LocalDate next(LocalDate transactionDate) { LocalDate previousDate = transactionDate; // the transaction date might be edited (or moved to the next months b/c // of public holidays) -> determine the "normalized" date by comparing // the three months around the current transactionDate if (transactionDate.getDayOfMonth() != start.getDayOfMonth()) { int daysBetween = Integer.MAX_VALUE; LocalDate testDate = transactionDate.minusMonths(1); testDate = testDate.withDayOfMonth(Math.min(testDate.lengthOfMonth(), start.getDayOfMonth())); for (int ii = 0; ii < 3; ii++) { int d = Dates.daysBetween(transactionDate, testDate); if (d < daysBetween) { daysBetween = d; previousDate = testDate; } testDate = testDate.plusMonths(1); testDate = testDate.withDayOfMonth(Math.min(testDate.lengthOfMonth(), start.getDayOfMonth())); } } LocalDate next = previousDate.plusMonths(interval); // correct day of month (say the transactions are to be generated on the // 31st, but the month has only 30 days) next = next.withDayOfMonth(Math.min(next.lengthOfMonth(), start.getDayOfMonth())); // do not generate a investment plan transaction on a public holiday TradeCalendar tradeCalendar = new TradeCalendar(); while (tradeCalendar.isHoliday(next)) next = next.plusDays(1); return next; } public LocalDate getDateOfNextTransactionToBeGenerated() { return transactions.isEmpty() ? start : next(getLastDate()); } public List<PortfolioTransaction> generateTransactions(CurrencyConverter converter) { LocalDate transactionDate = getDateOfNextTransactionToBeGenerated(); List<PortfolioTransaction> newlyCreated = new ArrayList<>(); LocalDate now = LocalDate.now(); while (!transactionDate.isAfter(now)) { PortfolioTransaction transaction = createTransaction(converter, transactionDate); transactions.add(transaction); newlyCreated.add(transaction); transactionDate = next(transactionDate); } return newlyCreated; } private PortfolioTransaction createTransaction(CurrencyConverter converter, LocalDate tDate) { String targetCurrencyCode = getCurrencyCode(); boolean needsCurrencyConversion = !targetCurrencyCode.equals(security.getCurrencyCode()); Transaction.Unit forex = null; long price = getSecurity().getSecurityPrice(tDate).getValue(); long availableAmount = amount - fees; if (needsCurrencyConversion) { Money availableMoney = Money.of(targetCurrencyCode, amount - fees); availableAmount = converter.with(security.getCurrencyCode()).convert(tDate, availableMoney).getAmount(); forex = new Transaction.Unit(Unit.Type.GROSS_VALUE, // availableMoney, // Money.of(security.getCurrencyCode(), availableAmount), // converter.with(targetCurrencyCode).getRate(tDate, security.getCurrencyCode()).getValue()); } long shares = Math .round(availableAmount * Values.Share.factor() * Values.Quote.factorToMoney() / (double) price); if (account != null) { // create buy transaction BuySellEntry entry = new BuySellEntry(portfolio, account); entry.setType(PortfolioTransaction.Type.BUY); entry.setDate(tDate); entry.setShares(shares); entry.setCurrencyCode(targetCurrencyCode); entry.setAmount(amount); entry.setSecurity(getSecurity()); if (fees != 0) entry.getPortfolioTransaction() .addUnit(new Transaction.Unit(Unit.Type.FEE, Money.of(targetCurrencyCode, fees))); if (forex != null) entry.getPortfolioTransaction().addUnit(forex); entry.insert(); return entry.getPortfolioTransaction(); } else { // create inbound delivery PortfolioTransaction transaction = new PortfolioTransaction(); transaction.setDate(tDate); transaction.setType(PortfolioTransaction.Type.DELIVERY_INBOUND); transaction.setSecurity(security); transaction.setCurrencyCode(targetCurrencyCode); transaction.setAmount(amount); transaction.setShares(shares); if (fees != 0) transaction.addUnit(new Transaction.Unit(Unit.Type.FEE, Money.of(targetCurrencyCode, fees))); if (forex != null) transaction.addUnit(forex); portfolio.addTransaction(transaction); return transaction; } } }