/*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 = "SymbolStockBuy ValueCurrent ValueNet ValueDividends"; 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 = "
"+ "Practice Stock Trading on Toronto Stock ExchangeNeed help? Watch this tutorial video."+ "
"+ "
"+ ""+ " Account Name

"+ "Balance: $ CAD     of

"+ "
"+ "

"+ "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(); }());