├── test ├── mocha.opts └── command-test.js ├── capture.png ├── docker-compose.yml ├── .dockerignore ├── .editorconfig ├── Dockerfile ├── quoinex ├── builder │ ├── pri.js │ ├── execution.js │ ├── account.js │ ├── trading.js │ ├── asset.js │ ├── trade.js │ ├── pub.js │ ├── base.js │ └── order.js └── api.js ├── package.json ├── LICENSE ├── .gitignore ├── core ├── terminal.js ├── polyfill.js ├── model.js ├── command.js └── product.js ├── console.js ├── README.md ├── executions.js ├── ticker.js ├── book.js └── yarn.lock /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --timeout 5000 2 | -------------------------------------------------------------------------------- /capture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yamorijp/quac/HEAD/capture.png -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | services: 3 | app: 4 | build: . 5 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .gitignore 3 | .node-version 4 | *.iml 5 | LICENSE 6 | VERSION 7 | README.md 8 | Changelog.md 9 | Makefile 10 | docker-compose.yml 11 | node_modules 12 | 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:boron-slim 2 | 3 | # install nc for sync-request 4 | ENV DEBIAN_FRONTEND noninteractive 5 | RUN apt-get update && apt-get install -y --no-install-recommends \ 6 | netcat 7 | 8 | ADD . /quac/ 9 | RUN cd /quac && npm install 10 | -------------------------------------------------------------------------------- /quoinex/builder/pri.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /** 4 | * II Authenticated API 5 | * 6 | * All requests to Authenticated endpoints must be properly 7 | * signed as shown in Authentication. 8 | */ 9 | 10 | const base = require("./base"); 11 | 12 | 13 | module.exports = Object.assign({}, 14 | require("./order"), 15 | require("./execution"), 16 | require("./account"), 17 | require("./asset"), 18 | require("./trading"), 19 | require("./trade") 20 | ); 21 | module.exports.set_credential = base.set_credential; 22 | module.exports.get_credential = base.get_credential; 23 | module.exports.clear_credential = base.clear_credential; 24 | 25 | -------------------------------------------------------------------------------- /quoinex/builder/execution.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /** 4 | * Executions 5 | */ 6 | 7 | const base = require("./base"); 8 | 9 | 10 | /** 11 | * Get Your Executions 12 | */ 13 | class GetExecutions extends base.PagerMixin(base.Request) { 14 | 15 | constructor() { 16 | super("GET", "/executions/me", {}, true); 17 | } 18 | 19 | _validation_schema() { 20 | return { 21 | type: "object", 22 | anyOf: [ 23 | {required: ["currency_pair_code"]}, 24 | {required: ["product_id"]} 25 | ] 26 | }; 27 | } 28 | 29 | currency_pair_code(v) { 30 | this._set("currency_pair_code", base.upper(v), {type: "string"}); 31 | return this; 32 | } 33 | 34 | product_id(v) { 35 | this._set("product_id", v, {type: "number"}); 36 | return this; 37 | } 38 | } 39 | 40 | 41 | module.exports.getexecutions = GetExecutions; 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "quac", 3 | "version": "0.3.0", 4 | "description": "LIQUID (QUOINE) Exchange API Console", 5 | "keywords": [ 6 | "liquid", 7 | "quoine", 8 | "quoinex", 9 | "api", 10 | "console", 11 | "repl" 12 | ], 13 | "main": "console.js", 14 | "scripts": { 15 | "test": "mocha", 16 | "start": "./console.js", 17 | "console": "./console.js", 18 | "board": "./board.js", 19 | "executions": "./executions.js" 20 | }, 21 | "engines": { 22 | "node": ">= 6.10.0" 23 | }, 24 | "author": "Nihon Yamori ", 25 | "private": true, 26 | "license": "MIT", 27 | "dependencies": { 28 | "commander": "^2.11.0", 29 | "jsonschema": "^1.2.0", 30 | "jsonwebtoken": "^8.1.0", 31 | "liquid-tap": "^0.3.5", 32 | "lodash.throttle": "^4.1.1", 33 | "pusher-js": "^4.2.1", 34 | "sync-request": "^4.1.0", 35 | "then-request": "^4.1.0" 36 | }, 37 | "devDependencies": { 38 | "mocha": "^8.4.0" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 yamorijp 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | *.iml 61 | .node-version 62 | .credential.json 63 | -------------------------------------------------------------------------------- /quoinex/builder/account.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /** 4 | * Accounts 5 | */ 6 | 7 | const base = require("./base"); 8 | 9 | 10 | /** 11 | * Get Fiat Accounts 12 | */ 13 | class GetFiatAccounts extends base.Request { 14 | 15 | constructor() { 16 | super("GET", "/fiat_accounts", {}, true); 17 | } 18 | } 19 | 20 | /** 21 | * Create an Fiat Account 22 | */ 23 | class CreateFiatAccount extends base.Request { 24 | 25 | constructor() { 26 | super("POST", "/fiat_accounts", {}, true); 27 | } 28 | 29 | _validation_schema() { 30 | return { type: "object", required: ["currency"] }; 31 | } 32 | 33 | currency(v) { 34 | this._set("currency", base.upper(v), {type: "string"}); 35 | return this; 36 | } 37 | } 38 | 39 | /** 40 | * Get Crypto Accounts 41 | */ 42 | class GetCryptoAccounts extends base.Request { 43 | 44 | constructor() { 45 | super("GET", "/crypto_accounts", {}, true); 46 | } 47 | } 48 | 49 | /** 50 | * Get all Account Balances 51 | */ 52 | class GetAccountBalances extends base.Request { 53 | 54 | constructor() { 55 | super("GET", "/accounts/balance", {}, true); 56 | } 57 | } 58 | 59 | module.exports.getfiataccounts = GetFiatAccounts; 60 | module.exports.createfiataccont = CreateFiatAccount; 61 | module.exports.getcryptoaccounts = GetCryptoAccounts; 62 | module.exports.getaccountbalances = GetAccountBalances; 63 | -------------------------------------------------------------------------------- /core/terminal.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const black = '\u001b[30m'; 4 | const red = '\u001b[31m'; 5 | const green = '\u001b[32m'; 6 | const yellow = '\u001b[33m'; 7 | const blue = '\u001b[34m'; 8 | const magenta = '\u001b[35m'; 9 | const cyan = '\u001b[36m'; 10 | const white = '\u001b[37m'; 11 | 12 | const bold = '\u001b[1m'; 13 | const underline = '\u001b[4m'; 14 | 15 | const reset = '\u001b[0m'; 16 | 17 | const clear = "\x1Bc"; 18 | const separator = "------------------------------------------------"; 19 | const nl = "\n"; 20 | 21 | const colorful = (color, value) => color + value + reset; 22 | const updown_color = (left, right) => { 23 | if (left == right) return white; 24 | return left > right ? green : red; 25 | }; 26 | 27 | module.exports.black = black; 28 | module.exports.red = red; 29 | module.exports.green = green; 30 | module.exports.yellow = yellow; 31 | module.exports.blue = blue; 32 | module.exports.magenta = magenta; 33 | module.exports.cyan = cyan; 34 | module.exports.white = white; 35 | 36 | module.exports.bold = bold; 37 | module.exports.underline = underline; 38 | 39 | module.exports.reset = reset; 40 | module.exports.clear = clear; 41 | module.exports.separator = separator; 42 | module.exports.nl = nl; 43 | module.exports.colorful = colorful; 44 | module.exports.updown_color = updown_color; 45 | module.exports.bid_color = green; 46 | module.exports.ask_color = red; 47 | -------------------------------------------------------------------------------- /quoinex/builder/trading.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /** 4 | * Get Trading Accounts 5 | */ 6 | 7 | const base = require("./base"); 8 | 9 | 10 | /** 11 | * Get Trading Accounts 12 | */ 13 | class GetTradingAccounts extends base.Request { 14 | 15 | constructor() { 16 | super("GET", "/trading_accounts", {}, true); 17 | } 18 | } 19 | 20 | 21 | /** 22 | * Get a Trading Account 23 | */ 24 | class GetTradingAccount extends base.Request { 25 | 26 | constructor() { 27 | super("GET", "/trading_accounts/:id", {}, true); 28 | } 29 | 30 | _validation_schema() { 31 | return { type: "object", required: [":id"] }; 32 | } 33 | 34 | id(v) { 35 | this._set(":id", v, {type: "number"}); 36 | return this; 37 | } 38 | } 39 | 40 | /** 41 | * Update Leverage Level 42 | */ 43 | class UpdateLeverageLevel extends base.Request { 44 | 45 | constructor() { 46 | super("PUT", "/trading_accounts/:id", {}, true); 47 | } 48 | 49 | _validation_schema() { 50 | return { type: "object", required: [":id", "leverage_level"] }; 51 | } 52 | 53 | id(v) { 54 | this._set(":id", v, {type: "number"}); 55 | return this; 56 | } 57 | 58 | leverage_level(v) { 59 | this._set("leverage_level", v, {type: "number"}); 60 | return this; 61 | } 62 | } 63 | 64 | module.exports.gettradingaccounts = GetTradingAccounts; 65 | module.exports.gettradingaccount = GetTradingAccount; 66 | module.exports.updateleveragelevel = UpdateLeverageLevel; 67 | -------------------------------------------------------------------------------- /core/polyfill.js: -------------------------------------------------------------------------------- 1 | // String#padEnd 2 | if (!String.prototype.padEnd) { 3 | String.prototype.padEnd = function padEnd(targetLength, padString) { 4 | targetLength = targetLength >> 0; //floor if number or convert non-number to 0; 5 | padString = String(padString || ' '); 6 | if (this.length > targetLength) { 7 | return String(this); 8 | } 9 | else { 10 | targetLength = targetLength - this.length; 11 | if (targetLength > padString.length) { 12 | padString += padString.repeat(targetLength / padString.length); //append to original to ensure we are longer than needed 13 | } 14 | return String(this) + padString.slice(0, targetLength); 15 | } 16 | }; 17 | } 18 | 19 | // String#padStart 20 | if (!String.prototype.padStart) { 21 | String.prototype.padStart = function padStart(targetLength, padString) { 22 | targetLength = targetLength >> 0; //floor if number or convert non-number to 0; 23 | padString = String(padString || ' '); 24 | if (this.length > targetLength) { 25 | return String(this); 26 | } 27 | else { 28 | targetLength = targetLength - this.length; 29 | if (targetLength > padString.length) { 30 | padString += padString.repeat(targetLength / padString.length); //append to original to ensure we are longer than needed 31 | } 32 | return padString.slice(0, targetLength) + String(this); 33 | } 34 | }; 35 | } 36 | 37 | // Object.values and Object.entries 38 | const reduce = Function.bind.call(Function.call, Array.prototype.reduce); 39 | const isEnumerable = Function.bind.call(Function.call, Object.prototype.propertyIsEnumerable); 40 | const concat = Function.bind.call(Function.call, Array.prototype.concat); 41 | const keys = Reflect.ownKeys; 42 | 43 | if (!Object.values) { 44 | Object.values = function values(O) { 45 | return reduce(keys(O), (v, k) => concat(v, typeof k === 'string' && isEnumerable(O, k) ? [O[k]] : []), []); 46 | }; 47 | } 48 | 49 | if (!Object.entries) { 50 | Object.entries = function entries(O) { 51 | return reduce(keys(O), (e, k) => concat(e, typeof k === 'string' && isEnumerable(O, k) ? [[k, O[k]]] : []), []); 52 | }; 53 | } 54 | -------------------------------------------------------------------------------- /quoinex/builder/asset.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /** 4 | * Assets Lending 5 | */ 6 | 7 | const base = require("./base"); 8 | 9 | /** 10 | * Create a loan bid 11 | */ 12 | class CreateLoadBid extends base.Request { 13 | 14 | constructor() { 15 | super("POST", "/loan_bids", {}, true); 16 | } 17 | 18 | _validation_schema() { 19 | return { type: "object", required: ["rate", "quantity", "currency"] }; 20 | } 21 | 22 | rate(v) { 23 | this._set("rate", v, {type: "number"}); 24 | return this; 25 | } 26 | 27 | quantity(v) { 28 | this._set("quantity", v, {type: "number"}); 29 | return this; 30 | } 31 | 32 | currency(v) { 33 | this._set("currency", base.upper(v), {type: "string"}); 34 | return this; 35 | } 36 | } 37 | 38 | /** 39 | * Get loan bids 40 | */ 41 | class GetLoanBid extends base.Request { 42 | 43 | constructor() { 44 | super("GET", "/loan_bids", {}, true); 45 | } 46 | 47 | currency(v) { 48 | this._set("currency", base.upper(v), {type: "string"}); 49 | return this; 50 | } 51 | } 52 | 53 | /** 54 | * Close loan bid 55 | */ 56 | class CloseLoadBid extends base.Request { 57 | 58 | constructor() { 59 | super("PUT", "/loan_bids/:id/close", {}, true); 60 | } 61 | 62 | _validation_schema() { 63 | return { type: "object", required: [":id"] }; 64 | } 65 | 66 | id(v) { 67 | this._set(":id", v, {type: "number"}); 68 | return this; 69 | } 70 | } 71 | 72 | /** 73 | * Get Loans 74 | */ 75 | class GetLoans extends base.Request { 76 | 77 | constructor() { 78 | super("GET", "/loans", {}, true); 79 | } 80 | 81 | currency(v) { 82 | this._set("currency", base.upper(v), {type: "string"}); 83 | return this; 84 | } 85 | } 86 | 87 | /** 88 | * Update a Loan 89 | */ 90 | class UpdateLoan extends base.Request { 91 | 92 | constructor() { 93 | super("PUT", "/loans/:id", {}, true); 94 | } 95 | 96 | _validation_schema() { 97 | return { type: "object", required: [":id"] }; 98 | } 99 | 100 | id(v) { 101 | this._set(":id", v, {type: "number"}); 102 | return this; 103 | } 104 | 105 | fund_reloaned(v) { 106 | this._set("fund_reloaned", v, {type: "boolean"}); 107 | return this; 108 | } 109 | 110 | } 111 | 112 | module.exports.createloanbid = CreateLoadBid; 113 | module.exports.getloanbid = GetLoanBid; 114 | module.exports.closeloanbid = CloseLoadBid; 115 | module.exports.getloans = GetLoans; 116 | module.exports.updateloan = UpdateLoan; 117 | -------------------------------------------------------------------------------- /quoinex/builder/trade.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /** 4 | * Trades 5 | */ 6 | 7 | const base = require("./base"); 8 | 9 | 10 | /** 11 | * Get Trades 12 | */ 13 | class GetTrades extends base.Request { 14 | 15 | constructor() { 16 | super("GET", "/trades", {}, true); 17 | } 18 | 19 | funding_currency(v) { 20 | this._set("funding_currency", base.upper(v), {type: "string"}); 21 | return this; 22 | } 23 | 24 | status(v) { 25 | this._set("status", base.lower(v), {"enum": ["open", "closed"]}); 26 | return this; 27 | } 28 | } 29 | 30 | /** 31 | * Close a trade 32 | */ 33 | class CloseTrade extends base.Request { 34 | 35 | constructor() { 36 | super("PUT", "/trades/:id/close", {}, true); 37 | } 38 | 39 | _validation_schema() { 40 | return { type: "object", required: [":id"] }; 41 | } 42 | 43 | id(v) { 44 | this._set(":id", v, {type: "number"}); 45 | return this; 46 | } 47 | 48 | closed_quantity(v) { 49 | this._set("closed_quantity", v, {type: "number"}); 50 | return this; 51 | } 52 | } 53 | 54 | /** 55 | * Close all trade 56 | */ 57 | class CloseAllTrade extends base.Request { 58 | 59 | constructor() { 60 | super("PUT", "/trades/close_all", {}, true); 61 | } 62 | 63 | side(v) { 64 | this._set("side", base.lower(v), {"enum": ["short", "long"]}); 65 | return this; 66 | } 67 | } 68 | 69 | /** 70 | * Update a trade 71 | */ 72 | class UpdateTrade extends base.Request { 73 | 74 | constructor() { 75 | super("PUT", "/trades/:id", {}, true); 76 | } 77 | 78 | _validation_schema() { 79 | return { type: "object", required: [":id"] }; 80 | } 81 | 82 | id(v) { 83 | this._set(":id", v, {type: "number"}); 84 | return this; 85 | } 86 | 87 | stop_loss(v) { 88 | this._set("stop_loss", v, {type: "number"}); 89 | return this; 90 | } 91 | 92 | take_profit(v) { 93 | this._set("take_profit", v, {type: "number"}); 94 | return this; 95 | } 96 | } 97 | 98 | /** 99 | * Get a trade's loans 100 | */ 101 | class GetTradeLoans extends base.Request { 102 | 103 | constructor() { 104 | super("GET", "/trades/:id/loans", {}, true); 105 | } 106 | 107 | 108 | _validation_schema() { 109 | return { type: "object", required: [":id"] }; 110 | } 111 | 112 | id(v) { 113 | this._set(":id", v, {type: "number"}); 114 | return this; 115 | } 116 | } 117 | 118 | 119 | module.exports.gettrades = GetTrades; 120 | module.exports.closetrade = CloseTrade; 121 | module.exports.closealltrade = CloseAllTrade; 122 | module.exports.updatetrade = UpdateTrade; 123 | module.exports.gettradeloans = GetTradeLoans; 124 | -------------------------------------------------------------------------------- /console.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | "use strict"; 4 | 5 | require('./core/polyfill'); 6 | 7 | const repl = require('repl'); 8 | 9 | const term = require('./core/terminal'); 10 | const command = require('./core/command'); 11 | 12 | const api = require('./quoinex/api'); 13 | const pub = require('./quoinex/builder/pub'); 14 | const pri = require('./quoinex/builder/pri'); 15 | 16 | const version = require('./package.json').version; 17 | const banner = `${term.yellow}${term.bold} 18 | __ _ _ _ __ _ ___ 19 | / _\` | | | |/ _\` |/ __| 20 | | (_| | |_| | (_| | (__ 21 | \\__, |\\__,_|\\__,_|\\___| 22 | | | 23 | |_| 24 | ${term.reset}${term.yellow} 25 | liquid (quoine) - api - console 26 | ${term.reset} 27 | 28 | コンテキスト変数: 29 | api -> APIクライアント 30 | pub -> パブリックAPI 31 | pri -> プライベートAPIと認証 32 | 33 | コマンド: 34 | .help または .qu_* help を参照 35 | > .qu_buy help 36 | 37 | APIドキュメント: 38 | quoine API - https://developers.quoine.com 39 | 40 | 例: 41 | > pri.set_credential(YOUR_API_ID, YOUR_API_SECRET) 42 | > pri.getorders.create() 43 | > _.currency_pair_code("btcjpy").status("live") 44 | > _.executeSync() 45 | 46 | 47 | `; 48 | 49 | const loadCredential = () => { 50 | try { 51 | const config = require('./.credential.json'); 52 | if (config.api_key && config.api_secret) { 53 | pri.set_credential(config.api_key, config.api_secret); 54 | console.log(term.colorful(term.green, " (.credential.json loaded)\n\n")); 55 | } 56 | } catch (e) { 57 | // not found 58 | } 59 | }; 60 | 61 | const initContext = (context) => { 62 | context.api = api; 63 | context.pub = pub; 64 | context.pri = pri; 65 | }; 66 | 67 | 68 | const main = (program) => { 69 | 70 | if (!program.nobanner) { 71 | process.stdout.write(term.clear); 72 | process.stdout.write(term.nl); 73 | process.stdout.write(banner); 74 | } 75 | 76 | loadCredential(); 77 | 78 | let server = repl.start('> '); 79 | 80 | initContext(server.context); 81 | server.on('reset', initContext); 82 | 83 | Object.values(command.commands) 84 | .forEach(cmd => server.defineCommand(cmd.getName(), { 85 | help: cmd.getHelp(), 86 | action(arg) { 87 | cmd.doAction(server, arg); 88 | } 89 | })); 90 | }; 91 | 92 | 93 | const program = require('commander'); 94 | program 95 | .version(require("./package.json").version) 96 | .description("liquid (quoine) - api - console") 97 | .option("-n, --no-banner", "Don't show ugly startup banner", false) 98 | .on("--help", () => { 99 | console.log(""); 100 | console.log(" Examples:"); 101 | console.log(""); 102 | console.log(" $ node console.js -n"); 103 | console.log(""); 104 | }) 105 | .parse(process.argv); 106 | 107 | main(program); 108 | -------------------------------------------------------------------------------- /quoinex/builder/pub.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /** 4 | * I. Public API 5 | * 6 | * Public API does not require authentication 7 | */ 8 | 9 | const base = require("./base"); 10 | 11 | /** 12 | * Get Products 13 | * 14 | * Get the list of all available products. 15 | */ 16 | class GetProducts extends base.Request { 17 | 18 | constructor() { 19 | super("GET", "/products", {}, false); 20 | } 21 | } 22 | 23 | 24 | /** 25 | * Get a Product 26 | */ 27 | class GetProduct extends base.Request { 28 | 29 | constructor() { 30 | super("GET", "/products/:id", {}, false); 31 | } 32 | 33 | _validation_schema() { 34 | return { type: "object", required: [":id"] }; 35 | } 36 | 37 | id(v) { 38 | this._set(":id", v, {type: "number"}); 39 | return this; 40 | } 41 | } 42 | 43 | /** 44 | * Get Order Book 45 | * 46 | * Format - Each price level follows: [price, amount] 47 | */ 48 | class GetOrderBook extends base.Request { 49 | 50 | constructor() { 51 | super("GET", "/products/:id/price_levels", {}, false); 52 | } 53 | 54 | _validation_schema() { 55 | return { type: "object", required: [":id"] }; 56 | } 57 | 58 | id(v) { 59 | this._set(":id", v, {type: "number"}); 60 | return this; 61 | } 62 | 63 | full(v) { 64 | this._set("full", v, {type: "number"}); 65 | return this; 66 | } 67 | } 68 | 69 | 70 | /** 71 | * Get Executions | Get Executions by Timestamp 72 | * 73 | * Get a list of recent executions from a product 74 | * (Executions are sorted in DESCENDING order - Latest first) 75 | * 76 | * Get a list of executions after a particular time 77 | * (Executions are sorted in ASCENDING order) 78 | */ 79 | class GetExecutions extends base.PagerMixin(base.Request) { 80 | 81 | constructor() { 82 | super("GET", "/executions", {}, false); 83 | } 84 | 85 | _validation_schema() { 86 | return { 87 | type: "object", 88 | anyOf: [ 89 | {required: ["product_id"]}, 90 | {required: ["currency_pair_code"]} 91 | ] 92 | }; 93 | } 94 | 95 | product_id(v) { 96 | this._set("product_id", v, {type: "number"}); 97 | return this; 98 | } 99 | 100 | currency_pair_code(v) { 101 | this._set("currency_pair_code", base.upper(v), {type: "string"}); 102 | return this; 103 | } 104 | 105 | timestamp(v) { 106 | this._set("timestamp", v, {type: "number"}); 107 | return this; 108 | } 109 | } 110 | 111 | 112 | /** 113 | * Get Interest Rate Ladder for a currency 114 | * 115 | * Each level follows: [rate, amount] 116 | */ 117 | class GetInterestRateLadder extends base.Request { 118 | 119 | constructor() { 120 | super("GET", "/ir_ladders/:currency", {}, false); 121 | } 122 | 123 | _validation_schema() { 124 | return { type: "object", required: [":currency"] }; 125 | } 126 | 127 | currency(v) { 128 | this._set(":currency", base.upper(v), {type: "string"}); 129 | return this; 130 | } 131 | } 132 | 133 | 134 | module.exports.getexecutions = module.exports.getexecutionsbytimestamp = GetExecutions; 135 | module.exports.getproducts = GetProducts; 136 | module.exports.getproduct = GetProduct; 137 | module.exports.getorderbook = GetOrderBook; 138 | module.exports.getinterestrateladder = GetInterestRateLadder; 139 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | QUAC 3 |

