From 3d9671552aa44c2a46db34d2561e9df218a93a27 Mon Sep 17 00:00:00 2001 From: Mike van Rossum Date: Sun, 27 Nov 2016 14:32:35 +0000 Subject: [PATCH 01/25] use ListManager for state --- web/cache.js | 14 ---------- web/routes/import.js | 57 +++++++++++++++------------------------- web/routes/imports.js | 5 ---- web/routes/list.js | 7 +++++ web/routes/livegekkos.js | 5 ---- web/routes/startGekko.js | 2 +- web/routes/strategies.js | 2 +- web/server.js | 45 ++++++++++++++++++++----------- 8 files changed, 60 insertions(+), 77 deletions(-) delete mode 100644 web/cache.js delete mode 100644 web/routes/imports.js create mode 100644 web/routes/list.js delete mode 100644 web/routes/livegekkos.js diff --git a/web/cache.js b/web/cache.js deleted file mode 100644 index 092105151..000000000 --- a/web/cache.js +++ /dev/null @@ -1,14 +0,0 @@ -const _ = require('lodash'); - -const cache = {}; - -module.exports = { - set: (name, val) => { - cache[name] = val; - return true; - }, - get: name => { - if(_.has(cache, name)) - return cache[name]; - } -} \ No newline at end of file diff --git a/web/routes/import.js b/web/routes/import.js index 202c7992d..e68c09eee 100644 --- a/web/routes/import.js +++ b/web/routes/import.js @@ -2,30 +2,12 @@ const _ = require('lodash'); const promisify = require('tiny-promisify'); const pipelineRunner = promisify(require('../../core/workers/pipeline/parent')); -const cache = require('../cache'); +const cache = require('../state/cache'); const broadcast = cache.get('broadcast'); +const importManager = cache.get('imports'); const base = require('./baseConfig'); -// everything is in memory at the moment: -// If Gekko UI crashes the import (in child process) -// will stop anyway -const addImportToCache = _import => { - let list = cache.get('running_imports'); - list.push(_.clone(_import)); - cache.set('running_imports', list); -} -const updateImportInCache = (id, latest) => { - let list = cache.get('running_imports'); - let _import = _.find(list, {id: id}); - _import.latest = latest; - cache.set('running_imports', list); -} -const removeImportFromCache = (id) => { - let list = cache.get('running_imports'); - cache.set('running_imports', list.filter(im => id !== im.id)); -} - // starts an import // requires a post body with a config object module.exports = function *() { @@ -42,14 +24,14 @@ module.exports = function *() { console.log('Import started'); pipelineRunner(mode, config, (err, event) => { - if(err) { - if(errored) - return; + if(errored) + return; + if(err) { errored = true; - console.error('RECEIVED ERROR IN ROUTE', importId); + console.error('RECEIVED ERROR IN IMPORT', importId); console.error(err); - removeImportFromCache(importId); + importManager.delete(importId); return broadcast({ type: 'import_error', import_id: importId, @@ -58,14 +40,20 @@ module.exports = function *() { } if(event && event.done) - removeImportFromCache(importId) + importManager.delete(importId) else - updateImportInCache(importId, event.latest) - - event.type = 'import_update'; - event.import_id = importId; + importManager.update(importId, {latest: event.latest}) + + let wsEvent = { + type: 'import_update', + import_id: importId, + updates: { + latest: event.latest, + done: event.done + } + } - broadcast(event); + broadcast(wsEvent); }); let daterange = this.request.body.importer.daterange; @@ -78,9 +66,6 @@ module.exports = function *() { to: daterange.to } - addImportToCache(_import); - - this.body = { - id: importId - } + importManager.add(_import); + this.body = _import; } \ No newline at end of file diff --git a/web/routes/imports.js b/web/routes/imports.js deleted file mode 100644 index bf146f9ca..000000000 --- a/web/routes/imports.js +++ /dev/null @@ -1,5 +0,0 @@ -const cache = require('../cache'); - -module.exports = function *() { - this.body = cache.get('running_imports'); -} \ No newline at end of file diff --git a/web/routes/list.js b/web/routes/list.js new file mode 100644 index 000000000..3e76b886e --- /dev/null +++ b/web/routes/list.js @@ -0,0 +1,7 @@ +const cache = require('../state/cache'); + +module.exports = function(name) { + return function *() { + this.body = cache.get(name).list(); + } +} \ No newline at end of file diff --git a/web/routes/livegekkos.js b/web/routes/livegekkos.js deleted file mode 100644 index 47ea8bf0c..000000000 --- a/web/routes/livegekkos.js +++ /dev/null @@ -1,5 +0,0 @@ -const cache = require('../cache'); - -module.exports = function *() { - this.body = cache.get('live_gekkos'); -} \ No newline at end of file diff --git a/web/routes/startGekko.js b/web/routes/startGekko.js index df21c0068..9c8660a5c 100644 --- a/web/routes/startGekko.js +++ b/web/routes/startGekko.js @@ -3,7 +3,7 @@ const promisify = require('tiny-promisify'); const moment = require('moment'); const pipelineRunner = promisify(require('../../core/workers/pipeline/parent')); -const cache = require('../cache'); +const cache = require('../state/cache'); const broadcast = cache.get('broadcast'); const base = require('./baseConfig'); diff --git a/web/routes/strategies.js b/web/routes/strategies.js index 896aa4e41..d9c604f44 100644 --- a/web/routes/strategies.js +++ b/web/routes/strategies.js @@ -12,7 +12,7 @@ module.exports = function *() { }); - // for every strat, check if there is a config file and it + // for every strat, check if there is a config file and add it const stratConfigPath = gekkoRoot + 'config/strategies'; const strategyParamsDir = yield fs.readdir(stratConfigPath); diff --git a/web/server.js b/web/server.js index b16673d43..39fb153c1 100644 --- a/web/server.js +++ b/web/server.js @@ -15,7 +15,8 @@ const app = koa(); const WebSocketServer = require('ws').Server; const wss = new WebSocketServer({ server: server }); -const cache = require('./cache'); +const cache = require('./state/cache'); +const ListManager = require('./state/listManager'); // broadcast function const broadcast = data => { @@ -28,30 +29,39 @@ const broadcast = data => { ); } cache.set('broadcast', broadcast); -cache.set('running_imports', []); -cache.set('live_gekkos', []); -const WEBROOT = __dirname + '/'; +// initialize lists and dump into cache +cache.set('imports', new ListManager); +cache.set('watchers', new ListManager); +cache.set('gekkos', new ListManager); + +// setup API routes -app.use(cors()); +const WEBROOT = __dirname + '/'; +const ROUTE = n => WEBROOT + 'routes/' + n; // attach routes -router.get('/api/strategies', require(WEBROOT + 'routes/strategies')); -router.get('/api/imports', require(WEBROOT + 'routes/imports')); -router.get('/api/livegekkos', require(WEBROOT + 'routes/liveGekkos')); -router.get('/api/configPart/:part', require(WEBROOT + 'routes/configPart')); +router.get('/api/strategies', require(ROUTE('strategies'))); +router.get('/api/configPart/:part', require(ROUTE('configPart'))); -router.post('/api/scan', require(WEBROOT + 'routes/scanDateRange')); -router.post('/api/scansets', require(WEBROOT + 'routes/scanDatasets')); -router.post('/api/backtest', require(WEBROOT + 'routes/backtest')); -router.post('/api/import', require(WEBROOT + 'routes/import')); -router.post('/api/startGekko', require(WEBROOT + 'routes/startGekko')); +const listWraper = require(ROUTE('list')); +router.get('/api/imports', listWraper('imports')); +router.get('/api/gekkos', listWraper('gekkos')); +router.get('/api/watchers', listWraper('watchers')); +router.post('/api/scan', require(ROUTE('scanDateRange'))); +router.post('/api/scansets', require(ROUTE('scanDatasets'))); +router.post('/api/backtest', require(ROUTE('backtest'))); +router.post('/api/import', require(ROUTE('import'))); +router.post('/api/startGekko', require(ROUTE('startGekko'))); + +// incoming WS: // wss.on('connection', ws => { // ws.on('message', _.noop); // }); app + .use(cors()) .use(serve(WEBROOT + 'vue')) .use(bodyParser()) .use(require('koa-logger')()) @@ -69,5 +79,10 @@ server.listen(config.port, () => { } console.log('Serving Gekko UI on ' + location + '\n'); - opn(location); + + // only open a browser when running `node gekko` + // this prevents opening the browser during development + let nodeCommand = _.last(process.argv[1].split('/')); + if(nodeCommand === 'gekko') + opn(location); }); \ No newline at end of file From a64f7313824076dd4a0edd6bb2f531831c9265b3 Mon Sep 17 00:00:00 2001 From: Mike van Rossum Date: Sun, 27 Nov 2016 14:32:56 +0000 Subject: [PATCH 02/25] run UI server seperately in dev --- docs/internals/gekko_ui/frontend.md | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/docs/internals/gekko_ui/frontend.md b/docs/internals/gekko_ui/frontend.md index 3569fa5e5..f1e4cdbc3 100644 --- a/docs/internals/gekko_ui/frontend.md +++ b/docs/internals/gekko_ui/frontend.md @@ -38,17 +38,16 @@ You first need to install all developer dependencies so the frontend app can be After this you can launch a hot reload version of the app which will automatically recompile the frontend and reload your browser: - # path to gekko - cd gekko - # launch normal gekko UI - we use this API - node gekko --ui - # now click away the browser tab (`http://localhost:3000`) + # path to webserver + cd gekko/web + # launch the server - we use this API + node server # path to vue app - cd gekko/web/vue + cd vue npm run dev -Gekko UI is now served from port 8080, the webpack dev server will compile the vue app (in memory) and intercept all calls to the app itself (`/dist/build.js`) and serve the in memory app. It is important to note that this UI still talks to the API served from the `node gekko --ui` commmand (on default http://localhost:3000/api) +Gekko UI is now served from port 8080, the webpack dev server will compile the vue app (in memory) and intercept all calls to the app itself (`/dist/build.js`) and serve the in memory app. It is important to note that this UI still talks to the API served from the `node server` commmand (on default http://localhost:3000/api) ### Recompiling the Gekko UI frontend From 18712ac03a4144c4efda24e8fe1db42b65f201e3 Mon Sep 17 00:00:00 2001 From: Mike van Rossum Date: Sun, 27 Nov 2016 15:16:08 +0000 Subject: [PATCH 03/25] moving frontend stuff around --- package.json | 3 +- web/state/cache.js | 14 ++++++ web/state/listManager.js | 45 +++++++++++++++++++ web/vue/src/App.vue | 4 +- .../backtester/backtestConfigBuilder.vue | 0 .../backtester/backtester.vue | 2 +- .../backtester/result/chartWrapper.vue | 2 +- .../backtester/result/result.vue | 0 .../backtester/result/summary.vue | 0 web/vue/src/{ => components}/data/data.vue | 2 +- .../data/import/importConfigBuilder.vue | 0 .../src/components/data/import/importState.js | 0 .../{ => components}/data/import/importer.vue | 4 +- .../{ => components}/data/import/single.vue | 4 +- .../gekko/gekkoConfigBuilder.vue | 2 +- web/vue/src/{ => components}/gekko/list.vue | 4 +- web/vue/src/{ => components}/gekko/new.vue | 2 +- web/vue/src/{ => components}/gekko/single.vue | 4 +- .../{ => components}/global/blockSpinner.vue | 0 .../global/configbuilder/datasetpicker.vue | 2 +- .../global/configbuilder/marketpicker.vue | 0 .../global/configbuilder/markets.js | 2 +- .../global/configbuilder/papertrader.vue | 2 +- .../global/configbuilder/rangecreator.vue | 2 +- .../global/configbuilder/rangepicker.vue | 2 +- .../global/configbuilder/stratpicker.vue | 2 +- .../global/configbuilder/typepicker.vue | 0 .../{ => components}/global/mixins/dataset.js | 2 +- .../src/{tools => components/global}/ws.js | 2 +- .../src/{ => components}/layout/footer.vue | 4 +- .../src/{ => components}/layout/header.vue | 0 web/vue/src/{ => components}/layout/home.vue | 2 +- web/vue/src/main.js | 20 +++++---- 33 files changed, 98 insertions(+), 36 deletions(-) create mode 100644 web/state/cache.js create mode 100644 web/state/listManager.js rename web/vue/src/{ => components}/backtester/backtestConfigBuilder.vue (100%) rename web/vue/src/{ => components}/backtester/backtester.vue (97%) rename web/vue/src/{ => components}/backtester/result/chartWrapper.vue (95%) rename web/vue/src/{ => components}/backtester/result/result.vue (100%) rename web/vue/src/{ => components}/backtester/result/summary.vue (100%) rename web/vue/src/{ => components}/data/data.vue (98%) rename web/vue/src/{ => components}/data/import/importConfigBuilder.vue (100%) create mode 100644 web/vue/src/components/data/import/importState.js rename web/vue/src/{ => components}/data/import/importer.vue (95%) rename web/vue/src/{ => components}/data/import/single.vue (96%) rename web/vue/src/{ => components}/gekko/gekkoConfigBuilder.vue (98%) rename web/vue/src/{ => components}/gekko/list.vue (95%) rename web/vue/src/{ => components}/gekko/new.vue (94%) rename web/vue/src/{ => components}/gekko/single.vue (95%) rename web/vue/src/{ => components}/global/blockSpinner.vue (100%) rename web/vue/src/{ => components}/global/configbuilder/datasetpicker.vue (97%) rename web/vue/src/{ => components}/global/configbuilder/marketpicker.vue (100%) rename web/vue/src/{ => components}/global/configbuilder/markets.js (75%) rename web/vue/src/{ => components}/global/configbuilder/papertrader.vue (97%) rename web/vue/src/{ => components}/global/configbuilder/rangecreator.vue (97%) rename web/vue/src/{ => components}/global/configbuilder/rangepicker.vue (98%) rename web/vue/src/{ => components}/global/configbuilder/stratpicker.vue (98%) rename web/vue/src/{ => components}/global/configbuilder/typepicker.vue (100%) rename web/vue/src/{ => components}/global/mixins/dataset.js (96%) rename web/vue/src/{tools => components/global}/ws.js (94%) rename web/vue/src/{ => components}/layout/footer.vue (76%) rename web/vue/src/{ => components}/layout/header.vue (100%) rename web/vue/src/{ => components}/layout/home.vue (93%) diff --git a/package.json b/package.json index 22383d3d5..41427708f 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,8 @@ "chai": "^2.0.0", "mocha": "^2.1.1", "proxyquire": "^1.7.10", - "sinon": "^1.12.2" + "sinon": "^1.12.2", + "vuex": "^2.0.0" }, "engines": { "node": ">=6.0" diff --git a/web/state/cache.js b/web/state/cache.js new file mode 100644 index 000000000..092105151 --- /dev/null +++ b/web/state/cache.js @@ -0,0 +1,14 @@ +const _ = require('lodash'); + +const cache = {}; + +module.exports = { + set: (name, val) => { + cache[name] = val; + return true; + }, + get: name => { + if(_.has(cache, name)) + return cache[name]; + } +} \ No newline at end of file diff --git a/web/state/listManager.js b/web/state/listManager.js new file mode 100644 index 000000000..874752abf --- /dev/null +++ b/web/state/listManager.js @@ -0,0 +1,45 @@ +// manages a list of things that change over time +// used for: +// - The currently running imports +// - The currently running gekko watchers +// - The live gekkos +// - etc.. + +// this is used in both the frontend +// as well as the backend! + +const _ = require('lodash'); + +var ListManager = function() { + this._list = []; +} + +// add an item to the list +ListManager.prototype.add = function(obj) { + if(!obj.id) + return false; + this._list.push(_.clone(obj)); + return true; +} + +// update some properties on an item +ListManager.prototype.update = function(id, updates) { + let item = this._list.find(i => i.id === id); + if(!item) + return false; + _.merge(item, updates); + return true; +} + +// delete an item from the list +ListManager.prototype.delete = function(id) { + this._list = this._list.filter(i => i.id !== id); + return true; +} + +// getter +ListManager.prototype.list = function() { + return this._list; +} + +module.exports = ListManager; \ No newline at end of file diff --git a/web/vue/src/App.vue b/web/vue/src/App.vue index f18157567..50870ef45 100644 --- a/web/vue/src/App.vue +++ b/web/vue/src/App.vue @@ -1,7 +1,7 @@ diff --git a/web/vue/src/components/gekko/new.vue b/web/vue/src/components/gekko/new.vue index c8013d66d..6261ed3d4 100644 --- a/web/vue/src/components/gekko/new.vue +++ b/web/vue/src/components/gekko/new.vue @@ -9,6 +9,7 @@ + + diff --git a/web/vue/src/components/global/configbuilder/typepicker.vue b/web/vue/src/components/global/configbuilder/typepicker.vue index 97d35e6e9..8c4176177 100644 --- a/web/vue/src/components/global/configbuilder/typepicker.vue +++ b/web/vue/src/components/global/configbuilder/typepicker.vue @@ -1,14 +1,15 @@ diff --git a/web/vue/src/main.js b/web/vue/src/main.js index 0d331f66c..0784355f9 100644 --- a/web/vue/src/main.js +++ b/web/vue/src/main.js @@ -15,7 +15,8 @@ import singleImport from './components/data/import/single.vue' import gekkoList from './components/gekko/list.vue' import newGekko from './components/gekko/new.vue' -import singleGekko from './components/gekko/single.vue' +import singleGekko from './components/gekko/singleGekko.vue' +import singleWatcher from './components/gekko/singleWatcher.vue' import { connect as connectWS } from './components/global/ws' import initializeState from './store/init' @@ -30,7 +31,8 @@ const router = new VueRouter({ { path: '/data/importer/import/:id', component: singleImport }, { path: '/live-gekkos', component: gekkoList }, { path: '/live-gekkos/new', component: newGekko }, - { path: '/live-gekkos/gekko/:id', component: singleGekko } + { path: '/live-gekkos/gekko/:id', component: singleGekko }, + { path: '/live-gekkos/watcher/:id', component: singleWatcher } ] }); diff --git a/web/vue/src/store/index.js b/web/vue/src/store/index.js index 0621e9c7b..c2e536a5b 100644 --- a/web/vue/src/store/index.js +++ b/web/vue/src/store/index.js @@ -1,21 +1,27 @@ import Vue from 'vue' import Vuex from 'vuex' -import { addImport, syncImports, updateImport } from './modules/imports/mutations' +import _ from 'lodash' + +import * as importMutations from './modules/imports/mutations' +import * as watchMutations from './modules/watchers/mutations' + Vue.use(Vuex) const debug = process.env.NODE_ENV !== 'production' +let mutations = {}; + +// TODO: spread syntax +_.merge(mutations, importMutations); +_.merge(mutations, watchMutations); + export default new Vuex.Store({ state: { imports: [], - gekkos: [], + stratrunners: [], watchers: [] }, - mutations: { - addImport, - syncImports, - updateImport - }, + mutations, strict: debug }) \ No newline at end of file diff --git a/web/vue/src/store/init.js b/web/vue/src/store/init.js index 99f1e0b90..82eca56c2 100644 --- a/web/vue/src/store/init.js +++ b/web/vue/src/store/init.js @@ -1,7 +1,9 @@ import Vue from 'vue' import Vuex from 'vuex' import syncImports from './modules/imports/sync' +import syncWatchers from './modules/watchers/sync' export default function() { syncImports(); + syncWatchers(); } \ No newline at end of file diff --git a/web/vue/src/store/modules/imports/sync.js b/web/vue/src/store/modules/imports/sync.js index f91d68e77..9fcaed336 100644 --- a/web/vue/src/store/modules/imports/sync.js +++ b/web/vue/src/store/modules/imports/sync.js @@ -3,7 +3,6 @@ import store from '../../' import { bus } from '../../../components/global/ws' const init = () => { - store.commit('syncImports', []); get('imports', (err, resp) => { store.commit('syncImports', resp); }); diff --git a/web/vue/src/store/modules/stratrunners/mutations.js b/web/vue/src/store/modules/stratrunners/mutations.js new file mode 100644 index 000000000..42f4f0f8e --- /dev/null +++ b/web/vue/src/store/modules/stratrunners/mutations.js @@ -0,0 +1,17 @@ +export const addStratrunner = (state, runner) => { + state.stratrunners.push(runner); + return state; +} + +export const syncStratrunners = (state, runners) => { + state.stratrunners = runners; + return state; +} + +export const updateWatcher = (state, update) => { + let item = state.stratrunners.find(i => i.id === update.gekko_id); + if(!item) + return state; + _.merge(item, update.updates); + return state; +} \ No newline at end of file diff --git a/web/vue/src/store/modules/stratrunners/sync.js b/web/vue/src/store/modules/stratrunners/sync.js new file mode 100644 index 000000000..00ada2187 --- /dev/null +++ b/web/vue/src/store/modules/stratrunners/sync.js @@ -0,0 +1,23 @@ +import { get } from '../../../tools/ajax' +import store from '../../' +import { bus } from '../../../components/global/ws' +import _ from 'lodash' + +const init = () => { + get('gekkos', (err, resp) => { + let watchers = _.filter(resp, {mode: 'leech'}); + store.commit('syncStratrunners', watchers); + }); +} + +const sync = () => { + bus.$on('update', data => { + if(data.gekko_mode === 'leech') + store.commit('updateStratrunner', data); + }); +} + +export default function() { + init(); + sync(); +} \ No newline at end of file diff --git a/web/vue/src/store/modules/watchers/mutations.js b/web/vue/src/store/modules/watchers/mutations.js new file mode 100644 index 000000000..6bad628e6 --- /dev/null +++ b/web/vue/src/store/modules/watchers/mutations.js @@ -0,0 +1,17 @@ +export const addWatcher = (state, watcher) => { + state.watchers.push(watcher); + return state; +} + +export const syncWatchers = (state, watchers) => { + state.watchers = watchers; + return state; +} + +export const updateWatcher = (state, update) => { + let item = state.watchers.find(i => i.id === update.gekko_id); + if(!item) + return state; + _.merge(item, update.updates); + return state; +} \ No newline at end of file diff --git a/web/vue/src/store/modules/watchers/sync.js b/web/vue/src/store/modules/watchers/sync.js new file mode 100644 index 000000000..4e8939f16 --- /dev/null +++ b/web/vue/src/store/modules/watchers/sync.js @@ -0,0 +1,33 @@ +import { get } from '../../../tools/ajax' +import store from '../../' +import { bus } from '../../../components/global/ws' +import _ from 'lodash' + +const init = () => { + get('gekkos', (err, resp) => { + let watchers = _.filter(resp, {mode: 'realtime'}); + store.commit('syncWatchers', watchers); + }); +} + +const sync = () => { + + bus.$on('new_gekko', data => { + if(data.gekko.mode === 'realtime') + store.commit('addWatcher', data.gekko); + }); + + + const update = (data) => { + if(data.gekko_mode === 'realtime') + store.commit('updateWatcher', data); + } + + bus.$on('update', update); + bus.$on('startAt', update); +} + +export default function() { + init(); + sync(); +} \ No newline at end of file From d7e12461aa7d0634257bdae3c86f50fc99afc51e Mon Sep 17 00:00:00 2001 From: Mike van Rossum Date: Thu, 1 Dec 2016 16:12:40 +0000 Subject: [PATCH 06/25] fix routing to new watcher --- web/routes/startGekko.js | 2 +- web/vue/src/components/gekko/new.vue | 10 +++++++--- web/vue/src/components/gekko/singleWatcher.vue | 4 ++-- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/web/routes/startGekko.js b/web/routes/startGekko.js index e6788cbfb..dd95f032c 100644 --- a/web/routes/startGekko.js +++ b/web/routes/startGekko.js @@ -89,5 +89,5 @@ module.exports = function *() { gekko }); - this.body = {status: 'ok'}; + this.body = gekko; } \ No newline at end of file diff --git a/web/vue/src/components/gekko/new.vue b/web/vue/src/components/gekko/new.vue index 6261ed3d4..5d772c333 100644 --- a/web/vue/src/components/gekko/new.vue +++ b/web/vue/src/components/gekko/new.vue @@ -60,10 +60,14 @@ export default { // check if the specified market is already being watched if(this.existingMarketWatcher) { alert('This market is already being watched, redirecting you now...') - console.log('ROUTING TO EXISTING MARKETWATCHER'); + this.$router.push({ + path: `/live-gekkos/watcher/${this.existingMarketWatcher.id}` + }); } else { - this.startWatcher(function(error, resp) { - console.log('ROUTING TO NEW MARKETWATCHER'); + this.startWatcher((error, resp) => { + this.$router.push({ + path: `/live-gekkos/watcher/${resp.id}` + }); }); } diff --git a/web/vue/src/components/gekko/singleWatcher.vue b/web/vue/src/components/gekko/singleWatcher.vue index 7eaa4a71b..b328df965 100644 --- a/web/vue/src/components/gekko/singleWatcher.vue +++ b/web/vue/src/components/gekko/singleWatcher.vue @@ -3,7 +3,7 @@ div(v-if='!data') h1 Unknown Watcher p Gekko doesn't know what whatcher this is... - div(v-if='data && !error') + div(v-if='data') h2 Market Watcher .grd h3 Market @@ -24,7 +24,7 @@ .grd-row-col-2-6 Received data until .grd-row-col-4-6 {{ fmt(data.latest) }} .grd-row - .grd-row-col-2-6 Running for + .grd-row-col-2-6 Data spanning .grd-row-col-4-6 {{ humanizeDuration(moment(data.latest).diff(moment(data.startAt))) }} h3 Market graph p TODO: candle price graph! From 617ed04dec0367e2b741b584c9b1e2dad3849dee Mon Sep 17 00:00:00 2001 From: Mike van Rossum Date: Sat, 3 Dec 2016 15:34:48 +0000 Subject: [PATCH 07/25] use TOML files for configuration --- config/adapters/mongodb.toml | 4 ++ config/adapters/postgresql.toml | 4 ++ config/adapters/sqlite.toml | 7 +++ config/general.toml | 10 ++++ config/plugins/candleWriter.toml | 3 +- config/plugins/pushbullet.toml | 16 +++++++ config/plugins/trader.toml | 9 ++++ config/plugins/tradingAdvisor.toml | 9 ++++ core/pluginUtil.js | 9 +--- core/tools/configBuilder.js | 38 +++++++++++++++ core/tools/dataStitcher.js | 2 +- core/util.js | 20 ++++++-- plugins/sqlite/handle.js | 4 +- plugins/sqlite/util.js | 2 +- plugins/tradingAdvisor/tradingAdvisor.js | 26 ---------- sample-config.js | 61 +++++++++++------------- 16 files changed, 146 insertions(+), 78 deletions(-) create mode 100644 config/adapters/mongodb.toml create mode 100644 config/adapters/postgresql.toml create mode 100644 config/adapters/sqlite.toml create mode 100644 config/general.toml create mode 100644 config/plugins/pushbullet.toml create mode 100644 config/plugins/trader.toml create mode 100644 config/plugins/tradingAdvisor.toml create mode 100644 core/tools/configBuilder.js diff --git a/config/adapters/mongodb.toml b/config/adapters/mongodb.toml new file mode 100644 index 000000000..c7162f502 --- /dev/null +++ b/config/adapters/mongodb.toml @@ -0,0 +1,4 @@ +connectionString = "mongodb://mongodb/gekko" +dependencies = [{"module"=>"mongojs", "version"=>"2.4.0"}] +path = "plugins/mongodb" +version = 0.1 diff --git a/config/adapters/postgresql.toml b/config/adapters/postgresql.toml new file mode 100644 index 000000000..697240896 --- /dev/null +++ b/config/adapters/postgresql.toml @@ -0,0 +1,4 @@ +connectionString = "postgres://user:pass@localhost:5432" +dependencies = [{"module"=>"pg", "version"=>"6.1.0"}] +path = "plugins/postgresql" +version = 0.1 \ No newline at end of file diff --git a/config/adapters/sqlite.toml b/config/adapters/sqlite.toml new file mode 100644 index 000000000..48e683341 --- /dev/null +++ b/config/adapters/sqlite.toml @@ -0,0 +1,7 @@ +dataDirectory = "history" +path = "plugins/sqlite" +version = 0.1 + +[[dependencies]] +module = "sqlite3" +version = "3.1.4" \ No newline at end of file diff --git a/config/general.toml b/config/general.toml new file mode 100644 index 000000000..9df75eaa0 --- /dev/null +++ b/config/general.toml @@ -0,0 +1,10 @@ +debug = true + +# what database should Gekko use? +adapter = 'sqlite' + +[watch] +exchange = 'Bitstamp' +currency = 'USD' +asset = 'BTC' + diff --git a/config/plugins/candleWriter.toml b/config/plugins/candleWriter.toml index 54599a38f..3477e9e31 100644 --- a/config/plugins/candleWriter.toml +++ b/config/plugins/candleWriter.toml @@ -1,2 +1 @@ -enabled = true -sqlite = "sqlite" \ No newline at end of file +enabled = true \ No newline at end of file diff --git a/config/plugins/pushbullet.toml b/config/plugins/pushbullet.toml new file mode 100644 index 000000000..686352ee9 --- /dev/null +++ b/config/plugins/pushbullet.toml @@ -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 = "jon_snow@westeros.org" + +# your pushbullet API key +key = "xxx" + +# will make Gekko messages start with [GEKKO] +tag = "[GEKKO]" \ No newline at end of file diff --git a/config/plugins/trader.toml b/config/plugins/trader.toml new file mode 100644 index 000000000..8a76fc2c3 --- /dev/null +++ b/config/plugins/trader.toml @@ -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 = "" \ No newline at end of file diff --git a/config/plugins/tradingAdvisor.toml b/config/plugins/tradingAdvisor.toml new file mode 100644 index 000000000..54bc8e4ff --- /dev/null +++ b/config/plugins/tradingAdvisor.toml @@ -0,0 +1,9 @@ +enabled = true + +candleSize = 60 +historySize = 25 +method = "MACD" + +[talib] +enabled = false +version = "1.0.2" \ No newline at end of file diff --git a/core/pluginUtil.js b/core/pluginUtil.js index 1a92e3dca..e3a5dbacc 100644 --- a/core/pluginUtil.js +++ b/core/pluginUtil.js @@ -64,13 +64,6 @@ var pluginHelper = { plugin.config = config[plugin.slug]; - if(!plugin.config) - log.warn( - 'unable to find', - plugin.name, - 'in the config. Is your config up to date?' - ); - if(!plugin.config || !plugin.config.enabled) return next(); @@ -94,7 +87,7 @@ var pluginHelper = { return next(cannotLoad); if(plugin.path) - var Constructor = require(pluginDir + plugin.path(plugin.config)); + var Constructor = require(pluginDir + plugin.path(config)); else var Constructor = require(pluginDir + plugin.slug); diff --git a/core/tools/configBuilder.js b/core/tools/configBuilder.js new file mode 100644 index 000000000..b9fb1dd39 --- /dev/null +++ b/core/tools/configBuilder.js @@ -0,0 +1,38 @@ +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 = util.getTOML(configDir + 'general.toml'); + fs.readdirSync(configDir + 'plugins').forEach(function(pluginFile) { + let pluginName = _.first(pluginFile.split('.')) + _config[pluginName] = util.getTOML(configDir + 'plugins/' + pluginFile); + }); + + // attach the proper adapter + let adapter = _config.adapter; + _config[adapter] = util.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] = util.getTOML(stratFile); + } + + return _config; +} \ No newline at end of file diff --git a/core/tools/dataStitcher.js b/core/tools/dataStitcher.js index c0c487bc9..bcd150ed9 100644 --- a/core/tools/dataStitcher.js +++ b/core/tools/dataStitcher.js @@ -45,7 +45,7 @@ Stitcher.prototype.prepareHistoricalData = function(done) { return done(); var requiredHistory = config.tradingAdvisor.candleSize * config.tradingAdvisor.historySize; - var Reader = require(dirs.plugins + config.tradingAdvisor.adapter + '/reader'); + var Reader = require(dirs.plugins + config.adapter + '/reader'); this.reader = new Reader; diff --git a/core/util.js b/core/util.js index a6eb07376..c1e363e9e 100644 --- a/core/util.js +++ b/core/util.js @@ -19,14 +19,24 @@ var util = { if(_config) return _config; - var configFile = path.resolve(program.config || util.dirs().gekko + 'config.js'); + if(program.config) { + // we will use one single config file + if(!fs.existsSync(configFile)) + util.die('Cannot find the specified config file.'); - if(!fs.existsSync(configFile)) - util.die('Cannot find a config file.'); + _config = require(configFile); + return _config; + } - _config = require(configFile); + // build the config out of TOML files + var buildConfig = require(util.dirs().tools + 'configBuilder'); + _config = buildConfig(); return _config; }, + getTOML: function(fileName) { + var raw = fs.readFileSync(fileName); + return toml.parse(raw); + }, // overwrite the whole config setConfig: function(config) { _config = config; @@ -153,7 +163,7 @@ var util = { return true; else return false; - }, + } } // NOTE: those options are only used diff --git a/plugins/sqlite/handle.js b/plugins/sqlite/handle.js index 90d2f5bb9..78b374030 100644 --- a/plugins/sqlite/handle.js +++ b/plugins/sqlite/handle.js @@ -5,13 +5,13 @@ var util = require('../../core/util.js'); var config = util.getConfig(); var dirs = util.dirs(); -var adapter = config.adapters.sqlite; +var adapter = config.sqlite; // verify the correct dependencies are installed var pluginHelper = require(dirs.core + 'pluginUtil'); var pluginMock = { slug: 'sqlite adapter', - dependencies: config.adapters.sqlite.dependencies + dependencies: adapter.dependencies }; var cannotLoad = pluginHelper.cannotLoad(pluginMock); diff --git a/plugins/sqlite/util.js b/plugins/sqlite/util.js index 66d31c9e8..bedb57e19 100644 --- a/plugins/sqlite/util.js +++ b/plugins/sqlite/util.js @@ -4,7 +4,7 @@ var watch = config.watch; var settings = { exchange: watch.exchange, pair: [watch.currency, watch.asset], - historyPath: config.adapters.sqlite.dataDirectory + historyPath: config.sqlite.dataDirectory } module.exports = { diff --git a/plugins/tradingAdvisor/tradingAdvisor.js b/plugins/tradingAdvisor/tradingAdvisor.js index f3129d610..23b1166b4 100644 --- a/plugins/tradingAdvisor/tradingAdvisor.js +++ b/plugins/tradingAdvisor/tradingAdvisor.js @@ -19,8 +19,6 @@ var Actor = function(done) { this.methodName = config.tradingAdvisor.method; - this.generalizeMethodSettings(); - this.setupTradingMethod(); var mode = util.gekkoMode(); @@ -35,30 +33,6 @@ var Actor = function(done) { util.makeEventEmitter(Actor); -Actor.prototype.generalizeMethodSettings = function() { - // method settings can be either part of the main config OR a seperate - // toml configuration file. In case of the toml config file we need to - // parse and attach to main config object - - // config already part of - if(config[this.methodName]) { - log.warn('\t', 'Config already has', this.methodName, 'parameters. Ignoring toml file'); - return; - } - - var tomlFile = dirs.config + 'strategies/' + this.methodName + '.toml'; - - if(!fs.existsSync(tomlFile)) { - log.warn('\t', 'toml configuration file not found.'); - return; - } - - var rawSettings = fs.readFileSync(tomlFile); - config[this.methodName] = toml.parse(rawSettings); - - util.setConfig(config); -} - Actor.prototype.setupTradingMethod = function() { if(!fs.existsSync(dirs.methods + this.methodName + '.js')) diff --git a/sample-config.js b/sample-config.js index 6fbea2579..f167d4dda 100644 --- a/sample-config.js +++ b/sample-config.js @@ -175,43 +175,38 @@ config.adviceWriter = { // CONFIGURING ADAPTER // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +config.sqlite = { + path: 'plugins/sqlite', + dataDirectory: 'history', + version: 0.1, + dependencies: [{ + module: 'sqlite3', + version: '3.1.4' + }] +} - - -config.adapters = { - sqlite: { - path: 'plugins/sqlite', - - dataDirectory: 'history', - version: 0.1, - - dependencies: [{ - module: 'sqlite3', - version: '3.1.4' - }] - }, // Postgres adapter example config (please note: requires postgres >= 9.5): - postgresql: { - path: 'plugins/postgresql', - version: 0.1, - connectionString: 'postgres://user:pass@localhost:5432', // if default port - dependencies: [{ - module: 'pg', - version: '6.1.0' - }] - }, - // Mongodb adapter, requires mongodb >= 3.3 (no version earlier tested) - mongodb: { - path: 'plugins/mongodb', - version: 0.1, - connectionString: 'mongodb://mongodb/gekko', // connection to mongodb server - dependencies: [{ - module: 'mongojs', - version: '2.4.0' - }] - } +config.postgresql = { + path: 'plugins/postgresql', + version: 0.1, + connectionString: 'postgres://user:pass@localhost:5432', // if default port + dependencies: [{ + module: 'pg', + version: '6.1.0' + }] +} + +// Mongodb adapter, requires mongodb >= 3.3 (no version earlier tested) +config.mongodb = { + path: 'plugins/mongodb', + version: 0.1, + connectionString: 'mongodb://mongodb/gekko', // connection to mongodb server + dependencies: [{ + module: 'mongojs', + version: '2.4.0' + }] } // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ From 6e20f0315ecb57f091353285d016f50a201a0afb Mon Sep 17 00:00:00 2001 From: Mike van Rossum Date: Sat, 3 Dec 2016 19:39:51 +0000 Subject: [PATCH 08/25] use new config structure everywhere --- config/adapters/mongodb.toml | 5 +++- config/adapters/postgresql.toml | 7 +++-- config/general.toml | 6 ++-- core/markets/backtest.js | 2 +- core/markets/importer.js | 2 +- core/tools/configBuilder.js | 14 ++++++---- core/tools/dateRangeScanner.js | 2 +- core/util.js | 4 --- core/workers/datasetScan/parent.js | 2 +- plugins/sqlite/scanner.js | 2 +- sample-config.js | 1 - web/routes/baseConfig.js | 45 +++++++++++++++++++++--------- 12 files changed, 58 insertions(+), 34 deletions(-) diff --git a/config/adapters/mongodb.toml b/config/adapters/mongodb.toml index c7162f502..2aed9d10a 100644 --- a/config/adapters/mongodb.toml +++ b/config/adapters/mongodb.toml @@ -1,4 +1,7 @@ connectionString = "mongodb://mongodb/gekko" -dependencies = [{"module"=>"mongojs", "version"=>"2.4.0"}] path = "plugins/mongodb" version = 0.1 + +[[dependencies]] +module = "mongojs" +version = "2.4.0" \ No newline at end of file diff --git a/config/adapters/postgresql.toml b/config/adapters/postgresql.toml index 697240896..834f442cd 100644 --- a/config/adapters/postgresql.toml +++ b/config/adapters/postgresql.toml @@ -1,4 +1,7 @@ connectionString = "postgres://user:pass@localhost:5432" -dependencies = [{"module"=>"pg", "version"=>"6.1.0"}] path = "plugins/postgresql" -version = 0.1 \ No newline at end of file +version = 0.1 + +[[dependencies]] +module = "pg" +version = "6.1.0" \ No newline at end of file diff --git a/config/general.toml b/config/general.toml index 9df75eaa0..fa1696ad7 100644 --- a/config/general.toml +++ b/config/general.toml @@ -1,10 +1,10 @@ -debug = true +debug = false # what database should Gekko use? adapter = 'sqlite' [watch] -exchange = 'Bitstamp' -currency = 'USD' +exchange = 'Poloniex' +currency = 'USDT' asset = 'BTC' diff --git a/core/markets/backtest.js b/core/markets/backtest.js index 7947079d6..405ec03bc 100644 --- a/core/markets/backtest.js +++ b/core/markets/backtest.js @@ -5,7 +5,7 @@ var dirs = util.dirs(); var log = require(dirs.core + 'log'); var moment = require('moment'); -var adapter = config.adapters[config.backtest.adapter]; +var adapter = config[config.adapter]; var Reader = require(dirs.gekko + adapter.path + '/reader'); var daterange = config.backtest.daterange; diff --git a/core/markets/importer.js b/core/markets/importer.js index ec6cd4419..e9dd4b679 100644 --- a/core/markets/importer.js +++ b/core/markets/importer.js @@ -6,7 +6,7 @@ var log = require(dirs.core + 'log'); var moment = require('moment'); var cp = require(dirs.core + 'cp'); -var adapter = config.adapters[config.importer.adapter]; +var adapter = config[config.adapter]; var daterange = config.importer.daterange; var from = moment.utc(daterange.from); diff --git a/core/tools/configBuilder.js b/core/tools/configBuilder.js index b9fb1dd39..7f4034825 100644 --- a/core/tools/configBuilder.js +++ b/core/tools/configBuilder.js @@ -5,7 +5,6 @@ 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); @@ -15,15 +14,15 @@ const getTOML = function(fileName) { module.exports = function() { const configDir = util.dirs().config; - let _config = util.getTOML(configDir + 'general.toml'); + let _config = getTOML(configDir + 'general.toml'); fs.readdirSync(configDir + 'plugins').forEach(function(pluginFile) { let pluginName = _.first(pluginFile.split('.')) - _config[pluginName] = util.getTOML(configDir + 'plugins/' + pluginFile); + _config[pluginName] = getTOML(configDir + 'plugins/' + pluginFile); }); // attach the proper adapter let adapter = _config.adapter; - _config[adapter] = util.getTOML(configDir + 'adapters/' + adapter + '.toml'); + _config[adapter] = getTOML(configDir + 'adapters/' + adapter + '.toml'); if(_config.tradingAdvisor.enabled) { // also load the strat @@ -31,8 +30,13 @@ module.exports = function() { let stratFile = configDir + 'strategies/' + strat + '.toml'; if(!fs.existsSync(stratFile)) util.die('Cannot find the strategy config file for ' + strat); - _config[strat] = util.getTOML(stratFile); + _config[strat] = getTOML(stratFile); } + const mode = util.gekkoMode(); + + if(mode === 'backtest') + _config.backtest = getTOML(configDir + 'backtest.toml'); + return _config; } \ No newline at end of file diff --git a/core/tools/dateRangeScanner.js b/core/tools/dateRangeScanner.js index 01587e235..40e2d7502 100644 --- a/core/tools/dateRangeScanner.js +++ b/core/tools/dateRangeScanner.js @@ -10,7 +10,7 @@ var config = util.getConfig(); var dirs = util.dirs(); var log = require(dirs.core + 'log'); -var adapter = config.adapters[config.backtest.adapter]; +var adapter = config[config.adapter]; var Reader = require(dirs.gekko + adapter.path + '/reader'); var reader = new Reader(); diff --git a/core/util.js b/core/util.js index c1e363e9e..53e28788f 100644 --- a/core/util.js +++ b/core/util.js @@ -33,10 +33,6 @@ var util = { _config = buildConfig(); return _config; }, - getTOML: function(fileName) { - var raw = fs.readFileSync(fileName); - return toml.parse(raw); - }, // overwrite the whole config setConfig: function(config) { _config = config; diff --git a/core/workers/datasetScan/parent.js b/core/workers/datasetScan/parent.js index b1f3f64a3..d8b66ea65 100644 --- a/core/workers/datasetScan/parent.js +++ b/core/workers/datasetScan/parent.js @@ -7,7 +7,7 @@ var config = util.getConfig(); var dirs = util.dirs(); var log = require(dirs.core + 'log'); -var adapter = config.adapters[config.backtest.adapter]; +var adapter = config[config.adapter]; var scan = require(dirs.gekko + adapter.path + '/scanner'); var dateRangeScan = require('../dateRangeScan/parent'); diff --git a/plugins/sqlite/scanner.js b/plugins/sqlite/scanner.js index 2e9ec82c8..31eaec801 100644 --- a/plugins/sqlite/scanner.js +++ b/plugins/sqlite/scanner.js @@ -10,7 +10,7 @@ const sqlite3 = require('sqlite3'); // todo: rewrite with generators or async/await.. module.exports = done => { - const dbDirectory = dirs.gekko + config.adapters.sqlite.dataDirectory + const dbDirectory = dirs.gekko + config.sqlite.dataDirectory if(!fs.existsSync(dbDirectory)) return done(null, []); diff --git a/sample-config.js b/sample-config.js index f167d4dda..c8dd5b771 100644 --- a/sample-config.js +++ b/sample-config.js @@ -217,7 +217,6 @@ config.mongodb = { // @link: https://github.com/askmike/gekko/blob/stable/docs/Backtesting.md config.backtest = { - adapter: 'sqlite', daterange: 'scan', batchSize: 50 } diff --git a/web/routes/baseConfig.js b/web/routes/baseConfig.js index 32635cfc4..384174fc6 100644 --- a/web/routes/baseConfig.js +++ b/web/routes/baseConfig.js @@ -12,7 +12,6 @@ config.debug = false; // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ config.tradingAdvisor = { - adapter: 'sqlite', talib: { enabled: false, // todo! version: '1.0.2' @@ -39,7 +38,6 @@ config.profitSimulator = { } config.candleWriter = { - adapter: 'sqlite', enabled: true } @@ -47,18 +45,40 @@ config.candleWriter = { // CONFIGURING ADAPTER // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -config.adapters = { - sqlite: { - path: 'plugins/sqlite', +config.adapter = 'sqlite'; - dataDirectory: 'history', - version: 0.1, +config.sqlite = { + path: 'plugins/sqlite', - dependencies: [{ - module: 'sqlite3', - version: '3.1.4' - }] - } + dataDirectory: 'history', + version: 0.1, + + dependencies: [{ + module: 'sqlite3', + version: '3.1.4' + }] +} + + // Postgres adapter example config (please note: requires postgres >= 9.5): +config.postgresql = { + path: 'plugins/postgresql', + version: 0.1, + connectionString: 'postgres://user:pass@localhost:5432', // if default port + dependencies: [{ + module: 'pg', + version: '6.1.0' + }] +} + +// Mongodb adapter, requires mongodb >= 3.3 (no version earlier tested) +config.mongodb = { + path: 'plugins/mongodb', + version: 0.1, + connectionString: 'mongodb://mongodb/gekko', // connection to mongodb server + dependencies: [{ + module: 'mongojs', + version: '2.4.0' + }] } // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -69,7 +89,6 @@ config.adapters = { // @link: https://github.com/askmike/gekko/blob/stable/docs/Backtesting.md config.backtest = { - adapter: 'sqlite', daterange: 'scan', batchSize: 50 } From 67ae8ab6529c58577046caf32c599a25d5f0d2c5 Mon Sep 17 00:00:00 2001 From: Mike van Rossum Date: Sat, 3 Dec 2016 21:02:51 +0000 Subject: [PATCH 09/25] implement getCandles & hook up to API --- config/backtest.toml | 8 ++ core/markets/backtest.js | 6 ++ core/tools/candleLoader.js | 97 +++++++++++++++++++ core/workers/loadCandles/child.js | 25 +++++ core/workers/loadCandles/parent.js | 62 ++++++++++++ web/routes/getCandles.js | 39 ++++++++ web/server.js | 1 + web/vue/src/components/gekko/list.vue | 10 +- .../src/components/gekko/singleWatcher.vue | 60 +++++++++++- web/vue/src/components/layout/header.vue | 1 + 10 files changed, 303 insertions(+), 6 deletions(-) create mode 100644 config/backtest.toml create mode 100644 core/tools/candleLoader.js create mode 100644 core/workers/loadCandles/child.js create mode 100644 core/workers/loadCandles/parent.js create mode 100644 web/routes/getCandles.js diff --git a/config/backtest.toml b/config/backtest.toml new file mode 100644 index 000000000..3569ec11d --- /dev/null +++ b/config/backtest.toml @@ -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" \ No newline at end of file diff --git a/core/markets/backtest.js b/core/markets/backtest.js index 405ec03bc..2e7cd3e53 100644 --- a/core/markets/backtest.js +++ b/core/markets/backtest.js @@ -15,6 +15,12 @@ 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); diff --git a/core/tools/candleLoader.js b/core/tools/candleLoader.js new file mode 100644 index 000000000..d1865661f --- /dev/null +++ b/core/tools/candleLoader.js @@ -0,0 +1,97 @@ +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(_.last(data).start >= toUnix) + DONE = true; + + batcher.write(data); + + if(DONE) { + console.log('reader closed!'); + reader.close(); + } else { + shiftIterator(); + getBatch(); + } +} + +const handleBatchedCandles = candle => { + result.push(candle); + if(DONE) + doneFn(); +} \ No newline at end of file diff --git a/core/workers/loadCandles/child.js b/core/workers/loadCandles/child.js new file mode 100644 index 000000000..0fbf2cb2a --- /dev/null +++ b/core/workers/loadCandles/child.js @@ -0,0 +1,25 @@ +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 => { + console.log('LOAD CALLBACK!'); + process.send(candles); + }) +} + +process.send('ready'); + +process.on('message', (m) => { + if(m.what === 'start') + start(m.config, m.candleSize, m.daterange); +}); \ No newline at end of file diff --git a/core/workers/loadCandles/parent.js b/core/workers/loadCandles/parent.js new file mode 100644 index 000000000..c9f41b3e7 --- /dev/null +++ b/core/workers/loadCandles/parent.js @@ -0,0 +1,62 @@ +// 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) => { + const child = fork(__dirname + '/child'); + + const message = { + what: 'start', + config + } + + const done = _.once(callback); + + child.on('message', function(m) { + console.log('GOT MESSAGE!') + if(m === 'ready') + return child.send(message); + + // else we are done and have candles! + done(null, m); + child.kill('SIGINT'); + }); + + child.on('exit', code => { + if(code !== 0) + done('ERROR, unable to load candles, please check the console.'); + }); +} \ No newline at end of file diff --git a/web/routes/getCandles.js b/web/routes/getCandles.js new file mode 100644 index 000000000..2265514c5 --- /dev/null +++ b/web/routes/getCandles.js @@ -0,0 +1,39 @@ +// simple POST request that returns the candles requested + +// expects a config like: + +// 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 +// } + +const _ = require('lodash'); +const promisify = require('tiny-promisify'); +const candleLoader = promisify(require('../../core/workers/loadCandles/parent')); + +// starts a backtest +// requires a post body with a config object +module.exports = function *() { + + this.body = yield candleLoader(this.request.body); +} \ No newline at end of file diff --git a/web/server.js b/web/server.js index 688a6375e..11c83f476 100644 --- a/web/server.js +++ b/web/server.js @@ -52,6 +52,7 @@ router.post('/api/scansets', require(ROUTE('scanDatasets'))); router.post('/api/backtest', require(ROUTE('backtest'))); router.post('/api/import', require(ROUTE('import'))); router.post('/api/startGekko', require(ROUTE('startGekko'))); +router.post('/api/getCandles', require(ROUTE('getCandles'))); // incoming WS: // wss.on('connection', ws => { diff --git a/web/vue/src/components/gekko/list.vue b/web/vue/src/components/gekko/list.vue index f30e2c092..9f4abfef0 100644 --- a/web/vue/src/components/gekko/list.vue +++ b/web/vue/src/components/gekko/list.vue @@ -5,7 +5,7 @@ h3 Market watchers .text(v-if='!watchers.length') p You are currently not watching any markets. - table.full(v-if='watchers.length') + table.full.clickable(v-if='watchers.length') thead tr th exchange @@ -94,6 +94,14 @@ export default { diff --git a/web/vue/src/components/gekko/list.vue b/web/vue/src/components/gekko/list.vue index 9f4abfef0..c5488de06 100644 --- a/web/vue/src/components/gekko/list.vue +++ b/web/vue/src/components/gekko/list.vue @@ -19,9 +19,12 @@ td {{ gekko.watch.exchange }} td {{ gekko.watch.currency }} td {{ gekko.watch.asset }} - td {{ fmt(gekko.startAt) }} - td {{ fmt(gekko.latest) }} - td {{ humanizeDuration(moment(gekko.latest).diff(moment(gekko.startAt))) }} + td + template(v-if='gekko.firstCandle') {{ fmt(gekko.firstCandle.start) }} + td + template(v-if='gekko.lastCandle') {{ fmt(gekko.lastCandle.start) }} + td + template(v-if='gekko.firstCandle && gekko.lastCandle') {{ timespan(gekko.lastCandle.start, gekko.firstCandle.start) }} .hr h3 Strat runners .text(v-if='!stratrunners.length') @@ -32,19 +35,21 @@ th exchange th currency th asset - th started at - th last update th duration - th type + th last update + th strategy + th profit tbody - tr.clickable(v-for='gekko in gekkos', v-on:click='$router.push({path: `live-gekkos/gekko/${gekko.id}`})') + tr.clickable(v-for='gekko in stratrunners', v-on:click='$router.push({path: `live-gekkos/stratrunner/${gekko.id}`})') td {{ gekko.watch.exchange }} td {{ gekko.watch.currency }} td {{ gekko.watch.asset }} - td {{ fmt(gekko.startAt) }} - td {{ fmt(gekko.latest) }} - td {{ humanizeDuration(gekko.latest.diff(gekko.startAt)) }} - td {{ gekko.type }} + td + template(v-if='gekko.firstCandle && gekko.lastCandle') {{ timespan(gekko.lastCandle.start, gekko.firstCandle.start) }} + td + template(v-if='gekko.lastCandle') {{ timespan(moment(), gekko.lastCandle.start) }} + td {{ gekko.strat.name }} + td TODO! .hr h2 Start a new live Gekko router-link(to='/live-gekkos/new') start a new live Gekko! @@ -52,7 +57,7 @@ diff --git a/web/vue/src/components/gekko/new.vue b/web/vue/src/components/gekko/new.vue index 5e9389895..439c67624 100644 --- a/web/vue/src/components/gekko/new.vue +++ b/web/vue/src/components/gekko/new.vue @@ -9,6 +9,7 @@ - - diff --git a/web/vue/src/components/gekko/singleStratrunner.vue b/web/vue/src/components/gekko/singleStratrunner.vue new file mode 100644 index 000000000..03bdcd236 --- /dev/null +++ b/web/vue/src/components/gekko/singleStratrunner.vue @@ -0,0 +1,169 @@ + + + + + diff --git a/web/vue/src/components/gekko/singleWatcher.vue b/web/vue/src/components/gekko/singleWatcher.vue index f8b160318..f61330c0a 100644 --- a/web/vue/src/components/gekko/singleWatcher.vue +++ b/web/vue/src/components/gekko/singleWatcher.vue @@ -17,19 +17,22 @@ .grd-row-col-2-6 Asset .grd-row-col-4-6 {{ data.watch.asset }} h3 Statistics - .grd-row(v-if='data.firstCandle') - .grd-row-col-2-6 Watching since - .grd-row-col-4-6 {{ fmt(data.firstCandle.start) }} - .grd-row(v-if='data.lastCandle') - .grd-row-col-2-6 Received data until - .grd-row-col-4-6 {{ fmt(data.lastCandle.start) }} - .grd-row(v-if='data.lastCandle && data.firstCandle') - .grd-row-col-2-6 Data spanning - .grd-row-col-4-6 {{ humanizeDuration(moment(data.lastCandle.start).diff(moment(data.firstCandle.start))) }} - h3.contain Market graph - spinner(v-if='candleFetch === "fetching"') - template(v-if='candles.length') - chart(:data='chartData') + spinner(v-if='isLoading') + template(v-if='!isLoading') + .grd-row(v-if='data.firstCandle') + .grd-row-col-2-6 Watching since + .grd-row-col-4-6 {{ fmt(data.firstCandle.start) }} + .grd-row(v-if='data.lastCandle') + .grd-row-col-2-6 Received data until + .grd-row-col-4-6 {{ fmt(data.lastCandle.start) }} + .grd-row(v-if='data.lastCandle && data.firstCandle') + .grd-row-col-2-6 Data spanning + .grd-row-col-4-6 {{ humanizeDuration(moment(data.lastCandle.start).diff(moment(data.firstCandle.start))) }} + template(v-if='!isLoading') + h3.contain Market graph + spinner(v-if='candleFetch === "fetching"') + template(v-if='candles.length') + chart(:data='chartData') + + diff --git a/web/vue/src/components/layout/modal.vue b/web/vue/src/components/layout/modal.vue new file mode 100644 index 000000000..c2cb03ba8 --- /dev/null +++ b/web/vue/src/components/layout/modal.vue @@ -0,0 +1,82 @@ + + + + + diff --git a/web/vue/src/d3/message.js b/web/vue/src/d3/message.js new file mode 100644 index 000000000..a1b3eb028 --- /dev/null +++ b/web/vue/src/d3/message.js @@ -0,0 +1,11 @@ +export const draw = function(message) { + d3.select("#chart").append("text") + .attr('class', 'message') + .attr('x', 150) + .attr('y', 150) + .text(message); +} + +export const clear = function() { + svg.find('text').remove(); +} \ No newline at end of file diff --git a/web/vue/src/main.js b/web/vue/src/main.js index 0784355f9..1ce81f3ef 100644 --- a/web/vue/src/main.js +++ b/web/vue/src/main.js @@ -15,7 +15,7 @@ import singleImport from './components/data/import/single.vue' import gekkoList from './components/gekko/list.vue' import newGekko from './components/gekko/new.vue' -import singleGekko from './components/gekko/singleGekko.vue' +import singleStratrunner from './components/gekko/singleStratrunner.vue' import singleWatcher from './components/gekko/singleWatcher.vue' import { connect as connectWS } from './components/global/ws' import initializeState from './store/init' @@ -31,7 +31,7 @@ const router = new VueRouter({ { path: '/data/importer/import/:id', component: singleImport }, { path: '/live-gekkos', component: gekkoList }, { path: '/live-gekkos/new', component: newGekko }, - { path: '/live-gekkos/gekko/:id', component: singleGekko }, + { path: '/live-gekkos/stratrunner/:id', component: singleStratrunner }, { path: '/live-gekkos/watcher/:id', component: singleWatcher } ] }); diff --git a/web/vue/src/store/index.js b/web/vue/src/store/index.js index c2e536a5b..8af0732a4 100644 --- a/web/vue/src/store/index.js +++ b/web/vue/src/store/index.js @@ -4,6 +4,7 @@ import _ from 'lodash' import * as importMutations from './modules/imports/mutations' import * as watchMutations from './modules/watchers/mutations' +import * as stratrunnerMutations from './modules/stratrunners/mutations' Vue.use(Vuex) @@ -15,6 +16,7 @@ let mutations = {}; // TODO: spread syntax _.merge(mutations, importMutations); _.merge(mutations, watchMutations); +_.merge(mutations, stratrunnerMutations); export default new Vuex.Store({ state: { diff --git a/web/vue/src/store/init.js b/web/vue/src/store/init.js index 82eca56c2..6c9a197ec 100644 --- a/web/vue/src/store/init.js +++ b/web/vue/src/store/init.js @@ -1,9 +1,12 @@ import Vue from 'vue' import Vuex from 'vuex' + import syncImports from './modules/imports/sync' import syncWatchers from './modules/watchers/sync' +import syncStratrunners from './modules/stratrunners/sync' export default function() { syncImports(); syncWatchers(); + syncStratrunners(); } \ No newline at end of file diff --git a/web/vue/src/store/modules/messages/mutations.js b/web/vue/src/store/modules/messages/mutations.js new file mode 100644 index 000000000..e69de29bb diff --git a/web/vue/src/store/modules/stratrunners/mutations.js b/web/vue/src/store/modules/stratrunners/mutations.js index 42f4f0f8e..a7ecc0f3c 100644 --- a/web/vue/src/store/modules/stratrunners/mutations.js +++ b/web/vue/src/store/modules/stratrunners/mutations.js @@ -1,3 +1,5 @@ +import Vue from 'vue' + export const addStratrunner = (state, runner) => { state.stratrunners.push(runner); return state; @@ -8,10 +10,14 @@ export const syncStratrunners = (state, runners) => { return state; } -export const updateWatcher = (state, update) => { - let item = state.stratrunners.find(i => i.id === update.gekko_id); +export const updateStratrunner = (state, update) => { + let index = state.stratrunners.findIndex(i => i.id === update.gekko_id); + let item = state.stratrunners[index]; if(!item) return state; - _.merge(item, update.updates); + + let updated = Vue.util.extend(item, update.updates); + Vue.set(state.stratrunners, index, updated); + return state; } \ No newline at end of file diff --git a/web/vue/src/store/modules/stratrunners/sync.js b/web/vue/src/store/modules/stratrunners/sync.js index 00ada2187..71e6366d8 100644 --- a/web/vue/src/store/modules/stratrunners/sync.js +++ b/web/vue/src/store/modules/stratrunners/sync.js @@ -5,16 +5,26 @@ import _ from 'lodash' const init = () => { get('gekkos', (err, resp) => { - let watchers = _.filter(resp, {mode: 'leech'}); - store.commit('syncStratrunners', watchers); + let runners = _.filter(resp, {type: 'leech'}); + store.commit('syncStratrunners', runners); }); } const sync = () => { - bus.$on('update', data => { - if(data.gekko_mode === 'leech') - store.commit('updateStratrunner', data); + + bus.$on('new_gekko', data => { + if(data.gekko.type === 'leech') + store.commit('addStratrunner', data.gekko); }); + + const update = (data) => { + store.commit('updateStratrunner', data); + } + + bus.$on('update', update); + bus.$on('startAt', update); + bus.$on('lastCandle', update); + bus.$on('firstCandle', update); } export default function() { diff --git a/web/vue/src/store/modules/watchers/sync.js b/web/vue/src/store/modules/watchers/sync.js index 28fa9dcf8..f29506e83 100644 --- a/web/vue/src/store/modules/watchers/sync.js +++ b/web/vue/src/store/modules/watchers/sync.js @@ -5,7 +5,7 @@ import _ from 'lodash' const init = () => { get('gekkos', (err, resp) => { - let watchers = _.filter(resp, {mode: 'realtime'}); + let watchers = _.filter(resp, {type: 'watcher'}); store.commit('syncWatchers', watchers); }); } @@ -13,14 +13,12 @@ const init = () => { const sync = () => { bus.$on('new_gekko', data => { - if(data.gekko.mode === 'realtime') + if(data.gekko.type === 'watcher') store.commit('addWatcher', data.gekko); }); - const update = (data) => { - if(data.gekko_mode === 'realtime') - store.commit('updateWatcher', data); + store.commit('updateWatcher', data); } bus.$on('update', update); From 92534c06a040a221c97c9726476af990b9721197 Mon Sep 17 00:00:00 2001 From: Mike van Rossum Date: Fri, 9 Dec 2016 13:55:18 +0000 Subject: [PATCH 23/25] fix if check.. (need more coffee) --- plugins/profitSimulator.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/profitSimulator.js b/plugins/profitSimulator.js index 23739fb08..ff8d2c92a 100644 --- a/plugins/profitSimulator.js +++ b/plugins/profitSimulator.js @@ -127,7 +127,7 @@ Logger.prototype.jsonReport = function(advice) { var price = advice.candle.close; var at = advice.candle.start; - if(what !== 'short' || what !== 'long') + if(what !== 'short' && what !== 'long') return; var payload; @@ -145,7 +145,7 @@ Logger.prototype.jsonReport = function(advice) { date: at, balance: this.current.asset * price } - + cp.trade(payload); } From ee491a93c00ecd90484d6d679b7e531e609dbe3c Mon Sep 17 00:00:00 2001 From: Mike van Rossum Date: Fri, 9 Dec 2016 18:12:52 +0000 Subject: [PATCH 24/25] add killGekko api endpoint --- core/markets/leech.js | 7 +++++++ web/routes/killGekko.js | 42 ++++++++++++++++++++++++++++++++++++++++ web/server.js | 1 + web/state/listManager.js | 7 ++++++- 4 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 web/routes/killGekko.js diff --git a/core/markets/leech.js b/core/markets/leech.js index fc90ab32f..fcb092aeb 100644 --- a/core/markets/leech.js +++ b/core/markets/leech.js @@ -78,6 +78,13 @@ Market.prototype.processCandles = function(err, 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); diff --git a/web/routes/killGekko.js b/web/routes/killGekko.js new file mode 100644 index 000000000..705ae6ba9 --- /dev/null +++ b/web/routes/killGekko.js @@ -0,0 +1,42 @@ +const _ = require('lodash'); +const promisify = require('tiny-promisify'); +const moment = require('moment'); + +const pipelineRunner = promisify(require('../../core/workers/pipeline/parent')); +const cache = require('../state/cache'); +const broadcast = cache.get('broadcast'); +const gekkoManager = cache.get('gekkos'); + +const base = require('./baseConfig'); + +// starts an import +// requires a post body with a config object +module.exports = function *() { + + let id = this.request.body.id; + + if(!id) { + this.body = { + status: 'not ok' + } + return; + } + + let deleted = gekkoManager.delete(id); + + if(!deleted){ + this.body = { + status: 'not ok' + } + return; + } + + broadcast({ + type: 'gekko_killed', + gekko_id: id + }); + + this.body = { + status: 'ok' + }; +} \ No newline at end of file diff --git a/web/server.js b/web/server.js index 11c83f476..6b06d1850 100644 --- a/web/server.js +++ b/web/server.js @@ -52,6 +52,7 @@ router.post('/api/scansets', require(ROUTE('scanDatasets'))); router.post('/api/backtest', require(ROUTE('backtest'))); router.post('/api/import', require(ROUTE('import'))); router.post('/api/startGekko', require(ROUTE('startGekko'))); +router.post('/api/killGekko', require(ROUTE('killGekko'))); router.post('/api/getCandles', require(ROUTE('getCandles'))); // incoming WS: diff --git a/web/state/listManager.js b/web/state/listManager.js index a3f0a3303..6877d1077 100644 --- a/web/state/listManager.js +++ b/web/state/listManager.js @@ -29,8 +29,13 @@ ListManager.prototype.update = function(id, updates) { // delete an item from the list ListManager.prototype.delete = function(id) { + let wasThere = this._list.find(i => i.id === id); this._list = this._list.filter(i => i.id !== id); - return true; + + if(wasThere) + return true; + else + return false; } // getter From c4edda479c49ca3e08286f62dbbed097b7846ca0 Mon Sep 17 00:00:00 2001 From: Mike van Rossum Date: Fri, 9 Dec 2016 21:24:35 +0000 Subject: [PATCH 25/25] watch for killed gekko --- web/routes/startGekko.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/web/routes/startGekko.js b/web/routes/startGekko.js index bd50478b4..7c7f3ffee 100644 --- a/web/routes/startGekko.js +++ b/web/routes/startGekko.js @@ -39,10 +39,16 @@ module.exports = function *() { if(errored) return; + let deleted = gekkoManager.delete(id); + + if(!deleted) + // it was already deleted + return; + + errored = true; console.error('RECEIVED ERROR IN GEKKO', id); console.error(err); - gekkoManager.delete(id); return broadcast({ type: 'gekko_error', gekko_id: id,