first commit

This commit is contained in:
Francois Klieber committed 2021-06-06 20:25:14 +02:00
commit aa7869eb33
386 files changed
+69375

No files matched your search

+7
View File
@@ -0,0 +1,7 @@
node_modules
.travis.yml
volumes
config.js
sample-config.js
.git
.gitignore
+16
View File
@@ -0,0 +1,16 @@
kind: pipeline
name: default
steps:
- name: publish to harbor frantchenco.page
image: plugins/docker
settings:
repo: harbor.frantchenco.page/library/gekko
dockerfile: dockerfile
username: admin
password: Harbor12345
registry: harbor.frantchenco.page
auto_tag: true
tags:
- latest
+9
View File
@@ -0,0 +1,9 @@
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
+3
View File
@@ -0,0 +1,3 @@
HOST=localhost
PORT=3000
USE_SSL=0
+17
View File
@@ -0,0 +1,17 @@
**Note: this is the technical bug tracker, please use other platforms for getting support and starting a (non technical) discussion. See the [getting help page](https://gekko.wizb.it/docs/introduction/getting-help.html) for details.**
**I'm submitting a ...**
[ ] bug report
[ ] question about the decisions made in the repository
**Action taken** (what you did)
**Expected result** (what you hoped would happen)
**Actual result** (unexpected outcome)
**Other information** (e.g. detailed explanation, stacktraces, related issues, suggestions how to fix, links for us to have context, eg. stackoverflow, etc)
+13
View File
@@ -0,0 +1,13 @@
* **What kind of change does this PR introduce?** (Bug fix, feature, docs update, ...)
* **What is the current behavior?** (You can also link to an open issue here)
* **What is the new behavior (if this is a feature change)?**
* **Other information**:
+19
View File
@@ -0,0 +1,19 @@
# Number of days of inactivity before an issue becomes stale
daysUntilStale: 60
# Number of days of inactivity before a stale issue is closed
daysUntilClose: 7
# Issues with these labels will never be considered stale
exemptLabels:
- pinned
- security
- important
# Label to use when marking an issue as stale
staleLabel: wontfix
# Comment to post when marking an issue as stale. Set to `false` to disable
markComment: >
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. Thank you
for your contributions. If you feel this is very a important issue please
reach out the maintainer of this project directly via e-mail: gekko at mvr dot me.
# Comment to post when closing a stale issue. Set to `false` to disable
closeComment: false
+52
View File
@@ -0,0 +1,52 @@
# Numerous always-ignore extensions
*.diff
*.err
*.orig
*.log
*.rej
*.swo
*.swp
*.vi
*~
*.sass-cache
# OS or Editor folders
.DS_Store
Thumbs.db
config.js
api-keys.js
.cache
.project
.settings
.tmproj
nbproject
*.sublime-project
*.sublime-workspace
# Dreamweaver added files
_notes
dwsync.xml
# Komodo
*.komodoproject
.komodotools
# Espresso
*.esproj
*.espressostorage
# Folders & files to ignore
node_modules
candles.csv
cexio.db
history
TMP_*
.idea
volumes
config.js
config-*.js
private-*.js
private-*.toml
SECRET-api-keys.json
exchange/nusadua
+4
View File
@@ -0,0 +1,4 @@
{
"trailingComma": "es5",
"singleQuote": true
}
+3
View File
@@ -0,0 +1,3 @@
language: node_js
node_js:
- "8.11.2"
+48
View File
@@ -0,0 +1,48 @@
# Contributing
Thanks for (thinking about) contributing to Gekko, all help is wanted!
## Before Submitting an Issue
First, please do a search in [open issues](https://github.com/askmike/gekko/issues/) to see if the issue or feature request has already been filed.
If you find your issue already exists, make relevant comments and add your reaction:
👍 - upvote
👎 - downvote
If you cannot find an existing issue that describes your bug or feature, submit an issue using the guidelines below.
### Submitting an issue (bug)
* Be as clear and concise as possible.
* Do not submit more than one bug in a single issue.
* Clearly explain the following:
1. Action taken (what you did)
2. Expected result (what you hoped would happen)
3. Actual result (unexpected outcome)
4. Your setup (any and all relevant technical facts, error messages, logs, etc)
### Feature requests
* First consider if **you** yourself might be able to work on the feature? If so, check out info on contributing code below.
* If you do not have the skills, then please take the time to clearly think through the request. Is is appropriate for all users of Gekko? If so, create a new issue containing the following:
1. Write `[request]` as the first item in the title, followed by a short, concise description, eg: `[request] Export data to CSV`.
2. Fill in details explaining your request. Be as clear and concise as possible and only 1 request at a time.
3. Add mock-ups, etc as needed to clearly demonstrate the idea.
## New Feature and code contributions
- If you want to add an exchange to Gekko, see [this doc](https://gekko.wizb.it/docs/extending/add_an_exchange.html) for all the information you need.
- If you want to Gekko react to anything from the market, you can most likely put this functionality into a plugin. See [this document](https://gekko.wizb.it/docs/internals/plugins.html) for details.
- If you want to create a strategy, please see [this page](https://gekko.wizb.it/docs/strategies/creating_a_strategy.html).
- If you just want to work on Gekko, you can use the open issues with the tag `open-for-pulls` for inspiration.
- If you want to work on the web interface (Gekko UI), please see [this frontend doc](https://gekko.wizb.it/docs/internals/gekko_ui.html) on the Vue.js frontend.
Things to take into consideration when submitting a pull request:
- Please submit all pull requests (except for hotfixes) to the [develop branch](https://github.com/askmike/gekko/tree/develop).
- Please keep current code styling in mind.
+34
View File
@@ -0,0 +1,34 @@
FROM node:8
ENV HOST localhost
ENV PORT 3000
# Create app directory
RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app
# Install GYP dependencies globally, will be used to code build other dependencies
RUN npm install -g --production node-gyp && \
npm cache clean --force
# Install Gekko dependencies
COPY package.json .
RUN npm install --production && \
npm install --production [email protected] [email protected] [email protected] pg && \
npm cache clean --force
# Install Gekko Broker dependencies
WORKDIR exchange
COPY exchange/package.json .
RUN npm install --production && \
npm cache clean --force
WORKDIR ../
# Bundle app source
COPY . /usr/src/app
EXPOSE 3000
RUN chmod +x /usr/src/app/docker-entrypoint.sh
ENTRYPOINT ["/usr/src/app/docker-entrypoint.sh"]
CMD ["--config", "config.js", "--ui"]
+9
View File
@@ -0,0 +1,9 @@
The MIT License (MIT)
Copyright (c) 2014-2017 Mike van Rossum [email protected]
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+43
View File
@@ -0,0 +1,43 @@
# This repo is not maintained anymore
I am officially not maintaining this project anymore. It was an amazing journey and I want to thank everyone for
playing the role in this amazing story!
More details can be found here: [https://medium.com/@gekkoplus/archiving-open-source-gekko-dba02e6efc7](https://medium.com/@gekkoplus/archiving-open-source-gekko-dba02e6efc7)
This only impacts my Gekko repo (askmike/gekko). There might be other forks of Gekko out there that are being maintained!
------
Old content:
------
# Gekko [![npm](https://img.shields.io/npm/dm/gekko.svg)]() [![Build Status](https://travis-ci.org/askmike/gekko.png)](https://travis-ci.org/askmike/gekko) [![Build status](https://ci.appveyor.com/api/projects/status/github/askmike/gekko?branch=stable&svg=true)](https://ci.appveyor.com/project/askmike/gekko)
![Gordon Gekko](http://mikevanrossum.nl/static/gekko.jpg)
*The most valuable commodity I know of is information.*
-Gordon Gekko
Gekko is a Bitcoin TA trading and backtesting platform that connects to popular Bitcoin exchanges. It is written in JavaScript and runs on [Node.js](http://nodejs.org).
*Use Gekko at your own risk.*
## Documentation
See [the documentation website](https://gekko.wizb.it/docs/introduction/about_gekko.html).
## Installation & Usage
See [the installing Gekko doc](https://gekko.wizb.it/docs/installation/installing_gekko.html).
## Community & Support
Gekko has [a forum](https://forum.gekko.wizb.it/) that is the place for discussions on using Gekko, automated trading and exchanges. In case you rather want to chat in realtime about Gekko feel free to join the [Gekko Support Discord](https://discord.gg/26wMygt).
## Final
If Gekko helped you in any way, you can always leave me a tip at (BTC) 13r1jyivitShUiv9FJvjLH7Nh1ZZptumwW
+15
View File
@@ -0,0 +1,15 @@
init:
- git config --global core.autocrlf input
environment:
nodejs_version: "9"
install:
- ps: Install-Product node $env:nodejs_version
- npm install
test_script:
- node --version
- npm --version
- cmd: npm test
build: off
+7
View File
@@ -0,0 +1,7 @@
connectionString = "mongodb://mongodb/gekko"
path = "plugins/mongodb"
version = 0.1
[[dependencies]]
module = "mongojs"
version = "2.4.0"
+7
View File
@@ -0,0 +1,7 @@
connectionString = "postgres://user:pass@localhost:5432"
path = "plugins/postgresql"
version = 0.1
[[dependencies]]
module = "pg"
version = "6.1.0"
+7
View File
@@ -0,0 +1,7 @@
dataDirectory = "history"
path = "plugins/sqlite"
version = 0.1
[[dependencies]]
module = "sqlite3"
version = "3.1.4"
+8
View File
@@ -0,0 +1,8 @@
batchSize = 50
# scan for available dateranges
daterange = "scan"
# or specify it like so:
# [daterange]
# from = "2015-01-01"
# to = "2016-01-01"
+10
View File
@@ -0,0 +1,10 @@
debug = true
# what database should Gekko use?
adapter = 'sqlite'
[watch]
exchange = 'Poloniex'
currency = 'USDT'
asset = 'BTC'
+2
View File
@@ -0,0 +1,2 @@
enabled = true
adapter = "sqlite"
+8
View File
@@ -0,0 +1,8 @@
feeMaker = 0.25
feeTaker = 0.25
feeUsing = 'maker'
slippage = 0.05
[simulationBalance]
asset = 1
currency = 100
+5
View File
@@ -0,0 +1,5 @@
# Used to calculate sharpe ratio
# yearly % of riskfree return for example
# treasury bonds or bank interest.
riskFreeReturn = 2
+16
View File
@@ -0,0 +1,16 @@
enabled = false
# Send 'Gekko starting' message if true
sendMessageOnStart = true
# disable advice printout if it's soft
muteSoft = true
# your email, change it unless you are Azor Ahai
email = "[email protected]"
# your pushbullet API key
key = "xxx"
# will make Gekko messages start with [GEKKO]
tag = "[GEKKO]"
+9
View File
@@ -0,0 +1,9 @@
enabled = false
## NOTE: once you filled in the following
## never share this file with anyone!
key = ""
secret = ""
# this is only required at specific exchanges
username = ""
+9
View File
@@ -0,0 +1,9 @@
enabled = true
candleSize = 60
historySize = 25
method = "MACD"
[talib]
enabled = false
version = "1.0.2"
+10
View File
@@ -0,0 +1,10 @@
# constant multiplier. 0.015 gets to around 70% fit
constant = 0.015
# history size, make same or smaller than history
history = 90
[thresholds]
up = 100
down = -100
persistence = 0
+5
View File
@@ -0,0 +1,5 @@
weight = 21
[thresholds]
down = -0.025
up = 0.025
+8
View File
@@ -0,0 +1,8 @@
short = 10
long = 21
signal = 9
[thresholds]
down = -0.025
up = 0.025
persistence = 1
+8
View File
@@ -0,0 +1,8 @@
short = 12
long = 26
signal = 9
[thresholds]
down = -0.025
up = 0.025
persistence = 2
+6
View File
@@ -0,0 +1,6 @@
interval = 14
[thresholds]
low = 30
high = 70
persistence = 1
+6
View File
@@ -0,0 +1,6 @@
interval = 3
[thresholds]
low = 20
high = 80
persistence = 3
+3
View File
@@ -0,0 +1,3 @@
short = 7
medium = 25
long = 99
+7
View File
@@ -0,0 +1,7 @@
short = 13
long = 25
[thresholds]
low = -25
high = 25
persistence = 1
+16
View File
@@ -0,0 +1,16 @@
[first]
weight = 4
period = 7
[second]
weight = 2
period = 14
[third]
weight = 1
period = 28
[thresholds]
low = 30
high = 70
persistence = 1
+1
View File
@@ -0,0 +1 @@
my_custom_setting = 10
+8
View File
@@ -0,0 +1,8 @@
[parameters]
optInFastPeriod = 10
optInSlowPeriod = 21
optInSignalPeriod = 9
[thresholds]
down = -0.025
up = 0.025
+7
View File
@@ -0,0 +1,7 @@
historySize = 80
optInTimePeriod = 15
candleSize = 10
[thresholds]
up = 30
down = 20
+8
View File
@@ -0,0 +1,8 @@
[parameters]
optInFastPeriod = 10
optInSlowPeriod = 21
optInSignalPeriod = 9
[thresholds]
down = -0.025
up = 0.025
+12
View File
@@ -0,0 +1,12 @@
optInTimePeriod = 2
optInFastPeriod = 4
optInSlowPeriod = 7
optInSignalPeriod = 23.707189677282354
candleSize = 1
historySize = 4
up = 24.52
down = 55.74
macd_up = 14.498233170768081
macd_down = -9.65220944122072
+9
View File
@@ -0,0 +1,9 @@
momentum = "TSI" # RSI, TSI or UO
[thresholds]
weightLow = 120
weightHigh = -120
# How many candle intervals should a trend persist
# before we consider it real?
persistence = 0
+78
View File
@@ -0,0 +1,78 @@
// Budfox is the realtime market for Gekko!
//
// Read more here:
// @link https://github.com/askmike/gekko/blob/stable/docs/internals/budfox.md
//
// > [getting up] I don't know. I guess I realized that I'm just Bud Fox.
// > As much as I wanted to be Gordon Gekko, I'll *always* be Bud Fox.
// > [tosses back the handkerchief and walks away]
var _ = require('lodash');
var async = require('async');
var util = require(__dirname + '/../util');
var dirs = util.dirs();
var Heart = require(dirs.budfox + 'heart');
var MarketDataProvider = require(dirs.budfox + 'marketDataProvider');
var CandleManager = require(dirs.budfox + 'candleManager');
var BudFox = function(config) {
_.bindAll(this);
Readable.call(this, {objectMode: true});
// BudFox internal modules:
this.heart = new Heart;
this.marketDataProvider = new MarketDataProvider(config);
this.candleManager = new CandleManager;
// BudFox data flow:
// relay a marketUpdate event
this.marketDataProvider.on(
'marketUpdate',
e => this.emit('marketUpdate', e)
);
// relay a marketStart event
this.marketDataProvider.on(
'marketStart',
e => this.emit('marketStart', e)
);
// Output the candles
this.candleManager.on(
'candles',
this.pushCandles
);
// on every `tick` retrieve trade data
this.heart.on(
'tick',
this.marketDataProvider.retrieve
);
// on new trade data create candles
this.marketDataProvider.on(
'trades',
this.candleManager.processTrades
);
this.heart.pump();
}
var Readable = require('stream').Readable;
BudFox.prototype = Object.create(Readable.prototype, {
constructor: { value: BudFox }
});
BudFox.prototype._read = function noop() {}
BudFox.prototype.pushCandles = function(candles) {
_.each(candles, this.push);
}
module.exports = BudFox;
+200
View File
@@ -0,0 +1,200 @@
// The CandleCreator creates one minute candles based on trade batches. Note
// that it also adds empty candles to fill gaps with no volume.
//
// Expects trade batches to be written like:
//
// {
// amount: x,
// start: (moment),
// end: (moment),
// first: (trade),
// last: (trade),
// timespan: x,
// all: [
// // batch of new trades with
// // moments instead of timestamps
// ]
// }
//
// Emits 'new candles' event with:
//
// [
// {
// start: (moment),
// end: (moment),
// high: (float),
// open: (float),
// low: (float),
// close: (float)
// volume: (float)
// vwp: (float) // volume weighted price
// },
// {
// start: (moment), // + 1
// end: (moment),
// high: (float),
// open: (float),
// low: (float),
// close: (float)
// volume: (float)
// vwp: (float) // volume weighted price
// }
// // etc.
// ]
//
var _ = require('lodash');
var moment = require('moment');
var util = require(__dirname + '/../util');
var CandleCreator = function() {
_.bindAll(this);
// TODO: remove fixed date
this.threshold = moment("1970-01-01", "YYYY-MM-DD");
// This also holds the leftover between fetches
this.buckets = {};
}
util.makeEventEmitter(CandleCreator);
CandleCreator.prototype.write = function(batch) {
var trades = batch.data;
if(_.isEmpty(trades))
return;
trades = this.filter(trades);
this.fillBuckets(trades);
var candles = this.calculateCandles();
candles = this.addEmptyCandles(candles);
if(_.isEmpty(candles))
return;
// the last candle is not complete
this.threshold = candles.pop().start;
this.emit('candles', candles);
}
CandleCreator.prototype.filter = function(trades) {
// make sure we only include trades more recent
// than the previous emitted candle
return _.filter(trades, function(trade) {
return trade.date > this.threshold;
}, this);
}
// put each trade in a per minute bucket
CandleCreator.prototype.fillBuckets = function(trades) {
_.each(trades, function(trade) {
var minute = trade.date.format('YYYY-MM-DD HH:mm');
if(!(minute in this.buckets))
this.buckets[minute] = [];
this.buckets[minute].push(trade);
}, this);
this.lastTrade = _.last(trades);
}
// convert each bucket into a candle
CandleCreator.prototype.calculateCandles = function() {
var minutes = _.size(this.buckets);
// catch error from high volume getTrades
if (this.lastTrade !== undefined)
// create a string referencing the minute this trade happened in
var lastMinute = this.lastTrade.date.format('YYYY-MM-DD HH:mm');
var candles = _.map(this.buckets, function(bucket, name) {
var candle = this.calculateCandle(bucket);
// clean all buckets, except the last one:
// this candle is not complete
if(name !== lastMinute)
delete this.buckets[name];
return candle;
}, this);
return candles;
}
CandleCreator.prototype.calculateCandle = function(trades) {
var first = _.first(trades);
var f = parseFloat;
var candle = {
start: first.date.clone().startOf('minute'),
open: f(first.price),
high: f(first.price),
low: f(first.price),
close: f(_.last(trades).price),
vwp: 0,
volume: 0,
trades: _.size(trades)
};
_.each(trades, function(trade) {
candle.high = _.max([candle.high, f(trade.price)]);
candle.low = _.min([candle.low, f(trade.price)]);
candle.volume += f(trade.amount);
candle.vwp += f(trade.price) * f(trade.amount);
});
candle.vwp /= candle.volume;
return candle;
}
// Gekko expects a candle every minute, if nothing happened
// during a particilar minute Gekko will add empty candles with:
//
// - open, high, close, low, vwp are the same as the close of the previous candle.
// - trades, volume are 0
CandleCreator.prototype.addEmptyCandles = function(candles) {
var amount = _.size(candles);
if(!amount)
return candles;
// iterator
var start = _.first(candles).start.clone();
var end = _.last(candles).start;
var i, j = -1;
var minutes = _.map(candles, function(candle) {
return +candle.start;
});
while(start < end) {
start.add(1, 'm');
i = +start;
j++;
if(_.contains(minutes, i))
continue; // we have a candle for this minute
var lastPrice = candles[j].close;
candles.splice(j + 1, 0, {
start: start.clone(),
open: lastPrice,
high: lastPrice,
low: lastPrice,
close: lastPrice,
vwp: lastPrice,
volume: 0,
trades: 0
});
}
return candles;
}
module.exports = CandleCreator;
+34
View File
@@ -0,0 +1,34 @@
// The candleManager consumes trades and emits:
// - `candles`: array of minutly candles.
// - `candle`: the most recent candle after a fetch Gekko.
var _ = require('lodash');
var moment = require('moment');
var fs = require('fs');
var util = require(__dirname + '/../util');
var dirs = util.dirs();
var config = util.getConfig();
var log = require(dirs.core + 'log');
var CandleCreator = require(dirs.budfox + 'candleCreator');
var Manager = function() {
_.bindAll(this);
this.candleCreator = new CandleCreator;
this.candleCreator
.on('candles', this.relayCandles);
};
util.makeEventEmitter(Manager);
Manager.prototype.processTrades = function(tradeBatch) {
this.candleCreator.write(tradeBatch);
}
Manager.prototype.relayCandles = function(candles) {
this.emit('candles', candles);
}
module.exports = Manager;
+51
View File
@@ -0,0 +1,51 @@
// The heart schedules and emit ticks every 20 seconds.
var util = require(__dirname + '/../util');
var log = require(util.dirs().core + 'log');
var _ = require('lodash');
var moment = require('moment');
if (util.getConfig().watch.tickrate)
var TICKRATE = util.getConfig().watch.tickrate;
else if(util.getConfig().watch.exchange === 'okcoin')
var TICKRATE = 2;
else
var TICKRATE = 20;
var Heart = function() {
this.lastTick = false;
_.bindAll(this);
}
util.makeEventEmitter(Heart);
Heart.prototype.pump = function() {
log.debug('scheduling ticks');
this.scheduleTicks();
}
Heart.prototype.tick = function() {
if(this.lastTick) {
// make sure the last tick happened not to lang ago
// @link https://github.com/askmike/gekko/issues/514
if(this.lastTick < moment().unix() - TICKRATE * 3)
util.die('Failed to tick in time, see https://github.com/askmike/gekko/issues/514 for details', true);
}
this.lastTick = moment().unix();
this.emit('tick');
}
Heart.prototype.scheduleTicks = function() {
setInterval(
this.tick,
+moment.duration(TICKRATE, 's')
);
// start!
_.defer(this.tick);
}
module.exports = Heart;
+45
View File
@@ -0,0 +1,45 @@
//
// The market data provider will fetch data from a datasource on tick. It emits:
//
// - `trades`: batch of newly detected trades
// - `trade`: after Gekko fetched new trades, this
// will be the most recent one.
const _ = require('lodash');
const util = require(__dirname + '/../util');
const MarketFetcher = require('./marketFetcher');
const dirs = util.dirs();
const Manager = function(config) {
_.bindAll(this);
// fetch trades
this.source = new MarketFetcher(config);
// relay newly fetched trades
this.source
.on('trades batch', this.relayTrades);
}
util.makeEventEmitter(Manager);
// HANDLERS
Manager.prototype.retrieve = function() {
this.source.fetch();
}
Manager.prototype.relayTrades = function(batch) {
this.sendMarketStart(batch);
this.emit('marketUpdate', batch.last.date);
this.emit('trades', batch);
}
Manager.prototype.sendMarketStart = _.once(function(batch) {
this.emit('marketStart', batch.first.date);
});
module.exports = Manager;
+109
View File
@@ -0,0 +1,109 @@
//
// The fetcher is responsible for fetching new
// market data at the exchange on interval. It will emit
// the following events:
//
// - `trades batch` - all new trades.
// - `trade` - the most recent trade after every fetch
const _ = require('lodash');
const moment = require('moment');
const utc = moment.utc;
const util = require(__dirname + '/../util');
const dirs = util.dirs();
const config = util.getConfig();
const log = require(dirs.core + 'log');
const exchangeChecker = require(dirs.gekko + 'exchange/exchangeChecker');
const TradeBatcher = require(util.dirs().budfox + 'tradeBatcher');
const Fetcher = function(config) {
if(!_.isObject(config))
throw new Error('TradeFetcher expects a config');
const exchangeName = config.watch.exchange.toLowerCase();
const DataProvider = require(util.dirs().gekko + 'exchange/wrappers/' + exchangeName);
_.bindAll(this);
// Create a public dataProvider object which can retrieve live
// trade information from an exchange.
this.exchangeTrader = new DataProvider(config.watch);
this.exchange = exchangeChecker.settings(config.watch);
var requiredHistory = config.tradingAdvisor.candleSize * config.tradingAdvisor.historySize;
// If the trading adviser is enabled we might need a very specific fetch since
// to line up [local db, trading method, and fetching]
if(config.tradingAdvisor.enabled && config.tradingAdvisor.firstFetchSince) {
this.firstSince = config.tradingAdvisor.firstFetchSince;
if(this.exchange.providesHistory === 'date') {
this.firstSince = moment.unix(this.firstSince).utc();
}
}
this.batcher = new TradeBatcher(this.exchange.tid);
this.pair = [
config.watch.asset,
config.watch.currency
].join('/');
log.info('Starting to watch the market:',
this.exchange.name,
this.pair
);
// if the exchange returns an error
// we will keep on retrying until next
// scheduled fetch.
this.tries = 0;
this.limit = 20; // [TODO]
this.firstFetch = true;
this.batcher.on('new batch', this.relayTrades);
}
util.makeEventEmitter(Fetcher);
Fetcher.prototype._fetch = function(since) {
if(++this.tries >= this.limit)
return;
this.exchangeTrader.getTrades(since, this.processTrades, false);
}
Fetcher.prototype.fetch = function() {
var since = false;
if(this.firstFetch) {
since = this.firstSince;
this.firstFetch = false;
} else
since = false;
this.tries = 0;
log.debug('Requested', this.pair, 'trade data from', this.exchange.name, '...');
this._fetch(since);
}
Fetcher.prototype.processTrades = function(err, trades) {
if(err || _.isEmpty(trades)) {
if(err) {
log.warn(this.exchange.name, 'returned an error while fetching trades:', err);
log.debug('refetching...');
} else
log.debug('Trade fetch came back empty, refetching...');
setTimeout(this._fetch, +moment.duration('s', 1));
return;
}
this.batcher.write(trades);
}
Fetcher.prototype.relayTrades = function(batch) {
this.emit('trades batch', batch);
}
module.exports = Fetcher;
+124
View File
@@ -0,0 +1,124 @@
//
// Small wrapper that only propagates new trades.
//
// Expects trade batches to be written like:
// [
// {
// tid: x, // tid is preferred, but if none available date will also work
// price: x,
// date: (timestamp),
// amount: x
// },
// {
// tid: x + 1,
// price: x,
// date: (timestamp),
// amount: x
// }
// ]
//
// Emits 'new trades' event with:
// {
// amount: x,
// start: (moment),
// end: (moment),
// first: (trade),
// last: (trade)
// data: [
// // batch of new trades with
// // moments instead of timestamps
// ]
// }
var _ = require('lodash');
var moment = require('moment');
var util = require('../util');
var log = require('../log');
var TradeBatcher = function(tid) {
if(!_.isString(tid))
throw new Error('tid is not a string');
_.bindAll(this);
this.tid = tid;
this.last = -1;
}
util.makeEventEmitter(TradeBatcher);
TradeBatcher.prototype.write = function(batch) {
if(!_.isArray(batch))
throw new Error('batch is not an array');
if(_.isEmpty(batch))
return log.debug('Trade fetch came back empty.');
var filterBatch = this.filter(batch);
var amount = _.size(filterBatch);
if(!amount)
return log.debug('No new trades.');
var momentBatch = this.convertDates(filterBatch);
var last = _.last(momentBatch);
var first = _.first(momentBatch);
log.debug(
'Processing', amount, 'new trades.',
'From',
first.date.format('YYYY-MM-DD HH:mm:ss'),
'UTC to',
last.date.format('YYYY-MM-DD HH:mm:ss'),
'UTC.',
'(' + first.date.from(last.date, true) + ')'
);
this.emit('new batch', {
amount: amount,
start: first.date,
end: last.date,
last: last,
first: first,
data: momentBatch
});
this.last = last[this.tid];
// we overwrote those, get unix ts back
if(this.tid === 'date')
this.last = this.last.unix();
}
TradeBatcher.prototype.filter = function(batch) {
// make sure we're not trying to count
// beyond infinity
var lastTid = _.last(batch)[this.tid];
if(lastTid === lastTid + 1)
util.die('trade tid is max int, Gekko can\'t process..');
// remove trades that have zero amount
// see @link
// https://github.com/askmike/gekko/issues/486
batch = _.filter(batch, function(trade) {
return trade.amount > 0;
});
// weed out known trades
// TODO: optimize by stopping as soon as the
// first trade is too old (reverse first)
return _.filter(batch, function(trade) {
return this.last < trade[this.tid];
}, this);
}
TradeBatcher.prototype.convertDates = function(batch) {
return _.map(_.cloneDeep(batch), function(trade) {
trade.date = moment.unix(trade.date).utc();
return trade;
});
}
module.exports = TradeBatcher;
+93
View File
@@ -0,0 +1,93 @@
// internally we only use 1m
// candles, this can easily
// convert them to any desired
// size.
// Acts as ~fake~ stream: takes
// 1m candles as input and emits
// bigger candles.
//
// input are transported candles.
var _ = require('lodash');
var util = require(__dirname + '/util');
var CandleBatcher = function(candleSize) {
if(!_.isNumber(candleSize))
throw new Error('candleSize is not a number');
this.candleSize = candleSize;
this.smallCandles = [];
this.calculatedCandles = [];
_.bindAll(this);
}
util.makeEventEmitter(CandleBatcher);
CandleBatcher.prototype.write = function(candles) {
if(!_.isArray(candles)) {
throw new Error('candles is not an array');
}
this.emitted = 0;
_.each(candles, function(candle) {
this.smallCandles.push(candle);
this.check();
}, this);
return this.emitted;
}
CandleBatcher.prototype.check = function() {
if(_.size(this.smallCandles) % this.candleSize !== 0)
return;
this.emitted++;
this.calculatedCandles.push(this.calculate());
this.smallCandles = [];
}
CandleBatcher.prototype.flush = function() {
_.each(
this.calculatedCandles,
candle => this.emit('candle', candle)
);
this.calculatedCandles = [];
}
CandleBatcher.prototype.calculate = function() {
// remove the id property of the small candle
var { id, ...first } = this.smallCandles.shift();
first.vwp = first.vwp * first.volume;
var candle = _.reduce(
this.smallCandles,
function(candle, m) {
candle.high = _.max([candle.high, m.high]);
candle.low = _.min([candle.low, m.low]);
candle.close = m.close;
candle.volume += m.volume;
candle.vwp += m.vwp * m.volume;
candle.trades += m.trades;
return candle;
},
first
);
if(candle.volume)
// we have added up all prices (relative to volume)
// now divide by volume to get the Volume Weighted Price
candle.vwp /= candle.volume;
else
// empty candle
candle.vwp = candle.open;
candle.start = first.start;
return candle;
}
module.exports = CandleBatcher;
+34
View File
@@ -0,0 +1,34 @@
// Gekko uses a custom event emitter within the GekkoStream (the plugins) to guarantee
// the correct order of events that are triggered by eachother. Turns sync events from
// LIFO into a FIFO stack based model.
//
// More details here: https://forum.gekko.wizb.it/thread-56579.html
const util = require('util');
const events = require('events');
const NativeEventEmitter = events.EventEmitter;
const GekkoEventEmitter = function() {
NativeEventEmitter.call(this);
this.defferedEvents = [];
}
util.inherits(GekkoEventEmitter, NativeEventEmitter);
// push to stack
GekkoEventEmitter.prototype.deferredEmit = function(name, payload) {
this.defferedEvents.push({name, payload});
}
// resolve FIFO
GekkoEventEmitter.prototype.broadcastDeferredEmit = function() {
if(this.defferedEvents.length === 0)
return false;
const event = this.defferedEvents.shift();
this.emit(event.name, event.payload);
return true;
}
module.exports = GekkoEventEmitter;
+121
View File
@@ -0,0 +1,121 @@
// Small writable stream wrapper that
// passes data to all `candleConsumers`.
const Writable = require('stream').Writable;
const _ = require('lodash');
const async = require('async');
const moment = require('moment');
const util = require('./util');
const env = util.gekkoEnv();
const mode = util.gekkoMode();
const config = util.getConfig();
const log = require(util.dirs().core + 'log');
var Gekko = function(plugins) {
this.plugins = plugins;
this.candleConsumers = plugins
.filter(plugin => plugin.processCandle);
Writable.call(this, {objectMode: true});
this.producers = this.plugins
.filter(p => p.meta.emits);
this.finalize = _.bind(this.finalize, this);
}
Gekko.prototype = Object.create(Writable.prototype, {
constructor: { value: Gekko }
});
if(config.debug && mode !== 'importer') {
// decorate with more debug information
Gekko.prototype._write = function(chunk, encoding, _done) {
if(chunk.isFinished) {
return this.finalize();
}
const start = moment();
var relayed = false;
var at = null;
const timer = setTimeout(() => {
if(!relayed)
log.error([
`The plugin "${at}" has not processed a candle for 1 second.`,
`This will cause Gekko to slow down or stop working completely.`
].join(' '));
}, 1000);
const flushEvents = _.after(this.candleConsumers.length, () => {
relayed = true;
clearInterval(timer);
this.flushDefferedEvents();
_done();
});
_.each(this.candleConsumers, function(c) {
at = c.meta.name;
c.processCandle(chunk, flushEvents);
}, this);
}
} else {
// skip decoration
Gekko.prototype._write = function(chunk, encoding, _done) {
if(chunk.isFinished) {
return this.finalize();
}
const flushEvents = _.after(this.candleConsumers.length, () => {
this.flushDefferedEvents();
_done();
});
_.each(this.candleConsumers, function(c) {
c.processCandle(chunk, flushEvents);
}, this);
}
}
Gekko.prototype.flushDefferedEvents = function() {
const broadcasted = _.find(
this.producers,
producer => producer.broadcastDeferredEmit()
);
// If we braodcasted anything we might have
// triggered more events, recurse until we
// have fully broadcasted everything.
if(broadcasted)
this.flushDefferedEvents();
}
Gekko.prototype.finalize = function() {
var tradingMethod = _.find(
this.candleConsumers,
c => c.meta.name === 'Trading Advisor'
);
if(!tradingMethod)
return this.shutdown();
tradingMethod.finish(this.shutdown.bind(this));
}
Gekko.prototype.shutdown = function() {
this.end();
async.eachSeries(
this.plugins,
function(c, callback) {
if (c.finalize) c.finalize(callback);
else callback();
},
() => {
// If we are a child process, we signal to the parent to kill the child once it is done
// so that is has time to process all remaining events (and send report data)
if (env === 'child-process') process.send('done');
else process.exit(0);
}
);
};
module.exports = Gekko;
+81
View File
@@ -0,0 +1,81 @@
/*
Lightweight logger, print everything that is send to error, warn
and messages to stdout (the terminal). If config.debug is set in config
also print out everything send to debug.
*/
var moment = require('moment');
var fmt = require('util').format;
var _ = require('lodash');
var util = require('./util');
var config = util.getConfig();
var debug = config.debug;
var silent = config.silent;
var sendToParent = function() {
var send = method => (...args) => {
process.send({log: method, message: args.join(' ')});
}
return {
error: send('error'),
warn: send('warn'),
info: send('info'),
write: send('write')
}
}
var Log = function() {
_.bindAll(this);
this.env = util.gekkoEnv();
if(this.env === 'standalone')
this.output = console;
else if(this.env === 'child-process')
this.output = sendToParent();
};
Log.prototype = {
_write: function(method, args, name) {
if(!name)
name = method.toUpperCase();
var message = moment().format('YYYY-MM-DD HH:mm:ss');
message += ' (' + name + '):\t';
message += fmt.apply(null, args);
this.output[method](message);
},
error: function() {
this._write('error', arguments);
},
warn: function() {
this._write('warn', arguments);
},
info: function() {
this._write('info', arguments);
},
write: function() {
var args = _.toArray(arguments);
var message = fmt.apply(null, args);
this.output.info(message);
}
}
if(debug)
Log.prototype.debug = function() {
this._write('info', arguments, 'DEBUG');
}
else
Log.prototype.debug = _.noop;
if(silent) {
Log.prototype.debug = _.noop;
Log.prototype.info = _.noop;
Log.prototype.warn = _.noop;
Log.prototype.write = _.noop;
}
module.exports = new Log;
+108
View File
@@ -0,0 +1,108 @@
var _ = require('lodash');
var util = require('../util');
var config = util.getConfig();
var dirs = util.dirs();
var log = require(dirs.core + 'log');
var moment = require('moment');
var adapter = config[config.adapter];
var Reader = require(dirs.gekko + adapter.path + '/reader');
var daterange = config.backtest.daterange;
var to = moment.utc(daterange.to);
var from = moment.utc(daterange.from);
if(to <= from)
util.die('This daterange does not make sense.')
if(!from.isValid())
util.die('invalid `from`');
if(!to.isValid())
util.die('invalid `to`');
var Market = function() {
_.bindAll(this);
this.pushing = false;
this.ended = false;
this.closed = false;
Readable.call(this, {objectMode: true});
log.write('');
log.info('\tWARNING: BACKTESTING FEATURE NEEDS PROPER TESTING');
log.info('\tWARNING: ACT ON THESE NUMBERS AT YOUR OWN RISK!');
log.write('');
this.reader = new Reader();
this.batchSize = config.backtest.batchSize;
this.iterator = {
from: from.clone(),
to: from.clone().add(this.batchSize, 'm').subtract(1, 's')
}
}
var Readable = require('stream').Readable;
Market.prototype = Object.create(Readable.prototype, {
constructor: { value: Market }
});
Market.prototype._read = _.once(function() {
this.get();
});
Market.prototype.get = function() {
if(this.iterator.to >= to) {
this.iterator.to = to;
this.ended = true;
}
this.reader.get(
this.iterator.from.unix(),
this.iterator.to.unix(),
'full',
this.processCandles
)
}
Market.prototype.processCandles = function(err, candles) {
this.pushing = true;
var amount = _.size(candles);
if(amount === 0) {
if(this.ended) {
this.closed = true;
this.reader.close();
this.push({isFinished: true});
} else {
util.die('Query returned no candles (do you have local data for the specified range?)');
}
}
if(!this.ended && amount < this.batchSize) {
var d = function(ts) {
return moment.unix(ts).utc().format('YYYY-MM-DD HH:mm:ss');
}
var from = d(_.first(candles).start);
var to = d(_.last(candles).start);
log.warn(`Simulation based on incomplete market data (${this.batchSize - amount} missing between ${from} and ${to}).`);
}
_.each(candles, function(c, i) {
c.start = moment.unix(c.start);
this.push(c);
}, this);
this.pushing = false;
this.iterator = {
from: this.iterator.from.clone().add(this.batchSize, 'm'),
to: this.iterator.from.clone().add(this.batchSize * 2, 'm').subtract(1, 's')
}
if(!this.closed)
this.get();
}
module.exports = Market;
+117
View File
@@ -0,0 +1,117 @@
var _ = require('lodash');
var util = require('../util');
var config = util.getConfig();
var dirs = util.dirs();
var log = require(dirs.core + 'log');
var moment = require('moment');
var gekkoEnv = util.gekkoEnv();
var adapter = config[config.adapter];
var daterange = config.importer.daterange;
var from = moment.utc(daterange.from);
if(daterange.to) {
var to = moment.utc(daterange.to);
} else {
var to = moment().utc();
log.debug(
'No end date specified for importing, setting to',
to.format()
);
}
log.debug(to.format());
if(!from.isValid())
util.die('invalid `from`');
if(!to.isValid())
util.die('invalid `to`');
var TradeBatcher = require(dirs.budfox + 'tradeBatcher');
var CandleManager = require(dirs.budfox + 'candleManager');
var exchangeChecker = require(dirs.gekko + 'exchange/exchangeChecker');
var error = exchangeChecker.cantFetchFullHistory(config.watch);
if(error)
util.die(error, true);
var fetcher = require(dirs.importers + config.watch.exchange);
if(to <= from)
util.die('This daterange does not make sense.')
var Market = function() {
_.bindAll(this);
this.exchangeSettings = exchangeChecker.settings(config.watch);
this.tradeBatcher = new TradeBatcher(this.exchangeSettings.tid);
this.candleManager = new CandleManager;
this.fetcher = fetcher({
to: to,
from: from
});
this.done = false;
this.fetcher.bus.on(
'trades',
this.processTrades
);
this.fetcher.bus.on(
'done',
function() {
this.done = true;
}.bind(this)
)
this.tradeBatcher.on(
'new batch',
this.candleManager.processTrades
);
this.candleManager.on(
'candles',
this.pushCandles
);
Readable.call(this, {objectMode: true});
this.get();
}
var Readable = require('stream').Readable;
Market.prototype = Object.create(Readable.prototype, {
constructor: { value: Market }
});
Market.prototype._read = _.noop;
Market.prototype.pushCandles = function(candles) {
_.each(candles, this.push);
}
Market.prototype.get = function() {
this.fetcher.fetch();
}
Market.prototype.processTrades = function(trades) {
this.tradeBatcher.write(trades);
if(this.done) {
log.info('Done importing!');
this.emit('end');
return;
}
if(_.size(trades) && gekkoEnv === 'child-process') {
let lastAtTS = _.last(trades).date;
let lastAt = moment.unix(lastAtTS).utc().format();
process.send({event: 'marketUpdate', payload: lastAt});
}
setTimeout(this.get, 1000);
}
module.exports = Market;
+91
View File
@@ -0,0 +1,91 @@
// a leech market is "semi-realtime" and pulls out candles of a
// database (which is expected to be updated regularly, like with a
// realtime market running in parallel).
const _ = require('lodash');
const moment = require('moment');
const util = require('../util');
const dirs = util.dirs();
const config = util.getConfig();
const exchangeChecker = require(dirs.gekko + 'exchange/exchangeChecker');
const adapter = config[config.adapter];
const Reader = require(dirs.gekko + adapter.path + '/reader');
const TICKINTERVAL = 20 * 1000; // 20 seconds
const slug = config.watch.exchange.toLowerCase();
const exchange = exchangeChecker.getExchangeCapabilities(slug);
if(!exchange)
util.die(`Unsupported exchange: ${slug}`)
const error = exchangeChecker.cantMonitor(config.watch);
if(error)
util.die(error, true);
if(config.market.from)
var fromTs = moment.utc(config.market.from).unix();
else
var fromTs = moment().startOf('minute').unix();
var Market = function() {
_.bindAll(this);
Readable.call(this, {objectMode: true});
this.reader = new Reader();
this.latestTs = fromTs;
setInterval(
this.get,
TICKINTERVAL
);
}
var Readable = require('stream').Readable;
Market.prototype = Object.create(Readable.prototype, {
constructor: { value: Market }
});
Market.prototype._read = _.once(function() {
this.get();
});
Market.prototype.get = function() {
var future = moment().add(1, 'minute').unix();
this.reader.get(
this.latestTs,
future,
'full',
this.processCandles
)
}
Market.prototype.processCandles = function(err, candles) {
var amount = _.size(candles);
if(amount === 0) {
// no new candles!
return;
}
// TODO:
// verify that the correct amount of candles was passed:
//
// if `this.latestTs` was at 10:00 and we receive 3 candles with the latest at 11:00
// we know we are missing 57 candles...
_.each(candles, function(c, i) {
c.start = moment.unix(c.start).utc();
this.push(c);
}, this);
this.latestTs = _.last(candles).start.unix() + 1;
}
module.exports = Market;
+19
View File
@@ -0,0 +1,19 @@
const _ = require('lodash');
const util = require('../util');
const dirs = util.dirs();
const exchangeChecker = require(dirs.gekko + 'exchange/exchangeChecker');
const config = util.getConfig();
const slug = config.watch.exchange.toLowerCase();
const exchange = exchangeChecker.getExchangeCapabilities(slug);
if(!exchange)
util.die(`Unsupported exchange: ${slug}`)
const error = exchangeChecker.cantMonitor(config.watch);
if(error)
util.die(error, true);
module.exports = require(dirs.budfox + 'budfox');
+252
View File
@@ -0,0 +1,252 @@
/*
A pipeline implements a full Gekko Flow based on a config and
a mode. The mode is an abstraction that tells Gekko what market
to load (realtime, backtesting or importing) while making sure
all enabled plugins are actually supported by that market.
Read more here:
https://gekko.wizb.it/docs/internals/architecture.html
*/
var util = require('./util');
var dirs = util.dirs();
var _ = require('lodash');
var async = require('async');
var log = require(dirs.core + 'log');
var pipeline = (settings) => {
var mode = settings.mode;
var config = settings.config;
// prepare a GekkoStream
var GekkoStream = require(dirs.core + 'gekkoStream');
// all plugins
var plugins = [];
// all emitting plugins
var emitters = {};
// all plugins interested in candles
var candleConsumers = [];
// utility to check and load plugins.
var pluginHelper = require(dirs.core + 'pluginUtil');
// meta information about every plugin that tells Gekko
// something about every available plugin
var pluginParameters = require(dirs.gekko + 'plugins');
// meta information about the events plugins can broadcast
// and how they should hooked up to consumers.
var subscriptions = require(dirs.gekko + 'subscriptions');
var market;
// Instantiate each enabled plugin
var loadPlugins = function(next) {
// load all plugins
async.mapSeries(
pluginParameters,
pluginHelper.load,
function(error, _plugins) {
if(error)
return util.die(error, true);
plugins = _.compact(_plugins);
next();
}
);
};
// Some plugins emit their own events, store
// a reference to those plugins.
var referenceEmitters = function(next) {
_.each(plugins, function(plugin) {
if(plugin.meta.emits)
emitters[plugin.meta.slug] = plugin;
});
next();
}
// Subscribe all plugins to other emitting plugins
var subscribePlugins = function(next) {
// events broadcasted by plugins
var pluginSubscriptions = _.filter(
subscriptions,
sub => sub.emitter !== 'market'
);
// some events can be broadcasted by different
// plugins, however the pipeline only allows a single
// emitting plugin for each event to be enabled.
_.each(
pluginSubscriptions.filter(s => _.isArray(s.emitter)),
subscription => {
// cache full list
subscription.emitters = subscription.emitter;
var singleEventEmitters = subscription.emitter
.filter(
s => _.size(plugins.filter(p => p.meta.slug === s))
);
if(_.size(singleEventEmitters) > 1) {
var error = `Multiple plugins are broadcasting`;
error += ` the event "${subscription.event}" (${singleEventEmitters.join(',')}).`;
error += 'This is unsupported.'
util.die(error);
} else {
subscription.emitter = _.first(singleEventEmitters);
}
}
);
// subscribe interested plugins to
// emitting plugins
_.each(plugins, function(plugin) {
_.each(pluginSubscriptions, function(sub) {
if(plugin[sub.handler]) {
// if a plugin wants to listen
// to something disabled
if(!emitters[sub.emitter]) {
if(!plugin.meta.greedy) {
let emitterMessage;
if(sub.emitters) {
emitterMessage = 'all of the emitting plugins [ ';
emitterMessage += sub.emitters.join(', ');
emitterMessage += ' ] are disabled.';
} else {
emitterMessage += 'the emitting plugin (' + sub.emitter;
emitterMessage += ')is disabled.'
}
log.error([
plugin.meta.name,
'wanted to listen to event',
sub.event + ',',
'however',
emitterMessage,
plugin.meta.name + ' might malfunction because of it.'
].join(' '));
}
return;
}
// attach handler
emitters[sub.emitter]
.on(sub.event,
plugin[
sub.handler
])
}
});
});
// events broadcasted by the market
var marketSubscriptions = _.filter(
subscriptions,
{emitter: 'market'}
);
// subscribe plugins to the market
_.each(plugins, function(plugin) {
_.each(marketSubscriptions, function(sub) {
if(plugin[sub.handler]) {
if(sub.event === 'candle')
candleConsumers.push(plugin);
}
});
});
next();
}
var prepareMarket = function(next) {
if(mode === 'backtest' && config.backtest.daterange === 'scan')
require(dirs.core + 'prepareDateRange')(next);
else
next();
}
var setupMarket = function(next) {
// load a market based on the config (or fallback to mode)
let marketType;
if(config.market)
marketType = config.market.type;
else
marketType = mode;
var Market = require(dirs.markets + marketType);
market = new Market(config);
next();
}
var subscribePluginsToMarket = function(next) {
// events broadcasted by the market
var marketSubscriptions = _.filter(
subscriptions,
{emitter: 'market'}
);
_.each(plugins, function(plugin) {
_.each(marketSubscriptions, function(sub) {
if(sub.event === 'candle')
// these are handled via the market stream
return;
if(plugin[sub.handler]) {
market.on(sub.event, plugin[sub.handler]);
}
});
});
next();
}
log.info('Setting up Gekko in', mode, 'mode');
log.info('');
async.series(
[
loadPlugins,
referenceEmitters,
subscribePlugins,
prepareMarket,
setupMarket,
subscribePluginsToMarket
],
function() {
var gekkoStream = new GekkoStream(plugins);
market
.pipe(gekkoStream)
// convert JS objects to JSON string
// .pipe(new require('stringify-stream')())
// output to standard out
// .pipe(process.stdout);
market.on('end', gekkoStream.finalize);
}
);
}
module.exports = pipeline;
+120
View File
@@ -0,0 +1,120 @@
var _ = require('lodash');
var async = require('async');
var Emitter = require('./emitter');
var util = require(__dirname + '/util');
var log = require(util.dirs().core + 'log');
var config = util.getConfig();
var pluginDir = util.dirs().plugins;
var gekkoMode = util.gekkoMode();
var inherits = require('util').inherits;
var pluginHelper = {
// Checks whether we can load a module
// @param Object plugin
// plugin config object
// @return String
// error message if we can't
// use the module.
cannotLoad: function(plugin) {
// verify plugin dependencies are installed
if(_.has(plugin, 'dependencies'))
var error = false;
_.each(plugin.dependencies, function(dep) {
try {
var a = require(dep.module);
}
catch(e) {
log.error('ERROR LOADING DEPENDENCY', dep.module);
if(!e.message) {
log.error(e);
util.die();
}
if(!e.message.startsWith('Cannot find module'))
return util.die(e);
error = [
'The plugin',
plugin.slug,
'expects the module',
dep.module,
'to be installed.',
'However it is not, install',
'it by running: \n\n',
'\tnpm install',
dep.module + '@' + dep.version
].join(' ');
}
});
return error;
},
// loads a plugin
//
// @param Object plugin
// plugin config object
// @param Function next
// callback
load: function(plugin, next) {
plugin.config = config[plugin.slug];
if(!plugin.config || !plugin.config.enabled)
return next();
if(!_.contains(plugin.modes, gekkoMode)) {
log.warn(
'The plugin',
plugin.name,
'does not support the mode',
gekkoMode + '.',
'It has been disabled.'
)
return next();
}
log.info('Setting up:');
log.info('\t', plugin.name);
log.info('\t', plugin.description);
var cannotLoad = pluginHelper.cannotLoad(plugin);
if(cannotLoad)
return next(cannotLoad);
if(plugin.path)
var Constructor = require(pluginDir + plugin.path(config));
else
var Constructor = require(pluginDir + plugin.slug);
if(plugin.async) {
inherits(Constructor, Emitter);
var instance = new Constructor(util.defer(function(err) {
next(err, instance);
}), plugin);
Emitter.call(instance);
instance.meta = plugin;
} else {
inherits(Constructor, Emitter);
var instance = new Constructor(plugin);
Emitter.call(instance);
instance.meta = plugin;
_.defer(function() {
next(null, instance);
});
}
if(!plugin.silent)
log.info('\n');
}
}
module.exports = pluginHelper;
+67
View File
@@ -0,0 +1,67 @@
var _ = require('lodash');
var prompt = require('prompt-lite');
var moment = require('moment');
var util = require('./util');
var config = util.getConfig();
var dirs = util.dirs();
var log = require(dirs.core + 'log');
var scan = require(dirs.tools + 'dateRangeScanner');
// helper to store the evenutally detected
// daterange.
var setDateRange = function(from, to) {
config.backtest.daterange = {
from: moment.unix(from).utc().format(),
to: moment.unix(to).utc().format(),
};
util.setConfig(config);
}
module.exports = function(done) {
scan((err, ranges) => {
if(_.size(ranges) === 0)
util.die('No history found for this market', true);
if(_.size(ranges) === 1) {
var r = _.first(ranges);
log.info('Gekko was able to find a single daterange in the locally stored history:');
log.info('\t', 'from:', moment.unix(r.from).utc().format('YYYY-MM-DD HH:mm:ss'));
log.info('\t', 'to:', moment.unix(r.to).utc().format('YYYY-MM-DD HH:mm:ss'));
setDateRange(r.from, r.to);
return done();
}
log.info(
'Gekko detected multiple dateranges in the locally stored history.',
'Please pick the daterange you are interested in testing:'
);
_.each(ranges, (range, i) => {
log.info('\t\t', `OPTION ${i + 1}:`);
log.info('\t', 'from:', moment.unix(range.from).utc().format('YYYY-MM-DD HH:mm:ss'));
log.info('\t', 'to:', moment.unix(range.to).utc().format('YYYY-MM-DD HH:mm:ss'));
});
prompt.get({name: 'option'}, (err, result) => {
var option = parseInt(result.option);
if(option === NaN)
util.die('Not an option..', true);
var range = ranges[option - 1];
if(!range)
util.die('Not an option..', true);
setDateRange(range.from, range.to);
return done();
});
});
}
+17
View File
@@ -0,0 +1,17 @@
const stats = require('stats-lite');
const lodash = require('lodash');
// simply monkey patch the stats with other stuff we
// need and pass on.
// sharpe ratio
//
// @param returns (array - list of returns)
// @param rfreturn (number - risk free return)
//
stats.sharpe = (returns, rfreturn) => {
return (stats.mean(returns) - rfreturn) / stats.stdev(returns);
}
module.exports = stats;
+2297
View File
File diff suppressed because it is too large. Load diff
+100
View File
@@ -0,0 +1,100 @@
// TODO: properly handle a daterange for which no data is available.
const batchSize = 1000;
const _ = require('lodash');
const fs = require('fs');
const moment = require('moment');
const util = require('../../core/util');
const config = util.getConfig();
const dirs = util.dirs();
const log = require(dirs.core + '/log');
const adapter = config[config.adapter];
const Reader = require(dirs.gekko + adapter.path + '/reader');
const daterange = config.daterange;
const CandleBatcher = require(dirs.core + 'candleBatcher');
const to = moment.utc(daterange.to).startOf('minute');
const from = moment.utc(daterange.from).startOf('minute');
const toUnix = to.unix();
if(to <= from)
util.die('This daterange does not make sense.')
if(!from.isValid())
util.die('invalid `from`');
if(!to.isValid())
util.die('invalid `to`');
let iterator = {
from: from.clone(),
to: from.clone().add(batchSize, 'm').subtract(1, 's')
}
var DONE = false;
var result = [];
var reader = new Reader();
var batcher;
var next;
var doneFn = () => {
process.nextTick(() => {
next(result);
})
};
module.exports = function(candleSize, _next) {
next = _.once(_next);
batcher = new CandleBatcher(candleSize)
.on('candle', handleBatchedCandles);
getBatch();
}
const getBatch = () => {
reader.get(
iterator.from.unix(),
iterator.to.unix(),
'full',
handleCandles
)
}
const shiftIterator = () => {
iterator = {
from: iterator.from.clone().add(batchSize, 'm'),
to: iterator.from.clone().add(batchSize * 2, 'm').subtract(1, 's')
}
}
const handleCandles = (err, data) => {
if(err) {
console.error(err);
util.die('Encountered an error..')
}
if(_.size(data) && _.last(data).start >= toUnix || iterator.from.unix() >= toUnix)
DONE = true;
batcher.write(data);
batcher.flush();
if(DONE) {
reader.close();
setTimeout(doneFn, 100);
} else {
shiftIterator();
getBatch();
}
}
const handleBatchedCandles = candle => {
result.push(candle);
}
+42
View File
@@ -0,0 +1,42 @@
const fs = require('fs');
const _ = require('lodash');
const toml = require('toml');
const util = require('../util');
const dirs = util.dirs();
const getTOML = function(fileName) {
var raw = fs.readFileSync(fileName);
return toml.parse(raw);
}
// build a config object out of a directory of TOML files
module.exports = function() {
const configDir = util.dirs().config;
let _config = getTOML(configDir + 'general.toml');
fs.readdirSync(configDir + 'plugins').forEach(function(pluginFile) {
let pluginName = _.first(pluginFile.split('.'))
_config[pluginName] = getTOML(configDir + 'plugins/' + pluginFile);
});
// attach the proper adapter
let adapter = _config.adapter;
_config[adapter] = getTOML(configDir + 'adapters/' + adapter + '.toml');
if(_config.tradingAdvisor.enabled) {
// also load the strat
let strat = _config.tradingAdvisor.method;
let stratFile = configDir + 'strategies/' + strat + '.toml';
if(!fs.existsSync(stratFile))
util.die('Cannot find the strategy config file for ' + strat);
_config[strat] = getTOML(stratFile);
}
const mode = util.gekkoMode();
if(mode === 'backtest')
_config.backtest = getTOML(configDir + 'backtest.toml');
return _config;
}
+233
View File
@@ -0,0 +1,233 @@
var _ = require('lodash');
var fs = require('fs');
var moment = require('moment');
var util = require('../util');
var config = util.getConfig();
var dirs = util.dirs();
var log = require(dirs.core + '/log');
var Stitcher = function(batcher) {
this.batcher = batcher;
}
Stitcher.prototype.ago = function(ts) {
var now = moment().utc();
var then = moment.unix(ts).utc();
return now.diff(then, 'minutes') + ' minutes ago';
}
Stitcher.prototype.verifyExchange = function() {
require(dirs.gekko + 'exchange/dependencyCheck');
const exchangeChecker = require(dirs.gekko + 'exchange/exchangeChecker');
const slug = config.watch.exchange.toLowerCase();
let exchange;
try {
exchange = exchangeChecker.getExchangeCapabilities(slug);
} catch(e) {
util.die(e.message);
}
if(!exchange)
util.die(`Unsupported exchange: ${slug}`);
var error = exchangeChecker.cantMonitor(config.watch);
if(error)
util.die(error, true);
}
Stitcher.prototype.prepareHistoricalData = function(done) {
this.verifyExchange();
// - step 1: check most recent stored candle window
// - step 2: check oldest trade reachable by API
// - step 3: see if overlap
// - step 4: feed candle stream into CandleBatcher
if(config.tradingAdvisor.historySize === 0)
return done();
var requiredHistory = config.tradingAdvisor.candleSize * config.tradingAdvisor.historySize;
var Reader = require(dirs.plugins + config.adapter + '/reader');
this.reader = new Reader;
log.info(
'\tThe trading method requests',
requiredHistory,
'minutes of historic data. Checking availablity..'
);
var endTime = moment().utc().startOf('minute');
var idealStartTime = endTime.clone().subtract(requiredHistory, 'm');
this.reader.mostRecentWindow(idealStartTime, endTime, function(localData) {
// now we know what data is locally available, what
// data would we need from the exchange?
if(!localData) {
log.info('\tNo usable local data available, trying to get as much as possible from the exchange..');
var idealExchangeStartTime = idealStartTime.clone();
var idealExchangeStartTimeTS = idealExchangeStartTime.unix();
}
else if (idealStartTime.unix() < localData.from) {
log.info('\tLocal data is still too recent, trying to get as much as possible from the exchange');
var idealExchangeStartTime = idealStartTime.clone();
var idealExchangeStartTimeTS = idealExchangeStartTime.unix();
}
else {
log.debug('\tAvailable local data:');
log.debug('\t\tfrom:', this.ago(localData.from));
log.debug('\t\tto:', this.ago(localData.to));
log.info('\tUsable local data available, trying to match with exchange data..')
// local data is available, we need the next minute
// make sure we grab back in history far enough
var secondsOverlap = 60 * 15; // 15 minutes
var idealExchangeStartTimeTS = localData.to - secondsOverlap;
var idealExchangeStartTime = moment.unix(idealExchangeStartTimeTS).utc();
// already set the
util.setConfigProperty(
'tradingAdvisor',
'firstFetchSince',
idealExchangeStartTimeTS
);
}
// Limit the history Gekko can try to get from the exchange.
var minutesAgo = endTime.diff(idealExchangeStartTime, 'minutes');
var maxMinutesAgo = 4 * 60; // 4 hours
if(minutesAgo > maxMinutesAgo) {
log.info('\tPreventing Gekko from requesting', minutesAgo, 'minutes of history.');
idealExchangeStartTime = endTime.clone().subtract(maxMinutesAgo, 'minutes');
idealExchangeStartTimeTS = idealExchangeStartTime.unix();
}
log.debug('\tFetching exchange data since', this.ago(idealExchangeStartTimeTS))
this.checkExchangeTrades(idealExchangeStartTime, function(err, exchangeData) {
log.debug('\tAvailable exchange data:');
log.debug('\t\tfrom:', this.ago(exchangeData.from));
log.debug('\t\tto:', this.ago(exchangeData.to));
// in case we have limited local data, and the
// exchange would offer more than we have: ignore
// the local data..
if(
localData &&
exchangeData &&
exchangeData.from < localData.from
) {
log.debug('\tExchange offered more data than locally available. Ignoring local data');
localData = false;
}
var stitchable = localData && exchangeData.from <= localData.to;
if(stitchable) {
log.debug('\tStitching datasets');
// we can combine local data with exchange data
if(idealStartTime.unix() >= localData.from) {
log.info(
'\tFull history locally available.',
'Seeding the trading method with all required historical candles.'
);
} else {
// stitchable but not enough
log.info(
'\tPartial history locally available, but',
Math.round((localData.from - idealStartTime.unix()) / 60),
'minutes are missing.')
log.info('\tSeeding the trading method with',
'partial historical data (Gekko needs more time before',
'it can give advice).'
);
}
// seed all historic data up to the point the exchange can provide.
var from = localData.from;
var to = moment.unix(exchangeData.from).utc()
.startOf('minute')
.subtract(1, 'minute')
.unix();
log.debug('\tSeeding with:');
log.debug('\t\tfrom:', this.ago(from));
log.debug('\t\tto:', this.ago(to));
return this.seedLocalData(from, to, done);
} else if(!stitchable) {
log.debug('\tUnable to stitch datasets.')
// we cannot use any local data..
log.info(
'\tNot seeding locally available data to the trading method.'
);
if(exchangeData.from < idealExchangeStartTimeTS) {
log.info('\tHowever the exchange returned enough data anyway!');
} else if(localData) {
log.info(
'\tThe exchange does not return enough data.',
Math.round((localData.from - idealStartTime.unix()) / 60),
'minutes are still missing.'
);
}
}
done();
}.bind(this));
}.bind(this));
}
Stitcher.prototype.checkExchangeTrades = function(since, next) {
var provider = config.watch.exchange.toLowerCase();
var DataProvider = require(util.dirs().gekko + 'exchange/wrappers/' + provider);
var exchangeConfig = config.watch;
// include trader config if trading is enabled
if (_.isObject(config.trader) && config.trader.enabled) {
exchangeConfig = _.extend(config.watch, config.trader);
}
var watcher = new DataProvider(exchangeConfig);
watcher.getTrades(since, function(e, d) {
if(e) {
util.die(e.message);
}
if(_.isEmpty(d))
return util.die(
`Gekko tried to retrieve data since ${since.format('YYYY-MM-DD HH:mm:ss')}, however
${provider} did not return any trades.`
);
next(e, {
from: _.first(d).date,
to: _.last(d).date
})
});
}
Stitcher.prototype.seedLocalData = function(from, to, next) {
this.reader.get(from, to, 'full', function(err, rows) {
rows = _.map(rows, row => {
row.start = moment.unix(row.start);
return row;
});
this.batcher.write(rows);
this.batcher.flush();
this.reader.close();
next();
}.bind(this));
}
module.exports = Stitcher;
+138
View File
@@ -0,0 +1,138 @@
var BATCH_SIZE = 60; // minutes
var MISSING_CANDLES_ALLOWED = 3; // minutes, per batch
var _ = require('lodash');
var moment = require('moment');
var async = require('async');
var util = require('../util');
var config = util.getConfig();
var dirs = util.dirs();
var log = require(dirs.core + 'log');
var adapter = config[config.adapter];
var Reader = require(dirs.gekko + adapter.path + '/reader');
var reader = new Reader();
// todo: rewrite with generators or async/await..
var scan = function(done) {
log.info('Scanning local history for backtestable dateranges.');
reader.tableExists('candles', (err, exists) => {
if(err)
return done(err, null, reader);
if(!exists)
return done(null, [], reader);
async.parallel({
boundry: reader.getBoundry,
available: reader.countTotal
}, (err, res) => {
var first = res.boundry.first;
var last = res.boundry.last;
var optimal = (last - first) / 60;
log.debug('Available', res.available);
log.debug('Optimal', optimal);
// There is a candle for every minute
if(res.available === optimal + 1) {
log.info('Gekko is able to fully use the local history.');
return done(false, [{
from: first,
to: last
}], reader);
}
// figure out where the gaps are..
var missing = optimal - res.available + 1;
log.info(`The database has ${missing} candles missing, Figuring out which ones...`);
var iterator = {
from: last - (BATCH_SIZE * 60),
to: last
}
var batches = [];
// loop through all candles we have
// in batches and track whether they
// are complete
async.whilst(
() => {
return iterator.from > first
},
next => {
var from = iterator.from;
var to = iterator.to;
reader.count(
from,
iterator.to,
(err, count) => {
var complete = count + MISSING_CANDLES_ALLOWED > BATCH_SIZE;
if(complete)
batches.push({
to: to,
from: from
});
next();
}
);
iterator.from -= BATCH_SIZE * 60;
iterator.to -= BATCH_SIZE * 60;
},
() => {
if(batches.length === 0) {
return done(null, [], reader);
}
// batches is now a list like
// [ {from: unix, to: unix } ]
var ranges = [ batches.shift() ];
_.each(batches, batch => {
var curRange = _.last(ranges);
if(batch.to === curRange.from)
curRange.from = batch.from;
else
ranges.push( batch );
})
// we have been counting chronologically reversed
// (backwards, from now into the past), flip definitions
ranges = ranges.reverse();
_.map(ranges, r => {
return {
from: r.to,
to: r.from
}
});
// ranges is now a list like
// [ {from: unix, to: unix } ]
//
// it contains all valid dataranges available for the
// end user.
return done(false, ranges, reader);
}
)
});
});
}
module.exports = scan;
+1002
View File
File diff suppressed because it is too large. Load diff
+189
View File
@@ -0,0 +1,189 @@
var moment = require('moment');
var _ = require('lodash');
var path = require('path');
var fs = require('fs');
var semver = require('semver');
var program = require('commander');
var startTime = moment();
var _config = false;
var _package = false;
var _nodeVersion = false;
var _gekkoMode = false;
var _gekkoEnv = false;
var _args = false;
// helper functions
var util = {
getConfig: function() {
// cache
if(_config)
return _config;
if(!program.config)
util.die('Please specify a config file.', true);
if(!fs.existsSync(util.dirs().gekko + program.config))
util.die('Cannot find the specified config file.', true);
_config = require(util.dirs().gekko + program.config);
return _config;
},
// overwrite the whole config
setConfig: function(config) {
_config = config;
},
setConfigProperty: function(parent, key, value) {
if(parent)
_config[parent][key] = value;
else
_config[key] = value;
},
getVersion: function() {
return util.getPackage().version;
},
getPackage: function() {
if(_package)
return _package;
_package = JSON.parse( fs.readFileSync(__dirname + '/../package.json', 'utf8') );
return _package;
},
getRequiredNodeVersion: function() {
return util.getPackage().engines.node;
},
recentNode: function() {
var required = util.getRequiredNodeVersion();
return semver.satisfies(process.version, required);
},
// check if two moments are corresponding
// to the same time
equals: function(a, b) {
return !(a < b || a > b)
},
minToMs: function(min) {
return min * 60 * 1000;
},
defer: function(fn) {
return function(args) {
var args = _.toArray(arguments);
return _.defer(function() { fn.apply(this, args) });
}
},
logVersion: function() {
return `Gekko version: v${util.getVersion()}`
+ `\nNodejs version: ${process.version}`;
},
die: function(m, soft) {
if(_gekkoEnv === 'child-process') {
return process.send({type: 'error', error: '\n ERROR: ' + m + '\n'});
}
var log = console.log.bind(console);
if(m) {
if(soft) {
log('\n ERROR: ' + m + '\n\n');
} else {
log(`\nGekko encountered an error and can\'t continue`);
log('\nError:\n');
log(m, '\n\n');
log('\nMeta debug info:\n');
log(util.logVersion());
log('');
}
}
process.exit(1);
},
dirs: function() {
var ROOT = __dirname + '/../';
return {
gekko: ROOT,
core: ROOT + 'core/',
markets: ROOT + 'core/markets/',
exchanges: ROOT + 'exchange/wrappers/',
plugins: ROOT + 'plugins/',
methods: ROOT + 'strategies/',
indicators: ROOT + 'strategies/indicators/',
budfox: ROOT + 'core/budfox/',
importers: ROOT + 'importers/exchanges/',
tools: ROOT + 'core/tools/',
workers: ROOT + 'core/workers/',
web: ROOT + 'web/',
config: ROOT + 'config/',
broker: ROOT + 'exchange/'
}
},
inherit: function(dest, source) {
require('util').inherits(
dest,
source
);
},
makeEventEmitter: function(dest) {
util.inherit(dest, require('events').EventEmitter);
},
setGekkoMode: function(mode) {
_gekkoMode = mode;
},
gekkoMode: function() {
if(_gekkoMode)
return _gekkoMode;
if(program['import'])
return 'importer';
else if(program.backtest)
return 'backtest';
else
return 'realtime';
},
gekkoModes: function() {
return [
'importer',
'backtest',
'realtime'
]
},
setGekkoEnv: function(env) {
_gekkoEnv = env;
},
gekkoEnv: function() {
return _gekkoEnv || 'standalone';
},
launchUI: function() {
if(program['ui'])
return true;
else
return false;
},
getStartTime: function() {
return startTime;
},
}
// NOTE: those options are only used
// in stand alone mode
program
.version(util.logVersion())
.option('-c, --config <file>', 'Config file')
.option('-b, --backtest', 'backtesting mode')
.option('-i, --import', 'importer mode')
.option('--ui', 'launch a web UI')
.parse(process.argv);
// make sure the current node version is recent enough
if(!util.recentNode())
util.die([
'Your local version of Node.js is too old. ',
'You have ',
process.version,
' and you need atleast ',
util.getRequiredNodeVersion()
].join(''), true);
module.exports = util;
+54
View File
@@ -0,0 +1,54 @@
var _ = require('lodash');
var moment = require('moment');
var async = require('async');
var os = require('os');
var util = require('../../util');
var dirs = util.dirs();
var dateRangeScan = require('../dateRangeScan/parent');
module.exports = function(config, done) {
util.setConfig(config);
var adapter = config[config.adapter];
var scan = require(dirs.gekko + adapter.path + '/scanner');
scan((err, markets) => {
if(err)
return done(err);
let numCPUCores = os.cpus().length;
if(numCPUCores === undefined)
numCPUCores = 1;
async.eachLimit(markets, numCPUCores, (market, next) => {
let marketConfig = _.clone(config);
marketConfig.watch = market;
dateRangeScan(marketConfig, (err, ranges) => {
if(err)
return next();
market.ranges = ranges;
next();
});
}, err => {
let resp = {
datasets: [],
errors: []
}
markets.forEach(market => {
if(market.ranges)
resp.datasets.push(market);
else
resp.errors.push(market);
})
done(err, resp);
})
});
}
+26
View File
@@ -0,0 +1,26 @@
var util = require(__dirname + '/../../util');
var dirs = util.dirs();
var ipc = require('relieve').IPCEE(process);
ipc.on('start', config => {
// force correct gekko env
util.setGekkoEnv('child-process');
// force disable debug
config.debug = false;
// persist config
util.setConfig(config);
var scan = require(dirs.tools + 'dateRangeScanner');
scan(
(err, ranges, reader) => {
reader.close();
ipc.send('ranges', ranges);
process.exit(0);
}
);
});
+21
View File
@@ -0,0 +1,21 @@
var ForkTask = require('relieve').tasks.ForkTask;
var fork = require('child_process').fork;
module.exports = function(config, done) {
var debug = typeof v8debug === 'object';
if (debug) {
process.execArgv = [];
}
task = new ForkTask(fork(__dirname + '/child'));
task.send('start', config);
task.once('ranges', ranges => {
return done(false, ranges);
});
task.on('exit', code => {
if(code !== 0)
done('ERROR, unable to scan dateranges, please check the console.');
});
}
+28
View File
@@ -0,0 +1,28 @@
var start = (config, candleSize, daterange) => {
var util = require(__dirname + '/../../util');
// force correct gekko env
util.setGekkoEnv('child-process');
// force disable debug
config.debug = false;
util.setConfig(config);
var dirs = util.dirs();
var load = require(dirs.tools + 'candleLoader');
load(config.candleSize, candles => {
process.send(candles);
})
}
process.send('ready');
process.on('message', (m) => {
if(m.what === 'start')
start(m.config, m.candleSize, m.daterange);
});
process.on('disconnect', function() {
process.exit(0);
})
+68
View File
@@ -0,0 +1,68 @@
// example usage:
// let config = {
// watch: {
// exchange: 'poloniex',
// currency: 'USDT',
// asset: 'BTC'
// },
// daterange: {
// from: '2016-05-22 11:22',
// to: '2016-06-03 19:56'
// },
// adapter: 'sqlite',
// sqlite: {
// path: 'plugins/sqlite',
// dataDirectory: 'history',
// version: 0.1,
// dependencies: [{
// module: 'sqlite3',
// version: '3.1.4'
// }]
// },
// candleSize: 100
// }
// module.exports(config, function(err, data) {
// console.log('FINAL CALLBACK');
// console.log('err', err);
// console.log('data', data.length);
// })
const fork = require('child_process').fork;
const _ = require('lodash');
module.exports = (config, callback) => {
var debug = typeof v8debug === 'object';
if (debug) {
process.execArgv = [];
}
const child = fork(__dirname + '/child');
const message = {
what: 'start',
config
}
const done = _.once(callback);
child.on('message', function(m) {
if(m === 'ready')
return child.send(message);
// else we are done and have candles!
done(null, m);
if (this.connected) {
this.disconnect();
}
});
child.on('exit', code => {
if(code !== 0)
done('ERROR, unable to load candles, please check the console.');
});
}
+66
View File
@@ -0,0 +1,66 @@
/*
Gekko is a Bitcoin trading bot for popular Bitcoin exchanges written
in node, it features multiple trading methods using technical analysis.
If you are interested in how Gekko works, read more about Gekko's
architecture here:
https://github.com/askmike/gekko/blob/stable/docs/internals/architecture.md
Disclaimer:
USE AT YOUR OWN RISK!
The author of this project is NOT responsible for any damage or loss caused
by this software. There can be bugs and the bot may not perform as expected
or specified. Please consider testing it first with paper trading and/or
backtesting on historical data. Also look at the code to see what how
it is working.
*/
var start = (mode, config) => {
var util = require(__dirname + '/../../util');
// force correct gekko env
util.setGekkoEnv('child-process');
var dirs = util.dirs();
// force correct gekko mode & config
util.setGekkoMode(mode);
util.setConfig(config);
var pipeline = require(dirs.core + 'pipeline');
pipeline({
config: config,
mode: mode
});
}
process.send('ready');
process.on('message', function(m) {
if(m.what === 'start')
start(m.mode, m.config);
if(m.what === 'exit')
process.exit(0);
});
process.on('disconnect', function() {
console.log('disconnect');
process.exit(-1);
})
process
.on('unhandledRejection', (message, p) => {
console.error('unhandledRejection', message);
process.send({type: 'error', message: message});
})
.on('uncaughtException', err => {
console.error('uncaughtException', err);
process.send({type: 'error', error: err});
process.exit(1);
});
@@ -0,0 +1,25 @@
// Relay the backtest message it when it comes in.
module.exports = done => {
let backtest;
return {
message: message => {
if(message.type === 'error') {
done(message.error);
}
if(message.backtest) {
done(null, message.backtest);
}
},
exit: status => {
if(status !== 0) {
if(backtest)
console.error('Child process died after finishing backtest');
else
done('Child process has died.');
}
}
}
}
@@ -0,0 +1,26 @@
module.exports = cb => {
return {
message: message => {
if(message.event === 'marketUpdate')
cb(null, {
done: false,
latest: message.payload
})
else if(message.type === 'error') {
cb(message.error);
}
else if(message.type === 'log')
console.log(message.log);
},
exit: status => {
if(status !== 0)
return cb('Child process has died.');
else
cb(null, { done: true });
}
}
}
@@ -0,0 +1,24 @@
// pass back all messages as is
// (except for errors and logs)
module.exports = cb => {
return {
message: message => {
if(message.type === 'error') {
cb(message.error);
}
else
cb(null, message);
},
exit: status => {
if(status !== 0)
cb('Child process has died.');
else
cb(null, { done: true });
}
}
}
+34
View File
@@ -0,0 +1,34 @@
var fork = require('child_process').fork;
module.exports = (mode, config, callback) => {
var debug = typeof v8debug === 'object';
if (debug) {
process.execArgv = [];
}
var child = fork(__dirname + '/child');
// How we should handle client messages depends
// on the mode of the Pipeline that is being ran.
var handle = require('./messageHandlers/' + mode + 'Handler')(callback);
var message = {
what: 'start',
mode: mode,
config: config
};
child.on('message', function(m) {
if(m === 'ready')
return child.send(message);
if(m === 'done')
return child.send({what: 'exit'});
handle.message(m);
});
child.on('exit', handle.exit);
return child;
}
+46
View File
@@ -0,0 +1,46 @@
version: '3'
services:
gekko:
build: ./
volumes:
- ./volumes/gekko/history:/usr/src/app/history
- ./config.js:/usr/src/app/config.js
links:
- redis
# - postgresql
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
# environment:
# - VIRTUAL_HOST=gekko
# - PORT=3000
# - DOMAIN=gekko
redis:
image: redis:latest
volumes:
- ./volumes/redis:/data
## optionally uncomment if you wish to use nginx as a frontend
# nginx:
# restart: always
# image: jwilder/nginx-proxy
# ports:
# - "80:80"
# volumes:
# - /var/run/docker.sock:/tmp/docker.sock:ro
## optionally uncomment if you wish to use postgresql as a db
# postgresql:
# restart: always
# image: postgres:9.6-alpine
# ports:
# - 5432:5432
# volumes:
# - ./_postgresql:/var/lib/postgresql/data:rw
# environment:
# POSTGRES_DB: gekko
# POSTGRES_USER: username
# POSTGRES_PASSWORD: password
+9
View File
@@ -0,0 +1,9 @@
#!/bin/bash
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 "$@"
+41
View File
@@ -0,0 +1,41 @@
# Importing
*Note: this documentation was written for running Gekko via the command line. If you are using the UI you can simply use the importer under the "local data" tab.*
If you want to use Gekko to [backtest against historical data](./backtesting.md), you most likely need some historical data to test against. Gekko comes with the functionality to automatically import historical data from some exchanges. However, only a few exchanges support this. You can find out with which exchanges Gekko is able to do this [here](https://github.com/askmike/gekko#supported-exchanges).
## Setup
For importing you should [enable and configure](./plugins.md) the following plugin:
- candleWriter (to store the imported data in a database)
Besides that, make sure to configure `config.watch` properly.
## Configure
In your config set the `importer.daterange` properties to the daterange you would like to import.
## Run
node gekko --config config.js --import
The result will be something like this:
2016-06-26 09:12:16 (INFO): Gekko v0.2.2 started
2016-06-26 09:12:16 (INFO): I'm gonna make you rich, Bud Fox.
2016-06-26 09:12:17 (INFO): Setting up Gekko in importer mode
2016-06-26 09:12:17 (INFO):
2016-06-26 09:12:17 (INFO): Setting up:
2016-06-26 09:12:17 (INFO): Candle writer
2016-06-26 09:12:17 (INFO): Store candles in a database
2016-06-26 09:12:17 (INFO):
2016-06-26 09:12:17 (WARN): The plugin Trading Advisor does not support the mode importer. It has been disabled.
2016-06-26 09:12:17 (WARN): The plugin Advice logger does not support the mode importer. It has been disabled.
2016-06-26 09:12:17 (WARN): The plugin Profit Simulator does not support the mode importer. It has been disabled.
2016-06-26 09:12:18 (DEBUG): Processing 798 new trades.
2016-06-26 09:12:18 (DEBUG): From 2015-09-09 12:00:04 UTC to 2015-09-09 13:58:55 UTC. (2 hours)
2016-06-26 09:12:20 (DEBUG): Processing 211 new trades.
(...)
+28
View File
@@ -0,0 +1,28 @@
# About the commandline
You don't have to use the UI to control Gekko, you can also use the commandline if you don't have access to a window manager or browser (on a server for example, or anything else you want to SSH into). This is only recommended for advanced users who feel comfortable editing config files and running shell commands.
Using the commandline you run one Gekko instance per command, eg. if you want to import 3 markets you need to run Gekko 3 times.
## Configuring Gekko
If you decide you want to run gekko over the commandline you need to decide two things:
1. What market / settings are you interested in?
2. What should Gekko do? This is either backtest, import or run live.
For the first one you configure a config file: copy `gekko/sample-config.js` to something else (for example `gekko/config.js`). Configure the plugins to your liking. What plugins you need to enable does depend on the answer of question 2. Check the documentation under commandline for that feature.
## Running Gekko
For live mode run:
node gekko --config config.js
To backtesting run:
node gekko --config config.js --backtest
To import run:
node gekko --config config.js --import
+71
View File
@@ -0,0 +1,71 @@
# Backtesting with Gekko
*Note: this documentation was written for running Gekko via the command line. If you are using the UI you can simply use the importer under the "local data" tab.*
Gekko is able to backtest strategies against historical data.
## Setup
For backtesting you should [enable and configure](./plugins.md) the following plugins:
- trading advisor (to run your strategy).
- paper trader (to execute simulated trades).
- performance analyzer (to calculate how succesfull the strategy would have been).
Besides that, make sure to configure `config.watch`.
## Historical data
Gekko requires historical data to backtest strategies against. The easiest way to get this is to let Gekko import historical data, however this is not supported by a lot of exchanges (see [here](https://github.com/askmike/gekko#supported-exchanges)). The second easiest and most universal way is to run Gekko on real markets using the UI (or alternatively via the commandline with the plugin sqliteWriter enabled). Though this takes a while, as you need to run Gekko for a week to have a week of data.
## Configure
In your config set the `backtest.daterange` to `scan`. This will force Gekko to scan the local database to figure out what dataranges are available. If you already know exactly what daterange you would like to backtest against, you can set the `backtest.daterange` directly (set `backtest.daterange` as an object with the keys `from` and `to` and the values as a data parsable by [moment](http://momentjs.com/docs/#/parsing/)).
## Run
node gekko --config config.js --backtest
The result will be something like this:
2016-06-11 08:53:20 (INFO): Gekko v0.2.1 started
2016-06-11 08:53:20 (INFO): I'm gonna make you rich, Bud Fox.
2016-06-11 08:53:20 (INFO): Setting up Gekko in backtest mode
2016-06-11 08:53:20 (INFO):
2016-06-11 08:53:20 (WARN): The plugin SQLite Datastore does not support the mode backtest. It has been disabled.
2016-06-11 08:53:20 (INFO): Setting up:
2016-06-11 08:53:20 (INFO): Trading Advisor
2016-06-11 08:53:20 (INFO): Calculate trading advice
2016-06-11 08:53:20 (INFO): Using the trading method: DEMA
2016-06-11 08:53:20 (INFO):
2016-06-11 08:53:20 (INFO): Setting up:
2016-06-11 08:53:20 (INFO): Profit Simulator
2016-06-11 08:53:20 (INFO): Paper trader that logs fake profits.
2016-06-11 08:53:20 (INFO):
2016-06-11 08:58:20 (INFO): Profit simulator got advice to long @ 2016-05-30 04:37:00, buying 1.1880062 BTC
2016-06-11 08:58:21 (INFO): Profit simulator got advice to short @ 2016-05-31 21:37:00, selling 1.1880062 BTC
2016-06-11 08:58:21 (INFO): Profit simulator got advice to long @ 2016-06-01 12:37:00, buying 1.14506098 BTC
2016-06-11 08:58:21 (INFO): Profit simulator got advice to short @ 2016-06-02 14:57:00, selling 1.14506098 BTC
2016-06-11 08:58:21 (INFO): Profit simulator got advice to long @ 2016-06-02 23:37:00, buying 1.11711818 BTC
2016-06-11 08:58:21 (INFO): Profit simulator got advice to short @ 2016-06-05 12:57:00, selling 1.11711818 BTC
2016-06-11 08:58:21 (INFO): Profit simulator got advice to long @ 2016-06-06 02:37:00, buying 1.08456953 BTC
2016-06-11 08:58:22 (INFO): Profit simulator got advice to short @ 2016-06-07 17:17:00, selling 1.08456953 BTC
2016-06-11 08:58:22 (INFO): Profit simulator got advice to long @ 2016-06-08 13:17:00, buying 1.05481755 BTC
2016-06-11 08:58:22 (INFO): Profit simulator got advice to short @ 2016-06-09 14:17:00, selling 1.05481755 BTC
2016-06-11 08:53:22 (INFO): (PROFIT REPORT) start time: 2016-05-29 23:34:00
2016-06-11 08:53:22 (INFO): (PROFIT REPORT) end time: 2016-06-10 08:56:00
2016-06-11 08:53:22 (INFO): (PROFIT REPORT) timespan: 11 days days
2016-06-11 08:53:22 (INFO): (PROFIT REPORT) start price: 516.19
2016-06-11 08:53:22 (INFO): (PROFIT REPORT) end price: 578.97
2016-06-11 08:53:22 (INFO): (PROFIT REPORT) Buy and Hold profit: 12.162189999999995%
2016-06-11 08:53:22 (INFO): (PROFIT REPORT) amount of trades: 10
2016-06-11 08:53:22 (INFO): (PROFIT REPORT) original simulated balance: 616.19000 USD
2016-06-11 08:53:22 (INFO): (PROFIT REPORT) current simulated balance: 602.59867 USD
2016-06-11 08:53:22 (INFO): (PROFIT REPORT) simulated profit: -13.59133 USD (-2.20570%)
2016-06-11 08:53:22 (INFO): (PROFIT REPORT) simulated yearly profit: -435.53244 USD (-70.68152%)
+214
View File
@@ -0,0 +1,214 @@
## Enabling plugins
*Note: this documentation was written for running Gekko via the command line. If you are using the UI you do not have to manually select plugins.*
Gekko currently has a couple plugins:
- trading advisor (run a TA strategy against a market)
- trader (execute advice from the TA strategy on a real exchange)
- advice logger
- paper trader
- Mailer
- IRC bot
- Campfire bot
- Redis beacon
- XMP Bot
- and more..
To configure a plugin, open up your `config.js` file with a text editor and configure the appropiate section.
## Trading Advisor
If you want Gekko to provide automated trading advice you need to configure this in Gekko. Note that this is a different plugin than the "trader" which is a responsible for actually creating orders based on this advice. (So if you want automated trading you need both this advice as well as the auto trader).
Documentation about strategies in Gekko can be found [here](../strategies/introduction.md).
### Trader
This plugin automatically creates orders based on the advice from the "Trading Advisor" from the market Gekko is watching. This turns Gekko into an automated trading bot.
Before Gekko can automatically trade you need to create API keys so that Gekko has the rights to create orders on your behalf, the rights Gekko needs are (naming differs per exchange): get info, get balance/portfolio, get open orders, get fee, buy, sell and cancel order. For all exchanges you need the API key and the API secret, for both Bitstamp and CEX.io you also need your username (which is a number at Bitstamp).
Configure it like this:
config.trader = {
enabled: true,
key: 'your-api-key',
secret: 'your-api-secret',
username: 'your-username' // your username, only fill in when using bitstamp or cexio
}
- enabled indicates whether this is on or off.
- key is your API key.
- secret is your API secret.
- username is the username (only required for CEX.io and Bitstamp).
### Advice logger
The advice logger is a small plugin that logs new advice calculated by Gekko as soon as there is any. Go to the config and configure it like this:
config.adviceLogger = {
enabled: true
}
- enabled indicates whether this is on or off.
The advice logged advice will look something like this in the terminal:
2014-01-15 14:31:44 (INFO): We have new trading advice!
2014-01-15 14:31:44 (INFO): Position to take: long
2014-01-15 14:31:44 (INFO): Market price: 5.96
2014-01-15 14:31:44 (INFO): Based on market time: 2014-01-15 14:31:01
### Paper trader
The paper trader listens to Gekko's advice and on a sell it will swap all (simulated) currency into (simulated) assets at the current price. On a buy it will be the other way around.
Go to the config and configure it like this:
// do you want Gekko to calculate the profit of its own advice?
config.paperTrader = {
enabled: true,
// report the profit in the currency or the asset?
reportInCurrency: true,
// start balance, on what the current balance is compared with
simulationBalance: {
// these are in the unit types configured in the watcher.
asset: 1,
currency: 100
},
// only want report after a sell? set to `false`.
verbose: false,
// how much fee in % does each trade cost?
feeMaker: 0.5,
feeTaker: 0.6,
// Using taker or maker fee?
feeUsing: 'maker',
// how much slippage should Gekko assume per trade?
slippage: 0.1
}
- enabled indicates whether this is on or off.
- reportInCurrency tells Gekko whether it should report in asset or in the currency.
- simulationBalance tells Gekko with what balance it should start.
- verbose specifies how often Gekko should log the results (false is after every trade, true is after every candle).
- fee is the exchange fee (in %) Gekko should take into consideration when simulating orders.
- slippage is the costs in (in %) associated with not being able to buy / sell at market price.*
*If you are trading a lot and you are buying 100% currency you might not get it all at market price and you have to walk the book in order to take that position. Also note that Gekko uses the candle close price and is unaware of the top asks bids, also take this into account. It is important that you set this number correctly or the resulted calculated profit be very wrong. Read more information [here](http://www.investopedia.com/terms/s/slippage.asp). Take these into consideration when setting a slippage:
- How much spread is there normally on this market?
- How thick is the top of the book normally?
- How volatile is this market (the more volatility the bigger the chance you will not get the price you expected)?
The output will be something like:
2013-06-02 18:21:15 (INFO): ADVICE is to SHORT @ 117.465 (0.132)
2013-06-02 18:21:15 (INFO): (PROFIT REPORT) original balance: 207.465 USD
2013-06-02 18:21:15 (INFO): (PROFIT REPORT) current balance: 217.465 USD
2013-06-02 18:21:15 (INFO): (PROFIT REPORT) profit: 10.000 USD (4.820%)
### Mailer
Mailer will automatically email you whenever Gekko has a new advice.
// want Gekko to send a mail on buy or sell advice?
config.mailer = {
enabled: false, // Send Emails if true, false to turn off
sendMailOnStart: true, // Send 'Gekko starting' message if true, not if false
Email: 'me@gmail.com', // Your GMail address
// You don't have to set your password here, if you leave it blank we will ask it
// when Gekko's starts.
//
// NOTE: Gekko is an open source project < https://github.com/askmike/gekko >,
// make sure you looked at the code or trust the maintainer of this bot when you
// fill in your email and password.
//
// WARNING: If you have NOT downloaded Gekko from the github page above we CANNOT
// guarantee that your email address & password are safe!
password: '', // Your GMail Password - if not supplied Gekko will prompt on startup.
tag: '[GEKKO] ' // Prefix all EMail subject lines with this
}
- enabled indicates whether this is on or off.
- sendMailOnStart will email you right away after Gekko started, you can also use this if you are automatically restarting Gekko to see crash behaviour.
- Email is your email address from which Gekko will send emails (to the same address).
- password is the email password: Gekko needs to login to your account to send emails to you:
> You don't have to set your password here, if you leave it blank we will ask it
> when Gekko's starts.
>
> NOTE: Gekko is an open source project < https://github.com/askmike/gekko >,
> make sure you looked at the code or trust the maintainer of this bot when you
> fill in your email and password.
>
> WARNING: If you have NOT downloaded Gekko from the github page above we CANNOT
> guarantee that your email address & password are safe!
- tag is some text that Gekko will put in all subject lines so you can easily group all advices together.
### IRC bot
IRC bot is a small plugin that connects Gekko to an IRC channel and lets users interact with it using basic commands.
config.ircbot = {
enabled: false,
emitUpdates: false,
channel: '#your-channel',
server: 'irc.freenode.net',
botName: 'gekkobot'
}
- enabled indicates whether this is on or off.
- emitUpdates tells Gekko that whenever there is a new advice it should broadcast this in the channel.
- channel is the IRC channel the bot will connect to.
- server is the IRC server.
- botName is the username Gekko will use.
### Campfire bot
Campfire bot is a small plugin that connects Gekko to a Campfire room and lets users interact with it using basic commands.
config.campfire = {
enabled: false,
emitUpdates: false,
nickname: 'Gordon',
roomId: 673783,
apiKey: 'e3b0c44298fc1c149afbf4c8996',
account: 'your-subdomain'
}
- enabled indicates whether this is on or off.
- emitUpdates tells Gekko that whenever there is a new advice it should broadcast this in the room.
- roomId is the ID of the Campfire room the bot will connect to.
- apiKey is the API key for the Campfire user Gekko will connect using.
- account is the subdomain for the account that the room belongs to.
### Redis beacon
This is an advanced plugin only for programmers! If you are interested in this read more [here](https://github.com/askmike/gekko/blob/stable/docs/internals/plugins.md#redis-beacon).
config.redisBeacon = {
enabled: false,
port: 6379, // redis default
host: '127.0.0.1', // localhost
// On default Gekko broadcasts
// events in the channel with
// the name of the event, set
// an optional prefix to the
// channel name.
channelPrefix: '',
broadcast: [
'small candle'
]
}
- enabled indicates whether this is on or off.
- port is the port redis is running on.
- host is the redis host.
- channelPrefix a string that Gekko will prefix all candles with.
- broadcast is a list of all events you want Gekko to publish.
+25
View File
@@ -0,0 +1,25 @@
# Tradebot
You can set Gekko up as a tradebot, this will instruct Gekko to:
- Watch a live market.
- Run a strategy (in semi-realtime) over live market data.
- Automatically create buy/sell orders based on signals coming from the strategy.
*As with everything in Gekko, the tradebot will make decisions based on the strategy selected/configured/created **by YOU**. If you end up losing money, you have no one to blame but yourself.*
## Configuration
First, set up Gekko for commandline usage (see [this document](./about_the_commandline.md) for details). After that, configure the following plugins:
- `config.watch` - the market to trade on.
- `candleWriter` - (optional) also store market data to disk.
- `tradingAdvisor` - configure the strategy and candle properties.
- `trader` - configure Gekko access to your exchange account.
- `performanceAnalyzer` - enable.
Turn off the paperTrader (there can only be 1 trade plugin active per instance).
Once done, run Gekko like so:
node gekko --config your-config-file.js
+38
View File
@@ -0,0 +1,38 @@
# Plugins
A plugin is a low level module or plugin that can act upon events bubbling
through Gekko. If you want to have custom functionality so that your rocket
flies to the moon as soon as the price hits X you should create a plugin for it.
All plugins live in `gekko/plugins`.
Note that in order to use custom plugins, you have to run Gekko over [the commandline](../commandline/about_the_commandline.md).
## Existing plugins:
- Candle Store: save trades to disk.
- Mailer: mail trading advice to your gmail account.
- Pushbullet: send messages to Pushbullet devices.
- Telegram: send messages over Telegram.
- IRC bot: logs Gekko on in an irc channel and lets users communicate with it.
- Paper Trader: simulates trades and calculates profit over these (and logs profit).
- Trading advisor (internal): calculates advice based on market data.
- Redis beacon (advanced): [see below!](#redis-beacon)
*And more! Take a look in the `gekko/plugins folder.`*
## Implementing a new plugin
If you want to add your own plugin you need to expose a constructor function inside
`plugins/[slugname of plugin].js`. The object needs methods based on which event you want
to listen to. All events can be found in [the events page](../architecture/events.md).
You also need to add an entry for your plugin inside `plugins.js` which registers your plugin for use with Gekko. Finally you need to add a configuration object to `sample-config.js` with at least:
config.[slug name of plugin] = {
enabled: true
}
Besides enabled you can also add other configurables here which users can set themselves.
That's it! Don't forget to create a pull request of the awesome plugin you've just created!
+181
View File
@@ -0,0 +1,181 @@
# Exchanges
*This is a technical document about adding a new exchange to Gekko and Gekko Broker.*
Gekko arranges all communication about when assets need to be bought or sold between the *strategy* and *Gekko Broker*. All differences between the different API's are abstracted away just below Gekko Broker inside an "exchange wrapper". This document describes all requirements for adding a new exchange wrapper (adding exchange support to Gekko).
When you add a new exchange to Gekko you need to expose an object that has methods to query the exchange. This exchange file needs to reside in `gekko/exchange/wrappers` and the filename is the slug of the exchange name + `.js`. So for example the exchange integration with Binance is explained in `gekko/exchange/wrappers/binance.js`.
Please use a npm module to query an exchange. This will separate the abstract API calls from the Gekko specific stuff (In the case of Bitstamp there was no module yet, so I [created one](https://www.npmjs.com/package/bitstamp)).
Finally Gekko needs to know how it can interact with the exchange, so add a static method `getCapabilities()` that returns it's properties. They are all described [here](#capabilities).
## Gekko's expectations
Gekko Broker implements an exchange like so:
const Exchange = require('./wrapper' + [exchange slug]);
this.exchange = new Exchange({key: '', secret: '', username: '', currency: 'USD', asset: 'BTC'});
It will run the following methods on the exchange object:
### getTrades
With this single method implemented Gekko won't be able to do live trading, but it will be able to do paper trading and storing market data for backtesting.
this.watcher.getTrades(since, callback, descending);
If since is truthy, Gekko requests as much trades as the exchange can give (up to ~10,000 trades, if the exchange supports more you can [create an importer](../features/importing.md)).
The callback expects an error and a `trades` object. Trades is an array of trade objects in chronological order (0 is older trade, 1 is newer trade). Each trade object needs to have:
- a `date` property (unix timestamp in either string or int)
- a `price` property (float) which represents the price in [currency] per 1 [asset].
- an `amount` property (float) which represent the amount of [asset].
- a `tid` property (float) which represents the tradeID.
## Gekko Broker's expectations
The methods below are needed for Gekko to automatically trade on the exchange.
### getTicker
this.exchange.getTicker(callback)
The callback needs to have the parameters of `err` and `ticker`. Ticker needs to be an object with at least the `bid` and `ask` property in float.
### getFee
this.exchange.getFee(callback);
The callback needs to have the parameters of `err` and `fee`. Fee is a float that represents the amount the exchange takes out of the orders Gekko places. If an exchange has a fee of 0.2% this should be `0.002`.
### getPortfolio
this.exchange.getPortfolio(callback);
The callback needs to have the parameters of `err` and `portfolio`. Portfolio needs to be an array of all currencies and assets combined in the form of objects, an example object looks like `{name: 'BTC', amount: 1.42131}` (name needs to be an uppercase string, amount needs to be a float).
### getLotSize
this.exchange.getLotSize(tradeType, amount, size, callback);
The callback needs to have the parameters of `err` and `lot`. Lot needs to be an object with `amount` and `purchase` size appropriately for the exchange. In the event that the lot is too small, return 0 to both fields and this will generate a lot size warning in the portfolioManager.
Note: This function is currently optional. If not implemented Gekko Broker will fallback to basic lot sizing mechanism it uses internally. However exchanges are not all the same in how rounding and lot sizing work, it is recommend to implement this function.
### buy
this.exchange.buy(amount, price, callback);
### sell
this.exchange.sell(amount, price, callback);
This should create a buy / sell order at the exchange for [amount] of [asset] at [price] per 1 asset. The callback needs to have the parameters `err` and `order`. The order needs to be some id that can be passed back to `checkOrder`, `getOrder` and `cancelOrder`.
### getOrder
this.exchange.getOrder(order, callback);
Will only be called on orders that have been completed. The order will be something that the manager previously received via the `sell` or `buy` methods. The callback should have the parameters `err` and `order`. Order is an object with properties `price`, `amount` and `date`. Price is the (volume weighted) average price of all trades necessary to execute the order. Amount is the amount of currency traded and Date is a moment object of the last trade part of this order.
### checkOrder
this.exchange.checkOrder(order, callback);
The order will be something that the manager previously received via the `sell` or `buy` methods. The callback should have the parameters `err` and a `result` object. The result object will have two or three properties:
- `open`: whether this order is currently in the orderbook.
- `executed`: whether this order has executed (filled completely).
- `filledAmount`: the amount of the order that has been filled. This property is only needed when both `open` is true and `completed` is false.
### cancelOrder
this.exchange.cancelOrder(order, callback);
The order will be something that the manager previously received via the `sell` or `buy` methods. The callback should have the parameters `err`, `filled` and optionally a `fill` object, `filled` should be true if the order was fully filled before it could be cancelled. If the exchange provided how much was filled before the cancel this should be passed in the `fill object` as a `amountFilled` property. If the exchange provided how much of the original amount is still remaining, pass this as the `remaining` property instead.
### roundPrice
this.exchange.roundPrice(rawPrice);
Should return a price. Rounds the price into a valid price Gekko Broker can later feed into a buy or sell order. *Note: if representation of the number is important for the exchange (for example if the exchange will not accept numbers expressed in scientific notation) you can return a string from this method (Gekko Broker will perform calculations so the string should be castable to a number.)*
### roundAmount
this.exchange.roundAmount(rawAmount);
Should return an amount. Rounds the amount into a valid amount Gekko Broker can later feed into a buy or sell order. *Note: if representation of the number is important for the exchange (for example if the exchange will not accept numbers expressed in scientific notation) you can return a string from this method (Gekko Broker will perform calculations so the string should be castable to a number.)*
## Gekko Broker's optional methods
### isValidPrice
this.exchange.isValidPrice(price);
Should return true or false. If the exchange has restrictions on the price you can submit limit orders at. If there are no such restrictions you should not implement this method.
### isValidLot
this.exchange.isValidLot(price, amount);
Should return true or false. If the exchange has restrictions on the lot size (order size expressed in "currency" amount) you can check for that here. If there are no such restrictions you should not implement this method.
## Error handling
It is the responsibility of the wrapper to handle errors and retry the call in case of a temporary error. Gekko exposes a retry helper you can use to implement an exponential backoff retry strategy. Your wrapper does need pass a proper error object explaining whether the call can be retried or not. If the error is fatal (for example private API calls with invalid API keys) the wrapper is expected to upstream this error. If the error is retryable (exchange is overloaded or a network error) an error should be passed with the property `notFatal` set to true. If the exchange replied with another error that might be temporary (for example an `Insufficient funds` error right after Gekko canceled an order, which might be caused by the exchange not having updated the balance yet) the error object can be extended with an `retry` property indicating that the call can be retried for n times but after that the error should be considered fatal.
For implementation refer to the bitfinex implementation, in a gist this is what a common flow looks like:
- (async call) `exchange.buy`, then
- handle the response and normalize the error so the retry helper understands it, then
- the retry helper will determine whether the call needs to be retried, then
- based on the error it will retry (for example `nonFatal` or `retry`)
- if no error it will pass it to your handle function that normalizes the output
## Capabilities
Each exchange *must* provide a `getCapabilities()` static method that returns an object with these parameters:
- `name`: Proper name of the exchange
- `slug`: slug name of the exchange (needs to match filename in `gekko/exchanges/`)
- `currencies`: all the currencies supported by the exchange implementation in gekko.
- `assets`: all the assets supported by the exchange implementation in gekko.
- `pairs`: all allowed currency / asset combinations that form a market
- `maxHistoryFetch`: the parameter fed to the getTrades call to get the max history.
- `providesHistory`: If the getTrades can be fed a since parameter that Gekko can use to get historical data, set this to:
- `date`: When Gekko can pass in a starting point in time to start returning data from.
- `tid`: When Gekko needs to pass in a trade id to act as a starting point in time.
- `false`: When the exchange does not support to give back historical data at all.
- `fetchTimespan`: if the timespan between first and last trade per fetch is fixed, set it here in minutes.
- `tradable`: if gekko supports automatic trading on this exchange.
- `requires`: if gekko supports automatic trading, this is an array of required api credentials gekko needs to pass into the constructor.
- `forceReorderDelay`: if after canceling an order a new one can't be created straight away since the balance is not updated fast enough, set this to true (only required for exchanges where Gekko can trade).
- `gekkoBroker`: set this to "0.6.2" for now, it indicates the version of Gekko Broker this wrapper is compatible with.
- `limitedCancelConfirmation` set to true if (in any case) the wrapper does NOT include partial fill information in the response (which is passed as the cancel callback)
Below is a real-case example how `bistamp` exchange provides its `getCapabilities()` method:
```
Trader.getCapabilities = function () {
return {
name: 'Bitstamp',
slug: 'bitstamp',
currencies: ['USD', 'EUR'],
assets: ['BTC', 'EUR'],
maxTradesAge: 60,
maxHistoryFetch: null,
markets: [
{ pair: ['USD', 'BTC'], minimalOrder: { amount: 1, unit: 'currency' } },
{ pair: ['EUR', 'BTC'], minimalOrder: { amount: 1, unit: 'currency' } },
{ pair: ['USD', 'EUR'], minimalOrder: { amount: 1, unit: 'currency' } }
],
requires: ['key', 'secret', 'username'],
fetchTimespan: 60,
tid: 'tid',
gekkoBroker: '0.6.2',
limitedCancelConfirmation: false
};
}
```
+21
View File
@@ -0,0 +1,21 @@
# Other Software
Since Gekko version 0.4 Gekko can launch a process which exposes a web (REST and Websocket) API that can be used to control Gekkos (start a backtest, start a running Gekko, etc). This makes it easy for other people to build new functionality on top of Gekko without having to work with Gekko internals, the runtime (or even javascript at all).
## List
### Backtesters
- [japonicus](https://github.com/Gab0/japonicus)
- [gekkoga](https://github.com/gekkowarez/gekkoga)
- [Gekko-BacktestTool](https://github.com/xFFFFF/GekkoBacktestTool)
- [Gekko Automated Backtest](https://github.com/tommiehansen/gab)
- [Gekko Warez Bruteforce Backtester](https://github.com/gekkowarez/bruteforce)
### UI mode
- [Unofficial Material UI](https://github.com/H256/gekko-quasar-ui)
- [Controlling bots](https://github.com/CyborgDroid/gekko-python)
### CLI mode
- [Batch generation of configuration files](https://github.com/bettimms/multi-gekko)
### Plugins
- [Store trades in Google SpreadSheet](https://github.com/RJPGriffin/google-forms-gekko-plugin)
+23
View File
@@ -0,0 +1,23 @@
# Backtesting
Gekko supports backtesting strategies over historical data. A Backtest is a simulation where you simulate running a strategy over a long time (such as the last 30 days) in a matter of seconds. Backtesting requires having market data locally available already. After a backtest Gekko will provide statistics about the market and the strategy's performance.
![screen shot of gekko backtesting](https://cloud.githubusercontent.com/assets/969743/24838718/8c790a86-1d45-11e7-99ae-e7e551cb40cb.png)
**Important things to remember:**
- Just because a strategy performed well in the past, does not mean it will perform well in the future.
- Be careful of overfitting, in other words: don't simply tweak a strategy until you get high profit and assume that will be as profitable when going live. Read more about overfitting in [this article](https://laplaceinsights.com/backtesting-strategies-and-overfitting/).
- The backtest simulation is limited, this is not really a problem on bigger markets (such as BTC/USD) but the differences between backtests and live traders on very low volume markets might be big. Read more about this below in the simplified simulation below.
## Simplified simulation
Simulating trades is done through a module called the paper trader. This module will use market candles together with fee, slippage and spread numbers to estimate trade executions costs. While the default settings work great for most big markets (USD/BTC or BTC/ETH), it becomes a lot less acurate on smaller markets with low volume and liquidity.
In live trading the notion of the "price" is more complicated than a single number. Both `spread` and `slippage` will effect your trade prices: these numbers describe your desired trades in relation to what people are currently offering in the market (this is called the orderbook). Read more about this in [this explanation](https://github.com/askmike/gekko/issues/2380#issuecomment-408744682).
If you look at the following backtest result:
![screen shot of backtesting at an illiquid market](https://cloud.githubusercontent.com/assets/969743/24840243/8f307022-1d61-11e7-9964-e6614d7433ea.png)
You can see a lot of "spikes" of the price moving up and down. These are not actually price fluctuations but simply trades that happen on both sides of the orderbook (a bid is taken then an ask is taken). How far it jumps up and down is the spread (between the asks and the bids). In these cases the statistics from the simulation won't be very accurate (unless you configured a higher slippage to account for the spread). This is unfortunately a limitation in Gekko's backtesting model.
+11
View File
@@ -0,0 +1,11 @@
# Importing
In order to [backtest](./backtesting.md) your strategies you will need to have historical market data to test with. The easiest way of getting this data is importing it directly from the exchange using the Gekko UI (note that this is not supported at all exchanges, check [this list](../introduction/supported_exchanges.md) to see what exchanges Gekko can import from).
You can start an import by navigating to the tab "Local tab" and scrolling to the bottom and click "Go to the importer". This brings you to the importing page with this at the bottom:
![importing wizard](https://user-images.githubusercontent.com/969743/28596822-a79509cc-7192-11e7-9293-53066f598053.png)
Once you configure the market and daterange you want to watch and click import Gekko will automatically download historical market data from the exchange:
![gekko importer](https://user-images.githubusercontent.com/969743/28597211-914dbe32-7194-11e7-8352-60a69afdf846.png)
+7
View File
@@ -0,0 +1,7 @@
# Paper trading
Gekko can automatically run a strategy over the live markets and simulate in realtime what happen if you would have traded on its signals. Paper trading and [backtesting](./backtesting.md) are the two simulation modes that come with Gekko. It's a great way to experiment with strategies without putting your money on the line.
You can start a paper trader by going to live gekkos and clicking on "Start a new live Gekko".
Keep in mind that a paper trader is a simulation, and the accuracy depends on the market you decide to run it on (you'll get pretty accurate results on big markets like USD/BTC). You can read more about the details and limitations of the simulation on [the backtesting page](./backtesting.md#Simplified-simulation).
+22
View File
@@ -0,0 +1,22 @@
# Trading bot
Once you have run enough simulations (using [backtesting](./backtesting.md) and [paper trading](./paper_trading.md)) and you are confident in your strategy you can use Gekko as a trading bot.
Gekko will run your strategy on the live market and automatically trade on your exchange account when trade signals come out of your strategy.
## Preparation
1. Make sure you are fully confident in your strategy! If you want to play around use either the [paper trader](./paper_trading.md) or the [backtester](./backtesting.md). Once you are confident continue with this list.
2. Gekko will need to have API keys to your exchange account that have permissions to view balances and orders and create new orders. Keep in mind:
- Gekko does NOT need withdrawal access, for your safety DO NOT create API keys that can withdraw.
- Make sure you only use the API key for Gekko, and for nothing else. If in doubt create a new key (and remove stale ones).
- If possible try to restrict the API key to the IP address you will run Gekko from (this makes moest sense in server environments)
3. Start your gekko through either the UI or the commandline interface!
## Notes
Gekko will trade on the market you configured that consists of two currencies (for example USD/BTC):
- Try to not trade either of these currencies on the account you use with Gekko. (in the example above: don't trade any USD nor any BTC).
- Try to not withdraw or deposit more of either of these currencies.
While Gekko will handle the situations above, all the profit calculations will be incorrect since your balances are taken into account while calculating profits.
+130
View File
@@ -0,0 +1,130 @@
# Gekko Broker
Order execution library for bitcoin and crypto exchanges. This library is Gekko's execution engine for all live orders (simulated orders do not through Gekko Broker, they go through the paper trader). This library is intended for developers of trading systems in need for advanced order types on multiple exchanges over a unified API.
## Introduction
This library makes it easy to do (advanced) orders all crypto exchanges where Gekko can live trade at. See the complete list [here](https://gekko.wizb.it/docs/introduction/supported_exchanges.html).
This library allows you to:
- Get basic market data
- ticker (BBO)
- ~orderbook~ (TODO)
- historical trades
- Get portfolio data
- Do an (advanced) order:
- Submit the order to the exchange
- Receive events about the status of the order:
- submitted
- open (accepted)
- partially filled
- rejected
- completed
- Mutate the order
- cancel
- amend
- move
- move limit
- Tracks orders submitted through the library.
## Status
Early WIP. All communication is via the REST APIs of exchanges. Not all exchanges are supported, see which ones are in [this doc](../introduction/supported_exchanges.md).
## Order types
This library aims to offer advanced order types, even on exchanges that do not natively support them by tracking the market and supplementing native order support on specific exchanges.
Working:
- Base orders
- [Sticky Order](./sticky_order.md)
TODO:
- Base orders:
- Limit Order
- Market Order
- Triggers:
- Stop
- If Touched (stop but opposite direction)
### Example
Set up a Gekko Broker instance:
// from the gekko repo (make sure you have deps installed
// inside the exchange folder).
const Broker = require('../gekko/exchange/GekkoBroker');
// or from NPM
// const Broker = require('gekko-broker');
const binance = new Broker({
currency: 'USDT',
asset: 'BTC',
private: true,
exchange: 'binance',
key: 'x', // add your API key
secret: 'y' // add your API secret
});
Now we have an instance that can create a [sticky order](./sticky_order.md):
const type = 'sticky';
const side = 'buy';
const amount = 1;
const order = binance.createOrder(type, side, amount);
order.on('statusChange', s => console.log(now(), 'new status', s));
order.on('fill', s => console.log(now(), 'filled', s));
order.on('error', s => console.log(now(), 'error!', e));
order.on('completed', a => {
console.log(new Date, 'completed!');
order.createSummary((err, s) => {
console.log(new Date, 'summary:');
console.log(JSON.stringify(s, null, 2));
});
});
This one doesn't have an upper limit price for what it will buy at. It will stick it's bid offer at BBO until it's filled. If you have a limit in mind you can specify it when creating, do this instead:
const order = binance.createOrder(type, side, amount, { limit: 100 });
It will never offer to buy for more than 100, even if the BBO is above 100 (the bid will end up deep in the book).
At any point in time you can change the limit (or the amount), for example:
order.moveLimit(120);
Running the above example (without setting a limit and moving it) will yield:
root@foxtail:~/gb/nusadua# node b
2018-07-29 03:46:02 new status SUBMITTED
2018-07-29 03:46:02 new status OPEN
2018-07-29 03:46:04 filled 1
2018-07-29 03:46:04 new status FILLED
2018-07-29T03:46:04.127Z 'completed!'
2018-07-29T03:46:04.358Z 'summary:'
{
"price": 0.0017479,
"amount": 1,
"date": "2018-07-29T03:46:02.576Z",
"side": "buy",
"orders": 1,
"fees": {
"BNB": 0.00075
},
"feePercent": 0.075
}
NOTE: not all status changes are documented and more events are planned but not implemented.
### TODO
- finish all exchange integrations that gekko supports
- finish all order types and triggers (under todo)
- implement websocket support (per exchange)
- use native move API calls wherever possible (poloniex)
+48
View File
@@ -0,0 +1,48 @@
# Sticky Order
An advanced order that stays at the top of the book (until the optional limit). The order will automatically stick to the best BBO until the complete amount has been filled.
TODO:
- implement fallback for when this order is alone at the top, some spread before everyone else
- finalize API
- add more events / ways to debug
- pull ticker data out of this order market data should flow from the broker (so we can easier move to at least public websocket streams).
## Example usage
const Broker = require('gekko-broker');
const gdax = new Broker({
currency: 'EUR',
asset: 'BTC',
exchange: 'gdax',
// Enables access to private endpoints.
// Needed to create orders and fetch portfolio
private: true,
key: 'x',
secret: 'y',
passphrase: 'z'
});
gdax.portfolio.setBalances(console.log);
const type = 'sticky';
const amount = 0.5;
const side = 'buy';
const limit = 6555;
const order = gdax.createOrder(type, side, amount, { limit });
order.on('statusChange', status => console.log(status));
order.on('filled', result => console.log(result));
order.on('completed', () => {
order.createSummary(summary => console.log)
});
// mutate like so
// order.moveAmount(1);
// order.moveLimit(6666);
+22
View File
@@ -0,0 +1,22 @@
# Wrapper API
Gekko Broker is a library that sits between trading applications and Gekko Broker Exchange Wrappers. Which means it has two APIs to communicate with other code:
![diagram describing Gekko Broker API interface](https://user-images.githubusercontent.com/969743/41892153-566293a0-7941-11e8-9998-7a5b5b554ffd.png)
This document descibres the API layer between the exchange wrappers and Gekko Broker.
## Wrapper API spec
The current API documentation is currently located [here](../extending/add_an_exchange.md).
## Wrapper API Changelog
### Gekko 0.5.x to Gekko (Broker) 0.6.0
NOTE: this API design might still have minor changes leading up to the release of Gekko 0.6. See [this thread](https://forum.gekko.wizb.it/thread-57279-post-59207.html) for more information.
- The wrapper files are now nested different (from `gekko/exchanges` to `gekkobroker/wrappers` (which equals `gekko/exchange/wrappers` for Gekko users).
- cancelOrder now requires a second parameter to be passed (that indicates whether the order was filled before it was canceled), see [details](https://github.com/askmike/gekko/commit/0e301f7d66e24ec97327f5f01380f691cc2d3725#diff-dbfe320ca090e208be32459d98fc11ed).
- checkOrder now expects an object with a few properties to be returned, see [details](https://github.com/askmike/gekko/commit/e0d4a7362cd74b4b4f50759b1012ce489ea44a0c#diff-dbfe320ca090e208be32459d98fc11ed).
- Error handling has gotten a lot more complex, with an updated error interface between a retry system (provided by Gekko) and the exchange wrapper. [Read more here](https://github.com/askmike/gekko/commit/e0d4a7362cd74b4b4f50759b1012ce489ea44a0c#diff-dbfe320ca090e208be32459d98fc11ed).
@@ -0,0 +1,148 @@
# Configuring Gekko on a server
*NOTE: unfortunately configuring Gekko to run on a server is hard. Unless you are running in a completely trusted network (home network) you WILL need to get an SSL certificate, upstream Gekko through a webserver, etc. I am creating an official Gekko service called [Gekko Plus](https://gekkoplus.com/) that will NOT require any of this stuff.*
Gekko runs great headless (on a server, raspberry PI) but the default configuration assumes that you will be using a browser from the same machine to access the interface.
# Installation
Please see the normal [installing gekko](./installing_gekko.md) document, but before starting gekko please update the configuration as stated below:
# Configuring Gekko
In order to setup Gekko so that you can access it remotely you need to open and edit the following file: `gekko/web/vue/dist/UIconfig.js`. You need to configure this file according to your use case:
- You want to Gekko headless in a trusted environment (eg. on a raspberry pi or old laptop in your home network) - [see here](#configuring-gekko-to-run-headless-in-a-trusted-environment).
- You want to run Gekko in the cloud on a server - [see here](#configuring-gekko-to-run-in-the-cloud).
## Configuring Gekko to run headless in a trusted environment
Edit the uiconfig file like so:
const CONFIG = {
headless: true,
api: {
host: '0.0.0.0',
port: 3000,
},
ui: {
ssl: false,
host: 'x.x.x.x', // Set this to the IP of the machine that will run Gekko
port: 3000,
path: '/'
},
adapter: 'sqlite'
}
You can now access the Gekko UI by going to `http://x.x.x.x:3000` in a browser (change `x.x.x.x` with the IP of the machine that will run Gekko).
## Configuring Gekko to run in the cloud
Important note: if you expose Gekko to the open internet (or on any non trusted network) you are recommended to put a secure reverse proxy (for example with both [SSL](#Obtaining-a-SSL-certificate) and [BasicAuth](#Create-htpasswd-for-basic-password-authentication)) in front of it. While we believe Gekko is hard to exploit, it allows for 24/7 backtesting which will drain your machine's resources (possible DoS).
The following assumes you configured a reverse proxy, if you did not simply follow [these instructions](#Configuring-NGINX-as-a-reverse-proxy) to do so.
const CONFIG = {
headless: true,
api: {
host: '127.0.0.1',
port: 3000,
},
ui: {
ssl: true,
host: 'gekko.example.com',
port: 443,
path: '/' // change this if you are serving from something like `example.com/gekko`
},
adapter: 'sqlite'
}
## Configuring NGINX as a reverse proxy
NGINX is a highly configurable, lightweight, yet easily deployed webserver allowing features such as a reverse proxying using secure sockets layer with authentication and much more.
Installing NGINX using your Operating Systems package manager of choice is pretty straight forward. For Debian Linux it is a simple sudo apt-get install nginx
Once NGINX is installed you will need to modify the configuration file. For Debian Linux the config is located at /etc/nginx/sites-enabled/default
server {
listen 80;
listen [::]:80;
server_name gekko.example.com;
return 301 https://$server_name$request_uri;
}
upstream websocket {
server localhost:3000;
}
server {
listen 443 ssl;
listen [::]:443 ssl;
root /var/www/html;
ssl_certificate /etc/nginx/ssl/nginx.crt;
ssl_certificate_key /etc/nginx/ssl/nginx.key;
location / {
proxy_buffers 8 32k;
proxy_buffer_size 64k;
proxy_pass http://websocket;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-NginX-Proxy true;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 86400s;
proxy_send_timeout 86400s;
auth_basic "Restricted Content";
auth_basic_user_file /etc/nginx/.htpasswd;
}
}
## Obtaining a SSL certificate
Your OS may or may not ship with openssl preinstalled. In the case it doesn't, simply install openssl using your package manager of choice. eg: `sudo apt-get install openssl`.
Below you can choose between creating a self signed certificate useful if you do not have a fqdn (fully qualified domain name), or if you by chance do have a fqdn you can use certbot to obtain a Let's Encrypt CA signed certificate.
### To create a self signed certificate:
sudo mkdir /etc/nginx/ssl
sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /etc/nginx/ssl/nginx.key -out /etc/nginx/ssl/nginx.crt
### To obtain a Let's Encrypt CA signed certificate:
Install certbot, a client to obtain signed ssl certificates for your domain.
sudo apt-get install certbot
Run the following command:
certbot certonly --standalone -d example.com -d gekko.example.com
Modify your NGINX config and replace the ssl lines with the following:
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
add_header Strict-Transport-Security "max-age=31536000";
## Create htpasswd for basic password authentication
Change username to desired username and enter password when prompted:
printf "username:`openssl passwd -apr1`\n" >> /etc/nginx/.htpasswd
+62
View File
@@ -0,0 +1,62 @@
# Installing Gekko
*NOTE: unfortunately installing and managing Gekko is hard. You will need to touch the commandline and install a few developer tools. I am creating an official Gekko service called [Gekko Plus](https://gekkoplus.com/) that will NOT require any installation.*
- *Windows user? Please see the doc [installing Gekko on windows](./installing_gekko_on_windows.md) instead.*
- *Docker user? You can run Gekko in a docker container, see [installing Gekko using Docker](./installing_gekko_using_docker.md) instead.*
- *Server user? In order to run Gekko headless, see [configuring Gekko on a server](./configuring_gekko_on_a_server.md) instead.*
Here is a video of me explaining how to install Gekko the easiest way possible:
[![screen shot 2017-04-20 at 00 03 45](https://cloud.githubusercontent.com/assets/969743/25205894/e7f4ea64-255c-11e7-891b-28c080a9fbf2.png)](https://www.youtube.com/watch?v=R68IwVujju8)
To get Gekko running you need to do the following:
- install nodejs
- install git
- download Gekko
- install Gekko & Gekko Broker dependencies
## Installing nodejs
Gekko requires [nodejs](https://nodejs.org/en/) to be installed. Go ahead and install this if it's not already (Gekko requires at least version 8.11.2). We advice to download the current LTS.
## Installing git
As part of Gekko's installation process git is used, see [this guide](https://www.linode.com/docs/development/version-control/how-to-install-git-on-linux-mac-and-windows/) for installation instructions. If you do not already have git on your system.
## Downloading Gekko
The recommended way of downloading Gekko is by using git. This makes keeping Gekko up to date a lot easier. Run this in a terminal:
git clone git://github.com/askmike/gekko.git -b stable
cd gekko
This will download the latest stable version of Gekko, remove the final `-b stable` part to download the current latest release (which might not be as stable).
Alternatively you can manually download the latest stable version of Gekko on the [releases page](https://github.com/askmike/gekko/releases).
## Installing Gekko's dependencies
Once you have Gekko downloaded you need to install the dependencies, open your terminal and navigate to the gekko folder and run:
npm install --only=production
*NOTE: You may see a vulnerability warning from NPM, if you run npm audit with --force, Gekko will break. See [here](https://github.com/askmike/gekko/issues/2585#issuecomment-428450997).*
You also need to install Gekko Broker's dependencies, run:
cd exchange
npm install --only=production
cd ..
## Starting Gekko
After all the above you can start Gekko by running the following in your terminal:
node gekko --ui
## Updating Gekko
See the [updating Gekko](./updating_gekko.md) doc.
@@ -0,0 +1,74 @@
# Installing Gekko on Raspberry Pi 2 or 3
This manual is intended for a Raspberry Pi 2 or 3 with freshly installed Raspbian Stretch (Lite).
To get Gekko running on your Pi you need to do the following:
- update your package lists
- install nodejs
- download Gekko
- install Gekko's dependencies
- (optional) Configure your Pi as a headless server in your local network
## Update your package lists
sudo apt-get update
sudo apt-get upgrade
## Installing nodejs
We recommend installing the latest LTS Version of Node, currently 8.x.x
curl -sL https://deb.nodesource.com/setup_8.x | sudo -E bash -
sudo apt-get install -y nodejs
Important: Ignore the errors!
=> It automatically builds from source instead, it can take over 10 minutes! Do not abort the process
## Downloading Gekko
The recommended way of downloading Gekko is by using git. This makes keeping Gekko up to date a lot easier. Run this in a terminal:
sudo apt-get install git
git clone git://github.com/askmike/gekko.git -b stable
cd gekko
This will download the latest stable version of Gekko, remove the final `-b stable` part to download the current latest release (which might not be as stable).
## Installing Gekko's dependencies
Once you have Gekko downloaded you need to install the dependencies, open your terminal and navigate to the gekko folder and run:
npm install --only=production
cd exchange
npm install --only=production
cd ..
## Configure your Pi as a headless server
cd web/vue/dist
nano UIconfig.js
Set headless to true
Set api.host to 0.0.0.0
Set ui.host to Pi's IP address (e.g. 192.168.1.74)
## Starting Gekko
After all the above you can start Gekko by running the following in your terminal:
node gekko --ui
Open your browser and type in Pi's IP:port (default port: 3000)
## Updating Gekko
See the [updating Gekko](./updating_gekko.md) doc.
## Run gekko in background
If you access Pi headless via SSH, you have to use a software like tmux to run Gekko in background
## Installing Gekko on Raspberry Pi 1
It is possible to run Gekko on Raspberry Pi 1, but you have to manually install Nodejs armv6 version
@@ -0,0 +1,77 @@
# Installing Gekko on windows
*NOTE: unfortunately installing and managing Gekko is hard. You will need to touch the commandline and install a few developer tools. I am creating an official Gekko service called [Gekko Plus](https://gekkoplus.com/), it will NOT require any installation.*
### Note:
#### Windows does not natively support TA-lib. We are currently working on implementing the Tulip Indicators Library, which will provide similar functionality (see [#708](https://github.com/askmike/gekko/issues/708)).
#### For advanced users only: As a temporary workaround until [#708](https://github.com/askmike/gekko/issues/708) is implemented, TA-lib can be used on Windows through Bash on Windows 10. See "Installing Gekko on Windows with bash on Windows 10"
Here is a youtube video I made that shows exactly how to set up Gekko:
[![screen shot 2017-04-20 at 00 03 45](https://cloud.githubusercontent.com/assets/969743/25205894/e7f4ea64-255c-11e7-891b-28c080a9fbf2.png)](https://www.youtube.com/watch?v=R68IwVujju8)
To get Gekko running on Windows you need to do the following:
- install nodejs
- download Gekko
- install Gekko's dependencies
## Install nodejs
Gekko runs on nodejs so we have to install that first. Head over the [nodejs homepage](http://nodejs.org/) and install the LTS version of nodejs.
## Install Gekko
The easiest way to download Gekko is to go to the [Github repo](https://github.com/askmike/gekko) and click on the 'zip' button at the top. Once you have downloaded the zip file it's the easiest to extract it. When you have done that we can begin with the cool stuff:
### Open up command line
* Start
* Type in 'cmd.exe'
* Press enter
### Install dependencies
(After every command, press enter)
First navigate to Gekko:
cd Downloads
cd gekko-stable
cd gekko-stable
Install Gekko's dependencies:
npm install --only=production
*NOTE: You may see a vulnerability warning from NPM, if you run npm audit with --force, Gekko will break. See [here](https://github.com/askmike/gekko/issues/2585#issuecomment-428450997).*
Install Gekko Broker's dependencies:
cd exchange
npm install --only=production
cd ..
### Install Tulip Indicators
If you are using Windows you will need to install python and the VC++ 2015 build tool, the easiest way to do this is through node as an administrator:
npm install windows-build-tools --global --production
Once your build tools are installed, or for other operating systems
npm install tulind --only=production
### Starting Gekko
node gekko --ui
Your browser should automatically open with the UI. If it doesn't, manually browse to [http://localhost:3000](http://localhost:3000).
### Stopping Gekko
In the command line hold `ctrl` + `c`.
### Updating Gekko
See the [updating Gekko](./updating_gekko.md) doc.
@@ -0,0 +1,91 @@
# Installing Gekko on Windows with bash on Windows 10
*NOTE: unfortunately installing and managing Gekko is hard. You will need to touch the commandline and install a few developer tools. I am creating an official Gekko service called [Gekko Plus](https://gekkoplus.com/), it will NOT require any installation.*
### Note:
#### This guide is for advance users only! This is a temporary solution until [#708](https://github.com/askmike/gekko/issues/708) is implemented
#### You must be running a 64-bit version of Windows 10 to use Bash on Windows 10
#### TA-lib does not support Windows 10, but it can be used on Bash on Windows 10 by following these instructions
## To get Gekko running on Bash on Windows you need to do the following:
- enable Bash on Windows 10
- install nodejs
- install git
- download and install Gekko and its dependencies
- edit the handle.js file
- install the correct version of TA-lib
## Enable Bash on Windows 10
To install Bash shell on your Windows 10 PC, do the following (see [here](https://www.windowscentral.com/how-install-bash-shell-command-line-windows-10) for more info):
- Open Settings.
- Click on Update & security.
- Click on For Developers.
- Under "Use developer features", select the Developer mode option to setup the environment to install Bash.
- On the message box, click Yes to turn on developer mode.
- After the necessary components install, you'll need to restart your computer.
- Once your computer reboots, open Control Panel.
- Click on Programs.
- Click on Turn Windows features on or off.
- Check the Windows Subsystem for Linux (beta) option.
- Click OK.
- Once the components installed on your computer, click the Restart now button to complete the task.
After your computer restarts, you will notice that Bash will not appear in the "Recently added" list of apps, this is because Bash isn't actually installed yet. Now that you have setup the necessary components, use the following steps to complete the installation of Bash:
- Open Start, do a search for bash.exe, and press Enter.
- On the command prompt, type y and press Enter to download and install Bash from the Windows Store.
Then you'll need to create a default UNIX user account. This account doesn't have to be the same as your Windows account. Enter the username in the required field and press Enter (you can't use the username "admin"). Close the "bash.exe" command prompt.
Now that you completed the installation and setup, you can open the Bash tool from the Start menu like you would with any other app.
## Install nodejs
Gekko runs on nodejs so we have to install that first.
Open up bash and install node.js: (taken from [here](https://nodejs.org/en/download/package-manager/#debian-and-ubuntu-based-linux-distributions))
```
sudo apt-get update
curl -sL https://deb.nodesource.com/setup_8.x | sudo -E bash -
sudo apt-get install -y nodejs
sudo apt-get install -y build-essential
```
## Install Gekko and its dependencies
The easiest way to download Gekko is to use Git:
- Install Git
`sudo apt-get install git`
- Download and install gekko in you home directory (/home/yourusername): (taken from [here](https://gekko.wizb.it/docs/installation/installing_gekko.html)):
```
git clone git://github.com/askmike/gekko.git
cd gekko
npm install --only=production
cd exchange
npm install --only=production
cd ..
```
*NOTE: You may see a vulnerability warning from NPM, if you run npm audit with --force, Gekko will break. See [here](https://github.com/askmike/gekko/issues/2585#issuecomment-428450997).*
### Starting Gekko
`node gekko --ui`
Your browser should automatically open with the UI. If it doesn't, manually browse to [http://localhost:3000](http://localhost:3000).
### Stopping Gekko
In bash hold `ctrl` + `c`.
### Updating Gekko
See the [updating Gekko](./updating_gekko.md) doc.
@@ -0,0 +1,25 @@
# Installing Gekko using Docker
Installing and running gekko in a docker container for use on the same machine is simple with the following commands:
```
$ docker-compose build
$ docker-compose up -d
```
You can now find your gekko instance running on `localhost:3000`.
## Installing for external access
However if you want to run Gekko on another machine (in the cloud, on a server), you need to specify the host machine and its port like so:
```
$ docker-compose build
$ HOST=mydomain.com PORT=3001 docker-compose up -d
```
You can now find your gekko instance running on `mydomain.com:3001`.
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`.
Loaded 100 of 386 files, more files were not shown because too many files have changed in this diff. Show more