4 | 5 | 6 | QUAC (Liquid/Quoine API Console) は 仮想通貨取引所 [Liquid (Quoine)](https://www.liquid.com/ja/) が提供する 7 | [Quoine Exchange API](https://developers.quoine.com/v2) を使用した非公式のCLIツールパッケージです。 8 | 9 | Liquid/Quoine Exchange APIクライアントを統合したjavascriptの対話型コンソールプログラム、 10 | リアルタイム更新の板表示プログラム、約定履歴表示プログラムとティッカー表示プログラムを含みます。 11 | 12 | 当プログラムは"experimental"です。 13 | 十分なテストが行われていません。オーダー発行を行う場合は最初に少額でお試しください。 14 | 15 | 16 | 姉妹品としてbitFlyer Lightning APIを使用した [BLAC](https://github.com/yamorijp/blac)もあります。 17 | 18 | 19 | 20 | ## 導入手順 21 | 22 | 当プログラムはNode.jsで動作するjsスクリプトです。 23 | 実行には[Node.js](https://nodejs.org) が必要です。バージョン6.10.0以上を導入してください。 24 | 25 | また、サードパーティ製のnpmモジュールに依存しています。 26 | [npm](https://www.npmjs.com/) か [yarn](https://yarnpkg.com/) を使用してインストールを行ってください。 27 | 28 | $ npm install 29 | 30 | 31 | 32 | ## プログラムの実行方法 33 | 34 | nodeコマンドでスクリプトを実行します。 35 | 36 | $ node console.js 37 | 38 | いくつかコマンドラインオプションがあります。`-h`オプションで確認してください。 39 | 40 | $ node console.js -h 41 | 42 | 43 | ## 対話型コンソール (console.js) 44 | 45 | Quoine Exchange APIクライアントを統合したREPLコンソールプログラムです。 46 | 対話型シェルでjavascriptを使用した注文発注や取消等、Liquid/Quoine Exchange APIの呼び出しが行えます。 47 | 48 | 残高の確認やオーダー発行などAuthenticated APIに属する機能の呼び出しにはAPI Token IDとAPI secretが必要になります。 49 | [LIQUID/QUOINEX](https://app.liquid.com)にログイン後、設定の[APIトークン](https://app.liquid.com/settings/api-tokens)からAPIトークンを作成してください。 50 | 51 | API Token IDとAPI secretは`.qc_set_key`コマンドで設定します。設定した認証情報はプログラム終了時まで有効です。 52 | 53 | > .qc_set_key YOUR_API_TOKEN_ID YOUR_API_SECRET 54 | 55 | `.qc_store_key`コマンドで書き出しを行っておくと起動時に自動で読み込みます。(※平文で保存されます。セキュリティに注意) 56 | 57 | 58 | APIの詳細は、[Quoine Exchange API Reference](https://developers.quoine.com/v2)を参照してください。 59 | 60 | 61 | オプション: 62 | 63 | -n, --no-banner スタートアップバナーを表示しない 64 | 65 | 例: 66 | 67 | $ node console.js -n 68 | 69 | 70 | 71 | ## 板表示プログラム (book.js) 72 | 73 | リアルタイム更新の板表示プログラムです。 74 | 値段範囲で注文をまとめるグルーピング表示に対応しています。(`-g`オプション) 75 | 76 | 77 | オプション: 78 | 79 | -p, --product 通貨ペアコード (デフォルト: BTCJPY) 80 | -r, --row 買いと売り注文の表示行数 (デフォルト: 20) 81 | -g, --group 指定範囲の注文をまとめて表示 (デフォルト: 無効) 82 | 83 | 例: 84 | 85 | $ node book.js -p BTC_JPY -r 32 -g 100 86 | 87 | 88 | ## 約定履歴表示プログラム (executions.js) 89 | 90 | リアルタイム更新の約定履歴表示プログラムです。 91 | 92 | 93 | オプション: 94 | 95 | -p, --product 通貨ペアコード (デフォルト: BTCJPY) 96 | -r, --row 履歴の表示行数 (デフォルト: 40) 97 | 98 | 例: 99 | 100 | $ node executions.js -p ETH_BTC -r 20 101 | 102 | 103 | # ティッカー表示プログラム (ticker.js) 104 | 105 | リアルタイム更新のティッカー表示プログラムです。 106 | 107 | 108 | オプション: 109 | 110 | -p, --product カンマ区切りの通貨ペアコード (デフォルト: "BTCJPY,ETHJPY,BCHJPY,ETHBTC,BTCUSD") 111 | 112 | 例: 113 | 114 | $ node ticker.js -p "BTCUSD,ETHBTC" 115 | 116 | 117 | ## ライセンス 118 | 119 | MIT 120 | 121 | 122 | ## チップ 123 | 124 | BTC: `1BpLZm4JEFiDqAnaexuYMhGJZKdRQJKixP` 125 | ETH: `0x51349760d4a5287dbfa3961ca2e0006936bc9d88` 126 | 127 | BAT ([Brave Rewards](https://brave.com/ja/brave-rewards/))でのチップも熱烈歓迎中! 128 | -------------------------------------------------------------------------------- /executions.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | "use strict"; 4 | 5 | require('./core/polyfill'); 6 | 7 | const throttle = require('lodash.throttle'); 8 | 9 | const term = require('./core/terminal'); 10 | const api = require('./quoinex/api'); 11 | const model = require('./core/model'); 12 | const products = require('./core/product'); 13 | 14 | const render_wait = 200; 15 | 16 | let product = null; 17 | let buffer = new model.ExecutionBuffer(); 18 | 19 | 20 | const _render = () => { 21 | let out = process.stdout; 22 | 23 | out.cork(); 24 | 25 | out.write(term.clear); 26 | out.write(term.nl); 27 | 28 | out.write(" Product:".padEnd(20)); 29 | out.write(product.name.padStart(26)); 30 | out.write(term.nl); 31 | 32 | let stats = buffer.getStats(); 33 | out.write(" Buy:".padEnd(20)); 34 | out.write(term.colorful( 35 | term.bid_color, product.format_volume(stats.buy_volume).padStart(26))); 36 | out.write(term.nl); 37 | 38 | out.write(" Sell:".padEnd(20)); 39 | out.write(term.colorful( 40 | term.ask_color, product.format_volume(stats.sell_volume).padStart(26))); 41 | out.write(term.nl); 42 | 43 | out.write(" Buy/Sell Ratio:".padEnd(20)); 44 | out.write(term.colorful( 45 | term.updown_color(stats.change, 1.0), stats.change.toFixed(2).padStart(26))); 46 | out.write(term.nl); 47 | 48 | out.write(term.separator + term.nl); 49 | 50 | for (let i=buffer.data.length-1; i>=0; i--) { 51 | const row = buffer.data[i]; 52 | out.write(" "); 53 | out.write(row.time.toLocaleTimeString().padEnd(11)); 54 | out.write(term.colorful( 55 | row.side == 'BUY' ? term.bid_color : term.ask_color, 56 | row.side.padEnd(4) + product.format_price(row.price).padStart(13))); 57 | out.write(product.format_volume(row.size).padStart(16)); 58 | out.write(term.nl); 59 | } 60 | 61 | out.write(term.separator + term.nl); 62 | out.write(term.nl); 63 | 64 | process.nextTick(() => out.uncork()); 65 | }; 66 | let render = throttle(_render, render_wait); 67 | 68 | 69 | const main = (program) => { 70 | 71 | product = products.get_product(program.product); 72 | buffer.lock().setCapacity(program.row); 73 | 74 | new api.PublicAPI() 75 | .call('GET', '/executions', {product_id: product.id, limit: program.row}) 76 | .then(data => { 77 | buffer.set(data.models.reverse()); 78 | buffer.unlock(); 79 | render(); 80 | }); 81 | 82 | buffer.unlock(); 83 | 84 | new api.RealtimeAPI() 85 | .subscribe(product.get_executions_channel()) 86 | .bind("created", data => { 87 | buffer.add(JSON.parse(data)); 88 | render(); 89 | }); 90 | }; 91 | 92 | process.on("uncaughtException", (err) => { 93 | console.error("Error:", err.message || err); 94 | process.exit(1); 95 | }); 96 | 97 | const program = require('commander'); 98 | program 99 | .version(require('./package.json').version) 100 | .description("Display QUOINE's execution history") 101 | .option("-p, --product ", "Currency pair code (default: BTCJPY)", 102 | s => s.toUpperCase(), "BTCJPY") 103 | .option("-r, --row ", "Number of display rows (default: 40)", v => parseInt(v), 40) 104 | .on("--help", () => { 105 | console.log(""); 106 | console.log(" Examples:"); 107 | console.log(""); 108 | console.log(" $ node executions.js -p ETHBTC -r 20"); 109 | console.log(""); 110 | }) 111 | .parse(process.argv || process.argv); 112 | 113 | main(program); 114 | -------------------------------------------------------------------------------- /ticker.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | "use strict"; 4 | 5 | require('./core/polyfill'); 6 | 7 | const throttle = require('lodash.throttle'); 8 | const api = require('./quoinex/api'); 9 | const pub = new api.PublicAPI(); 10 | 11 | const term = require('./core/terminal'); 12 | const model = require('./core/model'); 13 | const products = require('./core/product'); 14 | 15 | const render_wait = 200; 16 | 17 | let product_map = new Map(); 18 | let tickers = null; 19 | 20 | 21 | const _render = () => { 22 | let out = process.stdout; 23 | 24 | out.cork(); 25 | 26 | out.write(term.clear); 27 | out.write(term.nl); 28 | 29 | out.write(" Exchange:".padEnd(20)); 30 | out.write("Liquid Exchange".padStart(26)); 31 | out.write(term.nl); 32 | 33 | out.write(" Last Update:".padEnd(20)); 34 | out.write(new Date().toLocaleTimeString().padStart(26)); 35 | out.write(term.nl); 36 | 37 | out.write(term.nl); 38 | 39 | out.write(" " + "Code".padEnd(8)); 40 | out.write(" " + "Price".padStart(10)); 41 | out.write(" " + "24H".padStart(8)); 42 | out.write(" " + "Volume".padStart(15)); 43 | out.write(term.nl); 44 | 45 | out.write(term.separator + term.nl); 46 | 47 | product_map.forEach((p, id) => { 48 | const data = tickers.get(id); 49 | out.write(" " + p.pair.toUpperCase().padEnd(8)); 50 | out.write(" " + term.colorful( 51 | term.updown_color(data.price, data.price_old), 52 | p.format_price(data.price).padStart(10))); 53 | out.write(" " + term.colorful( 54 | term.updown_color(data.change, 0.0), 55 | p.format_change_p(data.change).padStart(8))); 56 | out.write(" " + p.format_volume(data.volume).padStart(15)); 57 | out.write(term.nl); 58 | }); 59 | 60 | out.write(term.separator + term.nl); 61 | out.write(term.nl); 62 | 63 | process.nextTick(() => out.uncork()); 64 | }; 65 | const render = throttle(_render, render_wait); 66 | 67 | 68 | const main = (program) => { 69 | program.product 70 | .split(",").filter(s => s.trim()) 71 | .forEach(s => { 72 | const p = products.get_product(s); 73 | product_map.set(p.id, p); 74 | }); 75 | 76 | tickers = new model.TickerBoard(product_map); 77 | pub.call("GET", "/products") 78 | .then(models => { 79 | models.forEach(m => tickers.update(m.id, m)); 80 | render(); 81 | }) 82 | .then(() => { 83 | const socket = new api.RealtimeAPI(); 84 | const callback = data => { 85 | const data_ = JSON.parse(data); 86 | tickers.update(data_.id, data_); 87 | render(); 88 | } 89 | product_map.forEach(v => socket.subscribe(v.get_ticker_channel()).bind('updated', callback)); 90 | }); 91 | }; 92 | 93 | process.on("uncaughtException", (err) => { 94 | console.error("Error:", err.message || err); 95 | process.exit(1); 96 | }); 97 | 98 | const program = require('commander'); 99 | program 100 | .version(require("./package.json").version) 101 | .description("Display LIQUID's ticker") 102 | .option("-p, --product ", 103 | "Currency pair codes, comma separated (default: BTCJPY,ETHJPY,BCHJPY,ETHBTC,BTCUSD)", 104 | s => s.toUpperCase(), 105 | "BTCJPY,ETHJPY,BCHJPY,ETHBTC,BTCUSD") 106 | .on("--help", () => { 107 | console.log(""); 108 | console.log(" Examples:"); 109 | console.log(""); 110 | console.log(" $ node ticker.js -p BTCUSD,ETHBTC"); 111 | console.log(""); 112 | }) 113 | .parse(process.argv); 114 | 115 | main(program); 116 | 117 | -------------------------------------------------------------------------------- /quoinex/builder/base.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const jschema = require('jsonschema'); 4 | const api = require('../api'); 5 | 6 | 7 | let apiPrivate = new api.PrivateAPI(null, null); 8 | let apiPublic = new api.PublicAPI(); 9 | let _credential = null; 10 | 11 | /** 12 | * Builderで使用するAPI keyとAPI secretを登録 13 | */ 14 | const set_credential = (api_key, api_secret) => { 15 | _credential = {api_key: api_key, api_secret: api_secret}; 16 | }; 17 | 18 | /** 19 | * Builderで使用中のAPI KeyとAPI secretを取得 20 | */ 21 | const get_credential = () => _credential; 22 | 23 | /** 24 | * API keyとAPI secretを削除 25 | */ 26 | const clear_credential = () => { 27 | set_credential(null, null); 28 | }; 29 | 30 | /** 31 | * null以外は強制的に大文字文字列化 32 | */ 33 | const upper = (v) => v === null ? v : (v + "").toUpperCase(); 34 | const lower = (v) => v === null ? v : (v + "").toLowerCase(); 35 | 36 | const bool = (v) => v === null ? v : !!v; 37 | 38 | 39 | /** 40 | * BuilderAPIベースクラス 41 | */ 42 | class Request { 43 | 44 | constructor(method, path, defaultParams, isPrivate) { 45 | this._method = method; 46 | this._path = path; 47 | this._params = defaultParams; 48 | this._is_private = isPrivate; 49 | } 50 | 51 | toString() { 52 | return `${this._method} ${this._path}\t${JSON.stringify(this._params)}`; 53 | } 54 | 55 | static create() { 56 | return new this(); 57 | } 58 | 59 | execute(sync) { 60 | this._validate(); 61 | 62 | let path = this._path; 63 | let params = {}; 64 | Object.keys(this._params).forEach(k => { 65 | if (k.startsWith(":")) { 66 | path = path.replace(k, this._params[k]); 67 | } else { 68 | params[k] = this._params[k]; 69 | } 70 | }); 71 | 72 | let client; 73 | if (this._is_private) { 74 | let credential = get_credential(); 75 | if (!credential) { 76 | throw new Error("Private API requires Credential. Please call 'auth.set_credential(API_KEY, API_SECRET)'."); 77 | } 78 | client = apiPrivate; 79 | client.setCredential(credential.api_key, credential.api_secret); 80 | } else { 81 | client = apiPublic; 82 | } 83 | 84 | if (sync) { 85 | return client.callSync(this._method, path, params); 86 | } else { 87 | return client.call(this._method, path, params); 88 | } 89 | } 90 | 91 | executeSync() { 92 | return this.execute(true); 93 | } 94 | 95 | _validate() { 96 | let result = jschema.validate(this._params, this._validation_schema()); 97 | if (result.errors.length > 0) throw result; 98 | } 99 | 100 | _validation_schema() { 101 | return {}; 102 | } 103 | 104 | _set(name, v, schema) { 105 | // null means delete 106 | if (v === null) { 107 | delete this._params[name]; 108 | return; 109 | } 110 | let result = jschema.validate(v, schema, {propertyName: name}); 111 | if (result.errors.length > 0) throw result; 112 | 113 | this._params[name] = result.instance; 114 | } 115 | 116 | setParams(data) { 117 | Object.entries(data).forEach(item => { 118 | if (typeof(this[item[0]]) === 'function') { 119 | this[item[0]](item[1]); 120 | } 121 | }); 122 | return this; 123 | } 124 | } 125 | 126 | 127 | /** 128 | * Mixin: ページング用プロパティを追加 129 | */ 130 | let PagerMixin = (superclass) => class extends superclass { 131 | 132 | limit(v) { 133 | this._set("limit", v, {type: "number"}); 134 | return this; 135 | } 136 | 137 | page(v) { 138 | this._set("page", v, {type: "number"}); 139 | return this; 140 | } 141 | }; 142 | 143 | 144 | module.exports.get_credential = get_credential; 145 | module.exports.set_credential = set_credential; 146 | module.exports.clear_credential = clear_credential; 147 | module.exports.upper = upper; 148 | module.exports.lower = lower; 149 | module.exports.bool = bool; 150 | 151 | module.exports.Request = Request; 152 | module.exports.PagerMixin = PagerMixin; 153 | -------------------------------------------------------------------------------- /book.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | "use strict"; 4 | 5 | require('./core/polyfill'); 6 | 7 | const throttle = require('lodash.throttle'); 8 | const api = require('./quoinex/api'); 9 | const pub = new api.PublicAPI(); 10 | 11 | const term = require('./core/terminal'); 12 | const model = require('./core/model'); 13 | const products = require('./core/product'); 14 | 15 | const render_wait = 300; 16 | 17 | let product = {}; 18 | let health = new model.Health(); 19 | let ticker = new model.Ticker(); 20 | let book = new model.OrderBook(); 21 | 22 | 23 | const _render = () => { 24 | let out = process.stdout; 25 | 26 | out.cork(); 27 | 28 | out.write(term.clear); 29 | out.write(term.nl); 30 | 31 | out.write(" Product:".padEnd(20)); 32 | out.write(product.name.padStart(26)); 33 | out.write(term.nl); 34 | 35 | out.write(" Last Price:".padEnd(20)); 36 | out.write(term.colorful( 37 | term.updown_color(ticker.price, ticker.price_old), 38 | product.format_price(ticker.price).padStart(26))); 39 | out.write(term.nl); 40 | 41 | out.write(" Bid/Ask Ratio:".padEnd(20)); 42 | const ratio = book.getBuySellRatio(); 43 | out.write(term.colorful( 44 | term.updown_color(ratio, 1.0), 45 | ratio.toFixed(2).padStart(26))); 46 | out.write(term.nl); 47 | 48 | out.write(" 24H Volume:".padEnd(20)); 49 | out.write(product.format_volume(ticker.volume).padStart(26)); 50 | out.write(term.nl); 51 | 52 | out.write(term.separator + term.nl); 53 | 54 | book.getAsks().forEach(row => { 55 | out.write(product.format_volume(row[1]).padStart(16)); 56 | out.write(" " + term.colorful( 57 | term.ask_color, product.format_price(row[0]).padStart(12)) + " "); 58 | out.write("".padEnd(16)); 59 | out.write(term.nl); 60 | }); 61 | book.getBids().forEach(row => { 62 | out.write("".padEnd(16)); 63 | out.write(" " + term.colorful( 64 | term.bid_color, product.format_price(row[0]).padStart(12)) + " "); 65 | out.write(product.format_volume(row[1]).padStart(16)); 66 | out.write(term.nl); 67 | }); 68 | 69 | out.write(term.separator + term.nl); 70 | 71 | out.write(` Service) ${health.health}`); 72 | out.write(term.nl); 73 | 74 | process.nextTick(() => out.uncork()); 75 | }; 76 | const render = throttle(_render, render_wait); 77 | 78 | 79 | const subscribe = () => { 80 | const socket = new api.RealtimeAPI(); 81 | socket.subscribe(product.get_ticker_channel()).bind("updated", data => { 82 | ticker.update(JSON.parse(data)); 83 | render(); 84 | }); 85 | 86 | // update 20 price levels 87 | socket.subscribe(product.get_ladders_buy_channel()).bind("updated", data => { 88 | book.updateBids(JSON.parse(data)); 89 | render(); 90 | }); 91 | socket.subscribe(product.get_ladders_sell_channel()).bind("updated", data => { 92 | book.updateAsks(JSON.parse(data)); 93 | render(); 94 | }); 95 | }; 96 | 97 | const poll_all_price_levels = () => { 98 | // update all price levels 99 | pub.call("GET", "/products/" + product.id + "/price_levels", {full: 1}) 100 | .then(res => { 101 | book.setBids(res.buy_price_levels); 102 | book.setAsks(res.ask_price_levels); 103 | render(); 104 | setTimeout(poll_all_price_levels, 20000); 105 | }); 106 | }; 107 | 108 | 109 | const main = (program) => { 110 | product = products.get_product(program.product); 111 | book.setRowCount(program.row).setGroupingFactor(program.group); 112 | 113 | const check_health = () => { 114 | pub.call("GET", "/products/" + product.id) 115 | .then(() => health.setHealth(1)) 116 | .catch(() => health.setHealth(0)) 117 | .then(() => render()); 118 | }; 119 | check_health(); 120 | setInterval(check_health, 60000); 121 | 122 | pub.call("GET", "/products/" + product.id) 123 | .then(res => { 124 | ticker.update(res); 125 | render(); 126 | poll_all_price_levels(); 127 | subscribe(); 128 | }) 129 | }; 130 | 131 | process.on("uncaughtException", (err) => { 132 | console.error("Error:", err.message || err); 133 | process.exit(1); 134 | }); 135 | 136 | const program = require('commander'); 137 | program 138 | .version(require("./package.json").version) 139 | .description("Display LIQUID's order book") 140 | .option("-p, --product ", "Currency pair code (default: BTCJPY)", s => s.toUpperCase(), "BTCJPY") 141 | .option("-r, --row ", "Number of display rows (default: 20)", v => parseInt(v), 20) 142 | .option("-g, --group ", "Order grouping unit (default: 0.0)", v => parseFloat(v), 0.0) 143 | .on("--help", () => { 144 | console.log(""); 145 | console.log(" Examples:"); 146 | console.log(""); 147 | console.log(" $ node book.js -p BTCJPY -r 32 -g 1000"); 148 | console.log(""); 149 | }) 150 | .parse(process.argv); 151 | 152 | main(program); 153 | 154 | -------------------------------------------------------------------------------- /quoinex/builder/order.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /** 4 | * Orders 5 | */ 6 | 7 | const base = require("./base"); 8 | 9 | 10 | /** 11 | * Create an Order 12 | */ 13 | class CreateOrder extends base.Request { 14 | 15 | constructor() { 16 | super("POST", "/orders", {}, true); 17 | } 18 | 19 | _validation_schema() { 20 | let schema = { 21 | type: "object", 22 | required: ["order_type", "product_id", "side", "quantity"], 23 | }; 24 | if (this._params.order_type == "limit") { 25 | schema.required.push("price"); 26 | } 27 | if (this._params.order_type == "market_with_range") { 28 | schema.required.push("price"); 29 | schema.required.push("price_range"); 30 | } 31 | return schema 32 | } 33 | 34 | order_type(v) { 35 | this._set("order_type", base.lower(v), {"enum": ["limit", "market", "market_with_range"]}); 36 | return this; 37 | } 38 | 39 | product_id(v) { 40 | this._set("product_id", v, {type: "number"}); 41 | return this; 42 | } 43 | 44 | side(v) { 45 | this._set("side", v, {"enum": ["buy", "sell"]}); 46 | return this; 47 | } 48 | 49 | quantity(v) { 50 | this._set("quantity", v, {type: "number"}); 51 | return this; 52 | } 53 | 54 | price(v) { 55 | this._set("price", v, {type: "number"}); 56 | return this; 57 | } 58 | 59 | price_range(v) { 60 | this._set("price_range", v, {type: "number"}); 61 | return this; 62 | } 63 | 64 | leverage_level(v) { 65 | this._set("leverage_level", v, {type: "number"}); 66 | return this; 67 | } 68 | 69 | funding_currency(v) { 70 | this._set("funding_currency", base.upper(v), {type: "string"}); 71 | return this; 72 | } 73 | 74 | order_direction(v) { 75 | this._set("order_direction", base.lower(v), {"enum": ["one_direction", "two_direction", "netout"]}); 76 | return this; 77 | } 78 | } 79 | 80 | 81 | /** 82 | * Get an Order 83 | */ 84 | class GetOrder extends base.Request { 85 | 86 | constructor() { 87 | super("GET", "/orders/:id", {}, true); 88 | } 89 | 90 | _validation_schema() { 91 | return { type: "object", required: [":id"] }; 92 | } 93 | 94 | id(v) { 95 | this._set(":id", v, {type: "number"}); 96 | return this; 97 | } 98 | } 99 | 100 | 101 | /** 102 | * Get Orders 103 | */ 104 | class GetOrders extends base.PagerMixin(base.Request) { 105 | 106 | constructor() { 107 | super("GET", "/orders", {}, true); 108 | } 109 | 110 | currency_pair_code(v) { 111 | this._set("currency_pair_code", base.upper(v), {type: "string"}); 112 | return this; 113 | } 114 | 115 | funding_currency(v) { 116 | this._set("funding_currency", base.upper(v), {type: "string"}); 117 | return this; 118 | } 119 | 120 | product_id(v) { 121 | this._set("product_id", v, {type: "number"}); 122 | return this; 123 | } 124 | 125 | // live, filled, cancelled 126 | status(v) { 127 | this._set("status", base.lower(v), {type: "string"}); 128 | return this; 129 | } 130 | 131 | with_details(v) { 132 | this._set("with_details", v, {type: "number"}); 133 | return this; 134 | } 135 | } 136 | 137 | 138 | /** 139 | * Cancel an Order 140 | */ 141 | class CancelOrder extends base.Request { 142 | 143 | constructor() { 144 | super("PUT", "/orders/:id/cancel", {}, true); 145 | } 146 | 147 | _validation_schema() { 148 | return { type: "object", required: [":id"] }; 149 | } 150 | 151 | id(v) { 152 | this._set(":id", v, {type: "number"}); 153 | return this; 154 | } 155 | } 156 | 157 | 158 | /** 159 | * Edit a Live Order 160 | */ 161 | class EditOrder extends base.Request { 162 | 163 | constructor() { 164 | super("PUT", "/orders/:id", {}, true); 165 | } 166 | 167 | _validation_schema() { 168 | return { type: "object", required: [":id"] }; 169 | } 170 | 171 | id(v) { 172 | this._set(":id", v, {type: "number"}); 173 | return this; 174 | } 175 | 176 | quantity(v) { 177 | this._set("quantity", v, {type: "number"}); 178 | return this; 179 | } 180 | 181 | price(v) { 182 | this._set("price", v, {type: "number"}); 183 | return this; 184 | } 185 | } 186 | 187 | 188 | /** 189 | * Get an Order's Trades 190 | */ 191 | class GetOrderTrades extends base.Request { 192 | 193 | constructor() { 194 | super("GET", "/orders/:id/trades", {}, true); 195 | } 196 | 197 | _validation_schema() { 198 | return { type: "object", required: [":id"] }; 199 | } 200 | 201 | id(v) { 202 | this._set(":id", v, {type: "number"}); 203 | return this; 204 | } 205 | } 206 | 207 | 208 | module.exports.createorder = CreateOrder; 209 | module.exports.getorder = GetOrder; 210 | module.exports.getorders = GetOrders; 211 | module.exports.cancelorder = CancelOrder; 212 | module.exports.editorder = EditOrder; 213 | module.exports.getordertrades = GetOrderTrades; 214 | -------------------------------------------------------------------------------- /quoinex/api.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Quoine API : Generic Client 3 | */ 4 | 5 | "use strict"; 6 | 7 | const qs = require('qs'); 8 | const Pusher = require('pusher-js'); 9 | const tap = require('liquid-tap'); 10 | const jwt = require('jsonwebtoken'); 11 | const request = require('then-request'); 12 | const requestSync = require('sync-request'); 13 | 14 | const ENDPOINT = "https://api.quoine.com"; 15 | const PUSHER_KEY = "2ff981bb060680b5ce97"; 16 | const API_VERSION = 2; 17 | 18 | let debug = false; 19 | 20 | const set_debug = b => debug = b; 21 | 22 | const decode_json = data => data === "" ? "" : JSON.parse(data); 23 | 24 | /** 25 | * HTTP Public API クライアント 26 | * 27 | * new PublicAPI() 28 | * .call("GET", "/products/product_cash_btcjpy_5") 29 | * .then(console.log) 30 | * .catch(console.error) 31 | */ 32 | class PublicAPI { 33 | 34 | makeRequest(method, path, params) { 35 | params = params && Object.keys(params).length ? params : null; 36 | method = method.toUpperCase(); 37 | 38 | if (method == 'GET' && params) path += '?' + qs.stringify(params); 39 | let body = (method != 'GET' && params) ? JSON.stringify(params) : ""; 40 | 41 | let url = ENDPOINT + path; 42 | let options = { 43 | headers: {"Content-Type": "application/json", "X-Quoine-API-Version": API_VERSION}, 44 | body: body, 45 | timeout: 10000, 46 | socketTimeout: 10000 47 | }; 48 | return {method: method, url: url, options: options}; 49 | } 50 | 51 | /** 52 | * リモートAPI呼び出し 53 | * 54 | * @param method {string} 55 | * @param path {string} 56 | * @param params {object} 57 | * @returns {Promise} 58 | */ 59 | call(method, path, params) { 60 | let req = this.makeRequest(method, path, params); 61 | if (debug) { 62 | return Promise.resolve(req); 63 | } else { 64 | return request(req.method, req.url, req.options).getBody('utf-8').then(decode_json); 65 | } 66 | } 67 | 68 | /** 69 | * リモートAPI呼び出し (同期) 70 | * ブロッキング処理につきサーバーでは使用しないこと 71 | * 72 | * @param method {string} 73 | * @param path {string} 74 | * @param params {object} 75 | * @return {object} 76 | */ 77 | callSync(method, path, params) { 78 | let req = this.makeRequest(method, path, params); 79 | if (debug) { 80 | return req; 81 | } else { 82 | let res = requestSync(req.method, req.url, req.options); 83 | return decode_json(res.getBody('utf-8')); 84 | } 85 | } 86 | } 87 | 88 | /** 89 | * HTTP Private API クライアント 90 | * 91 | * new PrivateAPI(API_TOKEN_ID, API_SECRET) 92 | * .call("GET", "/v1/me/getchildorders", {product_code: "BTC_JPY"}) 93 | * .then(console.log) 94 | * .catch(console.error) 95 | */ 96 | class PrivateAPI extends PublicAPI { 97 | 98 | constructor(api_token_id, api_secret) { 99 | super(); 100 | this.setCredential(api_token_id, api_secret); 101 | } 102 | 103 | /** 104 | * 認証情報を設定する 105 | * @param api_token_id 106 | * @param api_secret 107 | */ 108 | setCredential(api_token_id, api_secret) { 109 | this.key = api_token_id; 110 | this.secret = api_secret; 111 | } 112 | 113 | sign(path) { 114 | return jwt.sign({path: path, nonce: Date.now(), token_id: this.key}, this.secret); 115 | } 116 | 117 | makeRequest(method, path, params) { 118 | params = params && Object.keys(params).length ? params : null; 119 | method = method.toUpperCase(); 120 | 121 | if (params && method == "GET") path = path + "?" + qs.stringify(params); 122 | let body = params && method != "GET" ? JSON.stringify(params) : ""; 123 | 124 | let url = ENDPOINT + path; 125 | let options = { 126 | headers: { 127 | 'Content-Type': 'application/json', 128 | 'X-Quoine-API-Version': API_VERSION, 129 | 'X-Quoine-Auth': this.sign(path) 130 | }, 131 | body: body, 132 | timeout: 10000, 133 | socketTimeout: 10000 134 | }; 135 | 136 | return {method: method, url: url, options: options}; 137 | } 138 | } 139 | 140 | 141 | /** 142 | * @deprecated 143 | * @obsolete 144 | * WebSocket API: Pusherクライアントラッパー 145 | * 146 | * https://pusher.com/docs/client_api_guide 147 | * 148 | * new RealtimeAPI() 149 | * .subscribe("product_cash_btcjpy_5") 150 | * .bind("updated", console.log); 151 | */ 152 | class RealtimeAPI extends Pusher { 153 | 154 | constructor(options) { 155 | options = Object.assign({cluster: 'mt1'}, options || {}); 156 | super(PUSHER_KEY, options); 157 | } 158 | } 159 | 160 | 161 | /** 162 | * Realtime API (Liquid Tap) クライアントラッパー 163 | * 164 | * new RealtimeAPI() 165 | * .subscribe("product_cash_btcjpy_5") 166 | * .bind("updated", console.log); 167 | */ 168 | class RealtimeAPI2 { 169 | 170 | constructor(options) { 171 | this.client = new tap.TapClient(options); 172 | } 173 | 174 | subscribe(name) { 175 | return this.client.subscribe(name); 176 | } 177 | } 178 | 179 | module.exports.PublicAPI = PublicAPI; 180 | module.exports.PrivateAPI = PrivateAPI; 181 | module.exports.RealtimeAPI = RealtimeAPI2; 182 | module.exports.set_debug = set_debug; 183 | -------------------------------------------------------------------------------- /core/model.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | class OrderBook { 4 | 5 | constructor() { 6 | this.bids = new Map(); 7 | this.bid_volume = 0.0; 8 | this.asks = new Map(); 9 | this.ask_volume = 0.0; 10 | this.factor = 0.0; 11 | this.size = 24; 12 | } 13 | 14 | setGroupingFactor(n) { 15 | this.factor = n; 16 | return this; 17 | } 18 | 19 | setRowCount(n) { 20 | this.size = n; 21 | return this; 22 | } 23 | 24 | setBids(data) { 25 | this.bids.clear(); 26 | this._mergeBids(data); 27 | return this; 28 | } 29 | 30 | updateBids(data) { 31 | if (!data || !data.length) return this.setBids([]); 32 | 33 | const min = parseFloat(data[data.length - 1][0]); 34 | Array.from(this.bids.keys()) 35 | .filter(key => key >= min) 36 | .forEach(key => this.bids.delete(key)); 37 | 38 | this._mergeBids(data); 39 | return this; 40 | } 41 | 42 | _mergeBids(data=[]) { 43 | data.forEach(row => this.bids.set(parseFloat(row[0]), parseFloat(row[1]))); 44 | this.bid_volume = Array.from(this.bids.values()).reduce((x, y) => x + y); 45 | } 46 | 47 | setAsks(data) { 48 | this.asks.clear(); 49 | this._mergeAsks(data); 50 | return this; 51 | } 52 | 53 | updateAsks(data) { 54 | if (!data || !data.length) return this.setAsks([]); 55 | 56 | const max = parseFloat(data[data.length - 1][0]); 57 | Array.from(this.asks.keys()) 58 | .filter(key => key <= max ) 59 | .forEach(key => this.asks.delete(key)); 60 | 61 | this._mergeAsks(data); 62 | return this; 63 | } 64 | 65 | _mergeAsks(data) { 66 | data.forEach(row => this.asks.set(parseFloat(row[0]), parseFloat(row[1]))); 67 | this.ask_volume = Array.from(this.asks.values()).reduce((x, y) => x + y); 68 | } 69 | 70 | _grouping(data, factor, func) { 71 | let groups = new Map(); 72 | data.forEach((size, price) => { 73 | let group = func(price / factor) * factor; 74 | groups.set(group, (groups.get(group) || 0.0) + size); 75 | }); 76 | return groups; 77 | } 78 | 79 | getBids() { 80 | let rows = this.factor == 0.0 ? this.bids : this._grouping(this.bids, this.factor, Math.floor); 81 | return Array.from(rows.keys()) 82 | .sort((a, b) => b - a) 83 | .slice(0, this.size) 84 | .map(price => [price, rows.get(price)]) 85 | } 86 | 87 | getAsks() { 88 | let rows = this.factor == 0.0 ? this.asks : this._grouping(this.asks, this.factor, Math.ceil); 89 | return Array.from(rows.keys()) 90 | .sort((a, b) => b - a) 91 | .slice(-this.size) 92 | .map(price => [price, rows.get(price)]) 93 | } 94 | 95 | getBuySellRatio() { 96 | return this.bid_volume / this.ask_volume; 97 | } 98 | } 99 | 100 | 101 | class ExecutionBuffer { 102 | 103 | constructor() { 104 | this.capacity = 48; 105 | this.data = []; 106 | this.locking = false; 107 | this.pendings = []; 108 | this.tip = 0; 109 | } 110 | 111 | setCapacity(n) { 112 | this.capacity = n; 113 | return this; 114 | } 115 | 116 | lock() { 117 | this.locking = true; 118 | return this; 119 | } 120 | 121 | unlock() { 122 | this.locking = false; 123 | this.pendings.forEach(data => this.add(data)); 124 | this.pendings = []; 125 | return this; 126 | } 127 | 128 | size() { 129 | return this.data.length; 130 | } 131 | 132 | _toEntity(row) { 133 | return { 134 | id: row.id, 135 | time: new Date(row.created_at * 1000), 136 | side: row.taker_side.toUpperCase(), 137 | price: parseFloat(row.price), 138 | size: parseFloat(row.quantity), 139 | total: parseFloat(row.price) * parseFloat(row.quantity) 140 | }; 141 | } 142 | 143 | getStats() { 144 | let buy_volume = this.data 145 | .filter(row => row.side == 'BUY') 146 | .reduce((prev, curr) => prev + curr.size, 0.0); 147 | let sell_volume = this.data 148 | .filter(row => row.side == 'SELL') 149 | .reduce((prev, curr) => prev + curr.size, 0.0); 150 | let ratio = sell_volume === 0.0 ? 1.0 : buy_volume / sell_volume; 151 | return { 152 | buy_volume: buy_volume, 153 | sell_volume: sell_volume, 154 | change: ratio 155 | }; 156 | } 157 | 158 | set(obj) { 159 | // order: asc 160 | this.data = obj 161 | .slice(-this.capacity) 162 | .map(row => this._toEntity(row)); 163 | this.tip = this.data[this.data.length - 1].id; 164 | return this; 165 | } 166 | 167 | add(obj) { 168 | if (this.locking) { 169 | this.pendings.push(obj); 170 | } else if (obj.id > this.tip) { 171 | let entity = this._toEntity(obj); 172 | this.data.push(entity); 173 | this.tip = entity.id; 174 | if (this.data.length > this.capacity) { 175 | this.data = this.data.slice(-this.capacity); 176 | } 177 | } 178 | return this; 179 | } 180 | } 181 | 182 | 183 | class Ticker { 184 | 185 | constructor() { 186 | this.price_old = 0.0; 187 | this.price = 0.0; 188 | this.change = 0.0; 189 | this.volume = 0.0; 190 | } 191 | 192 | update(data) { 193 | let old = this.price; 194 | this.price = parseFloat(data.last_traded_price); 195 | this.volume = parseFloat(data.volume_24h); 196 | this.change = (1.0 - (this.price / parseFloat(data.last_price_24h))) * -1; 197 | this.price_old = old || this.price; 198 | return this; 199 | } 200 | } 201 | 202 | 203 | class TickerBoard { 204 | 205 | constructor(products) { 206 | this.data = new Map(); 207 | products.forEach((v, k) => this.data.set(k, new Ticker())); 208 | } 209 | 210 | update(id, data) { 211 | const id_ = parseInt(id); 212 | if (this.data.has(id_)) 213 | this.data.get(id_).update(data); 214 | } 215 | 216 | get(id) { 217 | return this.data.get(id); 218 | } 219 | } 220 | 221 | 222 | class Health { 223 | 224 | constructor() { 225 | this.health = ""; 226 | } 227 | 228 | setHealth(b) { 229 | this.health = b ? "ONLINE" : "OFFLINE"; 230 | } 231 | } 232 | 233 | 234 | module.exports.OrderBook = OrderBook; 235 | module.exports.ExecutionBuffer = ExecutionBuffer; 236 | module.exports.Ticker = Ticker; 237 | module.exports.TickerBoard = TickerBoard; 238 | module.exports.Health = Health; 239 | -------------------------------------------------------------------------------- /test/command-test.js: -------------------------------------------------------------------------------- 1 | require('../core/polyfill'); 2 | 3 | const assert = require('assert'); 4 | const command = require('../core/command'); 5 | const pri = require('../quoinex/builder/pri'); 6 | 7 | const upper = s => s.toUpperCase(); 8 | const pass = s => s; 9 | const load_credential = () => { 10 | try { 11 | const config = require('../.credential.json'); 12 | if (config.api_key && config.api_secret) { 13 | pri.set_credential(config.api_key, config.api_secret); 14 | return true; 15 | } 16 | } catch (e) { 17 | } 18 | return false; 19 | }; 20 | 21 | describe('command class argument parser', () => { 22 | describe('required arguments', () => { 23 | const cmd = new command.Command() 24 | .requireArg("r1", "require arg1", upper) 25 | .requireArg("r2", "require arg2", upper); 26 | it('should return array', () => { 27 | assert.deepEqual(cmd.parseArg("hello world"), ["HELLO", "WORLD"]); 28 | }); 29 | it('extra arguments are just added to array', () => { 30 | assert.deepEqual(cmd.parseArg("hello world mocha"), ["HELLO", "WORLD", "mocha"]); 31 | }); 32 | it('should throw error if some arguments is missing', () => { 33 | assert.throws(() => cmd.parseArg("hello"), Error); 34 | }); 35 | }); 36 | 37 | describe('optional arguments', () => { 38 | const cmd = new command.Command() 39 | .optionalArg("o1", "optional arg1", upper, "default1") 40 | .optionalArg("o2", "optional arg1", upper, "default2"); 41 | it('should return array', () => { 42 | assert.deepEqual(cmd.parseArg("hello world"), ["HELLO", "WORLD"]); 43 | }); 44 | it('it is ok even if some arguments is missing', () => { 45 | assert.deepEqual(cmd.parseArg("hello"), ["HELLO", "default2"]); 46 | assert.deepEqual(cmd.parseArg(null), ["default1", "default2"]); 47 | }); 48 | }); 49 | 50 | describe('mix require and optional', () => { 51 | const cmd = new command.Command() 52 | .requireArg("r1", "require arg1", upper, "default1") 53 | .optionalArg("o1", "optional arg1", upper, "default2"); 54 | it('should return array', () => { 55 | assert.deepEqual(cmd.parseArg("hello world"), ["HELLO", "WORLD"]); 56 | }); 57 | it('optional arguments is processed after required one', () => { 58 | assert.deepEqual(cmd.parseArg("hello"), ["HELLO", "default2"]); 59 | }); 60 | it('required arguments is require', () => { 61 | assert.throws(() => cmd.parseArg(""), Error); 62 | }); 63 | }); 64 | }); 65 | 66 | describe('cls command', () => { 67 | const cmd = command.commands.cls; 68 | 69 | describe('_action', () => { 70 | it('should return clear code', () => { 71 | assert.equal(cmd._action([]), require('../core/terminal').clear); 72 | }); 73 | }); 74 | }); 75 | 76 | describe('markets command', () => { 77 | const cmd = command.commands.markets; 78 | describe('_action', () => { 79 | it('should return market price', () => { 80 | let resp = cmd._action([""]); 81 | assert.ok(resp[0].code); 82 | assert.ok(resp[0].price); 83 | }); 84 | }); 85 | }); 86 | 87 | describe('price command', () => { 88 | const cmd = command.commands.price; 89 | describe('_action', () => { 90 | it('should return get_product api response', () => { 91 | let resp = cmd._action(["BTCJPY"]); 92 | assert.equal(resp.currency_pair_code, "BTCJPY"); 93 | assert.ok(resp.last_traded_price); 94 | }); 95 | }); 96 | }); 97 | 98 | 99 | 100 | describe('balance command', () => { 101 | const cmd = command.commands.balance; 102 | describe('_action', () => { 103 | 104 | it('should return api response', () => { 105 | if (!load_credential()) { 106 | assert.fail("this test requires credential!"); 107 | return; 108 | } 109 | let resp = cmd._action([]); 110 | assert.ok(Array.isArray(resp)); 111 | }); 112 | 113 | it('require credential', () => { 114 | pri.clear_credential(); 115 | assert.throws(() => cmd._action([]), Error); 116 | }); 117 | }); 118 | }); 119 | 120 | 121 | describe('orders command', () => { 122 | const cmd = command.commands.orders; 123 | describe('_action', () => { 124 | it('should return orders api response', () => { 125 | if (!load_credential()) { 126 | assert.fail("this test requires credential!"); 127 | return; 128 | } 129 | let resp = cmd._action(["BTCJPY"]); 130 | assert.ok(Array.isArray(resp)); 131 | }); 132 | 133 | it('require credential', () => { 134 | pri.clear_credential(); 135 | assert.throws(() => cmd._action([]), Error); 136 | }); 137 | }); 138 | }); 139 | 140 | describe('histories command', () => { 141 | const cmd = command.commands.histories; 142 | describe('_action', () => { 143 | it('should return getexecutions api response', () => { 144 | if (!load_credential()) { 145 | assert.fail("this test requires credential!"); 146 | return; 147 | } 148 | let resp = cmd._action(["BTCJPY"]); 149 | assert.ok(Array.isArray(resp)); 150 | }); 151 | 152 | it('require credential', () => { 153 | pri.clear_credential(); 154 | assert.throws(() => cmd._action([]), Error); 155 | }); 156 | }); 157 | }); 158 | 159 | describe('buy command', () => { 160 | const cmd = command.commands.buy; 161 | describe('_action', () => { 162 | it('should return order api response', () => { 163 | if (!load_credential()) { 164 | assert.fail("this test requires credential!"); 165 | return; 166 | } 167 | 168 | // I'm not rich. I don't wanna pay GAS. 169 | // let resp = cmd._action(["BTCJPY", 800000, 0.01]); 170 | // assert.ok(resp); 171 | 172 | try { 173 | cmd._action(["BTCJPY", 0.0, 0.0]); 174 | assert.fail("why no error?"); 175 | } catch (e) { 176 | assert.ok(e instanceof Error); 177 | } 178 | }); 179 | 180 | it('require credential', () => { 181 | pri.clear_credential(); 182 | assert.throws(() => cmd._action([]), Error); 183 | }); 184 | }); 185 | }); 186 | 187 | describe('sell command', () => { 188 | const cmd = command.commands.sell; 189 | describe('_action', () => { 190 | it('should return order api response', () => { 191 | if (!load_credential()) { 192 | assert.fail("this test requires credential!"); 193 | return; 194 | } 195 | 196 | // Before I said. I am a poor man. 197 | // let resp = cmd._action(["BTCJPY", 900000, 0.01]); 198 | // assert.ok(resp); 199 | 200 | try { 201 | cmd._action(["BTCJPY", 0.0, 0.0]); 202 | assert.fail("why no error?"); 203 | } catch (e) { 204 | assert.ok(e instanceof Error); 205 | } 206 | }); 207 | 208 | it('require credential', () => { 209 | pri.clear_credential(); 210 | assert.throws(() => cmd._action([]), Error); 211 | }); 212 | }); 213 | }); 214 | 215 | 216 | describe('cancel command', () => { 217 | const cmd = command.commands.cancel; 218 | describe('_action', () => { 219 | it('should return order api response', () => { 220 | if (!load_credential()) { 221 | assert.fail("this test requires credential!"); 222 | return; 223 | } 224 | 225 | try { 226 | cmd._action(["1"]); 227 | assert.fail("why no error?"); 228 | } catch (e) { 229 | assert.ok(true); 230 | } 231 | }); 232 | 233 | it('require credential', () => { 234 | pri.clear_credential(); 235 | assert.throws(() => cmd._action([1]), Error); 236 | }); 237 | }); 238 | }); 239 | -------------------------------------------------------------------------------- /core/command.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const term = require('./terminal'); 4 | const api = require('../quoinex/api'); 5 | const product = require('./product'); 6 | 7 | const base = require('../quoinex/builder/base'); 8 | const pub = require('../quoinex/builder/pub'); 9 | const pri = require('../quoinex/builder/pri'); 10 | 11 | const to_float = (s) => { 12 | let f = parseFloat(s); 13 | if (isNaN(f)) throw new Error("Error: could not convert string to number"); 14 | return f; 15 | }; 16 | 17 | const pass = (s) => s; 18 | 19 | const upper = (s) => s.toUpperCase(); 20 | 21 | const price_or_market = (s) => s.toUpperCase() == 'MARKET' ? 'MARKET' : to_float(s); 22 | 23 | 24 | const order = (side, argv) => { 25 | let order = new pri.createorder() 26 | .side(side) 27 | .product_id(product.get_product(argv[0]).id) 28 | .quantity(argv[2]); 29 | 30 | if (argv[1] == 'MARKET') { 31 | if (argv[3] !== null) { 32 | order.order_type('market_with_range'); 33 | order.price_range(argv[3]); 34 | } else { 35 | order.order_type('market'); 36 | } 37 | } else { 38 | order.order_type('limit'); 39 | order.price(argv[1]); 40 | } 41 | 42 | return order; 43 | }; 44 | 45 | class Command { 46 | constructor(name) { 47 | this._name = name; 48 | this._description = ""; 49 | this._requireArgs = []; 50 | this._optionalArgs = []; 51 | this._action = () => { 52 | }; 53 | } 54 | 55 | getName() { 56 | return this._name; 57 | } 58 | 59 | description(text) { 60 | this._description = text; 61 | return this; 62 | } 63 | 64 | getHelp() { 65 | return this._description; 66 | } 67 | 68 | getFullHelp() { 69 | return "\n " + this._description + "\n" + this.getUsage(); 70 | } 71 | 72 | requireArg(name, help, apply) { 73 | this._requireArgs.push({name: name, help: help, apply: apply}); 74 | return this; 75 | } 76 | 77 | optionalArg(name, help, apply, defaultValue) { 78 | this._optionalArgs.push({name: name, help: help, apply: apply, _: defaultValue}); 79 | return this; 80 | } 81 | 82 | action(func) { 83 | this._action = func; 84 | return this; 85 | } 86 | 87 | getUsage() { 88 | let usage = "\n Usage: ." + this._name + " " + 89 | this._requireArgs.map(rule => "<" + rule.name + ">").join(" ") + " " + 90 | this._optionalArgs.map(rule => "[" + rule.name + "]").join(" ") + "\n"; 91 | 92 | if (this._requireArgs.length || this._optionalArgs.length) { 93 | usage += "\n" + this.getUsageArgs() + "\n"; 94 | } 95 | return usage; 96 | } 97 | 98 | getUsageArgs() { 99 | return [].concat( 100 | this._requireArgs.map(rule => ` <${rule.name}> ${rule.help}`), 101 | this._optionalArgs.map(rule => ` [${rule.name}] ${rule.help}`) 102 | ).join("\n"); 103 | } 104 | 105 | parseArg(arg) { 106 | let argv = typeof arg === 'string' ? arg.trim().split(" ").filter(Boolean) : []; 107 | if (argv.length < this._requireArgs.length) throw new Error("Error: one or more arguments are required"); 108 | 109 | return [].concat( 110 | this._requireArgs 111 | .map(rule => rule.apply(argv.shift())), 112 | this._optionalArgs 113 | .map(rule => argv.length ? rule.apply(argv.shift()) : rule._), 114 | argv 115 | ); 116 | } 117 | 118 | doAction(context, arg) { 119 | if (arg == "help") { 120 | console.log(this.getFullHelp()); 121 | } else { 122 | try { 123 | let argv = this.parseArg(arg); 124 | try { 125 | let data = this._action(argv); 126 | console.log(data); 127 | } catch (e) { 128 | console.error(term.colorful(term.yellow, e.message)); 129 | } 130 | } catch (e) { // parse error 131 | console.error(term.colorful(term.yellow, e.message)); 132 | console.log(this.getUsage()); 133 | } 134 | } 135 | context.displayPrompt(); 136 | } 137 | } 138 | 139 | module.exports.Command = Command; 140 | module.exports.commands = {}; 141 | 142 | module.exports.commands.cls = new Command("qu_cls") 143 | .description("表示をクリアします") 144 | .action(argv => { 145 | return term.clear; 146 | }); 147 | 148 | module.exports.commands.set_key = new Command("qu_set_key") 149 | .description("API keyとAPI secretを登録します") 150 | .requireArg("api_key", "API key", pass) 151 | .requireArg("api_secret", "API secret", pass) 152 | .action(argv => { 153 | const pattern = /^[A-Za-z0-9/+]*=*$/; 154 | if (argv[0].match(pattern) && argv[1].match(pattern)) { 155 | pri.set_credential(argv[0], argv[1]); 156 | return "ok"; 157 | } else { 158 | throw new Error("Error: API key and secret are invalid"); 159 | } 160 | }); 161 | 162 | module.exports.commands.store_key = new Command("qu_store_key") 163 | .description("登録中のAPI keyとAPI secretをファイルに書き出します") 164 | .action(argv => { 165 | const c = pri.get_credential(); 166 | if (c.api_key && c.api_secret) { 167 | require('fs').writeFileSync( 168 | ".credential.json", JSON.stringify(c), {mode: 384 /*0o600*/}); 169 | return "'.credential.json' created"; 170 | } else { 171 | throw new Error("Error: API key and API secret are null"); 172 | } 173 | }); 174 | 175 | 176 | module.exports.commands.markets = new Command("qu_markets") 177 | .description("マーケットサマリーを表示します") 178 | .optionalArg("filter", "通貨ペアコードでの絞り込み", upper, "") 179 | .action(argv => { 180 | const data = new pub.getproducts().executeSync(); 181 | return data 182 | .filter(p => p.last_traded_price && parseFloat(p.last_traded_price)) 183 | .filter(p => p.currency_pair_code.indexOf(argv[0]) > -1) 184 | .map(p => { 185 | try { 186 | return { 187 | code: p.currency_pair_code, 188 | price: to_float(p.last_traded_price), 189 | change: to_float(p.last_traded_price) - to_float(p.last_price_24h), 190 | volume: to_float(p.volume_24h) 191 | }; 192 | } catch { 193 | return null; 194 | } 195 | }) 196 | .filter(p => !!p); 197 | }); 198 | 199 | module.exports.commands.price = new Command("qu_price") 200 | .description("通貨ペアの取引価格を表示します") 201 | .requireArg("currency_pair", "通貨ペアコード", upper) 202 | .action(argv => { 203 | return new pub.getproduct() 204 | .id(product.get_product(argv[0]).id) 205 | .executeSync(); 206 | }); 207 | 208 | module.exports.commands.balance = new Command("qu_balance") 209 | .description("資産残高を表示します *") 210 | .action(argv => { 211 | const fiats = new pri.getfiataccounts().executeSync(); 212 | const crypto = new pri.getcryptoaccounts().executeSync(); 213 | return fiats.concat(crypto) 214 | .filter(a => a.balance && parseFloat(a.balance)) 215 | .map(a => { 216 | return { 217 | currency: a.currency, 218 | balance: to_float(a.balance) 219 | }; 220 | }); 221 | }); 222 | 223 | module.exports.commands.orders = new Command("qu_orders") 224 | .description("オープンな注文を最大10件表示します *") 225 | .optionalArg("currency_pair", "通貨ペアコード", upper, null) 226 | .action(argv => { 227 | let orders = new pri.getorders() 228 | .status("live") 229 | .limit(10); 230 | if (argv[0] !== null) 231 | orders.product_id(product.get_product(argv[0]).id); 232 | return orders.executeSync().models; 233 | }); 234 | 235 | module.exports.commands.histories = new Command("qu_histories") 236 | .description("約定履歴を最大10件表示します *") 237 | .requireArg("currency_pair", "通貨ペアコード", upper) 238 | .action(argv => { 239 | return new pri.getexecutions() 240 | .product_id(product.get_product(argv[0]).id) 241 | .limit(10) 242 | .executeSync().models; 243 | }); 244 | 245 | module.exports.commands.buy = new Command("qu_buy") 246 | .description("買い注文を発行します *") 247 | .requireArg("code", "通貨ペア", upper) 248 | .requireArg("price", "価格 (成行の場合は'MARKET'を指定)", price_or_market) 249 | .requireArg("quantity", "数量", to_float) 250 | .optionalArg("slippage", "成行注文の場合はスリッページを指定できます", parseFloat, null) 251 | .action(argv => { 252 | return order('buy', argv).executeSync(); 253 | }); 254 | 255 | 256 | module.exports.commands.sell = new Command("qu_sell") 257 | .description("売り注文を発行します *") 258 | .requireArg("code", "通貨ペア", upper) 259 | .requireArg("price", "価格 (成行の場合は'MARKET'を指定)", price_or_market) 260 | .requireArg("quantity", "数量", to_float) 261 | .optionalArg("slippage", "成行注文の場合はスリッページを指定できます", parseFloat, null) 262 | .action(argv => { 263 | return order('sell', argv).executeSync(); 264 | }); 265 | 266 | module.exports.commands.cancel = new Command("qu_cancel") 267 | .description("注文をキャンセルします *") 268 | .requireArg("order_id", "注文ID", parseInt) 269 | .action(argv => { 270 | return new pri.cancelorder() 271 | .id(argv[0]) 272 | .executeSync(); 273 | }); 274 | 275 | -------------------------------------------------------------------------------- /core/product.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const api = require('../quoinex/api'); 4 | 5 | const PAIRS = { 6 | // id, code, price_fmt, volume_fmt 7 | "BTCUSD": [1, "CASH", 3, 8], 8 | "BTCEUR": [3, "CASH", 2, 8], 9 | "BTCJPY": [5, "CASH", 2, 8], 10 | "BTCSGD": [7, "CASH", 2, 8], 11 | "BTCHKD": [9, "CASH", 2, 8], 12 | "BTCIDR": [11, "CASH", 0, 8], 13 | "BTCAUD": [13, "CASH", 2, 8], 14 | "BTCPHP": [15, "CASH", 0, 8], 15 | "BTCCNY": [17, "CASH", 2, 8], 16 | "BTCINR": [18, "CASH", 8, 8], 17 | "ETHUSD": [27, "CASH", 3, 8], 18 | "ETHEUR": [28, "CASH", 2, 8], 19 | "ETHJPY": [29, "CASH", 2, 8], 20 | "ETHSGD": [30, "CASH", 2, 8], 21 | "ETHHKD": [31, "CASH", 2, 8], 22 | "ETHIDR": [32, "CASH", 0, 8], 23 | "ETHAUD": [33, "CASH", 2, 8], 24 | "ETHPHP": [34, "CASH", 0, 8], 25 | "ETHCNY": [35, "CASH", 2, 8], 26 | "ETHINR": [36, "CASH", 8, 8], 27 | "ETHBTC": [37, "CASH", 8, 8], 28 | "BCHUSD": [39, "CASH", 3, 8], 29 | "BCHSGD": [40, "CASH", 2, 8], 30 | "BCHJPY": [41, "CASH", 2, 8], 31 | "DASHSGD": [42, "CASH", 2, 8], 32 | "DASHUSD": [43, "CASH", 3, 8], 33 | "DASHJPY": [44, "CASH", 2, 8], 34 | "DASHEUR": [45, "CASH", 2, 8], 35 | "QTUMSGD": [46, "CASH", 2, 8], 36 | "QTUMUSD": [47, "CASH", 3, 8], 37 | "QTUMJPY": [48, "CASH", 2, 8], 38 | "QTUMEUR": [49, "CASH", 2, 8], 39 | "QASHJPY": [50, "CASH", 3, 8], 40 | "QASHETH": [51, "CASH", 8, 8], 41 | "QASHBTC": [52, "CASH", 8, 8], 42 | "NEOUSD": [53, "CASH", 3, 8], 43 | "NEOJPY": [54, "CASH", 2, 8], 44 | "NEOSGD": [55, "CASH", 2, 8], 45 | "NEOEUR": [56, "CASH", 2, 8], 46 | "QASHUSD": [57, "CASH", 3, 8], 47 | "QASHEUR": [58, "CASH", 2, 8], 48 | "QASHSGD": [59, "CASH", 2, 8], 49 | "QASHAUD": [60, "CASH", 2, 8], 50 | "QASHIDR": [61, "CASH", 0, 8], 51 | "QASHHKD": [62, "CASH", 2, 8], 52 | "QASHPHP": [63, "CASH", 0, 8], 53 | "QASHCNY": [64, "CASH", 2, 8], 54 | "QASHINR": [65, "CASH", 8, 8], 55 | "UBTCUSD": [71, "CASH", 3, 8], 56 | "UBTCJPY": [72, "CASH", 2, 8], 57 | "UBTCSGD": [73, "CASH", 2, 8], 58 | "UBTCBTC": [74, "CASH", 8, 8], 59 | "UBTCETH": [75, "CASH", 8, 8], 60 | "UBTCQASH": [76, "CASH", 8, 8], 61 | "XRPJPY": [83, "CASH", 3, 8], 62 | "XRPUSD": [84, "CASH", 3, 8], 63 | "XRPEUR": [85, "CASH", 2, 8], 64 | "XRPSGD": [86, "CASH", 2, 8], 65 | "XRPIDR": [87, "CASH", 0, 8], 66 | "XRPQASH": [88, "CASH", 8, 8], 67 | "RKTSGD": [102, "CASH", 2, 8], 68 | "RKTAUD": [103, "CASH", 2, 8], 69 | "RKTUSD": [104, "CASH", 3, 8], 70 | "RKTEUR": [105, "CASH", 2, 8], 71 | "RKTJPY": [106, "CASH", 2, 8], 72 | "ZECBTC": [107, "CASH", 8, 8], 73 | "REPBTC": [108, "CASH", 8, 8], 74 | "XMRBTC": [109, "CASH", 8, 8], 75 | "ETCBTC": [110, "CASH", 8, 8], 76 | "XRPBTC": [111, "CASH", 8, 8], 77 | "LTCBTC": [112, "CASH", 8, 8], 78 | "XEMBTC": [113, "CASH", 8, 8], 79 | "BCHBTC": [114, "CASH", 8, 8], 80 | "XLMBTC": [115, "CASH", 8, 8], 81 | "DASHBTC": [116, "CASH", 8, 8], 82 | "TRXBTC": [117, "CASH", 8, 8], 83 | "FCTBTC": [118, "CASH", 8, 8], 84 | "NEOBTC": [119, "CASH", 8, 8], 85 | "TRXETH": [120, "CASH", 8, 8], 86 | "INDBTC": [121, "CASH", 8, 8], 87 | "INDETH": [122, "CASH", 8, 8], 88 | "OAXBTC": [127, "CASH", 8, 8], 89 | "OAXETH": [128, "CASH", 8, 8], 90 | "STORJBTC": [131, "CASH", 8, 8], 91 | "STORJETH": [132, "CASH", 8, 8], 92 | "QTUMBTC": [133, "CASH", 8, 8], 93 | "QTUMETH": [134, "CASH", 8, 8], 94 | "NEOETH": [135, "CASH", 8, 8], 95 | "FCTETH": [136, "CASH", 8, 8], 96 | "STXETH": [137, "CASH", 8, 8], 97 | "STXBTC": [138, "CASH", 8, 8], 98 | "VETBTC": [139, "CASH", 8, 8], 99 | "VETETH": [140, "CASH", 8, 8], 100 | "XLMETH": [141, "CASH", 8, 8], 101 | "MCOETH": [142, "CASH", 8, 8], 102 | "MCOBTC": [143, "CASH", 8, 8], 103 | "MCOQASH": [144, "CASH", 8, 8], 104 | "SPHTXETH": [145, "CASH", 8, 8], 105 | "SPHTXBTC": [146, "CASH", 8, 8], 106 | "SPHTXQASH": [147, "CASH", 8, 8], 107 | "DENTBTC": [148, "CASH", 8, 8], 108 | "DENTETH": [149, "CASH", 8, 8], 109 | "DENTQASH": [150, "CASH", 8, 8], 110 | "VZTBTC": [151, "CASH", 8, 8], 111 | "VZTETH": [152, "CASH", 8, 8], 112 | "VZTQASH": [153, "CASH", 8, 8], 113 | "FDXBTC": [154, "CASH", 8, 8], 114 | "FDXETH": [155, "CASH", 8, 8], 115 | "FDXQASH": [156, "CASH", 8, 8], 116 | "TPTBTC": [157, "CASH", 8, 8], 117 | "TPTETH": [158, "CASH", 8, 8], 118 | "TPTQASH": [159, "CASH", 8, 8], 119 | "ONGBTC": [160, "CASH", 8, 8], 120 | "ONGETH": [161, "CASH", 8, 8], 121 | "ONGQASH": [162, "CASH", 8, 8], 122 | "CANBTC": [163, "CASH", 8, 8], 123 | "CANETH": [164, "CASH", 8, 8], 124 | "CANQASH": [165, "CASH", 8, 8], 125 | "IXTBTC": [166, "CASH", 8, 8], 126 | "IXTETH": [167, "CASH", 8, 8], 127 | "IXTQASH": [168, "CASH", 8, 8], 128 | "MTNETH": [169, "CASH", 8, 8], 129 | "SALBTC": [170, "CASH", 8, 8], 130 | "SALETH": [171, "CASH", 8, 8], 131 | "SALQASH": [172, "CASH", 8, 8], 132 | "MTNBTC": [173, "CASH", 8, 8], 133 | "MTNQASH": [174, "CASH", 8, 8], 134 | "SERBTC": [175, "CASH", 8, 8], 135 | "SERETH": [176, "CASH", 8, 8], 136 | "SERQASH": [177, "CASH", 8, 8], 137 | "ECHBTC": [178, "CASH", 8, 8], 138 | "ECHETH": [179, "CASH", 8, 8], 139 | "ECHQASH": [180, "CASH", 8, 8], 140 | "GATBTC": [181, "CASH", 8, 8], 141 | "GATETH": [182, "CASH", 8, 8], 142 | "GATQASH": [183, "CASH", 8, 8], 143 | "RKTETH": [184, "CASH", 8, 8], 144 | "BMCBTC": [185, "CASH", 8, 8], 145 | "BMCETH": [186, "CASH", 8, 8], 146 | "BMCQASH": [187, "CASH", 8, 8], 147 | "ETNBTC": [188, "CASH", 8, 8], 148 | "ETNETH": [189, "CASH", 8, 8], 149 | "ETNQASH": [190, "CASH", 8, 8], 150 | "GZEBTC": [191, "CASH", 8, 8], 151 | "GZEETH": [192, "CASH", 8, 8], 152 | "GZEQASH": [193, "CASH", 8, 8], 153 | "SNIPBTC": [194, "CASH", 8, 8], 154 | "SNIPETH": [195, "CASH", 8, 8], 155 | "SNIPQASH": [196, "CASH", 8, 8], 156 | "STUBTC": [197, "CASH", 8, 8], 157 | "STUETH": [198, "CASH", 8, 8], 158 | "STUQASH": [199, "CASH", 8, 8], 159 | "ENJBTC": [200, "CASH", 8, 8], 160 | "ENJETH": [201, "CASH", 8, 8], 161 | "ENJQASH": [202, "CASH", 8, 8], 162 | "RKTBTC": [203, "CASH", 8, 8], 163 | "RKTQASH": [204, "CASH", 8, 8], 164 | "STACBTC": [205, "CASH", 8, 8], 165 | "STACETH": [206, "CASH", 8, 8], 166 | "STACQASH": [207, "CASH", 8, 8], 167 | "FLIXXBTC": [208, "CASH", 8, 8], 168 | "FLIXXETH": [209, "CASH", 8, 8], 169 | "FLIXXQASH": [210, "CASH", 8, 8], 170 | "DRGBTC": [211, "CASH", 8, 8], 171 | "DRGETH": [212, "CASH", 8, 8], 172 | "DRGQASH": [213, "CASH", 8, 8], 173 | "1WOBTC": [214, "CASH", 8, 8], 174 | "1WOETH": [215, "CASH", 8, 8], 175 | "1WOQASH": [216, "CASH", 8, 8], 176 | "HEROBTC": [217, "CASH", 8, 8], 177 | "HEROETH": [218, "CASH", 8, 8], 178 | "HEROQASH": [219, "CASH", 8, 8], 179 | "EZTBTC": [220, "CASH", 8, 8], 180 | "EZTETH": [221, "CASH", 8, 8], 181 | "EZTQASH": [222, "CASH", 8, 8], 182 | "LDCBTC": [223, "CASH", 8, 8], 183 | "LDCETH": [224, "CASH", 8, 8], 184 | "LDCQASH": [225, "CASH", 8, 8], 185 | "LALABTC": [226, "CASH", 8, 8], 186 | "LALAETH": [227, "CASH", 8, 8], 187 | "LALAQASH": [228, "CASH", 8, 8], 188 | "AMLTBTC": [229, "CASH", 8, 8], 189 | "AMLTETH": [230, "CASH", 8, 8], 190 | "AMLTQASH": [231, "CASH", 8, 8], 191 | "MGOBTC": [232, "CASH", 8, 8], 192 | "MGOETH": [233, "CASH", 8, 8], 193 | "MGOQASH": [234, "CASH", 8, 8], 194 | "HAVBTC": [235, "CASH", 8, 8], 195 | "HAVETH": [236, "CASH", 8, 8], 196 | "HAVQASH": [237, "CASH", 8, 8], 197 | "UKGBTC": [238, "CASH", 8, 8], 198 | "UKGETH": [239, "CASH", 8, 8], 199 | "UKGQASH": [240, "CASH", 8, 8], 200 | "IPSXBTC": [241, "CASH", 8, 8], 201 | "IPSXETH": [242, "CASH", 8, 8], 202 | "IPSXQASH": [243, "CASH", 8, 8], 203 | "FLUZBTC": [244, "CASH", 8, 8], 204 | "FLUZETH": [245, "CASH", 8, 8], 205 | "FLUZQASH": [246, "CASH", 8, 8], 206 | "TPAYBTC": [247, "CASH", 8, 8], 207 | "TPAYETH": [248, "CASH", 8, 8], 208 | "TPAYQASH": [249, "CASH", 8, 8], 209 | "RBLXBTC": [250, "CASH", 8, 8], 210 | "RBLXETH": [251, "CASH", 8, 8], 211 | "RBLXQASH": [252, "CASH", 8, 8], 212 | "BTRNBTC": [253, "CASH", 8, 8], 213 | "BTRNETH": [254, "CASH", 8, 8], 214 | "BTRNQASH": [255, "CASH", 8, 8], 215 | "ADHBTC": [256, "CASH", 8, 8], 216 | "ADHETH": [257, "CASH", 8, 8], 217 | "ADHQASH": [258, "CASH", 8, 8], 218 | "PALBTC": [259, "CASH", 8, 8], 219 | "PALETH": [260, "CASH", 8, 8], 220 | "PALQASH": [261, "CASH", 8, 8], 221 | "FTXBTC": [262, "CASH", 8, 8], 222 | "FTXETH": [263, "CASH", 8, 8], 223 | "FTXQASH": [264, "CASH", 8, 8], 224 | "WINBTC": [265, "CASH", 8, 8], 225 | "WINETH": [266, "CASH", 8, 8], 226 | "WINQASH": [267, "CASH", 8, 8], 227 | "EARTHBTC": [268, "CASH", 8, 8], 228 | "EARTHETH": [269, "CASH", 8, 8], 229 | "EARTHQASH": [270, "CASH", 8, 8], 230 | "FSNBTC": [271, "CASH", 8, 8], 231 | "FSNETH": [272, "CASH", 8, 8], 232 | "FSNQASH": [273, "CASH", 8, 8], 233 | "THRTBTC": [274, "CASH", 8, 8], 234 | "THRTETH": [275, "CASH", 8, 8], 235 | "THRTQASH": [276, "CASH", 8, 8], 236 | "ZCOBTC": [277, "CASH", 8, 8], 237 | "ZCOETH": [278, "CASH", 8, 8], 238 | "ZCOQASH": [279, "CASH", 8, 8], 239 | "XESBTC": [280, "CASH", 8, 8], 240 | "XESETH": [281, "CASH", 8, 8], 241 | "XESQASH": [282, "CASH", 8, 8], 242 | "MITXBTC": [283, "CASH", 8, 8], 243 | "MITXETH": [284, "CASH", 8, 8], 244 | "MITXQASH": [285, "CASH", 8, 8], 245 | "XNKBTC": [286, "CASH", 8, 8], 246 | "XNKETH": [287, "CASH", 8, 8], 247 | "XNKQASH": [288, "CASH", 8, 8], 248 | "ALXBTC": [289, "CASH", 8, 8], 249 | "ALXETH": [290, "CASH", 8, 8], 250 | "ALXQASH": [291, "CASH", 8, 8], 251 | "SGNBTC": [292, "CASH", 8, 8], 252 | "SGNETH": [293, "CASH", 8, 8], 253 | "SGNQASH": [294, "CASH", 8, 8], 254 | "VUUBTC": [295, "CASH", 8, 8], 255 | "VUUQASH": [296, "CASH", 8, 8], 256 | "VUUETH": [297, "CASH", 8, 8], 257 | "CMCTBTC": [298, "CASH", 8, 8], 258 | "CMCTETH": [299, "CASH", 8, 8], 259 | "CMCTQASH": [300, "CASH", 8, 8], 260 | "IDHBTC": [301, "CASH", 8, 8], 261 | "IDHETH": [302, "CASH", 8, 8], 262 | "IDHQASH": [303, "CASH", 8, 8], 263 | "PLCBTC": [304, "CASH", 8, 8], 264 | "PLCETH": [305, "CASH", 8, 8], 265 | "PLCQASH": [306, "CASH", 8, 8], 266 | "GETBTC": [307, "CASH", 8, 8], 267 | "GETETH": [308, "CASH", 8, 8], 268 | "GETQASH": [309, "CASH", 8, 8], 269 | "LIKEBTC": [310, "CASH", 8, 8], 270 | "LIKEETH": [311, "CASH", 8, 8], 271 | "LIKEQASH": [312, "CASH", 8, 8], 272 | "PWVQASH": [313, "CASH", 8, 8], 273 | "PWVBTC": [314, "CASH", 8, 8], 274 | "PWVETH": [315, "CASH", 8, 8], 275 | "VIOBTC": [316, "CASH", 8, 8], 276 | "VIOQASH": [317, "CASH", 8, 8], 277 | "VIOETH": [318, "CASH", 8, 8], 278 | "CRPTBTC": [321, "CASH", 8, 8], 279 | "CRPTETH": [322, "CASH", 8, 8], 280 | "CRPTQASH": [323, "CASH", 8, 8], 281 | "ELYBTC": [325, "CASH", 8, 8], 282 | "ELYQASH": [326, "CASH", 8, 8], 283 | "ELYETH": [327, "CASH", 8, 8], 284 | "KRLBTC": [346, "CASH", 8, 8], 285 | "KRLETH": [347, "CASH", 8, 8], 286 | "KRLQASH": [348, "CASH", 8, 8], 287 | "SHPBTC": [349, "CASH", 8, 8], 288 | "SHPETH": [350, "CASH", 8, 8], 289 | "SHPQASH": [351, "CASH", 8, 8], 290 | "LNDBTC": [352, "CASH", 8, 8], 291 | "LNDETH": [353, "CASH", 8, 8], 292 | "LNDQASH": [354, "CASH", 8, 8], 293 | "MRKBTC": [358, "CASH", 8, 8], 294 | "MRKETH": [359, "CASH", 8, 8], 295 | "MRKQASH": [360, "CASH", 8, 8], 296 | "BRCBTC": [361, "CASH", 8, 8], 297 | "BRCETH": [362, "CASH", 8, 8], 298 | "BRCBCH": [363, "CASH", 8, 8], 299 | "BRCQASH": [364, "CASH", 8, 8], 300 | "FLPBTC": [365, "CASH", 8, 8], 301 | "FLPETH": [366, "CASH", 8, 8], 302 | "FLPQASH": [367, "CASH", 8, 8], 303 | "DACSBTC": [380, "CASH", 8, 8], 304 | "DACSETH": [381, "CASH", 8, 8], 305 | "DACSQASH": [382, "CASH", 8, 8], 306 | "ZPRBTC": [383, "CASH", 8, 8], 307 | "ZPRETH": [384, "CASH", 8, 8], 308 | "ZPRQASH": [385, "CASH", 8, 8], 309 | "UBTBTC": [386, "CASH", 8, 8], 310 | "UBTETH": [387, "CASH", 8, 8], 311 | "UBTQASH": [388, "CASH", 8, 8], 312 | "FTTBTC": [389, "CASH", 8, 8], 313 | "FTTETH": [390, "CASH", 8, 8], 314 | "FTTQASH": [391, "CASH", 8, 8] 315 | }; 316 | 317 | class Product { 318 | 319 | constructor(name, id, code, pair, price_fmt, volume_fmt, change_p_fmt) { 320 | this.name = name; 321 | this.id = id; 322 | this.code = code.toLowerCase(); 323 | this.pair = pair.toLowerCase(); 324 | this.price_formatter = price_fmt || fixed_formatter(0); 325 | this.volume_formatter = volume_fmt || fixed_formatter(8); 326 | this.percent_formatter = change_p_fmt || percent_formatter(2); 327 | } 328 | 329 | format_price(n) { 330 | return this.price_formatter(n); 331 | } 332 | 333 | format_volume(n) { 334 | return this.volume_formatter(n); 335 | } 336 | 337 | format_change_p(n) { 338 | return this.percent_formatter(n); 339 | } 340 | 341 | get_ticker_channel() { 342 | return `product_${this.code}_${this.pair}_${this.id}`; 343 | } 344 | 345 | get_executions_channel() { 346 | return `executions_${this.code}_${this.pair}`; 347 | } 348 | 349 | get_ladders_buy_channel() { 350 | return `price_ladders_${this.code}_${this.pair}_buy`; 351 | } 352 | 353 | get_ladders_sell_channel() { 354 | return `price_ladders_${this.code}_${this.pair}_sell`; 355 | } 356 | } 357 | 358 | class InvalidProductCodeError { 359 | 360 | constructor(product_code) { 361 | this.name = 'InvalidProductCodeError'; 362 | this.product_code = product_code; 363 | this.message = `"${product_code}" isn't supported.`; 364 | } 365 | } 366 | 367 | 368 | const fixed_formatter = (digit) => { 369 | return (n) => n.toFixed(digit); 370 | }; 371 | 372 | const percent_formatter = (digit) => { 373 | return (n) => `${n >= 0.0 ? "+" : ""}${(n*100.0).toFixed(digit)}%`; 374 | }; 375 | 376 | const find_pair = (code) => { 377 | if (code in PAIRS) return PAIRS[code]; 378 | 379 | const product = new api.PublicAPI() 380 | .callSync("GET", "/products") 381 | .find(row => row.currency_pair_code == code); 382 | if (product) { 383 | return [product.id, code, 4, 8]; 384 | } else { 385 | return null; 386 | } 387 | }; 388 | 389 | const get_product = (code) => { 390 | code = code.toUpperCase(); 391 | const pair = find_pair(code); 392 | if (pair) { 393 | return new Product( 394 | "Liquid " + code, pair[0], pair[1], code, 395 | fixed_formatter(pair[2]), 396 | fixed_formatter(pair[3]) 397 | ); 398 | } 399 | throw new InvalidProductCodeError(code); 400 | }; 401 | 402 | module.exports.get_product = get_product; 403 | module.exports.InvalidProductCodeError = InvalidProductCodeError; 404 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@types/concat-stream@^1.6.0": 6 | version "1.6.0" 7 | resolved "https://registry.yarnpkg.com/@types/concat-stream/-/concat-stream-1.6.0.tgz#394dbe0bb5fee46b38d896735e8b68ef2390d00d" 8 | dependencies: 9 | "@types/node" "*" 10 | 11 | "@types/form-data@0.0.33": 12 | version "0.0.33" 13 | resolved "https://registry.yarnpkg.com/@types/form-data/-/form-data-0.0.33.tgz#c9ac85b2a5fd18435b8c85d9ecb50e6d6c893ff8" 14 | dependencies: 15 | "@types/node" "*" 16 | 17 | "@types/node@*", "@types/node@^8.0.0": 18 | version "8.0.47" 19 | resolved "https://registry.yarnpkg.com/@types/node/-/node-8.0.47.tgz#968e596f91acd59069054558a00708c445ca30c2" 20 | 21 | "@types/node@^7.0.31": 22 | version "7.0.46" 23 | resolved "https://registry.yarnpkg.com/@types/node/-/node-7.0.46.tgz#c3dedd25558c676b3d6303e51799abb9c3f8f314" 24 | 25 | "@types/qs@^6.2.31": 26 | version "6.5.1" 27 | resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.5.1.tgz#a38f69c62528d56ba7bd1f91335a8004988d72f7" 28 | 29 | asap@~2.0.3: 30 | version "2.0.6" 31 | resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" 32 | 33 | asynckit@^0.4.0: 34 | version "0.4.0" 35 | resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" 36 | 37 | balanced-match@^1.0.0: 38 | version "1.0.0" 39 | resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" 40 | 41 | base64url@2.0.0, base64url@^2.0.0: 42 | version "2.0.0" 43 | resolved "https://registry.yarnpkg.com/base64url/-/base64url-2.0.0.tgz#eac16e03ea1438eff9423d69baa36262ed1f70bb" 44 | 45 | brace-expansion@^1.1.7: 46 | version "1.1.8" 47 | resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.8.tgz#c07b211c7c952ec1f8efd51a77ef0d1d3990a292" 48 | dependencies: 49 | balanced-match "^1.0.0" 50 | concat-map "0.0.1" 51 | 52 | browser-stdout@1.3.0: 53 | version "1.3.0" 54 | resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.0.tgz#f351d32969d32fa5d7a5567154263d928ae3bd1f" 55 | 56 | buffer-equal-constant-time@1.0.1: 57 | version "1.0.1" 58 | resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" 59 | 60 | caseless@~0.11.0: 61 | version "0.11.0" 62 | resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.11.0.tgz#715b96ea9841593cc33067923f5ec60ebda4f7d7" 63 | 64 | caseless@~0.12.0: 65 | version "0.12.0" 66 | resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" 67 | 68 | combined-stream@^1.0.5: 69 | version "1.0.5" 70 | resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.5.tgz#938370a57b4a51dea2c77c15d5c5fdf895164009" 71 | dependencies: 72 | delayed-stream "~1.0.0" 73 | 74 | command-exists@^1.2.2: 75 | version "1.2.2" 76 | resolved "https://registry.yarnpkg.com/command-exists/-/command-exists-1.2.2.tgz#12819c64faf95446ec0ae07fe6cafb6eb3708b22" 77 | 78 | commander@2.11.0, commander@^2.11.0: 79 | version "2.11.0" 80 | resolved "https://registry.yarnpkg.com/commander/-/commander-2.11.0.tgz#157152fd1e7a6c8d98a5b715cf376df928004563" 81 | 82 | concat-map@0.0.1: 83 | version "0.0.1" 84 | resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" 85 | 86 | concat-stream@^1.4.6, concat-stream@^1.4.7, concat-stream@^1.6.0: 87 | version "1.6.0" 88 | resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.0.tgz#0aac662fd52be78964d5532f694784e70110acf7" 89 | dependencies: 90 | inherits "^2.0.3" 91 | readable-stream "^2.2.2" 92 | typedarray "^0.0.6" 93 | 94 | core-util-is@~1.0.0: 95 | version "1.0.2" 96 | resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" 97 | 98 | debug@3.1.0: 99 | version "3.1.0" 100 | resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" 101 | dependencies: 102 | ms "2.0.0" 103 | 104 | delayed-stream@~1.0.0: 105 | version "1.0.0" 106 | resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" 107 | 108 | diff@3.3.1: 109 | version "3.3.1" 110 | resolved "https://registry.yarnpkg.com/diff/-/diff-3.3.1.tgz#aa8567a6eed03c531fc89d3f711cd0e5259dec75" 111 | 112 | ecdsa-sig-formatter@1.0.9: 113 | version "1.0.9" 114 | resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.9.tgz#4bc926274ec3b5abb5016e7e1d60921ac262b2a1" 115 | dependencies: 116 | base64url "^2.0.0" 117 | safe-buffer "^5.0.1" 118 | 119 | escape-string-regexp@1.0.5: 120 | version "1.0.5" 121 | resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" 122 | 123 | faye-websocket@0.9.4: 124 | version "0.9.4" 125 | resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.9.4.tgz#885934c79effb0409549e0c0a3801ed17a40cdad" 126 | dependencies: 127 | websocket-driver ">=0.5.1" 128 | 129 | form-data@^2.2.0: 130 | version "2.3.1" 131 | resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.1.tgz#6fb94fbd71885306d73d15cc497fe4cc4ecd44bf" 132 | dependencies: 133 | asynckit "^0.4.0" 134 | combined-stream "^1.0.5" 135 | mime-types "^2.1.12" 136 | 137 | fs.realpath@^1.0.0: 138 | version "1.0.0" 139 | resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" 140 | 141 | get-port@^3.1.0: 142 | version "3.2.0" 143 | resolved "https://registry.yarnpkg.com/get-port/-/get-port-3.2.0.tgz#dd7ce7de187c06c8bf353796ac71e099f0980ebc" 144 | 145 | glob@7.1.2: 146 | version "7.1.2" 147 | resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" 148 | dependencies: 149 | fs.realpath "^1.0.0" 150 | inflight "^1.0.4" 151 | inherits "2" 152 | minimatch "^3.0.4" 153 | once "^1.3.0" 154 | path-is-absolute "^1.0.0" 155 | 156 | growl@1.10.3: 157 | version "1.10.3" 158 | resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.3.tgz#1926ba90cf3edfe2adb4927f5880bc22c66c790f" 159 | 160 | has-flag@^2.0.0: 161 | version "2.0.0" 162 | resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-2.0.0.tgz#e8207af1cc7b30d446cc70b734b5e8be18f88d51" 163 | 164 | he@1.1.1: 165 | version "1.1.1" 166 | resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd" 167 | 168 | http-basic@^2.5.1: 169 | version "2.5.1" 170 | resolved "https://registry.yarnpkg.com/http-basic/-/http-basic-2.5.1.tgz#8ce447bdb5b6c577f8a63e3fa78056ec4bb4dbfb" 171 | dependencies: 172 | caseless "~0.11.0" 173 | concat-stream "^1.4.6" 174 | http-response-object "^1.0.0" 175 | 176 | http-basic@^5.0.3: 177 | version "5.0.3" 178 | resolved "https://registry.yarnpkg.com/http-basic/-/http-basic-5.0.3.tgz#b7a0008f6cac7ef404b807cdd599dfd939802418" 179 | dependencies: 180 | "@types/concat-stream" "^1.6.0" 181 | "@types/node" "^7.0.31" 182 | caseless "~0.11.0" 183 | concat-stream "^1.4.6" 184 | http-response-object "^2.0.3" 185 | parse-cache-control "^1.0.1" 186 | 187 | http-parser-js@>=0.4.0: 188 | version "0.4.9" 189 | resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.4.9.tgz#ea1a04fb64adff0242e9974f297dd4c3cad271e1" 190 | 191 | http-response-object@^1.0.0, http-response-object@^1.1.0: 192 | version "1.1.0" 193 | resolved "https://registry.yarnpkg.com/http-response-object/-/http-response-object-1.1.0.tgz#a7c4e75aae82f3bb4904e4f43f615673b4d518c3" 194 | 195 | http-response-object@^2.0.3: 196 | version "2.0.3" 197 | resolved "https://registry.yarnpkg.com/http-response-object/-/http-response-object-2.0.3.tgz#530a48c3b6b683be2398fc42417f7c54f8b401a8" 198 | dependencies: 199 | "@types/node" "^7.0.31" 200 | 201 | inflight@^1.0.4: 202 | version "1.0.6" 203 | resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" 204 | dependencies: 205 | once "^1.3.0" 206 | wrappy "1" 207 | 208 | inherits@2, inherits@^2.0.3, inherits@~2.0.3: 209 | version "2.0.3" 210 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" 211 | 212 | isarray@~1.0.0: 213 | version "1.0.0" 214 | resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" 215 | 216 | jsonschema@^1.2.0: 217 | version "1.2.0" 218 | resolved "https://registry.yarnpkg.com/jsonschema/-/jsonschema-1.2.0.tgz#d6ebaf70798db7b3a20c544f6c9ef9319b077de2" 219 | 220 | jsonwebtoken@^8.1.0: 221 | version "8.1.0" 222 | resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.1.0.tgz#c6397cd2e5fd583d65c007a83dc7bb78e6982b83" 223 | dependencies: 224 | jws "^3.1.4" 225 | lodash.includes "^4.3.0" 226 | lodash.isboolean "^3.0.3" 227 | lodash.isinteger "^4.0.4" 228 | lodash.isnumber "^3.0.3" 229 | lodash.isplainobject "^4.0.6" 230 | lodash.isstring "^4.0.1" 231 | lodash.once "^4.0.0" 232 | ms "^2.0.0" 233 | xtend "^4.0.1" 234 | 235 | jwa@^1.1.4: 236 | version "1.1.5" 237 | resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.1.5.tgz#a0552ce0220742cd52e153774a32905c30e756e5" 238 | dependencies: 239 | base64url "2.0.0" 240 | buffer-equal-constant-time "1.0.1" 241 | ecdsa-sig-formatter "1.0.9" 242 | safe-buffer "^5.0.1" 243 | 244 | jws@^3.1.4: 245 | version "3.1.4" 246 | resolved "https://registry.yarnpkg.com/jws/-/jws-3.1.4.tgz#f9e8b9338e8a847277d6444b1464f61880e050a2" 247 | dependencies: 248 | base64url "^2.0.0" 249 | jwa "^1.1.4" 250 | safe-buffer "^5.0.1" 251 | 252 | lodash.includes@^4.3.0: 253 | version "4.3.0" 254 | resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" 255 | 256 | lodash.isboolean@^3.0.3: 257 | version "3.0.3" 258 | resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" 259 | 260 | lodash.isinteger@^4.0.4: 261 | version "4.0.4" 262 | resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343" 263 | 264 | lodash.isnumber@^3.0.3: 265 | version "3.0.3" 266 | resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc" 267 | 268 | lodash.isplainobject@^4.0.6: 269 | version "4.0.6" 270 | resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" 271 | 272 | lodash.isstring@^4.0.1: 273 | version "4.0.1" 274 | resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" 275 | 276 | lodash.once@^4.0.0: 277 | version "4.1.1" 278 | resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" 279 | 280 | lodash.throttle@^4.1.1: 281 | version "4.1.1" 282 | resolved "https://registry.yarnpkg.com/lodash.throttle/-/lodash.throttle-4.1.1.tgz#c23e91b710242ac70c37f1e1cda9274cc39bf2f4" 283 | 284 | mime-db@~1.30.0: 285 | version "1.30.0" 286 | resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.30.0.tgz#74c643da2dd9d6a45399963465b26d5ca7d71f01" 287 | 288 | mime-types@^2.1.12: 289 | version "2.1.17" 290 | resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.17.tgz#09d7a393f03e995a79f8af857b70a9e0ab16557a" 291 | dependencies: 292 | mime-db "~1.30.0" 293 | 294 | minimatch@^3.0.4: 295 | version "3.0.4" 296 | resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" 297 | dependencies: 298 | brace-expansion "^1.1.7" 299 | 300 | minimist@0.0.8: 301 | version "0.0.8" 302 | resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" 303 | 304 | mkdirp@0.5.1: 305 | version "0.5.1" 306 | resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" 307 | dependencies: 308 | minimist "0.0.8" 309 | 310 | mocha@^4.0.1: 311 | version "4.0.1" 312 | resolved "https://registry.yarnpkg.com/mocha/-/mocha-4.0.1.tgz#0aee5a95cf69a4618820f5e51fa31717117daf1b" 313 | dependencies: 314 | browser-stdout "1.3.0" 315 | commander "2.11.0" 316 | debug "3.1.0" 317 | diff "3.3.1" 318 | escape-string-regexp "1.0.5" 319 | glob "7.1.2" 320 | growl "1.10.3" 321 | he "1.1.1" 322 | mkdirp "0.5.1" 323 | supports-color "4.4.0" 324 | 325 | ms@2.0.0, ms@^2.0.0: 326 | version "2.0.0" 327 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" 328 | 329 | once@^1.3.0: 330 | version "1.4.0" 331 | resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" 332 | dependencies: 333 | wrappy "1" 334 | 335 | parse-cache-control@^1.0.1: 336 | version "1.0.1" 337 | resolved "https://registry.yarnpkg.com/parse-cache-control/-/parse-cache-control-1.0.1.tgz#8eeab3e54fa56920fe16ba38f77fa21aacc2d74e" 338 | 339 | path-is-absolute@^1.0.0: 340 | version "1.0.1" 341 | resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" 342 | 343 | process-nextick-args@~1.0.6: 344 | version "1.0.7" 345 | resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3" 346 | 347 | promise@^7.1.1: 348 | version "7.3.1" 349 | resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf" 350 | dependencies: 351 | asap "~2.0.3" 352 | 353 | promise@^8.0.0: 354 | version "8.0.1" 355 | resolved "https://registry.yarnpkg.com/promise/-/promise-8.0.1.tgz#e45d68b00a17647b6da711bf85ed6ed47208f450" 356 | dependencies: 357 | asap "~2.0.3" 358 | 359 | pusher-js@^4.2.1: 360 | version "4.2.1" 361 | resolved "https://registry.yarnpkg.com/pusher-js/-/pusher-js-4.2.1.tgz#7c10e2f981b8ed4390c1cbed448eb5c0967aaa22" 362 | dependencies: 363 | faye-websocket "0.9.4" 364 | xmlhttprequest "^1.8.0" 365 | 366 | qs@^6.1.0, qs@^6.4.0: 367 | version "6.5.1" 368 | resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8" 369 | 370 | readable-stream@^2.2.2: 371 | version "2.3.3" 372 | resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.3.tgz#368f2512d79f9d46fdfc71349ae7878bbc1eb95c" 373 | dependencies: 374 | core-util-is "~1.0.0" 375 | inherits "~2.0.3" 376 | isarray "~1.0.0" 377 | process-nextick-args "~1.0.6" 378 | safe-buffer "~5.1.1" 379 | string_decoder "~1.0.3" 380 | util-deprecate "~1.0.1" 381 | 382 | safe-buffer@^5.0.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1: 383 | version "5.1.1" 384 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" 385 | 386 | string_decoder@~1.0.3: 387 | version "1.0.3" 388 | resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.0.3.tgz#0fc67d7c141825de94282dd536bec6b9bce860ab" 389 | dependencies: 390 | safe-buffer "~5.1.0" 391 | 392 | supports-color@4.4.0: 393 | version "4.4.0" 394 | resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.4.0.tgz#883f7ddabc165142b2a61427f3352ded195d1a3e" 395 | dependencies: 396 | has-flag "^2.0.0" 397 | 398 | sync-request@^4.1.0: 399 | version "4.1.0" 400 | resolved "https://registry.yarnpkg.com/sync-request/-/sync-request-4.1.0.tgz#324b4e506fb994d2afd2a0021a455f800725f07a" 401 | dependencies: 402 | command-exists "^1.2.2" 403 | concat-stream "^1.6.0" 404 | get-port "^3.1.0" 405 | http-response-object "^1.1.0" 406 | then-request "^2.2.0" 407 | 408 | then-request@^2.2.0: 409 | version "2.2.0" 410 | resolved "https://registry.yarnpkg.com/then-request/-/then-request-2.2.0.tgz#6678b32fa0ca218fe569981bbd8871b594060d81" 411 | dependencies: 412 | caseless "~0.11.0" 413 | concat-stream "^1.4.7" 414 | http-basic "^2.5.1" 415 | http-response-object "^1.1.0" 416 | promise "^7.1.1" 417 | qs "^6.1.0" 418 | 419 | then-request@^4.1.0: 420 | version "4.1.0" 421 | resolved "https://registry.yarnpkg.com/then-request/-/then-request-4.1.0.tgz#a2787b4a1fd94aedc6f8d4716850fd2b5b5132a0" 422 | dependencies: 423 | "@types/concat-stream" "^1.6.0" 424 | "@types/form-data" "0.0.33" 425 | "@types/node" "^8.0.0" 426 | "@types/qs" "^6.2.31" 427 | caseless "~0.12.0" 428 | concat-stream "^1.6.0" 429 | form-data "^2.2.0" 430 | http-basic "^5.0.3" 431 | http-response-object "^2.0.3" 432 | promise "^8.0.0" 433 | qs "^6.4.0" 434 | 435 | typedarray@^0.0.6: 436 | version "0.0.6" 437 | resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" 438 | 439 | util-deprecate@~1.0.1: 440 | version "1.0.2" 441 | resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" 442 | 443 | websocket-driver@>=0.5.1: 444 | version "0.7.0" 445 | resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.0.tgz#0caf9d2d755d93aee049d4bdd0d3fe2cca2a24eb" 446 | dependencies: 447 | http-parser-js ">=0.4.0" 448 | websocket-extensions ">=0.1.1" 449 | 450 | websocket-extensions@>=0.1.1: 451 | version "0.1.2" 452 | resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.2.tgz#0e18781de629a18308ce1481650f67ffa2693a5d" 453 | 454 | wrappy@1: 455 | version "1.0.2" 456 | resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" 457 | 458 | xmlhttprequest@^1.8.0: 459 | version "1.8.0" 460 | resolved "https://registry.yarnpkg.com/xmlhttprequest/-/xmlhttprequest-1.8.0.tgz#67fe075c5c24fef39f9d65f5f7b7fe75171968fc" 461 | 462 | xtend@^4.0.1: 463 | version "4.0.1" 464 | resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" 465 | --------------------------------------------------------------------------------