/*Ethan Alexander Shulman 2019 xaloez.com
Practice Trading on the Toronto Stock Exchange
INSTRUCTIONS:
1. Navigate to https://web.tmxmoney.com/getquote.php in Firefox or Chrome.
2. Right click anywhere on the web page and press 'Inspect Element'.
3. In the newly opened menu select 'Console'.
4. Copy and paste all of this text file into the console and press Enter to execute it.
5. Practice Trading UI should appear at the top of the webpage.*/
(function() {
var freader = new FileReader(),overlay,balanceTxt,accSel,accNameTxt,stockTable,selAcc = 0,accounts = [],data = {},parsedStock,
afterStockParse,buyStockCount,
FREQ_MONTHLY = 0, FREQ_QUARTERLY = 1, FREQ_ANNUALLY = 2, FREQ_NONE = 3,
FREQ_TIMES = [2628000000,7884000000,525600000];
function StockData() {
this.symbol = "";
this.price = 0;
this.dividend = 0;
this.divFreq = 0;
this.time = 0;
}
function Stock() {
this.count = 0;
this.buyPrice = 0;
this.divs = 0;
this.lastDivDate = Date.now();
}
function Account() {
this.name = "New Account";
this.balance = 0;
this.stocks = {};
}
function dge(s) {
return document.getElementById(s);
}
function avt(s,e,f) {
document.getElementById(s).addEventListener(e,f);
}
function tvis(e,v) {
e.style.display = v?"block":"none";
}
function ctd(t,n) {
var el = document.createElement("td");
el.className += "pt";
el.innerHTML = n;
t.appendChild(el);
}
function GET(url,callback) {
var req = new XMLHttpRequest();
req.open("GET",url,true);
if (callback) {
req.onreadystatechange = function() {
if (req.readyState == 4) callback(req.status, req.responseText);
};
}
req.send();
}
function updateAccSelect() {
accSel.innerHTML = "";
for (var i = 0; i < accounts.length; i++) {
var no = document.createElement("option");
no.innerHTML = accounts[i].name;
accSel.appendChild(no);
}
}
function updateBalance(v) {
var a = accounts[selAcc];
a.balance += v;
balanceTxt.innerHTML = ""+a.balance.toFixed(2);
}
function updateSelectedAcc() {
var a = accounts[selAcc];
accNameTxt.value = a.name;
updateBalance(0);
updateStockTable();
}
function updateStockTable() {
stockTable.innerHTML = "
Symbol
Stock
Buy Value
Current Value
Net Value
Dividends
";
var acc = accounts[selAcc], sh = Object.keys(acc.stocks);
for (var i = 0; i < sh.length; i++) {
var tr = document.createElement("tr"), sto = acc.stocks[sh[i]];
ctd(tr,sh[i]);
ctd(tr,sto.count);
ctd(tr,"$"+sto.buyPrice.toFixed(2));
var nval = data[sh[i]].price*sto.count;
ctd(tr,"$"+nval.toFixed(2));
nval -= sto.buyPrice;
var nstr;
if (nval === 0) nstr = "$0";
else if (nval < 0) nstr = "$"+nval.toFixed(2)+"";
else nstr = "$+"+nval.toFixed(2)+""
ctd(tr,nstr);
ctd(tr,"$"+sto.divs.toFixed(2));
stockTable.appendChild(tr);
}
}
function tryBuy() {
//try and buy stock
var acc = accounts[selAcc], tprice = buyStockCount*parsedStock.price;
if (tprice > acc.balance) {
alert("You do not have enough in your balance to buy "+buyStockCount+" "+parsedStock.symbol+" for $"+tprice+".");
return;
}
if (confirm("Are you sure you want to buy "+buyStockCount+" "+parsedStock.symbol+" for $"+tprice+"?")) {
var sto;
if ((sto = acc.stocks[parsedStock.symbol])) {
sto.count += buyStockCount;
sto.buyPrice += tprice;
} else {
sto = new Stock();
sto.count = buyStockCount;
sto.buyPrice = tprice;
acc.stocks[parsedStock.symbol] = sto;
}
updateBalance(-tprice);
updateStockTable();
}
}
function parseStockPage(status,txt) {
if (status !== 200) {
alert("Error could not find stock '"+parsedStock.symbol+"'.");
return;
}
parsedStock.time = Date.now();
var pind = txt.indexOf('class="price"');
if (pind === -1) {
alert("Error parsing html!");
return;
}
pind = txt.indexOf("",pind);
var eind = txt.indexOf("",pind);
parsedStock.price = parseFloat(txt.substring(pind+6,eind));
pind = txt.indexOf("Dividend:");
pind = txt.indexOf("",pind);
eind = txt.indexOf("",pind);
parsedStock.dividend = parseFloat(txt.substring(pind+8,eind));
pind = txt.indexOf("",eind);
eind = txt.indexOf("= 900000) {
dat.time = nowt;
updateData(dat);
}
}
for (var acci = 0; acci < accounts.length; acci++) {
var acc = accounts[acci],
sn = Object.keys(acc.stocks);
for (var i = 0; i < sn.length; i++) {
var sto = acc.stocks[sn[i]], sd = data[sn[i]];
if (sd.divFreq !== 3 && !isNaN(sd.dividend)) {
var dcount = Math.floor((nowt-sto.lastDivDate)/FREQ_TIMES[sd.divFreq]);
if (dcount > 0) {
var profit = sd.dividend*dcount;
updateBalance(profit);
sto.divs += profit;
sto.lastDivDate += dcount*FREQ_TIMES[sd.divFreq];
}
}
}
}
updateStockTable();
setTimeout(updateLoop,60000);
}
parsedStock = new StockData();
//remove current css styling
var headstyles = document.head.childNodes;
for (var i = 0; i < headstyles.length; i++) {
var ele = headstyles[i];
if (ele.rel && ele.rel === "stylesheet") document.head.removeChild(ele);
}
//inject css stylesheet
var stylecss = document.createElement("style");
stylecss.innerHTML = "html,body {"+
"margin: 0px;"+
"padding: 0px;"+
"background-color: #161616;"+
"color: #cfcfcf;"+
"font-size: 20px;"+
"font-family: Monospace;"+
"}"+
"span.t {"+
"font-size: 30px;"+
"font-weight: 700;"+
"}"+
"a {"+
"color: #45a0cf;"+
"}"+
"a:visited {"+
"color: #3090cf;"+
"}"+
"#st {"+
"width: 100%;"+
"background-color: #202020;"+
"}"+
"th.pt {"+
"background-color: #191919;"+
"color: #909090;"+
"font-weight: 600;"+
"}"+
"td.pt {"+
"background-color: #151515;"+
"color: #cfcfcf;"+
"}"+
"div.tcon {"+
"width: 100%;"+
"height: 60vh;"+
"overflow: auto;"+
"}"+
"span.possh {"+
"text-shadow: 1px 1px 1px #00FF00, -1px -1px 1px #00FF00;"+
"}"+
"span.negsh {"+
"text-shadow: 1px 1px 1px #FF1919, -1px -1px 1px #FF1919;"+
"}";
document.head.appendChild(stylecss);
//inject html over page
overlay = document.createElement("div");
overlay.style.position = "absolute";
overlay.style.zIndex = "10000"+Math.floor(Math.random()*10000);
overlay.style.left = "0px";
overlay.style.top = "0px";
overlay.style.width = "100%";
overlay.style.height = "100%";
overlay.style.backgroundColor = "black";
overlay.innerHTML = "
"+
""+
"Created by Ethan Alexander Shulman";
document.body.appendChild(overlay);
accSel = dge("a");
accNameTxt = dge("nm");
balanceTxt = dge("ba");
stockTable = dge("st");
//init with fresh account
accounts.push(new Account());
updateAccSelect();
updateSelectedAcc();
//on file load
freader.onload = function(v) {
var js = JSON.parse(v.target.result);
accounts = js.accs;
data = js.dat;
accSel.selectedIndex = 0;
selAcc = 0;
updateAccSelect();
updateSelectedAcc();
};
//on file select
avt("fs","change",function(e) {
var t = e.target;
if (t.files.length == 0) return;
freader.readAsText(t.files[0]);
});
//on click load button
avt("lb","click",function() {
if (confirm("Are you sure you want to load new data? This will erase all the current data, any unsaved progress will be lost.")) dge("fs").click();
});
//on click save button
avt("sb","click",function() {
var ah = document.createElement("a");
ah.href = "data:attachment/text," + encodeURI(JSON.stringify({accs:accounts,dat:data}));
ah.target = "_blank";
ah.download = "TSXPracticeTradingData.txt";
document.body.appendChild(ah);
ah.onclick = function() {
document.body.removeChild(ah);
ah = null;
};
ah.click();
});
//on click new account
avt("nb","click",function() {
selAcc = accounts.length;
accounts.push(new Account());
updateAccSelect();
updateSelectedAcc();
accSel.selectedIndex = selAcc;
});
//on click delete account
avt("db","click",function() {
if (accounts.length > 1 && confirm("Are you sure you want to delete this account?")) {
accounts.splice(selAcc,1);
selAcc = Math.max(0,selAcc-1);
updateSelectedAcc();
}
});
//on change account
accSel.addEventListener("change",function() {
selAcc = accSel.selectedIndex;
updateSelectedAcc();
});
//on change name
nm.addEventListener("input",function() {
var nn = accNameTxt.value;
accounts[selAcc].name = nn;
accSel.childNodes[selAcc].innerHTML = nn;
});
//on modify balance
avt("mb","click",function() {
var rv = parseFloat(prompt("Please enter the amount you would like to add(+) or remove(-) from your account:","1000"));
if (!isNaN(rv)) updateBalance(rv);
});
//buy stock
avt("by","click",function() {
var count = parseInt(dge("sc").value);
if (isNaN(count) || count < 1) count = 1;
buyStockCount = count;
parsedStock.symbol = dge("sn").value;
if (parsedStock.symbol.length < 1) {
alert("Please enter the symbol of the company you want to buy.");
return;
}
afterStockParse = 1;
var sdat;
if ((sdat = data[parsedStock.symbol]) && Date.now()-sdat.time < 900000) {
Object.assign(parsedStock,sdat);
tryBuy();
}
else GET("https://web.tmxmoney.com/quote.php?locale=en&qm_symbol="+parsedStock.symbol,parseStockPage);
});
//sell stock
avt("sy","click",function() {
var count = parseInt(dge("sc").value);
if (isNaN(count) || count < 1) count = 1;
var symb = dge("sn").value;
if (symb.length < 1) {
alert("Please enter the symbol of the company you want to sell.");
return;
}
var acc = accounts[selAcc],sto = acc.stocks[symb];
if (!sto || sto.count < count) {
alert("You do not have enough of that stock to sell.");
return;
}
var tprice = data[symb].price*count;
updateBalance(tprice);
sto.buyPrice *= 1.0-count/sto.count;
sto.count -= count;
if (sto.count === 0) delete acc.stocks[symb];
updateStockTable();
});
updateLoop();
}());