You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1051 lines
34 KiB
1051 lines
34 KiB
/***************************************************************************
|
|
webpricequote.cpp
|
|
-------------------
|
|
begin : Thu Dec 30 2004
|
|
copyright : (C) 2004 by Ace Jones
|
|
email : Ace Jones <acejones@users.sourceforge.net>
|
|
***************************************************************************/
|
|
|
|
/***************************************************************************
|
|
* *
|
|
* This program is free software; you can redistribute it and/or modify *
|
|
* it under the terms of the GNU General Public License as published by *
|
|
* the Free Software Foundation; either version 2 of the License, or *
|
|
* (at your option) any later version. *
|
|
* *
|
|
***************************************************************************/
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// QT Headers
|
|
|
|
#include <tqfile.h>
|
|
#include <tqregexp.h>
|
|
#include <tqtextstream.h>
|
|
#include <tqprocess.h>
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// KDE Headers
|
|
|
|
#include <kio/netaccess.h>
|
|
#include <kio/scheduler.h>
|
|
#include <kurl.h>
|
|
#include <klocale.h>
|
|
#include <kdebug.h>
|
|
#include <kapplication.h>
|
|
#include <kconfig.h>
|
|
#include <kglobal.h>
|
|
#include <kstandarddirs.h>
|
|
#include <kcalendarsystem.h>
|
|
#include <ktempfile.h>
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Project Headers
|
|
|
|
#include "../mymoney/mymoneyexception.h"
|
|
#include "mymoneyqifprofile.h"
|
|
#include "webpricequote.h"
|
|
|
|
// define static members
|
|
TQString WebPriceQuote::m_financeQuoteScriptPath;
|
|
TQStringList WebPriceQuote::m_financeQuoteSources;
|
|
|
|
TQString * WebPriceQuote::lastErrorMsg;
|
|
int WebPriceQuote::lastErrorCode = 0;
|
|
|
|
WebPriceQuote::WebPriceQuote( TQObject* _parent, const char* _name ):
|
|
TQObject( _parent, _name )
|
|
{
|
|
m_financeQuoteScriptPath =
|
|
KGlobal::dirs()->findResource("appdata", TQString("misc/financequote.pl"));
|
|
connect(&m_filter,TQT_SIGNAL(processExited(const TQString&)),this,TQT_SLOT(slotParseQuote(const TQString&)));
|
|
}
|
|
|
|
WebPriceQuote::~WebPriceQuote()
|
|
{
|
|
}
|
|
|
|
bool WebPriceQuote::launch( const TQString& _symbol, const TQString& _id, const TQString& _sourcename )
|
|
{
|
|
if (_sourcename.tqcontains("Finance::Quote"))
|
|
return (launchFinanceQuote (_symbol, _id, _sourcename));
|
|
else
|
|
return (launchNative (_symbol, _id, _sourcename));
|
|
}
|
|
|
|
bool WebPriceQuote::launchNative( const TQString& _symbol, const TQString& _id, const TQString& _sourcename ) {
|
|
bool result = true;
|
|
m_symbol = _symbol;
|
|
m_id = _id;
|
|
|
|
// emit status(TQString("(Debug) symbol=%1 id=%2...").tqarg(_symbol,_id));
|
|
|
|
// if we're running normally, with a UI, we can just get these the normal way,
|
|
// from the config file
|
|
if ( kapp )
|
|
{
|
|
TQString sourcename = _sourcename;
|
|
if ( sourcename.isEmpty() )
|
|
sourcename = "Yahoo";
|
|
|
|
if ( quoteSources().tqcontains(sourcename) )
|
|
m_source = WebPriceQuoteSource(sourcename);
|
|
else
|
|
emit error(TQString("Source <%1> does not exist.").tqarg(sourcename));
|
|
}
|
|
// otherwise, if we have no kapp, we have no config. so we just get them from
|
|
// the defaults
|
|
else
|
|
{
|
|
if ( _sourcename.isEmpty() )
|
|
m_source = defaultQuoteSources()["Yahoo"];
|
|
else
|
|
m_source = defaultQuoteSources()[_sourcename];
|
|
}
|
|
|
|
KURL url;
|
|
|
|
// if the source has room for TWO symbols..
|
|
if ( m_source.m_url.tqcontains("%2") )
|
|
{
|
|
// this is a two-symbol quote. split the symbol into two. valid symbol
|
|
// characters are: 0-9, A-Z and the dot. anything else is a separator
|
|
TQRegExp splitrx("([0-9a-z\\.]+)[^a-z0-9]+([0-9a-z\\.]+)",false /*case sensitive*/);
|
|
|
|
// if we've truly found 2 symbols delimited this way...
|
|
if ( splitrx.search(m_symbol) != -1 )
|
|
url = KURL::fromPathOrURL(m_source.m_url.tqarg(splitrx.cap(1),splitrx.cap(2)));
|
|
else
|
|
kdDebug(2) << "WebPriceQuote::launch() did not tqfind 2 symbols" << endl;
|
|
}
|
|
else
|
|
// a regular one-symbol quote
|
|
url = KURL::fromPathOrURL(m_source.m_url.tqarg(m_symbol));
|
|
|
|
// If we're running a non-interactive session (with no UI), we can't
|
|
// use KIO::NetAccess, so we have to get our web data the old-fashioned
|
|
// way... with 'wget'.
|
|
//
|
|
// Note that a 'non-interactive' session right now means only the test
|
|
// cases. Although in the future if KMM gains a non-UI mode, this would
|
|
// still be useful
|
|
if ( ! kapp && ! url.isLocalFile() )
|
|
url = KURL::fromPathOrURL("/usr/bin/wget -O - " + url.prettyURL());
|
|
|
|
if ( url.isLocalFile() )
|
|
{
|
|
emit status(TQString("Executing %1...").tqarg(url.path()));
|
|
|
|
m_filter.clearArguments();
|
|
m_filter << TQStringList::split(" ",url.path());
|
|
m_filter.setSymbol(m_symbol);
|
|
|
|
// if we're running non-interactive, we'll need to block.
|
|
// otherwise, just let us know when it's done.
|
|
KProcess::RunMode mode = KProcess::NotifyOnExit;
|
|
if ( ! kapp )
|
|
mode = KProcess::Block;
|
|
|
|
if(m_filter.start(mode, KProcess::All))
|
|
{
|
|
result = true;
|
|
m_filter.resume();
|
|
}
|
|
else
|
|
{
|
|
emit error(TQString("Unable to launch: %1").tqarg(url.path()));
|
|
slotParseQuote(TQString());
|
|
}
|
|
}
|
|
else
|
|
{
|
|
emit status(TQString("Fetching URL %1...").tqarg(url.prettyURL()));
|
|
|
|
TQString tmpFile;
|
|
if( download( url, tmpFile, NULL ) )
|
|
{
|
|
kdDebug(2) << "Downloaded " << tmpFile << endl;
|
|
TQFile f(tmpFile);
|
|
if ( f.open( IO_ReadOnly ) )
|
|
{
|
|
result = true;
|
|
TQString quote = TQTextStream(&f).read();
|
|
f.close();
|
|
slotParseQuote(quote);
|
|
}
|
|
else
|
|
{
|
|
slotParseQuote(TQString());
|
|
}
|
|
removeTempFile( tmpFile );
|
|
}
|
|
else
|
|
{
|
|
emit error(KIO::NetAccess::lastErrorString());
|
|
slotParseQuote(TQString());
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
void WebPriceQuote::removeTempFile(const TQString& tmpFile)
|
|
{
|
|
if(tmpFile == m_tmpFile) {
|
|
unlink(tmpFile);
|
|
m_tmpFile = TQString();
|
|
}
|
|
}
|
|
|
|
bool WebPriceQuote::download(const KURL& u, TQString & target, TQWidget* window)
|
|
{
|
|
m_tmpFile = TQString();
|
|
|
|
// the following code taken and adapted from KIO::NetAccess::download()
|
|
if (target.isEmpty())
|
|
{
|
|
KTempFile tmpFile;
|
|
target = tmpFile.name();
|
|
m_tmpFile = target;
|
|
}
|
|
|
|
KURL dest;
|
|
dest.setPath( target );
|
|
|
|
|
|
// the following code taken and adapted from KIO::NetAccess::filecopyInternal()
|
|
bJobOK = true; // success unless further error occurs
|
|
|
|
KIO::Scheduler::checkSlaveOnHold(true);
|
|
KIO::Job * job = KIO::file_copy( u, dest, -1, true, false, false );
|
|
job->setWindow (window);
|
|
job->addMetaData("cache", "reload"); // bypass cache
|
|
connect( job, TQT_SIGNAL( result (KIO::Job *) ),
|
|
this, TQT_SLOT( slotResult (KIO::Job *) ) );
|
|
|
|
enter_loop();
|
|
return bJobOK;
|
|
|
|
}
|
|
|
|
// The following parts are copied and adjusted from KIO::NetAccess
|
|
|
|
// If a troll sees this, he kills me
|
|
void qt_enter_modal( TQWidget *widget );
|
|
void qt_leave_modal( TQWidget *widget );
|
|
|
|
void WebPriceQuote::enter_loop(void)
|
|
{
|
|
TQWidget dummy(0,0,WType_Dialog | WShowModal);
|
|
dummy.setFocusPolicy( TQ_NoFocus );
|
|
qt_enter_modal(&dummy);
|
|
tqApp->enter_loop();
|
|
qt_leave_modal(&dummy);
|
|
}
|
|
|
|
void WebPriceQuote::slotResult( KIO::Job * job )
|
|
{
|
|
lastErrorCode = job->error();
|
|
bJobOK = !job->error();
|
|
if ( !bJobOK )
|
|
{
|
|
if ( !lastErrorMsg )
|
|
lastErrorMsg = new TQString;
|
|
*lastErrorMsg = job->errorString();
|
|
}
|
|
|
|
tqApp->exit_loop();
|
|
}
|
|
// The above parts are copied and adjusted from KIO::NetAccess
|
|
|
|
bool WebPriceQuote::launchFinanceQuote ( const TQString& _symbol, const TQString& _id,
|
|
const TQString& _sourcename ) {
|
|
bool result = true;
|
|
m_symbol = _symbol;
|
|
m_id = _id;
|
|
TQString FTQSource = _sourcename.section (" ", 1);
|
|
m_source = WebPriceQuoteSource (_sourcename, m_financeQuoteScriptPath,
|
|
"\"([^,\"]*)\",.*", // symbol regexp
|
|
"[^,]*,[^,]*,\"([^\"]*)\"", // price regexp
|
|
"[^,]*,([^,]*),.*", // date regexp
|
|
"%y-%m-%d"); // date format
|
|
|
|
//emit status(TQString("(Debug) symbol=%1 id=%2...").tqarg(_symbol,_id));
|
|
|
|
|
|
m_filter.clearArguments();
|
|
m_filter << "perl" << m_financeQuoteScriptPath << FTQSource << KProcess::quote(_symbol);
|
|
m_filter.setUseShell(true);
|
|
m_filter.setSymbol(m_symbol);
|
|
emit status(TQString("Executing %1 %2 %3...").tqarg(m_financeQuoteScriptPath).tqarg(FTQSource).tqarg(_symbol));
|
|
|
|
// if we're running non-interactive, we'll need to block.
|
|
// otherwise, just let us know when it's done.
|
|
KProcess::RunMode mode = KProcess::NotifyOnExit;
|
|
if ( ! kapp )
|
|
mode = KProcess::Block;
|
|
|
|
if(m_filter.start(mode, KProcess::All))
|
|
{
|
|
result = true;
|
|
m_filter.resume();
|
|
}
|
|
else
|
|
{
|
|
emit error(TQString("Unable to launch: %1").tqarg(m_financeQuoteScriptPath));
|
|
slotParseQuote(TQString());
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
void WebPriceQuote::slotParseQuote(const TQString& _quotedata)
|
|
{
|
|
TQString quotedata = _quotedata;
|
|
bool gotprice = false;
|
|
bool gotdate = false;
|
|
|
|
// kdDebug(2) << "WebPriceQuote::slotParseQuote( " << _quotedata << " ) " << endl;
|
|
|
|
if ( ! quotedata.isEmpty() )
|
|
{
|
|
if(!m_source.m_skipStripping) {
|
|
//
|
|
// First, remove extranous non-data elements
|
|
//
|
|
|
|
// HTML tags
|
|
quotedata.remove(TQRegExp("<[^>]*>"));
|
|
|
|
// &...;'s
|
|
quotedata.tqreplace(TQRegExp("&\\w+;")," ");
|
|
|
|
// Extra white space
|
|
quotedata = quotedata.simplifyWhiteSpace();
|
|
}
|
|
|
|
#if KMM_DEBUG
|
|
// Enable to get a look at the data coming back from the source after it's stripped
|
|
TQFile file("stripped.txt");
|
|
if ( file.open( IO_WriteOnly ) )
|
|
{
|
|
TQTextStream( &file ) << quotedata;
|
|
file.close();
|
|
}
|
|
#endif
|
|
|
|
TQRegExp symbolRegExp(m_source.m_sym);
|
|
TQRegExp dateRegExp(m_source.m_date);
|
|
TQRegExp priceRegExp(m_source.m_price);
|
|
|
|
if( symbolRegExp.search(quotedata) > -1)
|
|
emit status(i18n("Symbol found: %1").tqarg(symbolRegExp.cap(1)));
|
|
|
|
if(priceRegExp.search(quotedata)> -1)
|
|
{
|
|
gotprice = true;
|
|
|
|
// Deal with european quotes that come back as X.XXX,XX or XX,XXX
|
|
//
|
|
// We will make the assumption that ALL prices have a decimal separator.
|
|
// So "1,000" always means 1.0, not 1000.0.
|
|
//
|
|
// Remove all non-digits from the price string except the last one, and
|
|
// set the last one to a period.
|
|
TQString pricestr = priceRegExp.cap(1);
|
|
|
|
int pos = pricestr.tqfindRev(TQRegExp("\\D"));
|
|
if ( pos > 0 )
|
|
{
|
|
pricestr[pos] = '.';
|
|
pos = pricestr.tqfindRev(TQRegExp("\\D"),pos-1);
|
|
}
|
|
while ( pos > 0 )
|
|
{
|
|
pricestr.remove(pos,1);
|
|
pos = pricestr.tqfindRev(TQRegExp("\\D"),pos);
|
|
}
|
|
|
|
m_price = pricestr.toDouble();
|
|
emit status(i18n("Price found: %1 (%2)").tqarg(pricestr).tqarg(m_price));
|
|
}
|
|
|
|
if(dateRegExp.search(quotedata) > -1)
|
|
{
|
|
TQString datestr = dateRegExp.cap(1);
|
|
|
|
MyMoneyDateFormat dateparse(m_source.m_dateformat);
|
|
try
|
|
{
|
|
m_date = dateparse.convertString( datestr,false /*strict*/ );
|
|
gotdate = true;
|
|
emit status(i18n("Date found: %1").tqarg(m_date.toString()));;
|
|
}
|
|
catch (MyMoneyException* e)
|
|
{
|
|
// emit error(i18n("Unable to parse date %1 using format %2: %3").tqarg(datestr,dateparse.format(),e->what()));
|
|
m_date = TQDate::tqcurrentDate();
|
|
gotdate = true;
|
|
delete e;
|
|
}
|
|
}
|
|
|
|
if ( gotprice && gotdate )
|
|
{
|
|
emit quote( m_id, m_symbol, m_date, m_price );
|
|
}
|
|
else
|
|
{
|
|
emit error(i18n("Unable to update price for %1").tqarg(m_symbol));
|
|
emit failed( m_id, m_symbol );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
emit error(i18n("Unable to update price for %1").tqarg(m_symbol));
|
|
emit failed( m_id, m_symbol );
|
|
}
|
|
}
|
|
|
|
TQMap<TQString,WebPriceQuoteSource> WebPriceQuote::defaultQuoteSources(void)
|
|
{
|
|
TQMap<TQString,WebPriceQuoteSource> result;
|
|
|
|
result["Yahoo"] = WebPriceQuoteSource("Yahoo",
|
|
"http://finance.yahoo.com/d/quotes.csv?s=%1&f=sl1d1",
|
|
"\"([^,\"]*)\",.*", // symbolregexp
|
|
"[^,]*,([^,]*),.*", // priceregexp
|
|
"[^,]*,[^,]*,\"([^\"]*)\"", // dateregexp
|
|
"%m %d %y" // dateformat
|
|
);
|
|
|
|
result["Yahoo Currency"] = WebPriceQuoteSource("Yahoo Currency",
|
|
"http://finance.yahoo.com/d/quotes.csv?s=%1%2=X&f=sl1d1",
|
|
"\"([^,\"]*)\",.*", // symbolregexp
|
|
"[^,]*,([^,]*),.*", // priceregexp
|
|
"[^,]*,[^,]*,\"([^\"]*)\"", // dateregexp
|
|
"%m %d %y" // dateformat
|
|
);
|
|
|
|
// 2009-08-20 Yahoo UK has no quotes and has comma separators
|
|
// sl1d1 format for Yahoo UK doesn't seem to give a date ever
|
|
// sl1d3 gives US locale time (9:99pm) and date (mm/dd/yyyy)
|
|
result["Yahoo UK"] = WebPriceQuoteSource("Yahoo UK",
|
|
"http://uk.finance.yahoo.com/d/quotes.csv?s=%1&f=sl1d3",
|
|
"^([^,]*),.*", // symbolregexp
|
|
"^[^,]*,([^,]*),.*", // priceregexp
|
|
"^[^,]*,[^,]*,(.*)", // dateregexp
|
|
"%m/%d/%y" // dateformat
|
|
);
|
|
|
|
// sl1d1 format for Yahoo France doesn't seem to give a date ever
|
|
// sl1d3 gives us time (99h99) and date
|
|
result["Yahoo France"] = WebPriceQuoteSource("Yahoo France",
|
|
"http://fr.finance.yahoo.com/d/quotes.csv?s=%1&f=sl1d3",
|
|
"([^;]*).*", // symbolregexp
|
|
"[^;]*.([^;]*),*", // priceregexp
|
|
"[^;]*.[^;]*...h...([^;]*)", // dateregexp
|
|
"%d/%m/%y" // dateformat
|
|
);
|
|
|
|
result["Globe & Mail"] = WebPriceQuoteSource("Globe & Mail",
|
|
"http://globefunddb.theglobeandmail.com/gishome/plsql/gis.price_history?pi_fund_id=%1",
|
|
TQString(), // symbolregexp
|
|
"Reinvestment Price \\w+ \\d+, \\d+ (\\d+\\.\\d+)", // priceregexp
|
|
"Reinvestment Price (\\w+ \\d+, \\d+)", // dateregexp
|
|
"%m %d %y" // dateformat
|
|
);
|
|
|
|
result["MSN.CA"] = WebPriceQuoteSource("MSN.CA",
|
|
"http://ca.moneycentral.msn.com/investor/quotes/quotes.asp?symbol=%1",
|
|
TQString(), // symbolregexp
|
|
"Net Asset Value (\\d+\\.\\d+)", // priceregexp
|
|
"NAV update (\\d+\\D+\\d+\\D+\\d+)", // dateregexp
|
|
"%d %m %y" // dateformat
|
|
);
|
|
// Finanztreff (replaces VWD.DE) and boerseonline supplied by Micahel Zimmerman
|
|
result["Finanztreff"] = WebPriceQuoteSource("Finanztreff",
|
|
"http://finanztreff.de/kurse_einzelkurs_detail.htn?u=100&i=%1",
|
|
TQString(), // symbolregexp
|
|
"([0-9]+,\\d+).+Gattung:Fonds", // priceregexp
|
|
"\\).(\\d+\\D+\\d+\\D+\\d+)", // dateregexp (doesn't work; date in chart
|
|
"%d.%m.%y" // dateformat
|
|
);
|
|
|
|
result["boerseonline"] = WebPriceQuoteSource("boerseonline",
|
|
"http://www.boerse-online.de/tools/boerse/einzelkurs_kurse.htm?&s=%1",
|
|
TQString(), // symbolregexp
|
|
"Akt\\. Kurs.(\\d+,\\d\\d)", // priceregexp
|
|
"Datum.(\\d+\\.\\d+\\.\\d+)", // dateregexp (doesn't work; date in chart
|
|
"%d.%m.%y" // dateformat
|
|
);
|
|
|
|
// The following two price sources were contributed by
|
|
// Marc Zahnlecker <tf2k@users.sourceforge.net>
|
|
|
|
result["Wallstreet-Online.DE (Default)"] = WebPriceQuoteSource("Wallstreet-Online.DE (Default)",
|
|
"http://www.wallstreet-online.de/si/?k=%1&spid=ws",
|
|
"Symbol:(\\w+)", // symbolregexp
|
|
"Letzter Kurs: ([0-9.]+,\\d+)", // priceregexp
|
|
", (\\d+\\D+\\d+\\D+\\d+)", // dateregexp
|
|
"%d %m %y" // dateformat
|
|
);
|
|
|
|
// This quote source provided by Peter Lord
|
|
// The trading symbol will normally be the SEDOL (see wikipedia) but
|
|
// the flexibility presently (1/2008) in the code will allow use of
|
|
// the ISIN or MEXID (FT specific) codes
|
|
result["Financial Times UK Funds"] = WebPriceQuoteSource("Financial Times UK Funds",
|
|
"http://funds.ft.com/funds/simpleSearch.do?searchArea=%&search=%1",
|
|
"SEDOL[\\ ]*(\\d+.\\d+)", // symbol regexp
|
|
"\\(GBX\\)[\\ ]*([0-9,]*.\\d+)[\\ ]*", // price regexp
|
|
"Valuation date:[\\ ]*(\\d+/\\d+/\\d+)", // date regexp
|
|
"%d/%m/%y" // date format
|
|
);
|
|
|
|
// This quote source provided by Danny Scott
|
|
result["Yahoo Canada"] = WebPriceQuoteSource("Yahoo Canada",
|
|
"http://ca.finance.yahoo.com/q?s=%1",
|
|
"%1", // symbol regexp
|
|
"Last Trade: (\\d+\\.\\d+)", // price regexp
|
|
"day, (.\\D+\\d+\\D+\\d+)", // date regexp
|
|
"%m %d %y" // date format
|
|
);
|
|
|
|
// (tf2k) The "mpid" is I think the market place id. In this case five
|
|
// stands for Hamburg.
|
|
//
|
|
// Here the id for several market places: 2 Frankfurt, 3 Berlin, 4
|
|
// Düsseldorf, 5 Hamburg, 6 München/Munich, 7 Hannover, 9 Stuttgart, 10
|
|
// Xetra, 32 NASDAQ, 36 NYSE
|
|
|
|
result["Wallstreet-Online.DE (Hamburg)"] = WebPriceQuoteSource("Wallstreet-Online.DE (Hamburg)",
|
|
"http://fonds.wallstreet-online.de/si/?k=%1&spid=ws&mpid=5",
|
|
"Symbol:(\\w+)", // symbolregexp
|
|
"Fonds \\(EUR\\) ([0-9.]+,\\d+)", // priceregexp
|
|
", (\\d+\\D+\\d+\\D+\\d+)", // dateregexp
|
|
"%d %m %y" // dateformat
|
|
);
|
|
|
|
// The following price quote was contributed by
|
|
// Piotr Adacha <piotr.adacha@googlemail.com>
|
|
|
|
// I would like to post new Online Query Settings for KMyMoney. This set is
|
|
// suitable to query stooq.com service, providing quotes for stocks, futures,
|
|
// mutual funds and other financial instruments from Polish Gielda Papierow
|
|
// Wartosciowych (GPW). Unfortunately, none of well-known international
|
|
// services provide quotes for this market (biggest one in central and eastern
|
|
// Europe), thus, I think it could be helpful for Polish users of KMyMoney (and
|
|
// I am one of them for almost a year).
|
|
|
|
result["Gielda Papierow Wartosciowych (GPW)"] = WebPriceQuoteSource("Gielda Papierow Wartosciowych (GPW)",
|
|
"http://stooq.com/q/?s=%1",
|
|
TQString(), // symbol regexp
|
|
"Kurs.*(\\d+\\.\\d+).*Data", // price regexp
|
|
"(\\d{4,4}-\\d{2,2}-\\d{2,2})", // date regexp
|
|
"%y %m %d" // date format
|
|
);
|
|
|
|
// The following price quote is for getting prices of different funds
|
|
// at OMX Baltic market.
|
|
result["OMX Baltic funds"] = WebPriceQuoteSource("OMX Baltic funds",
|
|
"http://www.baltic.omxgroup.com/market/?pg=nontradeddetails¤cy=0&instrument=%1",
|
|
TQString(), // symbolregexp
|
|
"NAV (\\d+,\\d+)", // priceregexp
|
|
"Kpv (\\d+.\\d+.\\d+)", // dateregexp
|
|
"%d.%m.%y" // dateformat
|
|
);
|
|
|
|
// The following price quote was contributed by
|
|
// Peter Hargreaves <pete.h@pdh-online.info>
|
|
// The original posting can be found here:
|
|
// http://sourceforge.net/mailarchive/message.php?msg_name=200806060854.11682.pete.h%40pdh-online.info
|
|
|
|
// I have PEP and ISA accounts which I invest in Funds with Barclays
|
|
// Stockbrokers. They give me Fund data via Financial Express:
|
|
//
|
|
// https://webfund6.financialexpress.net/Clients/Barclays/default.aspx
|
|
//
|
|
// A typical Fund Factsheet is:
|
|
//
|
|
// https://webfund6.financialexpress.net/Clients/Barclays/search_factsheet_summary.aspx?code=0585239
|
|
//
|
|
// On the Factsheet to identify the fund you can see ISIN Code GB0005852396.
|
|
// In the url, this code is shortened by loosing the first four and last
|
|
// characters.
|
|
//
|
|
// Update:
|
|
//
|
|
// Nick Elliot has contributed a modified regular expression to cope with values presented
|
|
// in pounds as well as those presented in pence. The source can be found here:
|
|
// http://forum.kde.org/update-stock-and-currency-prices-t-32049.html
|
|
|
|
result["Financial Express"] = WebPriceQuoteSource("Financial Express",
|
|
"https://webfund6.financialexpress.net/Clients/Barclays/search_factsheet_summary.aspx?code=%1",
|
|
"ISIN Code[^G]*(GB..........).*", // symbolregexp
|
|
"Current Market Information[^0-9]*([0-9,\\.]+).*", // priceregexp
|
|
"Price Date[^0-9]*(../../....).*", // dateregexp
|
|
"%d/%m/%y" // dateformat
|
|
);
|
|
|
|
return result;
|
|
}
|
|
|
|
TQStringList WebPriceQuote::quoteSources (const _quoteSystemE _system) {
|
|
if (_system == Native)
|
|
return (quoteSourcesNative());
|
|
else
|
|
return (quoteSourcesFinanceQuote());
|
|
}
|
|
|
|
TQStringList WebPriceQuote::quoteSourcesNative()
|
|
{
|
|
KConfig *kconfig = KGlobal::config();
|
|
TQStringList groups = kconfig->groupList();
|
|
|
|
TQStringList::Iterator it;
|
|
TQRegExp onlineQuoteSource(TQString("^Online-Quote-Source-(.*)$"));
|
|
|
|
// get rid of all 'non online quote source' entries
|
|
for(it = groups.begin(); it != groups.end(); it = groups.remove(it)) {
|
|
if(onlineQuoteSource.search(*it) >= 0) {
|
|
// Insert the name part
|
|
groups.insert(it, onlineQuoteSource.cap(1));
|
|
}
|
|
}
|
|
|
|
// if the user has the OLD quote source defined, now is the
|
|
// time to remove that entry and convert it to the new system.
|
|
if ( ! groups.count() && kconfig->hasGroup("Online Quotes Options") )
|
|
{
|
|
kconfig->setGroup("Online Quotes Options");
|
|
TQString url(kconfig->readEntry("URL","http://finance.yahoo.com/d/quotes.csv?s=%1&f=sl1d1"));
|
|
TQString symbolRegExp(kconfig->readEntry("SymbolRegex","\"([^,\"]*)\",.*"));
|
|
TQString priceRegExp(kconfig->readEntry("PriceRegex","[^,]*,([^,]*),.*"));
|
|
TQString dateRegExp(kconfig->readEntry("DateRegex","[^,]*,[^,]*,\"([^\"]*)\""));
|
|
kconfig->deleteGroup("Online Quotes Options");
|
|
|
|
groups += "Old Source";
|
|
kconfig->setGroup(TQString("Online-Quote-Source-%1").tqarg("Old Source"));
|
|
kconfig->writeEntry("URL", url);
|
|
kconfig->writeEntry("SymbolRegex", symbolRegExp);
|
|
kconfig->writeEntry("PriceRegex",priceRegExp);
|
|
kconfig->writeEntry("DateRegex", dateRegExp);
|
|
kconfig->writeEntry("DateFormatRegex", "%m %d %y");
|
|
kconfig->sync();
|
|
}
|
|
|
|
// Set up each of the default sources. These are done piecemeal so that
|
|
// when we add a new source, it's automatically picked up.
|
|
TQMap<TQString,WebPriceQuoteSource> defaults = defaultQuoteSources();
|
|
TQMap<TQString,WebPriceQuoteSource>::const_iterator it_source = defaults.begin();
|
|
while ( it_source != defaults.end() )
|
|
{
|
|
if ( ! groups.tqcontains( (*it_source).m_name ) )
|
|
{
|
|
groups += (*it_source).m_name;
|
|
(*it_source).write();
|
|
kconfig->sync();
|
|
}
|
|
++it_source;
|
|
}
|
|
|
|
return groups;
|
|
}
|
|
|
|
TQStringList WebPriceQuote::quoteSourcesFinanceQuote()
|
|
{
|
|
if (m_financeQuoteSources.empty()) { // run the process one time only
|
|
FinanceQuoteProcess getList;
|
|
m_financeQuoteScriptPath =
|
|
KGlobal::dirs()->findResource("appdata", TQString("misc/financequote.pl"));
|
|
getList.launch( m_financeQuoteScriptPath );
|
|
while (!getList.isFinished()) {
|
|
tqApp->processEvents();
|
|
}
|
|
m_financeQuoteSources = getList.getSourceList();
|
|
}
|
|
return (m_financeQuoteSources);
|
|
}
|
|
|
|
//
|
|
// Helper class to load/save an individual source
|
|
//
|
|
|
|
WebPriceQuoteSource::WebPriceQuoteSource(const TQString& name, const TQString& url, const TQString& sym, const TQString& price, const TQString& date, const TQString& dateformat):
|
|
m_name(name),
|
|
m_url(url),
|
|
m_sym(sym),
|
|
m_price(price),
|
|
m_date(date),
|
|
m_dateformat(dateformat)
|
|
{
|
|
}
|
|
|
|
WebPriceQuoteSource::WebPriceQuoteSource(const TQString& name)
|
|
{
|
|
m_name = name;
|
|
KConfig *kconfig = KGlobal::config();
|
|
kconfig->setGroup(TQString("Online-Quote-Source-%1").tqarg(m_name));
|
|
m_sym = kconfig->readEntry("SymbolRegex");
|
|
m_date = kconfig->readEntry("DateRegex");
|
|
m_dateformat = kconfig->readEntry("DateFormatRegex","%m %d %y");
|
|
m_price = kconfig->readEntry("PriceRegex");
|
|
m_url = kconfig->readEntry("URL");
|
|
m_skipStripping = kconfig->readBoolEntry("SkipStripping", false);
|
|
}
|
|
|
|
void WebPriceQuoteSource::write(void) const
|
|
{
|
|
KConfig *kconfig = KGlobal::config();
|
|
kconfig->setGroup(TQString("Online-Quote-Source-%1").tqarg(m_name));
|
|
kconfig->writeEntry("URL", m_url);
|
|
kconfig->writeEntry("PriceRegex", m_price);
|
|
kconfig->writeEntry("DateRegex", m_date);
|
|
kconfig->writeEntry("DateFormatRegex", m_dateformat);
|
|
kconfig->writeEntry("SymbolRegex", m_sym);
|
|
if(m_skipStripping)
|
|
kconfig->writeEntry("SkipStripping", m_skipStripping);
|
|
else
|
|
kconfig->deleteEntry("SkipStripping");
|
|
}
|
|
|
|
void WebPriceQuoteSource::rename(const TQString& name)
|
|
{
|
|
remove();
|
|
m_name = name;
|
|
write();
|
|
}
|
|
|
|
void WebPriceQuoteSource::remove(void) const
|
|
{
|
|
KConfig *kconfig = KGlobal::config();
|
|
kconfig->deleteGroup(TQString("Online-Quote-Source-%1").tqarg(m_name));
|
|
}
|
|
|
|
//
|
|
// Helper class to babysit the KProcess used for running the local script in that case
|
|
//
|
|
|
|
WebPriceQuoteProcess::WebPriceQuoteProcess(void)
|
|
{
|
|
connect(this, TQT_SIGNAL(receivedStdout(KProcess*, char*, int)), this, TQT_SLOT(slotReceivedDataFromFilter(KProcess*, char*, int)));
|
|
connect(this, TQT_SIGNAL(processExited(KProcess*)), this, TQT_SLOT(slotProcessExited(KProcess*)));
|
|
}
|
|
|
|
void WebPriceQuoteProcess::slotReceivedDataFromFilter(KProcess* /*_process*/, char* _pcbuffer, int _nbufferlen)
|
|
{
|
|
TQByteArray data;
|
|
data.duplicate(_pcbuffer, _nbufferlen);
|
|
|
|
// kdDebug(2) << "WebPriceQuoteProcess::slotReceivedDataFromFilter(): " << TQString(data) << endl;
|
|
m_string += TQString(data);
|
|
}
|
|
|
|
void WebPriceQuoteProcess::slotProcessExited(KProcess*)
|
|
{
|
|
// kdDebug(2) << "WebPriceQuoteProcess::slotProcessExited()" << endl;
|
|
emit processExited(m_string);
|
|
m_string.truncate(0);
|
|
}
|
|
|
|
//
|
|
// Helper class to babysit the KProcess used for running the Finance Quote sources script
|
|
//
|
|
|
|
FinanceQuoteProcess::FinanceQuoteProcess(void)
|
|
{
|
|
m_isDone = false;
|
|
m_string = "";
|
|
m_fqNames["aex"] = "AEX";
|
|
m_fqNames["aex_futures"] = "AEX Futures";
|
|
m_fqNames["aex_options"] = "AEX Options";
|
|
m_fqNames["amfiindia"] = "AMFI India";
|
|
m_fqNames["asegr"] = "ASE";
|
|
m_fqNames["asia"] = "Asia (Yahoo, ...)";
|
|
m_fqNames["asx"] = "ASX";
|
|
m_fqNames["australia"] = "Australia (ASX, Yahoo, ...)";
|
|
m_fqNames["bmonesbittburns"] = "BMO NesbittBurns";
|
|
m_fqNames["brasil"] = "Brasil (Yahoo, ...)";
|
|
m_fqNames["canada"] = "Canada (Yahoo, ...)";
|
|
m_fqNames["canadamutual"] = "Canada Mutual (Fund Library, ...)";
|
|
m_fqNames["deka"] = "Deka Investments";
|
|
m_fqNames["dutch"] = "Dutch (AEX, ...)";
|
|
m_fqNames["dwsfunds"] = "DWS";
|
|
m_fqNames["europe"] = "Europe (Yahoo, ...)";
|
|
m_fqNames["fidelity"] = "Fidelity (Fidelity, ...)";
|
|
m_fqNames["fidelity_direct"] = "Fidelity Direct";
|
|
m_fqNames["financecanada"] = "Finance Canada";
|
|
m_fqNames["ftportfolios"] = "First Trust (First Trust, ...)";
|
|
m_fqNames["ftportfolios_direct"] = "First Trust Portfolios";
|
|
m_fqNames["fundlibrary"] = "Fund Library";
|
|
m_fqNames["greece"] = "Greece (ASE, ...)";
|
|
m_fqNames["indiamutual"] = "India Mutual (AMFI, ...)";
|
|
m_fqNames["maninv"] = "Man Investments";
|
|
m_fqNames["fool"] = "Motley Fool";
|
|
m_fqNames["nasdaq"] = "Nasdaq (Yahoo, ...)";
|
|
m_fqNames["nz"] = "New Zealand (Yahoo, ...)";
|
|
m_fqNames["nyse"] = "NYSE (Yahoo, ...)";
|
|
m_fqNames["nzx"] = "NZX";
|
|
m_fqNames["platinum"] = "Platinum Asset Management";
|
|
m_fqNames["seb_funds"] = "SEB";
|
|
m_fqNames["sharenet"] = "Sharenet";
|
|
m_fqNames["za"] = "South Africa (Sharenet, ...)";
|
|
m_fqNames["troweprice_direct"] = "T. Rowe Price";
|
|
m_fqNames["troweprice"] = "T. Rowe Price";
|
|
m_fqNames["tdefunds"] = "TD Efunds";
|
|
m_fqNames["tdwaterhouse"] = "TD Waterhouse Canada";
|
|
m_fqNames["tiaacref"] = "TIAA-CREF";
|
|
m_fqNames["trustnet"] = "Trustnet";
|
|
m_fqNames["uk_unit_trusts"] = "U.K. Unit Trusts";
|
|
m_fqNames["unionfunds"] = "Union Investments";
|
|
m_fqNames["tsp"] = "US Govt. Thrift Savings Plan";
|
|
m_fqNames["usfedbonds"] = "US Treasury Bonds";
|
|
m_fqNames["usa"] = "USA (Yahoo, Fool ...)";
|
|
m_fqNames["vanguard"] = "Vanguard";
|
|
m_fqNames["vwd"] = "VWD";
|
|
m_fqNames["yahoo"] = "Yahoo";
|
|
m_fqNames["yahoo_asia"] = "Yahoo Asia";
|
|
m_fqNames["yahoo_australia"] = "Yahoo Australia";
|
|
m_fqNames["yahoo_brasil"] = "Yahoo Brasil";
|
|
m_fqNames["yahoo_europe"] = "Yahoo Europe";
|
|
m_fqNames["yahoo_nz"] = "Yahoo New Zealand";
|
|
m_fqNames["zifunds"] = "Zuerich Investments";
|
|
connect(this, TQT_SIGNAL(receivedStdout(KProcess*, char*, int)), this, TQT_SLOT(slotReceivedDataFromFilter(KProcess*, char*, int)));
|
|
connect(this, TQT_SIGNAL(processExited(KProcess*)), this, TQT_SLOT(slotProcessExited(KProcess*)));
|
|
}
|
|
|
|
void FinanceQuoteProcess::slotReceivedDataFromFilter(KProcess* /*_process*/, char* _pcbuffer, int _nbufferlen)
|
|
{
|
|
TQByteArray data;
|
|
data.duplicate(_pcbuffer, _nbufferlen);
|
|
|
|
// kdDebug(2) << "WebPriceQuoteProcess::slotReceivedDataFromFilter(): " << TQString(data) << endl;
|
|
m_string += TQString(data);
|
|
}
|
|
|
|
void FinanceQuoteProcess::slotProcessExited(KProcess*)
|
|
{
|
|
// kdDebug(2) << "WebPriceQuoteProcess::slotProcessExited()" << endl;
|
|
m_isDone = true;
|
|
}
|
|
|
|
void FinanceQuoteProcess::launch (const TQString& scriptPath) {
|
|
clearArguments();
|
|
arguments.append(TQCString("perl"));
|
|
arguments.append (TQCString(scriptPath));
|
|
arguments.append (TQCString("-l"));
|
|
if (!start(KProcess::NotifyOnExit, KProcess::Stdout)) qFatal ("Unable to start FQ script");
|
|
return;
|
|
}
|
|
|
|
TQStringList FinanceQuoteProcess::getSourceList() {
|
|
TQStringList raw = TQStringList::split(0x0A, m_string);
|
|
TQStringList sources;
|
|
TQStringList::iterator it;
|
|
for (it = raw.begin(); it != raw.end(); ++it) {
|
|
if (m_fqNames[*it].isEmpty()) sources.append(*it);
|
|
else sources.append(m_fqNames[*it]);
|
|
}
|
|
sources.sort();
|
|
return (sources);
|
|
}
|
|
|
|
const TQString FinanceQuoteProcess::crypticName(const TQString& niceName) {
|
|
TQString ret (niceName);
|
|
fqNameMap::iterator it;
|
|
for (it = m_fqNames.begin(); it != m_fqNames.end(); ++it) {
|
|
if (niceName == it.data()) {
|
|
ret = it.key();
|
|
break;
|
|
}
|
|
}
|
|
return (ret);
|
|
}
|
|
|
|
const TQString FinanceQuoteProcess::niceName(const TQString& crypticName) {
|
|
TQString ret (m_fqNames[crypticName]);
|
|
if (ret.isEmpty()) ret = crypticName;
|
|
return (ret);
|
|
}
|
|
//
|
|
// Universal date converter
|
|
//
|
|
|
|
// In 'strict' mode, this is designed to be compatable with the TQIF profile date
|
|
// converter. However, that converter deals with the concept of an apostrophe
|
|
// format in a way I don't understand. So for the moment, they are 99%
|
|
// compatable, waiting on that issue. (acejones)
|
|
|
|
TQDate MyMoneyDateFormat::convertString(const TQString& _in, bool _strict, unsigned _centurymidpoint) const
|
|
{
|
|
//
|
|
// Break date format string into component parts
|
|
//
|
|
|
|
TQRegExp formatrex("%([mdy]+)(\\W+)%([mdy]+)(\\W+)%([mdy]+)",false /* case sensitive */);
|
|
if ( formatrex.search(m_format) == -1 )
|
|
{
|
|
throw new MYMONEYEXCEPTION("Invalid format string");
|
|
}
|
|
|
|
TQStringList formatParts;
|
|
formatParts += formatrex.cap(1);
|
|
formatParts += formatrex.cap(3);
|
|
formatParts += formatrex.cap(5);
|
|
|
|
TQStringList formatDelimiters;
|
|
formatDelimiters += formatrex.cap(2);
|
|
formatDelimiters += formatrex.cap(4);
|
|
|
|
//
|
|
// Break input string up into component parts,
|
|
// using the delimiters found in the format string
|
|
//
|
|
|
|
TQRegExp inputrex;
|
|
inputrex.setCaseSensitive(false);
|
|
|
|
// strict mode means we must enforce the delimiters as specified in the
|
|
// format. non-strict allows any delimiters
|
|
if ( _strict )
|
|
inputrex.setPattern(TQString("(\\w+)%1(\\w+)%2(\\w+)").tqarg(formatDelimiters[0],formatDelimiters[1]));
|
|
else
|
|
inputrex.setPattern("(\\w+)\\W+(\\w+)\\W+(\\w+)");
|
|
|
|
if ( inputrex.search(_in) == -1 )
|
|
{
|
|
throw new MYMONEYEXCEPTION("Invalid input string");
|
|
}
|
|
|
|
TQStringList scannedParts;
|
|
scannedParts += inputrex.cap(1).lower();
|
|
scannedParts += inputrex.cap(2).lower();
|
|
scannedParts += inputrex.cap(3).lower();
|
|
|
|
//
|
|
// Convert the scanned parts into actual date components
|
|
//
|
|
|
|
unsigned day = 0, month = 0, year = 0;
|
|
bool ok;
|
|
TQRegExp digitrex("(\\d+)");
|
|
TQStringList::const_iterator it_scanned = scannedParts.begin();
|
|
TQStringList::const_iterator it_format = formatParts.begin();
|
|
while ( it_scanned != scannedParts.end() )
|
|
{
|
|
switch ( (*it_format)[0] )
|
|
{
|
|
case 'd':
|
|
// remove any extraneous non-digits (e.g. read "3rd" as 3)
|
|
ok = false;
|
|
if ( digitrex.search(*it_scanned) != -1 )
|
|
day = digitrex.cap(1).toUInt(&ok);
|
|
if ( !ok || day > 31 )
|
|
throw new MYMONEYEXCEPTION(TQString("Invalid day entry: %1").tqarg(*it_scanned));
|
|
break;
|
|
case 'm':
|
|
month = (*it_scanned).toUInt(&ok);
|
|
if ( !ok )
|
|
{
|
|
// maybe it's a textual date
|
|
unsigned i = 1;
|
|
while ( i <= 12 )
|
|
{
|
|
if(KGlobal::locale()->calendar()->monthName(i, 2000, true).lower() == *it_scanned
|
|
|| KGlobal::locale()->calendar()->monthName(i, 2000, false).lower() == *it_scanned)
|
|
month = i;
|
|
++i;
|
|
}
|
|
}
|
|
|
|
if ( month < 1 || month > 12 )
|
|
throw new MYMONEYEXCEPTION(TQString("Invalid month entry: %1").tqarg(*it_scanned));
|
|
|
|
break;
|
|
case 'y':
|
|
if ( _strict && (*it_scanned).length() != (*it_format).length())
|
|
throw new MYMONEYEXCEPTION(TQString("Length of year (%1) does not match expected length (%2).")
|
|
.tqarg(*it_scanned,*it_format));
|
|
|
|
year = (*it_scanned).toUInt(&ok);
|
|
|
|
if (!ok)
|
|
throw new MYMONEYEXCEPTION(TQString("Invalid year entry: %1").tqarg(*it_scanned));
|
|
|
|
//
|
|
// 2-digit year case
|
|
//
|
|
// this algorithm will pick a year within +/- 50 years of the
|
|
// centurymidpoint parameter. i.e. if the midpoint is 2000,
|
|
// then 0-49 will become 2000-2049, and 50-99 will become 1950-1999
|
|
if ( year < 100 )
|
|
{
|
|
unsigned centuryend = _centurymidpoint + 50;
|
|
unsigned centurybegin = _centurymidpoint - 50;
|
|
|
|
if ( year < centuryend % 100 )
|
|
year += 100;
|
|
year += centurybegin - centurybegin % 100;
|
|
}
|
|
|
|
if ( year < 1900 )
|
|
throw new MYMONEYEXCEPTION(TQString("Invalid year (%1)").tqarg(year));
|
|
|
|
break;
|
|
default:
|
|
throw new MYMONEYEXCEPTION("Invalid format character");
|
|
}
|
|
|
|
++it_scanned;
|
|
++it_format;
|
|
}
|
|
|
|
TQDate result(year,month,day);
|
|
if ( ! result.isValid() )
|
|
throw new MYMONEYEXCEPTION(TQString("Invalid date (yr%1 mo%2 dy%3)").tqarg(year).tqarg(month).tqarg(day));
|
|
|
|
return result;
|
|
}
|
|
|
|
//
|
|
// Unit test helpers
|
|
//
|
|
|
|
convertertest::QuoteReceiver::QuoteReceiver(WebPriceQuote* q, TQObject* tqparent, const char *name) :
|
|
TQObject(tqparent,name)
|
|
{
|
|
connect(q,TQT_SIGNAL(quote(const TQString&,const TQDate&, const double&)),
|
|
this,TQT_SLOT(slotGetQuote(const TQString&,const TQDate&, const double&)));
|
|
connect(q,TQT_SIGNAL(status(const TQString&)),
|
|
this,TQT_SLOT(slottqStatus(const TQString&)));
|
|
connect(q,TQT_SIGNAL(error(const TQString&)),
|
|
this,TQT_SLOT(slotError(const TQString&)));
|
|
}
|
|
|
|
convertertest::QuoteReceiver::~QuoteReceiver()
|
|
{
|
|
}
|
|
|
|
void convertertest::QuoteReceiver::slotGetQuote(const TQString&,const TQDate& d, const double& m)
|
|
{
|
|
// kdDebug(2) << "test::QuoteReceiver::slotGetQuote( , " << d << " , " << m.toString() << " )" << endl;
|
|
|
|
m_price = MyMoneyMoney(m);
|
|
m_date = d;
|
|
}
|
|
void convertertest::QuoteReceiver::slottqStatus(const TQString& msg)
|
|
{
|
|
// kdDebug(2) << "test::QuoteReceiver::slottqStatus( " << msg << " )" << endl;
|
|
|
|
m_statuses += msg;
|
|
}
|
|
void convertertest::QuoteReceiver::slotError(const TQString& msg)
|
|
{
|
|
// kdDebug(2) << "test::QuoteReceiver::slotError( " << msg << " )" << endl;
|
|
|
|
m_errors += msg;
|
|
}
|
|
|
|
// vim:cin:si:ai:et:ts=2:sw=2:
|
|
|
|
#include "webpricequote.moc"
|