Browse Source

Merge

develop
mark-sch 1 year ago
parent
commit
8b13d75a43
26 changed files with 2844 additions and 511 deletions
  1. +2
    -1
      .env
  2. +0
    -1
      core/log.js
  3. +1
    -0
      docker-compose.yml
  4. +3
    -0
      docker-entrypoint.sh
  5. +3
    -1
      docs/installation/installing_gekko_using_docker.md
  6. +27
    -25
      docs/introduction/supported_exchanges.md
  7. +2
    -0
      docs/strategies/creating_a_strategy.md
  8. +3
    -3
      exchange/orders/order.js
  9. +4
    -0
      exchange/orders/states.js
  10. +132
    -30
      exchange/orders/sticky.js
  11. +15
    -0
      exchange/package-lock.json
  12. +2
    -1
      exchange/package.json
  13. +1818
    -415
      exchange/wrappers/binance-markets.json
  14. +18
    -0
      exchange/wrappers/binance.js
  15. +10
    -2
      exchange/wrappers/coinfalcon.js
  16. +44
    -0
      exchange/wrappers/poloniex.js
  17. +187
    -0
      exchange/wrappers/therocktrading-markets.json
  18. +286
    -0
      exchange/wrappers/therocktrading.js
  19. +1
    -0
      gekko.js
  20. +91
    -0
      importers/exchanges/therocktrading.js
  21. +30
    -23
      package-lock.json
  22. +14
    -0
      plugins.js
  23. +118
    -0
      plugins/blotter.js
  24. +17
    -6
      plugins/postgresql/reader.js
  25. +3
    -3
      plugins/tradingAdvisor/baseTradingMethod.js
  26. +13
    -0
      sample-config.js

+ 2
- 1
.env View File

@ -1,2 +1,3 @@
HOST=localhost
PORT=3000
PORT=3000
USE_SSL=0

+ 0
- 1
core/log.js View File

@ -75,7 +75,6 @@ if(silent) {
Log.prototype.debug = _.noop;
Log.prototype.info = _.noop;
Log.prototype.warn = _.noop;
Log.prototype.error = _.noop;
Log.prototype.write = _.noop;
}

+ 1
- 0
docker-compose.yml View File

@ -11,6 +11,7 @@ services:
environment:
- HOST
- PORT
- USE_SSL
ports: # you can comment this out when using the nginx frontend
- "${PORT}:${PORT}"
## optionally set nginx vars if you wish to frontend it with nginx

+ 3
- 0
docker-entrypoint.sh View File

@ -3,4 +3,7 @@
sed -i 's/127.0.0.1/0.0.0.0/g' /usr/src/app/web/vue/dist/UIconfig.js
sed -i 's/localhost/'${HOST}'/g' /usr/src/app/web/vue/dist/UIconfig.js
sed -i 's/3000/'${PORT}'/g' /usr/src/app/web/vue/dist/UIconfig.js
if [[ "${USE_SSL:-0}" == "1" ]] ; then
sed -i 's/ssl: false/ssl: true/g' /usr/src/app/web/vue/dist/UIconfig.js
fi
exec node gekko "$@"

+ 3
- 1
docs/installation/installing_gekko_using_docker.md View File

@ -20,4 +20,6 @@ $ HOST=mydomain.com PORT=3001 docker-compose up -d
You can now find your gekko instance running on `mydomain.com:3001`.
To see logs: `docker logs -f gekko_gekko_1`. View which dockers are running by executing `docker ps`.
If running behind an SSL-terminating proxy, make sure to set `USE_SSL=1` to tell the Gekko to use the HTTPS and WSS protocols instead of the default HTTP and WS protocols.
To see logs: `docker logs -f gekko_gekko_1`. View which dockers are running by executing `docker ps`.

+ 27
- 25
docs/introduction/supported_exchanges.md View File

