diff --git a/libgnucash/app-utils/gnc-quotes.cpp b/libgnucash/app-utils/gnc-quotes.cpp index 03bee51d4a..0f20f2a6fa 100644 --- a/libgnucash/app-utils/gnc-quotes.cpp +++ b/libgnucash/app-utils/gnc-quotes.cpp @@ -92,6 +92,7 @@ public: void fetch (QofBook *book); void fetch (CommVec& commodities); void fetch (gnc_commodity *comm); + void report (const char* source, const StrVec& commodities, bool verbose); const std::string& version() noexcept { return m_version.empty() ? not_found : m_version; } const QuoteSources& sources() noexcept { return m_sources; } @@ -101,8 +102,10 @@ public: std::string report_failures() noexcept; private: + std::string query_fq (const char* source, const StrVec& commoditites); std::string query_fq (const CommVec&); - void parse_quotes (const std::string& quote_str, const CommVec& commodities); + bpt::ptree parse_quotes (const std::string& quote_str); + void create_quotes(const bpt::ptree& pt, const CommVec& comm_vec); std::string comm_vec_to_json_string(const CommVec&) const; GNCPrice* parse_one_quote(const bpt::ptree&, gnc_commodity*); @@ -133,6 +136,9 @@ private: }; +static void show_quotes(const bpt::ptree& pt, const StrVec& commodities, bool verbose); +static void show_currency_quotes(const bpt::ptree& pt, const StrVec& commodities, bool verbose); + static const std::string empty_string{}; GncFQQuoteSource::GncFQQuoteSource() : @@ -293,9 +299,42 @@ GncQuotesImpl::fetch (CommVec& commodities) m_failures.clear(); if (commodities.empty()) return; + try + { + auto quote_str{query_fq (commodities)}; + auto ptree{parse_quotes (quote_str)}; + create_quotes(ptree, commodities); + } + catch (const GncQuoteException& err) + { + std::cerr << _("Finance::Quote retrieval failed with error ") << err.what() << std::endl; + } +} - auto quote_str{query_fq (commodities)}; - parse_quotes (quote_str, commodities); +void +GncQuotesImpl::report (const char* source, const StrVec& commodities, + bool verbose) +{ + bool is_currency{source && strcmp(source, "currency") == 0}; + m_failures.clear(); + if (commodities.empty()) + { + std::cerr << _("There were no commodities for which to retrieve quotes.") << std::endl; + return; + } + try + { + auto quote_str{query_fq (source, commodities)}; + auto ptree{parse_quotes (quote_str)}; + if (is_currency) + show_currency_quotes(ptree, commodities, verbose); + else + show_quotes(ptree, commodities, verbose); + } + catch (const GncQuoteException& err) + { + std::cerr << _("Finance::Quote retrieval failed with error ") << err.what() << std::endl; + } } const QFVec& @@ -388,11 +427,10 @@ GncQuotesImpl::comm_vec_to_json_string (const CommVec& comm_vec) const return result.str(); } -std::string -GncQuotesImpl::query_fq (const CommVec& comm_vec) +static inline std::string +get_quotes(const std::string& json_str, const std::unique_ptr& qs) { - auto json_str{comm_vec_to_json_string(comm_vec)}; - auto [rv, quotes, errors] = m_quotesource->get_quotes(json_str); + auto [rv, quotes, errors] = qs->get_quotes(json_str); std::string answer; if (rv == 0) @@ -408,13 +446,47 @@ GncQuotesImpl::query_fq (const CommVec& comm_vec) throw(GncQuoteException(err_str)); } - return answer; // for (auto line : quotes) // PINFO("Output line retrieved from wrapper:\n%s", line.c_str()); // // for (auto line : errors) // PINFO("Error line retrieved from wrapper:\n%s",line.c_str());˚ + return answer; +} + +std::string +GncQuotesImpl::query_fq (const char* source, const StrVec& commodities) +{ + bpt::ptree pt; + auto is_currency{strcmp(source, "currency") == 0}; + + if (is_currency && commodities.size() < 2) + throw(GncQuoteException(_("Currency quotes requires at least two currencies"))); + + if (is_currency) + pt.put("defaultcurrency", commodities[0].c_str()); + else + pt.put("defaultcurrency", gnc_commodity_get_mnemonic(m_dflt_curr)); + + std::for_each(is_currency ? ++commodities.cbegin() : commodities.cbegin(), + commodities.cend(), + [this, source, &pt](auto sym) + { + std::string key{source}; + key += "." + sym; + pt.put(key, ""); + }); + std::ostringstream result; + bpt::write_json(result, pt); + return get_quotes(result.str(), m_quotesource); +} + +std::string +GncQuotesImpl::query_fq (const CommVec& comm_vec) +{ + auto json_str{comm_vec_to_json_string(comm_vec)}; + return get_quotes(json_str, m_quotesource); } struct PriceParams @@ -636,8 +708,8 @@ GncQuotesImpl::parse_one_quote(const bpt::ptree& pt, gnc_commodity* comm) return gnc_price; } -void -GncQuotesImpl::parse_quotes (const std::string& quote_str, const CommVec& comm_vec) +bpt::ptree +GncQuotesImpl::parse_quotes (const std::string& quote_str) { bpt::ptree pt; std::istringstream ss {quote_str}; @@ -671,7 +743,12 @@ GncQuotesImpl::parse_quotes (const std::string& quote_str, const CommVec& comm_v error_msg += what; throw(GncQuoteException(error_msg)); } + return pt; +} +void +GncQuotesImpl::create_quotes (const bpt::ptree& pt, const CommVec& comm_vec) +{ auto pricedb{gnc_pricedb_get_db(m_book)}; for (auto comm : comm_vec) { @@ -685,8 +762,134 @@ GncQuotesImpl::parse_quotes (const std::string& quote_str, const CommVec& comm_v } } +static void +show_verbose_quote(const bpt::ptree& comm_pt) +{ + std::for_each(comm_pt.begin(), comm_pt.end(), + [](auto elem) { + std::cout << std::setw(12) << std::right << elem.first << " => " << + std::left << elem.second.data() << "\n"; + }); + std::cout << std::endl; +} + +static void +show_gnucash_quote(const bpt::ptree& comm_pt) +{ + constexpr const char* ptr{"<=== "}; + constexpr const char* dptr{"<=\\ "}; + constexpr const char* uptr{"<=/ "}; + //Translators: Means that the preceding element is required + const char* reqd{C_("Finance::Quote", "required")}; + //Translators: Means that the quote will work best if the preceding element is provided + const char* rec{C_("Finance::Quote", "recommended")}; + //Translators: Means that one of the indicated elements is required + const char* oot{C_("Finance::Quote", "one of these")}; + //Translators: Means that the preceding element is optional + const char* opt{C_("Finance::Quote", "optional")}; + //Translators: Means that a required element wasn't reported. The *s are for emphasis. + const char* miss{C_("Finance::Quote", "**missing**")}; + + const std::string miss_str{miss}; + auto outline{[](const char* label, std::string value, const char* pointer, const char* req) { + std::cout << std::setw(12) << std::right << label << std::setw(16) << std::left << + value << pointer << req << "\n"; + }}; + std::cout << _("Finance::Quote fields GnuCash uses:") << "\n"; +//Translators: The stock or Mutual Fund symbol, ISIN, CUSIP, etc. + outline(C_("Finance::Quote", "symbol: "), comm_pt.get("symbol", miss), ptr, reqd); +//Translators: The date of the quote. + outline(C_("Finance::Quote", "date: "), comm_pt.get("date", miss), ptr, rec); +//Translators: The quote currency + outline(C_("Finance::Quote", "currency: "), comm_pt.get("currency", miss), ptr, reqd); + auto last{comm_pt.get("last", "")}; + auto nav{comm_pt.get("nav", "")}; + auto price{comm_pt.get("nav", "")}; + auto no_price{last.empty() && nav.empty() && price.empty()}; +//Translators: The quote is for the most recent trade on the exchange + outline(C_("Finance::Quote", "last: "), no_price ? miss_str : last, dptr, ""); +//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. + outline(C_("Finance::Quote", "nav: "), no_price ? miss_str : nav, ptr, oot); +//Translators: The quote is neither a last trade nor an NAV. + outline(C_("Finance::Quote", "price: "), no_price ? miss_str : price, uptr, ""); + std::cout << std::endl; +} +static const bpt::ptree empty_tree{}; + +static inline const bpt::ptree& +get_commodity_data(const bpt::ptree& pt, const std::string& comm) +{ + auto commdata{pt.find(comm)}; + if (commdata == pt.not_found()) + { + std::cout << comm << " " << _("Finance::Quote returned no data and set no error.") << std::endl; + return empty_tree; + } + auto& comm_pt{commdata->second}; + auto success = comm_pt.get_optional ("success"); + if (!(success && *success)) + { + auto errormsg = comm_pt.get_optional ("errormsg"); + if (errormsg && !errormsg->empty()) + std::cout << _("Finance::Quote reported a failure for symbol ") << + comm << ": " << *errormsg << std::endl; + else + std::cout << _("Finance::Quote failed silently to retrieve a quote for symbol ") << + comm << std::endl; + return empty_tree; + } + return comm_pt; +} + +static void +show_quotes(const bpt::ptree& pt, const StrVec& commodities, bool verbose) +{ + for (auto comm : commodities) + { + auto comm_pt{get_commodity_data(pt, comm)}; + + if (comm_pt == empty_tree) + continue; + + if (verbose) + { + std::cout << comm << ":\n"; + show_verbose_quote(comm_pt); + } + else + { + show_gnucash_quote(comm_pt); + } + } +} +static void +show_currency_quotes(const bpt::ptree& pt, const StrVec& commodities, bool verbose) +{ + auto to_cur{commodities.front()}; + for (auto comm : commodities) + { + if (comm == to_cur) + continue; + + auto comm_pt{get_commodity_data(pt, comm)}; + if (comm_pt == empty_tree) + continue; + + if (verbose) + { + std::cout << comm << ":\n"; + show_verbose_quote(comm_pt); + } + else + { + std::cout << "1 " << comm << " = " << + comm_pt.get("last", "Not Found") << " " << to_cur << "\n"; + } + std::cout << std::endl; + } +} /******************************************************************** * gnc_quotes_get_quotable_commodities * list commodities in a given namespace that get price quotes @@ -805,6 +1008,12 @@ void GncQuotes::fetch (gnc_commodity *comm) m_impl->fetch (comm); } +void GncQuotes::report (const char* source, const StrVec& commodities, + bool verbose) +{ + m_impl->report(source, commodities, verbose); +} + const std::string& GncQuotes::version() noexcept { return m_impl->version (); diff --git a/libgnucash/app-utils/gnc-quotes.hpp b/libgnucash/app-utils/gnc-quotes.hpp index a35d097052..5cfa618504 100644 --- a/libgnucash/app-utils/gnc-quotes.hpp +++ b/libgnucash/app-utils/gnc-quotes.hpp @@ -91,7 +91,14 @@ public: * @note Commodity must have a quote source set or the call will silently fail. */ void fetch (gnc_commodity *comm); - + /** Report quote results from Finance::Quote to std::cout. + * + * @param source A valid quote source + * @param commodities A std::vector of symbols to request quotes for. + * @note If requesting currency rates the first symbol is the to-currency and the rest are from-currencies. For example, {"USD", "EUR", "CAD"} will print the price of 1 Euro and 1 Canadian Dollar in US Dollars. + * @param verbose Ignored for currency queries. If false it will print the six fields GnuCash uses regardless of whether a value was returned; if true it will print all of the fields for which Finanace::Quote returned values. + */ + void report (const char* source, const StrVec& commodities, bool verbose = false); /** Get the installed Finance::Quote version * * @return the Finance::Quote version string diff --git a/libgnucash/app-utils/test/gtest-gnc-quotes.cpp b/libgnucash/app-utils/test/gtest-gnc-quotes.cpp index ebcd66d04b..93dc56e832 100644 --- a/libgnucash/app-utils/test/gtest-gnc-quotes.cpp +++ b/libgnucash/app-utils/test/gtest-gnc-quotes.cpp @@ -142,10 +142,10 @@ TEST_F(GncQuotesTest, online_wiggle) quotes.fetch(m_book); auto pricedb{gnc_pricedb_get_db(m_book)}; auto failures{quotes.failures()}; - ASSERT_EQ(2u, failures.size()); + ASSERT_EQ(1u, failures.size()); EXPECT_EQ(GncQuoteError::QUOTE_FAILED, std::get<2>(failures[0])); - EXPECT_EQ(GncQuoteError::QUOTE_FAILED, std::get<2>(failures[1])); - EXPECT_EQ(2u, gnc_pricedb_get_num_prices(pricedb)); +// EXPECT_EQ(GncQuoteError::QUOTE_FAILED, std::get<2>(failures[1])); + EXPECT_EQ(3u, gnc_pricedb_get_num_prices(pricedb)); } #endif @@ -164,6 +164,29 @@ TEST_F(GncQuotesTest, offline_wiggle) EXPECT_EQ(3u, gnc_pricedb_get_num_prices(pricedb)); } +TEST_F(GncQuotesTest, offline_report) +{ + StrVec quote_vec{ + "{\"AAPL\":{\"eps\":6.05,\"success\":1,\"year_range\":\" 129.04 - 182.94\",\"currency\":\"USD\",\"exchange\":\"Sourced from Yahoo Finance (as JSON)\",\"volume\":73539475,\"close\":157.22,\"high\":158.39,\"open\":156.64,\"div_yield\":0.5660857,\"last\":157.96,\"isodate\":\"2022-09-01\",\"method\":\"yahoo_json\",\"name\":\"AAPL (Apple Inc.)\",\"pe\":26.10909,\"low\":154.67,\"type\":\"EQUITY\",\"symbol\":\"AAPL\",\"date\":\"09/01/2022\"},\"HPE\":{\"symbol\":\"HPE\",\"date\":\"09/01/2022\",\"low\":13.13,\"type\":\"EQUITY\",\"method\":\"yahoo_json\",\"name\":\"HPE (Hewlett Packard Enterprise Comp)\",\"isodate\":\"2022-09-01\",\"pe\":4.7921147,\"last\":13.37,\"high\":13.535,\"close\":13.6,\"open\":13.5,\"div_yield\":3.5294116,\"volume\":16370483,\"exchange\":\"Sourced from Yahoo Finance (as JSON)\",\"currency\":\"USD\",\"year_range\":\" 12.4 - 17.76\",\"eps\":2.79,\"success\":1},\"FKCM\":{\"success\":0,\"symbol\":\"FKCM\",\"errormsg\":\"Error retrieving quote for FKCM - no listing for this name found. Please check symbol and the two letter extension (if any)\"}}" + }; + StrVec commodities{"AAPL", "HPE", "FKCM"}; + StrVec err_vec; + GncQuotesImpl quotes(m_book, std::make_unique(std::move(quote_vec), std::move(err_vec))); + quotes.report("yahoo_json", commodities, false); + quotes.report("yahoo_json", commodities, true); +} + +TEST_F(GncQuotesTest, offline_currency_report) +{ + StrVec quote_vec{ + "{\"EUR\":{\"symbol\":\"EUR\",\"currency\":\"USD\",\"success\":\"1\",\"inverted\":0,\"last\":1.0004}}"}; + StrVec commodities{"USD", "EUR"}; + StrVec err_vec; + GncQuotesImpl quotes(m_book, std::make_unique(std::move(quote_vec), std::move(err_vec))); + quotes.report("currency", commodities, false); + quotes.report("currency", commodities, true); +} + TEST_F(GncQuotesTest, comvec_fetch) { StrVec quote_vec{