I wrote (and published) a class to handle historic fx calculations some time ago. It used the Yahoo finance interface and parts of it were quite slow.
When a client asked me to write some multi-currency financial reports for them, I dug out the yahoo class but for a number of reasons I decided not to use it. Predominantly these included the very slow API and the high deviation in results obtained from yahoo that were reported by the client’s own financial software.
Instead I used the rates published daily by the European Central Bank. To quote the ECB site:
The reference rates are usually updated by 3 p.m. C.E.T. They are based on a regular daily concertation procedure between central banks across Europe and worldwide, which normally takes place at 2.15 p.m. CET.
The site offers historic rates going back to 1999 and packaged as an xml file. it is quite a light weight download and, on my system, is parsed and written to a database in well under a minute. This is a once only event, of course. On subsequent days only the last 90 days of currency data is downloaded and of that only the necessary lines are parsed.
the class is below
<?php /** example usage **/ $f = new fxConverter(); $result = $f->convert('USD', 'GBP', 1, '1 October 1999'); print_r($result); /** end example usage **/ class fxConverter{ /** * constructor * @return */ public function __construct(){ date_default_timezone_set('UTC'); $this->pdo = new PDO ('sqlite:fxrates.sqlite'); $this->pdo->exec('create table if not exists fxRates (fxDate int, fxCurrency text, fxRate real, primary key (fxDate, fxCurrency) on conflict replace)'); $this->pdo->exec('create table if not exists fxTime (updateID integer primary key autoincrement, updateDate int)'); if ( 0 < ($lastUpdate = $this->needsUpdate())) $this->updateFx($lastUpdate); } /** * helper method to set the required date. * @param object $date in unixtimestamp or text based format * @return */ public function setDate($date){ if(is_string($date)) $this->date = date('Y-m-d', strtotime($date)); if(is_numeric($date)) $this->date = date('Y-m-d', $date); } /** * main public method for converting amounts from one currency to another * @param string $from - a supported currency * @param string $to - a supported currency * @param float $amount [optional] - the amount to be converted, defaults to 1 * @param int|string $date [optional] - the date for the conversion, specified in text form ('2011-01-01', '1 Jan 2011') or a unix timestamp * @param int|string $offset [optional] - an offset to apply. can be a percentage between 0 and 5 or one of the following string 'bank, 'credit card', 'cash') * @return either an error object or an array with the converted amount and the date of the currency pair used. */ public function convert($from, $to, $amount = 1, $date = NULL, $offset = NULL){ if(!$this->supportedCurrency($from, $to)) return $this->raiseError('This currency pair is not supported'); if($amount == 0) return 0; if (empty($date) && empty($this->date)) $this->setDate(time()); if(!empty($date)) $this->setDate($date); switch (strtolower($offset)): case 'creditcard': $offset = 0.97; break; case 'atm': $offset = 0.98; break; case 'bureau': case 'kiosk': $offset = 0.95; break; case '': case NULL: $offset = 1; break; default: $offset = 1 - $offset/100; endswitch; //get conversion rate for $from and $to $rate = $this->getFX($from, $to, $amount, $offset); return $rate; } /** * private method to ensure that the requested currency pairs are retrievable * @param object $from * @param object $to * @return */ private function supportedCurrency($from, $to){ $sql = <<<SQL SELECT COUNT(DISTINCT fxCurrency) as c FROM fxRates WHERE fxCurrency IN (?, ?) SQL; $s = $this->pdo->prepare($sql); $s->execute(array(strtoupper($from), strtoupper($to))); $row = $s->fetchObject(); return ($row->c == 2); } /** * private method to convert the requested amount by the rxrate. * * @param object $from * @param object $to * @param object $amount * @param object $offset [optional] * @return */ private function getFX($from, $to, $amount, $offset=NULL){ $sql = <<<SQL SELECT (b.fxRate/a.fxRate) * ? AS convertedAmount, (b.fxRate/a.fxRate) * ? * ? AS adjustedConvertedAmount, a.fxDate as fxDate, ? as 'interbank rate' FROM fxRates a JOIN fxRates b ON a.fxDate = b.fxDate WHERE a.fxCurrency=? AND b.fxCurrency=? AND a.fxDate <= round(julianday(?)) AND a.fxDate >= round(julianday(?)) - 5 ORDER BY a.fxDate DESC LIMIT 1 SQL; $s = $this->pdo->prepare($sql); $s->execute(array( $amount, $amount, $offset, $offset, $from, $to, $this->date, $this->date)); $row = $s->fetchObject(); if(is_null($row) || $row === false) return $this->raiseError('No recent rate for this currency pair'); return get_object_vars($row); //$row; } /** * private function to determine whether a new set of fx rates should be retrieved * @return true|false */ private function needsUpdate(){ $s = $this->pdo->prepare(<<<SQL SELECT IFNULL( (CASE WHEN (round(julianday('now')) > max(updateDate)) THEN (round(julianday('now')) - max(updateDate)) ELSE 0 END), 10000 ) as lastUpdate FROM fxTime SQL ); if(!$s) die(print_r($this->pdo->errorInfo())); $s->execute(); $o = $s->fetchObject(); return isset($o->lastUpdate) ? $o->lastUpdate : 0; } /** * private method to retrieve and update currency pairs from the ECB website * @param object $lastUpdate [optional] * @return */ private function updateFx($lastUpdate = NULL){ if( $lastUpdate > 90 || is_null($lastUpdate)): $url = 'http://www.ecb.europa.eu/stats/eurofxref/eurofxref-hist.xml'; else: $url = 'http://www.ecb.europa.eu/stats/eurofxref/eurofxref-hist-90d.xml'; endif; $XML=simplexml_load_file($url); if($XML === false) return $this->raiseError('Cannot update fxRates'); $query = 'insert into fxRates (fxDate, fxCurrency, fxRate) values (round(julianday(?)),?,?)'; $s = $this->pdo->prepare($query); if($s === false) print_r($this->pdo->errorInfo()); //work out the comparitive date $cDate = strtotime('-' . ($lastUpdate + 1) .' day' ); foreach($XML->Cube->Cube as $data): set_time_limit(30); $i = $data->attributes(); $date = $i['time']; if(strtotime($date) < $cDate) continue; $s->execute(array($date, 'EUR', 1)); foreach ($data->children() as $blob): set_time_limit(10); $d = $blob->attributes(); $s->execute(array( $date, $d['currency'], $d['rate'])); endforeach; endforeach; $this->pdo->exec("insert into fxTime values (NULL, round(julianday('now')))"); $this->pdo->exec('VACUUM'); } private function raiseError($message){ $this->hasErrors = true; $this->errors[] = new fxError($message); return $this->errors; } } class fxError{ public function __construct($message){ $this->message = $message; } } |