package name.abuchen.portfolio.model; import java.io.Serializable; import java.time.LocalDate; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.UUID; import name.abuchen.portfolio.money.CurrencyUnit; public final class Security implements Attributable, InvestmentVehicle { public static final class ByName implements Comparator<Security>, Serializable { private static final long serialVersionUID = 1L; @Override public int compare(Security s1, Security s2) { if (s1 == null) return s2 == null ? 0 : -1; return s1.name.compareToIgnoreCase(s2.name); } } private String uuid; private String name; private String currencyCode = CurrencyUnit.EUR; private String note; private String isin; private String tickerSymbol; private String wkn; // feed and feedURL are used to update historical prices private String feed; private String feedURL; private List<SecurityPrice> prices = new ArrayList<>(); // latestFeed and latestFeedURL are used to update the latest (current) // quote. If null, the values from feed and feedURL are used instead. private String latestFeed; private String latestFeedURL; private LatestSecurityPrice latest; private Attributes attributes; private List<SecurityEvent> events; private boolean isRetired = false; @Deprecated private String type; @Deprecated private String industryClassification; public Security() { this.uuid = UUID.randomUUID().toString(); } public Security(String name, String currencyCode) { this(); this.name = name; this.currencyCode = currencyCode; } public Security(String name, String isin, String tickerSymbol, String feed) { this(); this.name = name; this.isin = isin; this.tickerSymbol = tickerSymbol; this.feed = feed; } @Override public String getUUID() { return uuid; } /* package */void generateUUID() { // needed to assign UUIDs when loading older versions from XML uuid = UUID.randomUUID().toString(); } /** * Generates a UUID only if no UUID exists. For not yet known reasons, some * securities do miss the UUID. However, we do not want to make the * #generateUUID function public. */ public void fixMissingUUID() { if (uuid == null) generateUUID(); } @Override public String getName() { return name; } @Override public void setName(String name) { this.name = name; } @Override public String getCurrencyCode() { return currencyCode; } @Override public void setCurrencyCode(String currencyCode) { this.currencyCode = currencyCode; } @Override public String getNote() { return note; } @Override public void setNote(String note) { this.note = note; } public String getIsin() { return isin; } public void setIsin(String isin) { this.isin = isin; } public String getTickerSymbol() { return tickerSymbol; } public void setTickerSymbol(String tickerSymbol) { this.tickerSymbol = tickerSymbol; } public String getWkn() { return wkn; } public void setWkn(String wkn) { this.wkn = wkn; } /** * Returns ISIN, Ticker or WKN - whatever is available. */ public String getExternalIdentifier() { if (isin != null && isin.length() > 0) return isin; else if (tickerSymbol != null && tickerSymbol.length() > 0) return tickerSymbol; else if (wkn != null && wkn.length() > 0) return wkn; else return name; } @Deprecated /* package */String getIndustryClassification() { return industryClassification; } @Deprecated /* package */void setIndustryClassification(String industryClassification) { this.industryClassification = industryClassification; } @Deprecated /* package */String getType() { return type; } @Deprecated /* package */void setType(String type) { this.type = type; } public String getFeed() { return feed; } public void setFeed(String feed) { this.feed = feed; } public String getFeedURL() { return feedURL; } public void setFeedURL(String feedURL) { this.feedURL = feedURL; } public List<SecurityPrice> getPrices() { return Collections.unmodifiableList(prices); } /** * Returns a list of historical security prices that includes the latest * security price if no history price exists for that date */ public List<SecurityPrice> getPricesIncludingLatest() { if (latest == null) return getPrices(); int index = Collections.binarySearch(prices, new SecurityPrice(latest.getTime(), latest.getValue())); if (index >= 0) // historic quote exists -> use it return getPrices(); List<SecurityPrice> copy = new ArrayList<>(prices); copy.add(~index, latest); return copy; } /** * Adds security price to historical quotes. * * @return true if the historical quote was updated. */ public boolean addPrice(SecurityPrice price) { Objects.requireNonNull(price); int index = Collections.binarySearch(prices, price); if (index < 0) { prices.add(~index, price); return true; } else { SecurityPrice replaced = prices.get(index); if (!replaced.equals(price)) { // only replace if necessary -> UI might keep reference! prices.set(index, price); return true; } else { return false; } } } public void removePrice(SecurityPrice price) { prices.remove(price); } public void removeAllPrices() { prices.clear(); } public SecurityPrice getSecurityPrice(LocalDate requestedTime) { // assumption: prefer historic quote over latest if there are more // up-to-date historic quotes SecurityPrice lastHistoric = prices.isEmpty() ? null : prices.get(prices.size() - 1); // use latest quote only // * if one exists // * and if either no historic quotes exist // * or // ** if the requested time is after the latest quote // ** and the historic quotes are older than the latest quote if (latest != null // && (lastHistoric == null // || (!requestedTime.isBefore(latest.getTime()) && // !latest.getTime().isBefore(lastHistoric.getTime()) // ))) return latest; if (lastHistoric == null) return new SecurityPrice(requestedTime, 0); // avoid binary search if last historic quote <= requested date if (!lastHistoric.getTime().isAfter(requestedTime)) return lastHistoric; SecurityPrice p = new SecurityPrice(requestedTime, 0); int index = Collections.binarySearch(prices, p); if (index >= 0) return prices.get(index); else if (index == -1) // requested is date before first historic quote return prices.get(0); else return prices.get(-index - 2); } public String getLatestFeed() { return latestFeed; } public void setLatestFeed(String latestFeed) { this.latestFeed = latestFeed; } public String getLatestFeedURL() { return latestFeedURL; } public void setLatestFeedURL(String latestFeedURL) { this.latestFeedURL = latestFeedURL; } public LatestSecurityPrice getLatest() { return latest; } /** * Sets the latest security price. * * @return true if the latest security price was updated. */ public boolean setLatest(LatestSecurityPrice latest) { // only replace if necessary -> UI might keep reference! if ((this.latest != null && !this.latest.equals(latest)) || (this.latest == null && latest != null)) { this.latest = latest; return true; } else { return false; } } public boolean isRetired() { return isRetired; } public void setRetired(boolean isRetired) { this.isRetired = isRetired; } public List<SecurityEvent> getEvents() { if (this.events == null) this.events = new ArrayList<>(); return events; } public void addEvent(SecurityEvent event) { if (this.events == null) this.events = new ArrayList<>(); this.events.add(event); } @Override public Attributes getAttributes() { if (attributes == null) attributes = new Attributes(); return attributes; } @Override public void setAttributes(Attributes attributes) { this.attributes = attributes; } public List<TransactionPair<?>> getTransactions(Client client) { List<TransactionPair<?>> answer = new ArrayList<>(); for (Account account : client.getAccounts()) { account.getTransactions().stream() // .filter(t -> this.equals(t.getSecurity())) .filter(t -> t.getType() == AccountTransaction.Type.INTEREST || t.getType() == AccountTransaction.Type.DIVIDENDS || t.getType() == AccountTransaction.Type.TAX_REFUND) .map(t -> new TransactionPair<AccountTransaction>(account, t)) // .forEach(answer::add); } for (Portfolio portfolio : client.getPortfolios()) { portfolio.getTransactions().stream() // .filter(t -> this.equals(t.getSecurity())) .map(t -> new TransactionPair<PortfolioTransaction>(portfolio, t)) // .forEach(answer::add); } return answer; } public boolean hasTransactions(Client client) { for (Portfolio portfolio : client.getPortfolios()) { Optional<PortfolioTransaction> transaction = portfolio.getTransactions().stream() .filter(t -> this.equals(t.getSecurity())).findAny(); if (transaction.isPresent()) return true; } for (Account account : client.getAccounts()) { Optional<AccountTransaction> transaction = account.getTransactions().stream() .filter(t -> this.equals(t.getSecurity())).findAny(); if (transaction.isPresent()) return true; } return false; } public Security deepCopy() { Security answer = new Security(); answer.name = name; answer.currencyCode = currencyCode; answer.note = note; answer.isin = isin; answer.tickerSymbol = tickerSymbol; answer.wkn = wkn; answer.feed = feed; answer.feedURL = feedURL; answer.prices = new ArrayList<>(prices); answer.latestFeed = latestFeed; answer.latestFeedURL = latestFeedURL; answer.latest = latest; answer.events = new ArrayList<>(getEvents()); answer.isRetired = isRetired; return answer; } @Override public String toString() { return getName(); } public String toInfoString() { StringBuilder b = new StringBuilder(); b.append(name); if (notEmpty(isin)) b.append('\n').append(isin); if (notEmpty(wkn)) b.append('\n').append(wkn); if (notEmpty(tickerSymbol)) b.append('\n').append(tickerSymbol); if (notEmpty(note)) b.append("\n\n").append(note); //$NON-NLS-1$ return b.toString(); } private boolean notEmpty(String s) { return s != null && s.length() > 0; } }