Skip to content

Instantly share code, notes, and snippets.

@blueset
Last active February 26, 2018 00:53
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save blueset/04b894df2adfc30d45b39093301bc7a7 to your computer and use it in GitHub Desktop.
Save blueset/04b894df2adfc30d45b39093301bc7a7 to your computer and use it in GitHub Desktop.
BOC Exchange Rate Monitor (AUD/CNY)

Demo: https://labs.1a23.com/rate

simple_html_dom.php is available at http://sourceforge.net/projects/simplehtmldom/ (MIT License)

Requires a SQLite database data.db with structure:

CREATE TABLE data (
  time text PRIMARY KEY NOT NULL,
  value double(128) NOT NULL
);
CREATE UNIQUE INDEX timeindex ON data (time ASC);
CREATE TABLE meta (
  "key" varchar PRIMARY KEY NOT NULL,
  value varchar(128)
);

Requires a Telegram Bot for reminders.

Requires server to setup a cronjob to run data.php every minute for data update.

<?php
require_once( 'simple_html_dom.php' );
/**
*Retrieve info from boc website using simple html dom.
*
*Simple Html Dom Parser document and download: http://simplehtmldom.sourceforge.net/
*/
function get_boc_exchange_rate_table(){
//Get web page by simple html dom parser
$html = file_get_html( 'http://www.boc.cn/sourcedb/whpj/enindex.html' );
//Control the items in the currency list.
$allowed_currency = array( 'TWD', 'GBP', 'HKD', 'USD', 'CHF', 'SGD', 'SEK', 'DKK', 'NOK', 'JPY', 'CAD', 'AUD', 'MYR', 'EUR', 'MOP', 'PHP', 'THB', 'NZD', 'KRW', 'RUB' );
//Stores the final data
$exchange_rates = array();
foreach( $html->find('table tr[align=center]') as $tr ){
$currency_name = $tr->children(0)->plaintext;
if( in_array( $currency_name, $allowed_currency ) ){
$exchange_rates[ $currency_name ]['currency_name'] = $currency_name;
$exchange_rates[ $currency_name ]['buying_rate'] = $tr->children(1)->plaintext;
$exchange_rates[ $currency_name ]['cash_buying_rate'] = $tr->children(2)->plaintext;
$exchange_rates[ $currency_name ]['selling_rate'] = $tr->children(3)->plaintext;
$exchange_rates[ $currency_name ]['cash_selling_rate'] = $tr->children(4)->plaintext;
$exchange_rates[ $currency_name ]['middle_rate'] = $tr->children(5)->plaintext;
$exchange_rates[ $currency_name ]['pub_time'] = str_replace("&nbsp;", '',$tr->children(6)->plaintext);
try {
$datetime = new DateTime( $exchange_rates[ $currency_name ]['pub_time'] );
} catch( Exception $e ){
echo $e->getMessage();
}
$exchange_rates[ $currency_name ]['pub_time'] = $datetime->format('Y-m-d H:i:s');
}
}
return $exchange_rates;
}
<?php
require_once('bocrate.php');
$data = get_boc_exchange_rate_table();
$m = ['', number_format($data['AUD']['selling_rate'], 2)];
$n = ['', $data['AUD']['pub_time']];
if ($m[1] == "0.00") exit(0);
$s = new PDO('sqlite:data.db');
$s->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// NOTE: Date format is YYYY-MM-DD HH:MM:SS
// 0, 13 means YYYY-MM-DD HH, for the same line-crossing, sends only one reminder per hour.
$today = substr($n[1], 0, 13);
$last_val = floatval($s->query('SELECT * FROM data ORDER BY time DESC LIMIT 1')->fetch(PDO::FETCH_ASSOC)['value']);
// NOTE: `/ 10` means prompt for changes every 10 cents.
$last_k_val = floor(intval($last_val) / 10);
$k_val = floor(intval(floatval($m[1])) / 10);
if ($last_k_val - $k_val != 0){ // crossing border
$border = max($last_k_val, $k_val);
$db_date = $s->query('SELECT * FROM meta where key = "last_alarm_date"')->fetch(PDO::FETCH_ASSOC)['value'];
$db_border = intval($s->query('SELECT * FROM meta where key = "last_alarm_value"')->fetch(PDO::FETCH_ASSOC)['value']);
if ($db_date != $today or $db_border != $border){
if ($last_k_val < $k_val) $type = "📈"; else $type = "📉";
$now = $m[1];
// Message format.
$msg = "BOCAUD $last_val $type $now\n\n汇率变动提醒\n中国银行 AUD/CNY 现汇卖出价";
// TODO: Fill bot token here
$url = "https://api.telegram.org/botBOT_TOKEN_GOES_HERE/sendMessage";
$chat_id = 0; // TODO: fill chat ID here.
$fields = "chat_id=$chat_id&text=" . urlencode($msg);
$ch = curl_init( $url );
curl_setopt( $ch, CURLOPT_POST, 1);
curl_setopt( $ch, CURLOPT_POSTFIELDS, $fields);
curl_exec( $ch );
curl_close($ch);
$s->prepare("UPDATE meta SET value = :date WHERE key = \"last_alarm_date\"")->execute(["date"=>$today]);
$s->prepare("UPDATE meta SET value = :border WHERE key = \"last_alarm_value\"")->execute(["border"=>$border]);
}
}
$q2 = $s->prepare('SELECT * FROM data WHERE time = :time');
$q2->execute(["time"=>$n[1]]);
if (!boolval($q2->fetch())){
$q = $s->prepare('INSERT INTO data (time, value) VALUES (:time, :value)');
$q->execute(["time"=>$n[1], "value"=>floatval($m[1])]);
if ($q->rowCount() > 0) exit("Done.");
exit("Failed.");
} else {
exit("Exist.");
}
<?php
$s = new PDO('sqlite:data.db');
$s->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$q2 = $s->prepare('SELECT * FROM data ORDER BY time DESC LIMIT 5000');
$q2->execute();
$data = json_encode($q2->fetchAll(PDO::FETCH_ASSOC));
?><!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>BankOfChina AUD/CNY</title>
<link href="https://fonts.googleapis.com/css?family=Roboto:100,300,400" rel="stylesheet">
<link rel="stylesheet" href="https://fonts.googleapis.com/earlyaccess/notosanssc.css">
<meta name="viewport" content="width=device-width, minimum-scale=1.0">
<script src="https://code.highcharts.com/stock/highstock.js"></script>
<script src="https://code.highcharts.com/stock/modules/exporting.js"></script>
<!-- script src="https://code.highcharts.com/modules/broken-axis.js"></script-->
<script type="text/javascript">
!function(e,t,n){typeof module!="undefined"&&module.exports?module.exports=n():typeof define=="function"&&define.amd?define(n):t[e]=n()}("reqwest",this,function(){function succeed(e){var t=protocolRe.exec(e.url);return t=t&&t[1]||context.location.protocol,httpsRe.test(t)?twoHundo.test(e.request.status):!!e.request.response}function handleReadyState(e,t,n){return function(){if(e._aborted)return n(e.request);if(e._timedOut)return n(e.request,"Request is aborted: timeout");e.request&&e.request[readyState]==4&&(e.request.onreadystatechange=noop,succeed(e)?t(e.request):n(e.request))}}function setHeaders(e,t){var n=t.headers||{},r;n.Accept=n.Accept||defaultHeaders.accept[t.type]||defaultHeaders.accept["*"];var i=typeof FormData!="undefined"&&t.data instanceof FormData;!t.crossOrigin&&!n[requestedWith]&&(n[requestedWith]=defaultHeaders.requestedWith),!n[contentType]&&!i&&(n[contentType]=t.contentType||defaultHeaders.contentType);for(r in n)n.hasOwnProperty(r)&&"setRequestHeader"in e&&e.setRequestHeader(r,n[r])}function setCredentials(e,t){typeof t.withCredentials!="undefined"&&typeof e.withCredentials!="undefined"&&(e.withCredentials=!!t.withCredentials)}function generalCallback(e){lastValue=e}function urlappend(e,t){return e+(/\?/.test(e)?"&":"?")+t}function handleJsonp(e,t,n,r){var i=uniqid++,s=e.jsonpCallback||"callback",o=e.jsonpCallbackName||reqwest.getcallbackPrefix(i),u=new RegExp("((^|\\?|&)"+s+")=([^&]+)"),a=r.match(u),f=doc.createElement("script"),l=0,c=navigator.userAgent.indexOf("MSIE 10.0")!==-1;return a?a[3]==="?"?r=r.replace(u,"$1="+o):o=a[3]:r=urlappend(r,s+"="+o),context[o]=generalCallback,f.type="text/javascript",f.src=r,f.async=!0,typeof f.onreadystatechange!="undefined"&&!c&&(f.htmlFor=f.id="_reqwest_"+i),f.onload=f.onreadystatechange=function(){if(f[readyState]&&f[readyState]!=="complete"&&f[readyState]!=="loaded"||l)return!1;f.onload=f.onreadystatechange=null,f.onclick&&f.onclick(),t(lastValue),lastValue=undefined,head.removeChild(f),l=1},head.appendChild(f),{abort:function(){f.onload=f.onreadystatechange=null,n({},"Request is aborted: timeout",{}),lastValue=undefined,head.removeChild(f),l=1}}}function getRequest(e,t){var n=this.o,r=(n.method||"GET").toUpperCase(),i=typeof n=="string"?n:n.url,s=n.processData!==!1&&n.data&&typeof n.data!="string"?reqwest.toQueryString(n.data):n.data||null,o,u=!1;return(n["type"]=="jsonp"||r=="GET")&&s&&(i=urlappend(i,s),s=null),n["type"]=="jsonp"?handleJsonp(n,e,t,i):(o=n.xhr&&n.xhr(n)||xhr(n),o.open(r,i,n.async===!1?!1:!0),setHeaders(o,n),setCredentials(o,n),context[xDomainRequest]&&o instanceof context[xDomainRequest]?(o.onload=e,o.onerror=t,o.onprogress=function(){},u=!0):o.onreadystatechange=handleReadyState(this,e,t),n.before&&n.before(o),u?setTimeout(function(){o.send(s)},200):o.send(s),o)}function Reqwest(e,t){this.o=e,this.fn=t,init.apply(this,arguments)}function setType(e){if(e===null)return undefined;if(e.match("json"))return"json";if(e.match("javascript"))return"js";if(e.match("text"))return"html";if(e.match("xml"))return"xml"}function init(o,fn){function complete(e){o.timeout&&clearTimeout(self.timeout),self.timeout=null;while(self._completeHandlers.length>0)self._completeHandlers.shift()(e)}function success(resp){var type=o.type||resp&&setType(resp.getResponseHeader("Content-Type"));resp=type!=="jsonp"?self.request:resp;var filteredResponse=globalSetupOptions.dataFilter(resp.responseText,type),r=filteredResponse;try{resp.responseText=r}catch(e){}if(r)switch(type){case"json":try{resp=context.JSON?context.JSON.parse(r):eval("("+r+")")}catch(err){return error(resp,"Could not parse JSON in response",err)}break;case"js":resp=eval(r);break;case"html":resp=r;break;case"xml":resp=resp.responseXML&&resp.responseXML.parseError&&resp.responseXML.parseError.errorCode&&resp.responseXML.parseError.reason?null:resp.responseXML}self._responseArgs.resp=resp,self._fulfilled=!0,fn(resp),self._successHandler(resp);while(self._fulfillmentHandlers.length>0)resp=self._fulfillmentHandlers.shift()(resp);complete(resp)}function timedOut(){self._timedOut=!0,self.request.abort()}function error(e,t,n){e=self.request,self._responseArgs.resp=e,self._responseArgs.msg=t,self._responseArgs.t=n,self._erred=!0;while(self._errorHandlers.length>0)self._errorHandlers.shift()(e,t,n);complete(e)}this.url=typeof o=="string"?o:o.url,this.timeout=null,this._fulfilled=!1,this._successHandler=function(){},this._fulfillmentHandlers=[],this._errorHandlers=[],this._completeHandlers=[],this._erred=!1,this._responseArgs={};var self=this;fn=fn||function(){},o.timeout&&(this.timeout=setTimeout(function(){timedOut()},o.timeout)),o.success&&(this._successHandler=function(){o.success.apply(o,arguments)}),o.error&&this._errorHandlers.push(function(){o.error.apply(o,arguments)}),o.complete&&this._completeHandlers.push(function(){o.complete.apply(o,arguments)}),this.request=getRequest.call(this,success,error)}function reqwest(e,t){return new Reqwest(e,t)}function normalize(e){return e?e.replace(/\r?\n/g,"\r\n"):""}function serial(e,t){var n=e.name,r=e.tagName.toLowerCase(),i=function(e){e&&!e.disabled&&t(n,normalize(e.attributes.value&&e.attributes.value.specified?e.value:e.text))},s,o,u,a;if(e.disabled||!n)return;switch(r){case"input":/reset|button|image|file/i.test(e.type)||(s=/checkbox/i.test(e.type),o=/radio/i.test(e.type),u=e.value,(!s&&!o||e.checked)&&t(n,normalize(s&&u===""?"on":u)));break;case"textarea":t(n,normalize(e.value));break;case"select":if(e.type.toLowerCase()==="select-one")i(e.selectedIndex>=0?e.options[e.selectedIndex]:null);else for(a=0;e.length&&a<e.length;a++)e.options[a].selected&&i(e.options[a])}}function eachFormElement(){var e=this,t,n,r=function(t,n){var r,i,s;for(r=0;r<n.length;r++){s=t[byTag](n[r]);for(i=0;i<s.length;i++)serial(s[i],e)}};for(n=0;n<arguments.length;n++)t=arguments[n],/input|select|textarea/i.test(t.tagName)&&serial(t,e),r(t,["input","select","textarea"])}function serializeQueryString(){return reqwest.toQueryString(reqwest.serializeArray.apply(null,arguments))}function serializeHash(){var e={};return eachFormElement.apply(function(t,n){t in e?(e[t]&&!isArray(e[t])&&(e[t]=[e[t]]),e[t].push(n)):e[t]=n},arguments),e}function buildParams(e,t,n,r){var i,s,o,u=/\[\]$/;if(isArray(t))for(s=0;t&&s<t.length;s++)o=t[s],n||u.test(e)?r(e,o):buildParams(e+"["+(typeof o=="object"?s:"")+"]",o,n,r);else if(t&&t.toString()==="[object Object]")for(i in t)buildParams(e+"["+i+"]",t[i],n,r);else r(e,t)}var context=this;if("window"in context)var doc=document,byTag="getElementsByTagName",head=doc[byTag]("head")[0];else{var XHR2;try{XHR2=require("xhr2")}catch(ex){throw new Error("Peer dependency `xhr2` required! Please npm install xhr2")}}var httpsRe=/^http/,protocolRe=/(^\w+):\/\//,twoHundo=/^(20\d|1223)$/,readyState="readyState",contentType="Content-Type",requestedWith="X-Requested-With",uniqid=0,callbackPrefix="reqwest_"+ +(new Date),lastValue,xmlHttpRequest="XMLHttpRequest",xDomainRequest="XDomainRequest",noop=function(){},isArray=typeof Array.isArray=="function"?Array.isArray:function(e){return e instanceof Array},defaultHeaders={contentType:"application/x-www-form-urlencoded",requestedWith:xmlHttpRequest,accept:{"*":"text/javascript, text/html, application/xml, text/xml, */*",xml:"application/xml, text/xml",html:"text/html",text:"text/plain",json:"application/json, text/javascript",js:"application/javascript, text/javascript"}},xhr=function(e){if(e.crossOrigin===!0){var t=context[xmlHttpRequest]?new XMLHttpRequest:null;if(t&&"withCredentials"in t)return t;if(context[xDomainRequest])return new XDomainRequest;throw new Error("Browser does not support cross-origin requests")}return context[xmlHttpRequest]?new XMLHttpRequest:XHR2?new XHR2:new ActiveXObject("Microsoft.XMLHTTP")},globalSetupOptions={dataFilter:function(e){return e}};return Reqwest.prototype={abort:function(){this._aborted=!0,this.request.abort()},retry:function(){init.call(this,this.o,this.fn)},then:function(e,t){return e=e||function(){},t=t||function(){},this._fulfilled?this._responseArgs.resp=e(this._responseArgs.resp):this._erred?t(this._responseArgs.resp,this._responseArgs.msg,this._responseArgs.t):(this._fulfillmentHandlers.push(e),this._errorHandlers.push(t)),this},always:function(e){return this._fulfilled||this._erred?e(this._responseArgs.resp):this._completeHandlers.push(e),this},fail:function(e){return this._erred?e(this._responseArgs.resp,this._responseArgs.msg,this._responseArgs.t):this._errorHandlers.push(e),this},"catch":function(e){return this.fail(e)}},reqwest.serializeArray=function(){var e=[];return eachFormElement.apply(function(t,n){e.push({name:t,value:n})},arguments),e},reqwest.serialize=function(){if(arguments.length===0)return"";var e,t,n=Array.prototype.slice.call(arguments,0);return e=n.pop(),e&&e.nodeType&&n.push(e)&&(e=null),e&&(e=e.type),e=="map"?t=serializeHash:e=="array"?t=reqwest.serializeArray:t=serializeQueryString,t.apply(null,n)},reqwest.toQueryString=function(e,t){var n,r,i=t||!1,s=[],o=encodeURIComponent,u=function(e,t){t="function"==typeof t?t():t==null?"":t,s[s.length]=o(e)+"="+o(t)};if(isArray(e))for(r=0;e&&r<e.length;r++)u(e[r].name,e[r].value);else for(n in e)e.hasOwnProperty(n)&&buildParams(n,e[n],i,u);return s.join("&").replace(/%20/g,"+")},reqwest.getcallbackPrefix=function(){return callbackPrefix},reqwest.compat=function(e,t){return e&&(e.type&&(e.method=e.type)&&delete e.type,e.dataType&&(e.type=e.dataType),e.jsonpCallback&&(e.jsonpCallbackName=e.jsonpCallback)&&delete e.jsonpCallback,e.jsonp&&(e.jsonpCallback=e.jsonp)),new Reqwest(e,t)},reqwest.ajaxSetup=function(e){e=e||{};for(var t in e)globalSetupOptions[t]=e[t]},reqwest});
</script>
<meta name="theme-color" content="#2196F3">
</head>
<body>
<style>
html, body{
margin: 0;
padding: 0;
bottom: 0;
}
.card{
display: flex;
align-items: stretch;
}
span.block{
display: inline-block;
}
@media screen and (min-width: 720px) {
.card{
position: absolute;
top: 5vh;
bottom: 5vh;
left: 5vh;
right: 5vh;
font-size: 15pt;
box-shadow: 0 0 5px rgba(0,0,0,0.3), 0 10px 20px rgba(0,0,0,0.3);
-webkit-flex-direction: row;
-moz-flex-direction: row;
-ms-flex-direction: row;
-o-flex-direction: row;
flex-direction: row;
}
.heading{
padding-top: 2em;
}
.title{
flex-basis: 33%;
display: flex;
flex-direction: column;
}
.graph{
flex-basis: 67%;
}
.attribute{
display: flex;
flex-direction: column;
flex-grow:1;
text-align: right;
}
.attribute::before{
content: "";
flex-grow: 1;
}
}
@media screen and (max-width: 719px){
.card{
-webkit-flex-direction: column;
-moz-flex-direction: column;
-ms-flex-direction: column;
-o-flex-direction: column;
flex-direction: column;
height: 100vh;
}
.heading{
padding-top: 2em;
}
.graph{
flex-grow: 1;
}
.attribute{
position: absolute;
top: 1em;
right: 1em;
}
}
.title{
background: #2196F3;
color: #fff;
font-family: "Roboto", "Noto Sans SC", "Noto Sans CJK SC", "Source Han Sans SC";
font-weight: 100;
padding: 1em;
}
.heading-small{
font-size: 1.5em;
}
.heading-large {
font-size: 2.5em;
}
.value {
font-size: 5em;
}
.attribute{
font-size: 0.5em;
}
</style>
<div class="card">
<div class="title">
<div class="heading">
<div class="heading-small">中国银行*</div>
<div class="heading-large"><span class="block">AUD/CNY</span> <span class="block">现汇卖出价</span></div>
</div>
<div class="value" id="value">
123.45
</div>
<div class="attribute">* Footnote goes here。</div>
</div>
<div class="graph" id="graph-container">
</div>
</div>
<script>
var rdata = <?php echo $data ?>;
var data = [];
for (var i = rdata.length - 1; i >= 0; i--){
rdata[i].value = parseFloat(rdata[i].value).toFixed(2);
data.push([new Date(rdata[i].time).valueOf(), parseFloat(rdata[i].value)]);
}
document.getElementById('value').innerText = data[data.length - 1][1];
window.last_date = data[data.length - 1][0];
Highcharts.setOptions({global : {
useUTC: false
}});
var offset = new Date().getTimezoneOffset() / 60;
var chart = Highcharts.stockChart('graph-container', {
plotOptions: {
series: {
marker: {
//enabled: true
}
}
},
rangeSelector: {
selected: 1
},
tooltip:{
dateTimeLabelFormats: {
second: '%Y-%m-%d %H:%M:%S',
minute: '%Y-%m-%d %H:%M:%S',
hour: '%Y-%m-%d %H:%M:%S',
day: '%Y-%m-%d %H:%M:%S',
week: '%Y-%m-%d %H:%M:%S',
month: '%Y-%m-%d %H:%M:%S',
year: '%Y-%m-%d %H:%M:%S',
}
},
series: [{
name: '现汇卖出价',
data: data,
//gapSize: 500,
//type: 'area',
tooltip: {
valueDecimals: 2
},
fillColor: {
linearGradient: {
x1: 0,
y1: 0,
x2: 0,
y2: 1
},
stops: [
[0, Highcharts.getOptions().colors[0]],
[1, Highcharts.Color(Highcharts.getOptions().colors[0]).setOpacity(0).get('rgba')]
]
},
threshold: null
}],
rangeSelector: {
buttons: [{
type: 'hour',
count: 12,
text: '12h'
}, {
type: 'day',
count: 1,
text: '1d'
}, {
type: 'week',
count: 1,
text: '1w'
}, {
type: 'month',
count: 1,
text: '1m'
}, {
type: 'ytd',
text: 'YTD'
}, {
type: 'all',
text: 'All'
}],
selected: 0
},
xAxis: {ordinal: false},
});
setInterval(function(){
var a = new Date();
var m = a.getMinutes();
if (m % 1 == 0){
reqwest("last.php").then(function(d){
var date = new Date(d.time).valueOf();
var val = parseFloat(d.value);
if (date > window.last_date){
chart.series[0].addPoint([date, val], true, true);
window.last_date = date;
document.getElementById('value').innerText = val;
}
}
});
}
}, 60000);
</script>
</body>
</html>
<?php
$s = new PDO('sqlite:data.db');
$s->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
header("Content-type: application/json");
echo json_encode($s->query('SELECT * FROM data ORDER BY time DESC LIMIT 1')->fetch(PDO::FETCH_ASSOC));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment