GnuCash  5.6-150-g038405b370+
gnc-quotes.cpp
1 /********************************************************************\
2  * gnc-quotes.hpp -- proxy for Finance::Quote *
3  * Copyright (C) 2021 Geert Janssens <geert@kobaltwit.be> *
4  * *
5  * This program is free software; you can redistribute it and/or *
6  * modify it under the terms of the GNU General Public License as *
7  * published by the Free Software Foundation; either version 2 of *
8  * the License, or (at your option) any later version. *
9  * *
10  * This program is distributed in the hope that it will be useful, *
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of *
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
13  * GNU General Public License for more details. *
14  * *
15  * You should have received a copy of the GNU General Public License*
16  * along with this program; if not, contact: *
17  * *
18  * Free Software Foundation Voice: +1-617-542-5942 *
19  * 51 Franklin Street, Fifth Floor Fax: +1-617-542-2652 *
20  * Boston, MA 02110-1301, USA gnu@gnu.org *
21 \ *******************************************************************/
22 
23 #include <boost/process/environment.hpp>
24 #include <config.h>
25 #include <qoflog.h>
26 
27 #include <algorithm>
28 #include <stdexcept>
29 #include <vector>
30 #include <string>
31 #include <iostream>
32 #include <boost/version.hpp>
33 #if BOOST_VERSION < 107600
34 // json_parser uses a deprecated version of bind.hpp
35 #define BOOST_BIND_GLOBAL_PLACEHOLDERS
36 #endif
37 #include <boost/algorithm/string.hpp>
38 #include <boost/filesystem.hpp>
39 #ifdef BOOST_WINDOWS_API
40 #include <boost/process/windows.hpp>
41 #endif
42 #include <boost/process.hpp>
43 #include <boost/regex.hpp>
44 #include <boost/property_tree/ptree.hpp>
45 #include <boost/property_tree/json_parser.hpp>
46 #include <boost/iostreams/device/array.hpp>
47 #include <boost/iostreams/stream_buffer.hpp>
48 #include <boost/locale.hpp>
49 #include <boost/asio.hpp>
50 #include <glib.h>
51 #include "gnc-commodity.hpp"
52 #include <gnc-datetime.hpp>
53 #include <gnc-numeric.hpp>
54 #include "gnc-quotes.hpp"
55 
56 #include <gnc-commodity.h>
57 #include <gnc-path.h>
58 #include "gnc-ui-util.h"
59 #include <gnc-prefs.h>
60 #include <gnc-session.h>
61 #include <regex.h>
62 #include <qofbook.h>
63 
64 static const QofLogModule log_module = "gnc.price-quotes";
65 static const char* av_api_env = "ALPHAVANTAGE_API_KEY";
66 static const char* av_api_key = "alphavantage-api-key";
67 static const char* yh_api_env = "FINANCEAPI_API_KEY";
68 static const char* yh_api_key = "yhfinance-api-key";
69 
70 namespace bl = boost::locale;
71 namespace bp = boost::process;
72 namespace bfs = boost::filesystem;
73 namespace bpt = boost::property_tree;
74 namespace bio = boost::iostreams;
75 
76 using QuoteResult = std::tuple<int, StrVec, StrVec>;
77 
78 struct GncQuoteSourceError : public std::runtime_error
79 {
80  GncQuoteSourceError(const std::string& err) : std::runtime_error(err) {}
81 };
82 
83 CommVec
84 gnc_quotes_get_quotable_commodities(const gnc_commodity_table * table);
85 
87 {
88 public:
89  virtual ~GncQuoteSource() = default;
90  virtual const StrVec& get_sources() const noexcept = 0;
91  virtual const std::string & get_version() const noexcept = 0;
92  virtual QuoteResult get_quotes(const std::string& json_str) const = 0;
93 };
94 
95 
97 {
98 public:
99  // Constructor - checks for presence of Finance::Quote and import version and quote sources
100  GncQuotesImpl ();
101  explicit GncQuotesImpl (QofBook *book);
102  GncQuotesImpl(QofBook*, std::unique_ptr<GncQuoteSource>);
103 
104  void fetch (QofBook *book);
105  void fetch (CommVec& commodities);
106  void fetch (gnc_commodity *comm);
107  void report (const char* source, const StrVec& commodities, bool verbose);
108 
109  const std::string& version() noexcept { return m_quotesource->get_version(); }
110  const QuoteSources& sources() noexcept { return m_sources; }
111  bool had_failures() noexcept { return !m_failures.empty(); }
112  const QFVec& failures() noexcept;
113  std::string report_failures() noexcept;
114 
115 private:
116  std::string query_fq (const char* source, const StrVec& commoditites);
117  std::string query_fq (const CommVec&);
118  bpt::ptree parse_quotes (const std::string& quote_str);
119  void create_quotes(const bpt::ptree& pt, const CommVec& comm_vec);
120  std::string comm_vec_to_json_string(const CommVec&) const;
121  GNCPrice* parse_one_quote(const bpt::ptree&, gnc_commodity*);
122 
123  std::unique_ptr<GncQuoteSource> m_quotesource;
124  QuoteSources m_sources;
125  QFVec m_failures;
126  QofBook *m_book;
127  gnc_commodity *m_dflt_curr;
128 };
129 
130 class GncFQQuoteSource final : public GncQuoteSource
131 {
132  const bfs::path c_cmd;
133  std::string c_fq_wrapper;
134  std::string m_version;
135  StrVec m_sources;
136  bp::environment m_env;
137 public:
139  ~GncFQQuoteSource() = default;
140  const std::string& get_version() const noexcept override { return m_version; }
141  const StrVec& get_sources() const noexcept override { return m_sources; }
142  QuoteResult get_quotes(const std::string&) const override;
143 private:
144  QuoteResult run_cmd (const StrVec& args, const std::string& json_string) const;
145  void set_api_key(const char* api_pref, const char* api_env);
146 };
147 
148 static void show_quotes(const bpt::ptree& pt, const StrVec& commodities, bool verbose);
149 static void show_currency_quotes(const bpt::ptree& pt, const StrVec& commodities, bool verbose);
150 static std::string parse_quotesource_error(const std::string& line);
151 
152 static const std::string empty_string{};
153 
154 GncFQQuoteSource::GncFQQuoteSource() :
155 c_cmd{bp::search_path("perl")},
156 m_version{}, m_sources{}, m_env{boost::this_process::environment()}
157 {
158  char *bindir = gnc_path_get_bindir();
159  c_fq_wrapper = std::string(bindir) + "/finance-quote-wrapper";
160  g_free(bindir);
161  StrVec args{"-w", c_fq_wrapper, "-v"};
162  auto [rv, sources, errors] = run_cmd(args, empty_string);
163  if (rv)
164  {
165  std::string err{bl::translate("Failed to initialize Finance::Quote: ")};
166  for (const auto& err_line : errors)
167  err += err_line.empty() ? "" : err_line + "\n";
168  throw(GncQuoteSourceError(err));
169  }
170  if (!errors.empty())
171  {
172  std::string err{bl::translate("Finance::Quote check returned error ")};
173  for(const auto& err_line : errors)
174  err += err.empty() ? "" : err_line + "\n";
175  throw(GncQuoteSourceError(err));
176  }
177  auto version{sources.front()};
178  if (version.empty())
179  {
180  std::string err{bl::translate("No Finance::Quote Version")};
181  throw(GncQuoteSourceError(err));
182  }
183  m_version = std::move(version);
184  sources.erase(sources.begin());
185  m_sources = std::move(sources);
186  std::sort (m_sources.begin(), m_sources.end());
187 
188  set_api_key(av_api_key, av_api_env);
189  set_api_key(yh_api_key, yh_api_env);
190 }
191 
192 QuoteResult
193 GncFQQuoteSource::get_quotes(const std::string& json_str) const
194 {
195  StrVec args{"-w", c_fq_wrapper, "-f" };
196  return run_cmd(args, json_str);
197 }
198 
199 QuoteResult
200 GncFQQuoteSource::run_cmd (const StrVec& args, const std::string& json_string) const
201 {
202  StrVec out_vec, err_vec;
203  int cmd_result;
204 
205  try
206  {
207  std::future<std::vector<char> > out_buf, err_buf;
208  boost::asio::io_context svc;
209 
210  auto input_buf = bp::buffer (json_string);
211  bp::child process;
212  process = bp::child(c_cmd, args,
213  bp::std_out > out_buf,
214  bp::std_err > err_buf,
215  bp::std_in < input_buf,
216 #ifdef BOOST_WINDOWS_API
217  bp::windows::create_no_window,
218 #endif
219  m_env,
220  svc);
221 
222  svc.run();
223  process.wait();
224 
225  {
226  auto raw = out_buf.get();
227  std::vector<std::string> data;
228  std::string line;
229  bio::stream_buffer<bio::array_source> sb(raw.data(), raw.size());
230  std::istream is(&sb);
231 
232  while (std::getline(is, line) && !line.empty())
233  {
234 #ifdef __WIN32
235  if (line.back() == '\r')
236  line.pop_back();
237 #endif
238  out_vec.push_back (std::move(line));
239  }
240  raw = err_buf.get();
241  bio::stream_buffer<bio::array_source> eb(raw.data(), raw.size());
242  std::istream es(&eb);
243 
244  while (std::getline(es, line) && !line.empty())
245  err_vec.push_back (std::move(line));
246  }
247  cmd_result = process.exit_code();
248  }
249  catch (std::exception &e)
250  {
251  cmd_result = -1;
252  err_vec.push_back(e.what());
253  };
254 
255  return QuoteResult (cmd_result, std::move(out_vec), std::move(err_vec));
256 }
257 
258 void
259 GncFQQuoteSource::set_api_key(const char* api_key, const char* api_env)
260 {
261  auto key = gnc_prefs_get_string("general.finance-quote", api_key);
262  if (key && *key)
263  {
264  m_env[api_env] = key;
265  g_free(key);
266  }
267  else
268  {
269  if (api_key == av_api_key && m_env.find(api_env) == m_env.end())
270  PWARN("No Alpha Vantage API key set, currency quotes and other "
271  "AlphaVantage based quotes won't work.");
272  g_free(key);
273  }
274 }
275 
276 /* GncQuotes implementation */
277 GncQuotesImpl::GncQuotesImpl() : m_quotesource{new GncFQQuoteSource},
278  m_sources{}, m_failures{},
279  m_book{qof_session_get_book(gnc_get_current_session())},
280  m_dflt_curr{gnc_default_currency()}
281 {
282  m_sources = m_quotesource->get_sources();
283 }
284 
285 GncQuotesImpl::GncQuotesImpl(QofBook* book) : m_quotesource{new GncFQQuoteSource},
286 m_sources{}, m_book{book},
287 m_dflt_curr{gnc_default_currency()}
288 {
289  m_sources = m_quotesource->get_sources();
290 }
291 
292 GncQuotesImpl::GncQuotesImpl(QofBook* book, std::unique_ptr<GncQuoteSource> quote_source) :
293 m_quotesource{std::move(quote_source)},
294 m_sources{}, m_book{book}, m_dflt_curr{gnc_default_currency()}
295 {
296  m_sources = m_quotesource->get_sources();
297 }
298 
299 void
300 GncQuotesImpl::fetch (QofBook *book)
301 {
302  if (!book)
303  throw (GncQuoteException(bl::translate("GncQuotes::Fetch called with no book.")));
304  auto commodities = gnc_quotes_get_quotable_commodities (
306  fetch (commodities);
307 }
308 
309 void
310 GncQuotesImpl::fetch (gnc_commodity *comm)
311 {
312  auto commodities = CommVec {comm};
313  fetch (commodities);
314 }
315 
316 void
317 GncQuotesImpl::fetch (CommVec& commodities)
318 {
319  m_failures.clear();
320  if (commodities.empty())
321  throw (GncQuoteException(bl::translate("GncQuotes::Fetch called with no commodities.")));
322  auto quote_str{query_fq (commodities)};
323  auto ptree{parse_quotes (quote_str)};
324  create_quotes(ptree, commodities);
325 }
326 
327 void
328 GncQuotesImpl::report (const char* source, const StrVec& commodities,
329  bool verbose)
330 {
331  if (!source)
332  throw (GncQuoteException(bl::translate("GncQuotes::Report called with no source.")));
333 
334  bool is_currency{strcmp(source, "currency") == 0};
335  m_failures.clear();
336  if (commodities.empty())
337  {
338  std::cerr << _("There were no commodities for which to retrieve quotes.") << std::endl;
339  return;
340  }
341  try
342  {
343  auto quote_str{query_fq (source, commodities)};
344  auto ptree{parse_quotes (quote_str)};
345  auto source_pt_ai{ptree.find(source)};
346  if (is_currency)
347  show_currency_quotes(source_pt_ai->second, commodities, verbose);
348  else
349  show_quotes(source_pt_ai->second, commodities, verbose);
350  }
351  catch (const GncQuoteException& err)
352  {
353  std::cerr << _("Finance::Quote retrieval failed with error ") << err.what() << std::endl;
354  }
355 }
356 
357 const QFVec&
358 GncQuotesImpl::failures() noexcept
359 {
360  return m_failures;
361 }
362 
363 static std::string
364 explain(GncQuoteError err, const std::string& errmsg)
365 {
366  std::string retval;
367  switch (err)
368  {
369  case GncQuoteError::NO_RESULT:
370  if (errmsg.empty())
371  retval += _("Finance::Quote returned no data and set no error.");
372  else
373  retval += _("Finance::Quote returned an error: ") + errmsg;
374  break;
375  case GncQuoteError::QUOTE_FAILED:
376  if (errmsg.empty())
377  retval += _("Finance::Quote reported failure set no error.");
378  else
379  retval += _("Finance::Quote reported failure with error: ") + errmsg;
380  break;
381  case GncQuoteError::NO_CURRENCY:
382  retval += _("Finance::Quote returned a quote with no currency.");
383  break;
384  case GncQuoteError::UNKNOWN_CURRENCY:
385  retval += _("Finance::Quote returned a quote with a currency GnuCash doesn't know about.");
386  break;
387  case GncQuoteError::NO_PRICE:
388  retval += _("Finance::Quote returned a quote with no price element.");
389  break;
390  case GncQuoteError::PRICE_PARSE_FAILURE:
391  retval += _("Finance::Quote returned a quote with a price that GnuCash was unable to covert to a number.");
392  break;
393  case GncQuoteError::SUCCESS:
394  default:
395  retval += _("The quote has no error set.");
396  break;
397  }
398  return retval;
399 }
400 
401 std::string
402 GncQuotesImpl::report_failures() noexcept
403 {
404  std::string retval{_("Quotes for the following commodities were unavailable or unusable:\n")};
405  std::for_each(m_failures.begin(), m_failures.end(),
406  [&retval](auto failure)
407  {
408  auto [ns, sym, reason, err] = failure;
409  retval += "* " + ns + ":" + sym + " " +
410  explain(reason, err) + "\n";
411  });
412  return retval;
413 }
414 
415 /* **** Private function implementations ****/
416 
417 using Path = bpt::ptree::path_type;
418 static inline Path make_quote_path(const std::string &name_space,
419  const std::string &symbol)
420 {
421  using Path = bpt::ptree::path_type;
422  Path key{name_space, '|'};
423  key /= Path{symbol, '|'};
424  return key;
425 };
426 
427 std::string
428 GncQuotesImpl::comm_vec_to_json_string(const CommVec &comm_vec) const
429 {
430  bpt::ptree pt, pt_child;
431  pt.put("defaultcurrency", gnc_commodity_get_mnemonic(m_dflt_curr));
432 
433  std::for_each (comm_vec.cbegin(), comm_vec.cend(),
434  [this, &pt] (auto comm)
435  {
436  auto comm_mnemonic = gnc_commodity_get_mnemonic (comm);
437  auto comm_ns = std::string("currency");
438  if (gnc_commodity_is_currency (comm))
439  {
440  if (gnc_commodity_equiv(comm, m_dflt_curr) ||
441  (!comm_mnemonic || (strcmp(comm_mnemonic, "XXX") == 0)))
442  return;
443  }
444  else
446 
447  pt.put (make_quote_path(comm_ns, comm_mnemonic), "");
448  }
449  );
450 
451  std::ostringstream result;
452  bpt::write_json(result, pt);
453  return result.str();
454 }
455 
456 static inline std::string
457 get_quotes(const std::string& json_str, const std::unique_ptr<GncQuoteSource>& qs)
458 {
459  auto [rv, quotes, errors] = qs->get_quotes(json_str);
460  std::string answer;
461 
462  if (rv == 0)
463  {
464  for (const auto& line : quotes)
465  answer.append(line + "\n");
466  }
467  else
468  {
469  std::string err_str;
470  for (const auto& line: errors)
471  {
472  if (line == "invalid_json\n")
473  PERR("Finanace Quote Wrapper was unable to parse %s",
474  json_str.c_str());
475  err_str += parse_quotesource_error(line);
476  }
477  throw(GncQuoteException(err_str));
478  }
479 
480  return answer;
481 }
482 
483 std::string
484 GncQuotesImpl::query_fq (const char* source, const StrVec& commodities)
485 {
486  bpt::ptree pt;
487  auto is_currency{strcmp(source, "currency") == 0};
488 
489  if (is_currency && commodities.size() < 2)
490  throw(GncQuoteException(_("Currency quotes requires at least two currencies")));
491 
492  if (is_currency)
493  pt.put("defaultcurrency", commodities[0].c_str());
494  else
495  pt.put("defaultcurrency", gnc_commodity_get_mnemonic(m_dflt_curr));
496 
497  std::for_each(is_currency ? ++commodities.cbegin() : commodities.cbegin(),
498  commodities.cend(),
499  [source, &pt](auto sym)
500  {
501  pt.put(make_quote_path(source, sym), "");
502  });
503  std::ostringstream result;
504  bpt::write_json(result, pt);
505  auto result_str{result.str()};
506  PINFO("Query JSON: %s\n", result_str.c_str());
507  return get_quotes(result.str(), m_quotesource);
508 }
509 
510 std::string
511 GncQuotesImpl::query_fq (const CommVec& comm_vec)
512 {
513  auto json_str{comm_vec_to_json_string(comm_vec)};
514  PINFO("Query JSON: %s\n", json_str.c_str());
515  return get_quotes(json_str, m_quotesource);
516 }
517 
519 {
520  const char* ns;
521  const char* mnemonic;
522  bool success;
523  std::string type;
524  boost::optional<std::string> price;
525  bool inverted;
526  boost::optional<std::string> date;
527  boost::optional<std::string> time;
528  boost::optional<std::string> currency;
529  boost::optional<std::string> errormsg;
530 };
531 
532 static void
533 get_price_and_type(PriceParams& p, const bpt::ptree& comm_pt)
534 {
535  p.type = "last";
536  p.price = comm_pt.get_optional<std::string> (p.type);
537 
538  if (!p.price)
539  {
540  p.type = "nav";
541  p.price = comm_pt.get_optional<std::string> (p.type);
542  }
543 
544  if (!p.price)
545  {
546  p.type = "price";
547  p.price = comm_pt.get_optional<std::string> (p.type);
548  /* guile wrapper used "unknown" as price type when "price" was found,
549  * reproducing here to keep same result for users in the pricedb */
550  p.type = p.price ? "unknown" : "missing";
551  }
552 }
553 
554 static void
555 parse_quote_json(PriceParams& p, const bpt::ptree& comm_pt)
556 {
557  auto success = comm_pt.get_optional<bool> ("success");
558  p.success = success && *success;
559  if (!p.success)
560  p.errormsg = comm_pt.get_optional<std::string> ("errormsg");
561  get_price_and_type(p, comm_pt);
562  auto inverted = comm_pt.get_optional<bool> ("inverted");
563  p.inverted = inverted && *inverted;
564  p.date = comm_pt.get_optional<std::string> ("date");
565  p.time = comm_pt.get_optional<std::string> ("time");
566  p.currency = comm_pt.get_optional<std::string> ("currency");
567 
568 
569  PINFO("Commodity: %s", p.mnemonic);
570  PINFO(" Success: %s", (p.success ? "yes" : "no"));
571  PINFO(" Date: %s", (p.date ? p.date->c_str() : "missing"));
572  PINFO(" Time: %s", (p.time ? p.time->c_str() : "missing"));
573  PINFO(" Currency: %s", (p.currency ? p.currency->c_str() : "missing"));
574  PINFO(" Price: %s", (p.price ? p.price->c_str() : "missing"));
575  PINFO(" Inverted: %s\n", (p.inverted ? "yes" : "no"));
576 }
577 
578 static time64
579 calc_price_time(const PriceParams& p)
580 {
581  /* Note that as of F::Q v. 1.52 the only sources that provide
582  * quote times are ftfunds (aka ukfunds), morningstarch, and
583  * mstaruk_fund, but it's faked with a comment "Set a dummy time
584  * as gnucash insists on having a valid format". It's also wrong,
585  * as it lacks seconds. Best ignored.
586  */
587  if (p.date && !p.date->empty())
588  {
589  try
590  {
591  auto quote_time{GncDateTime(GncDate(*p.date, "m-d-y"))};
592  PINFO("Quote date included, using %s for %s:%s",
593  quote_time.format("%Y-%m-%d %H:%M:%S %z").c_str(), p.ns, p.mnemonic);
594  return static_cast<time64>(quote_time);
595  }
596  catch (const std::exception &err)
597  {
598  auto now{GncDateTime()};
599  PWARN("Warning: failed to parse quote date '%s' for %s:%s because %s - will use %s",
600  p.date->c_str(), p.ns, p.mnemonic, err.what(), now.format("%Y-%m-%d %H:%M:%S %z").c_str());
601  return static_cast<time64>(now);
602  }
603  }
604 
605  auto now{GncDateTime()};
606  PINFO("No date was returned for %s:%s - will use %s",
607  p.ns, p.mnemonic, now.format("%Y-%m-%d %H:%M:%S %z").c_str());
608  return static_cast<time64>(now);
609 }
610 
611 static boost::optional<GncNumeric>
612 get_price(const PriceParams& p)
613 {
614  boost::optional<GncNumeric> price;
615  try
616  {
617  price = GncNumeric { *p.price };
618  }
619  catch (...)
620  {
621  PWARN("Skipped %s:%s - failed to parse returned price '%s'",
622  p.ns, p.mnemonic, p.price->c_str());
623  }
624 
625  if (price && p.inverted)
626  *price = price->inv();
627 
628  return price;
629 }
630 
631 static gnc_commodity*
632 get_currency(const PriceParams& p, QofBook* book, QFVec& failures)
633 {
634  if (!p.currency)
635  {
636  failures.emplace_back(p.ns, p.mnemonic, GncQuoteError::NO_CURRENCY,
637  empty_string);
638  PWARN("Skipped %s:%s - Finance::Quote returned a quote with no currency",
639  p.ns, p.mnemonic);
640  return nullptr;
641  }
642  std::string curr_str = *p.currency;
643  boost::to_upper (curr_str);
644  auto commodity_table = gnc_commodity_table_get_table (book);
645  auto currency = gnc_commodity_table_lookup (commodity_table, "ISO4217", curr_str.c_str());
646 
647  if (!currency)
648  {
649  failures.emplace_back(p.ns, p.mnemonic,
650  GncQuoteError::UNKNOWN_CURRENCY, empty_string);
651  PWARN("Skipped %s:%s - failed to parse returned currency '%s'",
652  p.ns, p.mnemonic, p.currency->c_str());
653  return nullptr;
654  }
655 
656  return currency;
657 }
658 
659 GNCPrice*
660 GncQuotesImpl::parse_one_quote(const bpt::ptree& pt, gnc_commodity* comm)
661 {
662  PriceParams p;
663  bpt::ptree comm_pt;
664 
665  p.ns = gnc_commodity_get_namespace (comm);
666  p.mnemonic = gnc_commodity_get_mnemonic (comm);
667  if (gnc_commodity_equiv(comm, m_dflt_curr) ||
668  (!p.mnemonic || (strcmp (p.mnemonic, "XXX") == 0)))
669  return nullptr;
671  auto source_pt_ai{pt.find(source)};
672  auto ok{source_pt_ai != pt.not_found()};
673  if (ok)
674  {
675  auto comm_pt_ai{source_pt_ai->second.find(p.mnemonic)};
676  ok = (comm_pt_ai != pt.not_found());
677  if (ok)
678  comm_pt = comm_pt_ai->second;
679  }
680  if (!ok)
681  {
682  m_failures.emplace_back(p.ns, p.mnemonic, GncQuoteError::NO_RESULT,
683  empty_string);
684  PINFO("Skipped %s:%s - Finance::Quote didn't return any data from %s.",
685  p.ns, p.mnemonic, source);
686  return nullptr;
687  }
688 
689  parse_quote_json(p, comm_pt);
690  if (!p.success)
691  {
692  m_failures.emplace_back(p.ns, p.mnemonic, GncQuoteError::QUOTE_FAILED,
693  p.errormsg ? *p.errormsg : empty_string);
694  PWARN("Skipped %s:%s - Finance::Quote returned fetch failure.\nReason %s",
695  p.ns, p.mnemonic,
696  (p.errormsg ? p.errormsg->c_str() : "unknown"));
697  return nullptr;
698  }
699 
700  if (!p.price)
701  {
702  m_failures.emplace_back(p.ns, p.mnemonic,
703  GncQuoteError::NO_PRICE, empty_string);
704  PWARN("Skipped %s:%s - Finance::Quote didn't return a valid price",
705  p.ns, p.mnemonic);
706  return nullptr;
707  }
708 
709  auto price{get_price(p)};
710  if (!price)
711  {
712  m_failures.emplace_back(p.ns, p.mnemonic,
713  GncQuoteError::PRICE_PARSE_FAILURE,
714  empty_string);
715  return nullptr;
716  }
717 
718  auto currency{get_currency(p, m_book, m_failures)};
719  if (!currency)
720  return nullptr;
721 
722  auto quotedt{calc_price_time(p)};
723  auto gnc_price = gnc_price_create (m_book);
724  gnc_price_begin_edit (gnc_price);
725  gnc_price_set_commodity (gnc_price, comm);
726  gnc_price_set_currency (gnc_price, currency);
727  gnc_price_set_time64 (gnc_price, static_cast<time64> (quotedt));
728  gnc_price_set_source (gnc_price, PRICE_SOURCE_FQ);
729  gnc_price_set_typestr (gnc_price, p.type.c_str());
730  gnc_price_set_value (gnc_price, *price);
731  gnc_price_commit_edit (gnc_price);
732  return gnc_price;
733 }
734 
735 bpt::ptree
736 GncQuotesImpl::parse_quotes (const std::string& quote_str)
737 {
738  bpt::ptree pt;
739  std::istringstream ss {quote_str};
740  std::string what;
741 
742  try
743  {
744  bpt::read_json (ss, pt);
745  }
746  catch (bpt::json_parser_error &e) {
747  what = e.what();
748  }
749  catch (const std::runtime_error& e)
750  {
751  what = e.what();
752  }
753  catch (const std::logic_error& e)
754  {
755  what = e.what();
756  }
757  catch (...) {
758  std::string error_msg{_("Failed to parse result returned by Finance::Quote.")};
759  error_msg += "\n";
760  //Translators: This labels the return value of a query to Finance::Quote written in an error.
761  error_msg += _("Result:");
762  error_msg += "\n";
763  error_msg += quote_str;
764  throw(GncQuoteException(error_msg));
765  }
766  if (!what.empty())
767  {
768  std::string error_msg{_("Failed to parse result returned by Finance::Quote.")};
769  error_msg += "\n";
770  //Translators: This is the error message reported by the Online Quotes processing code.
771  error_msg += _("Error message:");
772  error_msg += "\n";
773  error_msg += what;
774  error_msg += "\n";
775  //Translators: This labels the return value of a query to Finance::Quote written in an error.
776  error_msg += _("Result:");
777  error_msg += "\n";
778  error_msg += quote_str;
779  throw(GncQuoteException(error_msg));
780  }
781  return pt;
782 }
783 
784 void
785 GncQuotesImpl::create_quotes (const bpt::ptree& pt, const CommVec& comm_vec)
786 {
787  auto pricedb{gnc_pricedb_get_db(m_book)};
788  for (auto comm : comm_vec)
789  {
790  auto price{parse_one_quote(pt, comm)};
791  if (!price)
792  continue;
793 // See the comment at gnc_pricedb_add_price
794  gnc_pricedb_add_price(pricedb, price);
795  }
796 }
797 
798 static void
799 show_verbose_quote(const bpt::ptree& comm_pt)
800 {
801  std::for_each(comm_pt.begin(), comm_pt.end(),
802  [](auto elem) {
803  std::cout << std::setw(12) << std::right << elem.first << " => " <<
804  std::left << elem.second.data() << "\n";
805  });
806  std::cout << std::endl;
807 }
808 
809 static void
810 show_gnucash_quote(const bpt::ptree& comm_pt)
811 {
812  constexpr const char* ptr{"<=== "};
813  constexpr const char* dptr{"<=\\ "};
814  constexpr const char* uptr{"<=/ "};
815  //Translators: Means that the preceding element is required
816  const char* reqd{C_("Finance::Quote", "required")};
817  //Translators: Means that the quote will work best if the preceding element is provided
818  const char* rec{C_("Finance::Quote", "recommended")};
819  //Translators: Means that one of the indicated elements is required
820  const char* oot{C_("Finance::Quote", "one of these")};
821  //Translators: Means that a required element wasn't reported. The *s are for emphasis.
822  const char* miss{C_("Finance::Quote", "**missing**")};
823 
824  const std::string miss_str{miss};
825  auto outline{[](const char* label, std::string value, const char* pointer, const char* req) {
826  std::cout << std::setw(12) << std::right << label << std::setw(16) << std::left <<
827  value << pointer << req << "\n";
828  }};
829  std::cout << _("Finance::Quote fields GnuCash uses:") << "\n";
830 //Translators: The stock or Mutual Fund symbol, ISIN, CUSIP, etc.
831  outline(C_("Finance::Quote", "symbol: "), comm_pt.get<char>("symbol", miss), ptr, reqd);
832 //Translators: The date of the quote.
833  outline(C_("Finance::Quote", "date: "), comm_pt.get<char>("date", miss), ptr, rec);
834 //Translators: The quote currency
835  outline(C_("Finance::Quote", "currency: "), comm_pt.get<char>("currency", miss), ptr, reqd);
836  auto last{comm_pt.get<char>("last", "")};
837  auto nav{comm_pt.get<char>("nav", "")};
838  auto price{comm_pt.get<char>("nav", "")};
839  auto no_price{last.empty() && nav.empty() && price.empty()};
840 //Translators: The quote is for the most recent trade on the exchange
841  outline(C_("Finance::Quote", "last: "), no_price ? miss_str : last, dptr, "");
842 //Translators: The quote is for an open-ended mutual fund and represents the net asset value of one unit of the fund at the previous close of trading.
843  outline(C_("Finance::Quote", "nav: "), no_price ? miss_str : nav, ptr, oot);
844 //Translators: The quote is neither a last trade nor an NAV.
845  outline(C_("Finance::Quote", "price: "), no_price ? miss_str : price, uptr, "");
846  std::cout << std::endl;
847 }
848 static const bpt::ptree empty_tree{};
849 
850 static inline const bpt::ptree&
851 get_commodity_data(const bpt::ptree& pt, const std::string& comm)
852 {
853  auto commdata{pt.find(comm)};
854  if (commdata == pt.not_found())
855  {
856  std::cout << comm << " " << _("Finance::Quote returned no data and set no error.") << std::endl;
857  return empty_tree;
858  }
859  auto& comm_pt{commdata->second};
860  auto success = comm_pt.get_optional<bool> ("success");
861  if (!(success && *success))
862  {
863  auto errormsg = comm_pt.get_optional<std::string> ("errormsg");
864  if (errormsg && !errormsg->empty())
865  std::cout << _("Finance::Quote reported a failure for symbol ") <<
866  comm << ": " << *errormsg << std::endl;
867  else
868  std::cout << _("Finance::Quote failed silently to retrieve a quote for symbol ") <<
869  comm << std::endl;
870  return empty_tree;
871  }
872  return comm_pt;
873 }
874 
875 static void
876 show_quotes(const bpt::ptree& pt, const StrVec& commodities, bool verbose)
877 {
878  for (const auto& comm : commodities)
879  {
880  auto comm_pt{get_commodity_data(pt, comm)};
881 
882  if (comm_pt == empty_tree)
883  continue;
884 
885  if (verbose)
886  {
887  std::cout << comm << ":\n";
888  show_verbose_quote(comm_pt);
889  }
890  else
891  {
892  show_gnucash_quote(comm_pt);
893  }
894  }
895 }
896 
897 static void
898 show_currency_quotes(const bpt::ptree& pt, const StrVec& commodities, bool verbose)
899 {
900  auto to_cur{commodities.front()};
901  for (const auto& comm : commodities)
902  {
903  if (comm == to_cur)
904  continue;
905 
906  auto comm_pt{get_commodity_data(pt, comm)};
907 
908  if (comm_pt == empty_tree)
909  continue;
910 
911  if (verbose)
912  {
913  std::cout << comm << ":\n";
914  show_verbose_quote(comm_pt);
915  }
916  else
917  {
918  std::cout << "1 " << comm << " = " <<
919  comm_pt.get<char>("last", "Not Found") << " " << to_cur << "\n";
920  }
921  std::cout << std::endl;
922  }
923 }
924 
925 static std::string
926 parse_quotesource_error(const std::string& line)
927 {
928  std::string err_str;
929  if (line == "invalid_json\n")
930  {
931  err_str += _("GnuCash submitted invalid json to Finance::Quote. The details were logged.");
932  }
933  else if (line.substr(0, 15) == "missing_modules")
934  {
935  PERR("Missing Finance::Quote Dependencies: %s",
936  line.substr(17).c_str());
937  err_str += _("Perl is missing the following modules. Please see https://wiki.gnucash.org/wiki/Online_Quotes#Finance::Quote for detailed corrective action. ");
938  err_str += line.substr(17);
939  }
940  else
941  {
942  PERR("Unrecognized Finance::Quote Error %s", line.c_str());
943  err_str +=_("Unrecognized Finance::Quote Error: ");
944  err_str += line;
945  }
946  err_str += "\n";
947  return err_str;
948 }
949 
950 /********************************************************************
951  * gnc_quotes_get_quotable_commodities
952  * list commodities in a given namespace that get price quotes
953  ********************************************************************/
954 /* Helper function to be passed to g_list_for_each applied to the result
955  * of gnc_commodity_namespace_get_commodity_list.
956  */
957 static void
958 get_quotables_helper1 (gpointer value, gpointer data)
959 {
960  auto l = static_cast<CommVec *> (data);
961  auto comm = static_cast<gnc_commodity *> (value);
962  auto quote_flag = gnc_commodity_get_quote_flag (comm);
963  auto quote_source = gnc_commodity_get_quote_source (comm);
964  auto quote_source_supported = gnc_quote_source_get_supported (quote_source);
965 
966  if (!quote_flag ||
967  !quote_source || !quote_source_supported)
968  return;
969  l->push_back (comm);
970 }
971 
972 // Helper function to be passed to gnc_commodity_table_for_each
973 static gboolean
974 get_quotables_helper2 (gnc_commodity *comm, gpointer data)
975 {
976  auto l = static_cast<CommVec *> (data);
977  auto quote_flag = gnc_commodity_get_quote_flag (comm);
978  auto quote_source = gnc_commodity_get_quote_source (comm);
979  auto quote_source_supported = gnc_quote_source_get_supported (quote_source);
980 
981  if (!quote_flag ||
982  !quote_source || !quote_source_supported)
983  return TRUE;
984  l->push_back (comm);
985  return TRUE;
986 }
987 
988 CommVec
989 gnc_quotes_get_quotable_commodities (const gnc_commodity_table * table)
990 {
991  gnc_commodity_namespace * ns = NULL;
992  const char *name_space;
993  GList * nslist, * tmp;
994  CommVec l;
995  regex_t pattern;
996  const char *expression = gnc_prefs_get_namespace_regexp ();
997 
998  // ENTER("table=%p, expression=%s", table, expression);
999  if (!table)
1000  return CommVec ();
1001 
1002  if (expression && *expression)
1003  {
1004  if (regcomp (&pattern, expression, REG_EXTENDED | REG_ICASE) != 0)
1005  {
1006  // LEAVE ("Cannot compile regex");
1007  return CommVec ();
1008  }
1009 
1011  for (tmp = nslist; tmp; tmp = tmp->next)
1012  {
1013  name_space = static_cast<const char *> (tmp->data);
1014  if (regexec (&pattern, name_space, 0, NULL, 0) == 0)
1015  {
1016  // DEBUG ("Running list of %s commodities", name_space);
1017  ns = gnc_commodity_table_find_namespace (table, name_space);
1018  if (ns)
1019  {
1020  auto cm_list = gnc_commodity_namespace_get_commodity_list (ns);
1021  g_list_foreach (cm_list, &get_quotables_helper1, (gpointer) &l);
1022  g_list_free (cm_list);
1023  }
1024  }
1025  }
1026  g_list_free (nslist);
1027  regfree (&pattern);
1028  }
1029  else
1030  {
1031  gnc_commodity_table_foreach_commodity (table, get_quotables_helper2,
1032  (gpointer) &l);
1033  }
1034  //LEAVE ("list head %p", &l);
1035  return l;
1036 }
1037 
1038 /* Public interface functions */
1039 // Constructor - checks for presence of Finance::Quote and import version and quote sources
1041 {
1042  try
1043  {
1044  m_impl = std::make_unique<GncQuotesImpl>();
1045  } catch (const GncQuoteSourceError &err) {
1046  throw(GncQuoteException(err.what()));
1047  }
1048 }
1049 
1050 
1051 void
1052 GncQuotes::fetch (QofBook *book)
1053 {
1054  m_impl->fetch (book);
1055 }
1056 
1057 void GncQuotes::fetch (CommVec& commodities)
1058 {
1059  m_impl->fetch (commodities);
1060 }
1061 
1062 void GncQuotes::fetch (gnc_commodity *comm)
1063 {
1064  m_impl->fetch (comm);
1065 }
1066 
1067 void GncQuotes::report (const char* source, const StrVec& commodities,
1068  bool verbose)
1069 {
1070  m_impl->report(source, commodities, verbose);
1071 }
1072 
1073 const std::string& GncQuotes::version() noexcept
1074 {
1075  return m_impl->version ();
1076 }
1077 
1078 const QuoteSources& GncQuotes::sources() noexcept
1079 {
1080  return m_impl->sources ();
1081 }
1082 
1083 GncQuotes::~GncQuotes() = default;
1084 
1085 bool
1087 {
1088  return m_impl->had_failures();
1089 }
1090 
1091 const QFVec&
1093 {
1094  return m_impl->failures();
1095 }
1096 
1097 const std::string
1098 GncQuotes::report_failures() noexcept
1099 {
1100  return m_impl->report_failures();
1101 }
GNCPrice * gnc_price_create(QofBook *book)
gnc_price_create - returns a newly allocated and initialized price with a reference count of 1...
gboolean gnc_commodity_table_foreach_commodity(const gnc_commodity_table *table, gboolean(*f)(gnc_commodity *cm, gpointer user_data), gpointer user_data)
Call a function once for each commodity in the commodity table.
gnc_commodity_table * gnc_commodity_table_get_table(QofBook *book)
Returns the commodity table associated with a book.
gboolean gnc_commodity_is_currency(const gnc_commodity *cm)
Checks to see if the specified commodity is an ISO 4217 recognized currency or a legacy currency...
GnuCash DateTime class.
gchar * gnc_prefs_get_string(const gchar *group, const gchar *pref_name)
Get a string value from the preferences backend.
const char * gnc_commodity_get_mnemonic(const gnc_commodity *cm)
Retrieve the mnemonic for the specified commodity.
utility functions for the GnuCash UI
#define PINFO(format, args...)
Print an informational note.
Definition: qoflog.h:256
gboolean gnc_commodity_get_quote_flag(const gnc_commodity *cm)
Retrieve the automatic price quote flag for the specified commodity.
Commodity handling public routines (C++ api)
gboolean gnc_pricedb_add_price(GNCPriceDB *db, GNCPrice *p)
Add a price to the pricedb.
gboolean gnc_quote_source_get_supported(const gnc_quote_source *source)
Given a gnc_quote_source data structure, return the flag that indicates whether this particular quote...
bool had_failures() noexcept
Report if there were quotes requested but not retrieved.
The primary numeric class for representing amounts and values.
Definition: gnc-numeric.hpp:60
const char * gnc_commodity_get_namespace(const gnc_commodity *cm)
Retrieve the namespace for the specified commodity.
#define PERR(format, args...)
Log a serious error.
Definition: qoflog.h:244
GList * gnc_commodity_namespace_get_commodity_list(const gnc_commodity_namespace *name_space)
Return a list of all commodity data structures in the specified namespace.
GNCPriceDB * gnc_pricedb_get_db(QofBook *book)
Return the pricedb associated with the book.
gnc_commodity * gnc_default_currency(void)
Return the default currency set by the user.
#define PWARN(format, args...)
Log a warning.
Definition: qoflog.h:250
QofBook * qof_session_get_book(const QofSession *session)
Returns the QofBook of this session.
Definition: qofsession.cpp:574
GList * gnc_commodity_table_get_namespaces(const gnc_commodity_table *table)
Return a list of all namespaces in the commodity table.
const QFVec & failures() noexcept
Report the commodities for which quotes were requested but not successfully retrieved.
GncQuotes()
Create a GncQuotes object.
Encapsulate all the information about a dataset.
Generic api to store and retrieve preferences.
gnc_quote_source * gnc_commodity_get_quote_source(const gnc_commodity *cm)
Retrieve the automatic price quote source for the specified commodity.
gnc_commodity_namespace * gnc_commodity_table_find_namespace(const gnc_commodity_table *table, const char *name_space)
This function finds a commodity namespace in the set of existing commodity namespaces.
void report(const char *source, const StrVec &commodities, bool verbose=false)
Report quote results from Finance::Quote to std::cout.
void fetch(QofBook *book)
Fetch quotes for all commodities in our db that have a quote source set.
gint64 time64
Most systems that are currently maintained, including Microsoft Windows, BSD-derived Unixes and Linux...
Definition: gnc-date.h:87
const char * gnc_quote_source_get_internal_name(const gnc_quote_source *source)
Given a gnc_quote_source data structure, return the internal name of this quote source.
const std::string & version() noexcept
Get the installed Finance::Quote version.
const QuoteSources & sources() noexcept
Get the available Finance::Quote sources as a std::vector.
Commodity handling public routines.
gboolean gnc_commodity_equiv(const gnc_commodity *a, const gnc_commodity *b)
This routine returns TRUE if the two commodities are equivalent.
GnuCash Date class.