UNPKG

tlab-trading-toolkit

Version:

A trading toolkit for building advanced trading bots on the GDAX platform

595 lines (533 loc) 23.8 kB
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1, user-scalable=yes"> <title>Demo</title> <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.0.3/socket.io.js"></script> <script src="https://d3js.org/d3-array.v1.min.js"></script> <script src="https://d3js.org/d3-collection.v1.min.js"></script> <script src="https://d3js.org/d3-color.v1.min.js"></script> <script src="https://d3js.org/d3-format.v1.min.js"></script> <script src="https://d3js.org/d3-interpolate.v1.min.js"></script> <script src="https://d3js.org/d3-time.v1.min.js"></script> <script src="https://d3js.org/d3-time-format.v2.min.js"></script> <script src="https://d3js.org/d3-scale.v1.min.js"></script> <script src="dist/browser.js"></script> <link rel="import" href="bower_components/polymer/polymer.html"> <link rel="import" href="bower_components/polymer/polymer-element.html"> <link rel="import" href="bower_components/polymer/lib/elements/dom-repeat.html"> <link rel="import" href="bower_components/polymer/lib/utils/debounce.html"> <link rel="import" href="bower_components/paper-button/paper-button.html"> <style> html, body { width: 100%; height: 100%; margin: 0; font-family: 'Roboto', 'Noto', sans-serif; line-height: 1.5; } market-depth { margin: 10px; width: 100%; height: 100%; overflow: auto; } </style> </head> <body> <input type="text" id="it" value="Binance:BTC/USDT"></input> <paper-button id="button">Change</paper-button> <paper-button id="buttongdax">Change GDAX</paper-button> <paper-button id="buttonbittrex">Change Bittrex</paper-button> <script> document.querySelector('#button').addEventListener('tap', function() { document.querySelector('#depth').scripId = document.querySelector('#it').value; }); let gdaxList = ['GDAX:BTC/USD', 'GDAX:LTC/USD', 'GDAX:ETH/USD', 'GDAX:ETH/BTC']; let gdaxPointer = 0; document.querySelector('#buttongdax').addEventListener('tap', function() { if(gdaxPointer === (gdaxList.length - 1)) { gdaxPointer = -1; } gdaxPointer++; document.querySelector('#depth').scripId = gdaxList[gdaxPointer]; }); let bittrexList = ['Bittrex:NEO/BTC', 'Bittrex:BTC/USDT', 'Bittrex:ETH/USDT', 'Bittrex:ETH/BTC', 'Bittrex:ARK/BTC']; let bittrexPointer = 0; document.querySelector('#buttonbittrex').addEventListener('tap', function() { if(bittrexPointer === (bittrexList.length - 1)) { bittrexPointer = -1; } bittrexPointer++; document.querySelector('#depth').scripId = bittrexList[bittrexPointer]; }); </script> <market-depth id="depth"></market-depth> <dom-module id="market-depth"> <template> <style> :host { display: block; position: relative; width: 100%; height: 100%; box-sizing: border-box; height: 100%; padding: 15px; } .depth-row { position: relative; font-size: 13px; display: flex; justify-content: space-between; text-align: right; word-break: normal; flex-wrap: nowrap; white-space: nowrap; } .bid { color: #549650; } .ask { color: #FC6A42; } .spread { width: 100%; height: 15px; padding: 4px; font-size: 13px; display: flex; justify-content: space-around; } .flex-text { width: 55%; word-break: normal; flex-wrap: nowrap; white-space: nowrap } .price { width: 40%; } .size { width: 25%; } .value { width: 35%; } .size-chart { height: 100%; position: absolute; min-width: 1px; opacity: 0.5; } .ask-size-background { background-color: #FC6A42; } .bid-size-background { background-color: #549650; } .grid { display: grid; grid-gap: 1rem; width: 100%; height: 100%; grid-template-columns: 2fr 1.3fr; grid-template-rows: 8fr 3fr; grid-template-areas: "book history" "depth-chart depth-chart" } .book { grid-area: book; max-height: 100%; overflow: scroll; } .history { grid-area: history; max-height: 100%; overflow: scroll; } .depth-chart { grid-area: depth-chart; } ::-webkit-scrollbar { display: none; } .buy-trade { background: #549650 } .sell-trade { background: #FC6A42; } </style> <div class="grid"> <div id="book" class="book"> <div class="depth-row" style="text-align: right;"> <span class="flex-text size">size</span> <span class="flex-text price">price</span> <span class="flex-text value">value</span> </div> <template is="dom-repeat" items="[[asks]]"> <div class="depth-row"> <div class="size-chart ask-size-background" style$="width:[[item.relativeSizeWidth]]px;"></div> <span class="flex-text size">[[item.size]]</span> <span class="flex-text ask price">[[item.price]]</span> <span class="flex-text value">[[item.value]]</span> </div> </template> <div class="spread"> spread : [[spread]]% </div> <template is="dom-repeat" items="[[bids]]"> <div class="depth-row"> <div class="size-chart bid-size-background" style$="width:[[item.relativeSizeWidth]]px;"></div> <span class="flex-text size">[[item.size]]</span> <span class="flex-text bid price">[[item.price]]</span> <span class="flex-text value">[[item.value]]</span> </div> </template> </div> <div class="history"> <template is="dom-repeat" items="[[trade]]"> <div class="depth-row"> <div class$="size-chart {{getClassForTrade(item)}}" style="width:100%;"></div> <span class="flex-text">[[item.size]]</span> <span class="flex-text">[[item.price]]</span> </div> </template> </div> <div class="depth-chart"> <div style="width:100%;height:100%;position:relative;"> <canvas style="position:absolute;" id="canvas"></canvas> </div> </div> </div> </template> <script> function nFormatter(num, digits = 1) { num = parseFloat(num); var si = [ { value: 1E18, symbol: "E" }, { value: 1E15, symbol: "P" }, { value: 1E12, symbol: "T" }, { value: 1E9, symbol: "G" }, { value: 1E6, symbol: "M" }, { value: 1E3, symbol: "k" } ], rx = /\.0+$|(\.[0-9]*[1-9])0+$/, i; for (i = 0; i < si.length; i++) { if (num >= si[i].value) { return (num / si[i].value).toFixed(digits).replace(rx, "$1") + si[i].symbol; } } return num.toFixed(digits).replace(rx, "$1"); } class MarketDepth extends Polymer.Element { static get is() { return 'market-depth'; } static get properties() { return { scripId: { type: String, value: 'Binance:BTC/USDT', observer: 'scripChanged' }, book: { type: Object }, trade: { type: Array, value: [] }, zoomLevel: { type: Number, value: 4 } } } /** * Instance of the element is created/upgraded. Useful for initializing * state, set up event listeners, create shadow dom. * @constructor */ constructor() { super(); this.bookChange = this._bookChange.bind(this); this._onTrade = this._onTrade.bind(this); } scripChanged(newVal, oldVal) { if (this.book) { this.book.removeAllListeners('LiveOrderbook.update') this.book.removeAllListeners('LiveOrderbook.trade') console.log("Unsubscribe Book", oldVal); unsubscribeBook(oldVal) } this.centeredBook = false; console.log("subscribe Book", newVal); this.set('trade', []); this.book = subscribeBook(newVal); this.book.addListener('LiveOrderbook.update', this.bookChange) this.book.addListener('LiveOrderbook.trade', this._onTrade) } _bookChange() { if(this.book.book.lowestAsk && this.book.book.highestBid) { this._debouncer = Polymer.Debouncer.debounce( this._debouncer, // initially undefined Polymer.Async.timeOut.after(50), () => { console.log('Book changed'); this.updateOrderBook() this.updateDepthMap(); if(!this.centeredBook) { setTimeout(() => { this.$.book.scrollTop = this.$.book.clientHeight / 2; this.centeredBook = true; }, 0) } }); } } getClassForTrade(msg) { if (msg.side === 'buy') { return 'buy-trade'; } return 'sell-trade'; } _onTrade(msg) { msg.price = parseFloat(msg.price).toPrecision(8); msg.size = parseFloat(msg.size).toPrecision(8); this.unshift('trade', msg) } updateOrderBook() { let state = this.book.book.state() let max = Math.max(...state.asks.slice(0, 30).map((val) => val.totalValue.toNumber()), ...state.bids.slice(0, 30).map((val) => val.totalValue.toNumber())); this.set('asks', state.asks.map((ask) => { return { relativeSizeWidth: (38 / max) * ask.totalValue.toNumber(), size: ask.totalSize.toFixed(3), price: ask.price.toFixed(8), value: ask.totalValue.toFixed(3) } }).slice(0, 30).reverse()); this.set('bids', state.bids.map((bid) => { return { relativeSizeWidth: (38 / max) * bid.totalValue.toNumber(), size: bid.totalSize.toFixed(3), price: bid.price.toFixed(8), value: bid.totalValue.toFixed(3) } }).slice(0, 30)); this.set('spread', this.book.book.lowestAsk.price.minus(this.book.book.highestBid.price).dividedBy(this.book.book.highestBid.price).mul(100).toFixed(4)); } updateDepthMap() { //Find mid market price let midMarketPrice = this.book.book.lowestAsk.price.plus(this.book.book.highestBid.price).dividedBy(2); //Get zoom level 3%, 5%, 10%, 20%, let zoomLevel = this.zoomLevel; //Get price for zoom level let endBidPrice = midMarketPrice.mul(1 - zoomLevel / 100); let endAskPrice = midMarketPrice.mul(1 + zoomLevel / 100); //Get orderbook iterator let bidIterator = this.book.book.bids.iterator(); let bidSizeReached = false; let bidValues = []; let bidTotalSize = 0; let bidTotalValue = 0; let bidLevel; while ((bidLevel = bidIterator.prev()) && !bidSizeReached) { if (bidLevel.price.gt(endBidPrice)) { bidTotalSize = bidLevel.totalSize.plus(bidTotalSize); bidTotalValue = bidLevel.price.times(bidLevel.totalSize).plus(bidTotalValue) bidValues.push({ price: bidLevel.price.toNumber(), cumSize: bidTotalSize.toNumber(), cumValue: bidTotalValue.toNumber() }) } else { bidSizeReached = true; } } let askIterator = this.book.book.asks.iterator(); let askSizeReached = false; let askValues = []; let askTotalSize = 0; let askTotalValue = 0; let askLevel; while ((askLevel = askIterator.next()) && !askSizeReached) { if (askLevel.price.lt(endAskPrice)) { askTotalSize = askLevel.totalSize.plus(askTotalSize); askTotalValue = askLevel.price.times(askLevel.totalSize).plus(askTotalValue) askValues.push({ price: askLevel.price.toNumber(), cumSize: askTotalSize.toNumber(), cumValue: askTotalValue.toNumber() }) } else { askSizeReached = true; } } window.requestAnimationFrame(()=> { this.drawDepth(bidValues, askValues) }) } _generateAxisValues(min, max, reqNumbers) { var approx = (max - min) / reqNumbers; var lookup = [ 0.000000025, 0.00000005, 0.00000001, 0.00000025, 0.0000005, 0.0000001, 0.0000025, 0.000005, 0.000001, 0.000025, 0.00005, 0.00001, 0.00025, 0.0005, 0.0001, 0.0025, 0.005, 0.001, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 25.0, 50.0, 100.0, 250.0, 500.0, 1000.0, 2500.0, 5000.0, 10000.0, 25000.0, 50000.0, 100000.0, 500000.0, 1000000.0]; var na = []; for (var i in lookup) { var b = lookup[i] / approx; if (b < 1.0) { b = 1 / b; } na.push(b); } var closest = lookup[na.indexOf(Math.min.apply(this, na))]; var minindex = Math.ceil(min / closest); var maxindex = Math.floor(max / closest); var vals = []; for (var j = minindex; j <= maxindex; j++) { vals.push((j * closest).toFixed(8)); } return vals; } _scaleCanvasForHDPI(canvas, context, width, height) { canvas.width = width * window.devicePixelRatio; canvas.height = height * window.devicePixelRatio; canvas.style.width = width + 'px'; canvas.style.height = height + 'px'; context.translate(0.5, 0.5); context.translate(0, height * window.devicePixelRatio); context.scale(window.devicePixelRatio, -1 * window.devicePixelRatio); } _prepareCanvas() { var width; var height; var changed = false; //Iterate through all layers and get Canvas var canvas = this.$.canvas; var context = canvas.getContext("2d"); context.font = '9pt Roboto'; if (!width || !height) { width = (canvas.parentElement.clientWidth); height = (canvas.parentElement.clientHeight); } this._scaleCanvasForHDPI(canvas, context, width, height); return { context, width, height }; } drawDepth(bidValues, askValues) { let { context, width, height } = this._prepareCanvas(); // context.fillStyle = 'black'; // context.fillRect(0,0, width, height); var startPosition = 0; var endPosition = width / 2; let prices = bidValues.map((val) => val.price) .concat(askValues.map((val) => val.price)) let sizes = bidValues.map((val) => val.cumValue) .concat(askValues.map((val) => val.cumValue)) let min = Math.min(...prices); let max = Math.max(...prices); let minSize = Math.min(...sizes); let maxSize = Math.max(...sizes); let xAxisValues = this._generateAxisValues(min, max, 5); let yAxisValues = this._generateAxisValues(minSize, maxSize, 5); let xScale = d3.scaleLinear(); xScale.domain([min, max]); xScale.range([1, width]); let yScale = d3.scaleLinear(); yScale.domain([minSize, maxSize]); yScale.range([1, height]); context.lineWidth = 2; context.strokeWidth = 2; context.beginPath(); context.save(); context.font = '9pt Roboto'; context.scale(1, -1); yAxisValues.forEach((yValue) => { context.fillText(nFormatter(yValue), 2, -yScale(yValue)); }); yAxisValues.forEach((yValue) => { context.fillText(nFormatter(yValue), width - 45, -yScale(yValue)); }); xAxisValues.forEach((xValue) => { let price = parseFloat(xValue); let label = ''; if(price > 0) { label = price } else { label = price.toExponential(); } context.fillText(label , xScale(xValue), -2); }); context.restore(); context.beginPath(); var grd = context.createLinearGradient(0, 0, 0, height); grd.addColorStop(0, '#FFFFFF'); grd.addColorStop(1, '#549650'); context.fillStyle = '#2C4238'; context.strokeStyle = '#79F65B'; context.moveTo(0, 0); context.lineTo(bidValues[0].price, 0); context.moveTo(xScale(bidValues[0].price) , 0); bidValues.forEach(({ price, cumSize, cumValue }) => { context.lineTo(xScale(price), yScale(cumValue)) }) context.lineTo(0, 0); context.closePath(); // if(!this.printed) { // console.log(xScale(bidValues[0].price), xScale(bidValues[bidValues.length - 1].price) , 0); // console.log(bidValues[0].price, bidValues[bidValues.length - 1].price , 0); // this.printed = true; // } context.stroke(); context.globalAlpha = 0.5; context.fill(); context.globalAlpha = 1; // // Draw Mid point // context.beginPath(); // context.strokeStyle = 'grey'; // context.fillStyle = 'grey'; // context.globalAlpha = 0.1; // context.moveTo(xScale(bidValues[0].price) , 0); // context.lineTo(xScale(bidValues[0].price) , height); // context.lineTo(xScale(askValues[0].price), height) // context.lineTo(xScale(askValues[0].price), 0) // context.closePath(); // context.stroke(); // context.fill(); // context.globalAlpha = 1; // // context.stroke(); context.beginPath(); var grd = context.createLinearGradient(0, 0, 0, height); grd.addColorStop(0, '#FFFFFF'); grd.addColorStop(1, '#FC6A42'); context.fillStyle = '#433333'; context.strokeStyle = '#FF5E32'; context.moveTo(xScale(askValues[0].price), 0) askValues.forEach(({ price, cumSize, cumValue }) => { context.lineTo(xScale(price), yScale(cumValue)) }) context.lineTo(xScale(askValues[askValues.length - 1].price), 0); context.closePath(); context.stroke(); context.globalAlpha = 0.5; context.fill(); context.globalAlpha = 1; } } customElements.define(MarketDepth.is, MarketDepth); </script> </dom-module> </body> </html>