From 94d836feaf3d88c48c309ba3c4909a2b52064a43 Mon Sep 17 00:00:00 2001 From: eigilb Date: Tue, 6 Jun 2017 00:43:12 +0200 Subject: [PATCH 01/27] Add files via upload --- extensions/exchanges.md | 234 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 234 insertions(+) create mode 100644 extensions/exchanges.md diff --git a/extensions/exchanges.md b/extensions/exchanges.md new file mode 100644 index 0000000000..9c7ad718eb --- /dev/null +++ b/extensions/exchanges.md @@ -0,0 +1,234 @@ +Zenbot exchange API +----------------------------- +This document is written to help developers implement new extensions for Zenbot. + +It is reverse engineered from inspecting the Zenbot files and the GDAX extension and is not a definitive guide for developing an extension. + +Any contribution that makes this document better is certainly welcome. + +The document is an attempt to describe the interface functions used for communication with an exchange and a few helper functions. Each function has a set of calling parameters and return values and statuses + +The input parameters are packed in the "opts" object, and the results from invoking the function are returned in an object. + +Error handling +------------------- + +**Non recoverable errors** should be handled by the actual extension function. A typical error is "Page not found", which most likely is caused by a malformed URL. Such errors should return a descriptive message and force a program exit. + +**Recoverable errors** affecting trades should be handled by zenbot, while others could be handled in the extension layer. This needs to be clarified. + +Some named errors are already handled by the main program (see getTrades below). These are: +``` + 'ETIMEDOUT', // possibly recoverable + 'ENOTFOUND', // not recoverable (404?) + 'ECONNRESET' // possibly recoverable +``` +Zenbot may have some GDAX-specific code. In particular that pertains to return values from exchange functions. Return values in general should be handled in a exchange agnostic and standardized way to make it easiest possible to write extensions. + +Some variables in the "exchange" object are worth mentioning +----------------------------------------------------------------------------------- +``` + name: 'some_exchange_name' + historyScan: 'forward', 'backward' or false + makerFee: fee_from_exchange (numeric) + backfillRateLimit: some_value_fitting_exchange_policy or 0 +``` +The functions +------------------ + +```javascript +funcion publicClient () +``` +Function for connecting to the exchange for public requests +Called from: +- extension/*/exchange.js + +Returns a "client" object for use in exchange public access functions. + +```javascript +function authedClient () +``` +Function for connecting and authenticating private requests +Called from: +- extension/*/exchange.js + +The function gets parameters from conf.js in the c object +In particular these are: +``` + c..key + c..secret +``` +For specific exchanges also: +``` + c.bitstamp.client_id + c.gdax.passphrase +``` +The functionm returns a "client" object for use in exchange authenticated access functions + +```javascript +function statusErr (resp, body) +``` +Helper function for returning conformant error messages +Called from: +- extension/*/exchange.js + +```javascript +getTrades: function (opts, cb) +``` +Public function for getting history and trade data from the exchange +Called from: +- https://github.com/carlos8f/zenbot/blob/master/commands/backfill.js +- https://github.com/carlos8f/zenbot/blob/master/commands/trade.js + +Input: +``` + opts.product_id + opts.from + opts.to +``` +Return: +``` + trades.length + (array of?) { + trade_id: some_id + time: 'transaction_time', + size: trade_size, + price: trade_prize, + side : 'buy' or 'sell' + } +``` +Expected error codes if error: +``` + err.code + + 'ETIMEDOUT', // possibly recoverable + 'ENOTFOUND', // not recoverable + 'ECONNRESET' // possibly recoverable +``` +```javascript +getBalance: function (opts, cb) +``` +Function for getting wallet getBalances from the exchange +Called from: +- https://github.com/carlos8f/zenbot/blob/master/lib/engine.js + +Input: +``` + opts.currency + opts.asset +``` +Return: +``` + balance.asset + balance.asset_hold + balance.currency + balance.currency_hold +``` +Comment: +Asset vs asset_hold and currency vs currency_hold is kind of mysterious to me. +For most exchanges I would just return something similar to available_asset and available_currency +For exchanges that returns some other values, I would do the calculation on the extension layer +and not leave it to engine.js, because available_asset and available_currency are only interesting +values from a buy/sell view, IMHO. If someone knows better, please clarify + +```javascript +getQuote: function (opts, cb) +``` +Public function for getting ticker data from the exchange +Called from: +- https://github.com/carlos8f/zenbot/blob/master/lib/engine.js +- https://github.com/carlos8f/zenbot/blob/master/commands/buyjs +- https://github.com/carlos8f/zenbot/blob/master/commands/sell.js + +Input: +``` + opts.product_id +``` +Return: +``` + {bid: value_of_bid, ask: value_of_ask} +``` +```javascript +cancelOrder: function (opts, cb) +``` +Obviously a function for canceling an placed order +Called from: +- https://github.com/carlos8f/zenbot/blob/master/lib/engine.js + +Input: +``` + opts.order_id +``` +```javascript +buy: function (opts, cb) +``` +The function for buying +Called from: +- https://github.com/carlos8f/zenbot/blob/master/lib/engine.js + +Input: +``` + opts.price + opts.size +``` +Returns: +``` + +``` + +```javascript +sell: function (opts, cb) +``` + +The function for selling +Called from: +- https://github.com/carlos8f/zenbot/blob/master/lib/engine.js + +Input: +``` + opts.price + opts.size +``` +Returns: +``` + +``` + +```javascript +getOrder: function (opts, cb) +``` + +Function to get data from a placed order +Called from: +- https://github.com/carlos8f/zenbot/blob/master/lib/engine.js + +Input: +``` + opts.order_id + opts.product_id +``` +Returns: +``` + order.status +``` +Expected values: 'done', 'rejected' + If 'rejected' order.reject_reason = some_reason ('post only') +Is '*post only*' spesific for GDAX? +Comment: Needs some clarifying + +```javascript +getCursor: function (trade) +``` +Function to get details from an executed trade +Called from: +- https://github.com/carlos8f/zenbot/blob/master/commands/backfill.js +- https://github.com/carlos8f/zenbot/blob/master/commands/trade.js + +Input: +``` + trade.trade_id +``` +Return: +``` + trade.trade_id +``` From 87613b02a9bc3e94033ebc8671f05e3d588ecdda Mon Sep 17 00:00:00 2001 From: eigilb Date: Tue, 6 Jun 2017 01:07:33 +0200 Subject: [PATCH 02/27] Delete exchanges.md --- extensions/exchanges.md | 234 ---------------------------------------- 1 file changed, 234 deletions(-) delete mode 100644 extensions/exchanges.md diff --git a/extensions/exchanges.md b/extensions/exchanges.md deleted file mode 100644 index 9c7ad718eb..0000000000 --- a/extensions/exchanges.md +++ /dev/null @@ -1,234 +0,0 @@ -Zenbot exchange API ------------------------------ -This document is written to help developers implement new extensions for Zenbot. - -It is reverse engineered from inspecting the Zenbot files and the GDAX extension and is not a definitive guide for developing an extension. - -Any contribution that makes this document better is certainly welcome. - -The document is an attempt to describe the interface functions used for communication with an exchange and a few helper functions. Each function has a set of calling parameters and return values and statuses - -The input parameters are packed in the "opts" object, and the results from invoking the function are returned in an object. - -Error handling -------------------- - -**Non recoverable errors** should be handled by the actual extension function. A typical error is "Page not found", which most likely is caused by a malformed URL. Such errors should return a descriptive message and force a program exit. - -**Recoverable errors** affecting trades should be handled by zenbot, while others could be handled in the extension layer. This needs to be clarified. - -Some named errors are already handled by the main program (see getTrades below). These are: -``` - 'ETIMEDOUT', // possibly recoverable - 'ENOTFOUND', // not recoverable (404?) - 'ECONNRESET' // possibly recoverable -``` -Zenbot may have some GDAX-specific code. In particular that pertains to return values from exchange functions. Return values in general should be handled in a exchange agnostic and standardized way to make it easiest possible to write extensions. - -Some variables in the "exchange" object are worth mentioning ------------------------------------------------------------------------------------ -``` - name: 'some_exchange_name' - historyScan: 'forward', 'backward' or false - makerFee: fee_from_exchange (numeric) - backfillRateLimit: some_value_fitting_exchange_policy or 0 -``` -The functions ------------------- - -```javascript -funcion publicClient () -``` -Function for connecting to the exchange for public requests -Called from: -- extension/*/exchange.js - -Returns a "client" object for use in exchange public access functions. - -```javascript -function authedClient () -``` -Function for connecting and authenticating private requests -Called from: -- extension/*/exchange.js - -The function gets parameters from conf.js in the c object -In particular these are: -``` - c..key - c..secret -``` -For specific exchanges also: -``` - c.bitstamp.client_id - c.gdax.passphrase -``` -The functionm returns a "client" object for use in exchange authenticated access functions - -```javascript -function statusErr (resp, body) -``` -Helper function for returning conformant error messages -Called from: -- extension/*/exchange.js - -```javascript -getTrades: function (opts, cb) -``` -Public function for getting history and trade data from the exchange -Called from: -- https://github.com/carlos8f/zenbot/blob/master/commands/backfill.js -- https://github.com/carlos8f/zenbot/blob/master/commands/trade.js - -Input: -``` - opts.product_id - opts.from - opts.to -``` -Return: -``` - trades.length - (array of?) { - trade_id: some_id - time: 'transaction_time', - size: trade_size, - price: trade_prize, - side : 'buy' or 'sell' - } -``` -Expected error codes if error: -``` - err.code - - 'ETIMEDOUT', // possibly recoverable - 'ENOTFOUND', // not recoverable - 'ECONNRESET' // possibly recoverable -``` -```javascript -getBalance: function (opts, cb) -``` -Function for getting wallet getBalances from the exchange -Called from: -- https://github.com/carlos8f/zenbot/blob/master/lib/engine.js - -Input: -``` - opts.currency - opts.asset -``` -Return: -``` - balance.asset - balance.asset_hold - balance.currency - balance.currency_hold -``` -Comment: -Asset vs asset_hold and currency vs currency_hold is kind of mysterious to me. -For most exchanges I would just return something similar to available_asset and available_currency -For exchanges that returns some other values, I would do the calculation on the extension layer -and not leave it to engine.js, because available_asset and available_currency are only interesting -values from a buy/sell view, IMHO. If someone knows better, please clarify - -```javascript -getQuote: function (opts, cb) -``` -Public function for getting ticker data from the exchange -Called from: -- https://github.com/carlos8f/zenbot/blob/master/lib/engine.js -- https://github.com/carlos8f/zenbot/blob/master/commands/buyjs -- https://github.com/carlos8f/zenbot/blob/master/commands/sell.js - -Input: -``` - opts.product_id -``` -Return: -``` - {bid: value_of_bid, ask: value_of_ask} -``` -```javascript -cancelOrder: function (opts, cb) -``` -Obviously a function for canceling an placed order -Called from: -- https://github.com/carlos8f/zenbot/blob/master/lib/engine.js - -Input: -``` - opts.order_id -``` -```javascript -buy: function (opts, cb) -``` -The function for buying -Called from: -- https://github.com/carlos8f/zenbot/blob/master/lib/engine.js - -Input: -``` - opts.price - opts.size -``` -Returns: -``` - -``` - -```javascript -sell: function (opts, cb) -``` - -The function for selling -Called from: -- https://github.com/carlos8f/zenbot/blob/master/lib/engine.js - -Input: -``` - opts.price - opts.size -``` -Returns: -``` - -``` - -```javascript -getOrder: function (opts, cb) -``` - -Function to get data from a placed order -Called from: -- https://github.com/carlos8f/zenbot/blob/master/lib/engine.js - -Input: -``` - opts.order_id - opts.product_id -``` -Returns: -``` - order.status -``` -Expected values: 'done', 'rejected' - If 'rejected' order.reject_reason = some_reason ('post only') -Is '*post only*' spesific for GDAX? -Comment: Needs some clarifying - -```javascript -getCursor: function (trade) -``` -Function to get details from an executed trade -Called from: -- https://github.com/carlos8f/zenbot/blob/master/commands/backfill.js -- https://github.com/carlos8f/zenbot/blob/master/commands/trade.js - -Input: -``` - trade.trade_id -``` -Return: -``` - trade.trade_id -``` From ef20364c25928be9de01d7990ca2d55fe8f0f497 Mon Sep 17 00:00:00 2001 From: eigilb Date: Tue, 6 Jun 2017 01:10:11 +0200 Subject: [PATCH 03/27] Create exchanges.md --- docs/exchanges.md | 234 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 234 insertions(+) create mode 100644 docs/exchanges.md diff --git a/docs/exchanges.md b/docs/exchanges.md new file mode 100644 index 0000000000..9c7ad718eb --- /dev/null +++ b/docs/exchanges.md @@ -0,0 +1,234 @@ +Zenbot exchange API +----------------------------- +This document is written to help developers implement new extensions for Zenbot. + +It is reverse engineered from inspecting the Zenbot files and the GDAX extension and is not a definitive guide for developing an extension. + +Any contribution that makes this document better is certainly welcome. + +The document is an attempt to describe the interface functions used for communication with an exchange and a few helper functions. Each function has a set of calling parameters and return values and statuses + +The input parameters are packed in the "opts" object, and the results from invoking the function are returned in an object. + +Error handling +------------------- + +**Non recoverable errors** should be handled by the actual extension function. A typical error is "Page not found", which most likely is caused by a malformed URL. Such errors should return a descriptive message and force a program exit. + +**Recoverable errors** affecting trades should be handled by zenbot, while others could be handled in the extension layer. This needs to be clarified. + +Some named errors are already handled by the main program (see getTrades below). These are: +``` + 'ETIMEDOUT', // possibly recoverable + 'ENOTFOUND', // not recoverable (404?) + 'ECONNRESET' // possibly recoverable +``` +Zenbot may have some GDAX-specific code. In particular that pertains to return values from exchange functions. Return values in general should be handled in a exchange agnostic and standardized way to make it easiest possible to write extensions. + +Some variables in the "exchange" object are worth mentioning +----------------------------------------------------------------------------------- +``` + name: 'some_exchange_name' + historyScan: 'forward', 'backward' or false + makerFee: fee_from_exchange (numeric) + backfillRateLimit: some_value_fitting_exchange_policy or 0 +``` +The functions +------------------ + +```javascript +funcion publicClient () +``` +Function for connecting to the exchange for public requests +Called from: +- extension/*/exchange.js + +Returns a "client" object for use in exchange public access functions. + +```javascript +function authedClient () +``` +Function for connecting and authenticating private requests +Called from: +- extension/*/exchange.js + +The function gets parameters from conf.js in the c object +In particular these are: +``` + c..key + c..secret +``` +For specific exchanges also: +``` + c.bitstamp.client_id + c.gdax.passphrase +``` +The functionm returns a "client" object for use in exchange authenticated access functions + +```javascript +function statusErr (resp, body) +``` +Helper function for returning conformant error messages +Called from: +- extension/*/exchange.js + +```javascript +getTrades: function (opts, cb) +``` +Public function for getting history and trade data from the exchange +Called from: +- https://github.com/carlos8f/zenbot/blob/master/commands/backfill.js +- https://github.com/carlos8f/zenbot/blob/master/commands/trade.js + +Input: +``` + opts.product_id + opts.from + opts.to +``` +Return: +``` + trades.length + (array of?) { + trade_id: some_id + time: 'transaction_time', + size: trade_size, + price: trade_prize, + side : 'buy' or 'sell' + } +``` +Expected error codes if error: +``` + err.code + + 'ETIMEDOUT', // possibly recoverable + 'ENOTFOUND', // not recoverable + 'ECONNRESET' // possibly recoverable +``` +```javascript +getBalance: function (opts, cb) +``` +Function for getting wallet getBalances from the exchange +Called from: +- https://github.com/carlos8f/zenbot/blob/master/lib/engine.js + +Input: +``` + opts.currency + opts.asset +``` +Return: +``` + balance.asset + balance.asset_hold + balance.currency + balance.currency_hold +``` +Comment: +Asset vs asset_hold and currency vs currency_hold is kind of mysterious to me. +For most exchanges I would just return something similar to available_asset and available_currency +For exchanges that returns some other values, I would do the calculation on the extension layer +and not leave it to engine.js, because available_asset and available_currency are only interesting +values from a buy/sell view, IMHO. If someone knows better, please clarify + +```javascript +getQuote: function (opts, cb) +``` +Public function for getting ticker data from the exchange +Called from: +- https://github.com/carlos8f/zenbot/blob/master/lib/engine.js +- https://github.com/carlos8f/zenbot/blob/master/commands/buyjs +- https://github.com/carlos8f/zenbot/blob/master/commands/sell.js + +Input: +``` + opts.product_id +``` +Return: +``` + {bid: value_of_bid, ask: value_of_ask} +``` +```javascript +cancelOrder: function (opts, cb) +``` +Obviously a function for canceling an placed order +Called from: +- https://github.com/carlos8f/zenbot/blob/master/lib/engine.js + +Input: +``` + opts.order_id +``` +```javascript +buy: function (opts, cb) +``` +The function for buying +Called from: +- https://github.com/carlos8f/zenbot/blob/master/lib/engine.js + +Input: +``` + opts.price + opts.size +``` +Returns: +``` + +``` + +```javascript +sell: function (opts, cb) +``` + +The function for selling +Called from: +- https://github.com/carlos8f/zenbot/blob/master/lib/engine.js + +Input: +``` + opts.price + opts.size +``` +Returns: +``` + +``` + +```javascript +getOrder: function (opts, cb) +``` + +Function to get data from a placed order +Called from: +- https://github.com/carlos8f/zenbot/blob/master/lib/engine.js + +Input: +``` + opts.order_id + opts.product_id +``` +Returns: +``` + order.status +``` +Expected values: 'done', 'rejected' + If 'rejected' order.reject_reason = some_reason ('post only') +Is '*post only*' spesific for GDAX? +Comment: Needs some clarifying + +```javascript +getCursor: function (trade) +``` +Function to get details from an executed trade +Called from: +- https://github.com/carlos8f/zenbot/blob/master/commands/backfill.js +- https://github.com/carlos8f/zenbot/blob/master/commands/trade.js + +Input: +``` + trade.trade_id +``` +Return: +``` + trade.trade_id +``` From 009f885e43786580f8560544e6e4aa5de099982a Mon Sep 17 00:00:00 2001 From: eigilb Date: Tue, 6 Jun 2017 01:13:15 +0200 Subject: [PATCH 04/27] Delete exchanges.md --- docs/exchanges.md | 234 ---------------------------------------------- 1 file changed, 234 deletions(-) delete mode 100644 docs/exchanges.md diff --git a/docs/exchanges.md b/docs/exchanges.md deleted file mode 100644 index 9c7ad718eb..0000000000 --- a/docs/exchanges.md +++ /dev/null @@ -1,234 +0,0 @@ -Zenbot exchange API ------------------------------ -This document is written to help developers implement new extensions for Zenbot. - -It is reverse engineered from inspecting the Zenbot files and the GDAX extension and is not a definitive guide for developing an extension. - -Any contribution that makes this document better is certainly welcome. - -The document is an attempt to describe the interface functions used for communication with an exchange and a few helper functions. Each function has a set of calling parameters and return values and statuses - -The input parameters are packed in the "opts" object, and the results from invoking the function are returned in an object. - -Error handling -------------------- - -**Non recoverable errors** should be handled by the actual extension function. A typical error is "Page not found", which most likely is caused by a malformed URL. Such errors should return a descriptive message and force a program exit. - -**Recoverable errors** affecting trades should be handled by zenbot, while others could be handled in the extension layer. This needs to be clarified. - -Some named errors are already handled by the main program (see getTrades below). These are: -``` - 'ETIMEDOUT', // possibly recoverable - 'ENOTFOUND', // not recoverable (404?) - 'ECONNRESET' // possibly recoverable -``` -Zenbot may have some GDAX-specific code. In particular that pertains to return values from exchange functions. Return values in general should be handled in a exchange agnostic and standardized way to make it easiest possible to write extensions. - -Some variables in the "exchange" object are worth mentioning ------------------------------------------------------------------------------------ -``` - name: 'some_exchange_name' - historyScan: 'forward', 'backward' or false - makerFee: fee_from_exchange (numeric) - backfillRateLimit: some_value_fitting_exchange_policy or 0 -``` -The functions ------------------- - -```javascript -funcion publicClient () -``` -Function for connecting to the exchange for public requests -Called from: -- extension/*/exchange.js - -Returns a "client" object for use in exchange public access functions. - -```javascript -function authedClient () -``` -Function for connecting and authenticating private requests -Called from: -- extension/*/exchange.js - -The function gets parameters from conf.js in the c object -In particular these are: -``` - c..key - c..secret -``` -For specific exchanges also: -``` - c.bitstamp.client_id - c.gdax.passphrase -``` -The functionm returns a "client" object for use in exchange authenticated access functions - -```javascript -function statusErr (resp, body) -``` -Helper function for returning conformant error messages -Called from: -- extension/*/exchange.js - -```javascript -getTrades: function (opts, cb) -``` -Public function for getting history and trade data from the exchange -Called from: -- https://github.com/carlos8f/zenbot/blob/master/commands/backfill.js -- https://github.com/carlos8f/zenbot/blob/master/commands/trade.js - -Input: -``` - opts.product_id - opts.from - opts.to -``` -Return: -``` - trades.length - (array of?) { - trade_id: some_id - time: 'transaction_time', - size: trade_size, - price: trade_prize, - side : 'buy' or 'sell' - } -``` -Expected error codes if error: -``` - err.code - - 'ETIMEDOUT', // possibly recoverable - 'ENOTFOUND', // not recoverable - 'ECONNRESET' // possibly recoverable -``` -```javascript -getBalance: function (opts, cb) -``` -Function for getting wallet getBalances from the exchange -Called from: -- https://github.com/carlos8f/zenbot/blob/master/lib/engine.js - -Input: -``` - opts.currency - opts.asset -``` -Return: -``` - balance.asset - balance.asset_hold - balance.currency - balance.currency_hold -``` -Comment: -Asset vs asset_hold and currency vs currency_hold is kind of mysterious to me. -For most exchanges I would just return something similar to available_asset and available_currency -For exchanges that returns some other values, I would do the calculation on the extension layer -and not leave it to engine.js, because available_asset and available_currency are only interesting -values from a buy/sell view, IMHO. If someone knows better, please clarify - -```javascript -getQuote: function (opts, cb) -``` -Public function for getting ticker data from the exchange -Called from: -- https://github.com/carlos8f/zenbot/blob/master/lib/engine.js -- https://github.com/carlos8f/zenbot/blob/master/commands/buyjs -- https://github.com/carlos8f/zenbot/blob/master/commands/sell.js - -Input: -``` - opts.product_id -``` -Return: -``` - {bid: value_of_bid, ask: value_of_ask} -``` -```javascript -cancelOrder: function (opts, cb) -``` -Obviously a function for canceling an placed order -Called from: -- https://github.com/carlos8f/zenbot/blob/master/lib/engine.js - -Input: -``` - opts.order_id -``` -```javascript -buy: function (opts, cb) -``` -The function for buying -Called from: -- https://github.com/carlos8f/zenbot/blob/master/lib/engine.js - -Input: -``` - opts.price - opts.size -``` -Returns: -``` - -``` - -```javascript -sell: function (opts, cb) -``` - -The function for selling -Called from: -- https://github.com/carlos8f/zenbot/blob/master/lib/engine.js - -Input: -``` - opts.price - opts.size -``` -Returns: -``` - -``` - -```javascript -getOrder: function (opts, cb) -``` - -Function to get data from a placed order -Called from: -- https://github.com/carlos8f/zenbot/blob/master/lib/engine.js - -Input: -``` - opts.order_id - opts.product_id -``` -Returns: -``` - order.status -``` -Expected values: 'done', 'rejected' - If 'rejected' order.reject_reason = some_reason ('post only') -Is '*post only*' spesific for GDAX? -Comment: Needs some clarifying - -```javascript -getCursor: function (trade) -``` -Function to get details from an executed trade -Called from: -- https://github.com/carlos8f/zenbot/blob/master/commands/backfill.js -- https://github.com/carlos8f/zenbot/blob/master/commands/trade.js - -Input: -``` - trade.trade_id -``` -Return: -``` - trade.trade_id -``` From 8571b6edd00b14a4c8a559bcd4638b84c41bfae1 Mon Sep 17 00:00:00 2001 From: eigilb Date: Tue, 6 Jun 2017 01:14:10 +0200 Subject: [PATCH 05/27] Create developers.md --- docs/developers.md | 234 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 234 insertions(+) create mode 100644 docs/developers.md diff --git a/docs/developers.md b/docs/developers.md new file mode 100644 index 0000000000..9c7ad718eb --- /dev/null +++ b/docs/developers.md @@ -0,0 +1,234 @@ +Zenbot exchange API +----------------------------- +This document is written to help developers implement new extensions for Zenbot. + +It is reverse engineered from inspecting the Zenbot files and the GDAX extension and is not a definitive guide for developing an extension. + +Any contribution that makes this document better is certainly welcome. + +The document is an attempt to describe the interface functions used for communication with an exchange and a few helper functions. Each function has a set of calling parameters and return values and statuses + +The input parameters are packed in the "opts" object, and the results from invoking the function are returned in an object. + +Error handling +------------------- + +**Non recoverable errors** should be handled by the actual extension function. A typical error is "Page not found", which most likely is caused by a malformed URL. Such errors should return a descriptive message and force a program exit. + +**Recoverable errors** affecting trades should be handled by zenbot, while others could be handled in the extension layer. This needs to be clarified. + +Some named errors are already handled by the main program (see getTrades below). These are: +``` + 'ETIMEDOUT', // possibly recoverable + 'ENOTFOUND', // not recoverable (404?) + 'ECONNRESET' // possibly recoverable +``` +Zenbot may have some GDAX-specific code. In particular that pertains to return values from exchange functions. Return values in general should be handled in a exchange agnostic and standardized way to make it easiest possible to write extensions. + +Some variables in the "exchange" object are worth mentioning +----------------------------------------------------------------------------------- +``` + name: 'some_exchange_name' + historyScan: 'forward', 'backward' or false + makerFee: fee_from_exchange (numeric) + backfillRateLimit: some_value_fitting_exchange_policy or 0 +``` +The functions +------------------ + +```javascript +funcion publicClient () +``` +Function for connecting to the exchange for public requests +Called from: +- extension/*/exchange.js + +Returns a "client" object for use in exchange public access functions. + +```javascript +function authedClient () +``` +Function for connecting and authenticating private requests +Called from: +- extension/*/exchange.js + +The function gets parameters from conf.js in the c object +In particular these are: +``` + c..key + c..secret +``` +For specific exchanges also: +``` + c.bitstamp.client_id + c.gdax.passphrase +``` +The functionm returns a "client" object for use in exchange authenticated access functions + +```javascript +function statusErr (resp, body) +``` +Helper function for returning conformant error messages +Called from: +- extension/*/exchange.js + +```javascript +getTrades: function (opts, cb) +``` +Public function for getting history and trade data from the exchange +Called from: +- https://github.com/carlos8f/zenbot/blob/master/commands/backfill.js +- https://github.com/carlos8f/zenbot/blob/master/commands/trade.js + +Input: +``` + opts.product_id + opts.from + opts.to +``` +Return: +``` + trades.length + (array of?) { + trade_id: some_id + time: 'transaction_time', + size: trade_size, + price: trade_prize, + side : 'buy' or 'sell' + } +``` +Expected error codes if error: +``` + err.code + + 'ETIMEDOUT', // possibly recoverable + 'ENOTFOUND', // not recoverable + 'ECONNRESET' // possibly recoverable +``` +```javascript +getBalance: function (opts, cb) +``` +Function for getting wallet getBalances from the exchange +Called from: +- https://github.com/carlos8f/zenbot/blob/master/lib/engine.js + +Input: +``` + opts.currency + opts.asset +``` +Return: +``` + balance.asset + balance.asset_hold + balance.currency + balance.currency_hold +``` +Comment: +Asset vs asset_hold and currency vs currency_hold is kind of mysterious to me. +For most exchanges I would just return something similar to available_asset and available_currency +For exchanges that returns some other values, I would do the calculation on the extension layer +and not leave it to engine.js, because available_asset and available_currency are only interesting +values from a buy/sell view, IMHO. If someone knows better, please clarify + +```javascript +getQuote: function (opts, cb) +``` +Public function for getting ticker data from the exchange +Called from: +- https://github.com/carlos8f/zenbot/blob/master/lib/engine.js +- https://github.com/carlos8f/zenbot/blob/master/commands/buyjs +- https://github.com/carlos8f/zenbot/blob/master/commands/sell.js + +Input: +``` + opts.product_id +``` +Return: +``` + {bid: value_of_bid, ask: value_of_ask} +``` +```javascript +cancelOrder: function (opts, cb) +``` +Obviously a function for canceling an placed order +Called from: +- https://github.com/carlos8f/zenbot/blob/master/lib/engine.js + +Input: +``` + opts.order_id +``` +```javascript +buy: function (opts, cb) +``` +The function for buying +Called from: +- https://github.com/carlos8f/zenbot/blob/master/lib/engine.js + +Input: +``` + opts.price + opts.size +``` +Returns: +``` + +``` + +```javascript +sell: function (opts, cb) +``` + +The function for selling +Called from: +- https://github.com/carlos8f/zenbot/blob/master/lib/engine.js + +Input: +``` + opts.price + opts.size +``` +Returns: +``` + +``` + +```javascript +getOrder: function (opts, cb) +``` + +Function to get data from a placed order +Called from: +- https://github.com/carlos8f/zenbot/blob/master/lib/engine.js + +Input: +``` + opts.order_id + opts.product_id +``` +Returns: +``` + order.status +``` +Expected values: 'done', 'rejected' + If 'rejected' order.reject_reason = some_reason ('post only') +Is '*post only*' spesific for GDAX? +Comment: Needs some clarifying + +```javascript +getCursor: function (trade) +``` +Function to get details from an executed trade +Called from: +- https://github.com/carlos8f/zenbot/blob/master/commands/backfill.js +- https://github.com/carlos8f/zenbot/blob/master/commands/trade.js + +Input: +``` + trade.trade_id +``` +Return: +``` + trade.trade_id +``` From 2eddac82b36258f9b854a4fbf5a5b2cffec502d7 Mon Sep 17 00:00:00 2001 From: eigilb Date: Tue, 6 Jun 2017 10:03:03 +0200 Subject: [PATCH 06/27] Update developers.md --- docs/developers.md | 87 ++++++++++++++++++++++++++++++++++------------ 1 file changed, 64 insertions(+), 23 deletions(-) diff --git a/docs/developers.md b/docs/developers.md index 9c7ad718eb..55d6f750d2 100644 --- a/docs/developers.md +++ b/docs/developers.md @@ -1,5 +1,4 @@ -Zenbot exchange API ------------------------------ +## Zenbot exchange API This document is written to help developers implement new extensions for Zenbot. It is reverse engineered from inspecting the Zenbot files and the GDAX extension and is not a definitive guide for developing an extension. @@ -10,13 +9,22 @@ The document is an attempt to describe the interface functions used for communic The input parameters are packed in the "opts" object, and the results from invoking the function are returned in an object. -Error handling -------------------- +## Error handling + +Errors are returned to calling program through a callback functon of this form: +```javascript +cb (err) +``` +The expected content of "err" is as follows: +```javascript + { code: 'HTTP_STATUS', body: body } +``` **Non recoverable errors** should be handled by the actual extension function. A typical error is "Page not found", which most likely is caused by a malformed URL. Such errors should return a descriptive message and force a program exit. **Recoverable errors** affecting trades should be handled by zenbot, while others could be handled in the extension layer. This needs to be clarified. + Some named errors are already handled by the main program (see getTrades below). These are: ``` 'ETIMEDOUT', // possibly recoverable @@ -25,30 +33,28 @@ Some named errors are already handled by the main program (see getTrades below). ``` Zenbot may have some GDAX-specific code. In particular that pertains to return values from exchange functions. Return values in general should be handled in a exchange agnostic and standardized way to make it easiest possible to write extensions. -Some variables in the "exchange" object are worth mentioning ------------------------------------------------------------------------------------ +**Some variables in the "exchange" object are worth mentioning** ``` name: 'some_exchange_name' historyScan: 'forward', 'backward' or false makerFee: fee_from_exchange (numeric) backfillRateLimit: some_value_fitting_exchange_policy or 0 ``` -The functions ------------------- +## Functions +**Connecting to the exchange for public requests** ```javascript funcion publicClient () ``` -Function for connecting to the exchange for public requests Called from: - extension/*/exchange.js Returns a "client" object for use in exchange public access functions. +**Connecting and authenticating private requests** ```javascript function authedClient () ``` -Function for connecting and authenticating private requests Called from: - extension/*/exchange.js @@ -65,17 +71,17 @@ For specific exchanges also: ``` The functionm returns a "client" object for use in exchange authenticated access functions +**Helper function for returning conformant error messages** ```javascript function statusErr (resp, body) ``` -Helper function for returning conformant error messages Called from: - extension/*/exchange.js +**Getting public history and trade data** ```javascript getTrades: function (opts, cb) ``` -Public function for getting history and trade data from the exchange Called from: - https://github.com/carlos8f/zenbot/blob/master/commands/backfill.js - https://github.com/carlos8f/zenbot/blob/master/commands/trade.js @@ -105,10 +111,15 @@ Expected error codes if error: 'ENOTFOUND', // not recoverable 'ECONNRESET' // possibly recoverable ``` +Callback: +```javascript +cb(null, trades) +``` + +**Getting wallet balances** ```javascript getBalance: function (opts, cb) ``` -Function for getting wallet getBalances from the exchange Called from: - https://github.com/carlos8f/zenbot/blob/master/lib/engine.js @@ -124,6 +135,10 @@ Return: balance.currency balance.currency_hold ``` +Callback: +```javascript +cb(null, balance) +``` Comment: Asset vs asset_hold and currency vs currency_hold is kind of mysterious to me. For most exchanges I would just return something similar to available_asset and available_currency @@ -131,10 +146,10 @@ For exchanges that returns some other values, I would do the calculation on the and not leave it to engine.js, because available_asset and available_currency are only interesting values from a buy/sell view, IMHO. If someone knows better, please clarify +**Getting public ticker data** ```javascript getQuote: function (opts, cb) ``` -Public function for getting ticker data from the exchange Called from: - https://github.com/carlos8f/zenbot/blob/master/lib/engine.js - https://github.com/carlos8f/zenbot/blob/master/commands/buyjs @@ -148,10 +163,15 @@ Return: ``` {bid: value_of_bid, ask: value_of_ask} ``` +Callback: +```javascript +cb(null, {bid: body.bid, ask: body.ask}) +``` + +**Canceling a placed order** ```javascript cancelOrder: function (opts, cb) ``` -Obviously a function for canceling an placed order Called from: - https://github.com/carlos8f/zenbot/blob/master/lib/engine.js @@ -159,10 +179,15 @@ Input: ``` opts.order_id ``` +Callback: +```javascript +cb() +``` + +**Buying function** ```javascript buy: function (opts, cb) ``` -The function for buying Called from: - https://github.com/carlos8f/zenbot/blob/master/lib/engine.js @@ -174,13 +199,16 @@ Input: Returns: ``` +``` +Callback: +```javascript +cb(null, body) ``` +**Selling function** ```javascript sell: function (opts, cb) ``` - -The function for selling Called from: - https://github.com/carlos8f/zenbot/blob/master/lib/engine.js @@ -192,13 +220,16 @@ Input: Returns: ``` +``` +Callback: +```javascript +cb(null, body) ``` +**Getting data from a placed order** ```javascript getOrder: function (opts, cb) ``` - -Function to get data from a placed order Called from: - https://github.com/carlos8f/zenbot/blob/master/lib/engine.js @@ -211,24 +242,34 @@ Returns: ``` order.status ``` -Expected values: 'done', 'rejected' +Expected values in https://github.com/carlos8f/zenbot/blob/master/lib/engine.js: +- 'done', 'rejected' If 'rejected' order.reject_reason = some_reason ('post only') Is '*post only*' spesific for GDAX? Comment: Needs some clarifying +Callback: +```javascript +cb(null, body) +``` + +**Getting details from an executed trade** ```javascript getCursor: function (trade) ``` -Function to get details from an executed trade Called from: - https://github.com/carlos8f/zenbot/blob/master/commands/backfill.js - https://github.com/carlos8f/zenbot/blob/master/commands/trade.js Input: ``` - trade.trade_id + trade ``` Return: ``` trade.trade_id ``` +Callback: +```javascript + +``` From 4f86032408edb33fbb7864e65d47f22d0ec41796 Mon Sep 17 00:00:00 2001 From: eigilb Date: Wed, 7 Jun 2017 12:32:20 +0200 Subject: [PATCH 07/27] Update developers.md --- docs/developers.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/developers.md b/docs/developers.md index 55d6f750d2..f9132463c9 100644 --- a/docs/developers.md +++ b/docs/developers.md @@ -37,7 +37,8 @@ Zenbot may have some GDAX-specific code. In particular that pertains to return v ``` name: 'some_exchange_name' historyScan: 'forward', 'backward' or false - makerFee: fee_from_exchange (numeric) + makerFee: exchange_maker_fee (numeric) // Set by a function if the exchange supports it + takerFee: exchange_taker_fee (numeric) // Else set with a constant backfillRateLimit: some_value_fitting_exchange_policy or 0 ``` ## Functions From 4b54f3f5636697e03fd4db7076fcbb9f882fecd8 Mon Sep 17 00:00:00 2001 From: eigilb Date: Thu, 8 Jun 2017 14:33:02 +0200 Subject: [PATCH 08/27] New command "zenbot balance " Introduces a new comand: "zenbot balance " Sample output: BTC-USD Asset: 9.87654321. Currency: 1.23 --- commands/_codemap.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/commands/_codemap.js b/commands/_codemap.js index 2a1b634491..2f16f627d5 100644 --- a/commands/_codemap.js +++ b/commands/_codemap.js @@ -6,6 +6,7 @@ module.exports = { 'list-strategies': require('./list-strategies'), 'backfill': require('./backfill'), 'sim': require('./sim'), + 'balance': require('./balance'), 'trade': require('./trade'), 'buy': require('./buy'), 'sell': require('./sell'), @@ -14,7 +15,8 @@ module.exports = { 'list[30]': '#commands.list-strategies', 'list[50]': '#commands.backfill', 'list[60]': '#commands.sim', + 'list[65]': '#commands.balance', 'list[70]': '#commands.trade', 'list[80]': '#commands.buy', 'list[90]': '#commands.sell' -} \ No newline at end of file +} From b23a9126ff9bd9fdb1e325a4bf491a8066b891b9 Mon Sep 17 00:00:00 2001 From: eigilb Date: Thu, 8 Jun 2017 14:36:57 +0200 Subject: [PATCH 09/27] New command "zenbot balance " --- commands/balance.js | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 commands/balance.js diff --git a/commands/balance.js b/commands/balance.js new file mode 100644 index 0000000000..a613db0b6d --- /dev/null +++ b/commands/balance.js @@ -0,0 +1,41 @@ +var minimist = require('minimist') + , n = require('numbro') + , colors = require('colors') + +module.exports = function container (get, set, clear) { + var c = get('conf') + return function (program) { + program + .command('balance [selector]') + .allowUnknownOption() + .description('get asset and currency balance from the exchange') + //.option('--all', 'output all balances') + .option('--debug', 'output detailed debug info') + .action(function (selector, cmd) { + var s = {options: minimist(process.argv)} + s.selector = get('lib.normalize-selector')(selector || c.selector) + var exch = s.selector.split('.')[0] + s.exchange = get('exchanges.' + exch) + s.product_id = s.selector.split('.')[1] + s.asset = s.product_id.split('-')[0] + s.currency = s.product_id.split('-')[1] + var so = s.options + delete so._ + Object.keys(c).forEach(function (k) { + if (typeof cmd[k] !== 'undefined') { + so[k] = cmd[k] + } + }) + so.debug = cmd.debug + function balance () { + s.exchange.getBalance(s, function (err, balance) { + if (err) return cb(err) + var bal = s.product_id + ' Asset: ' + balance.asset + ' Currency: ' + balance.currency + console.log(bal) + process.exit() + }) + } + balance() + }) + } +} From eee2986a8e78cc83734f5d2b2e2504790f811f7b Mon Sep 17 00:00:00 2001 From: Eigil Bjorgum Date: Sun, 11 Jun 2017 21:51:04 +0200 Subject: [PATCH 10/27] The increase in traffic on the exhanges we have seen lately can be quite a challenge when developing a trading bot. Traffic conguestion, DOS attacks, broken connections and what not. On the exchange side they need tu have mechanisms to conrol the traffic. One measurement the bot are facing is the risk of being blocked with rate limiting. With the use of HTTP requests a lot of connecting, closing and reconnecting are taking place, and it is easy to get into a rate limit situation. In the interface layer for Bitstamp we have used websockets for getting data. Unfortunately Bitstamp only have implemented two public channel, tickets and orderbook. Luckily it is tickers and orderbook that generate most of the traffic from the exchange. In short it means that we have to use traditional HTTP requests for the private authenticated functions. But use of websockets functions means we can connect once per session and listen for streaming data without the fear of being locked out by a rate limit block. The Bitstamp interface may still have some bugs and shortcomings, but hopefully such problems can be sorted out. --- extensions/exchanges/bitstamp/_codemap.js | 7 + extensions/exchanges/bitstamp/exchange.js | 329 ++++++++++++++++++++ extensions/exchanges/bitstamp/package.json | 9 + extensions/exchanges/bitstamp/products.json | 56 ++++ 4 files changed, 401 insertions(+) create mode 100644 extensions/exchanges/bitstamp/_codemap.js create mode 100644 extensions/exchanges/bitstamp/exchange.js create mode 100644 extensions/exchanges/bitstamp/package.json create mode 100644 extensions/exchanges/bitstamp/products.json diff --git a/extensions/exchanges/bitstamp/_codemap.js b/extensions/exchanges/bitstamp/_codemap.js new file mode 100644 index 0000000000..e333eb0e84 --- /dev/null +++ b/extensions/exchanges/bitstamp/_codemap.js @@ -0,0 +1,7 @@ +module.exports = { + _ns: 'zenbot', + _name: 'bitstamp', + + 'exchanges.bitstamp': require('./exchange'), + 'exchanges.list[]': '#exchanges.bitstamp' +} diff --git a/extensions/exchanges/bitstamp/exchange.js b/extensions/exchanges/bitstamp/exchange.js new file mode 100644 index 0000000000..8b21e7526c --- /dev/null +++ b/extensions/exchanges/bitstamp/exchange.js @@ -0,0 +1,329 @@ +var Bitstamp = require('bitstamp') + , minimist = require('minimist') + , path = require('path') + , colors = require('colors') + , numbro = require('numbro') + , Pusher = require('pusher-js/node') + +var args = process.argv + +var wsOpts = { + encrypted: true, + pairOk: false, + currencyPair: 'btcusd', + trades: {evType: 'trade', channel: 'live_trades'}, + quotes: {evType: 'data', channel: 'order_book'} +} + +// The use of bitstamp-ws requires that +// Knowledge of the asset/currency pair +// before the first call for a trade +// As zenbot dont returns the currency pair +// before the first trade is requested +// it ha ben neccessary to get it from +// the command line arguments +args.forEach(function(value) { + if (value.match(/bitstamp|BITSTAMP/)) { + var p = value.split('.')[1] + var prod = p.split('-')[0] + p.split('-')[1] + var pair = prod.toLowerCase() + if (!wsOpts.pairOk) { + if (pair !== 'btcusd') { + wsOpts.trades.channel = 'live_trades_' + pair + wsOpts.quotes.channel = 'order_book_' + pair + } + wsOpts.currencyPair = pair + wsOpts.pairOk = true + } + } +}) + +function joinProduct (product_id) { + return product_id.split('-')[0] + product_id.split('-')[1] +} + + +module.exports = function container (get, set, clear) { + + var c = get('conf') + var defs = require('./conf-sample') + + try { + c.bitstamp = require('./conf') + } + catch (e) { + c.bitstamp = {} + } + Object.keys(defs).forEach(function (k) { + if (typeof c.bitstamp[k] === 'undefined') { + c.bitstamp[k] = defs[k] + } + }) + + //console.log(c.bitstamp) + function authedClient () { + if (c.bitstamp.key && c.bitstamp.key !== 'YOUR-API-KEY') { + return new Bitstamp(c.bitstamp.key, c.bitstamp.secret, c.bitstamp.client_id) + } + throw new Error('please configure your Bitstamp credentials in ' + path.resolve(__dirname, 'conf.js')) + } + +//*************************************************** +// +// The websocket functions +// +var BITSTAMP_PUSHER_KEY = 'de504dc5763aeef9ff52' + +var Bitstamp_WS = function(opts) { + if (opts) { + this.opts = opts + } + else { + this.opts = { + encrypted: true, + } + } + + this.client = new Pusher(BITSTAMP_PUSHER_KEY, { + encrypted: this.opts.encrypted + //encrypted: true + }) + + // bitstamp publishes all data over just 2 channels + // make sure we only subscribe to each channel once + this.bound = { + trade: false, + data: false + } + + this.subscribe() +} + +var util = require('util') +var EventEmitter = require('events').EventEmitter +util.inherits(Bitstamp_WS, EventEmitter) + + +Bitstamp_WS.prototype.subscribe = function() { +//console.log('wsOpts ==> ', wsOpts) + if (wsOpts.pairOk) { + this.client.subscribe(wsOpts.trades.channel) + this.client.bind(wsOpts.trades.evType, this.broadcast(wsOpts.trades.evType)) + this.client.subscribe(wsOpts.quotes.channel) + this.client.bind(wsOpts.quotes.evType, this.broadcast(wsOpts.quotes.evType)) + } +} + +Bitstamp_WS.prototype.broadcast = function(name) { + if(this.bound[name]) + return function noop() {} + this.bound[name] = true + + return function(e) { + this.emit(name, e) + }.bind(this) +} + // Placeholders + wsquotes = {bid: 0, ask: 0} + wstrades = + [ + { + trade_id: 0, + time:1000, + size: 0, + price: 0, + side: '' + } + ] + +var wsTrades = new Bitstamp_WS({ + channel: wsOpts.trades.channel, + evType: 'trade' +}) + +var wsQuotes = new Bitstamp_WS({ + channel: wsOpts.quotes.channel, + evType: 'data' +}) + +wsTrades.on('data', function(data) { + wsquotes = { + bid: data.bids[0][0], + ask: data.asks[0][0] + } +}) + +wsQuotes.on('trade', function(data) { + wstrades.push( { + trade_id: data.id, + time: Number(data.timestamp) * 1000, + size: data.amount, + price: data.price, + side: data.type === 0 ? 'buy' : 'sell' + }) + if (wstrades.length > 30) wstrades.splice(0,10) +// console.log('trades: ',wstrades) +}) + +//*************************************************** + + function statusErr (err, body) { + if (typeof body === 'undefined') { + var ret = {} + var res = err.toString().split(':',2) + ret.status = res[1] + var ret = new Error(ret.status ) + return ret + } else { + if (body.error) { + var ret = new Error('Error: ' + body.error) + return ret + } else { + return body + } + } + } + + function retry (method, args) { + var to = args.wait + if (method !== 'getTrades') { + console.error(('\nBitstamp API is not answering! unable to call ' + method + ',OB retrying...').red) + } + setTimeout(function () { + exchange[method].apply(exchange, args) + }, args.wait) + } + + var exchange = { + + getProducts: function (opts) { + return require('./products.json') + }, + + //----------------------------------------------------- + // Public API functions + // getQuote() and getTrades are using Bitstamp websockets + // The data is not done by calling the interface function, + // but rather pulled from the "wstrades" and "wsquotes" JSOM objects + // Those objects are populated by the websockets event handlers + + getTrades: function (opts, cb) { + var currencyPair = joinProduct(opts.product_id).toLowerCase() + + var args = { + wait: 2000, + product_id: wsOpts.currencyPair + } + + if (opts.from) { + args.before = opts.from + } + else if (opts.to) { + args.after = opts.to + } + +  if (typeof wstrades.time == undefined) return retry('getTrades', args) + var t = wstrades + var trades = t.map(function (trade) { + return (trade) + }) + + cb(null, trades) + }, + + getQuote: function (opts, cb) { + var args = { + wait: 2000, + currencyPair: wsOpts.currencyPair + } +  if (typeof wsquotes.bid == undefined) return retry('getQuote', args ) + cb(null, wsquotes) + }, + + //----------------------------------------------------- + // Private (authenticated) functions + // + + getBalance: function (opts, cb) { + var client = authedClient() +   client.balance(null, function (err, body) { + body = statusErr(err,body) + var balance = {asset: 0, currency: 0} + balance.currency = body[opts.currency.toLowerCase() + '_available'] + balance.asset = body[opts.asset.toLowerCase() + '_available'] + balance.currency_hold = 0 + balance.asset_hold = 0 + cb(null, balance) + }) + }, + + cancelOrder: function (opts, cb) { + var client = authedClient() + client.cancel_order(opts.order_id, function (err, body) { + body = statusErr(err,body) + cb() + }) + }, + + cancelOrders: function (opts, cb) { + var client = authedClient() + client.cancel_all_orders(function (err, body) { + body = statusErr(err,body) + cb() + }) + }, + + buy: function (opts, cb) { + var client = authedClient() + var currencyPair = joinProduct(opts.product_id).toLowerCase() + if (typeof opts.type === 'undefined' ) { + opts.ordertype = 'limit' + } + if (opts.ordertype === 'limit') { + // Fix limit_price? + client.buy(currencyPair, opts.size, opts.price, false, function (err, body) { + body = statusErr(err,body) + cb(null, body) + }) + } else { + client.buyMarket(currencyPair, opts.size, function (err, body) { + body = statusErr(err,body) + cb(null, body) + }) + } + }, + + sell: function (opts, cb) { + var client = authedClient() + var currencyPair = joinProduct(opts.product_id).toLowerCase() + if (typeof opts.ordertype === 'undefined' ) { + opts.ordertype = 'limit' + } + if (opts.ordertype === 'limit') { + client.sell(currencyPair, opts.size, opts.price, false, function (err, body) { + body = statusErr(err,body) + cb(null, body) + }) + } else { + client.sellMarket(currencyPair, opts.size, function (err, body) { + body = statusErr(err,body) + cb(null, body) + }) + } + }, + + getOrder: function (opts, cb) { + var client = authedClient() + client.getOrder(opts.order_id, function (err, body) { + body = statusErr(err,body) + cb(null, body) + }) + }, + + // return the property used for range querying. + getCursor: function (trade) { + return trade.trade_id + } + } + return exchange +} diff --git a/extensions/exchanges/bitstamp/package.json b/extensions/exchanges/bitstamp/package.json new file mode 100644 index 0000000000..6d729dadcf --- /dev/null +++ b/extensions/exchanges/bitstamp/package.json @@ -0,0 +1,9 @@ +{ + "name": "zenbot_bitstamp", + "version": "0.0.0", + "description": "zenbot supporting code for Bitstamp", + "dependencies": { + "bitstamp": "^1.0.1", + "pusher-js": "^4.1.0" + } +} diff --git a/extensions/exchanges/bitstamp/products.json b/extensions/exchanges/bitstamp/products.json new file mode 100644 index 0000000000..837c65058d --- /dev/null +++ b/extensions/exchanges/bitstamp/products.json @@ -0,0 +1,56 @@ +[ + { + "id": "BTCUSD", + "asset": "BTC", + "currency": "USD", + "min_size": "0.01", + "max_size": "10000", + "increment": "0.01", + "label": "BTC/USD" + }, + { + "id": "BTCEUR", + "asset": "BTC", + "currency": "EUR", + "min_size": "0.01", + "max_size": "10000", + "increment": "0.01", + "label": "BTC/EUR" + }, + { + "id": "EURUSD", + "asset": "EUR", + "currency": "USD", + "min_size": "0.01", + "max_size": "10000", + "increment": "0.01", + "label": "EUR/USD" + }, + { + "id": "XRPUSD", + "asset": "XRP", + "currency": "USD", + "min_size": "0.01", + "max_size": "1000000", + "increment": "0.01", + "label": "XRP/USD" + }, + { + "id": "XRPEUR", + "asset": "XRP", + "currency": "EUR", + "min_size": "0.01", + "max_size": "1000000", + "increment": "0.01", + "label": "XRP/EUR" + }, + { + "id": "XRPBTC", + "asset": "XRP", + "currency": "BTC", + "min_size": "0.01", + "max_size": "1000000", + "increment": "0.00001", + "label": "XRP/BTC" + } +] From 0f6602c58ae5d5e5af13351c5160b3fa556c553f Mon Sep 17 00:00:00 2001 From: Eigil Bjorgum Date: Mon, 12 Jun 2017 09:22:20 +0200 Subject: [PATCH 11/27] Updated conf-sample.js --- conf-sample.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/conf-sample.js b/conf-sample.js index 810b4c7fa7..65c6a5647c 100644 --- a/conf-sample.js +++ b/conf-sample.js @@ -51,6 +51,12 @@ c.bitfinex.secret = 'YOUR-SECRET' // May use 'exchange' or 'trading' wallet balances. However margin trading may not work...read the API documentation. c.bitfinex.wallet = 'exchange' +// to enable Bitfinex trading, enter your API credentials: +c.bitstamp.key = 'YOUR-API-KEY' +c.bitstamp.secret = 'YOUR-SECRET' +// A client ID is required on Bitstamp +c.bitstamp.client_id = 'YOUR-CLIENT-ID' + // Optional stop-order triggers: // sell if price drops below this % of bought price (0 to disable) From 4d87e0804497d0432148381616bfcf5607c0e634 Mon Sep 17 00:00:00 2001 From: Eigil Bjorgum Date: Mon, 12 Jun 2017 09:35:15 +0200 Subject: [PATCH 12/27] Small changes in exchange.js --- extensions/exchanges/bitstamp/exchange.js | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/extensions/exchanges/bitstamp/exchange.js b/extensions/exchanges/bitstamp/exchange.js index 8b21e7526c..6f49f594db 100644 --- a/extensions/exchanges/bitstamp/exchange.js +++ b/extensions/exchanges/bitstamp/exchange.js @@ -195,6 +195,10 @@ wsQuotes.on('trade', function(data) { } var exchange = { + name: 'bitstamp', + historyScan: false, + makerFee: 0.25, + takerFee: 0.25, getProducts: function (opts) { return require('./products.json') @@ -276,11 +280,11 @@ wsQuotes.on('trade', function(data) { buy: function (opts, cb) { var client = authedClient() var currencyPair = joinProduct(opts.product_id).toLowerCase() - if (typeof opts.type === 'undefined' ) { - opts.ordertype = 'limit' + if (typeof opts.order_type === 'undefined' ) { + opts.order_type = 'maker' } - if (opts.ordertype === 'limit') { - // Fix limit_price? + if (opts.order_type === 'maker') { + // Fix maker? client.buy(currencyPair, opts.size, opts.price, false, function (err, body) { body = statusErr(err,body) cb(null, body) @@ -296,10 +300,10 @@ wsQuotes.on('trade', function(data) { sell: function (opts, cb) { var client = authedClient() var currencyPair = joinProduct(opts.product_id).toLowerCase() - if (typeof opts.ordertype === 'undefined' ) { - opts.ordertype = 'limit' + if (typeof opts.order_type === 'undefined' ) { + opts.order_type = 'maker' } - if (opts.ordertype === 'limit') { + if (opts.order_type === 'maker') { client.sell(currencyPair, opts.size, opts.price, false, function (err, body) { body = statusErr(err,body) cb(null, body) From 4f0000f76591dbc1ea4ef673bfd7134cc4db2e11 Mon Sep 17 00:00:00 2001 From: Eigil Bjorgum Date: Mon, 12 Jun 2017 10:25:57 +0200 Subject: [PATCH 13/27] Small change in conf-sample.js --- conf-sample.js | 1 + 1 file changed, 1 insertion(+) diff --git a/conf-sample.js b/conf-sample.js index 65c6a5647c..daa0c66fd1 100644 --- a/conf-sample.js +++ b/conf-sample.js @@ -52,6 +52,7 @@ c.bitfinex.secret = 'YOUR-SECRET' c.bitfinex.wallet = 'exchange' // to enable Bitfinex trading, enter your API credentials: +c.bitstamp = {} c.bitstamp.key = 'YOUR-API-KEY' c.bitstamp.secret = 'YOUR-SECRET' // A client ID is required on Bitstamp From c9dbeec1e20936b2fee9a07e0912e217f9bfbaec Mon Sep 17 00:00:00 2001 From: Eigil Bjorgum Date: Mon, 12 Jun 2017 11:39:21 +0200 Subject: [PATCH 14/27] Small insert a LF in gront of error messages --- extensions/exchanges/bitstamp/exchange.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/extensions/exchanges/bitstamp/exchange.js b/extensions/exchanges/bitstamp/exchange.js index 6f49f594db..496d62f8a3 100644 --- a/extensions/exchanges/bitstamp/exchange.js +++ b/extensions/exchanges/bitstamp/exchange.js @@ -65,7 +65,7 @@ module.exports = function container (get, set, clear) { if (c.bitstamp.key && c.bitstamp.key !== 'YOUR-API-KEY') { return new Bitstamp(c.bitstamp.key, c.bitstamp.secret, c.bitstamp.client_id) } - throw new Error('please configure your Bitstamp credentials in ' + path.resolve(__dirname, 'conf.js')) + throw new Error('\nPlease configure your Bitstamp credentials in ' + path.resolve(__dirname, 'conf.js')) } //*************************************************** @@ -176,7 +176,7 @@ wsQuotes.on('trade', function(data) { return ret } else { if (body.error) { - var ret = new Error('Error: ' + body.error) + var ret = new Error('\nError: ' + body.error) return ret } else { return body @@ -187,11 +187,11 @@ wsQuotes.on('trade', function(data) { function retry (method, args) { var to = args.wait if (method !== 'getTrades') { - console.error(('\nBitstamp API is not answering! unable to call ' + method + ',OB retrying...').red) + console.error(('\nBitstamp API is not answering! unable to call ' + method + ',OB retrying in ' + args.wait + 's').red) } setTimeout(function () { exchange[method].apply(exchange, args) - }, args.wait) + }, args.wait * 1000) } var exchange = { @@ -215,7 +215,7 @@ wsQuotes.on('trade', function(data) { var currencyPair = joinProduct(opts.product_id).toLowerCase() var args = { - wait: 2000, + wait: 2, // Seconds product_id: wsOpts.currencyPair } @@ -237,7 +237,7 @@ wsQuotes.on('trade', function(data) { getQuote: function (opts, cb) { var args = { - wait: 2000, + wait: 2, // Seconds currencyPair: wsOpts.currencyPair }   if (typeof wsquotes.bid == undefined) return retry('getQuote', args ) From fc1dc4eba2c392f7c78f07dd737ced2f7b0aa9bb Mon Sep 17 00:00:00 2001 From: Eigil Bjorgum Date: Mon, 12 Jun 2017 16:06:35 +0200 Subject: [PATCH 15/27] Done some cosmetic changes --- extensions/exchanges/bitstamp/exchange.js | 145 +++++++++++----------- 1 file changed, 72 insertions(+), 73 deletions(-) diff --git a/extensions/exchanges/bitstamp/exchange.js b/extensions/exchanges/bitstamp/exchange.js index 496d62f8a3..b41d75ba7c 100644 --- a/extensions/exchanges/bitstamp/exchange.js +++ b/extensions/exchanges/bitstamp/exchange.js @@ -20,7 +20,7 @@ var wsOpts = { // before the first call for a trade // As zenbot dont returns the currency pair // before the first trade is requested -// it ha ben neccessary to get it from +// it has been neccessary to get it from // the command line arguments args.forEach(function(value) { if (value.match(/bitstamp|BITSTAMP/)) { @@ -68,64 +68,63 @@ module.exports = function container (get, set, clear) { throw new Error('\nPlease configure your Bitstamp credentials in ' + path.resolve(__dirname, 'conf.js')) } -//*************************************************** -// -// The websocket functions -// -var BITSTAMP_PUSHER_KEY = 'de504dc5763aeef9ff52' - -var Bitstamp_WS = function(opts) { - if (opts) { - this.opts = opts - } - else { - this.opts = { - encrypted: true, + //*************************************************** + // + // The websocket functions + // + var BITSTAMP_PUSHER_KEY = 'de504dc5763aeef9ff52' + + var Bitstamp_WS = function(opts) { + if (opts) { + this.opts = opts + } else { + this.opts = { + encrypted: true, + } } - } - this.client = new Pusher(BITSTAMP_PUSHER_KEY, { + this.client = new Pusher(BITSTAMP_PUSHER_KEY, { encrypted: this.opts.encrypted //encrypted: true - }) + }) - // bitstamp publishes all data over just 2 channels - // make sure we only subscribe to each channel once - this.bound = { - trade: false, - data: false - } + // bitstamp publishes all data over just 2 channels + // make sure we only subscribe to each channel once + this.bound = { + trade: false, + data: false + } - this.subscribe() -} + this.subscribe() + } -var util = require('util') -var EventEmitter = require('events').EventEmitter -util.inherits(Bitstamp_WS, EventEmitter) + var util = require('util') + var EventEmitter = require('events').EventEmitter + util.inherits(Bitstamp_WS, EventEmitter) -Bitstamp_WS.prototype.subscribe = function() { -//console.log('wsOpts ==> ', wsOpts) - if (wsOpts.pairOk) { - this.client.subscribe(wsOpts.trades.channel) - this.client.bind(wsOpts.trades.evType, this.broadcast(wsOpts.trades.evType)) - this.client.subscribe(wsOpts.quotes.channel) - this.client.bind(wsOpts.quotes.evType, this.broadcast(wsOpts.quotes.evType)) + Bitstamp_WS.prototype.subscribe = function() { + //console.log('wsOpts ==> ', wsOpts) + if (wsOpts.pairOk) { + this.client.subscribe(wsOpts.trades.channel) + this.client.bind(wsOpts.trades.evType, this.broadcast(wsOpts.trades.evType)) + this.client.subscribe(wsOpts.quotes.channel) + this.client.bind(wsOpts.quotes.evType, this.broadcast(wsOpts.quotes.evType)) + } } -} -Bitstamp_WS.prototype.broadcast = function(name) { - if(this.bound[name]) - return function noop() {} - this.bound[name] = true + Bitstamp_WS.prototype.broadcast = function(name) { + if(this.bound[name]) + return function noop() {} + this.bound[name] = true - return function(e) { - this.emit(name, e) - }.bind(this) -} + return function(e) { + this.emit(name, e) + }.bind(this) + } // Placeholders - wsquotes = {bid: 0, ask: 0} - wstrades = + var wsquotes = {bid: 0, ask: 0} + var wstrades = [ { trade_id: 0, @@ -136,36 +135,35 @@ Bitstamp_WS.prototype.broadcast = function(name) { } ] -var wsTrades = new Bitstamp_WS({ - channel: wsOpts.trades.channel, - evType: 'trade' -}) + var wsTrades = new Bitstamp_WS({ + channel: wsOpts.trades.channel, + evType: 'trade' + }) -var wsQuotes = new Bitstamp_WS({ - channel: wsOpts.quotes.channel, - evType: 'data' -}) + var wsQuotes = new Bitstamp_WS({ + channel: wsOpts.quotes.channel, + evType: 'data' + }) -wsTrades.on('data', function(data) { - wsquotes = { - bid: data.bids[0][0], - ask: data.asks[0][0] - } -}) + wsTrades.on('data', function(data) { + wsquotes = { + bid: data.bids[0][0], + ask: data.asks[0][0] + } + }) -wsQuotes.on('trade', function(data) { - wstrades.push( { - trade_id: data.id, - time: Number(data.timestamp) * 1000, - size: data.amount, - price: data.price, - side: data.type === 0 ? 'buy' : 'sell' + wsQuotes.on('trade', function(data) { + wstrades.push( { + trade_id: data.id, + time: Number(data.timestamp) * 1000, + size: data.amount, + price: data.price, + side: data.type === 0 ? 'buy' : 'sell' + }) + if (wstrades.length > 30) wstrades.splice(0,10) }) - if (wstrades.length > 30) wstrades.splice(0,10) -// console.log('trades: ',wstrades) -}) -//*************************************************** + //*************************************************** function statusErr (err, body) { if (typeof body === 'undefined') { @@ -253,7 +251,7 @@ wsQuotes.on('trade', function(data) {   client.balance(null, function (err, body) { body = statusErr(err,body) var balance = {asset: 0, currency: 0} - balance.currency = body[opts.currency.toLowerCase() + '_available'] + balance.currency = body[opts.currency.toLowerCase() + '_available'] balance.asset = body[opts.asset.toLowerCase() + '_available'] balance.currency_hold = 0 balance.asset_hold = 0 @@ -282,7 +280,7 @@ wsQuotes.on('trade', function(data) { var currencyPair = joinProduct(opts.product_id).toLowerCase() if (typeof opts.order_type === 'undefined' ) { opts.order_type = 'maker' - } + } if (opts.order_type === 'maker') { // Fix maker? client.buy(currencyPair, opts.size, opts.price, false, function (err, body) { @@ -331,3 +329,4 @@ wsQuotes.on('trade', function(data) { } return exchange } + From a03a13d44e84b62d402928f5ad9cef10623467fc Mon Sep 17 00:00:00 2001 From: Eigil Bjorgum Date: Sat, 17 Jun 2017 12:03:37 +0200 Subject: [PATCH 16/27] Prpare buy/sell for using market order and make it suitable for exchange API testing --- commands/buy.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/commands/buy.js b/commands/buy.js index c133a82e6b..69752f6780 100644 --- a/commands/buy.js +++ b/commands/buy.js @@ -31,7 +31,11 @@ module.exports = function container (get, set, clear) { var order_types = ['maker', 'taker'] if (!so.order_type in order_types || !so.order_type) { so.order_type = 'maker' - } + } else { + so.order_type = so.taker ? 'taker' : 'maker' + so.order_type === 'taker' ? delete so.taker : delete so.maker + } + s.options.order_type = so.order_type so.mode = 'live' so.strategy = c.strategy so.stats = true @@ -56,7 +60,7 @@ module.exports = function container (get, set, clear) { }) } else { - console.log('placing order...') + console.log('placing ' + so.order_type + ' sell order...') } } setInterval(checkOrder, c.order_poll_time) From c2e514051020ff671d0a75bba05b98996dd89f63 Mon Sep 17 00:00:00 2001 From: Eigil Bjorgum Date: Sat, 17 Jun 2017 12:06:55 +0200 Subject: [PATCH 17/27] Prepare buy/sell for using market order and make it suitable for exchange API testing --- commands/sell.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/commands/sell.js b/commands/sell.js index 1c401628ac..2affc7bbff 100644 --- a/commands/sell.js +++ b/commands/sell.js @@ -31,7 +31,11 @@ module.exports = function container (get, set, clear) { var order_types = ['maker', 'taker'] if (!so.order_type in order_types || !so.order_type) { so.order_type = 'maker' - } + } else { + so.order_type = so.taker ? 'taker' : 'maker' + so.order_type === 'taker' ? delete so.taker : delete so.maker + } + s.options.order_type = so.order_type so.mode = 'live' so.strategy = c.strategy so.stats = true @@ -56,7 +60,7 @@ module.exports = function container (get, set, clear) { }) } else { - console.log('placing order...') + console.log('placing ' + so.order_type + ' sell order...') } } setInterval(checkOrder, c.order_poll_time) From b8ac748973d9654cb306988f6ee671df74052fbe Mon Sep 17 00:00:00 2001 From: Eigil Bjorgum Date: Tue, 20 Jun 2017 07:26:52 +0200 Subject: [PATCH 18/27] Small changes --- extensions/exchanges/bitstamp/exchange.js | 80 +++++++++-------------- 1 file changed, 32 insertions(+), 48 deletions(-) diff --git a/extensions/exchanges/bitstamp/exchange.js b/extensions/exchanges/bitstamp/exchange.js index b41d75ba7c..a1ad4d6961 100644 --- a/extensions/exchanges/bitstamp/exchange.js +++ b/extensions/exchanges/bitstamp/exchange.js @@ -1,9 +1,7 @@ var Bitstamp = require('bitstamp') - , minimist = require('minimist') , path = require('path') - , colors = require('colors') - , numbro = require('numbro') , Pusher = require('pusher-js/node') + , colors = require('colors') var args = process.argv @@ -20,22 +18,22 @@ var wsOpts = { // before the first call for a trade // As zenbot dont returns the currency pair // before the first trade is requested -// it has been neccessary to get it from +// it has been neccessary to get it from // the command line arguments args.forEach(function(value) { - if (value.match(/bitstamp|BITSTAMP/)) { - var p = value.split('.')[1] - var prod = p.split('-')[0] + p.split('-')[1] - var pair = prod.toLowerCase() - if (!wsOpts.pairOk) { - if (pair !== 'btcusd') { - wsOpts.trades.channel = 'live_trades_' + pair - wsOpts.quotes.channel = 'order_book_' + pair - } - wsOpts.currencyPair = pair - wsOpts.pairOk = true + if (value.toLowerCase().match(/bitstamp/)) { + var p = value.split('.')[1] + var prod = p.split('-')[0] + p.split('-')[1] + var pair = prod.toLowerCase() + if (!wsOpts.pairOk) { + if (pair !== 'btcusd') { + wsOpts.trades.channel = 'live_trades_' + pair + wsOpts.quotes.channel = 'order_book_' + pair } - } + wsOpts.currencyPair = pair + wsOpts.pairOk = true + } + } }) function joinProduct (product_id) { @@ -44,9 +42,7 @@ function joinProduct (product_id) { module.exports = function container (get, set, clear) { - var c = get('conf') - var defs = require('./conf-sample') try { c.bitstamp = require('./conf') @@ -54,13 +50,7 @@ module.exports = function container (get, set, clear) { catch (e) { c.bitstamp = {} } - Object.keys(defs).forEach(function (k) { - if (typeof c.bitstamp[k] === 'undefined') { - c.bitstamp[k] = defs[k] - } - }) - //console.log(c.bitstamp) function authedClient () { if (c.bitstamp.key && c.bitstamp.key !== 'YOUR-API-KEY') { return new Bitstamp(c.bitstamp.key, c.bitstamp.secret, c.bitstamp.client_id) @@ -68,7 +58,6 @@ module.exports = function container (get, set, clear) { throw new Error('\nPlease configure your Bitstamp credentials in ' + path.resolve(__dirname, 'conf.js')) } - //*************************************************** // // The websocket functions // @@ -80,7 +69,7 @@ module.exports = function container (get, set, clear) { } else { this.opts = { encrypted: true, - } + } } this.client = new Pusher(BITSTAMP_PUSHER_KEY, { @@ -132,7 +121,7 @@ module.exports = function container (get, set, clear) { size: 0, price: 0, side: '' - } + } ] var wsTrades = new Bitstamp_WS({ @@ -170,14 +159,12 @@ module.exports = function container (get, set, clear) { var ret = {} var res = err.toString().split(':',2) ret.status = res[1] - var ret = new Error(ret.status ) - return ret - } else { + return new Error(ret.status) + } else { if (body.error) { - var ret = new Error('\nError: ' + body.error) - return ret + return new Error('\nError: ' + body.error) } else { - return body + return body } } } @@ -185,11 +172,11 @@ module.exports = function container (get, set, clear) { function retry (method, args) { var to = args.wait if (method !== 'getTrades') { - console.error(('\nBitstamp API is not answering! unable to call ' + method + ',OB retrying in ' + args.wait + 's').red) + console.error(('\nBitstamp API is not answering! unable to call ' + method + ',OB retrying in ' + to + 's').red) } setTimeout(function () { exchange[method].apply(exchange, args) - }, args.wait * 1000) + }, to * 1000) } var exchange = { @@ -198,20 +185,18 @@ module.exports = function container (get, set, clear) { makerFee: 0.25, takerFee: 0.25, - getProducts: function (opts) { + getProducts: function () { return require('./products.json') }, //----------------------------------------------------- - // Public API functions + // Public API functions // getQuote() and getTrades are using Bitstamp websockets // The data is not done by calling the interface function, // but rather pulled from the "wstrades" and "wsquotes" JSOM objects // Those objects are populated by the websockets event handlers getTrades: function (opts, cb) { - var currencyPair = joinProduct(opts.product_id).toLowerCase() - var args = { wait: 2, // Seconds product_id: wsOpts.currencyPair @@ -224,7 +209,7 @@ module.exports = function container (get, set, clear) { args.after = opts.to } -  if (typeof wstrades.time == undefined) return retry('getTrades', args) + if (typeof wstrades.time == undefined) return retry('getTrades', args) var t = wstrades var trades = t.map(function (trade) { return (trade) @@ -238,7 +223,7 @@ module.exports = function container (get, set, clear) { wait: 2, // Seconds currencyPair: wsOpts.currencyPair } -  if (typeof wsquotes.bid == undefined) return retry('getQuote', args ) + if (typeof wsquotes.bid == undefined) return retry('getQuote', args ) cb(null, wsquotes) }, @@ -248,13 +233,13 @@ module.exports = function container (get, set, clear) { getBalance: function (opts, cb) { var client = authedClient() -   client.balance(null, function (err, body) { + client.balance(null, function (err, body) { body = statusErr(err,body) var balance = {asset: 0, currency: 0} balance.currency = body[opts.currency.toLowerCase() + '_available'] balance.asset = body[opts.asset.toLowerCase() + '_available'] - balance.currency_hold = 0 - balance.asset_hold = 0 + balance.currency_hold = 0 + balance.asset_hold = 0 cb(null, balance) }) }, @@ -279,10 +264,10 @@ module.exports = function container (get, set, clear) { var client = authedClient() var currencyPair = joinProduct(opts.product_id).toLowerCase() if (typeof opts.order_type === 'undefined' ) { - opts.order_type = 'maker' + opts.order_type = 'maker' } if (opts.order_type === 'maker') { - // Fix maker? + // Fix maker? client.buy(currencyPair, opts.size, opts.price, false, function (err, body) { body = statusErr(err,body) cb(null, body) @@ -299,7 +284,7 @@ module.exports = function container (get, set, clear) { var client = authedClient() var currencyPair = joinProduct(opts.product_id).toLowerCase() if (typeof opts.order_type === 'undefined' ) { - opts.order_type = 'maker' + opts.order_type = 'maker' } if (opts.order_type === 'maker') { client.sell(currencyPair, opts.size, opts.price, false, function (err, body) { @@ -329,4 +314,3 @@ module.exports = function container (get, set, clear) { } return exchange } - From 6099841e45bf5b48fb2f08c6c733fd406e2e0175 Mon Sep 17 00:00:00 2001 From: Eigil Bjorgum Date: Wed, 21 Jun 2017 20:23:49 +0200 Subject: [PATCH 19/27] Changes to package.json and exchange.js --- extensions/exchanges/bitstamp/exchange.js | 114 +++++++++++--------- extensions/exchanges/bitstamp/products.json | 27 +++++ package.json | 2 + syncfromCarlos.sh | 8 ++ 4 files changed, 99 insertions(+), 52 deletions(-) create mode 100644 syncfromCarlos.sh diff --git a/extensions/exchanges/bitstamp/exchange.js b/extensions/exchanges/bitstamp/exchange.js index a1ad4d6961..64fe1a81e1 100644 --- a/extensions/exchanges/bitstamp/exchange.js +++ b/extensions/exchanges/bitstamp/exchange.js @@ -19,7 +19,7 @@ var wsOpts = { // As zenbot dont returns the currency pair // before the first trade is requested // it has been neccessary to get it from -// the command line arguments +// t:he command line arguments args.forEach(function(value) { if (value.toLowerCase().match(/bitstamp/)) { var p = value.split('.')[1] @@ -44,13 +44,6 @@ function joinProduct (product_id) { module.exports = function container (get, set, clear) { var c = get('conf') - try { - c.bitstamp = require('./conf') - } - catch (e) { - c.bitstamp = {} - } - function authedClient () { if (c.bitstamp.key && c.bitstamp.key !== 'YOUR-API-KEY') { return new Bitstamp(c.bitstamp.key, c.bitstamp.secret, c.bitstamp.client_id) @@ -87,6 +80,20 @@ module.exports = function container (get, set, clear) { this.subscribe() } + Bitstamp.prototype.tradeDaily = function(direction, market, amount, price, callback) { + this._post(market, direction, callback, { + amount: amount, + price: price, + daily_order: true + }); + } + + Bitstamp.prototype.tradeMarket = function(direction, market, amount, callback) { + this._post(market, direction + '/market', callback, { + amount: amount, + }); + } + var util = require('util') var EventEmitter = require('events').EventEmitter util.inherits(Bitstamp_WS, EventEmitter) @@ -106,7 +113,6 @@ module.exports = function container (get, set, clear) { if(this.bound[name]) return function noop() {} this.bound[name] = true - return function(e) { this.emit(name, e) }.bind(this) @@ -134,14 +140,14 @@ module.exports = function container (get, set, clear) { evType: 'data' }) - wsTrades.on('data', function(data) { + wsQuotes.on('data', function(data) { wsquotes = { bid: data.bids[0][0], ask: data.asks[0][0] } }) - wsQuotes.on('trade', function(data) { + wsTrades.on('trade', function(data) { wstrades.push( { trade_id: data.id, time: Number(data.timestamp) * 1000, @@ -149,7 +155,7 @@ module.exports = function container (get, set, clear) { price: data.price, side: data.type === 0 ? 'buy' : 'sell' }) - if (wstrades.length > 30) wstrades.splice(0,10) + if (wstrades.length > 30) wstrades.splice(0,10) }) //*************************************************** @@ -159,6 +165,7 @@ module.exports = function container (get, set, clear) { var ret = {} var res = err.toString().split(':',2) ret.status = res[1] +//console.log('statusErr:\n', ret) return new Error(ret.status) } else { if (body.error) { @@ -179,6 +186,8 @@ module.exports = function container (get, set, clear) { }, to * 1000) } + var orders = {} + var exchange = { name: 'bitstamp', historyScan: false, @@ -191,7 +200,7 @@ module.exports = function container (get, set, clear) { //----------------------------------------------------- // Public API functions - // getQuote() and getTrades are using Bitstamp websockets + // getQuote() and getTrades() are using Bitstamp websockets // The data is not done by calling the interface function, // but rather pulled from the "wstrades" and "wsquotes" JSOM objects // Those objects are populated by the websockets event handlers @@ -201,20 +210,11 @@ module.exports = function container (get, set, clear) { wait: 2, // Seconds product_id: wsOpts.currencyPair } - - if (opts.from) { - args.before = opts.from - } - else if (opts.to) { - args.after = opts.to - } - if (typeof wstrades.time == undefined) return retry('getTrades', args) var t = wstrades var trades = t.map(function (trade) { return (trade) }) - cb(null, trades) }, @@ -245,64 +245,74 @@ module.exports = function container (get, set, clear) { }, cancelOrder: function (opts, cb) { + var func_args = [].slice.call(arguments) var client = authedClient() client.cancel_order(opts.order_id, function (err, body) { body = statusErr(err,body) + if (body.status === 'error') { + return retry('cancelOrder', func_args, err) + } cb() }) }, - cancelOrders: function (opts, cb) { - var client = authedClient() - client.cancel_all_orders(function (err, body) { - body = statusErr(err,body) - cb() - }) - }, - - buy: function (opts, cb) { + trade: function (type,opts, cb) { +//console.log('Trade ' + type + ' options:\n',opts) var client = authedClient() var currencyPair = joinProduct(opts.product_id).toLowerCase() if (typeof opts.order_type === 'undefined' ) { opts.order_type = 'maker' } + // Bitstamp has no "post only" trade type + opts.post_only = false +//opts.order_type = 'taker' if (opts.order_type === 'maker') { - // Fix maker? - client.buy(currencyPair, opts.size, opts.price, false, function (err, body) { + client.tradeDaily(type, currencyPair, opts.size, opts.price, function (err, body) { body = statusErr(err,body) + if (body.status === 'error') { + var order = { status: 'rejected', reject_reason: 'balance' } + return cb(null, order) + } else { + // Statuses: + // 'In Queue', 'Open', 'Finished' + body.status = 'done' + } + orders['~' + body.id] = body cb(null, body) }) } else { - client.buyMarket(currencyPair, opts.size, function (err, body) { + client.tradeMarket(type, currencyPair, opts.size, function (err, body) { body = statusErr(err,body) + if (body.status === 'error') { + var order = { status: 'rejected', reject_reason: 'balance' } + return cb(null, order) + } else { + body.status = 'done' + } + orders['~' + body.id] = body cb(null, body) - }) + }) } }, + buy: function (opts, cb) { + exchange.trade('buy', opts, cb) + }, + sell: function (opts, cb) { - var client = authedClient() - var currencyPair = joinProduct(opts.product_id).toLowerCase() - if (typeof opts.order_type === 'undefined' ) { - opts.order_type = 'maker' - } - if (opts.order_type === 'maker') { - client.sell(currencyPair, opts.size, opts.price, false, function (err, body) { - body = statusErr(err,body) - cb(null, body) - }) - } else { - client.sellMarket(currencyPair, opts.size, function (err, body) { - body = statusErr(err,body) - cb(null, body) - }) - } + exchange.trade('sell', opts, cb) }, getOrder: function (opts, cb) { + var func_args = [].slice.call(arguments) var client = authedClient() - client.getOrder(opts.order_id, function (err, body) { + client.order_status(opts.order_id, function (err, body) { body = statusErr(err,body) + if (body.status === 'error') { + body = orders['~' + opts.order_id] + body.status = 'done' + body.done_reason = 'canceled' + } cb(null, body) }) }, diff --git a/extensions/exchanges/bitstamp/products.json b/extensions/exchanges/bitstamp/products.json index 837c65058d..3219a14b52 100644 --- a/extensions/exchanges/bitstamp/products.json +++ b/extensions/exchanges/bitstamp/products.json @@ -26,6 +26,33 @@ "increment": "0.01", "label": "EUR/USD" }, + { + "id": "LTCUSD", + "asset": "LTC", + "currency": "USD", + "min_size": "0.01", + "max_size": "1000000", + "increment": "0.01", + "label": "LTC/USD" + }, + { + "id": "LTCEUR", + "asset": "LTC", + "currency": "EUR", + "min_size": "0.01", + "max_size": "1000000", + "increment": "0.01", + "label": "LTC/EUR" + }, + { + "id": "LTCBTC", + "asset": "LTC", + "currency": "BTC", + "min_size": "0.01", + "max_size": "1000000", + "increment": "0.00001", + "label": "LTC/BTC" + }, { "id": "XRPUSD", "asset": "XRP", diff --git a/package.json b/package.json index 28fed8df5a..c70614a136 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,9 @@ "zenbot": "./zenbot.sh" }, "dependencies": { + "autobahn": "^17.5.2", "bitfinex-api-node": "^1.0.1", + "bitstamp": "^1.0.1", "bl": "^1.2.1", "codemap": "^1.3.1", "colors": "^1.1.2", diff --git a/syncfromCarlos.sh b/syncfromCarlos.sh new file mode 100644 index 0000000000..137e49d264 --- /dev/null +++ b/syncfromCarlos.sh @@ -0,0 +1,8 @@ +#!/bin/sh + +#cd zenbot-Eigil +#username tuxitor + +git checkout master +git pull https://github.com/carlos8f/zenbot.git master +git push origin master From ad42640e26c93922e9f1e6dee73fc2a0a407c314 Mon Sep 17 00:00:00 2001 From: tuxitor Date: Wed, 21 Jun 2017 20:39:27 +0200 Subject: [PATCH 20/27] Delete syncfromCarlos.sh --- syncfromCarlos.sh | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 syncfromCarlos.sh diff --git a/syncfromCarlos.sh b/syncfromCarlos.sh deleted file mode 100644 index 137e49d264..0000000000 --- a/syncfromCarlos.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/sh - -#cd zenbot-Eigil -#username tuxitor - -git checkout master -git pull https://github.com/carlos8f/zenbot.git master -git push origin master From 8919ca4c9fe910ff53f1d132e4fa4e56198a4ee0 Mon Sep 17 00:00:00 2001 From: Eigil Bjorgum Date: Wed, 21 Jun 2017 21:47:57 +0200 Subject: [PATCH 21/27] Fixed indenting --- commands/buy.js | 6 +-- commands/sell.js | 6 +-- extensions/exchanges/bitstamp/exchange.js | 51 +++++++++++------------ package.json | 2 +- 4 files changed, 31 insertions(+), 34 deletions(-) diff --git a/commands/buy.js b/commands/buy.js index 69752f6780..c3f3917807 100644 --- a/commands/buy.js +++ b/commands/buy.js @@ -32,9 +32,9 @@ module.exports = function container (get, set, clear) { if (!so.order_type in order_types || !so.order_type) { so.order_type = 'maker' } else { - so.order_type = so.taker ? 'taker' : 'maker' - so.order_type === 'taker' ? delete so.taker : delete so.maker - } + so.order_type = so.taker ? 'taker' : 'maker' + so.order_type === 'taker' ? delete so.taker : delete so.maker + } s.options.order_type = so.order_type so.mode = 'live' so.strategy = c.strategy diff --git a/commands/sell.js b/commands/sell.js index 2affc7bbff..d4f079f950 100644 --- a/commands/sell.js +++ b/commands/sell.js @@ -32,9 +32,9 @@ module.exports = function container (get, set, clear) { if (!so.order_type in order_types || !so.order_type) { so.order_type = 'maker' } else { - so.order_type = so.taker ? 'taker' : 'maker' - so.order_type === 'taker' ? delete so.taker : delete so.maker - } + so.order_type = so.taker ? 'taker' : 'maker' + so.order_type === 'taker' ? delete so.taker : delete so.maker + } s.options.order_type = so.order_type so.mode = 'live' so.strategy = c.strategy diff --git a/extensions/exchanges/bitstamp/exchange.js b/extensions/exchanges/bitstamp/exchange.js index 64fe1a81e1..d62ed7fe01 100644 --- a/extensions/exchanges/bitstamp/exchange.js +++ b/extensions/exchanges/bitstamp/exchange.js @@ -155,7 +155,7 @@ module.exports = function container (get, set, clear) { price: data.price, side: data.type === 0 ? 'buy' : 'sell' }) - if (wstrades.length > 30) wstrades.splice(0,10) + if (wstrades.length > 30) wstrades.splice(0,10) }) //*************************************************** @@ -165,7 +165,6 @@ module.exports = function container (get, set, clear) { var ret = {} var res = err.toString().split(':',2) ret.status = res[1] -//console.log('statusErr:\n', ret) return new Error(ret.status) } else { if (body.error) { @@ -179,7 +178,7 @@ module.exports = function container (get, set, clear) { function retry (method, args) { var to = args.wait if (method !== 'getTrades') { - console.error(('\nBitstamp API is not answering! unable to call ' + method + ',OB retrying in ' + to + 's').red) + console.error(('\nBitstamp API is not answering! unable to call ' + method + ', retrying in ' + to + 's').red) } setTimeout(function () { exchange[method].apply(exchange, args) @@ -249,15 +248,14 @@ module.exports = function container (get, set, clear) { var client = authedClient() client.cancel_order(opts.order_id, function (err, body) { body = statusErr(err,body) - if (body.status === 'error') { - return retry('cancelOrder', func_args, err) - } + if (body.status === 'error') { + return retry('cancelOrder', func_args, err) + } cb() }) }, trade: function (type,opts, cb) { -//console.log('Trade ' + type + ' options:\n',opts) var client = authedClient() var currencyPair = joinProduct(opts.product_id).toLowerCase() if (typeof opts.order_type === 'undefined' ) { @@ -265,33 +263,32 @@ module.exports = function container (get, set, clear) { } // Bitstamp has no "post only" trade type opts.post_only = false -//opts.order_type = 'taker' if (opts.order_type === 'maker') { client.tradeDaily(type, currencyPair, opts.size, opts.price, function (err, body) { body = statusErr(err,body) - if (body.status === 'error') { - var order = { status: 'rejected', reject_reason: 'balance' } + if (body.status === 'error') { + var order = { status: 'rejected', reject_reason: 'balance' } return cb(null, order) - } else { - // Statuses: - // 'In Queue', 'Open', 'Finished' - body.status = 'done' - } + } else { + // Statuses: + // 'In Queue', 'Open', 'Finished' + body.status = 'done' + } orders['~' + body.id] = body cb(null, body) }) - } else { + } else { // order_type = taker client.tradeMarket(type, currencyPair, opts.size, function (err, body) { body = statusErr(err,body) - if (body.status === 'error') { - var order = { status: 'rejected', reject_reason: 'balance' } + if (body.status === 'error') { + var order = { status: 'rejected', reject_reason: 'balance' } return cb(null, order) - } else { - body.status = 'done' - } + } else { + body.status = 'done' + } orders['~' + body.id] = body cb(null, body) - }) + }) } }, @@ -308,11 +305,11 @@ module.exports = function container (get, set, clear) { var client = authedClient() client.order_status(opts.order_id, function (err, body) { body = statusErr(err,body) - if (body.status === 'error') { - body = orders['~' + opts.order_id] - body.status = 'done' - body.done_reason = 'canceled' - } + if (body.status === 'error') { + body = orders['~' + opts.order_id] + body.status = 'done' + body.done_reason = 'canceled' + } cb(null, body) }) }, diff --git a/package.json b/package.json index c70614a136..2ef7d81846 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "zenbot": "./zenbot.sh" }, "dependencies": { - "autobahn": "^17.5.2", + "autobahn": "^17.5.2", "bitfinex-api-node": "^1.0.1", "bitstamp": "^1.0.1", "bl": "^1.2.1", From 13bb7f600f196562c03ba684749cf4a15f3f4033 Mon Sep 17 00:00:00 2001 From: Eigil Bjorgum Date: Thu, 22 Jun 2017 23:31:28 +0200 Subject: [PATCH 22/27] Fix for communication hiccups --- extensions/exchanges/bitstamp/exchange.js | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/extensions/exchanges/bitstamp/exchange.js b/extensions/exchanges/bitstamp/exchange.js index d62ed7fe01..1cb13a2980 100644 --- a/extensions/exchanges/bitstamp/exchange.js +++ b/extensions/exchanges/bitstamp/exchange.js @@ -100,7 +100,6 @@ module.exports = function container (get, set, clear) { Bitstamp_WS.prototype.subscribe = function() { - //console.log('wsOpts ==> ', wsOpts) if (wsOpts.pairOk) { this.client.subscribe(wsOpts.trades.channel) this.client.bind(wsOpts.trades.evType, this.broadcast(wsOpts.trades.evType)) @@ -159,6 +158,12 @@ module.exports = function container (get, set, clear) { }) //*************************************************** + + function beep (c) { + for (var i = 0; i < c; i++) { + process.stdout.write("\007") + } + } function statusErr (err, body) { if (typeof body === 'undefined') { @@ -185,6 +190,7 @@ module.exports = function container (get, set, clear) { }, to * 1000) } + var lastBalance = {asset: 0, currency: 0} var orders = {} var exchange = { @@ -231,14 +237,28 @@ module.exports = function container (get, set, clear) { // getBalance: function (opts, cb) { + var args = { + currency: opts.currency.toLowerCase(), + asset: opts.asset.toLowerCase(), + wait: 10 + } var client = authedClient() client.balance(null, function (err, body) { body = statusErr(err,body) + if (body.status === 'error') { + return retry('getBalance', args) + } var balance = {asset: 0, currency: 0} balance.currency = body[opts.currency.toLowerCase() + '_available'] balance.asset = body[opts.asset.toLowerCase() + '_available'] balance.currency_hold = 0 balance.asset_hold = 0 + if (typeof balance.asset == undefined || typeof balance.currency == undefined ) { + console.log('Communication delay, fallback to previous balance') + balance = lastBalance + } else { + lastBalance = balance + } cb(null, balance) }) }, From d319c1c0a0e6c3b54e101c61714b2c041738cc61 Mon Sep 17 00:00:00 2001 From: Eigil Bjorgum Date: Fri, 23 Jun 2017 14:07:15 +0200 Subject: [PATCH 23/27] Fix the 5 figures issue in Bitfinex --- extensions/exchanges/bitfinex/exchange.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/exchanges/bitfinex/exchange.js b/extensions/exchanges/bitfinex/exchange.js index 0f824108eb..a79b953775 100644 --- a/extensions/exchanges/bitfinex/exchange.js +++ b/extensions/exchanges/bitfinex/exchange.js @@ -148,7 +148,7 @@ module.exports = function container (get, set, clear) { var is_postonly = opts.post_only var params = { symbol, - amount, + amount.toPrecision(5), price, exchange, side, @@ -199,7 +199,7 @@ module.exports = function container (get, set, clear) { var is_postonly = opts.post_only var params = { symbol, - amount, + amount.toPrecision(5), price, exchange, side, From 2b93e4836b0eaf4c9d119f7c42a8ddaef271f212 Mon Sep 17 00:00:00 2001 From: Eigil Bjorgum Date: Fri, 23 Jun 2017 14:50:00 +0200 Subject: [PATCH 24/27] Fix the fix for the Bitfinex 5 figure limit issue --- extensions/exchanges/bitfinex/exchange.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/extensions/exchanges/bitfinex/exchange.js b/extensions/exchanges/bitfinex/exchange.js index a79b953775..64e066529c 100644 --- a/extensions/exchanges/bitfinex/exchange.js +++ b/extensions/exchanges/bitfinex/exchange.js @@ -139,7 +139,7 @@ module.exports = function container (get, set, clear) { opts.post_only = true } var symbol = joinProduct(opts.product_id) - var amount = opts.size + var amount = opts.size.toPrecision(5) var price = opts.price var exchange = 'bitfinex' var side = 'buy' @@ -148,7 +148,7 @@ module.exports = function container (get, set, clear) { var is_postonly = opts.post_only var params = { symbol, - amount.toPrecision(5), + amount, price, exchange, side, @@ -190,7 +190,7 @@ module.exports = function container (get, set, clear) { opts.post_only = true } var symbol = joinProduct(opts.product_id) - var amount = opts.size + var amount = opts.size.toPrecision(5) var price = opts.price var exchange = 'bitfinex' var side = 'sell' @@ -199,7 +199,7 @@ module.exports = function container (get, set, clear) { var is_postonly = opts.post_only var params = { symbol, - amount.toPrecision(5), + amount, price, exchange, side, From 503f4dd5e488fdd07e19d9817c2ef490b1724859 Mon Sep 17 00:00:00 2001 From: tuxitor Date: Fri, 30 Jun 2017 18:57:10 +0200 Subject: [PATCH 25/27] Overview of a Zenbot websocket agent --- docs/Zentalk.md | 152 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 152 insertions(+) create mode 100644 docs/Zentalk.md diff --git a/docs/Zentalk.md b/docs/Zentalk.md new file mode 100644 index 0000000000..a2e39d4a3f --- /dev/null +++ b/docs/Zentalk.md @@ -0,0 +1,152 @@ +## The Zentalk concept + +**Zentalk** is a small set of programs that makes it easy to monitor and make use data from Zenbot without interupting its operation. +Most notably this is the start options, trades, periods and a lot more. The most important parts are the **talker** program +that enables *websockets* (*WS*) on **Zenbot** and two client programs to make use of the data from **Zenbot**. +The two programs are **zentalk** and **zenout**. The first one is a fullblown *websocket* CLI program to inspect the data from **Zenbot**. +The other one is a lightweight *websocket* streaming client which can *subscribe* to data objects from **Zenbot** for use in other programs. +With **zenout** as an example one can do some simple node programming to use the output as anything thinkable. +Here are some examples: + + - a *messaging bot* which can send trading events + - write data that later can be used for statistics or graphing + - producing data for a web front end + - and a lot of other stuff + +### talker + +The **talker** program is a leightweight *wesocket* server loosely connected to **Zenbot**. +Some small modifications are necessary to the **lib/engine.js** program to get useful data from the system. +The modifications are kept small to get a slim footprint into the program. Some small modifications are also done in +**conf-sample.js** to get defaut TCP ports for the first connected client. Spesification of TCP ports to +additional clients are done on the command line when starting another instance of **Zenbot**. +To achieve this a modification is done on the *commands/trade.js* to pick up the ports for the clients. +A pair of ports are used, one for **zentalk** and one for **zenout** + +```--talk_port ``` for **zenout** and ```--command_port ``` for **zentalk** + +If not using the default ports, **Zenbot** should be invoked like this: + +```zenbot trade --talk_port 8080 --command_port 8081 ``` + +### zentalk + +The **zentalk** program connects to a TCP port has a comand line interface with a few simple commands to control its operation. +It has a help command to show the available commands. The help command gives this output: +``` +> help + Usage: get + Objects are: + who + balance + product + period + strat + quote + status + trades +``` +The data is delevered as beautified *JSON* data. Here are a some examples: +``` +> get options + { + "paper": true, + "reset_profit": true, + "talk_port": 8082, + "command_port": 8083, + "strategy": "trend_ema", + "sell_stop_pct": 0, + "buy_stop_pct": 0, + "profit_stop_enable_pct": 0, + "profit_stop_pct": 1, + "max_slippage_pct": 5, + "buy_pct": 99, + "sell_pct": 99, + "order_adjust_time": 30000, + "max_sell_loss_pct": 25, + "order_poll_time": 5000, + "markup_pct": 0, + "order_type": "maker", + "poll_trades": 30000, + "currency_capital": 1000, + "asset_capital": 0, + "rsi_periods": 14, + "avg_slippage_pct": 0.045, + "stats": true, + "mode": "paper", + "selector": "bitfinex.XRP-USD", + "period": "2m", + "min_periods": 52, + "trend_ema": 26, + "neutral_rate": "auto", + "oversold_rsi_periods": 14, + "oversold_rsi": 10 +} +> get period + { + "period_id": "2m12490313", + "size": "2m", + "time": 1498837560000, + "open": 0.255, + "high": 0.25502, + "low": 0.255, + "close": 0.25502, + "volume": 3160.1400000000003, + "close_time": 1498837618000, + "trend_ema": 0.25246624412245805, + "oversold_rsi_avg_gain": 0.00038205180780631186, + "oversold_rsi_avg_loss": 0.00018113157638666248, + "oversold_rsi": 68, + "trend_ema_rate": 0.08098743204999516, + "trend_ema_stddev": 0.016211589445806786, + "id": "ff7f935e", + "selector": "bitfinex.XRP-USD", + "session_id": "51b6adf3", + "rsi_avg_gain": 0.00038205180780631186, + "rsi_avg_loss": 0.00018113157638666248, + "rsi": 68 +} +> get balance + { + "asset": "3901.42011835", + "currency": "11.11735266" +} +> get trades + [ + { + "time": 1498836575000, + "execution_time": 61000, + "slippage": 0.0004499802449624344, + "type": "buy", + "size": "3911.49743185", + "fee": 3.90532544379, + "price": "0.25321389", + "order_type": "maker", + "id": "74db29b9", + "selector": "bitfinex.XRP-USD", + "session_id": "51b6adf3", + "mode": "paper" + } +] +> +``` + +### zenout + +Basicly the **zenout** program delivers the same data as the **zentalk** program. +The difference is the invocation and the form of the output which is *raw JSON*. +It is invoke from the command line with host: port as parameters. +``` +$ ./zentalk -c localhost:8083 +``` +However, to get something useful from the progran you need to subscribe to data +``` +$ ./zentalk -c localhost:8083 --sub lastTrade +``` +With this option you will get the last trade from *Zenbot*. +It is possible to stop the client and start it again withot losing the subscription, meaning you can start it without a new subscription. +If you want to get another set of data, it is wise to unsubscribe the previous subscription. This can be done in one operation. +``` +./zentalk -c localhost:8083 --unsub lastTrade --sub period +``` + From 5b0c01ebb8690c2483db27af18bf9a2793343eb2 Mon Sep 17 00:00:00 2001 From: Eigil Bjorgum Date: Fri, 30 Jun 2017 19:37:07 +0200 Subject: [PATCH 26/27] Adding program and some file updates for the Zentalk concept --- commands/trade.js | 7 +- conf-sample.js | 5 ++ lib/_codemap.js | 5 +- lib/engine.js | 47 +++++++++++ package.json | 1 + zenout.sh | 128 ++++++++++++++++++++++++++++ zentalk.sh | 211 ++++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 399 insertions(+), 5 deletions(-) create mode 100755 zenout.sh create mode 100755 zentalk.sh diff --git a/commands/trade.js b/commands/trade.js index 2e568024f5..2ed30964ab 100644 --- a/commands/trade.js +++ b/commands/trade.js @@ -36,6 +36,8 @@ module.exports = function container (get, set, clear) { .option('--poll_trades ', 'poll new trades at this interval in ms', Number, c.poll_trades) .option('--disable_stats', 'disable printing order stats') .option('--reset_profit', 'start new profit calculation from 0') + .option('--talk_port ', 'TCP port for receiving messages', Number, c.talk_port) + .option('--command_port ', 'TCP port to talk through', Number, c.command_port) .option('--debug', 'output detailed debug info') .action(function (selector, cmd) { var raw_opts = minimist(process.argv) @@ -64,13 +66,13 @@ module.exports = function container (get, set, clear) { console.error('cannot trade ' + so.selector + ': exchange not implemented') process.exit(1) } + var engine = get('lib.engine')(s) var order_types = ['maker', 'taker'] if (!so.order_type in order_types || !so.order_type) { so.order_type = 'maker' } - var db_cursor, trade_cursor var query_start = tb().resize(so.period).subtract(so.min_periods * 2).toMilliseconds() var days = Math.ceil((new Date().getTime() - query_start) / 86400000) @@ -93,7 +95,6 @@ module.exports = function container (get, set, clear) { var my_trades_size = 0 var my_trades = get('db.my_trades') var periods = get('db.periods') - console.log('fetching pre-roll data:') var backfiller = spawn(path.resolve(__dirname, '..', 'zenbot.sh'), ['backfill', so.selector, '--days', days]) backfiller.stdout.pipe(process.stdout) @@ -119,7 +120,7 @@ module.exports = function container (get, set, clear) { get('db.trades').select(opts, function (err, trades) { if (err) throw err if (!trades.length) { - console.log('---------------------------- STARTING ' + so.mode.toUpperCase() + ' TRADING ----------------------------') + console.log('---------------------------- STARTING '+ so.mode.toUpperCase() + ' TRADING WITH "' + so.order_type + '" ORDERS ----------------------------') if (so.mode === 'paper') { console.log('!!! Paper mode enabled. No real trades are performed until you remove --paper from the startup command.') } diff --git a/conf-sample.js b/conf-sample.js index daa0c66fd1..f56646e449 100644 --- a/conf-sample.js +++ b/conf-sample.js @@ -10,6 +10,11 @@ c.mongo.password = null // when using mongodb replication, i.e. when running a mongodb cluster, you can define your replication set here; when you are not using replication (most of the users), just set it to `null` (default). c.mongo.replicaSet = null +// Default ports for the Zentalk concept, one for the *zentalk* program +// and one for the **zenout** program +c.talk_port = 3010 +c.command_port = 3011 + // default selector. only used if omitting [selector] argument from a command. c.selector = 'gdax.BTC-USD' // name of default trade strategy diff --git a/lib/_codemap.js b/lib/_codemap.js index 36ef498444..494c735ec1 100644 --- a/lib/_codemap.js +++ b/lib/_codemap.js @@ -7,5 +7,6 @@ module.exports = { 'normalize-selector': require('./normalize-selector'), 'rsi': require('./rsi'), 'srsi': require('./srsi'), - 'stddev': require('./stddev') -} \ No newline at end of file + 'stddev': require('./stddev'), + 'talker': require('./talker.js') +} diff --git a/lib/engine.js b/lib/engine.js index d2f99861df..5fae1333ce 100644 --- a/lib/engine.js +++ b/lib/engine.js @@ -6,6 +6,7 @@ var tb = require('timebucket') , series = require('run-series') , abbreviate = require('number-abbreviate') , readline = require('readline') + , talker = require('../lib/talker') var nice_errors = new RegExp(/(slippage protection|loss protection)/) @@ -47,6 +48,15 @@ module.exports = function container (get, set, clear) { console.error(s_copy) } + //============================================= + // WbSocket objects needed for WS operation + var zenTalk = talker.update(s) + //var wsCommand = zenTalk.wsCommand + var wsTalker = zenTalk.wsTalker + var lastPeriod = '' + + //============================================= + s.ctx = { option: function (name, desc, type, def) { if (typeof so[name] === 'undefined') { @@ -169,6 +179,23 @@ module.exports = function container (get, set, clear) { } function syncBalance (cb) { + //========================================================= + // Experimental WS interface to show and modify params + // Access with ./zentalk --connect + //========================================================= + //console.log(s) + zenTalk = talker.update(s) + if (zenTalk.subscribed.period) { + if (s.period.period_id !== lastPeriod) { + var period = {'period': s.period} + var msg = JSON.stringify(period) + '\n\n' + wsTalker.emit('transmit',msg) + lastPeriod = s.period.period_id + } + } + //========================================================= */ + + if (so.mode !== 'live') { return cb() } @@ -547,6 +574,16 @@ module.exports = function container (get, set, clear) { price: price, order_type: so.order_type } + + //=============================================== + // + if (zenTalk.subscribed.lastTrade) { + var trade = {'lastTrade': my_trade} + var msg = JSON.stringify(trade) + '\n\n' + wsTalker.emit('transmit',msg) + } + //=============================================== */ + s.my_trades.push(my_trade) if (so.stats) { console.log(('\nbuy order completed at ' + moment(trade.time).format('YYYY-MM-DD HH:mm:ss') + ':\n\n' + fa(my_trade.size) + ' at ' + fc(my_trade.price) + '\ntotal ' + fc(my_trade.size * my_trade.price) + '\n' + n(my_trade.slippage).format('0.0000%') + ' slippage (orig. price ' + fc(s.buy_order.orig_price) + ')\nexecution: ' + moment.duration(my_trade.execution_time).humanize() + '\n').cyan) @@ -595,6 +632,16 @@ module.exports = function container (get, set, clear) { price: price, order_type: so.order_type } + + //=============================================== + // + if (zenTalk.subscribed.lastTrade) { + var trade = {'lastTrade': my_trade} + var msg = JSON.stringify(trade) + '\n\n' + wsTalker.emit('transmit',msg) + } + //=============================================== */ + s.my_trades.push(my_trade) if (so.stats) { console.log(('\nsell order completed at ' + moment(trade.time).format('YYYY-MM-DD HH:mm:ss') + ':\n\n' + fa(my_trade.size) + ' at ' + fc(my_trade.price) + '\ntotal ' + fc(my_trade.size * my_trade.price) + '\n' + n(my_trade.slippage).format('0.0000%') + ' slippage (orig. price ' + fc(s.sell_order.orig_price) + ')\nexecution: ' + moment.duration(my_trade.execution_time).humanize() + '\n').cyan) diff --git a/package.json b/package.json index 2ef7d81846..b517f72ce1 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "number-abbreviate": "^2.0.0", "numbro": "git+https://github.com/carlos8f/numbro.git", "poloniex.js": "0.0.7", + "pusher-js": "^4.1.0", "run-parallel": "^1.1.6", "run-series": "^1.1.4", "semver": "^5.3.0", diff --git a/zenout.sh b/zenout.sh new file mode 100755 index 0000000000..ff2ec3cce7 --- /dev/null +++ b/zenout.sh @@ -0,0 +1,128 @@ +#!/usr/bin/env node + +/* + * A node.js websocket client for + * data extraction from Zenbot + */ + +var program = require('commander') + , read = require('read') + , events = require('events') + , WebSocket = require('ws') + , fs = require('fs') + +function appender(xs) { + xs = xs || [] + + return function (x) { + xs.push(x) + return xs + } +} + +function into(obj, kvals) { + kvals.forEach(function (kv) { + obj[kv[0]] = kv[1] + }) + + return obj +} + +function splitOnce(sep, str) { // sep can be either String or RegExp + var tokens = str.split(sep) + return [tokens[0], str.replace(sep, '').substr(tokens[0].length)] +} + +program +// .version(version) + .usage('[options] (--connect )') + .option('-c, --connect ', 'connect to a websocket server') + .option('-p, --protocol ', 'optional protocol version') + .option('-o, --origin ', 'optional origin') + .option('--host ', 'optional host') + .option('-s, --subprotocol ', 'optional subprotocol') + .option('-n, --no-check', 'Do not check for unauthorized certificates') + .option('-H, --header ', 'Set an HTTP header. Repeat to set multiple.', appender(), []) + .option('--auth ', 'Add basic HTTP authentication header.') + .option('--ca ', 'Specify a Certificate Authority.') + .option('--cert ', 'Specify a Client SSL Certificate.') + .option('--key ', 'Specify a Client SSL Certificate\'s key.') + .option('--passphrase [passphrase]', 'Specify a Client SSL Certificate Key\'s passphrase.' + + '\n\t\t\t\t If you don\'t provide a value, it will be prompted for.') + .option('--sub ', 'Subscribes to a specific object or object list' + + '\n\t\t\t\t (comma separated list with no space)') + .option('--unsub ', 'Unsubscribes to a specific object or object list' + + '\n\t\t\t\t (comma separated list with no space)') + .parse(process.argv) + +if (program.listen && program.connect) { + console.error('\033[33merror: use either --connect\033[39m') + process.exit(-1) +} else if (program.connect) { + var options = {} + var cont = function () { + + if (program.protocol) options.protocolVersion = +program.protocol + if (program.origin) options.origin = program.origin + if (program.subprotocol) options.protocol = program.subprotocol + if (program.host) options.host = program.host + if (!program.check) options.rejectUnauthorized = program.check + if (program.ca) options.ca = fs.readFileSync(program.ca) + if (program.cert) options.cert = fs.readFileSync(program.cert) + if (program.key) options.key = fs.readFileSync(program.key) + if (program.sub) options.sub = program.sub + if (program.unsub) options.unsub = program.unsub + + var headers = into({}, (program.header || []).map(function split(s) { + return splitOnce(':', s) + })) + + if (program.auth) { + headers.Authorization = 'Basic '+ new Buffer(program.auth).toString('base64') + } + + var connectUrl = program.connect + if (!connectUrl.match(/\w+:\/\/.*$/i)) { + connectUrl = 'ws://' + connectUrl + } + + options.headers = headers + var ws = new WebSocket(connectUrl, options) + + ws.on('open', function open() { + if (program.sub) ws.send('sub ' + program.sub) + if (program.unsub) ws.send('unsub ' + program.unsub) + }).on('close', function close() { + process.exit() + }).on('error', function error(code, description) { + console.log(code + (description ? ' ' + description : '')) + process.exit(-1) + }).on('message', function message(data, flags) { + console.log(data) + }) + + ws.on('close', function close() { + ws.close() + process.exit() + }) + } + + if (program.passphrase === true) { + var readOptions = { + prompt: 'Passphrase: ', + silent: true, + replace: '*' + } + read(readOptions, function(err, passphrase) { + options.passphrase = passphrase + cont() + }) + } else if (typeof program.passphrase === 'string') { + options.passphrase = program.passphrase + cont() + } else { + cont() + } +} else { + program.help() +} diff --git a/zentalk.sh b/zentalk.sh new file mode 100755 index 0000000000..f9c4f8c3f2 --- /dev/null +++ b/zentalk.sh @@ -0,0 +1,211 @@ +#!/usr/bin/env node + +/*! + * ws: a node.js websocket client + * Copyright(c) 2011 Einar Otto Stangvik + * MIT Licensed + */ + +/** + * Module dependencies. + */ + +var program = require('commander') + , readline = require('readline') + , read = require('read') + , events = require('events') + , WebSocket = require('ws') + , util = require('util') + , fs = require('fs') + , tty = require('tty') + +/** + * InputReader - processes console input. + */ +function Console() { + if (!(this instanceof Console)) return new Console() + + this.stdin = process.stdin + this.stdout = process.stdout + + this.readlineInterface = readline.createInterface(this.stdin, this.stdout) + + var self = this + + this.readlineInterface.on('line', function line(data) { + self.emit('line', data) + }).on('close', function close() { + self.emit('close') + }) + + this._resetInput = function() { + self.clear() + } +} + +util.inherits(Console, events.EventEmitter) + +Console.Colors = { + Red: '\033[31m', + Green: '\033[32m', + Yellow: '\033[33m', + Blue: '\033[34m', + Default: '\033[39m' +} + +Console.Types = { + Incoming: '', + Control: '', + Error: 'error: ', +} + +Console.prototype.prompt = function prompt() { + this.readlineInterface.prompt() +} + +Console.prototype.print = function print(type, msg, color) { + if (tty.isatty(1)) { + this.clear() + color = color || Console.Colors.Default + this.stdout.write(color + type + msg + Console.Colors.Default + '\n') + this.prompt() + } else if (type === Console.Types.Incoming) { + this.stdout.write(msg + '\n') + } else { + // is a control message and we're not in a tty... drop it. + } +} + +Console.prototype.clear = function clear() { + if (tty.isatty(1)) { + this.stdout.write('\033[2K\033[E') + } +} + +Console.prototype.pause = function pausing() { + this.stdin.on('keypress', this._resetInput) +} + +Console.prototype.resume = function resume() { + this.stdin.removeListener('keypress', this._resetInput) +} + +function appender(xs) { + xs = xs || [] + + return function (x) { + xs.push(x) + return xs + } +} + +function into(obj, kvals) { + kvals.forEach(function (kv) { + obj[kv[0]] = kv[1] + }) + + return obj +} + +function splitOnce(sep, str) { // sep can be either String or RegExp + var tokens = str.split(sep) + return [tokens[0], str.replace(sep, '').substr(tokens[0].length)] +} + +/** + * The actual application + */ +//var version = require('./package.json').version + +program +// .version(version) + .usage('[options] (--connect )') + .option('-c, --connect ', 'connect to a websocket server') + .option('-p, --protocol ', 'optional protocol version') + .option('-o, --origin ', 'optional origin') + .option('--host ', 'optional host') + .option('-s, --subprotocol ', 'optional subprotocol') + .option('-n, --no-check', 'Do not check for unauthorized certificates') + .option('-H, --header ', 'Set an HTTP header. Repeat to set multiple.', appender(), []) + .option('--auth ', 'Add basic HTTP authentication header.') + .option('--ca ', 'Specify a Certificate Authority.') + .option('--cert ', 'Specify a Client SSL Certificate.') + .option('--key ', 'Specify a Client SSL Certificate\'s key.') + .option('--passphrase [passphrase]', 'Specify a Client SSL Certificate Key\'s passphrase. If you don\'t provide a value, it will be prompted for.') + .parse(process.argv) + +if (program.listen && program.connect) { + console.error('\033[33merror: use either --connect\033[39m') + process.exit(-1) +} else if (program.connect) { + var options = {} + var cont = function () { + var wsConsole = new Console() + + if (program.protocol) options.protocolVersion = +program.protocol + if (program.origin) options.origin = program.origin + if (program.subprotocol) options.protocol = program.subprotocol + if (program.host) options.host = program.host + if (!program.check) options.rejectUnauthorized = program.check + if (program.ca) options.ca = fs.readFileSync(program.ca) + if (program.cert) options.cert = fs.readFileSync(program.cert) + if (program.key) options.key = fs.readFileSync(program.key) + + var headers = into({}, (program.header || []).map(function split(s) { + return splitOnce(':', s) + })) + + if (program.auth) { + headers.Authorization = 'Basic '+ new Buffer(program.auth).toString('base64') + } + + var connectUrl = program.connect + if (!connectUrl.match(/\w+:\/\/.*$/i)) { + connectUrl = 'ws://' + connectUrl + } + + options.headers = headers + var ws = new WebSocket(connectUrl, options) + + ws.on('open', function open() { + wsConsole.print(Console.Types.Control, 'connected (press CTRL+C to quit)', Console.Colors.Green) + wsConsole.on('line', function line(data) { + ws.send(data) + wsConsole.prompt() + }) + }).on('close', function close() { + wsConsole.print(Console.Types.Control, 'disconnected', Console.Colors.Green) + wsConsole.clear() + process.exit() + }).on('error', function error(code, description) { + wsConsole.print(Console.Types.Error, code + (description ? ' ' + description : ''), Console.Colors.Yellow) + process.exit(-1) + }).on('message', function message(data, flags) { + wsConsole.print(Console.Types.Incoming, data, Console.Colors.Blue) + }) + + wsConsole.on('close', function close() { + ws.close() + process.exit() + }) + } + + if (program.passphrase === true) { + var readOptions = { + prompt: 'Passphrase: ', + silent: true, + replace: '*' + } + read(readOptions, function(err, passphrase) { + options.passphrase = passphrase + cont() + }) + } else if (typeof program.passphrase === 'string') { + options.passphrase = program.passphrase + cont() + } else { + cont() + } +} else { + program.help() +} From 568ba50d93df2b89b5682096b750d59fc0ac9cac Mon Sep 17 00:00:00 2001 From: Eigil Bjorgum Date: Sun, 2 Jul 2017 21:21:00 +0200 Subject: [PATCH 27/27] Removed port setting from cmd line options in favor of auto grab --- conf-sample.js | 8 +- lib/engine.js | 11 +- lib/talker.js | 277 +++++++++++++++++++++++++++++++++++++++++++++++++ package.json | 2 + zentalk.sh | 2 + 5 files changed, 290 insertions(+), 10 deletions(-) create mode 100644 lib/talker.js diff --git a/conf-sample.js b/conf-sample.js index f56646e449..42b0509776 100644 --- a/conf-sample.js +++ b/conf-sample.js @@ -10,10 +10,10 @@ c.mongo.password = null // when using mongodb replication, i.e. when running a mongodb cluster, you can define your replication set here; when you are not using replication (most of the users), just set it to `null` (default). c.mongo.replicaSet = null -// Default ports for the Zentalk concept, one for the *zentalk* program -// and one for the **zenout** program -c.talk_port = 3010 -c.command_port = 3011 +// TCP ports used bu the Zentalk concept, +// The program grabs the two first available ports from a specified range of ports, +// one for the *zentalk* program and one for the **zenout** program +c.talker_port_range = {min: 3000, max:3020,retrieve:2} // default selector. only used if omitting [selector] argument from a command. c.selector = 'gdax.BTC-USD' diff --git a/lib/engine.js b/lib/engine.js index 5fae1333ce..38c71820d0 100644 --- a/lib/engine.js +++ b/lib/engine.js @@ -6,6 +6,7 @@ var tb = require('timebucket') , series = require('run-series') , abbreviate = require('number-abbreviate') , readline = require('readline') + // Needed by Zentalk , talker = require('../lib/talker') var nice_errors = new RegExp(/(slippage protection|loss protection)/) @@ -49,12 +50,10 @@ module.exports = function container (get, set, clear) { } //============================================= - // WbSocket objects needed for WS operation + // Zentalk WS init varables var zenTalk = talker.update(s) - //var wsCommand = zenTalk.wsCommand var wsTalker = zenTalk.wsTalker var lastPeriod = '' - //============================================= s.ctx = { @@ -180,7 +179,7 @@ module.exports = function container (get, set, clear) { function syncBalance (cb) { //========================================================= - // Experimental WS interface to show and modify params + // Zentalk WS interface to show (and modify) params // Access with ./zentalk --connect //========================================================= //console.log(s) @@ -576,7 +575,7 @@ module.exports = function container (get, set, clear) { } //=============================================== - // + // Zentalk WS feature if (zenTalk.subscribed.lastTrade) { var trade = {'lastTrade': my_trade} var msg = JSON.stringify(trade) + '\n\n' @@ -634,7 +633,7 @@ module.exports = function container (get, set, clear) { } //=============================================== - // + // Zentalk WS feature if (zenTalk.subscribed.lastTrade) { var trade = {'lastTrade': my_trade} var msg = JSON.stringify(trade) + '\n\n' diff --git a/lib/talker.js b/lib/talker.js new file mode 100644 index 0000000000..6b57618d54 --- /dev/null +++ b/lib/talker.js @@ -0,0 +1,277 @@ + +// Websockets additions +var EventEmitter = require('events').EventEmitter + , WebSocket = require('ws') + , util = require('util') + , port = require('portastic') + , c = require('../conf') + + +function emitter() { + EventEmitter.call(this) +} +util.inherits(emitter, EventEmitter) + + +var lastPeriod = '' +var subscribed = + { + 'balance': false, + 'product': false, + 'period': false, + 'quote': false, + 'status': false, + 'strat': false, + 'trades': false, + 'lastTrade':false + } + + +//======================== +// WebSocket helper functions + +// Show help for WS use +function showHelp(issue) { + var wsObjects = + "\n Objects are:" + + "\n\twho" + + "\n\tbalance" + + "\n\tproduct" + + "\n\tperiod" + + "\n\tstrat" + + "\n\tquote" + + "\n\tstatus" + + "\n\ttrades" + + return 'Usage: get ' + wsObjects +} + +function runStatus() { + var stat = { + "last_period_id": s.last_period_id, + "acted_on_stop": s.acted_on_stop, + "action": s.action, + "signal": s.signal, + "acted_on_trend": s.acted_on_trend, + "trend": s.trend, + "cancel_down": s.cancel_down, + "orig_capital": s.orig_capital, + "orig_price": s.orig_price, + "start_capital": s.start_capital, + "start_price": s.start_price, + "last_signal": s.last_signal, + "quote": s.quote + } + //if (so.debug) console.log(stat) + return stat +} + +function subscribe(data) { + var s = data.split(' ') + sub = s[1] + if (s[0] === 'sub') { + subscribed[sub] = true + } else { + subscribed[sub] = false + } +} + +// Parse command input from client +function getObject(beautify,message) { + var stat = runStatus() + // Global to pick up some variables + var wsObjects = + { + "balance": s.balance, + "options": s.options, + "product": s.product, + "period":s.period, + "quote": s.quote, + "strat": s.strategy, + "trades": s.my_trades, + "status": stat + } + + var args = message.split(" ") + if (args.length === 1) return "At least one argument is needed, try " + var cmd = args[0] + var arg = args[1] + if (beautify) { + return JSON.stringify(wsObjects[arg],false,4) + } else return JSON.stringify(wsObjects[arg]) +} + +function setVar(message) { + var stat = runStatus() + var args = message.split(" ",3) + if (args.length < 3) return "Usage: set