@ -6,31 +6,32 @@ Gekko is able to directly communicate with the APIs of a number of exchanges. Ho
- Live Trading: Gekko is able to automatically execute orders (based on the signals of your strategy). This turns Gekko into a trading bot.
- Importing: Gekko is able to retrieve historical market data. This way you can easily get a month of market data over which you can [backtest][1] your strategy.
| Exchange | Monitoring | Live Trading | Importing | Notes |
| -------------------- |:----------:|:------------:|:---------:| ------------------------- |
| [Binance][24] | ✓ | ✓ | ✓ | |
| [Poloniex][2] | ✓ | ✓ | ✓ | |
| [GDAX][3] | ✓ | ✓ | ✓ | |
| [BTCC][4]* | ✓ | ✓ | ✓ | (=BTCChina) |
| [Bitstamp][5]* | ✓ | ✓ | ✕ | |
| [Kraken][6] | ✓ | ✓ | ✓ | |
| [Bitfinex][7] | ✓ | ✓ | ✓ | |
| [Bittrex][8] | ✓ | ✕ | ✕ | API problems ([#2310][26])|
| [coinfalcon][25] | ✓ | ✓ | ✓ | |
| [EXMO][27] | ✓ | ✓ | ✕ | |
| [wex.nz][9]* | ✓ | ✓ | ✕ | |
| [Gemini][10]* | ✓ | ✓ | ✕ | |
| [Okcoin.cn][11]* | ✓ | ✓ | ✕ | China, see [#352][20] |
| [Cex.io][12]* | ✓ | ✕ | ✕ | |
| [BTC Markets][13]* | ✓ | ✓ | ✕ | |
| [Luno][14] | ✓ | ✓ | ✓ | previously BitX |
| [lakeBTC][15]* | ✓ | ✕ | ✕ | |
| [meXBT][16]* | ✓ | ✕ | ✕ | see [here][21] |
| [zaif][17]* | ✓ | ✕ | ✕ | |
| [lakeBTC][18]* | ✓ | ✕ | ✕ | |
| [bx.in.th][19]* | ✓ | ✕ | ✕ | |
| [bitcoin.co.id][22]* | ✓ | ✓ | ✕ | |
| [Quadriga CX][23]* | ✓ | ✓ | ✕ | | |
| Exchange | Monitoring | Live Trading | Importing | Notes |
| --------------------- |:----------:|:------------:|:---------:| ------------------------- |
| [Binance][24] | ✓ | ✓ | ✓ | |
| [Poloniex][2] | ✓ | ✓ | ✓ | |
| [GDAX][3] | ✓ | ✓ | ✓ | |
| [BTCC][4]* | ✓ | ✓ | ✓ | (=BTCChina) |
| [Bitstamp][5]* | ✓ | ✓ | ✕ | |
| [Kraken][6] | ✓ | ✓ | ✓ | |
| [Bitfinex][7] | ✓ | ✓ | ✓ | |
| [Bittrex][8] | ✓ | ✕ | ✕ | API problems ([#2310][26])|
| [coinfalcon][25] | ✓ | ✓ | ✓ | |
| [The Rock Trading][28]| ✓ | ✓ | ✓ | |
| [EXMO][27] | ✓ | ✓ | ✕ | |
| [wex.nz][9]* | ✓ | ✓ | ✕ | |
| [Gemini][10]* | ✓ | ✓ | ✕ | |
| [Okcoin.cn][11]* | ✓ | ✓ | ✕ | China, see [#352][20] |
| [Cex.io][12]* | ✓ | ✕ | ✕ | |
| [BTC Markets][13]* | ✓ | ✓ | ✕ | |
| [Luno][14] | ✓ | ✓ | ✓ | previously BitX |
| [lakeBTC][15]* | ✓ | ✕ | ✕ | |
| [meXBT][16]* | ✓ | ✕ | ✕ | see [here][21] |
| [zaif][17]* | ✓ | ✕ | ✕ | |
| [lakeBTC][18]* | ✓ | ✕ | ✕ | |
| [bx.in.th][19]* | ✓ | ✕ | ✕ | |
| [bitcoin.co.id][22]* | ✓ | ✓ | ✕ | |
| [Quadriga CX][23]* | x | x | ✕ | Exchange is down. | |
*Temporary disabled since 0.6! If you were planning on using this exchange please e-mail me (address at the bottom of this page).
@ -62,3 +63,4 @@ Gekko is able to directly communicate with the APIs of a number of exchanges. Ho
[25]: https://coinfalcon.com/?ref=CFJSQBMXZZDS
[26]: https://github.com/askmike/gekko/pull/2310
[27]: https://exmo.com
[28]: https://www.therocktrading.com/

+ 2
- 0
docs/strategies/creating_a_strategy.md View File

@ -1,5 +1,7 @@
# Creating a strategy
*Have you made your own strategy? Gekko Plus is hosting an official Strategy contest: submit your strategy and have a change to win 0.1 BTC! Read more details on [Gekko Plus](https://app.gekkoplus.com/contest).*
Strategies are the core of Gekko's trading bot. They look at the market and decide what to do based on technical analysis indicators. A single strategy is limited to a single market on a single exchange.
Gekko currently comes with [a couple of strategies](./introduction.md) out of the box. Besides those you can also write your own strategy in javascript. If you want to understand how to create your own strategy you can watch this video or read the tech docs on this page.

+ 3
- 3
exchange/orders/order.js View File

@ -62,17 +62,17 @@ class BaseOrder extends EventEmitter {
rejected(reason) {
this.rejectedReason = reason;
this.emitStatus();
this.status = states.REJECTED;
this.emitStatus();
console.log(new Date, 'sticky rejected', reason)
this.finish();
}
filled(price) {
this.status = states.FILLED;
this.emitStatus();
this.completed = true;
console.log(new Date, 'sticky filled')
this.finish(true);
}

+ 4
- 0
exchange/orders/states.js View File

@ -12,6 +12,10 @@ const states = {
OPEN: 'OPEN',
CHECKING: 'CHECKING',
CHECKED: 'CHECKED',
// the orders below indicate a fully completed order

+ 132
- 30
exchange/orders/sticky.js View File

@ -36,6 +36,9 @@ class StickyOrder extends BaseOrder {
if(_.isFunction(this.api.outbidPrice)) {
this.outbidPrice = this.api.outbidPrice.bind(this.api);
}
this.debug = this.api.name === 'Deribit2';
this.log = m => this.debug && console.log(new Date, m);
}
create(side, rawAmount, params = {}) {
@ -233,13 +236,16 @@ class StickyOrder extends BaseOrder {
delete this.orders[this.id];
// register new order
this.log(`${this.side} old id: ${this.id} new id: ${id}`);
this.id = id;
this.orders[id] = {
price: this.price,
filled: 0
}
this.emit('new order', this.id);
this.emit('movelimit handled', new Date);
this.status = states.OPEN;
this.emitStatus();
@ -247,22 +253,32 @@ class StickyOrder extends BaseOrder {
this.scheduleNextCheck();
}
scheduleNextCheck() {
// remove lock
this.sticking = false;
initiateDefferedAction() {
// check whether we had an action pending
if(this.cancelling) {
return this.cancel();
this.cancel();
return true;
}
if(this.movingLimit) {
return this.moveLimit();
if(this.movingLimit && this.moveLimit()) {
return true;
}
if(this.movingAmount) {
return this.moveAmount();
this.moveAmount();
return true;
}
return false;
}
scheduleNextCheck() {
// remove lock
this.sticking = false;
if(this.initiateDefferedAction()) {
return;
}
// register check
@ -273,12 +289,46 @@ class StickyOrder extends BaseOrder {
checkOrder() {
if(this.completed || this.completing) {
return console.log(new Date, 'checkOrder called on completed/completing order..', this.completed, this.completing);
return console.log(new Date, this.side, 'checkOrder called on completed/completing order..', this.completed, this.completing);
}
if(this.status === states.MOVING) {
return console.log(new Date, this.side, 'refusing to check, in the middle of move');
}
if(this.initiateDefferedAction()) {
console.log(new Date, this.side, 'skipping check logic, better things to do - 0');
return;
}
this.sticking = true;
const checkId = this.id;
this.api.checkOrder(this.id, (err, result) => {
if(!this.debug) {
if(this.ignoreCheckResult) {
this.log(this.side + ' debug ignoring check result');
this.ignoreCheckResult = false;
return;
}
}
if(this.status === states.MOVING) {
this.log(`${this.side} ${this.id} debug ignoring check result - in the middle of move`);
this.ignoreCheckResult = false;
return;
}
if(checkId !== this.id) {
this.log(this.side + ' debug got check on old id ' + checkId + ', ' + this.id);
this.ignoreCheckResult = false;
return;
}
this.status = states.CHECKED;
this.emitStatus();
if(this.handleError(err)) {
console.log(new Date, 'checkOrder error');
return;
@ -297,12 +347,28 @@ class StickyOrder extends BaseOrder {
return;
}
if(this.initiateDefferedAction()) {
console.log(new Date, this.side, 'skipping check ticker logic, better things to do 1');
return;
}
this.api.getTicker((err, ticker) => {
if(this.handleError(err)) {
console.log(new Date, 'getTicker error');
return;
}
if(this.initiateDefferedAction()) {
console.log(new Date, this.side, 'skipping check ticker logic, better things to do 2');
return;
}
if(this.status === states.MOVING) {
this.log(`${this.side} ${this.id} debug ignoring check result - in the middle of move ---- 2`);
this.ignoreCheckResult = false;
return;
}
this.ticker = ticker;
this.emit('ticker', ticker);
@ -310,9 +376,9 @@ class StickyOrder extends BaseOrder {
// note: might be string VS float
if(ticker[bookSide] != this.price) {
return this.move(this.calculatePrice(ticker));
} else {
this.scheduleNextCheck();
}
this.scheduleNextCheck();
});
return;
@ -326,9 +392,9 @@ class StickyOrder extends BaseOrder {
// not open and not executed means it never hit the book
this.createRetry++;
if (this.createRetry > 3) {
this.status = states.REJECTED;
this.emitStatus();
this.finish();
console.log(this.side, this.status, this.id, 'not open not executed!', result);
this.rejected();
throw 'a';
return;
} else {
console.log('Error while opening a new order, will retry in 1 min. Attempt:', this.createRetry);
@ -342,6 +408,10 @@ class StickyOrder extends BaseOrder {
this.emit('fill', this.amount);
this.filled(this.price);
});
this.status = states.CHECKING;
this.emitStatus();
}
// global error handler
@ -400,14 +470,20 @@ class StickyOrder extends BaseOrder {
this.status = states.MOVING;
this.emitStatus();
this.api.cancelOrder(this.id, (err, filled, data) => {
this.log(`${this.side} ${this.id} this.move 1`);
const cancelId = this.id;
this.api.cancelOrder(cancelId, (err, filled, data) => {
this.log(`${this.side} ${this.id} this.move 2`);
if(this.handleError(err)) {
console.log(new Date, 'error move');
return;
}
// it got filled before we could cancel
if(this.handleCancel(filled, data)) {
if(cancelId === this.id && this.handleCancel(filled, data)) {
return;
}
@ -434,6 +510,10 @@ class StickyOrder extends BaseOrder {
if(!limit) {
limit = this.moveLimitTo;
// console.log(new Date, this.side, 'debug resuming move!');
} else {
this.limitRequested = new Date;
this.log(this.side + ' received move limit');
}
if(this.limit === this.roundPrice(limit)) {
@ -445,34 +525,56 @@ class StickyOrder extends BaseOrder {
return false;
}
this.emit('movelimit', new Date);
if(
this.status === states.INITIALIZING ||
this.status === states.SUBMITTED ||
this.status === states.MOVING ||
this.status === states.CHECKING ||
this.sticking
) {
this.moveLimitTo = limit;
this.movingLimit = true;
return true;
if(
!this.api.fillDataOnCancel ||
this.status === states.MOVING ||
this.status === states.SUBMITTED
) {
// console.log(new Date, '[sticky]', this.side, 'skipping because:', this.status);
this.moveLimitTo = limit;
this.movingLimit = true;
return false;
}
}
this.limit = this.roundPrice(limit);
clearTimeout(this.timeout);
this.movingLimit = false;
if(this.side === 'buy' && this.limit < this.price) {
this.sticking = true;
this.move(this.limit);
} else if(this.side === 'sell' && this.limit > this.price) {
const moveBuy = this.side === 'buy' && this.limit < this.price;
const moveSell = this.side === 'sell' && this.limit > this.price;
if(moveBuy || moveSell) {
this.sticking = true;
clearTimeout(this.timeout);
if(this.api.fillDataOnCancel && this.status === states.CHECKING) {
this.log(this.side + 'debug move overwrites running check ' + this.status + ' ' + this.id);
this.ignoreCheckResult = true;
}
this.log(this.side + ' moving ' + this.id);
const timeToMove = new Date - this.limitRequested;
if(timeToMove > 300) {
console.log(new Date, this.side, 'long time to move:', timeToMove);
}
this.move(this.limit);
} else {
this.scheduleNextCheck();
return true;
}
return true;
this.emit('movelimit handled', new Date);
return false;
}
moveAmount(amount) {

+ 15
- 0
exchange/package-lock.json View File

@ -1047,6 +1047,21 @@
"resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
"integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU="
},
"therocktrading": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/therocktrading/-/therocktrading-0.9.0.tgz",
"integrity": "sha512-yHYefoQ05GJsytI6QsAbm3G+cnQta9Q0wC+4etmxDiesWvtSeH87PN3JIpq56UYsUxjn37mgiK2c5oSTK4yRpQ==",
"requires": {
"underscore": "1.4.4"
},
"dependencies": {
"underscore": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.4.4.tgz",
"integrity": "sha1-YaajIBBiKvoHljvzJSA88SI51gQ="
}
}
},
"timed-out": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz",

+ 2
- 1
exchange/package.json View File

@ -36,6 +36,7 @@
"lodash": "^4.17.5",
"moment": "^2.22.1",
"request-promise": "^4.2.2",
"retry": "^0.12.0"
"retry": "^0.12.0",
"therocktrading": "^0.9.0"
}
}

+ 1818
- 415
exchange/wrappers/binance-markets.json
File diff suppressed because it is too large
View File


+ 18
- 0
exchange/wrappers/binance.js View File

@ -332,6 +332,24 @@ Trader.prototype.addOrder = function(tradeType, amount, price, callback) {
retry(undefined, handler, setOrder);
};
Trader.prototype.getOpenOrders = function(callback) {
const get = (err, data) => {
if(err) {
return callback(err);
}
callback(null, data.map(o => o.orderId));
}
const reqData = {
symbol: this.pair
}
const handler = cb => this.binance.openOrders(reqData, this.handleResponse('getOpenOrders', cb));
retry(undefined, handler, get);
}
Trader.prototype.getOrder = function(order, callback) {
const get = (err, data) => {
if (err) return callback(err);

+ 10
- 2
exchange/wrappers/coinfalcon.js View File

@ -64,6 +64,12 @@ const recoverableErrors = [
'EAI_AGAIN'
];
// errors that might mean
// the API call succeeded.
const unknownResultErrors = [
'524',
]
Trader.prototype.processResponse = function(method, args, next) {
const requestAt = moment();
@ -77,11 +83,13 @@ Trader.prototype.processResponse = function(method, args, next) {
}
const catcher = err => {
if(!err || !err.message)
if(!err || !err.message) {
err = new Error(err || 'Empty error');
}
if(includes(err.message, recoverableErrors))
if(includes(err.message, recoverableErrors)) {
return this.retry(method, args);
}
console.log(new Date, '[cf] big error!', err.message, method);

+ 44
- 0
exchange/wrappers/poloniex.js View File

@ -18,6 +18,8 @@ const Trader = function(config) {
this.pair = this.currency + '_' + this.asset;
this.fillDataOnCancel = true;
this.poloniex = new Poloniex({
key: this.key,
secret: this.secret,
@ -240,6 +242,8 @@ Trader.prototype.getOpenOrders = function(callback) {
return callback(err);
}
console.log(orders);
const ids = orders.map(o => o.orderNumber);
return callback(undefined, ids);
@ -275,6 +279,38 @@ Trader.prototype.getPortfolio = function(callback) {
retry(null, fetch, handle);
}
Trader.prototype.getFullPortfolio = function(callback) {
const handle = (err, data) => {
if(err) {
return callback(err);
}
const asset = data[this.asset];
const currency = data[this.currency];
let assetAmount = parseFloat(asset.available) + parseFloat(asset.onOrders);
let currencyAmount = parseFloat(currency.available) + parseFloat(asset.onOrders);
if(!_.isNumber(assetAmount) || _.isNaN(assetAmount)) {
assetAmount = 0;
}
if(!_.isNumber(currencyAmount) || _.isNaN(currencyAmount)) {
currencyAmount = 0;
}
var portfolio = [
{ name: this.asset, amount: assetAmount },
{ name: this.currency, amount: currencyAmount }
];
callback(undefined, portfolio);
}
const fetch = next => this.poloniex.returnCompleteBalances(this.processResponse(next));
retry(null, fetch, handle);
}
Trader.prototype.getTicker = function(callback) {
const handle = (err, data) => {
if(err)
@ -325,6 +361,8 @@ Trader.prototype.createOrder = function(side, amount, price, callback) {
return callback(err);
}
// console.log(new Date, 'PORDER created', result.orderNumber);
callback(undefined, result.orderNumber);
}
@ -355,6 +393,7 @@ Trader.prototype.checkOrder = function(id, callback) {
const order = _.find(result, function(o) { return o.orderNumber === id });
if(!order) {
console.log(new Date, 'order not open', id, result);
// if the order is not open it's fully executed
return callback(undefined, { executed: true, open: false });
}
@ -409,7 +448,10 @@ Trader.prototype.cancelOrder = function(order, callback) {
return callback(err);
}
// console.log(new Date, 'PORDER cancel', order);
if(result.filled) {
console.log(new Date, 'result.filled', result);
return callback(undefined, true);
}
@ -417,6 +459,8 @@ Trader.prototype.cancelOrder = function(order, callback) {
if(result.amount) {
data = { remaining: result.amount };
} else {
console.log(new Date, 'not result.amount', result);
}

+ 187
- 0
exchange/wrappers/therocktrading-markets.json View File

@ -0,0 +1,187 @@
{
"assets": [
"ETH",
"LTC",
"BTC",
"BCH",
"EUR",
"PPC",
"ZEC",
"NOKU",
"FDZ"
],
"currencies": [
"BTC",
"EUR",
"ETH",
"GUSD",
"XRP"
],
"markets": [
{
"pair": [
"EUR",
"BTC"
],
"minimalOrder": {
"amount": 0.0005,
"price": 0.01,
"order": 0
}
},
{
"pair": [
"EUR",
"ETH"
],
"minimalOrder": {
"amount": 0.005,
"price": 0.01,
"order": 0
}
},
{
"pair": [
"EUR",
"LTC"
],
"minimalOrder": {
"amount": 0.01,
"price": 0.01,
"order": 0
}
},
{
"pair": [
"EUR",
"PPC"
],
"minimalOrder": {
"amount": 0.05,
"price": 0.01,
"order": 0
}
},
{
"pair": [
"EUR",
"ZEC"
],
"minimalOrder": {
"amount": 0.005,
"price": 0.01,
"order": 0
}
},
{
"pair": [
"EUR",
"BCH"
],
"minimalOrder": {
"amount": 0.005,
"price": 0.01,
"order": 0
}
},
{
"pair": [
"EUR",
"GUSD"
],
"minimalOrder": {
"amount": 0.01,
"price": 0.01,
"order": 0
}
},
{
"pair": [
"BTC",
"ETH"
],
"minimalOrder": {
"amount": 0.001,
"price": 1e-8,
"order": 0
}
},
{
"pair": [
"BTC",
"PPC"
],
"minimalOrder": {
"amount": 0.05,
"price": 1e-8,
"order": 0
}
},
{
"pair": [
"BTC",
"LTC"
],
"minimalOrder": {
"amount": 0.01,
"price": 1e-8,
"order": 0
}
},
{
"pair": [
"BTC",
"BCH"
],
"minimalOrder": {
"amount": 0.005,
"price": 1e-8,
"order": 0
}
},
{
"pair": [
"BTC",
"ZEC"
],
"minimalOrder": {
"amount": 0.005,
"price": 1e-8,
"order": 0
}
},
{
"pair": [
"GUSD",
"BTC"
],
"minimalOrder": {
"amount": 0.0005,
"price": 0.01,
"order": 0
}
},
{
"pair": [
"XRP",
"EUR"
],
"minimalOrder": {
"amount": 0.01,
"price": 0.01,
"order": 0
}
},
{
"pair": [
"XRP",
"BTC"
],
"minimalOrder": {
"amount": 0.005,
"price": 0.01,
"order": 0
}
}
]
}

+ 286
- 0
exchange/wrappers/therocktrading.js View File

@ -0,0 +1,286 @@
const Therocktrading = require('therocktrading');
const _ = require('lodash');
const moment = require('moment');
const retry = require('../exchangeUtils').retry;
const marketData = require("./therocktrading-markets.json");
const QUERY_DELAY = 350;
const Trader = function(config) {
this.post_only = true;
this.use_sandbox = false;
this.name = 'Therocktrading';
this.scanback = true;
this.scanbackTid = 0;
this.since = null;
this.asset = config.asset;
this.currency = config.currency;
this.api_url = 'https://api.therocktrading.com';
this.api_sandbox_url = 'https://api-staging.therocktrading.com';
if (_.isObject(config)) {
this.key = config.key;
this.secret = config.secret;
this.pair = [config.asset, config.currency].join('').toUpperCase();
this.post_only =
typeof config.post_only !== 'undefined' ? config.post_only : true;
if (config.sandbox) {
this.use_sandbox = config.sandbox;
}
}
this.therocktrading = new Therocktrading(this.key, this.secret, 'Gekko Broker v' + require('../package.json').version);
};
const recoverableErrors = [
'SOCKETTIMEDOUT',
'TIMEDOUT',
'CONNRESET',
'CONNREFUSED',
'NOTFOUND',
'Rate limit exceeded',
'Response code 5',
'Therocktrading is currently under maintenance.',
'HTTP 408 Error',
'HTTP 504 Error',
'HTTP 503 Error',
'EHOSTUNREACH',
'EAI_AGAIN',
'ENETUNREACH'
];
const includes = (str, list) => {
if(!_.isString(str))
return false;
return _.some(list, item => str.includes(item));
}
Trader.prototype.processResponse = function(method, next) {
return (error, body) => {
if (body && body.code) {
error = new Error(`Error ${body.code}: ${body.msg}`);
}
if(!error && body && !_.isEmpty(body.message)) {
error = new Error(body.message);
}
/*if(
response &&
response.statusCode < 200 &&
response.statusCode >= 300
) {
error = new Error(`Response code ${response.statusCode}`);
}*/
if(error) {
if(_.isString(error)) {
error = new Error(error);
}
if(includes(error.message, recoverableErrors)) {
error.notFatal = true;
}
return next(error);
}
return next(undefined, body);
}
};
Trader.prototype.getTrades = function(since, callback, descending) {
const handle = (err, data) => {
if (err) return callback(err);
var trades = [];
if (_.isArray(data.trades)) {
trades = _.map(data.trades, function(trade) {
return {
tid: trade.id,
price: trade.price,
amount: trade.amount,
date: moment.utc(trade.date).format('X')
};
});
}
callback(null, descending ? trades : trades.reverse());
};
if (moment.isMoment(since)) since = since.format();
//if (moment.isMoment(to)) to = to.format();
var options = {
order: "DESC" // which is default at therock
}
if (since)
options.after = since;
const fetch = cb => this.therocktrading.trades(this.pair, options, this.processResponse('getTrades', cb));
retry(null, fetch, handle);
};
Trader.prototype.getTicker = function(callback) {
const result = (err, data) => {
if (err) return callback(err);
callback(undefined, { bid: data.bid, ask: data.ask });
};
const fetch = cb =>
this.therocktrading.ticker(this.pair, this.processResponse('getTicker', cb));
retry(null, fetch, result);
};
Trader.prototype.getFee = function(callback) {
const fee = 0.2;
// should check for discounts ?
callback(undefined, fee);
};
Trader.prototype.getPortfolio = function(callback) {
const result = (err, data) => {
if (err) return callback(err);
var portfolio = data.balances.map(function(balance) {
return {
name: balance.currency.toUpperCase(),
amount: parseFloat(balance.trading_balance),
};
});
callback(undefined, portfolio);
};
const fetch = cb => this.therocktrading.balances(this.processResponse('getPortfolio', cb));
retry(undefined, fetch, result);
};
Trader.prototype.buy = function(amount, price, callback) {
const handle = (err, result) => {
if(err) {
return callback(err);
}
callback(undefined, result.id);
}
const fetch = next => {
this.therocktrading.buy(this.pair, amount.toString(), price.toString(), this.processResponse('order', next))
};
retry(null, fetch, handle);
}
Trader.prototype.sell = function(amount, price, callback) {
const handle = (err, result) => {
if(err) {
return callback(err);
}
callback(undefined, result.id);
}
const fetch = next => {
this.therocktrading.sell(this.pair, amount.toString(), price.toString(), this.processResponse('order', next))
};
retry(null, fetch, handle);
}
Trader.prototype.getOrder = function(order, callback) {
const handle = (err, result) => {
if(err)
return callback(err);
let price = 0;
let amount = 0;
let date = moment(0);
if(result.amount === result.amount_unfilled) {
return callback(null, {price, amount, date});
}
_.each(result.trades, trade => {
date = moment(trade.date);
price = ((price * amount) + (+trade.price * trade.amount)) / (+trade.amount + amount);
amount += +trade.amount;
});
const fees = {};
const feePercent = this.makerFee;
const fee = price * amount * feePercent;
fees[this.currency] = fee;
callback(err, {price, amount, date, fees, feePercent});
};
const fetch = cb => this.therocktrading.order_status(this.pair, order, this.processResponse('order_status', cb));
retry(null, fetch, handle);
};
Trader.prototype.checkOrder = function(order, callback) {
const result = (err, data) => {
if (err) return callback(err);
var status = data.status;
if (status == 'executed') {
return callback(undefined, { executed: true, open: false, filledAmount: parseFloat(data.amount) });
} else if (status === 'deleted') {
return callback(undefined, { executed: false, open: false, filledAmount: parseFloat(data.amount - data.amount_unfilled) });
} else if (status === 'active') {
// should check if not expired (data.close_on)?
return callback(undefined, { executed: false, open: true, filledAmount: parseFloat(data.amount - data.amount_unfilled) });
}
callback(new Error('Unknown status ' + status));
};
const fetch = cb =>
this.therocktrading.order_status(this.pair, order, this.processResponse('order_status', cb));
retry(null, fetch, result);
};
Trader.prototype.cancelOrder = function(order, callback) {
// callback for cancelOrder should be true if the order was already filled, otherwise false
const result = (err, data) => {
if(err) {
return callback(null, true); // need to catch the specific error but usually an error on cancel means it was filled
}
return callback(null, false);
};
const fetch = cb =>
this.therocktrading.cancel_order(this.pair, order, this.processResponse('cancel_order', cb));
retry(null, fetch, result);
};
Trader.prototype.roundAmount = function(amount) {
return _.floor(amount, 8);
}
Trader.prototype.roundPrice = function(price) {
return +price;
}
Trader.getCapabilities = function() {
return {
name: 'Therocktrading',
slug: 'therocktrading',
currencies: marketData.currencies,
assets: marketData.assets,
markets: marketData.markets,
requires: ['key', 'secret'],
providesHistory: 'date',
providesFullHistory: true,
// following tid, define type to use on trade to batch trades
tid: 'tid',
tradable: true,
gekkoBroker: 0.6
};
};
module.exports = Trader;

+ 1
- 0
gekko.js View File

@ -50,6 +50,7 @@ const config = util.getConfig();
const mode = util.gekkoMode();
if(
config.trader &&
config.trader.enabled &&
!config['I understand that Gekko only automates MY OWN trading strategies']
)

+ 91
- 0
importers/exchanges/therocktrading.js View File

@ -0,0 +1,91 @@
const moment = require('moment');
const util = require('../../core/util');
const log = require('../../core/log');
const _ = require('lodash');
const retry = require('../../exchange/exchangeUtils').retry;
const config = util.getConfig();
const dirs = util.dirs();
const Fetcher = require(dirs.exchanges + 'therocktrading');
Fetcher.prototype.getTrades = function(since, to, page, callback, descending) {
const handle = (err, data) => {
if (err) return callback(err);
var trades = [];
if (_.isArray(data.trades)) {
trades = _.map(data.trades, function(trade) {
return {
tid: trade.id,
price: trade.price,
amount: trade.amount,
date: moment.utc(trade.date).format('X')
};
});
}
callback(null, descending ? trades : trades.reverse());
};
if (moment.isMoment(since)) since = since.format();
if (moment.isMoment(to)) to = to.format();
let options = {
before: to,
after: since,
page: page,
order: "ASC",
per_page: 200
}
const fetch = cb => this.therocktrading.trades(this.pair, options, this.processResponse('getTrades', cb));
retry(null, fetch, handle);
};
util.makeEventEmitter(Fetcher);
var end = false;
var done = false;
var from = false;
var page = 1;
var fetcher = new Fetcher(config.watch);
var fetch = () => {
fetcher.import = true;
log.debug("IMPORTER: starting fetcher")
fetcher.getTrades(from, end, page, handleFetch);
}
var handleFetch = (err, trades) => {
if(!err && !trades.length) {
console.log('no more trades');
fetcher.emit('done');
}
if (err) {
log.error(`There was an error importing from TheRockTrading ${err}`);
fetcher.emit('done');
return fetcher.emit('trades', []);
}
if (trades.length > 0) {
page = page + 1;
}
fetcher.emit('trades', trades);
}
module.exports = function (daterange) {
from = daterange.from.clone();
end = daterange.to.clone();
return {
bus: fetcher,
fetch: fetch
}
}

+ 30
- 23
package-lock.json View File

@ -3208,9 +3208,10 @@
}
},
"glob": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz",
"integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==",
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
"integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==",
"dev": true,
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
@ -4554,27 +4555,27 @@
}
},
"needle": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/needle/-/needle-2.2.4.tgz",
"integrity": "sha512-HyoqEb4wr/rsoaIDfTH2aVL9nWtQqba2/HvMv+++m8u0dz808MaagKILxtfeSN7QU7nvbQ79zk3vYOJp9zsNEA==",
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/needle/-/needle-2.3.1.tgz",
"integrity": "sha512-CaLXV3W8Vnbps8ZANqDGz7j4x7Yj1LW4TWF/TQuDfj7Cfx4nAPTvw98qgTevtto1oHDrh3pQkaODbqupXlsWTg==",
"requires": {
"debug": "^2.1.2",
"debug": "^4.1.0",
"iconv-lite": "^0.4.4",
"sax": "^1.2.4"
},
"dependencies": {
"debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
"integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
"requires": {
"ms": "2.0.0"
"ms": "^2.1.1"
}
},
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
"integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg=="
}
}
},
@ -5903,15 +5904,21 @@
"integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==",
"requires": {
"glob": "^7.1.3"
}
},
"ripemd160": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz",
"integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==",
"requires": {
"hash-base": "^3.0.0",
"inherits": "^2.0.1"
},
"dependencies": {
"glob": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz",
"integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==",
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.0.4",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
}
}
}
},
"safe-buffer": {

+ 14
- 0
plugins.js View File

@ -162,6 +162,13 @@ var plugins = [
async: false,
modes: ['realtime']
},
{
name: 'Candle Uploader',
description: 'Upload candles to an extneral server',
slug: 'candleUploader',
async: true,
modes: ['realtime']
},
{
name: 'Twitter',
description: 'Sends trades to twitter.',
@ -266,6 +273,13 @@ var plugins = [
modes: ['realtime', 'backtest'],
emits: true,
path: config => 'tradingAdvisor/tradingAdvisor.js',
},
{
name: 'Blotter',
description: 'Writes all buy/sell trades to a blotter CSV file',
slug: 'blotter',
async: false,
modes: ['realtime'],
}
];

+ 118
- 0
plugins/blotter.js View File

@ -0,0 +1,118 @@
const fsw = require('fs');
const _ = require('lodash');
const log = require('../core/log.js');
const util = require('../core/util.js');
const config = util.getConfig();
const blotterConfig = config.blotter;
var Blotter = function(done) {
_.bindAll(this);
this.time;
this.valueAtBuy = 0.0;
this.filename = blotterConfig.filename;
this.dateformat = blotterConfig.dateFormat;
this.timezone = blotterConfig.timezone;
this.headertxt = '';
this.outtxt = '';
this.tradeError = false;
this.inaccurateData = false;
this.done = done;
this.setup();
};
Blotter.prototype.setup = function(done) {
this.headertxt = "Date,Price,Amount,Side,Fees,Value,P&L,Notes\n";
fsw.readFile(this.filename, (err, _) => {
if (err) {
log.warn('No file with the name', this.filename, 'found. Creating new blotter file');
fsw.appendFile(this.filename, this.headertxt, 'utf8', (err) => {
if(err) {
log.error('Unable to write header text to blotter');
}
});
}
});
};
Blotter.prototype.processTradeCompleted = function(trade) {
// if exchange doesn't send correct timezone, correct it
if (trade.date.format('Z') == '+00:00') {
var adjustTimezone = trade.date.utcOffset(this.timezone);
this.time = adjustTimezone.format(this.dateformat);
} else {
this.time = trade.date.format(this.dateformat);
}
// If a trade date is from 1969 or 1970, there was an error with the trade
if (trade.date.format('YY') == '69' || trade.date.format('YY') == '70') {
log.error('Received 1969/1970 error, trade failed to execute, did not record in blotter');
// Prevent roundTrip from writing error P&L in processRoundtrip method
if (trade.action == 'sell') {
this.tradeError = true;
}
return;
}
this.valueAtBuy = this.roundUp(trade.effectivePrice * trade.amount);
if (trade.action === 'buy') {
//time, price, amount, side, fees, value at buy
this.outtxt = this.time + "," + trade.effectivePrice.toFixed(2) + "," + trade.amount.toFixed(8) + "," + trade.action + "," + trade.feePercent + "," + this.valueAtBuy;
if ((trade.price == 0 || isNaN(trade.price)) && (trade.amount == 0|| isNaN(trade.amount))) {
this.outtxt = this.outtxt + "," + ",Trade probably went through but didn't receive correct price/amount info\n";
} else {
this.outtxt = this.outtxt + "\n";
}
this.writeBlotter();
return;
}
else if (trade.action === 'sell'){
var sellValue = (this.roundUp(trade.effectivePrice * trade.amount));
//time, price, amount, side, fees, value at sell
this.outtxt = this.time + "," + trade.effectivePrice.toFixed(2) + "," + trade.amount.toFixed(8) + "," + trade.action + "," + trade.feePercent + "," + sellValue + ",";
if ((trade.price == 0 || isNaN(trade.price)) && (trade.amount == 0|| isNaN(trade.amount))) {
this.inaccurateData = true;
}
this.valueAtBuy = 0.0;
// wait til processRoundtrip complete to write to file
}
}
Blotter.prototype.writeBlotter = function() {
fsw.appendFile(this.filename, this.outtxt, 'utf8', (err) => {
if(err) {
log.error('Unable to write trade to blotter');
}
});
}
Blotter.prototype.processRoundtrip = function(trip) {
log.info('Roundtrip', trip);
if (!this.tradeError) {
this.outtxt = this.outtxt + this.roundUp(trip.pnl) +'\n';
this.writeBlotter();
return;
}
if (this.inaccurateData) {
this.outtxt = this.outtxt + this.roundUp(trip.pnl) + ",Trade probably went through but didn't receive correct price/amount info\n";
this.inaccurateData = false;
this.writeBlotter;
return;
}
// 1969/1970 sell trade error not written to blotter, resetting flag to false
this.tradeError = false;
}
Blotter.prototype.roundUp = function(value) {
var cents = value * 100;
var roundedCents = Math.round(cents);
return roundedCents / 100;
}
module.exports = Blotter;

+ 17
- 6
plugins/postgresql/reader.js View File

@ -25,11 +25,17 @@ Reader.prototype.mostRecentWindow = function(from, to, next) {
from = from.unix();
var maxAmount = to - from + 1;
this.db.connect((err,client,done) => {
this.db.connect((err, client, done) => {
if(err) {
log.error(err);
return util.die(err.message);
}
var query = client.query(new Query(`
SELECT start from ${postgresUtil.table('candles')}
WHERE start <= ${to} AND start >= ${from}
ORDER BY start DESC
SELECT start from ${postgresUtil.table('candles')}
WHERE start <= ${to} AND start >= ${from}
ORDER BY start DESC
`), function (err, result) {
if (err) {
// bail out if the table does not exist
@ -152,9 +158,14 @@ Reader.prototype.get = function(from, to, what, next, mytable) {
Reader.prototype.count = function(from, to, next) {
this.db.connect((err,client,done) => {
if(err) {
log.error(err);
return util.die(err.message);
}
var query = client.query(new Query(`
SELECT COUNT(*) as count from ${postgresUtil.table('candles')}
WHERE start <= ${to} AND start >= ${from}
SELECT COUNT(*) as count from ${postgresUtil.table('candles')}
WHERE start <= ${to} AND start >= ${from}
`));
var rows = [];
query.on('row', function(row) {

+ 3
- 3
plugins/tradingAdvisor/baseTradingMethod.js View File

@ -78,10 +78,10 @@ var Base = function(settings) {
if(!this.onRemoteCandle)
this.onRemoteCandle = function() {};
//if no requiredHistory was provided, set default from tradingAdvisor
if (!_.isNumber(this.requiredHistory)){
this.requiredHistory = config.tradingAdvisor.historySize;
if(_.isNumber(this.requiredHistory)) {
log.debug('Ignoring strategy\'s required history, using the "config.tradingAdvisor.historySize" instead.');
}
this.requiredHistory = config.tradingAdvisor.historySize;
if(!config.debug || !this.log)
this.log = function() {};

+ 13
- 0
sample-config.js View File

@ -113,6 +113,13 @@ config.pushover = {
user: ''
}
config.blotter = {
enabled: false,
filename: 'blotter.csv',
dateFormat: 'l LT',
timezone: -300, // -300 minutes for EST(-5:00), only used if exchange doesn't provide correct timezone
}
// want Gekko to send a mail on buy or sell advice?
config.mailer = {
enabled: false, // Send Emails if true, false to turn off
@ -329,6 +336,12 @@ config.mongodb = {
}]
}
config.candleUploader = {
enabled: false,
url: '',
apiKey: ''
}
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// CONFIGURING BACKTESTING
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Loading…
Cancel
Save