package name.abuchen.portfolio.online.impl; import static name.abuchen.portfolio.online.impl.YahooHelper.asPrice; import static name.abuchen.portfolio.online.impl.YahooHelper.stripQuotes; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.net.URL; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.text.DecimalFormat; import java.text.DecimalFormatSymbols; import java.text.ParseException; import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.Set; import java.util.stream.Collectors; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; import org.jsoup.select.Elements; import name.abuchen.portfolio.Messages; import name.abuchen.portfolio.model.Security; import name.abuchen.portfolio.money.Values; import name.abuchen.portfolio.online.SecuritySearchProvider; import name.abuchen.portfolio.online.impl.YahooSymbolSearch.Result; public class YahooSearchProvider implements SecuritySearchProvider { private static final String SEARCH_URL = "https://de.finance.yahoo.com/lookup?s=%s&t=A&b=0&m=ALL"; //$NON-NLS-1$ private static final String LOOKUP_URL = "https://download.finance.yahoo.com/d/quotes.csv?s=%s&f=snl1"; //$NON-NLS-1$ private static final ThreadLocal<DecimalFormat> FMT_INDEX = new ThreadLocal<DecimalFormat>() { protected DecimalFormat initialValue() { return new DecimalFormat("#,##0.##", new DecimalFormatSymbols(Locale.GERMANY)); //$NON-NLS-1$ } }; public static class YahooResultItem extends ResultItem { @Override public void applyTo(Security security) { super.applyTo(security); security.setFeed(YahooFinanceQuoteFeed.ID); } public static ResultItem from(Result r) { YahooResultItem item = new YahooResultItem(); item.setSymbol(r.getSymbol()); item.setName(r.getName()); item.setExchange(r.getExchange()); item.setType(r.getType()); return item; } } @Override public String getName() { return Messages.LabelYahooFinance; } @Override public List<ResultItem> search(String query) throws IOException { // search both the HTML page as well as the symbol search String url = String.format(SEARCH_URL, URLEncoder.encode(query, StandardCharsets.UTF_8.name())); Document document = Jsoup.connect(url).get(); List<ResultItem> answer = extractFrom(document); addSymbolSearchResults(answer, query); if (answer.isEmpty()) { ResultItem item = searchCSV(query); if (item == null) { item = new YahooResultItem(); item.setName(String.format(Messages.MsgNoResults, query)); } answer.add(item); } else if (answer.size() >= 20) { ResultItem item = new YahooResultItem(); item.setName(Messages.MsgMoreResultsAvailable); answer.add(item); } return answer; } private void addSymbolSearchResults(List<ResultItem> answer, String query) throws IOException { Set<String> existingSymbols = answer.stream().map(r -> r.getSymbol()).collect(Collectors.toSet()); new YahooSymbolSearch().search(query)// .filter(r -> !existingSymbols.contains(r.getSymbol())) .forEach(r -> answer.add(YahooResultItem.from(r))); } /* protected */List<ResultItem> extractFrom(Document document) throws IOException { List<ResultItem> answer = new ArrayList<ResultItem>(); Elements tables = document.getElementsByAttribute("SUMMARY"); //$NON-NLS-1$ for (Element table : tables) { if (!"YFT_SL_TABLE_SUMMARY".equals(table.attr("SUMMARY"))) //$NON-NLS-1$ //$NON-NLS-2$ continue; Elements rows = table.select("> tbody > tr"); //$NON-NLS-1$ for (Element row : rows) { Elements cells = row.select("> td"); //$NON-NLS-1$ if (cells.size() != 6) continue; ResultItem item = new YahooResultItem(); item.setSymbol(cells.get(0).text()); item.setName(cells.get(1).text()); item.setIsin(cells.get(2).text()); // last trace String lastTrade = cells.get(3).text(); if (!"NaN".equals(lastTrade)) //$NON-NLS-1$ item.setLastTrade(parseIndex(lastTrade)); item.setType(cells.get(4).text()); item.setExchange(cells.get(5).text()); answer.add(item); } } return answer; } /* protected */ResultItem searchCSV(String query) throws IOException { String csv = String.format(LOOKUP_URL, URLEncoder.encode(query.toUpperCase(), StandardCharsets.UTF_8.name())); BufferedReader reader = new BufferedReader(new InputStreamReader(new URL(csv).openStream())); String line = reader.readLine(); if (line != null) { String[] values = line.split(","); //$NON-NLS-1$ // result must have 3 values -> otherwise error message if (values.length != 3) return null; // Yahoo always returns a value if query is a syntactically correct // symbol even if it does not exist -> filter String symbol = stripQuotes(values[0]); String name = stripQuotes(values[1]); if (symbol.equals(name)) return null; try { ResultItem answer = new ResultItem(); answer.setSymbol(symbol); answer.setName(name); answer.setLastTrade(asPrice(values[2])); return answer; } catch (ParseException e) { throw new IOException(e); } } else { return null; } } private long parseIndex(String text) throws IOException { try { Number q = FMT_INDEX.get().parse(text); return (long) (q.doubleValue() * Values.Quote.factor()); } catch (ParseException e) { throw new IOException(e); } } }