├── .dockerignore ├── .editorconfig ├── .env ├── .github ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .prettierrc ├── .travis.yml ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── README.md ├── appveyor.yml ├── bin ├── gekko_launch.sh ├── gekko_launch_screen.sh ├── gekko_log_grab.sh └── gekko_screen_grab.sh ├── config ├── adapters │ ├── mongodb.toml │ ├── postgresql.toml │ └── sqlite.toml ├── backtest.toml ├── general.toml ├── plugins │ ├── candleWriter.toml │ ├── paperTrader.toml │ ├── performanceAnalyzer.toml │ ├── pushbullet.toml │ ├── trader.toml │ └── tradingAdvisor.toml └── strategies │ ├── CCI.toml │ ├── DEMA.toml │ ├── MACD.toml │ ├── PPO.toml │ ├── RSI.toml │ ├── StochRSI.toml │ ├── TSI.toml │ ├── UO.toml │ ├── custom.toml │ ├── talib-macd.toml │ ├── tulip-adx.toml │ ├── tulip-macd.toml │ ├── tulip-multi-strat.toml │ └── varPPO.toml ├── core ├── budfox │ ├── budfox.js │ ├── candleCreator.js │ ├── candleManager.js │ ├── heart.js │ ├── marketDataProvider.js │ ├── marketFetcher.js │ └── tradeBatcher.js ├── candleBatcher.js ├── cp.js ├── error.js ├── eventLogger.js ├── exchangeChecker.js ├── gekkoStream.js ├── log.js ├── markets │ ├── backtest.js │ ├── importer.js │ ├── leech.js │ └── realtime.js ├── pipeline.js ├── pluginUtil.js ├── prepareDateRange.js ├── stats.js ├── talib.js ├── tools │ ├── candleLoader.js │ ├── configBuilder.js │ ├── dataStitcher.js │ └── dateRangeScanner.js ├── tulind.js ├── util.js └── workers │ ├── datasetScan │ └── parent.js │ ├── dateRangeScan │ ├── child.js │ └── parent.js │ ├── loadCandles │ ├── child.js │ └── parent.js │ └── pipeline │ ├── child.js │ ├── messageHandlers │ ├── backtestHandler.js │ ├── importerHandler.js │ └── realtimeHandler.js │ └── parent.js ├── docker-compose.yml ├── docker-entrypoint.sh ├── docs ├── commandline │ ├── Importing.md │ ├── about_the_commandline.md │ ├── backtesting.md │ ├── plugins.md │ └── tradebot.md ├── extending │ ├── add_a_plugin.md │ ├── add_an_exchange.md │ └── other_software.md ├── features │ ├── backtesting.md │ └── importing.md ├── installation │ ├── installing_gekko.md │ ├── installing_gekko_on_a_server.md │ ├── installing_gekko_on_windows.md │ ├── installing_gekko_on_windows_with_bash_on_windows_10.md │ └── installing_gekko_using_docker.md ├── internals │ ├── architecture.md │ ├── budfox.md │ └── gekko_ui.md ├── introduction │ ├── about_gekko.md │ ├── roadmap.md │ ├── scope.md │ ├── supported_exchanges.md │ └── supporting_the_project.md └── strategies │ ├── creating_a_strategy.md │ ├── example_strategies.md │ ├── gekko_indicators.md │ ├── talib_indicators.md │ └── tulip_indicators.md ├── exchanges ├── binance.js ├── bitcoin-co-id.js ├── bitfinex.js ├── bitstamp.js ├── bittrex.js ├── bitx.js ├── btc-markets.js ├── btcc.js ├── bx.in.th.js ├── cexio.js ├── gdax.js ├── gemini.js ├── kraken.js ├── lakebtc.js ├── mexbt.js ├── mtgox.js ├── okcoin.js ├── poloniex.js ├── quadriga.js ├── wex.nz.js └── zaif.jp.js ├── gekko.js ├── importers └── exchanges │ ├── binance.js │ ├── bitfinex.js │ ├── btcc.js │ ├── gdax.js │ ├── kraken.js │ └── poloniex.js ├── logs └── .gitignore ├── package-lock.json ├── package.json ├── plugins.js ├── plugins ├── adviceLogger.js ├── campfire.js ├── ifttt.js ├── ircbot.js ├── mailer.js ├── mongodb │ ├── handle.js │ ├── reader.js │ ├── scanner.js │ ├── util.js │ └── writer.js ├── paperTrader │ └── paperTrader.js ├── performanceAnalyzer │ ├── cpRelay.js │ ├── logger.js │ └── performanceAnalyzer.js ├── postgresql │ ├── handle.js │ ├── reader.js │ ├── scanner.js │ ├── util.js │ └── writer.js ├── pushbullet.js ├── pushover.js ├── redisBeacon.js ├── slack.js ├── sqlite │ ├── handle.js │ ├── reader.js │ ├── scanner.js │ ├── util.js │ └── writer.js ├── telegrambot.js ├── trader │ ├── portfolioManager.js │ └── trader.js ├── tradingAdvisor │ ├── baseTradingMethod.js │ └── tradingAdvisor.js ├── twitter.js ├── webserver.js └── xmppbot.js ├── sample-config.js ├── strategies ├── CCI.js ├── DEMA.js ├── MACD.js ├── PPO.js ├── RSI.js ├── StochRSI.js ├── TSI.js ├── UO.js ├── custom.js ├── debug-advice.js ├── indicators │ ├── CCI.js │ ├── DEMA.js │ ├── EMA.js │ ├── LRC.js │ ├── MACD.js │ ├── PPO.js │ ├── RSI.js │ ├── SMA.js │ ├── SMMA.js │ ├── TSI.js │ └── UO.js ├── noop.js ├── talib-macd.js ├── tulip-adx.js ├── tulip-macd.js ├── tulip-multi-strat.js └── varPPO.js ├── subscriptions.js ├── techstack.md ├── techstack.yml ├── test ├── _prepare.js ├── candleBatcher.js ├── exchanges │ ├── bitstamp.js │ └── data │ │ └── bitstamp_trades.json ├── indicators │ ├── dema.js │ ├── ema.js │ ├── macd.js │ ├── ppo.js │ ├── rsi.js │ ├── sma.js │ └── smma.js ├── marketFetcher.js ├── plugins │ └── portfolioManager.js ├── test-config.json └── tradeBatcher.js └── web ├── apiKeyManager.js ├── isWindows.js ├── routes ├── apiKeys.js ├── backtest.js ├── baseConfig.js ├── configPart.js ├── exchanges.js ├── getCandles.js ├── import.js ├── killGekko.js ├── list.js ├── scanDatasets.js ├── scanDateRange.js ├── startGekko.js └── strategies.js ├── server.js ├── state ├── cache.js ├── listManager.js └── logger.js └── vue ├── .babelrc ├── README.md ├── UIconfig.js ├── assets ├── d3.js ├── furtive.min.css ├── gekko.jpg ├── humanize-duration.js ├── moment.js ├── reconnecting-websocket.min.js ├── select-arrow.png └── toml.js ├── dist └── build.js ├── index.html ├── package.json ├── src ├── App.vue ├── components │ ├── backtester │ │ ├── backtestConfigBuilder.vue │ │ ├── backtester.vue │ │ └── result │ │ │ ├── chartWrapper.vue │ │ │ ├── result.vue │ │ │ ├── roundtripTable.vue │ │ │ └── summary.vue │ ├── config │ │ ├── apiConfigBuilder.vue │ │ └── config.vue │ ├── data │ │ ├── data.vue │ │ └── import │ │ │ ├── importConfigBuilder.vue │ │ │ ├── importer.vue │ │ │ └── single.vue │ ├── gekko │ │ ├── gekkoConfigBuilder.vue │ │ ├── list.vue │ │ ├── new.vue │ │ ├── singleStratrunner.vue │ │ └── singleWatcher.vue │ ├── global │ │ ├── blockSpinner.vue │ │ ├── configbuilder │ │ │ ├── datasetpicker.vue │ │ │ ├── exchangepicker.vue │ │ │ ├── marketpicker.vue │ │ │ ├── papertrader.vue │ │ │ ├── rangecreator.vue │ │ │ ├── rangepicker.vue │ │ │ ├── stratpicker.vue │ │ │ └── typepicker.vue │ │ ├── mixins │ │ │ └── dataset.js │ │ ├── paperTradeSummary.vue │ │ ├── progressBar.vue │ │ └── ws.js │ └── layout │ │ ├── footer.vue │ │ ├── header.vue │ │ ├── home.vue │ │ └── modal.vue ├── d3 │ ├── chart3.js │ ├── chart4.js │ └── message.js ├── main.js ├── store │ ├── index.js │ ├── init.js │ └── modules │ │ ├── config │ │ ├── mutations.js │ │ └── sync.js │ │ ├── imports │ │ ├── mutations.js │ │ └── sync.js │ │ ├── messages │ │ └── mutations.js │ │ ├── notifications │ │ ├── mutations.js │ │ └── sync.js │ │ ├── stratrunners │ │ ├── mutations.js │ │ └── sync.js │ │ └── watchers │ │ ├── mutations.js │ │ └── sync.js └── tools │ ├── ajax.js │ ├── api.js │ └── marked.js └── webpack.config.js /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .travis.yml 3 | volumes 4 | config.js 5 | sample-config.js 6 | .git 7 | .gitignore -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | HOST=localhost 2 | PORT=3000 -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | **Note: for support questions, please join our [Discord server](https://discord.gg/26wMygt)** 2 | 3 | * **I'm submitting a ...** 4 | [ ] bug report 5 | [ ] feature request 6 | [ ] question about the decisions made in the repository 7 | 8 | * **Action taken** (what you did) 9 | 10 | 11 | * **Expected result** (what you hoped would happen) 12 | 13 | 14 | * **Actual result** (unexpected outcome) 15 | 16 | 17 | * **Other information** (e.g. detailed explanation, stacktraces, related issues, suggestions how to fix, links for us to have context, eg. stackoverflow, etc) 18 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | * **What kind of change does this PR introduce?** (Bug fix, feature, docs update, ...) 2 | 3 | 4 | 5 | * **What is the current behavior?** (You can also link to an open issue here) 6 | 7 | 8 | 9 | * **What is the new behavior (if this is a feature change)?** 10 | 11 | 12 | 13 | * **Other information**: 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Numerous always-ignore extensions 2 | *.diff 3 | *.err 4 | *.orig 5 | *.log 6 | *.rej 7 | *.swo 8 | *.swp 9 | *.vi 10 | *~ 11 | *.sass-cache 12 | 13 | # OS or Editor folders 14 | .DS_Store 15 | Thumbs.db 16 | config.js 17 | api-keys.js 18 | .cache 19 | .project 20 | .settings 21 | .tmproj 22 | nbproject 23 | *.sublime-project 24 | *.sublime-workspace 25 | 26 | # Dreamweaver added files 27 | _notes 28 | dwsync.xml 29 | 30 | # Komodo 31 | *.komodoproject 32 | .komodotools 33 | 34 | # Espresso 35 | *.esproj 36 | *.espressostorage 37 | 38 | # Folders & files to ignore 39 | 40 | node_modules 41 | candles.csv 42 | cexio.db 43 | history 44 | TMP_* 45 | .idea 46 | volumes 47 | SECRET-api-keys.json -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "singleQuote": true 4 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "8.0.0" -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:8 2 | 3 | ENV HOST localhost 4 | ENV PORT 3000 5 | 6 | # Create app directory 7 | RUN mkdir -p /usr/src/app 8 | WORKDIR /usr/src/app 9 | 10 | # Install GYP dependencies globally, will be used to code build other dependencies 11 | RUN npm install -g --production node-gyp && \ 12 | npm cache clean --force 13 | 14 | # Install app dependencies 15 | COPY package.json /usr/src/app 16 | RUN npm install --production && \ 17 | npm install --production redis@0.10.0 talib@1.0.2 tulind@0.8.7 pg && \ 18 | npm cache clean --force 19 | 20 | # Bundle app source 21 | COPY . /usr/src/app 22 | 23 | EXPOSE 3000 24 | RUN chmod +x /usr/src/app/docker-entrypoint.sh 25 | ENTRYPOINT ["/usr/src/app/docker-entrypoint.sh"] 26 | 27 | 28 | CMD [ "npm", "start" ] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014-2017 Mike van Rossum mike@mvr.me 4 | 5 | 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: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | 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. 10 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | init: 2 | - git config --global core.autocrlf input 3 | 4 | environment: 5 | nodejs_version: "6" 6 | 7 | install: 8 | - ps: Install-Product node $env:nodejs_version 9 | - npm install 10 | test_script: 11 | - node --version 12 | - npm --version 13 | - cmd: npm test 14 | 15 | build: off -------------------------------------------------------------------------------- /bin/gekko_launch.sh: -------------------------------------------------------------------------------- 1 | #navigate to gekko folder, change this if your location is different 2 | cd ~/gekko 3 | 4 | # zip previous log files for this config 5 | now="`date +%Y%m%d%H%M%S`" 6 | current_log=log/gekko_log.$1.txt 7 | archive_log=log/gekko_log.$1.$now.txt 8 | 9 | if [ -f $current_log ]; then 10 | mv log/gekko_log.$1.txt $archive_log 11 | zip -vu log/gekko_logs.zip $archive_log 12 | 13 | # remove raw text files now that they're zipped 14 | rm $archive_log 15 | fi 16 | 17 | # finally launch gekko and log output to log file as well as stdout 18 | node gekko config="config/user/$1.js" 2>&1 | tee $current_log 19 | -------------------------------------------------------------------------------- /bin/gekko_launch_screen.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | screen -dmS gekko_$1 ~/gekko/bin/gekko_launch.sh $1 3 | 4 | -------------------------------------------------------------------------------- /bin/gekko_log_grab.sh: -------------------------------------------------------------------------------- 1 | tail -f ~/gekko/log/gekko_log.$1.txt 2 | -------------------------------------------------------------------------------- /bin/gekko_screen_grab.sh: -------------------------------------------------------------------------------- 1 | screen -dR gekko_$1 2 | -------------------------------------------------------------------------------- /config/adapters/mongodb.toml: -------------------------------------------------------------------------------- 1 | connectionString = "mongodb://mongodb/gekko" 2 | path = "plugins/mongodb" 3 | version = 0.1 4 | 5 | [[dependencies]] 6 | module = "mongojs" 7 | version = "2.4.0" -------------------------------------------------------------------------------- /config/adapters/postgresql.toml: -------------------------------------------------------------------------------- 1 | connectionString = "postgres://user:pass@localhost:5432" 2 | path = "plugins/postgresql" 3 | version = 0.1 4 | 5 | [[dependencies]] 6 | module = "pg" 7 | version = "6.1.0" -------------------------------------------------------------------------------- /config/adapters/sqlite.toml: -------------------------------------------------------------------------------- 1 | dataDirectory = "history" 2 | path = "plugins/sqlite" 3 | version = 0.1 4 | 5 | [[dependencies]] 6 | module = "sqlite3" 7 | version = "3.1.4" -------------------------------------------------------------------------------- /config/backtest.toml: -------------------------------------------------------------------------------- 1 | batchSize = 50 2 | 3 | # scan for available dateranges 4 | daterange = "scan" 5 | # or specify it like so: 6 | # [daterange] 7 | # from = "2015-01-01" 8 | # to = "2016-01-01" -------------------------------------------------------------------------------- /config/general.toml: -------------------------------------------------------------------------------- 1 | debug = true 2 | 3 | # what database should Gekko use? 4 | adapter = 'sqlite' 5 | 6 | [watch] 7 | exchange = 'Poloniex' 8 | currency = 'USDT' 9 | asset = 'BTC' 10 | 11 | -------------------------------------------------------------------------------- /config/plugins/candleWriter.toml: -------------------------------------------------------------------------------- 1 | enabled = true 2 | adapter = "sqlite" -------------------------------------------------------------------------------- /config/plugins/paperTrader.toml: -------------------------------------------------------------------------------- 1 | feeMaker = 0.25 2 | feeTaker = 0.25 3 | feeUsing = 'maker' 4 | slippage = 0.05 5 | 6 | [simulationBalance] 7 | asset = 1 8 | currency = 100 9 | -------------------------------------------------------------------------------- /config/plugins/performanceAnalyzer.toml: -------------------------------------------------------------------------------- 1 | # Used to calculate sharpe ratio 2 | # yearly % of riskfree return for example 3 | # treasury bonds or bank interest. 4 | 5 | riskFreeReturn = 2 -------------------------------------------------------------------------------- /config/plugins/pushbullet.toml: -------------------------------------------------------------------------------- 1 | enabled = false 2 | 3 | # Send 'Gekko starting' message if true 4 | sendMessageOnStart = true 5 | 6 | # disable advice printout if it's soft 7 | muteSoft = true 8 | 9 | # your email, change it unless you are Azor Ahai 10 | email = "jon_snow@westeros.org" 11 | 12 | # your pushbullet API key 13 | key = "xxx" 14 | 15 | # will make Gekko messages start with [GEKKO] 16 | tag = "[GEKKO]" -------------------------------------------------------------------------------- /config/plugins/trader.toml: -------------------------------------------------------------------------------- 1 | enabled = false 2 | 3 | ## NOTE: once you filled in the following 4 | ## never share this file with anyone! 5 | 6 | key = "" 7 | secret = "" 8 | # this is only required at specific exchanges 9 | username = "" -------------------------------------------------------------------------------- /config/plugins/tradingAdvisor.toml: -------------------------------------------------------------------------------- 1 | enabled = true 2 | 3 | candleSize = 60 4 | historySize = 25 5 | method = "MACD" 6 | 7 | [talib] 8 | enabled = false 9 | version = "1.0.2" -------------------------------------------------------------------------------- /config/strategies/CCI.toml: -------------------------------------------------------------------------------- 1 | # constant multiplier. 0.015 gets to around 70% fit 2 | constant = 0.015 3 | 4 | # history size, make same or smaller than history 5 | history = 90 6 | 7 | [thresholds] 8 | up = 100 9 | down = -100 10 | persistence = 0 -------------------------------------------------------------------------------- /config/strategies/DEMA.toml: -------------------------------------------------------------------------------- 1 | short = 10 2 | long = 21 3 | 4 | [thresholds] 5 | down = -0.025 6 | up = 0.025 -------------------------------------------------------------------------------- /config/strategies/MACD.toml: -------------------------------------------------------------------------------- 1 | short = 10 2 | long = 21 3 | signal = 9 4 | 5 | [thresholds] 6 | down = -0.025 7 | up = 0.025 8 | persistence = 1 -------------------------------------------------------------------------------- /config/strategies/PPO.toml: -------------------------------------------------------------------------------- 1 | short = 12 2 | long = 26 3 | signal = 9 4 | 5 | [thresholds] 6 | down = -0.025 7 | up = 0.025 8 | persistence = 2 -------------------------------------------------------------------------------- /config/strategies/RSI.toml: -------------------------------------------------------------------------------- 1 | interval = 14 2 | 3 | [thresholds] 4 | low = 30 5 | high = 70 6 | persistence = 1 -------------------------------------------------------------------------------- /config/strategies/StochRSI.toml: -------------------------------------------------------------------------------- 1 | interval = 3 2 | 3 | [thresholds] 4 | low = 20 5 | high = 80 6 | persistence = 3 -------------------------------------------------------------------------------- /config/strategies/TSI.toml: -------------------------------------------------------------------------------- 1 | short = 13 2 | long = 25 3 | 4 | [thresholds] 5 | low = -25 6 | high = 25 7 | persistence = 1 -------------------------------------------------------------------------------- /config/strategies/UO.toml: -------------------------------------------------------------------------------- 1 | [first] 2 | weight = 4 3 | period = 7 4 | 5 | [second] 6 | weight = 2 7 | period = 14 8 | 9 | [third] 10 | weight = 1 11 | period = 28 12 | 13 | [thresholds] 14 | low = 30 15 | high = 70 16 | persistence = 1 17 | -------------------------------------------------------------------------------- /config/strategies/custom.toml: -------------------------------------------------------------------------------- 1 | my_custom_setting = 10 -------------------------------------------------------------------------------- /config/strategies/talib-macd.toml: -------------------------------------------------------------------------------- 1 | [parameters] 2 | optInFastPeriod = 10 3 | optInSlowPeriod = 21 4 | optInSignalPeriod = 9 5 | 6 | [thresholds] 7 | down = -0.025 8 | up = 0.025 -------------------------------------------------------------------------------- /config/strategies/tulip-adx.toml: -------------------------------------------------------------------------------- 1 | historySize = 80 2 | optInTimePeriod = 15 3 | candleSize = 10 4 | 5 | [thresholds] 6 | up = 30 7 | down = 20 -------------------------------------------------------------------------------- /config/strategies/tulip-macd.toml: -------------------------------------------------------------------------------- 1 | [parameters] 2 | optInFastPeriod = 10 3 | optInSlowPeriod = 21 4 | optInSignalPeriod = 9 5 | 6 | [thresholds] 7 | down = -0.025 8 | up = 0.025 -------------------------------------------------------------------------------- /config/strategies/tulip-multi-strat.toml: -------------------------------------------------------------------------------- 1 | optInTimePeriod = 2 2 | optInFastPeriod = 4 3 | optInSlowPeriod = 7 4 | optInSignalPeriod = 23.707189677282354 5 | 6 | candleSize = 1 7 | historySize = 4 8 | 9 | up = 24.52 10 | down = 55.74 11 | macd_up = 14.498233170768081 12 | macd_down = -9.65220944122072 13 | -------------------------------------------------------------------------------- /config/strategies/varPPO.toml: -------------------------------------------------------------------------------- 1 | momentum = "TSI" # RSI, TSI or UO 2 | 3 | [thresholds] 4 | weightLow = 120 5 | weightHigh = -120 6 | 7 | # How many candle intervals should a trend persist 8 | # before we consider it real? 9 | persistence = 0 -------------------------------------------------------------------------------- /core/budfox/budfox.js: -------------------------------------------------------------------------------- 1 | // Budfox is the realtime market for Gekko! 2 | // 3 | // Read more here: 4 | // @link https://github.com/askmike/gekko/blob/stable/docs/internals/budfox.md 5 | // 6 | // > [getting up] I don't know. I guess I realized that I'm just Bud Fox. 7 | // > As much as I wanted to be Gordon Gekko, I'll *always* be Bud Fox. 8 | // > [tosses back the handkerchief and walks away] 9 | 10 | var _ = require('lodash'); 11 | var async = require('async'); 12 | 13 | var util = require(__dirname + '/../util'); 14 | var dirs = util.dirs(); 15 | 16 | var Heart = require(dirs.budfox + 'heart'); 17 | var MarketDataProvider = require(dirs.budfox + 'marketDataProvider'); 18 | var CandleManager = require(dirs.budfox + 'candleManager'); 19 | 20 | var BudFox = function(config) { 21 | _.bindAll(this); 22 | 23 | Readable.call(this, {objectMode: true}); 24 | 25 | // BudFox internal modules: 26 | 27 | this.heart = new Heart; 28 | this.marketDataProvider = new MarketDataProvider(config); 29 | this.candleManager = new CandleManager; 30 | 31 | // BudFox data flow: 32 | 33 | // on every `tick` retrieve trade data 34 | this.heart.on( 35 | 'tick', 36 | this.marketDataProvider.retrieve 37 | ); 38 | 39 | // on new trade data create candles 40 | this.marketDataProvider.on( 41 | 'trades', 42 | this.candleManager.processTrades 43 | ); 44 | 45 | // Output the candles 46 | this.candleManager.on( 47 | 'candles', 48 | this.pushCandles 49 | ); 50 | 51 | this.heart.pump(); 52 | 53 | // Budfox also reports: 54 | 55 | // Trades & last trade 56 | // 57 | // this.marketDataProvider.on( 58 | // 'trades', 59 | // this.broadcast('trades') 60 | // ); 61 | // this.marketDataProvider.on( 62 | // 'trades', 63 | // this.broadcastTrade 64 | // ); 65 | } 66 | 67 | var Readable = require('stream').Readable; 68 | 69 | BudFox.prototype = Object.create(Readable.prototype, { 70 | constructor: { value: BudFox } 71 | }); 72 | 73 | BudFox.prototype._read = function noop() {} 74 | 75 | BudFox.prototype.pushCandles = function(candles) { 76 | _.each(candles, this.push); 77 | } 78 | 79 | // BudFox.prototype.broadcastTrade = function(trades) { 80 | // _.defer(function() { 81 | // this.emit('trade', trades.last); 82 | // }.bind(this)); 83 | // } 84 | 85 | // BudFox.prototype.broadcast = function(message) { 86 | // return function(payload) { 87 | // _.defer(function() { 88 | // this.emit(message, payload); 89 | // }.bind(this)); 90 | // }.bind(this); 91 | // } 92 | 93 | module.exports = BudFox; 94 | -------------------------------------------------------------------------------- /core/budfox/candleManager.js: -------------------------------------------------------------------------------- 1 | // The candleManager consumes trades and emits: 2 | // - `candles`: array of minutly candles. 3 | // - `candle`: the most recent candle after a fetch Gekko. 4 | 5 | var _ = require('lodash'); 6 | var moment = require('moment'); 7 | var fs = require('fs'); 8 | 9 | var util = require(__dirname + '/../util'); 10 | var dirs = util.dirs(); 11 | var config = util.getConfig(); 12 | var log = require(dirs.core + 'log'); 13 | var cp = require(dirs.core + 'cp'); 14 | 15 | var CandleCreator = require(dirs.budfox + 'candleCreator'); 16 | 17 | var Manager = function() { 18 | _.bindAll(this); 19 | 20 | this.candleCreator = new CandleCreator; 21 | 22 | this.candleCreator 23 | .on('candles', this.relayCandles); 24 | 25 | this.messageFirstCandle = _.once(candle => { 26 | cp.firstCandle(candle); 27 | }) 28 | }; 29 | 30 | util.makeEventEmitter(Manager); 31 | Manager.prototype.processTrades = function(tradeBatch) { 32 | this.candleCreator.write(tradeBatch); 33 | } 34 | 35 | Manager.prototype.relayCandles = function(candles) { 36 | this.emit('candles', candles); 37 | 38 | if(!_.size(candles)) 39 | return; 40 | 41 | this.messageFirstCandle(_.first(candles)); 42 | cp.lastCandle(_.last(candles)); 43 | } 44 | 45 | module.exports = Manager; 46 | -------------------------------------------------------------------------------- /core/budfox/heart.js: -------------------------------------------------------------------------------- 1 | // The heart schedules and emit ticks every 20 seconds. 2 | 3 | var util = require(__dirname + '/../util'); 4 | var log = require(util.dirs().core + 'log'); 5 | 6 | var _ = require('lodash'); 7 | var moment = require('moment'); 8 | 9 | if (util.getConfig().watch.tickrate) 10 | var TICKRATE = util.getConfig().watch.tickrate; 11 | else if(util.getConfig().watch.exchange === 'okcoin') 12 | var TICKRATE = 2; 13 | else 14 | var TICKRATE = 20; 15 | 16 | var Heart = function() { 17 | this.lastTick = false; 18 | 19 | _.bindAll(this); 20 | } 21 | 22 | util.makeEventEmitter(Heart); 23 | 24 | Heart.prototype.pump = function() { 25 | log.debug('scheduling ticks'); 26 | this.scheduleTicks(); 27 | } 28 | 29 | Heart.prototype.tick = function() { 30 | if(this.lastTick) { 31 | // make sure the last tick happened not to lang ago 32 | // @link https://github.com/askmike/gekko/issues/514 33 | if(this.lastTick < moment().unix() - TICKRATE * 3) 34 | util.die('Failed to tick in time, see https://github.com/askmike/gekko/issues/514 for details', true); 35 | } 36 | 37 | this.lastTick = moment().unix(); 38 | this.emit('tick'); 39 | } 40 | 41 | Heart.prototype.scheduleTicks = function() { 42 | setInterval( 43 | this.tick, 44 | +moment.duration(TICKRATE, 's') 45 | ); 46 | 47 | // start! 48 | _.defer(this.tick); 49 | } 50 | 51 | module.exports = Heart; 52 | -------------------------------------------------------------------------------- /core/budfox/marketDataProvider.js: -------------------------------------------------------------------------------- 1 | // 2 | // The market data provider will fetch data from a datasource on tick. It emits: 3 | // 4 | // - `trades`: batch of newly detected trades 5 | // - `trade`: after Gekko fetched new trades, this 6 | // will be the most recent one. 7 | 8 | const _ = require('lodash'); 9 | const util = require(__dirname + '/../util'); 10 | 11 | const MarketFetcher = require('./marketFetcher'); 12 | const dirs = util.dirs(); 13 | const cp = require(dirs.core + 'cp'); 14 | 15 | const Manager = function(config) { 16 | 17 | _.bindAll(this); 18 | 19 | // fetch trades 20 | this.source = new MarketFetcher(config); 21 | 22 | // relay newly fetched trades 23 | this.source 24 | .on('trades batch', this.relayTrades); 25 | } 26 | 27 | util.makeEventEmitter(Manager); 28 | 29 | // HANDLERS 30 | Manager.prototype.retrieve = function() { 31 | this.source.fetch(); 32 | } 33 | 34 | 35 | Manager.prototype.relayTrades = function(batch) { 36 | this.emit('trades', batch); 37 | 38 | this.sendStartAt(batch); 39 | cp.update(batch.last.date.format()); 40 | } 41 | 42 | Manager.prototype.sendStartAt = _.once(function(batch) { 43 | cp.startAt(batch.first.date.format()) 44 | }); 45 | 46 | module.exports = Manager; -------------------------------------------------------------------------------- /core/candleBatcher.js: -------------------------------------------------------------------------------- 1 | // internally we only use 1m 2 | // candles, this can easily 3 | // convert them to any desired 4 | // size. 5 | 6 | // Acts as ~fake~ stream: takes 7 | // 1m candles as input and emits 8 | // bigger candles. 9 | // 10 | // input are transported candles. 11 | 12 | var _ = require('lodash'); 13 | var util = require(__dirname + '/util'); 14 | 15 | var CandleBatcher = function(candleSize) { 16 | if(!_.isNumber(candleSize)) 17 | throw 'candleSize is not a number'; 18 | 19 | this.candleSize = candleSize; 20 | this.smallCandles = []; 21 | 22 | _.bindAll(this); 23 | } 24 | 25 | util.makeEventEmitter(CandleBatcher); 26 | 27 | CandleBatcher.prototype.write = function(candles) { 28 | if(!_.isArray(candles)) 29 | throw 'candles is not an array'; 30 | 31 | _.each(candles, function(candle) { 32 | this.smallCandles.push(candle); 33 | this.check(); 34 | }, this); 35 | } 36 | 37 | CandleBatcher.prototype.check = function() { 38 | if(_.size(this.smallCandles) % this.candleSize !== 0) 39 | return; 40 | 41 | this.emit('candle', this.calculate()); 42 | this.smallCandles = []; 43 | } 44 | 45 | CandleBatcher.prototype.calculate = function() { 46 | var first = this.smallCandles.shift(); 47 | 48 | first.vwp = first.vwp * first.volume; 49 | 50 | var candle = _.reduce( 51 | this.smallCandles, 52 | function(candle, m) { 53 | candle.high = _.max([candle.high, m.high]); 54 | candle.low = _.min([candle.low, m.low]); 55 | candle.close = m.close; 56 | candle.volume += m.volume; 57 | candle.vwp += m.vwp * m.volume; 58 | candle.trades += m.trades; 59 | return candle; 60 | }, 61 | first 62 | ); 63 | 64 | if(candle.volume) 65 | // we have added up all prices (relative to volume) 66 | // now divide by volume to get the Volume Weighted Price 67 | candle.vwp /= candle.volume; 68 | else 69 | // empty candle 70 | candle.vwp = candle.open; 71 | 72 | candle.start = first.start; 73 | return candle; 74 | } 75 | 76 | module.exports = CandleBatcher; 77 | -------------------------------------------------------------------------------- /core/cp.js: -------------------------------------------------------------------------------- 1 | // functions that emit data to the parent process. 2 | // 3 | // noops if this gekko instance is not a child process! 4 | 5 | var _ = require('lodash'); 6 | var util = require('./util'); 7 | var config = util.getConfig(); 8 | var dirs = util.dirs(); 9 | var moment = require('moment'); 10 | 11 | var ENV = util.gekkoEnv(); 12 | 13 | var message = (type, payload) => { 14 | payload.type = type; 15 | process.send(payload); 16 | } 17 | 18 | var cp = { 19 | // string like: '2016-12-03T22:23:00.000Z' 20 | update: latest => message('update', { latest }), 21 | startAt: startAt => message('startAt', { startAt }), 22 | 23 | // object like: 24 | // 25 | // { 26 | // start: '2016-12-03T22:23:00.000Z', 27 | // open: 765, 28 | // high: 765, 29 | // low: 765, 30 | // close: 765, 31 | // vwp: 765, 32 | // volume: 0, 33 | // trades: 0 34 | // } 35 | lastCandle: lastCandle => message('lastCandle', { lastCandle }), 36 | firstCandle: firstCandle => message('firstCandle', { firstCandle }), 37 | 38 | // object like: 39 | // 40 | // { 41 | // action: 'buy', 42 | // price: 942.80838846, 43 | // portfolio: { asset: 1.07839516, currency: 0, balance: false }, 44 | // balance: 1016.7200029226638, 45 | // date: 46 | // } 47 | trade: trade => message('trade', { trade }), 48 | // object like: 49 | // { 50 | // currency: 'USDT', 51 | // asset: 'BTC', 52 | // startTime: '2017-03-25 19:41:00', 53 | // endTime: '2017-03-25 20:01:00', 54 | // timespan: '20 minutes', 55 | // market: -0.316304880517734, 56 | // balance: 1016.7200029226638, 57 | // profit: -26.789997197336106, 58 | // relativeProfit: -2.5672966425099304, 59 | // yearlyProfit: '-704041.12634599', 60 | // relativeYearlyProfit: '-67468.55576516', 61 | // startPrice: 945.80000002, 62 | // endPrice: 942.80838846, 63 | // trades: 10, 64 | // startBalance: 1043.5100001199999, 65 | // sharpe: -2.676305165560598 66 | // } 67 | report: report => message('report', { report }), 68 | 69 | // object like: 70 | // { 71 | // entryAt: Moment<'2017-03-25 19:41:00'>, 72 | // entryPrice: 10.21315498, 73 | // entryBalance: 98.19707799420277, 74 | // exitAt: Moment<'2017-03-25 19:41:00'> 75 | // exitPrice: 10.22011632, 76 | // exitBalance: 97.9692176, 77 | // duration: 3600000, 78 | // pnl: -0.2278603942027786, 79 | // profit: -0.2320439659276161, 80 | // } 81 | roundtrip: roundtrip => message('roundtrip', { roundtrip }), 82 | } 83 | 84 | if(ENV !== 'child-process') { 85 | _.each(cp, (val, key) => { 86 | cp[key] = _.noop; 87 | }); 88 | } 89 | 90 | module.exports = cp; -------------------------------------------------------------------------------- /core/error.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | 3 | let RetryError = function(message) { 4 | _.bindAll(this); 5 | 6 | this.name = "RetryError"; 7 | this.message = message; 8 | } 9 | 10 | RetryError.prototype = new Error(); 11 | 12 | let AbortError = function(message) { 13 | _.bindAll(this); 14 | 15 | this.name = "AbortError"; 16 | this.message = message; 17 | } 18 | 19 | AbortError.prototype = new Error(); 20 | 21 | module.exports = { 22 | 'RetryError': RetryError, 23 | 'AbortError': AbortError 24 | }; 25 | 26 | -------------------------------------------------------------------------------- /core/eventLogger.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | 3 | var util = require('./util'); 4 | var dirs = util.dirs(); 5 | var log = require(dirs.core + 'log'); 6 | 7 | var EventLogger = function() { 8 | _.bindAll(this); 9 | } 10 | 11 | var subscriptions = require(dirs.core + 'subscriptions'); 12 | _.each(subscriptions, function(subscription) { 13 | EventLogger.prototype[subscription.handler] = function(e) { 14 | if(subscription.event === 'tick') 15 | log.empty(); 16 | 17 | if(_.has(e, 'data')) 18 | log.debug( 19 | '\tnew event:', 20 | subscription.event, 21 | '(' + _.size(e.data), 22 | 'items)' 23 | ); 24 | else 25 | log.debug( 26 | '\tnew event:', 27 | subscription.event 28 | ); 29 | } 30 | }); 31 | 32 | module.exports = EventLogger; 33 | -------------------------------------------------------------------------------- /core/gekkoStream.js: -------------------------------------------------------------------------------- 1 | // Small writable stream wrapper that 2 | // passes data to all `candleConsumers`. 3 | 4 | var Writable = require('stream').Writable; 5 | var _ = require('lodash'); 6 | var async = require('async'); 7 | 8 | var util = require('./util'); 9 | var env = util.gekkoEnv(); 10 | var mode = util.gekkoMode(); 11 | 12 | var Gekko = function(candleConsumers) { 13 | this.candleConsumers = candleConsumers; 14 | Writable.call(this, {objectMode: true}); 15 | 16 | this.finalize = _.bind(this.finalize, this); 17 | } 18 | 19 | Gekko.prototype = Object.create(Writable.prototype, { 20 | constructor: { value: Gekko } 21 | }); 22 | 23 | Gekko.prototype._write = function(chunk, encoding, _done) { 24 | var done = _.after(this.candleConsumers.length, _done); 25 | _.each(this.candleConsumers, function(c) { 26 | c.processCandle(chunk, done); 27 | }); 28 | } 29 | 30 | Gekko.prototype.finalize = function() { 31 | var tradingMethod = _.find( 32 | this.candleConsumers, 33 | c => c.meta.name === 'Trading Advisor' 34 | ); 35 | 36 | if(!tradingMethod) 37 | return this.shutdown(); 38 | 39 | tradingMethod.finish(this.shutdown.bind(this)); 40 | } 41 | 42 | Gekko.prototype.shutdown = function() { 43 | async.eachSeries( 44 | this.candleConsumers, 45 | function(c, callback) { 46 | if (c.finalize) c.finalize(callback); 47 | else callback(); 48 | }, 49 | function() { 50 | // If we are a child process, we signal to the parent to kill the child once it is done 51 | // so that is has time to process all remaining events (and send report data) 52 | if (env === 'child-process') process.send('done'); 53 | } 54 | ); 55 | }; 56 | 57 | module.exports = Gekko; -------------------------------------------------------------------------------- /core/log.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Lightweight logger, print everything that is send to error, warn 4 | and messages to stdout (the terminal). If config.debug is set in config 5 | also print out everything send to debug. 6 | 7 | */ 8 | 9 | var moment = require('moment'); 10 | var fmt = require('util').format; 11 | var _ = require('lodash'); 12 | var util = require('./util'); 13 | var config = util.getConfig(); 14 | var debug = config.debug; 15 | var silent = config.silent; 16 | 17 | var sendToParent = function() { 18 | var send = method => (...args) => { 19 | process.send({'log': args.join(' ')}); 20 | } 21 | 22 | return { 23 | error: send('error'), 24 | warn: send('warn'), 25 | info: send('info'), 26 | write: send('write') 27 | } 28 | } 29 | 30 | var Log = function() { 31 | _.bindAll(this); 32 | this.env = util.gekkoEnv(); 33 | 34 | if(this.env === 'standalone') 35 | this.output = console; 36 | else if(this.env === 'child-process') 37 | this.output = sendToParent(); 38 | }; 39 | 40 | Log.prototype = { 41 | _write: function(method, args, name) { 42 | if(!name) 43 | name = method.toUpperCase(); 44 | 45 | var message = moment().format('YYYY-MM-DD HH:mm:ss'); 46 | message += ' (' + name + '):\t'; 47 | message += fmt.apply(null, args); 48 | 49 | this.output[method](message); 50 | }, 51 | error: function() { 52 | this._write('error', arguments); 53 | }, 54 | warn: function() { 55 | this._write('warn', arguments); 56 | }, 57 | info: function() { 58 | this._write('info', arguments); 59 | }, 60 | write: function() { 61 | var args = _.toArray(arguments); 62 | var message = fmt.apply(null, args); 63 | this.output.info(message); 64 | } 65 | } 66 | 67 | if(debug) 68 | Log.prototype.debug = function() { 69 | this._write('info', arguments, 'DEBUG'); 70 | } 71 | else 72 | Log.prototype.debug = _.noop; 73 | 74 | if(silent) { 75 | Log.prototype.debug = _.noop; 76 | Log.prototype.info = _.noop; 77 | Log.prototype.warn = _.noop; 78 | Log.prototype.error = _.noop; 79 | Log.prototype.write = _.noop; 80 | } 81 | 82 | module.exports = new Log; -------------------------------------------------------------------------------- /core/markets/realtime.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | 3 | var util = require('../util'); 4 | var dirs = util.dirs(); 5 | 6 | var exchangeChecker = require(dirs.core + 'exchangeChecker'); 7 | var config = util.getConfig(); 8 | 9 | const slug = config.watch.exchange.toLowerCase(); 10 | const exchange = exchangeChecker.getExchangeCapabilities(slug); 11 | 12 | if(!exchange) 13 | util.die(`Unsupported exchange: ${slug}`) 14 | 15 | var error = exchangeChecker.cantMonitor(config.watch); 16 | if(error) 17 | util.die(error, true); 18 | 19 | module.exports = require(dirs.budfox + 'budfox'); -------------------------------------------------------------------------------- /core/prepareDateRange.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | var prompt = require('prompt-lite'); 3 | var moment = require('moment'); 4 | 5 | var util = require('./util'); 6 | var config = util.getConfig(); 7 | var dirs = util.dirs(); 8 | var log = require(dirs.core + 'log'); 9 | 10 | var scan = require(dirs.tools + 'dateRangeScanner'); 11 | 12 | // helper to store the evenutally detected 13 | // daterange. 14 | var setDateRange = function(from, to) { 15 | config.backtest.daterange = { 16 | from: moment.unix(from).utc().format(), 17 | to: moment.unix(to).utc().format(), 18 | }; 19 | util.setConfig(config); 20 | } 21 | 22 | 23 | module.exports = function(done) { 24 | scan((err, ranges) => { 25 | 26 | if(_.size(ranges) === 0) 27 | util.die('No history found for this market', true); 28 | 29 | if(_.size(ranges) === 1) { 30 | var r = _.first(ranges); 31 | log.info('Gekko was able to find a single daterange in the locally stored history:'); 32 | log.info('\t', 'from:', moment.unix(r.from).utc().format('YYYY-MM-DD HH:mm:ss')); 33 | log.info('\t', 'to:', moment.unix(r.to).utc().format('YYYY-MM-DD HH:mm:ss')); 34 | 35 | 36 | setDateRange(r.from, r.to); 37 | return done(); 38 | } 39 | 40 | log.info( 41 | 'Gekko detected multiple dateranges in the locally stored history.', 42 | 'Please pick the daterange you are interested in testing:' 43 | ); 44 | 45 | _.each(ranges, (range, i) => { 46 | log.info('\t\t', `OPTION ${i + 1}:`); 47 | log.info('\t', 'from:', moment.unix(range.from).utc().format('YYYY-MM-DD HH:mm:ss')); 48 | log.info('\t', 'to:', moment.unix(range.to).utc().format('YYYY-MM-DD HH:mm:ss')); 49 | }); 50 | 51 | prompt.get({name: 'option'}, (err, result) => { 52 | 53 | var option = parseInt(result.option); 54 | if(option === NaN) 55 | util.die('Not an option..', true); 56 | 57 | var range = ranges[option - 1]; 58 | 59 | if(!range) 60 | util.die('Not an option..', true); 61 | 62 | setDateRange(range.from, range.to); 63 | return done(); 64 | }); 65 | 66 | }); 67 | } -------------------------------------------------------------------------------- /core/stats.js: -------------------------------------------------------------------------------- 1 | const stats = require('stats-lite'); 2 | const lodash = require('lodash'); 3 | 4 | 5 | // simply monkey patch the stats with other stuff we 6 | // need and pass on. 7 | 8 | // sharpe ratio 9 | // 10 | // @param returns (array - list of returns) 11 | // @param rfreturn (number - risk free return) 12 | // 13 | stats.sharpe = (returns, rfreturn) => { 14 | return (stats.mean(returns) - rfreturn) / stats.stdev(returns); 15 | } 16 | 17 | module.exports = stats; 18 | -------------------------------------------------------------------------------- /core/tools/candleLoader.js: -------------------------------------------------------------------------------- 1 | // TODO: properly handle a daterange for which no data is available. 2 | 3 | const batchSize = 1000; 4 | 5 | const _ = require('lodash'); 6 | const fs = require('fs'); 7 | const moment = require('moment'); 8 | 9 | const util = require('../../core/util'); 10 | const config = util.getConfig(); 11 | const dirs = util.dirs(); 12 | const log = require(dirs.core + '/log'); 13 | 14 | const adapter = config[config.adapter]; 15 | const Reader = require(dirs.gekko + adapter.path + '/reader'); 16 | const daterange = config.daterange; 17 | 18 | const CandleBatcher = require(dirs.core + 'candleBatcher'); 19 | 20 | const to = moment.utc(daterange.to).startOf('minute'); 21 | const from = moment.utc(daterange.from).startOf('minute'); 22 | const toUnix = to.unix(); 23 | 24 | if(to <= from) 25 | util.die('This daterange does not make sense.') 26 | 27 | if(!from.isValid()) 28 | util.die('invalid `from`'); 29 | 30 | if(!to.isValid()) 31 | util.die('invalid `to`'); 32 | 33 | let iterator = { 34 | from: from.clone(), 35 | to: from.clone().add(batchSize, 'm').subtract(1, 's') 36 | } 37 | 38 | var DONE = false; 39 | 40 | var result = []; 41 | var reader = new Reader(); 42 | var batcher; 43 | var next; 44 | var doneFn = () => { 45 | process.nextTick(() => { 46 | next(result); 47 | }) 48 | }; 49 | 50 | module.exports = function(candleSize, _next) { 51 | next = _.once(_next); 52 | 53 | batcher = new CandleBatcher(candleSize) 54 | .on('candle', handleBatchedCandles); 55 | 56 | getBatch(); 57 | } 58 | 59 | const getBatch = () => { 60 | reader.get( 61 | iterator.from.unix(), 62 | iterator.to.unix(), 63 | 'full', 64 | handleCandles 65 | ) 66 | } 67 | 68 | const shiftIterator = () => { 69 | iterator = { 70 | from: iterator.from.clone().add(batchSize, 'm'), 71 | to: iterator.from.clone().add(batchSize * 2, 'm').subtract(1, 's') 72 | } 73 | } 74 | 75 | const handleCandles = (err, data) => { 76 | if(err) { 77 | console.error(err); 78 | util.die('Encountered an error..') 79 | } 80 | 81 | if(_.size(data) && _.last(data).start >= toUnix) 82 | DONE = true; 83 | 84 | batcher.write(data); 85 | 86 | if(DONE) { 87 | reader.close(); 88 | 89 | setTimeout(doneFn, 100); 90 | 91 | } else { 92 | shiftIterator(); 93 | getBatch(); 94 | } 95 | } 96 | 97 | const handleBatchedCandles = candle => { 98 | result.push(candle); 99 | } -------------------------------------------------------------------------------- /core/tools/configBuilder.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const _ = require('lodash'); 3 | const toml = require('toml'); 4 | 5 | const util = require('../util'); 6 | const dirs = util.dirs(); 7 | 8 | const getTOML = function(fileName) { 9 | var raw = fs.readFileSync(fileName); 10 | return toml.parse(raw); 11 | } 12 | 13 | // build a config object out of a directory of TOML files 14 | module.exports = function() { 15 | const configDir = util.dirs().config; 16 | 17 | let _config = getTOML(configDir + 'general.toml'); 18 | fs.readdirSync(configDir + 'plugins').forEach(function(pluginFile) { 19 | let pluginName = _.first(pluginFile.split('.')) 20 | _config[pluginName] = getTOML(configDir + 'plugins/' + pluginFile); 21 | }); 22 | 23 | // attach the proper adapter 24 | let adapter = _config.adapter; 25 | _config[adapter] = getTOML(configDir + 'adapters/' + adapter + '.toml'); 26 | 27 | if(_config.tradingAdvisor.enabled) { 28 | // also load the strat 29 | let strat = _config.tradingAdvisor.method; 30 | let stratFile = configDir + 'strategies/' + strat + '.toml'; 31 | if(!fs.existsSync(stratFile)) 32 | util.die('Cannot find the strategy config file for ' + strat); 33 | _config[strat] = getTOML(stratFile); 34 | } 35 | 36 | const mode = util.gekkoMode(); 37 | 38 | if(mode === 'backtest') 39 | _config.backtest = getTOML(configDir + 'backtest.toml'); 40 | 41 | return _config; 42 | } -------------------------------------------------------------------------------- /core/workers/datasetScan/parent.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | var moment = require('moment'); 3 | var async = require('async'); 4 | 5 | var util = require('../../util'); 6 | var dirs = util.dirs(); 7 | 8 | var dateRangeScan = require('../dateRangeScan/parent'); 9 | 10 | module.exports = function(config, done) { 11 | 12 | util.setConfig(config); 13 | 14 | var adapter = config[config.adapter]; 15 | var scan = require(dirs.gekko + adapter.path + '/scanner'); 16 | 17 | scan((err, markets) => { 18 | 19 | if(err) 20 | return done(err); 21 | 22 | async.each(markets, (market, next) => { 23 | 24 | let marketConfig = _.clone(config); 25 | marketConfig.watch = market; 26 | 27 | dateRangeScan(marketConfig, (err, ranges) => { 28 | if(err) 29 | return next(); 30 | 31 | market.ranges = ranges; 32 | 33 | next(); 34 | }); 35 | 36 | }, err => { 37 | let resp = { 38 | datasets: [], 39 | errors: [] 40 | } 41 | markets.forEach(market => { 42 | if(market.ranges) 43 | resp.datasets.push(market); 44 | else 45 | resp.errors.push(market); 46 | }) 47 | done(err, resp); 48 | }) 49 | }); 50 | } -------------------------------------------------------------------------------- /core/workers/dateRangeScan/child.js: -------------------------------------------------------------------------------- 1 | var util = require(__dirname + '/../../util'); 2 | 3 | var dirs = util.dirs(); 4 | var ipc = require('relieve').IPCEE(process); 5 | 6 | ipc.on('start', config => { 7 | 8 | // force correct gekko env 9 | util.setGekkoEnv('child-process'); 10 | 11 | // force disable debug 12 | config.debug = false; 13 | 14 | // persist config 15 | util.setConfig(config); 16 | 17 | var scan = require(dirs.tools + 'dateRangeScanner'); 18 | scan( 19 | (err, ranges, reader) => { 20 | reader.close(); 21 | ipc.send('ranges', ranges) 22 | process.exit(0); 23 | } 24 | ); 25 | }); 26 | 27 | -------------------------------------------------------------------------------- /core/workers/dateRangeScan/parent.js: -------------------------------------------------------------------------------- 1 | var ForkTask = require('relieve').tasks.ForkTask 2 | var fork = require('child_process').fork 3 | 4 | module.exports = function(config, done) { 5 | task = new ForkTask(fork(__dirname + '/child')); 6 | 7 | task.send('start', config); 8 | 9 | task.once('ranges', ranges => { 10 | return done(false, ranges); 11 | }); 12 | task.on('exit', code => { 13 | if(code !== 0) 14 | done('ERROR, unable to scan dateranges, please check the console.'); 15 | }); 16 | } -------------------------------------------------------------------------------- /core/workers/loadCandles/child.js: -------------------------------------------------------------------------------- 1 | var start = (config, candleSize, daterange) => { 2 | var util = require(__dirname + '/../../util'); 3 | 4 | // force correct gekko env 5 | util.setGekkoEnv('child-process'); 6 | 7 | // force disable debug 8 | config.debug = false; 9 | util.setConfig(config); 10 | 11 | var dirs = util.dirs(); 12 | 13 | var load = require(dirs.tools + 'candleLoader'); 14 | load(config.candleSize, candles => { 15 | process.send(candles); 16 | }) 17 | } 18 | 19 | process.send('ready'); 20 | 21 | process.on('message', (m) => { 22 | if(m.what === 'start') 23 | start(m.config, m.candleSize, m.daterange); 24 | }); -------------------------------------------------------------------------------- /core/workers/loadCandles/parent.js: -------------------------------------------------------------------------------- 1 | // example usage: 2 | 3 | // let config = { 4 | // watch: { 5 | // exchange: 'poloniex', 6 | // currency: 'USDT', 7 | // asset: 'BTC' 8 | // }, 9 | // daterange: { 10 | // from: '2016-05-22 11:22', 11 | // to: '2016-06-03 19:56' 12 | // }, 13 | // adapter: 'sqlite', 14 | // sqlite: { 15 | // path: 'plugins/sqlite', 16 | 17 | // dataDirectory: 'history', 18 | // version: 0.1, 19 | 20 | // dependencies: [{ 21 | // module: 'sqlite3', 22 | // version: '3.1.4' 23 | // }] 24 | // }, 25 | // candleSize: 100 26 | // } 27 | 28 | // module.exports(config, function(err, data) { 29 | // console.log('FINAL CALLBACK'); 30 | // console.log('err', err); 31 | // console.log('data', data.length); 32 | // }) 33 | 34 | 35 | const fork = require('child_process').fork; 36 | const _ = require('lodash'); 37 | 38 | module.exports = (config, callback) => { 39 | const child = fork(__dirname + '/child'); 40 | 41 | const message = { 42 | what: 'start', 43 | config 44 | } 45 | 46 | const done = _.once(callback); 47 | 48 | child.on('message', function(m) { 49 | if(m === 'ready') 50 | return child.send(message); 51 | 52 | // else we are done and have candles! 53 | done(null, m); 54 | child.kill('SIGINT'); 55 | }); 56 | 57 | child.on('exit', code => { 58 | if(code !== 0) 59 | done('ERROR, unable to load candles, please check the console.'); 60 | }); 61 | } -------------------------------------------------------------------------------- /core/workers/pipeline/child.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Gekko is a Bitcoin trading bot for popular Bitcoin exchanges written 4 | in node, it features multiple trading methods using technical analysis. 5 | 6 | If you are interested in how Gekko works, read more about Gekko's 7 | architecture here: 8 | 9 | https://github.com/askmike/gekko/blob/stable/docs/internals/architecture.md 10 | 11 | Disclaimer: 12 | 13 | USE AT YOUR OWN RISK! 14 | 15 | The author of this project is NOT responsible for any damage or loss caused 16 | by this software. There can be bugs and the bot may not perform as expected 17 | or specified. Please consider testing it first with paper trading and/or 18 | backtesting on historical data. Also look at the code to see what how 19 | it is working. 20 | 21 | */ 22 | 23 | var start = (mode, config) => { 24 | var util = require(__dirname + '/../../util'); 25 | 26 | // force correct gekko env 27 | util.setGekkoEnv('child-process'); 28 | 29 | var dirs = util.dirs(); 30 | 31 | // force correct gekko mode & config 32 | util.setGekkoMode(mode); 33 | util.setConfig(config); 34 | 35 | var pipeline = require(dirs.core + 'pipeline'); 36 | pipeline({ 37 | config: config, 38 | mode: mode 39 | }); 40 | } 41 | 42 | process.send('ready'); 43 | 44 | process.on('message', function(m) { 45 | if(m.what === 'start') 46 | start(m.mode, m.config); 47 | 48 | if(m.what === 'exit') 49 | process.exit(0); 50 | }); 51 | 52 | process.on('disconnect', function() { 53 | console.log("disconnect"); 54 | process.exit(-1); 55 | }) -------------------------------------------------------------------------------- /core/workers/pipeline/messageHandlers/backtestHandler.js: -------------------------------------------------------------------------------- 1 | // listen to all messages and internally queue 2 | // all candles and trades, when done report them 3 | // all back at once 4 | 5 | module.exports = done => { 6 | var trades = []; 7 | var roundtrips = [] 8 | var candles = []; 9 | var report = false; 10 | 11 | return { 12 | message: message => { 13 | 14 | if(message.type === 'candle') 15 | candles.push(message.candle); 16 | 17 | else if(message.type === 'trade') 18 | trades.push(message.trade); 19 | 20 | else if(message.type === 'roundtrip') 21 | roundtrips.push(message.roundtrip); 22 | 23 | else if(message.type === 'report') 24 | report = message.report; 25 | 26 | else if(message.log) 27 | console.log(message.log); 28 | }, 29 | exit: status => { 30 | if(status !== 0) 31 | done('Child process has died.'); 32 | else 33 | done(null, { 34 | trades: trades, 35 | candles: candles, 36 | report: report, 37 | roundtrips: roundtrips 38 | }); 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /core/workers/pipeline/messageHandlers/importerHandler.js: -------------------------------------------------------------------------------- 1 | module.exports = cb => { 2 | 3 | return { 4 | message: message => { 5 | 6 | if(message.type === 'update') 7 | cb(null, { 8 | done: false, 9 | latest: message.latest 10 | }) 11 | 12 | else if(message.type === 'error') { 13 | cb(message.error); 14 | console.error(message.error); 15 | } 16 | 17 | else if(message.type === 'log') 18 | console.log(message.log); 19 | }, 20 | exit: status => { 21 | if(status !== 0) 22 | return cb('Child process has died.'); 23 | else 24 | cb(null, { done: true }); 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /core/workers/pipeline/messageHandlers/realtimeHandler.js: -------------------------------------------------------------------------------- 1 | // pass back all messages as is 2 | // (except for errors and logs) 3 | 4 | module.exports = cb => { 5 | 6 | return { 7 | message: message => { 8 | 9 | if(message.type === 'error') { 10 | cb(message.error); 11 | console.error(message.error); 12 | } 13 | 14 | else 15 | cb(null, message); 16 | 17 | }, 18 | exit: status => { 19 | if(status !== 0) 20 | cb('Child process has died.'); 21 | else 22 | cb(null, { done: true }); 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /core/workers/pipeline/parent.js: -------------------------------------------------------------------------------- 1 | var fork = require('child_process').fork; 2 | 3 | module.exports = (mode, config, callback) => { 4 | var child = fork(__dirname + '/child'); 5 | 6 | // How we should handle client messages depends 7 | // on the mode of the Pipeline that is being ran. 8 | var handle = require('./messageHandlers/' + mode + 'Handler')(callback); 9 | 10 | var message = { 11 | what: 'start', 12 | mode: mode, 13 | config: config 14 | } 15 | 16 | child.on('message', function(m) { 17 | 18 | if(m === 'ready') 19 | return child.send(message); 20 | 21 | if(m === 'done') 22 | return child.send({what: 'exit'}); 23 | 24 | handle.message(m); 25 | }); 26 | 27 | child.on('exit', handle.exit); 28 | 29 | return child; 30 | } -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | gekko: 4 | restart: always 5 | build: ./ 6 | volumes: 7 | - ./volumes/gekko/history:/usr/src/app/history 8 | - ./config.js:/usr/src/app/config.js 9 | links: 10 | - redis 11 | # - postgresql 12 | environment: 13 | - HOST 14 | - PORT 15 | ports: # you can comment this out when using the nginx frontend 16 | - "${PORT}:${PORT}" 17 | ## optionally set nginx vars if you wish to frontend it with nginx 18 | # environment: 19 | # - VIRTUAL_HOST=gekko 20 | # - PORT=3000 21 | # - DOMAIN=gekko 22 | 23 | redis: 24 | image: redis:latest 25 | volumes: 26 | - ./volumes/redis:/data 27 | ## optionally uncomment if you wish to use nginx as a frontend 28 | # nginx: 29 | # restart: always 30 | # image: jwilder/nginx-proxy 31 | # ports: 32 | # - "80:80" 33 | # volumes: 34 | # - /var/run/docker.sock:/tmp/docker.sock:ro 35 | ## optionally uncomment if you wish to use postgresql as a db 36 | # postgresql: 37 | # restart: always 38 | # image: postgres:9.6-alpine 39 | # ports: 40 | # - 5432:5432 41 | # volumes: 42 | # - ./_postgresql:/var/lib/postgresql/data:rw 43 | # environment: 44 | # POSTGRES_DB: gekko 45 | # POSTGRES_USER: username 46 | # POSTGRES_PASSWORD: password 47 | -------------------------------------------------------------------------------- /docker-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | sed -i 's/127.0.0.1/0.0.0.0/g' /usr/src/app/web/vue/UIconfig.js 4 | sed -i 's/localhost/'${HOST}'/g' /usr/src/app/web/vue/UIconfig.js 5 | sed -i 's/3000/'${PORT}'/g' /usr/src/app/web/vue/UIconfig.js 6 | exec "$@" -------------------------------------------------------------------------------- /docs/commandline/Importing.md: -------------------------------------------------------------------------------- 1 | # Importing 2 | 3 | *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.* 4 | 5 | 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). 6 | 7 | ## Setup 8 | 9 | For importing you should [enable and configure](./plugins.md) the following plugin: 10 | 11 | - candleWriter (to store the imported data in a database) 12 | 13 | Besides that, make sure to configure `config.watch` properly. 14 | 15 | ## Configure 16 | 17 | In your config set the `importer.daterange` properties to the daterange you would like to import. 18 | 19 | ## Run 20 | 21 | node gekko --import 22 | 23 | The result will be something like this: 24 | 25 | 2016-06-26 09:12:16 (INFO): Gekko v0.2.2 started 26 | 2016-06-26 09:12:16 (INFO): I'm gonna make you rich, Bud Fox. 27 | 28 | 2016-06-26 09:12:17 (INFO): Setting up Gekko in importer mode 29 | 2016-06-26 09:12:17 (INFO): 30 | 2016-06-26 09:12:17 (INFO): Setting up: 31 | 2016-06-26 09:12:17 (INFO): Candle writer 32 | 2016-06-26 09:12:17 (INFO): Store candles in a database 33 | 2016-06-26 09:12:17 (INFO): 34 | 35 | 2016-06-26 09:12:17 (WARN): The plugin Trading Advisor does not support the mode importer. It has been disabled. 36 | 2016-06-26 09:12:17 (WARN): The plugin Advice logger does not support the mode importer. It has been disabled. 37 | 2016-06-26 09:12:17 (WARN): The plugin Profit Simulator does not support the mode importer. It has been disabled. 38 | 2016-06-26 09:12:18 (DEBUG): Processing 798 new trades. 39 | 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) 40 | 2016-06-26 09:12:20 (DEBUG): Processing 211 new trades. 41 | (...) 42 | -------------------------------------------------------------------------------- /docs/commandline/about_the_commandline.md: -------------------------------------------------------------------------------- 1 | # About the commandline 2 | 3 | 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. 4 | 5 | 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. 6 | 7 | ## Configuring Gekko 8 | 9 | If you decide you want to run gekko over the commandline you need to decide two things: 10 | 11 | 1. What market / settings are you interested in? 12 | 2. What should Gekko do? This is either backtest, import or run live. 13 | 14 | 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. 15 | 16 | ## Running Gekko 17 | 18 | For live mode run: 19 | 20 | node gekko --config config.js 21 | 22 | To backtesting run: 23 | 24 | node gekko --config config.js --backtest 25 | 26 | To import run: 27 | 28 | node gekko --config config.js --import -------------------------------------------------------------------------------- /docs/commandline/tradebot.md: -------------------------------------------------------------------------------- 1 | # Tradebot 2 | 3 | You can set Gekko up as a tradebot, this will instruct Gekko to: 4 | 5 | - Watch a live market. 6 | - Run a strategy (in semi-realtime) over live market data. 7 | - Automatically create buy/sell orders based on signals coming from the strategy. 8 | 9 | *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.* 10 | 11 | ## Configuration 12 | 13 | First, set up Gekko for commandline usage (see [this document](./about_the_commandline.md) for details). After that, configure the following plugins: 14 | 15 | - `config.watch` - the market to trade on. 16 | - `candleWriter` - (optional) also store market data to disk. 17 | - `tradingAdvisor` - configure the strategy and candle properties. 18 | - `trader` - configure Gekko access to your exchange account. 19 | 20 | Turn off the paperTrader (to not get conflicting profit reports). 21 | 22 | Once done, run Gekko in live mode. 23 | -------------------------------------------------------------------------------- /docs/extending/other_software.md: -------------------------------------------------------------------------------- 1 | # Other Software 2 | 3 | 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). 4 | 5 | ## List 6 | 7 | Coming soon! -------------------------------------------------------------------------------- /docs/features/backtesting.md: -------------------------------------------------------------------------------- 1 | # Backtesting 2 | 3 | Gekko supports backtesting strategies over historical data. This means that Gekko will simulate running a strategy in realtime against a live market. Backtesting requires having data locally available already. After a backtest Gekko will provide statistics about the market and the strategy's performance. 4 | 5 | ![screen shot of gekko backtesting](https://cloud.githubusercontent.com/assets/969743/24838718/8c790a86-1d45-11e7-99ae-e7e551cb40cb.png) 6 | 7 | ## Simplified simulation 8 | 9 | Gekko backtests using a very limited datasource (only OHCL candles). This means that Gekko estimates trades (and thus profits), which depending on the liquidity and market depth might be estimated very wrong. By configuring the paper trader's fee and slippage you can better mimic trading at the real market. 10 | 11 | In order to backtest with 100% accuracy one would need the exact state of the orderbook (spread and depth) as well as information about orders happening around the time of each advice. With Gekko we made the decision to not store all this information (to simplify importing and storing market data). In voluminous and liquid markets this shouldn't be too much of a problem, but if you are backtesting over a small market (like some altcoins) the estimation will be of poor accuracy. 12 | 13 | If you look at the following backtest result: 14 | 15 | ![screen shot of backtesting at an illiquid market](https://cloud.githubusercontent.com/assets/969743/24840243/8f307022-1d61-11e7-9964-e6614d7433ea.png) 16 | 17 | You can see a lot of "spikes" of the price moving up and down. These are not actually price fluctiations 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. 18 | -------------------------------------------------------------------------------- /docs/features/importing.md: -------------------------------------------------------------------------------- 1 | # Importing 2 | 3 | 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). 4 | 5 | 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: 6 | 7 | ![importing wizard](https://user-images.githubusercontent.com/969743/28596822-a79509cc-7192-11e7-9293-53066f598053.png) 8 | 9 | Once you configure the market and daterange you want to watch and click import Gekko will automatically download historical market data from the exchange: 10 | 11 | ![gekko importer](https://user-images.githubusercontent.com/969743/28597211-914dbe32-7194-11e7-8352-60a69afdf846.png) 12 | -------------------------------------------------------------------------------- /docs/installation/installing_gekko.md: -------------------------------------------------------------------------------- 1 | # Installing Gekko 2 | 3 | - *Windows user? Please see the doc [installing Gekko on windows](./installing_gekko_on_windows.md) instead.* 4 | - *Docker user? You can run Gekko in a docker container, see [installing Gekko using Docker](./installing_gekko_using_docker.md) instead.* 5 | - *Server user? In order to run Gekko headless, see [installing Gekko on a server](./installing_gekko_on_a_server.md) instead.* 6 | 7 | Here is a video of me explaining how to install Gekko the easiest way possible: 8 | 9 | [![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) 10 | 11 | To get Gekko running you need to do the following: 12 | 13 | - install nodejs 14 | - download Gekko 15 | - install Gekko's dependencies 16 | 17 | ## Installing nodejs 18 | 19 | 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 6). We advice to download the current LTS. 20 | 21 | ## Downloading Gekko 22 | 23 | The recommanded way of downloading Gekko is by using git. This makes keeping Gekko updated a lot easier. For git, run this in a terminal: 24 | 25 | git clone git://github.com/askmike/gekko.git 26 | cd gekko 27 | 28 | Alternatively download and extract [the zip here](https://github.com/askmike/gekko/archive/stable.zip). 29 | 30 | ## Installing Gekko dependencies 31 | 32 | Once it is installed we need to install Gekko's dependencies, open your terminal and navigate to the gekko folder and run: 33 | 34 | npm install --only=production 35 | 36 | ## Starting Gekko 37 | 38 | After all the above you can start Gekko by running the following in your terminal: 39 | 40 | node gekko --ui 41 | 42 | ## Updating Gekko 43 | 44 | If you installed Gekko via git, simply run: 45 | 46 | git pull 47 | npm install --only=production 48 | 49 | If you downloaded the zip you can just download the new version. If you want to move historical data over (for backtesting purposes), copy the constents of the `history` folder found inside the gekko folder. If you have written your own strategies, don't forget to move them over as well. -------------------------------------------------------------------------------- /docs/installation/installing_gekko_on_windows.md: -------------------------------------------------------------------------------- 1 | # Installing Gekko on windows 2 | 3 | ### Note: 4 | #### 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)). 5 | #### 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" 6 | 7 | Here is a youtube video I made that shows exactly how to set up Gekko: 8 | 9 | [![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) 10 | 11 | To get Gekko running on Windows you need to do the following: 12 | 13 | - install nodejs 14 | - download Gekko 15 | - install Gekko's dependencies 16 | 17 | ## Install nodejs 18 | 19 | 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. 20 | 21 | ## Install Gekko 22 | 23 | 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: 24 | 25 | ### Open up command line 26 | 27 | * Start 28 | * Type in 'cmd.exe' 29 | * Press enter 30 | 31 | ### Install dependencies 32 | 33 | (After every command, press enter) 34 | 35 | First navigate to Gekko: 36 | 37 | cd Downloads 38 | cd gekko-stable 39 | cd gekko-stable 40 | 41 | Install Gekko's dependencies: 42 | 43 | npm install --only=production 44 | 45 | ### Install Tulip Indicators 46 | 47 | 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: 48 | 49 | npm install windows-build-tools --global --production 50 | 51 | Once your build tools are installed, or for other operating systems 52 | 53 | npm install tulind --only=production 54 | 55 | ### Starting Gekko 56 | 57 | node gekko --ui 58 | 59 | Your browser should automatically open with the UI. If it doesn't, manually browse to [http://localhost:3000](http://localhost:3000). 60 | 61 | ### Stopping Gekko 62 | 63 | In the command line hold `ctrl` + `c`. 64 | -------------------------------------------------------------------------------- /docs/installation/installing_gekko_using_docker.md: -------------------------------------------------------------------------------- 1 | # Installing Gekko using Docker 2 | 3 | Installing and running gekko in a docker container for use on the same machine is simple with the following commands: 4 | 5 | ``` 6 | $ docker-compose build 7 | $ docker-compose up -d 8 | ``` 9 | 10 | You can now find your gekko instance running on `localhost:3000`. 11 | 12 | ## Installing for external access 13 | 14 | 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: 15 | 16 | ``` 17 | $ docker-compose build 18 | $ HOST=mydomain.com PORT=3001 docker-compose up -d 19 | ``` 20 | 21 | You can now find your gekko instance running on `mydomain.com:3001`. 22 | 23 | To see logs: `docker logs -f gekko_gekko_1`. View which dockers are running by executing `docker ps`. -------------------------------------------------------------------------------- /docs/internals/gekko_ui.md: -------------------------------------------------------------------------------- 1 | # Gekko UI 2 | 3 | When you launch Gekko UI, you start a basic nodejs webserver with 3 components: 4 | 5 | - It will serve frontend (HTML/CSS/JS) files for frontend written as a [vuejs app](https://vuejs.org/) (v2). 6 | - It will will handle API requests as [koa](http://koajs.com/) (v1) routes. 7 | - It will start a websocket server used to broadcast messages in realtime (used for long lived processes Importing and Live Gekkos for example). 8 | 9 | **Warning: The UI and the APIs are anything but stable.** 10 | 11 | ## Gekko UI configurables 12 | 13 | By default Gekko UI is setup up so it runs locally on your own machine, if you want to run Gekko anywhere else please configure the GekkoUI config [todo: link] to your liking. 14 | 15 | ## Gekko UI Frontend 16 | 17 | The frontend is setup as a very basic vue app. Additionally the following libraries are used: 18 | 19 | - [Reconnecting websocket](https://github.com/joewalnes/reconnecting-websocket) 20 | - [Moment](http://momentjs.com/) 21 | - [d3.js](https://d3js.org/) 22 | - [toml](https://github.com/BinaryMuse/toml-node) 23 | - [humanizeDuration](https://github.com/EvanHahn/HumanizeDuration.js) 24 | 25 | The vue app itself uses the following libraries: 26 | 27 | - [marked](https://github.com/chjj/marked) 28 | - [jade](https://github.com/pugjs) (all html is either written in jade of markdown) 29 | - [vue-router](https://github.com/vuejs/vue-router) 30 | - [superagent](https://github.com/visionmedia/superagent) (cross browser ajax) 31 | 32 | ### Developing for the Gekko UI frontend 33 | 34 | You first need to install all developer dependencies so the frontend app can be recompiled on your machine. 35 | 36 | cd gekko/web/vue 37 | npm install 38 | 39 | After this you can launch a hot reload version of the app which will automatically recompile the frontend and reload your browser: 40 | 41 | # path to webserver 42 | cd gekko/web 43 | # launch the server - we use this API 44 | node server 45 | 46 | # path to vue app 47 | cd vue 48 | npm run dev 49 | 50 | 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) 51 | 52 | ### Recompiling the Gekko UI frontend 53 | 54 | When you are done developing and adding your contributions by recompiling the app: 55 | 56 | # path to vue app 57 | cd gekko/web/vue 58 | npm run build -------------------------------------------------------------------------------- /docs/introduction/roadmap.md: -------------------------------------------------------------------------------- 1 | # Roadmap 2 | 3 | Gekko is ever evolving and we are constantly working on new features and upgrades! 4 | 5 | This is a small selection of things currently on the roadmap: 6 | 7 | ## Current TODO 8 | 9 | - [UI] Improve management and display of live gekkos ([#911](https://github.com/askmike/gekko/issues/911) - [askmike](https://github.com/askmike)) 10 | - Simplify config ([#956](https://github.com/askmike/gekko/issues/956) - [cassvail](https://github.com/cassvail)) 11 | - fix BTC Markets exchange ([#936](https://github.com/askmike/gekko/pull/936) - [anotherkabab](https://github.com/anotherkabab)) 12 | 13 | ## Major active discussions 14 | 15 | - [UI] Draw indicators results in the UI ([#793](https://github.com/askmike/gekko/pull/793) - [askmike](https://github.com/askmike)) 16 | - Store roundtrips in CSV ([#978](https://github.com/askmike/gekko/issues/978)) 17 | - Integrating Machine Learning AI into to Gekko ([#789](https://github.com/askmike/gekko/issues/789)) 18 | 19 | ----- 20 | 21 | Note that besides this list there are a big number of smaller issues and bugfixes happening constantly. See the [issue tracker](https://github.com/askmike/gekko/issues) for more details. -------------------------------------------------------------------------------- /docs/introduction/supporting_the_project.md: -------------------------------------------------------------------------------- 1 | # Supporting the project 2 | 3 | Gekko is free and open source software. We don't make any money by publishing it, keeping it up to date or providing support. 4 | 5 | If Gekko helped you in any way (financially or otherwise) feel free to donate. If you want to see a specific feature implemented faster, feel free to [send an e-mail](mailto:gekko@mvr.me) to discuss. 6 | 7 | - Bitcoin tip jar: 13r1jyivitShUiv9FJvjLH7Nh1ZZptumwW 8 | - Ethereum tip jar: 0x46e9249ADc30F5bfc6632e1d0b4286496d071Be7 -------------------------------------------------------------------------------- /docs/strategies/gekko_indicators.md: -------------------------------------------------------------------------------- 1 | # Gekko indicators 2 | 3 | When [creating your own strategy](./creating_a_strategy.md) there are a few built in indicators you can use that ship with Gekko. 4 | 5 | ## Example 6 | 7 | If you want to use the MACD indicator from Gekko, you need to register it in your strategy like so: 8 | 9 | method.init = function() { 10 | var settings = { 11 | short: 10, 12 | long: 21, 13 | signal: 9 14 | }; 15 | 16 | // add the indicator to the strategy 17 | this.addIndicator('mymacd', 'MACD', settings); 18 | } 19 | 20 | method.check = function() { 21 | // use indicator results 22 | var macdiff = this.indicators.mymacd.result; 23 | 24 | // do something with macdiff 25 | } 26 | 27 | ## Indicators 28 | 29 | Here is a list of all supported indicators, click on them to read more about what they are and how to implement them in Gekko: 30 | 31 | - [EMA](#ema) 32 | - [PPO](#ppo) 33 | - [CCI](#cci) 34 | - [DEMA](#dema) 35 | - [LRC](#lrc) 36 | - [MACD](#macd) 37 | - [RSI](#rsi) 38 | - [SMA](#sma) 39 | - [TSI](#tsi) 40 | - [UO](#UO) 41 | 42 | ### EMA 43 | 44 | > **What is an 'Exponential Moving Average - EMA'** 45 | > An exponential moving average (EMA) is a type of moving average that is similar to a simple moving average, except that more weight is given to the latest data. It's also known as the exponentially weighted moving average. This type of moving average reacts faster to recent price changes than a simple moving average. 46 | 47 | *[More info on investopedia](http://www.investopedia.com/terms/e/ema.asp).* 48 | 49 | You can implement the EMA like so: 50 | 51 | method.init = function() { 52 | var weight = 10; 53 | 54 | // add the indicator to the strategy 55 | this.addIndicator('myema', 'EMA', weight); 56 | } 57 | 58 | method.check = function() { 59 | // use indicator results 60 | var ema = this.indicators.myema.result; 61 | 62 | // do something with macdiff 63 | } 64 | 65 | 66 | ### PPO 67 | 68 | [todo] 69 | 70 | ### CCI 71 | 72 | [todo] 73 | 74 | ### DEMA 75 | 76 | [todo] 77 | 78 | ### LRC 79 | 80 | [todo] 81 | 82 | ### MACD 83 | 84 | [todo] 85 | 86 | ### RSI 87 | 88 | [todo] 89 | 90 | ### SMA 91 | 92 | [todo] 93 | 94 | ### TSI 95 | 96 | [todo] 97 | 98 | ### UO 99 | 100 | [todo] -------------------------------------------------------------------------------- /exchanges/bx.in.th.js: -------------------------------------------------------------------------------- 1 | var BitexthaiAPI = require('bitexthai'); 2 | var util = require('../core/util.js'); 3 | var _ = require('lodash'); 4 | var moment = require('moment'); 5 | var log = require('../core/log'); 6 | 7 | var Trader = function(config) { 8 | _.bindAll(this); 9 | if(_.isObject(config)) { 10 | this.key = config.key; 11 | this.secret = config.secret; 12 | this.clientID = config.username; 13 | } 14 | this.name = 'BX.in.th'; 15 | 16 | this.pair = 1; // todo 17 | 18 | this.bitexthai = new BitexthaiAPI(this.key, this.secret, this.clientID); 19 | } 20 | 21 | // if the exchange errors we try the same call again after 22 | // waiting 10 seconds 23 | Trader.prototype.retry = function(method, args) { 24 | var wait = +moment.duration(10, 'seconds'); 25 | log.debug(this.name, 'returned an error, retrying..'); 26 | 27 | var self = this; 28 | 29 | // make sure the callback (and any other fn) 30 | // is bound to Trader 31 | _.each(args, function(arg, i) { 32 | if(_.isFunction(arg)) 33 | args[i] = _.bind(arg, self); 34 | }); 35 | 36 | // run the failed method again with the same 37 | // arguments after wait 38 | setTimeout( 39 | function() { method.apply(self, args) }, 40 | wait 41 | ); 42 | } 43 | 44 | Trader.prototype.getTrades = function(since, callback, descending) { 45 | var args = _.toArray(arguments); 46 | var process = function(err, result) { 47 | if(err) 48 | return this.retry(this.getTrades, args); 49 | 50 | var parsedTrades = []; 51 | _.each(result.trades, function(trade) { 52 | // We get trade time in local time, which is GMT+7 53 | var date = moment(trade.trade_date).subtract(7, 'hours').unix(); 54 | 55 | parsedTrades.push({ 56 | date: date, 57 | price: parseFloat(trade.rate), 58 | amount: parseFloat(trade.amount), 59 | tid: trade.trade_id 60 | }); 61 | }, this); 62 | 63 | if(descending) 64 | callback(null, parsedTrades.reverse()); 65 | else 66 | callback(null, parsedTrades); 67 | }.bind(this); 68 | 69 | this.bitexthai.trades(this.pair, process); 70 | } 71 | 72 | Trader.getCapabilities = function () { 73 | return { 74 | name: 'BX.in.th', 75 | slug: 'bx.in.th', 76 | currencies: ['THB'], 77 | assets: ['BTC'], 78 | markets: [ 79 | { 80 | pair: ['THB', 'BTC'], minimalOrder: { amount: 0.0001, unit: 'asset' }, 81 | } 82 | ], 83 | requires: ['key', 'secret'], 84 | tradeError: 'NOT IMPLEMENTED YET', 85 | providesHistory: false 86 | }; 87 | } 88 | 89 | module.exports = Trader; -------------------------------------------------------------------------------- /gekko.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Gekko is a Bitcoin trading bot for popular Bitcoin exchanges written 4 | in node, it features multiple trading methods using technical analysis. 5 | 6 | If you are interested in how Gekko works, read more about Gekko's 7 | architecture here: 8 | 9 | https://github.com/askmike/gekko/blob/stable/docs/internals/architecture.md 10 | 11 | Disclaimer: 12 | 13 | USE AT YOUR OWN RISK! 14 | 15 | The author of this project is NOT responsible for any damage or loss caused 16 | by this software. There can be bugs and the bot may not perform as expected 17 | or specified. Please consider testing it first with paper trading and/or 18 | backtesting on historical data. Also look at the code to see what how 19 | it is working. 20 | 21 | */ 22 | 23 | console.log(` 24 | ______ ________ __ __ __ __ ______ 25 | / \\ / |/ | / |/ | / | / \\ 26 | /$$$$$$ |$$$$$$$$/ $$ | /$$/ $$ | /$$/ /$$$$$$ | 27 | $$ | _$$/ $$ |__ $$ |/$$/ $$ |/$$/ $$ | $$ | 28 | $$ |/ |$$ | $$ $$< $$ $$< $$ | $$ | 29 | $$ |$$$$ |$$$$$/ $$$$$ \\ $$$$$ \\ $$ | $$ | 30 | $$ \\__$$ |$$ |_____ $$ |$$ \\ $$ |$$ \\ $$ \\__$$ | 31 | $$ $$/ $$ |$$ | $$ |$$ | $$ |$$ $$/ 32 | $$$$$$/ $$$$$$$$/ $$/ $$/ $$/ $$/ $$$$$$/ 33 | `); 34 | 35 | const util = require(__dirname + '/core/util'); 36 | 37 | console.log('\tGekko v' + util.getVersion()); 38 | console.log('\tI\'m gonna make you rich, Bud Fox.', '\n\n'); 39 | 40 | const dirs = util.dirs(); 41 | 42 | if(util.launchUI()) 43 | return require(util.dirs().web + 'server'); 44 | 45 | const pipeline = require(dirs.core + 'pipeline'); 46 | const config = util.getConfig(); 47 | const mode = util.gekkoMode(); 48 | 49 | if( 50 | config.trader.enabled && 51 | !config['I understand that Gekko only automates MY OWN trading strategies'] 52 | ) 53 | util.die('Do you understand what Gekko will do with your money? Read this first:\n\nhttps://github.com/askmike/gekko/issues/201'); 54 | 55 | // > Ever wonder why fund managers can't beat the S&P 500? 56 | // > 'Cause they're sheep, and sheep get slaughtered. 57 | pipeline({ 58 | config: config, 59 | mode: mode 60 | }); 61 | 62 | -------------------------------------------------------------------------------- /importers/exchanges/binance.js: -------------------------------------------------------------------------------- 1 | const moment = require('moment'); 2 | const util = require('../../core/util.js'); 3 | const _ = require('lodash'); 4 | const log = require('../../core/log'); 5 | 6 | var config = util.getConfig(); 7 | var dirs = util.dirs(); 8 | 9 | var Fetcher = require(dirs.exchanges + 'binance'); 10 | 11 | util.makeEventEmitter(Fetcher); 12 | 13 | var end = false; 14 | var done = false; 15 | var from = false; 16 | 17 | var fetcher = new Fetcher(config.watch); 18 | 19 | var fetch = () => { 20 | fetcher.import = true; 21 | fetcher.getTrades(from, handleFetch); 22 | }; 23 | 24 | var handleFetch = (unk, trades) => { 25 | if (trades.length > 0) { 26 | var last = moment.unix(_.last(trades).date).utc(); 27 | var next = last.clone(); 28 | } else { 29 | var next = from.clone().add(1, 'd'); 30 | log.debug('Import step returned no results, moving to the next 24h period'); 31 | } 32 | 33 | if (from.add(1, 'd') >= end) { 34 | fetcher.emit('done'); 35 | 36 | var endUnix = end.unix(); 37 | trades = _.filter(trades, t => t.date <= endUnix); 38 | } 39 | 40 | from = next.clone(); 41 | fetcher.emit('trades', trades); 42 | }; 43 | 44 | module.exports = function(daterange) { 45 | from = daterange.from.clone().utc(); 46 | end = daterange.to.clone().utc(); 47 | 48 | return { 49 | bus: fetcher, 50 | fetch: fetch, 51 | }; 52 | }; 53 | -------------------------------------------------------------------------------- /importers/exchanges/btcc.js: -------------------------------------------------------------------------------- 1 | var BTCChina = require('btc-china-fork'); 2 | var util = require('../../core/util.js'); 3 | var _ = require('lodash'); 4 | var moment = require('moment'); 5 | var log = require('../../core/log'); 6 | 7 | var config = util.getConfig(); 8 | 9 | var dirs = util.dirs(); 10 | 11 | var Fetcher = require(dirs.exchanges + 'btcc'); 12 | 13 | // patch getTrades.. 14 | Fetcher.prototype.getTrades = function(fromTid, sinceTime, callback) { 15 | var args = _.toArray(arguments); 16 | var process = function(err, result) { 17 | if(err) 18 | return this.retry(this.getTrades, args); 19 | 20 | callback(result); 21 | }.bind(this); 22 | 23 | if(sinceTime) 24 | var params = { 25 | limit: 1, 26 | sincetype: 'time', 27 | since: sinceTime 28 | } 29 | 30 | else if(fromTid) 31 | var params = { 32 | limit: 5000, 33 | since: fromTid 34 | } 35 | 36 | this.btcc.getHistoryData(process, params); 37 | } 38 | 39 | util.makeEventEmitter(Fetcher); 40 | 41 | var iterator = false; 42 | var end = false; 43 | var done = false; 44 | var from = false; 45 | 46 | var fetcher = new Fetcher(config.watch); 47 | 48 | var fetch = () => { 49 | if(!iterator) 50 | fetcher.getTrades(false, from, handleFirstFetch); 51 | else 52 | fetcher.getTrades(iterator, false, handleFetch); 53 | } 54 | 55 | // we use the first fetch to figure out 56 | // the tid of the moment we want data from 57 | var handleFirstFetch = trades => { 58 | iterator = _.first(trades).tid; 59 | fetch(); 60 | } 61 | 62 | var handleFetch = trades => { 63 | 64 | iterator = _.last(trades).tid; 65 | var last = moment.unix(_.last(trades).date); 66 | 67 | if(last > end) { 68 | fetcher.emit('done'); 69 | 70 | var endUnix = end.unix(); 71 | trades = _.filter( 72 | trades, 73 | t => t.date <= endUnix 74 | ); 75 | } 76 | 77 | fetcher.emit('trades', trades); 78 | } 79 | 80 | module.exports = function (daterange) { 81 | from = daterange.from.unix(); 82 | end = daterange.to.clone(); 83 | 84 | return { 85 | bus: fetcher, 86 | fetch: fetch 87 | } 88 | } 89 | 90 | -------------------------------------------------------------------------------- /importers/exchanges/gdax.js: -------------------------------------------------------------------------------- 1 | var Gdax = require('gdax'); 2 | var util = require('../../core/util.js'); 3 | var _ = require('lodash'); 4 | var moment = require('moment'); 5 | var log = require('../../core/log'); 6 | 7 | var config = util.getConfig(); 8 | 9 | var dirs = util.dirs(); 10 | 11 | var Fetcher = require(dirs.exchanges + 'gdax'); 12 | 13 | util.makeEventEmitter(Fetcher); 14 | 15 | var end = false; 16 | var done = false; 17 | var from = false; 18 | 19 | var prevLastId = false; 20 | 21 | var fetcher = new Fetcher(config.watch); 22 | 23 | var fetch = () => { 24 | fetcher.import = true; 25 | fetcher.getTrades(from, handleFetch); 26 | } 27 | 28 | var handleFetch = (unk, trades) => { 29 | var last = moment.unix(_.last(trades).date); 30 | var lastId = _.last(trades).tid 31 | 32 | if(last < from) { 33 | log.debug('Skipping data, they are before from date', last.format()); 34 | return fetch(); 35 | } 36 | 37 | if (last > end || lastId === prevLastId) { 38 | fetcher.emit('done'); 39 | 40 | var endUnix = end.unix(); 41 | trades = _.filter( 42 | trades, 43 | t => t.date <= endUnix 44 | ) 45 | } 46 | 47 | prevLastId = lastId 48 | fetcher.emit('trades', trades); 49 | } 50 | 51 | module.exports = function (daterange) { 52 | 53 | from = daterange.from.clone(); 54 | end = daterange.to.clone(); 55 | 56 | return { 57 | bus: fetcher, 58 | fetch: fetch 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /importers/exchanges/kraken.js: -------------------------------------------------------------------------------- 1 | var KrakenClient = require('kraken-api-es5') 2 | var _ = require('lodash'); 3 | var moment = require('moment'); 4 | 5 | var util = require('../../core/util.js'); 6 | var log = require('../../core/log'); 7 | var Errors = require('../../core/error.js') 8 | 9 | var config = util.getConfig(); 10 | 11 | var dirs = util.dirs(); 12 | 13 | var Fetcher = require(dirs.exchanges + 'kraken'); 14 | 15 | util.makeEventEmitter(Fetcher); 16 | 17 | var end = false; 18 | var done = false; 19 | var from = false; 20 | 21 | var lastId = false; 22 | var prevLastId = false; 23 | 24 | var fetcher = new Fetcher(config.watch); 25 | 26 | var fetch = () => { 27 | fetcher.import = true; 28 | 29 | if (lastId) { 30 | var tidAsTimestamp = lastId / 1000000; 31 | fetcher.getTrades(tidAsTimestamp, handleFetch); 32 | } 33 | else 34 | fetcher.getTrades(from, handleFetch); 35 | } 36 | 37 | var handleFetch = (unk, trades) => { 38 | var last = moment.unix(_.last(trades).date); 39 | lastId = _.last(trades).tid 40 | 41 | if(last < from) { 42 | log.debug('Skipping data, they are before from date', last.format()); 43 | return fetch(); 44 | } 45 | 46 | if (last > end || lastId === prevLastId) { 47 | fetcher.emit('done'); 48 | 49 | var endUnix = end.unix(); 50 | trades = _.filter( 51 | trades, 52 | t => t.date <= endUnix 53 | ) 54 | } 55 | 56 | prevLastId = lastId 57 | fetcher.emit('trades', trades); 58 | } 59 | 60 | module.exports = function (daterange) { 61 | 62 | from = daterange.from.clone(); 63 | end = daterange.to.clone(); 64 | 65 | return { 66 | bus: fetcher, 67 | fetch: fetch 68 | } 69 | } 70 | 71 | 72 | -------------------------------------------------------------------------------- /logs/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gekko", 3 | "version": "0.5.11", 4 | "description": "A bitcoin trading bot for auto trading at various exchanges", 5 | "keywords": [ 6 | "trading", 7 | "bot", 8 | "bitcoin", 9 | "TA", 10 | "finance" 11 | ], 12 | "scripts": { 13 | "test": "./node_modules/.bin/mocha test/*.js --recursive test -u tdd --reporter spec", 14 | "start": "node ./gekko --config config.js --ui" 15 | }, 16 | "author": "Mike van Rossum ", 17 | "dependencies": { 18 | "@slack/client": "^3.10.0", 19 | "async": "2.1.2", 20 | "binance": "^1.0.2", 21 | "bitcoin-co-id": "0.0.1", 22 | "bitexthai": "^0.1.0", 23 | "bitfinex-api-node": "^1.2.0", 24 | "bitstamp": "^1.0.3", 25 | "bitx": "^1.5.0", 26 | "btc-china-fork": "0.0.6", 27 | "btc-markets": "0.0.10", 28 | "cexio": "0.0.x", 29 | "co-fs": "^1.2.0", 30 | "commander": "^2.9.0", 31 | "gdax": "^0.4.2", 32 | "gekko": "0.0.9", 33 | "gemini-exchange-coffee-api": "2.0.6", 34 | "humanize-duration": "^3.10.0", 35 | "koa": "^1.2.0", 36 | "koa-bodyparser": "^2.2.0", 37 | "koa-cors": "0.0.16", 38 | "koa-logger": "^1.3.0", 39 | "koa-router": "^5.4.0", 40 | "koa-static": "^2.0.0", 41 | "kraken-api-es5": "^1.0.0", 42 | "lakebtc_nodejs": "0.1.x", 43 | "lodash": "2.x", 44 | "moment": "2.19.3", 45 | "node-wex": "^1.0.3", 46 | "node.bittrex.api": "^0.4.3", 47 | "okcoin-china": "0.0.7", 48 | "opn": "^4.0.2", 49 | "poloniex.js": "0.0.7", 50 | "promisify-node": "^0.4.0", 51 | "prompt-lite": "0.1.1", 52 | "pushbullet": "1.4.3", 53 | "quadrigacx": "0.0.7", 54 | "relieve": "^2.1.3", 55 | "retry": "^0.10.1", 56 | "semver": "5.4.1", 57 | "sqlite3": "^3.1.8", 58 | "stats-lite": "^2.0.4", 59 | "tiny-promisify": "^0.1.1", 60 | "toml": "^2.3.0", 61 | "twitter": "^1.7.1", 62 | "zaif.jp": "^0.1.4" 63 | }, 64 | "devDependencies": { 65 | "chai": "^2.0.0", 66 | "mocha": "^2.1.1", 67 | "proxyquire": "^1.7.10", 68 | "sinon": "^1.12.2" 69 | }, 70 | "engines": { 71 | "node": ">=6.0" 72 | }, 73 | "license": "MIT", 74 | "repository": { 75 | "type": "git", 76 | "url": "https://github.com/askmike/gekko.git" 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /plugins/adviceLogger.js: -------------------------------------------------------------------------------- 1 | var log = require('../core/log'); 2 | var moment = require('moment'); 3 | var _ = require('lodash'); 4 | var util = require('../core/util.js'); 5 | var config = util.getConfig(); 6 | var adviceLoggerConfig = config.adviceLogger; 7 | 8 | var Actor = function() { 9 | this.price = 'N/A'; 10 | this.marketTime = {format: function() {return 'N/A'}}; 11 | _.bindAll(this); 12 | } 13 | 14 | Actor.prototype.processCandle = function(candle, done) { 15 | this.price = candle.close; 16 | this.marketTime = candle.start; 17 | 18 | done(); 19 | }; 20 | 21 | Actor.prototype.processAdvice = function(advice) { 22 | if (adviceLoggerConfig.muteSoft && advice.recommendation == 'soft') return; 23 | console.log() 24 | log.info('We have new trading advice!'); 25 | log.info('\t Position:', advice.recommendation); 26 | log.info('\t Market price:', this.price); 27 | log.info('\t Based on market time:', this.marketTime.format('YYYY-MM-DD HH:mm:ss')); 28 | console.log() 29 | }; 30 | 31 | Actor.prototype.finalize = function(advice, done) { 32 | // todo 33 | done(); 34 | }; 35 | 36 | module.exports = Actor; 37 | -------------------------------------------------------------------------------- /plugins/ifttt.js: -------------------------------------------------------------------------------- 1 | const superagent = require('superagent'); 2 | const _ = require('lodash'); 3 | const log = require('../core/log.js'); 4 | const util = require('../core/util.js'); 5 | const config = util.getConfig(); 6 | const iftttConfig = config.ifttt; 7 | 8 | const IFTTT = function(done) { 9 | _.bindAll(this); 10 | this.ifttt; 11 | this.price = 'N/A'; 12 | this.done = done; 13 | this.setup(); 14 | }; 15 | 16 | IFTTT.prototype.setup = function(done) { 17 | var setupIFTTT = function () { 18 | if(iftttConfig.sendMessageOnStart){ 19 | var exchange = config.watch.exchange; 20 | var currency = config.watch.currency; 21 | var asset = config.watch.asset; 22 | var body = "Gekko has started, Ive started watching " 23 | +exchange 24 | +" " 25 | +currency 26 | +" " 27 | +asset 28 | +" I'll let you know when I got some advice"; 29 | this.send(body); 30 | }else{ 31 | log.debug('Skipping Send message on startup') 32 | } 33 | }; 34 | setupIFTTT.call(this) 35 | }; 36 | 37 | IFTTT.prototype.processCandle = function(candle, done) { 38 | this.price = candle.close; 39 | 40 | done(); 41 | }; 42 | 43 | IFTTT.prototype.portfolioUpdate = function(portfolio) { 44 | var message = "Gekko has detected a portfolio update. " + 45 | "Your current " + config.watch.currency + " balance is " + portfolio.currency + '.' + 46 | "Your current " + config.watch.exchange + " balance is " + portfolio.assert + '.'; 47 | this.send(message); 48 | } 49 | 50 | IFTTT.prototype.processAdvice = function(advice) { 51 | if (advice.recommendation == 'soft' && iftttConfig.muteSoft) return; 52 | 53 | const text = [ 54 | 'Gekko is watching ', 55 | config.watch.exchange, 56 | ' and has detected a new trend, advice is to go ', 57 | advice.recommendation, 58 | '.\n\nThe current ', 59 | config.watch.asset, 60 | ' price is ', 61 | this.price 62 | ].join(''); 63 | 64 | this.send(text); 65 | }; 66 | 67 | IFTTT.prototype.send = function(content) { 68 | superagent. 69 | post('https://maker.ifttt.com/trigger/' + iftttConfig.eventName + '/with/key/' + iftttConfig.makerKey) 70 | .send({value1: content}) 71 | .end(function(err, res){ 72 | if(err || !res){ 73 | log.error('IFTTT ERROR:', error) 74 | }else{ 75 | log.info('IFTTT Message Sent') 76 | } 77 | }); 78 | }; 79 | 80 | IFTTT.prototype.checkResults = function(error) { 81 | if (error) { 82 | log.warn('error sending IFTTT', error); 83 | } else { 84 | log.info('Send advice via IFTTT.'); 85 | } 86 | }; 87 | 88 | module.exports = IFTTT; 89 | -------------------------------------------------------------------------------- /plugins/mongodb/handle.js: -------------------------------------------------------------------------------- 1 | var mongojs = require('mongojs'); 2 | var mongoUtil = require('./util'); 3 | 4 | var util = require('../../core/util.js'); 5 | var config = util.getConfig(); 6 | var dirs = util.dirs(); 7 | 8 | // verify the correct dependencies are installed 9 | var pluginHelper = require(`${dirs.core}pluginUtil`); 10 | var pluginMock = { 11 | slug: 'mongodb adapter', 12 | dependencies: config.mongodb.dependencies 13 | } 14 | 15 | // exit if plugin couldn't be loaded 16 | var cannotLoad = pluginHelper.cannotLoad(pluginMock); 17 | if (cannotLoad) { 18 | util.die(cannotLoad); 19 | } 20 | 21 | var mode = util.gekkoMode(); 22 | 23 | var collections = [ 24 | mongoUtil.settings.historyCollection, 25 | mongoUtil.settings.adviceCollection 26 | ] 27 | 28 | var connection = mongojs(config.mongodb.connectionString, collections); 29 | var collection = connection.collection(mongoUtil.settings.historyCollection); 30 | 31 | if (mode === 'backtest') { 32 | var pair = mongoUtil.settings.pair.join('_'); 33 | 34 | collection.find({ pair }).toArray((err, docs) => { // check if we've got any records 35 | if (err) { 36 | util.die(err); 37 | } 38 | if (docs.length === 0) { 39 | util.die(`History table for ${config.watch.exchange} with pair ${pair} is empty.`); 40 | } 41 | }) 42 | } 43 | 44 | if(mongoUtil.settings.exchange) { 45 | collection.createIndex({start: 1, pair: 1}, {unique: true}); // create unique index on "time" and "pair" 46 | } 47 | module.exports = connection; 48 | -------------------------------------------------------------------------------- /plugins/mongodb/scanner.js: -------------------------------------------------------------------------------- 1 | const async = require('async'); 2 | var _ = require('lodash'); 3 | var util = require('../../core/util.js'); 4 | var log = require(`${util.dirs().core}log`); 5 | 6 | var handle = require('./handle'); 7 | 8 | module.exports = done => { 9 | this.db = handle; 10 | 11 | let markets = []; 12 | async.waterfall([ 13 | (cb) => { 14 | handle.getCollectionNames(cb) 15 | }, 16 | (collections, cb) => { 17 | async.each(collections, (collection, cb) => { 18 | let [exchange, type] = collection.split('_'); 19 | if (type === 'candles') { 20 | handle.collection(collection).distinct('pair', {}, (err, pairs) => { 21 | console.log(exchange); 22 | pairs.forEach((pair) => { 23 | pair = pair.split('_'); 24 | markets.push({ 25 | exchange: exchange, 26 | currency: _.first(pair), 27 | asset: _.last(pair) 28 | }); 29 | }); 30 | cb(); 31 | }) 32 | } else { 33 | cb(); 34 | } 35 | }, () => { 36 | cb(null, markets) 37 | }) 38 | } 39 | ], done) 40 | } -------------------------------------------------------------------------------- /plugins/mongodb/util.js: -------------------------------------------------------------------------------- 1 | var config = require('../../core/util.js').getConfig(); 2 | 3 | var watch = config.watch; 4 | var exchangeLowerCase = watch ? watch.exchange.toLowerCase() : watch = {}; // Do not crash on this, not needed to read from db 5 | 6 | var settings = { 7 | exchange: watch.exchange, 8 | pair: [watch.currency, watch.asset], 9 | historyCollection: `${exchangeLowerCase}_candles`, 10 | adviceCollection: `${exchangeLowerCase}_advices` 11 | }; 12 | 13 | module.exports = { 14 | settings 15 | }; 16 | -------------------------------------------------------------------------------- /plugins/performanceAnalyzer/cpRelay.js: -------------------------------------------------------------------------------- 1 | // relay paper trade results using cp 2 | 3 | const _ = require('lodash'); 4 | const moment = require('moment'); 5 | 6 | const util = require('../../core/util.js'); 7 | const dirs = util.dirs(); 8 | const mode = util.gekkoMode(); 9 | const log = require(dirs.core + 'log'); 10 | const cp = require(dirs.core + 'cp'); 11 | 12 | const Relay = function() {} 13 | 14 | Relay.prototype.handleTrade = function(trade, report) { 15 | cp.trade(trade); 16 | cp.report(report); 17 | } 18 | 19 | Relay.prototype.handleRoundtrip = function(rt) { 20 | cp.roundtrip(rt); 21 | } 22 | 23 | Relay.prototype.finalize = function(report) { 24 | cp.report(report); 25 | } 26 | 27 | 28 | module.exports = Relay; -------------------------------------------------------------------------------- /plugins/postgresql/util.js: -------------------------------------------------------------------------------- 1 | var config = require('../../core/util.js').getConfig(); 2 | 3 | var watch = config.watch; 4 | if(watch) { 5 | var settings = { 6 | exchange: watch.exchange, 7 | pair: [watch.currency, watch.asset] 8 | } 9 | } 10 | 11 | /** 12 | * Returns true if we use single database where 13 | * all our tables are stored. The default is to store 14 | * every exchange into it's own db. 15 | * 16 | * Set config.postgresql.database to use single db setup 17 | */ 18 | function useSingleDatabase() { 19 | return !!config.postgresql.database; 20 | } 21 | 22 | /** 23 | * Postgres has tables in lowercase if you don't 24 | * escape their names. Which we don't and so let's 25 | * just lowercase them. 26 | */ 27 | function useLowerCaseTableNames() { 28 | return !config.postgresql.noLowerCaseTableName; 29 | } 30 | 31 | module.exports = { 32 | settings: settings, 33 | 34 | // true if we have single db setup (see postrgesql.database config key) 35 | useSingleDatabase: useSingleDatabase, 36 | 37 | // returns DB name (depends on single db setup) 38 | database: function () { 39 | return useSingleDatabase() ? 40 | config.postgresql.database : 41 | config.watch.exchange.toLowerCase(); 42 | }, 43 | 44 | // returns table name which can be different if we use 45 | // single or multiple db setup. 46 | table: function (name) { 47 | if (useSingleDatabase()) { 48 | name = watch.exchange + '_' + name; 49 | } 50 | var fullName = [name, settings.pair.join('_')].join('_'); 51 | return useLowerCaseTableNames() ? fullName.toLowerCase() : fullName; 52 | }, 53 | 54 | // postgres schema name. defaults to 'public' 55 | schema: function () { 56 | return config.postgresql.schema ? config.postgresql.schema : 'public'; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /plugins/postgresql/writer.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | var config = require('../../core/util.js').getConfig(); 3 | 4 | var handle = require('./handle'); 5 | var postgresUtil = require('./util'); 6 | 7 | var Store = function(done, pluginMeta) { 8 | _.bindAll(this); 9 | this.done = done; 10 | 11 | this.db = handle; 12 | this.upsertTables(); 13 | 14 | this.cache = []; 15 | } 16 | 17 | Store.prototype.upsertTables = function() { 18 | var createQueries = [ 19 | `CREATE TABLE IF NOT EXISTS 20 | ${postgresUtil.table('candles')} ( 21 | id BIGSERIAL PRIMARY KEY, 22 | start integer UNIQUE, 23 | open double precision NOT NULL, 24 | high double precision NOT NULL, 25 | low double precision NOT NULL, 26 | close double precision NOT NULL, 27 | vwp double precision NOT NULL, 28 | volume double precision NOT NULL, 29 | trades INTEGER NOT NULL 30 | );` 31 | ]; 32 | 33 | var next = _.after(_.size(createQueries), this.done); 34 | 35 | _.each(createQueries, function(q) { 36 | this.db.query(q,next); 37 | }, this); 38 | } 39 | 40 | Store.prototype.writeCandles = function() { 41 | if(_.isEmpty(this.cache)){ 42 | return; 43 | } 44 | 45 | var stmt = ` 46 | INSERT INTO ${postgresUtil.table('candles')} 47 | (start, open, high,low, close, vwp, volume, trades) 48 | values($1, $2, $3, $4, $5, $6, $7, $8) ON CONFLICT DO NOTHING; 49 | `; 50 | 51 | _.each(this.cache, candle => { 52 | this.db.query(stmt,[ 53 | candle.start.unix(), 54 | candle.open, 55 | candle.high, 56 | candle.low, 57 | candle.close, 58 | candle.vwp, 59 | candle.volume, 60 | candle.trades 61 | ]); 62 | }); 63 | 64 | this.cache = []; 65 | } 66 | 67 | var processCandle = function(candle, done) { 68 | this.cache.push(candle); 69 | if (this.cache.length > 100) 70 | this.writeCandles(); 71 | 72 | done(); 73 | }; 74 | 75 | var finalize = function(done) { 76 | this.writeCandles(); 77 | this.db = null; 78 | done(); 79 | } 80 | 81 | if(config.candleWriter.enabled) { 82 | Store.prototype.processCandle = processCandle; 83 | Store.prototype.finalize = finalize; 84 | } 85 | 86 | module.exports = Store; 87 | -------------------------------------------------------------------------------- /plugins/pushover.js: -------------------------------------------------------------------------------- 1 | var push = require( 'pushover-notifications' ); 2 | var _ = require('lodash'); 3 | var log = require('../core/log.js'); 4 | var util = require('../core/util.js'); 5 | var config = util.getConfig(); 6 | var pushoverConfig = config.pushover; 7 | 8 | var Pushover = function() { 9 | _.bindAll(this); 10 | 11 | this.p; 12 | this.price = 'N/A'; 13 | 14 | this.setup(); 15 | } 16 | 17 | Pushover.prototype.setup = function() { 18 | var setupPushover = function() { 19 | this.p = new push( { 20 | user: pushoverConfig.user, 21 | token: pushoverConfig.key, 22 | }); 23 | 24 | if(pushoverConfig.sendPushoverOnStart) { 25 | this.send( 26 | "Gekko has started", 27 | [ 28 | "I've just started watching ", 29 | config.watch.exchange, 30 | ' ', 31 | config.watch.currency, 32 | '/', 33 | config.watch.asset, 34 | ". I'll let you know when I got some advice" 35 | ].join('') 36 | ); 37 | } else 38 | log.debug('Setup pushover adviser.'); 39 | } 40 | setupPushover.call(this); 41 | } 42 | 43 | Pushover.prototype.send = function(subject, content) { 44 | var msg = { 45 | // These values correspond to the parameters detailed on https://pushover.net/api 46 | // 'message' is required. All other values are optional. 47 | message: content, 48 | title: pushoverConfig.tag + subject, 49 | device: 'devicename', 50 | priority: 1 51 | }; 52 | 53 | this.p.send( msg, function( err, result ) { 54 | if ( err ) { 55 | throw err; 56 | } 57 | 58 | console.log( result ); 59 | }); 60 | 61 | } 62 | 63 | Pushover.prototype.processCandle = function(candle, callback) { 64 | this.price = candle.close; 65 | callback(); 66 | } 67 | 68 | Pushover.prototype.processAdvice = function(advice) { 69 | if (advice.recommendation == 'soft' && pushoverConfig.muteSoft) return; 70 | var text = [ 71 | advice.recommendation, 72 | this.price 73 | ].join(' '); 74 | var subject = text; 75 | this.send(subject, text); 76 | } 77 | 78 | Pushover.prototype.checkResults = function(err) { 79 | if(err) 80 | log.warn('error sending pushover', err); 81 | else 82 | log.info('Send advice via pushover.'); 83 | } 84 | 85 | module.exports = Pushover; 86 | -------------------------------------------------------------------------------- /plugins/redisBeacon.js: -------------------------------------------------------------------------------- 1 | var log = require('../core/log.js'); 2 | var util = require('../core/util'); 3 | var config = util.getConfig(); 4 | var redisBeacon = config.redisBeacon; 5 | var watch = config.watch; 6 | 7 | var subscriptions = require('../subscriptions'); 8 | var _ = require('lodash'); 9 | 10 | var redis = require("redis"); 11 | 12 | var Actor = function(done) { 13 | _.bindAll(this); 14 | 15 | this.market = [ 16 | watch.exchange, 17 | watch.currency, 18 | watch.asset 19 | ].join('-'); 20 | 21 | this.init(done); 22 | } 23 | 24 | // This actor is dynamically build based on 25 | // what the config specifies it should emit. 26 | // 27 | // This way we limit overhead because Gekko 28 | // only binds to events redis is going to 29 | // emit. 30 | 31 | var proto = {}; 32 | _.each(redisBeacon.broadcast, function(e) { 33 | // grab the corresponding subscription 34 | var subscription = _.find(subscriptions, function(s) { return s.event === e }); 35 | 36 | if(!subscription) 37 | util.die('Gekko does not know this event:' + e); 38 | 39 | var channel = redisBeacon.channelPrefix + subscription.event 40 | 41 | proto[subscription.handler] = function(message, cb) { 42 | if(!_.isFunction(cb)) 43 | cb = _.noop; 44 | 45 | this.emit(channel, { 46 | market: this.market, 47 | data: message 48 | }, cb); 49 | }; 50 | 51 | }, this) 52 | 53 | Actor.prototype = proto; 54 | 55 | Actor.prototype.init = function(done) { 56 | this.client = redis.createClient(redisBeacon.port, redisBeacon.host); 57 | this.client.on('ready', _.once(done)); 58 | } 59 | 60 | Actor.prototype.emit = function(channel, message) { 61 | log.debug('Going to publish to redis channel:', channel); 62 | 63 | var data = JSON.stringify(message); 64 | this.client.publish(channel, data); 65 | } 66 | 67 | module.exports = Actor; -------------------------------------------------------------------------------- /plugins/slack.js: -------------------------------------------------------------------------------- 1 | const WebClient = require('@slack/client').WebClient; 2 | const _ = require('lodash'); 3 | const log = require('../core/log.js'); 4 | const util = require('../core/util.js'); 5 | const config = util.getConfig(); 6 | const slackConfig = config.slack; 7 | 8 | const Slack = function(done) { 9 | _.bindAll(this); 10 | 11 | this.slack; 12 | this.price = 'N/A'; 13 | 14 | this.done = done; 15 | this.setup(); 16 | }; 17 | 18 | Slack.prototype.setup = function(done) { 19 | this.slack = new WebClient(slackConfig.token); 20 | 21 | const setupSlack = function(error, result) { 22 | if(slackConfig.sendMessageOnStart){ 23 | const exchange = config.watch.exchange; 24 | const currency = config.watch.currency; 25 | const asset = config.watch.asset; 26 | const body = 'Gekko has started, Ive started watching ' 27 | +exchange 28 | +' ' 29 | +currency 30 | +' ' 31 | +asset 32 | +' I\'ll let you know when I got some advice'; 33 | this.send(body); 34 | }else{ 35 | log.debug('Skipping Send message on startup') 36 | } 37 | }; 38 | setupSlack.call(this) 39 | }; 40 | 41 | Slack.prototype.processCandle = function(candle, done) { 42 | this.price = candle.close; 43 | 44 | done(); 45 | }; 46 | 47 | Slack.prototype.processAdvice = function(advice) { 48 | if (advice.recommendation == 'soft' && slackConfig.muteSoft) return; 49 | 50 | const text = [ 51 | 'Gekko is watching ', 52 | config.watch.exchange, 53 | ' and has detected a new trend, advice is to go ', 54 | advice.recommendation, 55 | '.\n\nThe current ', 56 | config.watch.asset, 57 | ' price is ', 58 | this.price 59 | ].join(''); 60 | 61 | this.send(text); 62 | }; 63 | 64 | Slack.prototype.send = function(content, done) { 65 | this.slack.chat.postMessage(slackConfig.channel, content, (error, response) => { 66 | if (error || !response) { 67 | log.error('Slack ERROR:', error) 68 | } else { 69 | log.info('Slack Message Sent') 70 | } 71 | }); 72 | }; 73 | 74 | Slack.prototype.checkResults = function(error) { 75 | if (error) { 76 | log.warn('error sending slack', error); 77 | } else { 78 | log.info('Send advice via slack.'); 79 | } 80 | }; 81 | 82 | module.exports = Slack; -------------------------------------------------------------------------------- /plugins/sqlite/handle.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | var fs = require('fs'); 3 | 4 | var util = require('../../core/util.js'); 5 | var config = util.getConfig(); 6 | var dirs = util.dirs(); 7 | 8 | var adapter = config.sqlite; 9 | 10 | // verify the correct dependencies are installed 11 | var pluginHelper = require(dirs.core + 'pluginUtil'); 12 | var pluginMock = { 13 | slug: 'sqlite adapter', 14 | dependencies: adapter.dependencies 15 | }; 16 | 17 | var cannotLoad = pluginHelper.cannotLoad(pluginMock); 18 | if(cannotLoad) 19 | util.die(cannotLoad); 20 | 21 | // should be good now 22 | if(config.debug) 23 | var sqlite3 = require('sqlite3').verbose(); 24 | else 25 | var sqlite3 = require('sqlite3'); 26 | 27 | var plugins = require(util.dirs().gekko + 'plugins'); 28 | 29 | var version = adapter.version; 30 | 31 | var dbName = config.watch.exchange.toLowerCase() + '_' + version + '.db'; 32 | var dir = dirs.gekko + adapter.dataDirectory; 33 | 34 | var fullPath = [dir, dbName].join('/'); 35 | 36 | var mode = util.gekkoMode(); 37 | if(mode === 'realtime' || mode === 'importer') { 38 | 39 | if(!fs.existsSync(dir)) 40 | fs.mkdirSync(dir); 41 | 42 | 43 | } else if(mode === 'backtest') { 44 | 45 | if(!fs.existsSync(dir)) 46 | util.die('History directory does not exist.'); 47 | 48 | if(!fs.existsSync(fullPath)) 49 | util.die(`History database does not exist for exchange ${config.watch.exchange} at version ${version}.`); 50 | } 51 | 52 | var journalMode = config.sqlite.journalMode || 'PERSIST'; 53 | var syncMode = journalMode === 'WAL' ? 'NORMAL' : 'FULL'; 54 | 55 | var db = new sqlite3.Database(fullPath); 56 | db.run('PRAGMA synchronous = ' + syncMode); 57 | db.run('PRAGMA journal_mode = ' + journalMode); 58 | db.configure('busyTimeout', 1500); 59 | 60 | module.exports = db; 61 | -------------------------------------------------------------------------------- /plugins/sqlite/scanner.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const async = require('async'); 3 | const fs = require('fs'); 4 | 5 | const util = require('../../core/util.js'); 6 | const config = util.getConfig(); 7 | const dirs = util.dirs(); 8 | 9 | const sqlite3 = require('sqlite3'); 10 | 11 | // todo: rewrite with generators or async/await.. 12 | module.exports = done => { 13 | const dbDirectory = dirs.gekko + config.sqlite.dataDirectory 14 | 15 | if(!fs.existsSync(dbDirectory)) 16 | return done(null, []); 17 | 18 | const files = fs.readdirSync(dbDirectory); 19 | 20 | const dbs = files 21 | .filter(f => { 22 | let parts = f.split('.'); 23 | if(_.last(parts) === 'db') 24 | return true; 25 | }) 26 | 27 | if(!_.size(dbs)) 28 | return done(null, []); 29 | 30 | let markets = []; 31 | 32 | async.each(dbs, (db, next) => { 33 | 34 | const exchange = _.first(db.split('_')); 35 | const handle = new sqlite3.Database(dbDirectory + '/' + db, sqlite3.OPEN_READONLY, err => { 36 | if(err) 37 | return next(err); 38 | 39 | handle.all(`SELECT name FROM sqlite_master WHERE type='table'`, (err, tables) => { 40 | if(err) 41 | return next(err); 42 | 43 | _.each(tables, table => { 44 | let parts = table.name.split('_'); 45 | let first = parts.shift(); 46 | if(first === 'candles') 47 | markets.push({ 48 | exchange: exchange, 49 | currency: _.first(parts), 50 | asset: _.last(parts) 51 | }); 52 | }); 53 | 54 | next(); 55 | }); 56 | }); 57 | 58 | 59 | }, 60 | // got all tables! 61 | err => { 62 | done(err, markets); 63 | }); 64 | } -------------------------------------------------------------------------------- /plugins/sqlite/util.js: -------------------------------------------------------------------------------- 1 | var config = require('../../core/util.js').getConfig(); 2 | 3 | var watch = config.watch; 4 | var settings = { 5 | exchange: watch.exchange, 6 | pair: [watch.currency, watch.asset], 7 | historyPath: config.sqlite.dataDirectory 8 | } 9 | 10 | module.exports = { 11 | settings: settings, 12 | table: function(name) { 13 | return [name, settings.pair.join('_')].join('_'); 14 | } 15 | } -------------------------------------------------------------------------------- /plugins/trader/trader.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | var util = require('../../core/util.js'); 3 | var config = util.getConfig(); 4 | var dirs = util.dirs(); 5 | 6 | var log = require(dirs.core + 'log'); 7 | var Manager = require('./portfolioManager'); 8 | 9 | var Trader = function(next) { 10 | _.bindAll(this); 11 | 12 | this.manager = new Manager(_.extend(config.trader, config.watch)); 13 | this.manager.init(next); 14 | 15 | let sendPortfolio = false; 16 | 17 | this.manager.on('trade', trade => { 18 | 19 | if(!sendPortfolio && this.initialPortfolio) { 20 | this.emit('portfolioUpdate', this.initialPortfolio); 21 | sendPortfolio = true; 22 | } 23 | 24 | this.emit('trade', trade); 25 | }); 26 | 27 | this.manager.once('portfolioUpdate', portfolioUpdate => { 28 | this.initialPortfolio = portfolioUpdate; 29 | }) 30 | } 31 | 32 | // teach our trader events 33 | util.makeEventEmitter(Trader); 34 | 35 | Trader.prototype.processCandle = (candle, done) => done(); 36 | 37 | Trader.prototype.processAdvice = function(advice) { 38 | if(advice.recommendation == 'long') { 39 | log.info( 40 | 'Trader', 41 | 'Received advice to go long.', 42 | 'Buying ', config.trader.asset 43 | ); 44 | this.manager.trade('BUY'); 45 | } else if(advice.recommendation == 'short') { 46 | log.info( 47 | 'Trader', 48 | 'Received advice to go short.', 49 | 'Selling ', config.trader.asset 50 | ); 51 | this.manager.trade('SELL'); 52 | } 53 | } 54 | 55 | module.exports = Trader; 56 | -------------------------------------------------------------------------------- /plugins/twitter.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | var log = require('../core/log.js'); 3 | var util = require('../core/util.js'); 4 | var config = util.getConfig(); 5 | var twitterConfig = config.twitter; 6 | var TwitterApi = require('twitter'); 7 | 8 | require('dotenv').config() 9 | 10 | var Twitter = function(done) { 11 | _.bindAll(this); 12 | 13 | this.twitter; 14 | this.price = 'N/A'; 15 | this.done = done; 16 | this.setup(); 17 | }; 18 | 19 | Twitter.prototype.setup = function(done){ 20 | var setupTwitter = function (err, result) { 21 | this.client = new TwitterApi({ 22 | consumer_key: config.consumer_key, 23 | consumer_secret: config.consumer_secret, 24 | access_token_key: config.access_token_key, 25 | access_token_secret: config.access_token_secret 26 | }); 27 | 28 | if(twitterConfig.sendMessageOnStart){ 29 | var exchange = config.watch.exchange; 30 | var currency = config.watch.currency; 31 | var asset = config.watch.asset; 32 | var body = "Watching " 33 | +exchange 34 | +" " 35 | +currency 36 | +" " 37 | +asset 38 | this.mail(body); 39 | }else{ 40 | log.debug('Skipping Send message on startup') 41 | } 42 | }; 43 | setupTwitter.call(this) 44 | }; 45 | 46 | Twitter.prototype.processCandle = function(candle, done) { 47 | this.price = candle.close; 48 | 49 | done(); 50 | }; 51 | 52 | Twitter.prototype.processAdvice = function(advice) { 53 | if (advice.recommendation == "soft" && twitterConfig.muteSoft) return; 54 | var text = [ 55 | 'New #ethereum trend. Attempting to ', 56 | advice.recommendation == "short" ? "sell" : "buy", 57 | ' @', 58 | this.price, 59 | ].join(''); 60 | 61 | this.mail(text); 62 | }; 63 | 64 | Twitter.prototype.mail = function(content, done) { 65 | log.info("trying to tweet"); 66 | this.client.post('statuses/update', {status: content}, function(error, tweet, response) { 67 | if(error || !response) { 68 | log.error('Pushbullet ERROR:', error) 69 | } else if(response && response.active){ 70 | log.info('Pushbullet Message Sent') 71 | } 72 | }); 73 | }; 74 | 75 | Twitter.prototype.checkResults = function(err) { 76 | if(err) 77 | log.warn('error sending email', err); 78 | else 79 | log.info('Send advice via email.'); 80 | }; 81 | 82 | module.exports = Twitter; 83 | -------------------------------------------------------------------------------- /plugins/webserver.js: -------------------------------------------------------------------------------- 1 | var log = require('../core/log'); 2 | var moment = require('moment'); 3 | var _ = require('lodash'); 4 | var Server = require('../web/server.js'); 5 | 6 | var Actor = function(next) { 7 | _.bindAll(this); 8 | 9 | this.server = new Server(); 10 | this.server.setup(next); 11 | } 12 | 13 | Actor.prototype.init = function(data) { 14 | this.server.broadcastHistory(data); 15 | }; 16 | 17 | Actor.prototype.processCandle = function(candle, next) { 18 | this.server.broadcastCandle(candle); 19 | 20 | next(); 21 | }; 22 | 23 | Actor.prototype.processAdvice = function(advice) { 24 | this.server.broadcastAdvice(advice); 25 | }; 26 | 27 | module.exports = Actor; -------------------------------------------------------------------------------- /strategies/DEMA.js: -------------------------------------------------------------------------------- 1 | // helpers 2 | var _ = require('lodash'); 3 | var log = require('../core/log.js'); 4 | 5 | // let's create our own method 6 | var method = {}; 7 | 8 | // prepare everything our method needs 9 | method.init = function() { 10 | this.name = 'DEMA'; 11 | 12 | this.currentTrend; 13 | this.requiredHistory = this.tradingAdvisor.historySize; 14 | 15 | // define the indicators we need 16 | this.addIndicator('dema', 'DEMA', this.settings); 17 | } 18 | 19 | // what happens on every new candle? 20 | method.update = function(candle) { 21 | // nothing! 22 | } 23 | 24 | // for debugging purposes: log the last calculated 25 | // EMAs and diff. 26 | method.log = function() { 27 | var dema = this.indicators.dema; 28 | 29 | log.debug('calculated DEMA properties for candle:'); 30 | log.debug('\t', 'long ema:', dema.long.result.toFixed(8)); 31 | log.debug('\t', 'short ema:', dema.short.result.toFixed(8)); 32 | log.debug('\t diff:', dema.result.toFixed(5)); 33 | log.debug('\t DEMA age:', dema.short.age, 'candles'); 34 | } 35 | 36 | method.check = function(candle) { 37 | var dema = this.indicators.dema; 38 | var diff = dema.result; 39 | var price = candle.close; 40 | 41 | var message = '@ ' + price.toFixed(8) + ' (' + diff.toFixed(5) + ')'; 42 | 43 | if(diff > this.settings.thresholds.up) { 44 | log.debug('we are currently in uptrend', message); 45 | 46 | if(this.currentTrend !== 'up') { 47 | this.currentTrend = 'up'; 48 | this.advice('long'); 49 | } else 50 | this.advice(); 51 | 52 | } else if(diff < this.settings.thresholds.down) { 53 | log.debug('we are currently in a downtrend', message); 54 | 55 | if(this.currentTrend !== 'down') { 56 | this.currentTrend = 'down'; 57 | this.advice('short'); 58 | } else 59 | this.advice(); 60 | 61 | } else { 62 | log.debug('we are currently not in an up or down trend', message); 63 | this.advice(); 64 | } 65 | } 66 | 67 | module.exports = method; 68 | -------------------------------------------------------------------------------- /strategies/TSI.js: -------------------------------------------------------------------------------- 1 | // helpers 2 | var _ = require('lodash'); 3 | var log = require('../core/log.js'); 4 | 5 | var TSI = require('./indicators/TSI.js'); 6 | 7 | // let's create our own method 8 | var method = {}; 9 | 10 | // prepare everything our method needs 11 | method.init = function() { 12 | this.name = 'TSI'; 13 | 14 | this.trend = { 15 | direction: 'none', 16 | duration: 0, 17 | persisted: false, 18 | adviced: false 19 | }; 20 | 21 | this.requiredHistory = this.tradingAdvisor.historySize; 22 | 23 | // define the indicators we need 24 | this.addIndicator('tsi', 'TSI', this.settings); 25 | } 26 | 27 | // for debugging purposes log the last 28 | // calculated parameters. 29 | method.log = function(candle) { 30 | var digits = 8; 31 | var tsi = this.indicators.tsi; 32 | 33 | log.debug('calculated Ultimate Oscillator properties for candle:'); 34 | log.debug('\t', 'tsi:', tsi.tsi.toFixed(digits)); 35 | log.debug('\t', 'price:', candle.close.toFixed(digits)); 36 | } 37 | 38 | method.check = function() { 39 | var tsi = this.indicators.tsi; 40 | var tsiVal = tsi.tsi; 41 | 42 | if(tsiVal > this.settings.thresholds.high) { 43 | 44 | // new trend detected 45 | if(this.trend.direction !== 'high') 46 | this.trend = { 47 | duration: 0, 48 | persisted: false, 49 | direction: 'high', 50 | adviced: false 51 | }; 52 | 53 | this.trend.duration++; 54 | 55 | log.debug('In high since', this.trend.duration, 'candle(s)'); 56 | 57 | if(this.trend.duration >= this.settings.thresholds.persistence) 58 | this.trend.persisted = true; 59 | 60 | if(this.trend.persisted && !this.trend.adviced) { 61 | this.trend.adviced = true; 62 | this.advice('short'); 63 | } else 64 | this.advice(); 65 | 66 | } else if(tsiVal < this.settings.thresholds.low) { 67 | 68 | // new trend detected 69 | if(this.trend.direction !== 'low') 70 | this.trend = { 71 | duration: 0, 72 | persisted: false, 73 | direction: 'low', 74 | adviced: false 75 | }; 76 | 77 | this.trend.duration++; 78 | 79 | log.debug('In low since', this.trend.duration, 'candle(s)'); 80 | 81 | if(this.trend.duration >= this.settings.thresholds.persistence) 82 | this.trend.persisted = true; 83 | 84 | if(this.trend.persisted && !this.trend.adviced) { 85 | this.trend.adviced = true; 86 | this.advice('long'); 87 | } else 88 | this.advice(); 89 | 90 | } else { 91 | 92 | log.debug('In no trend'); 93 | 94 | this.advice(); 95 | } 96 | } 97 | 98 | module.exports = method; 99 | -------------------------------------------------------------------------------- /strategies/UO.js: -------------------------------------------------------------------------------- 1 | // helpers 2 | var _ = require('lodash'); 3 | var log = require('../core/log.js'); 4 | 5 | var UO = require('./indicators/UO.js'); 6 | 7 | // let's create our own method 8 | var method = {}; 9 | 10 | // prepare everything our method needs 11 | method.init = function() { 12 | this.name = 'UO'; 13 | 14 | this.trend = { 15 | direction: 'none', 16 | duration: 0, 17 | persisted: false, 18 | adviced: false 19 | }; 20 | 21 | this.requiredHistory = this.tradingAdvisor.historySize; 22 | 23 | // define the indicators we need 24 | this.addIndicator('uo', 'UO', this.settings); 25 | } 26 | 27 | // for debugging purposes log the last 28 | // calculated parameters. 29 | method.log = function(candle) { 30 | var digits = 8; 31 | var uo = this.indicators.uo; 32 | 33 | log.debug('calculated Ultimate Oscillator properties for candle:'); 34 | log.debug('\t', 'UO:', uo.uo.toFixed(digits)); 35 | log.debug('\t', 'price:', candle.close.toFixed(digits)); 36 | } 37 | 38 | method.check = function() { 39 | var uo = this.indicators.uo; 40 | var uoVal = uo.uo; 41 | 42 | if(uoVal > this.settings.thresholds.high) { 43 | 44 | // new trend detected 45 | if(this.trend.direction !== 'high') 46 | this.trend = { 47 | duration: 0, 48 | persisted: false, 49 | direction: 'high', 50 | adviced: false 51 | }; 52 | 53 | this.trend.duration++; 54 | 55 | log.debug('In high since', this.trend.duration, 'candle(s)'); 56 | 57 | if(this.trend.duration >= this.settings.thresholds.persistence) 58 | this.trend.persisted = true; 59 | 60 | if(this.trend.persisted && !this.trend.adviced) { 61 | this.trend.adviced = true; 62 | this.advice('short'); 63 | } else 64 | this.advice(); 65 | 66 | } else if(uoVal < this.settings.thresholds.low) { 67 | 68 | // new trend detected 69 | if(this.trend.direction !== 'low') 70 | this.trend = { 71 | duration: 0, 72 | persisted: false, 73 | direction: 'low', 74 | adviced: false 75 | }; 76 | 77 | this.trend.duration++; 78 | 79 | log.debug('In low since', this.trend.duration, 'candle(s)'); 80 | 81 | if(this.trend.duration >= this.settings.thresholds.persistence) 82 | this.trend.persisted = true; 83 | 84 | if(this.trend.persisted && !this.trend.adviced) { 85 | this.trend.adviced = true; 86 | this.advice('long'); 87 | } else 88 | this.advice(); 89 | 90 | } else { 91 | 92 | log.debug('In no trend'); 93 | 94 | this.advice(); 95 | } 96 | } 97 | 98 | module.exports = method; 99 | -------------------------------------------------------------------------------- /strategies/custom.js: -------------------------------------------------------------------------------- 1 | // This is a basic example strategy for Gekko. 2 | // For more information on everything please refer 3 | // to this document: 4 | // 5 | // https://gekko.wizb.it/docs/strategies/creating_a_strategy.html 6 | // 7 | // The example below is pretty bad investment advice: on every new candle there is 8 | // a 10% chance it will recommend to change your position (to either 9 | // long or short). 10 | 11 | var log = require('../core/log'); 12 | 13 | // Let's create our own strat 14 | var strat = {}; 15 | 16 | // Prepare everything our method needs 17 | strat.init = function() { 18 | this.input = 'candle'; 19 | this.currentTrend = 'long'; 20 | this.requiredHistory = 0; 21 | } 22 | 23 | // What happens on every new candle? 24 | strat.update = function(candle) { 25 | 26 | // Get a random number between 0 and 1. 27 | this.randomNumber = Math.random(); 28 | 29 | // There is a 10% chance it is smaller than 0.1 30 | this.toUpdate = this.randomNumber < 0.1; 31 | } 32 | 33 | // For debugging purposes. 34 | strat.log = function() { 35 | log.debug('calculated random number:'); 36 | log.debug('\t', this.randomNumber.toFixed(3)); 37 | } 38 | 39 | // Based on the newly calculated 40 | // information, check if we should 41 | // update or not. 42 | strat.check = function() { 43 | 44 | // Only continue if we have a new update. 45 | if(!this.toUpdate) 46 | return; 47 | 48 | if(this.currentTrend === 'long') { 49 | 50 | // If it was long, set it to short 51 | this.currentTrend = 'short'; 52 | this.advice('short'); 53 | 54 | } else { 55 | 56 | // If it was short, set it to long 57 | this.currentTrend = 'long'; 58 | this.advice('long'); 59 | 60 | } 61 | } 62 | 63 | module.exports = strat; 64 | -------------------------------------------------------------------------------- /strategies/debug-advice.js: -------------------------------------------------------------------------------- 1 | var settings = { 2 | wait: 0, 3 | advice: 'short' 4 | }; 5 | 6 | // ------- 7 | 8 | var _ = require('lodash'); 9 | var log = require('../core/log.js'); 10 | 11 | var i = 0; 12 | 13 | var method = { 14 | init: _.noop, 15 | update: _.noop, 16 | log: _.noop, 17 | check: function() { 18 | 19 | log.info('iteration:', i); 20 | if(settings.wait === i) 21 | this.advice(settings.advice); 22 | 23 | i++ 24 | 25 | } 26 | }; 27 | 28 | module.exports = method; -------------------------------------------------------------------------------- /strategies/indicators/CCI.js: -------------------------------------------------------------------------------- 1 | /* 2 | * CCI 3 | */ 4 | var log = require('../../core/log'); 5 | var LRC = require('./LRC'); 6 | 7 | var Indicator = function(settings) { 8 | this.input = 'candle'; 9 | this.tp = 0.0; 10 | this.TP = new LRC(settings.history); 11 | this.result = false; 12 | this.hist = []; // needed for mean? 13 | this.mean = 0.0; 14 | this.size = 0; 15 | this.constant = settings.constant; 16 | this.maxSize = settings.history; 17 | for (var i = 0; i < this.maxSize; i++) 18 | this.hist.push(0.0); 19 | } 20 | 21 | Indicator.prototype.update = function(candle) { 22 | 23 | // We need sufficient history to get the right result. 24 | 25 | var tp = (candle.high + candle.close + candle.low) / 3; 26 | if (this.size < this.maxSize) { 27 | this.hist[this.size] = tp; 28 | this.size++; 29 | } else { 30 | for (var i = 0; i < this.maxSize-1; i++) { 31 | this.hist[i] = this.hist[i+1]; 32 | } 33 | this.hist[this.maxSize-1] = tp; 34 | } 35 | 36 | this.TP.update(tp); 37 | 38 | if (this.size < this.maxSize) { 39 | this.result = false; 40 | } else { 41 | this.calculate(tp); 42 | } 43 | } 44 | 45 | /* 46 | * Handle calculations 47 | */ 48 | Indicator.prototype.calculate = function(tp) { 49 | 50 | // calculate current TP 51 | 52 | var avgtp = this.TP.result; 53 | if (typeof(avgtp) == 'boolean') { 54 | log.error("Failed to get average tp from indicator."); 55 | return; 56 | } 57 | 58 | this.tp = tp; 59 | 60 | var sum = 0.0; 61 | // calculate tps 62 | for (var i = 0; i < this.size; i++) { 63 | 64 | var z = (this.hist[i] - avgtp); 65 | if (z < 0) z = z * -1.0; 66 | sum = sum + z; 67 | 68 | } 69 | 70 | this.mean = (sum / this.size); 71 | 72 | 73 | 74 | this.result = (this.tp - avgtp) / (this.constant * this.mean); 75 | 76 | // log.debug("===\t", this.mean, "\t", this.tp, '\t', this.TP.result, "\t", sum, "\t", avgtp, '\t', this.result.toFixed(2)); 77 | } 78 | 79 | module.exports = Indicator; 80 | -------------------------------------------------------------------------------- /strategies/indicators/DEMA.js: -------------------------------------------------------------------------------- 1 | // required indicators 2 | var EMA = require('./EMA.js'); 3 | 4 | var Indicator = function(config) { 5 | this.input = 'price' 6 | this.result = false; 7 | this.short = new EMA(config.short); 8 | this.long = new EMA(config.long); 9 | } 10 | 11 | // add a price and calculate the EMAs and 12 | // the diff for that price 13 | Indicator.prototype.update = function(price) { 14 | this.short.update(price); 15 | this.long.update(price); 16 | this.calculateEMAdiff(); 17 | } 18 | 19 | // @link https://github.com/virtimus/GoxTradingBot/blob/85a67d27b856949cf27440ae77a56d4a83e0bfbe/background.js#L145 20 | Indicator.prototype.calculateEMAdiff = function() { 21 | var shortEMA = this.short.result; 22 | var longEMA = this.long.result; 23 | 24 | this.result = 100 * (shortEMA - longEMA) / ((shortEMA + longEMA) / 2); 25 | } 26 | 27 | module.exports = Indicator; 28 | -------------------------------------------------------------------------------- /strategies/indicators/EMA.js: -------------------------------------------------------------------------------- 1 | // @link http://en.wikipedia.org/wiki/Exponential_moving_average#Exponential_moving_average 2 | 3 | var Indicator = function(weight) { 4 | this.input = 'price'; 5 | this.weight = weight; 6 | this.result = false; 7 | this.age = 0; 8 | } 9 | 10 | Indicator.prototype.update = function(price) { 11 | // The first time we can't calculate based on previous 12 | // ema, because we haven't calculated any yet. 13 | if(this.result === false) 14 | this.result = price; 15 | 16 | this.age++; 17 | this.calculate(price); 18 | 19 | return this.result; 20 | } 21 | 22 | // calculation (based on tick/day): 23 | // EMA = Price(t) * k + EMA(y) * (1 – k) 24 | // t = today, y = yesterday, N = number of days in EMA, k = 2 / (N+1) 25 | Indicator.prototype.calculate = function(price) { 26 | // weight factor 27 | var k = 2 / (this.weight + 1); 28 | 29 | // yesterday 30 | var y = this.result; 31 | 32 | // calculation 33 | this.result = price * k + y * (1 - k); 34 | } 35 | 36 | module.exports = Indicator; 37 | -------------------------------------------------------------------------------- /strategies/indicators/MACD.js: -------------------------------------------------------------------------------- 1 | // required indicators 2 | var EMA = require('./EMA.js'); 3 | 4 | var Indicator = function(config) { 5 | this.input = 'price'; 6 | this.diff = false; 7 | this.short = new EMA(config.short); 8 | this.long = new EMA(config.long); 9 | this.signal = new EMA(config.signal); 10 | } 11 | 12 | Indicator.prototype.update = function(price) { 13 | this.short.update(price); 14 | this.long.update(price); 15 | this.calculateEMAdiff(); 16 | this.signal.update(this.diff); 17 | this.result = this.diff - this.signal.result; 18 | } 19 | 20 | Indicator.prototype.calculateEMAdiff = function() { 21 | var shortEMA = this.short.result; 22 | var longEMA = this.long.result; 23 | 24 | this.diff = shortEMA - longEMA; 25 | } 26 | 27 | module.exports = Indicator; 28 | -------------------------------------------------------------------------------- /strategies/indicators/PPO.js: -------------------------------------------------------------------------------- 1 | // required indicators 2 | var EMA = require('./EMA.js'); 3 | 4 | var Indicator = function(config) { 5 | this.result = {}; 6 | this.input = 'price'; 7 | this.macd = 0; 8 | this.ppo = 0; 9 | this.short = new EMA(config.short); 10 | this.long = new EMA(config.long); 11 | this.MACDsignal = new EMA(config.signal); 12 | this.PPOsignal = new EMA(config.signal); 13 | } 14 | 15 | Indicator.prototype.update = function(price) { 16 | this.short.update(price); 17 | this.long.update(price); 18 | this.calculatePPO(); 19 | this.MACDsignal.update(this.result.macd); 20 | this.MACDhist = this.result.macd - this.MACDsignal.result; 21 | this.PPOsignal.update(this.result.ppo); 22 | this.PPOhist = this.result.ppo - this.PPOsignal.result; 23 | 24 | this.result.MACDsignal = this.MACDsignal.result; 25 | this.result.MACDhist = this.MACDhist; 26 | this.result.PPOsignal = this.PPOsignal.result; 27 | this.result.PPOhist = this.PPOhist; 28 | } 29 | 30 | Indicator.prototype.calculatePPO = function() { 31 | this.result.shortEMA = this.short.result; 32 | this.result.longEMA = this.long.result; 33 | this.result.macd = this.result.shortEMA - this.result.longEMA; 34 | this.result.ppo = 100 * (this.result.macd / this.result.longEMA); 35 | } 36 | 37 | module.exports = Indicator; 38 | -------------------------------------------------------------------------------- /strategies/indicators/RSI.js: -------------------------------------------------------------------------------- 1 | // required indicators 2 | var SMMA = require('./SMMA.js'); 3 | 4 | var Indicator = function (settings) { 5 | this.input = 'candle'; 6 | this.lastClose = null; 7 | this.weight = settings.interval; 8 | this.avgU = new SMMA(this.weight); 9 | this.avgD = new SMMA(this.weight); 10 | this.u = 0; 11 | this.d = 0; 12 | this.rs = 0; 13 | this.result = 0; 14 | this.age = 0; 15 | } 16 | 17 | Indicator.prototype.update = function (candle) { 18 | var currentClose = candle.close; 19 | 20 | if (this.lastClose === null) { 21 | // Set initial price to prevent invalid change calculation 22 | this.lastClose = currentClose; 23 | 24 | // Do not calculate RSI for this reason - there's no change! 25 | this.age++; 26 | return; 27 | } 28 | 29 | if (currentClose > this.lastClose) { 30 | this.u = currentClose - this.lastClose; 31 | this.d = 0; 32 | } else { 33 | this.u = 0; 34 | this.d = this.lastClose - currentClose; 35 | } 36 | 37 | this.avgU.update(this.u); 38 | this.avgD.update(this.d); 39 | 40 | this.rs = this.avgU.result / this.avgD.result; 41 | this.result = 100 - (100 / (1 + this.rs)); 42 | 43 | if (this.avgD.result === 0 && this.avgU.result !== 0) { 44 | this.result = 100; 45 | } else if (this.avgD.result === 0) { 46 | this.result = 0; 47 | } 48 | 49 | this.lastClose = currentClose; 50 | this.age++; 51 | } 52 | 53 | module.exports = Indicator; 54 | -------------------------------------------------------------------------------- /strategies/indicators/SMA.js: -------------------------------------------------------------------------------- 1 | // required indicators 2 | // Simple Moving Average - O(1) implementation 3 | 4 | var Indicator = function(windowLength) { 5 | this.input = 'price'; 6 | this.windowLength = windowLength; 7 | this.prices = []; 8 | this.result = 0; 9 | this.age = 0; 10 | this.sum = 0; 11 | } 12 | 13 | Indicator.prototype.update = function(price) { 14 | var tail = this.prices[this.age] || 0; // oldest price in window 15 | this.prices[this.age] = price; 16 | this.sum += price - tail; 17 | this.result = this.sum / this.prices.length; 18 | this.age = (this.age + 1) % this.windowLength 19 | } 20 | 21 | module.exports = Indicator; 22 | -------------------------------------------------------------------------------- /strategies/indicators/SMMA.js: -------------------------------------------------------------------------------- 1 | // required indicators 2 | var SMA = require('./SMA'); 3 | 4 | var Indicator = function (weight) { 5 | this.input = 'price'; 6 | this.sma = new SMA(weight); 7 | this.weight = weight; 8 | this.prices = []; 9 | this.result = 0; 10 | this.age = 0; 11 | } 12 | 13 | Indicator.prototype.update = function (price) { 14 | this.prices[this.age] = price; 15 | 16 | if(this.prices.length < this.weight) { 17 | this.sma.update(price); 18 | } else if(this.prices.length === this.weight) { 19 | this.sma.update(price); 20 | this.result = this.sma.result; 21 | } else { 22 | this.result = (this.result * (this.weight - 1) + price) / this.weight; 23 | } 24 | 25 | this.age++; 26 | } 27 | 28 | module.exports = Indicator; 29 | -------------------------------------------------------------------------------- /strategies/indicators/TSI.js: -------------------------------------------------------------------------------- 1 | // required indicators 2 | var EMA = require('./EMA.js'); 3 | 4 | var Indicator = function(settings) { 5 | this.input = 'candle'; 6 | this.lastClose = 0; 7 | this.tsi = 0; 8 | this.inner = new EMA(settings.long); 9 | this.outer = new EMA(settings.short); 10 | this.absoluteInner = new EMA(settings.long); 11 | this.absoluteOuter = new EMA(settings.short); 12 | } 13 | 14 | Indicator.prototype.update = function(candle) { 15 | var close = candle.close; 16 | var prevClose = this.lastClose; 17 | var momentum = close - prevClose; 18 | 19 | this.inner.update(momentum); 20 | this.outer.update(this.inner.result); 21 | 22 | this.absoluteInner.update(Math.abs(momentum)); 23 | this.absoluteOuter.update(this.absoluteInner.result); 24 | 25 | this.tsi = 100 * this.outer.result / this.absoluteOuter.result; 26 | 27 | this.lastClose = close; 28 | } 29 | 30 | module.exports = Indicator; 31 | -------------------------------------------------------------------------------- /strategies/indicators/UO.js: -------------------------------------------------------------------------------- 1 | // required indicators 2 | var SMA = require('./SMA.js'); 3 | 4 | var Indicator = function(settings) { 5 | this.input = 'candle'; 6 | this.lastClose = 0; 7 | this.uo = 0; 8 | this.firstWeight = settings.first.weight; 9 | this.secondWeight = settings.second.weight; 10 | this.thirdWeight = settings.third.weight; 11 | this.firstLow = new SMA(settings.first.period); 12 | this.firstHigh = new SMA(settings.first.period); 13 | this.secondLow = new SMA(settings.second.period); 14 | this.secondHigh = new SMA(settings.second.period); 15 | this.thirdLow = new SMA(settings.third.period); 16 | this.thirdHigh = new SMA(settings.third.period); 17 | } 18 | 19 | Indicator.prototype.update = function(candle) { 20 | var close = candle.close; 21 | var prevClose = this.lastClose; 22 | var low = candle.low; 23 | var high = candle.high; 24 | 25 | var bp = close - Math.min(low, prevClose); 26 | var tr = Math.max(high, prevClose) - Math.min(low, prevClose); 27 | 28 | this.firstLow.update(tr); 29 | this.secondLow.update(tr); 30 | this.thirdLow.update(tr); 31 | 32 | this.firstHigh.update(bp); 33 | this.secondHigh.update(bp); 34 | this.thirdHigh.update(bp); 35 | 36 | var first = this.firstHigh.result / this.firstLow.result; 37 | var second = this.secondHigh.result / this.secondLow.result; 38 | var third = this.thirdHigh.result / this.secondLow.result; 39 | 40 | this.uo = 100 * (this.firstWeight * first + this.secondWeight * second + this.thirdWeight * third) / (this.firstWeight + this.secondWeight + this.thirdWeight); 41 | 42 | this.lastClose = close; 43 | } 44 | 45 | module.exports = Indicator; 46 | -------------------------------------------------------------------------------- /strategies/noop.js: -------------------------------------------------------------------------------- 1 | // This method is a noop (it doesn't do anything) 2 | 3 | var _ = require('lodash'); 4 | 5 | // Let's create our own method 6 | var method = {}; 7 | 8 | method.init = _.noop; 9 | method.update = _.noop; 10 | method.log = _.noop; 11 | method.check = _.noop; 12 | 13 | module.exports = method; -------------------------------------------------------------------------------- /strategies/talib-macd.js: -------------------------------------------------------------------------------- 1 | // If you want to use your own trading methods you can 2 | // write them here. For more information on everything you 3 | // can use please refer to this document: 4 | // 5 | // https://github.com/askmike/gekko/blob/stable/docs/trading_methods.md 6 | 7 | // Let's create our own method 8 | var method = {}; 9 | 10 | // Prepare everything our method needs 11 | method.init = function() { 12 | this.name = 'talib-macd' 13 | this.input = 'candle'; 14 | // keep state about the current trend 15 | // here, on every new candle we use this 16 | // state object to check if we need to 17 | // report it. 18 | this.trend = 'none'; 19 | 20 | // how many candles do we need as a base 21 | // before we can start giving advice? 22 | this.requiredHistory = this.tradingAdvisor.historySize; 23 | 24 | var customMACDSettings = this.settings.parameters; 25 | 26 | // define the indicators we need 27 | this.addTalibIndicator('mymacd', 'macd', customMACDSettings); 28 | } 29 | 30 | // What happens on every new candle? 31 | method.update = function(candle) { 32 | // nothing! 33 | } 34 | 35 | 36 | method.log = function() { 37 | // nothing! 38 | } 39 | 40 | // Based on the newly calculated 41 | // information, check if we should 42 | // update or not. 43 | method.check = function(candle) { 44 | var price = candle.close; 45 | var result = this.talibIndicators.mymacd.result; 46 | var macddiff = result['outMACD'] - result['outMACDSignal']; 47 | 48 | if(this.settings.thresholds.down > macddiff && this.trend !== 'short') { 49 | this.trend = 'short'; 50 | this.advice('short'); 51 | 52 | } else if(this.settings.thresholds.up < macddiff && this.trend !== 'long'){ 53 | this.trend = 'long'; 54 | this.advice('long'); 55 | 56 | } 57 | } 58 | 59 | module.exports = method; 60 | -------------------------------------------------------------------------------- /strategies/tulip-adx.js: -------------------------------------------------------------------------------- 1 | // Let's create our own method 2 | var method = {}; 3 | // Prepare everything our method needs 4 | method.init = function() { 5 | this.name = 'tulip-adx' 6 | this.trend = 'none'; 7 | this.requiredHistory = this.settings.historySize; 8 | this.addTulipIndicator('myadx', 'adx', this.settings); 9 | } 10 | // What happens on every new candle? 11 | method.update = function(candle) { 12 | // nothing! 13 | } 14 | method.log = function() { 15 | // nothing! 16 | } 17 | method.check = function(candle) { 18 | var price = candle.close; 19 | var adx = this.tulipIndicators.myadx.result.result; 20 | // console.dir(adx) 21 | 22 | if(this.settings.thresholds.down > adx && this.trend !== 'short') { 23 | this.trend = 'short'; 24 | this.advice('short'); 25 | } else if(this.settings.thresholds.up < adx && this.trend !== 'long'){ 26 | this.trend = 'long'; 27 | this.advice('long'); 28 | } 29 | } 30 | 31 | module.exports = method; 32 | -------------------------------------------------------------------------------- /strategies/tulip-macd.js: -------------------------------------------------------------------------------- 1 | // If you want to use your own trading methods you can 2 | // write them here. For more information on everything you 3 | // can use please refer to this document: 4 | // 5 | // https://github.com/askmike/gekko/blob/stable/docs/trading_methods.md 6 | 7 | // Let's create our own method 8 | var method = {}; 9 | 10 | // Prepare everything our method needs 11 | method.init = function() { 12 | this.name = 'tulip-macd' 13 | // keep state about the current trend 14 | // here, on every new candle we use this 15 | // state object to check if we need to 16 | // report it. 17 | this.trend = 'none'; 18 | 19 | // how many candles do we need as a base 20 | // before we can start giving advice? 21 | this.requiredHistory = this.tradingAdvisor.historySize; 22 | 23 | var customMACDSettings = this.settings.parameters; 24 | 25 | // define the indicators we need 26 | this.addTulipIndicator('mymacd', 'macd', customMACDSettings); 27 | } 28 | 29 | // What happens on every new candle? 30 | method.update = function(candle) { 31 | // nothing! 32 | } 33 | 34 | 35 | method.log = function() { 36 | // nothing! 37 | } 38 | 39 | // Based on the newly calculated 40 | // information, check if we should 41 | // update or not. 42 | method.check = function(candle) { 43 | var price = candle.close; 44 | var result = this.tulipIndicators.mymacd.result; 45 | var macddiff = result['macd'] - result['macdSignal']; 46 | 47 | if(this.settings.thresholds.down > macddiff && this.trend !== 'short') { 48 | this.trend = 'short'; 49 | this.advice('short'); 50 | 51 | } else if(this.settings.thresholds.up < macddiff && this.trend !== 'long'){ 52 | this.trend = 'long'; 53 | this.advice('long'); 54 | 55 | } 56 | } 57 | 58 | module.exports = method; 59 | -------------------------------------------------------------------------------- /strategies/tulip-multi-strat.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | var log = require('../core/log.js'); 3 | 4 | var method = {}; 5 | method.init = function() { 6 | // strat name 7 | this.name = 'tulip-multi-strat'; 8 | // trend information 9 | this.trend = 'none' 10 | // tulip indicators use this sometimes 11 | this.requiredHistory = this.settings.historySize; 12 | // define the indicators we need 13 | this.addTulipIndicator('myadx', 'adx', this.settings); 14 | this.addTulipIndicator('mymacd', 'macd', this.settings); 15 | } 16 | 17 | // what happens on every new candle? 18 | method.update = function(candle) { 19 | // tulip results 20 | this.adx = this.tulipIndicators.myadx.result.result; 21 | this.macd = this.tulipIndicators.mymacd.result.macdHistogram; 22 | } 23 | // for debugging purposes log the last 24 | // calculated parameters. 25 | method.log = function() { 26 | log.debug( 27 | `--------------------- 28 | Tulip ADX: ${this.adx} 29 | Tulip MACD: ${this.macd} 30 | `); 31 | } 32 | 33 | method.check = function() { 34 | // just add a long and short to each array when new indicators are used 35 | const all_long = [ 36 | this.adx > this.settings.up && this.trend!=='long', 37 | this.settings.macd_up < this.macd && this.trend!=='long', 38 | ].reduce((total, long)=>long && total, true) 39 | const all_short = [ 40 | this.adx < this.settings.down && this.trend!=='short', 41 | this.settings.macd_down > this.macd && this.trend!=='short', 42 | ].reduce((total, long)=>long && total, true) 43 | 44 | // combining all indicators with AND 45 | if(all_long){ 46 | log.debug(`tulip-multi-strat In low`); 47 | this.advice('long'); 48 | }else if(all_short){ 49 | log.debug(`tulip-multi-strat In high`); 50 | this.advice('short'); 51 | }else{ 52 | log.debug(`tulip-multi-strat In no trend`); 53 | this.advice(); 54 | } 55 | } 56 | 57 | module.exports = method; 58 | -------------------------------------------------------------------------------- /subscriptions.js: -------------------------------------------------------------------------------- 1 | // 2 | // Subscriptions glue plugins to events 3 | // flowing through the Gekko. 4 | // 5 | 6 | var subscriptions = [ 7 | { 8 | emitter: 'market', 9 | event: 'candle', 10 | handler: 'processCandle' 11 | }, 12 | { 13 | emitter: 'market', 14 | event: 'history', 15 | handler: 'processHistory' 16 | }, 17 | { 18 | emitter: 'tradingAdvisor', 19 | event: 'advice', 20 | handler: 'processAdvice' 21 | }, 22 | { 23 | emitter: ['trader', 'paperTrader'], 24 | event: 'trade', 25 | handler: 'processTrade' 26 | }, 27 | { 28 | emitter: ['trader', 'paperTrader'], 29 | event: 'portfolioUpdate', 30 | handler: 'processPortfolioUpdate' 31 | }, 32 | ]; 33 | 34 | module.exports = subscriptions; -------------------------------------------------------------------------------- /test/_prepare.js: -------------------------------------------------------------------------------- 1 | // overwrite config with test-config 2 | 3 | var utils = require(__dirname + '/../core/util'); 4 | var testConfig = require(__dirname + '/test-config.json'); 5 | utils.setConfig(testConfig); -------------------------------------------------------------------------------- /test/indicators/dema.js: -------------------------------------------------------------------------------- 1 | var chai = require('chai'); 2 | var expect = chai.expect; 3 | var should = chai.should; 4 | var sinon = require('sinon'); 5 | 6 | var _ = require('lodash'); 7 | 8 | var util = require('../../core/util'); 9 | var dirs = util.dirs(); 10 | var INDICATOR_PATH = dirs.indicators; 11 | 12 | // Fake input prices to verify all indicators 13 | // are working correctly by comparing fresh 14 | // calculated results to pre calculated results. 15 | 16 | // The precalculated results are already compared 17 | // to MS Excel results, more info here: 18 | // 19 | // https://github.com/askmike/gekko/issues/161 20 | 21 | var prices = [81, 24, 75, 21, 34, 25, 72, 92, 99, 2, 86, 80, 76, 8, 87, 75, 32, 65, 41, 9, 13, 26, 56, 28, 65, 58, 17, 90, 87, 86, 99, 3, 70, 1, 27, 9, 92, 68, 9]; 22 | 23 | 24 | 25 | xdescribe('indicators/DEMA', function() { 26 | 27 | var DEMA = require(INDICATOR_PATH + 'DEMA'); 28 | 29 | xit('should correctly calculate DEMAs', function() { 30 | // TODO! 31 | }); 32 | 33 | 34 | }); -------------------------------------------------------------------------------- /test/plugins/portfolioManager.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magicdude4eva/MD4ECryptoBot/1812ec881d1ea08a11e162ae3774ff6083b6a535/test/plugins/portfolioManager.js -------------------------------------------------------------------------------- /web/apiKeyManager.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const _ = require('lodash'); 3 | const cache = require('./state/cache'); 4 | const broadcast = cache.get('broadcast'); 5 | 6 | const apiKeysFile = __dirname + '/../SECRET-api-keys.json'; 7 | 8 | // on init: 9 | const noApiKeysFile = !fs.existsSync(apiKeysFile); 10 | 11 | if(noApiKeysFile) 12 | fs.writeFileSync( 13 | apiKeysFile, 14 | JSON.stringify({}) 15 | ); 16 | 17 | const apiKeys = JSON.parse( fs.readFileSync(apiKeysFile, 'utf8') ); 18 | 19 | module.exports = { 20 | get: () => _.keys(apiKeys), 21 | 22 | // note: overwrites if exists 23 | add: (exchange, props) => { 24 | apiKeys[exchange] = props; 25 | fs.writeFileSync(apiKeysFile, JSON.stringify(apiKeys)); 26 | 27 | broadcast({ 28 | type: 'apiKeys', 29 | exchanges: _.keys(apiKeys) 30 | }); 31 | }, 32 | remove: exchange => { 33 | if(!apiKeys[exchange]) 34 | return; 35 | 36 | delete apiKeys[exchange]; 37 | fs.writeFileSync(apiKeysFile, JSON.stringify(apiKeys)); 38 | 39 | broadcast({ 40 | type: 'apiKeys', 41 | exchanges: _.keys(apiKeys) 42 | }); 43 | }, 44 | 45 | // retrieve api keys 46 | // this cannot touch the frontend for security reaons. 47 | _getApiKeyPair: key => apiKeys[key] 48 | } -------------------------------------------------------------------------------- /web/isWindows.js: -------------------------------------------------------------------------------- 1 | const os = require('os'); 2 | 3 | var isWindows = ( 4 | os.platform() == 'win32' // true evenon 64 bit archs 5 | || os.release().indexOf('Microsoft') > -1 // bash on Windows 10 scenario 6 | ); 7 | 8 | module.exports = isWindows; 9 | 10 | -------------------------------------------------------------------------------- /web/routes/apiKeys.js: -------------------------------------------------------------------------------- 1 | const cache = require('../state/cache'); 2 | const manager = cache.get('apiKeyManager'); 3 | 4 | module.exports = { 5 | get: function *() { 6 | this.body = manager.get(); 7 | }, 8 | add: function *() { 9 | const content = this.request.body; 10 | 11 | manager.add(content.exchange, content.values); 12 | 13 | this.body = { 14 | status: 'ok' 15 | }; 16 | }, 17 | remove: function *() { 18 | const exchange = this.request.body.exchange; 19 | 20 | manager.remove(exchange); 21 | 22 | this.body = { 23 | status: 'ok' 24 | }; 25 | } 26 | } -------------------------------------------------------------------------------- /web/routes/backtest.js: -------------------------------------------------------------------------------- 1 | // simple POST request that returns the backtest result 2 | 3 | const _ = require('lodash'); 4 | const promisify = require('tiny-promisify'); 5 | const pipelineRunner = promisify(require('../../core/workers/pipeline/parent')); 6 | 7 | // starts a backtest 8 | // requires a post body like: 9 | // 10 | // { 11 | // gekkoConfig: {watch: {exchange: "poloniex", currency: "USDT", asset: "BTC"},…},…} 12 | // data: { 13 | // candleProps: ["close", "start"], 14 | // indicatorResults: true, 15 | // report: true, 16 | // roundtrips: true 17 | // } 18 | // } 19 | module.exports = function *() { 20 | var mode = 'backtest'; 21 | 22 | var config = {}; 23 | 24 | var base = require('./baseConfig'); 25 | 26 | var req = this.request.body; 27 | 28 | _.merge(config, base, req.gekkoConfig); 29 | 30 | var result = yield pipelineRunner(mode, config); 31 | 32 | if(!req.data.report) 33 | delete result.report; 34 | 35 | if(!req.data.roundtrips) 36 | delete result.roundtrips; 37 | 38 | if(!req.data.trades) 39 | delete result.trades; 40 | 41 | // todo: indicatorResults 42 | 43 | result.candles = _.map( 44 | result.candles, 45 | c => _.pick(c, req.data.candleProps) 46 | ); 47 | 48 | this.body = result; 49 | } -------------------------------------------------------------------------------- /web/routes/baseConfig.js: -------------------------------------------------------------------------------- 1 | var UIconfig = require('../vue/UIconfig'); 2 | 3 | var config = {}; 4 | 5 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 6 | // GENERAL SETTINGS 7 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 8 | 9 | config.silent = false; 10 | config.debug = true; 11 | 12 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 13 | // CONFIGURING TRADING ADVICE 14 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 15 | 16 | config.tradingAdvisor = { 17 | } 18 | 19 | config.candleWriter = { 20 | enabled: false 21 | } 22 | config.adviceWriter = { 23 | enabled: false, 24 | muteSoft: true, 25 | } 26 | 27 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 28 | // CONFIGURING ADAPTER 29 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 30 | 31 | // configurable in the UIconfig 32 | config.adapter = UIconfig.adapter; 33 | 34 | config.sqlite = { 35 | path: 'plugins/sqlite', 36 | version: 0.1, 37 | dataDirectory: 'history', 38 | journalMode: require('../isWindows.js') ? 'PERSIST' : 'WAL', 39 | dependencies: [{ 40 | module: 'sqlite3', 41 | version: '3.1.4' 42 | }] 43 | } 44 | 45 | // Postgres adapter example config (please note: requires postgres >= 9.5): 46 | config.postgresql = { 47 | path: 'plugins/postgresql', 48 | version: 0.1, 49 | connectionString: 'postgres://user:pass@localhost:5432', // if default port 50 | database: null, // if set, we'll put all tables into a single database. 51 | schema: 'public', 52 | dependencies: [{ 53 | module: 'pg', 54 | version: '6.1.0' 55 | }] 56 | } 57 | 58 | // Mongodb adapter, requires mongodb >= 3.3 (no version earlier tested) 59 | config.mongodb = { 60 | path: 'plugins/mongodb', 61 | version: 0.1, 62 | connectionString: 'mongodb://mongodb/gekko', // connection to mongodb server 63 | dependencies: [{ 64 | module: 'mongojs', 65 | version: '2.4.0' 66 | }] 67 | } 68 | 69 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 70 | // CONFIGURING BACKTESTING 71 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 72 | 73 | // Note that these settings are only used in backtesting mode, see here: 74 | // @link: https://github.com/askmike/gekko/blob/stable/docs/Backtesting.md 75 | 76 | config.backtest = { 77 | daterange: 'scan', 78 | batchSize: 50 79 | } 80 | 81 | config.importer = { 82 | daterange: { 83 | // NOTE: these dates are in UTC 84 | from: "2016-06-01 12:00:00" 85 | } 86 | } 87 | 88 | module.exports = config; 89 | -------------------------------------------------------------------------------- /web/routes/configPart.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const fs = require('co-fs'); 3 | 4 | const parts = { 5 | paperTrader: 'config/plugins/paperTrader', 6 | candleWriter: 'config/plugins/candleWriter', 7 | performanceAnalyzer: 'config/plugins/performanceAnalyzer' 8 | } 9 | 10 | const gekkoRoot = __dirname + '/../../'; 11 | 12 | module.exports = function *() { 13 | if(!_.has(parts, this.params.part)) 14 | return this.body = 'error :('; 15 | 16 | const fileName = gekkoRoot + '/' + parts[this.params.part] + '.toml'; 17 | this.body = { 18 | part: yield fs.readFile(fileName, 'utf8') 19 | } 20 | } -------------------------------------------------------------------------------- /web/routes/exchanges.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const fs = require('co-fs'); 3 | 4 | const gekkoRoot = __dirname + '/../../'; 5 | var util = require(__dirname + '/../../core/util'); 6 | 7 | var config = {}; 8 | 9 | config.debug = false; 10 | config.silent = false; 11 | 12 | util.setConfig(config); 13 | 14 | module.exports = function *() { 15 | const exchangesDir = yield fs.readdir(gekkoRoot + 'exchanges'); 16 | const exchanges = exchangesDir 17 | .filter(f => _.last(f, 3).join('') === '.js') 18 | .map(f => f.slice(0, -3)); 19 | 20 | let allCapabilities = []; 21 | 22 | exchanges.forEach(function (exchange) { 23 | let Trader = null; 24 | 25 | try { 26 | Trader = require(gekkoRoot + 'exchanges/' + exchange); 27 | } catch (e) { 28 | return; 29 | } 30 | 31 | if (!Trader || !Trader.getCapabilities) { 32 | return; 33 | } 34 | 35 | allCapabilities.push(Trader.getCapabilities()); 36 | }); 37 | 38 | this.body = allCapabilities; 39 | } -------------------------------------------------------------------------------- /web/routes/getCandles.js: -------------------------------------------------------------------------------- 1 | // simple POST request that returns the candles requested 2 | 3 | // expects a config like: 4 | 5 | // let config = { 6 | // watch: { 7 | // exchange: 'poloniex', 8 | // currency: 'USDT', 9 | // asset: 'BTC' 10 | // }, 11 | // daterange: { 12 | // from: '2016-05-22 11:22', 13 | // to: '2016-06-03 19:56' 14 | // }, 15 | // adapter: 'sqlite', 16 | // sqlite: { 17 | // path: 'plugins/sqlite', 18 | 19 | // dataDirectory: 'history', 20 | // version: 0.1, 21 | 22 | // dependencies: [{ 23 | // module: 'sqlite3', 24 | // version: '3.1.4' 25 | // }] 26 | // }, 27 | // candleSize: 100 28 | // } 29 | 30 | const _ = require('lodash'); 31 | const promisify = require('tiny-promisify'); 32 | const candleLoader = promisify(require('../../core/workers/loadCandles/parent')); 33 | const base = require('./baseConfig'); 34 | 35 | module.exports = function *() { 36 | 37 | config = {}; 38 | _.merge(config, base, this.request.body); 39 | this.body = yield candleLoader(config); 40 | } -------------------------------------------------------------------------------- /web/routes/import.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const promisify = require('tiny-promisify'); 3 | const pipelineRunner = promisify(require('../../core/workers/pipeline/parent')); 4 | 5 | const cache = require('../state/cache'); 6 | const broadcast = cache.get('broadcast'); 7 | const importManager = cache.get('imports'); 8 | 9 | const base = require('./baseConfig'); 10 | 11 | // starts an import 12 | // requires a post body with a config object 13 | module.exports = function *() { 14 | let mode = 'importer'; 15 | 16 | let config = {} 17 | 18 | _.merge(config, base, this.request.body); 19 | 20 | let importId = (Math.random() + '').slice(3); 21 | 22 | let errored = false; 23 | 24 | console.log('Import', importId, 'started'); 25 | 26 | pipelineRunner(mode, config, (err, event) => { 27 | if(errored) 28 | return; 29 | 30 | if(err) { 31 | errored = true; 32 | console.error('RECEIVED ERROR IN IMPORT', importId); 33 | console.error(err); 34 | importManager.delete(importId); 35 | return broadcast({ 36 | type: 'import_error', 37 | import_id: importId, 38 | error: err 39 | }); 40 | } 41 | 42 | if(!event) 43 | return; 44 | 45 | // update local cache 46 | importManager.update(importId, { 47 | latest: event.latest, 48 | done: event.done 49 | }); 50 | 51 | // emit update over ws 52 | let wsEvent = { 53 | type: 'import_update', 54 | import_id: importId, 55 | updates: { 56 | latest: event.latest, 57 | done: event.done 58 | } 59 | } 60 | broadcast(wsEvent); 61 | }); 62 | 63 | let daterange = this.request.body.importer.daterange; 64 | 65 | const _import = { 66 | watch: config.watch, 67 | id: importId, 68 | latest: '', 69 | from: daterange.from, 70 | to: daterange.to 71 | } 72 | 73 | importManager.add(_import); 74 | this.body = _import; 75 | } -------------------------------------------------------------------------------- /web/routes/killGekko.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const promisify = require('tiny-promisify'); 3 | const moment = require('moment'); 4 | 5 | const pipelineRunner = promisify(require('../../core/workers/pipeline/parent')); 6 | const cache = require('../state/cache'); 7 | const broadcast = cache.get('broadcast'); 8 | const gekkoManager = cache.get('gekkos'); 9 | 10 | const base = require('./baseConfig'); 11 | 12 | // starts an import 13 | // requires a post body with a config object 14 | module.exports = function *() { 15 | 16 | let id = this.request.body.id; 17 | 18 | if(!id) { 19 | this.body = { 20 | status: 'not ok' 21 | } 22 | return; 23 | } 24 | 25 | let deleted = gekkoManager.delete(id); 26 | 27 | if(!deleted){ 28 | this.body = { 29 | status: 'not ok' 30 | } 31 | return; 32 | } 33 | 34 | broadcast({ 35 | type: 'gekko_killed', 36 | gekko_id: id 37 | }); 38 | 39 | this.body = { 40 | status: 'ok' 41 | }; 42 | } -------------------------------------------------------------------------------- /web/routes/list.js: -------------------------------------------------------------------------------- 1 | const cache = require('../state/cache'); 2 | 3 | module.exports = function(name) { 4 | return function *() { 5 | this.body = cache.get(name).list(); 6 | } 7 | } -------------------------------------------------------------------------------- /web/routes/scanDatasets.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const promisify = require('promisify-node'); 3 | 4 | const scan = promisify(require('../../core/workers/datasetScan/parent')); 5 | 6 | // starts a scan 7 | // requires a post body with configuration of: 8 | // 9 | // - config.watch 10 | const route = function *() { 11 | 12 | var config = require('./baseConfig'); 13 | 14 | _.merge(config, this.request.body); 15 | 16 | this.body = yield scan(config); 17 | }; 18 | 19 | module.exports = route; -------------------------------------------------------------------------------- /web/routes/scanDateRange.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const promisify = require('promisify-node'); 3 | 4 | const scan = promisify(require('../../core/workers/dateRangeScan/parent')); 5 | 6 | // starts a scan 7 | // requires a post body with configuration of: 8 | // 9 | // - config.watch 10 | const route = function *() { 11 | 12 | var config = require('./baseConfig'); 13 | 14 | _.merge(config, this.request.body); 15 | 16 | this.body = yield scan(config); 17 | }; 18 | 19 | module.exports = route; -------------------------------------------------------------------------------- /web/routes/strategies.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const fs = require('co-fs'); 3 | 4 | const gekkoRoot = __dirname + '/../../'; 5 | 6 | module.exports = function *() { 7 | const strategyDir = yield fs.readdir(gekkoRoot + 'strategies'); 8 | const strats = strategyDir 9 | .filter(f => _.last(f, 3).join('') === '.js') 10 | .map(f => { 11 | return { name: f.slice(0, -3) } 12 | }); 13 | 14 | // for every strat, check if there is a config file and add it 15 | const stratConfigPath = gekkoRoot + 'config/strategies'; 16 | const strategyParamsDir = yield fs.readdir(stratConfigPath); 17 | 18 | for(let i = 0; i < strats.length; i++) { 19 | let strat = strats[i]; 20 | if(strategyParamsDir.indexOf(strat.name + '.toml') !== -1) 21 | strat.params = yield fs.readFile(stratConfigPath + '/' + strat.name + '.toml', 'utf8') 22 | else 23 | strat.params = ''; 24 | } 25 | 26 | this.body = strats; 27 | } -------------------------------------------------------------------------------- /web/state/cache.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | 3 | const cache = {}; 4 | 5 | module.exports = { 6 | set: (name, val) => { 7 | cache[name] = val; 8 | return true; 9 | }, 10 | get: name => { 11 | if(_.has(cache, name)) 12 | return cache[name]; 13 | } 14 | } -------------------------------------------------------------------------------- /web/state/listManager.js: -------------------------------------------------------------------------------- 1 | // manages a list of things that change over time 2 | // used for: 3 | // - The currently running imports 4 | // - The currently running gekko watchers 5 | // - The live gekkos 6 | // - etc.. 7 | const _ = require('lodash'); 8 | 9 | var ListManager = function() { 10 | this._list = []; 11 | } 12 | 13 | // add an item to the list 14 | ListManager.prototype.add = function(obj) { 15 | if(!obj.id) 16 | return false; 17 | this._list.push(_.clone(obj)); 18 | return true; 19 | } 20 | 21 | // update some properties on an item 22 | ListManager.prototype.update = function(id, updates) { 23 | let item = this._list.find(i => i.id === id); 24 | if(!item) 25 | return false; 26 | _.merge(item, updates); 27 | return true; 28 | } 29 | 30 | // push a value to a array proprty of an item 31 | ListManager.prototype.push = function(id, prop, value) { 32 | let item = this._list.find(i => i.id === id); 33 | if(!item) 34 | return false; 35 | 36 | item[prop].push(value); 37 | return true; 38 | } 39 | 40 | // delete an item from the list 41 | ListManager.prototype.delete = function(id) { 42 | let wasThere = this._list.find(i => i.id === id); 43 | this._list = this._list.filter(i => i.id !== id); 44 | 45 | if(wasThere) 46 | return true; 47 | else 48 | return false; 49 | } 50 | 51 | // getter 52 | ListManager.prototype.list = function() { 53 | return this._list; 54 | } 55 | 56 | module.exports = ListManager; -------------------------------------------------------------------------------- /web/state/logger.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const moment = require('moment'); 3 | const _ = require('lodash'); 4 | 5 | const BASEPATH = __dirname + '/../../logs/'; 6 | 7 | const Logger = function(type) { 8 | 9 | const now = moment().utc().format('YYYY-MM-DD-HH-mm'); 10 | this.fileName = `${now}-UTC-${type}.log`; 11 | 12 | this.writing = false; 13 | this.queue = []; 14 | 15 | _.bindAll(this); 16 | } 17 | 18 | Logger.prototype.write = function(line) { 19 | if(!this.writing) { 20 | this.writing = true; 21 | fs.appendFile( 22 | BASEPATH + this.fileName, 23 | line + '\n', 24 | this.handleWriteCallback 25 | ); 26 | } else 27 | this.queue.push(line); 28 | } 29 | 30 | Logger.prototype.handleWriteCallback = function(err) { 31 | if(err) 32 | console.error(`ERROR WRITING LOG FILE ${this.fileName}:`, err); 33 | 34 | this.writing = false; 35 | 36 | if(_.size(this.queue)) 37 | this.write(this.queue.shift()) 38 | } 39 | 40 | module.exports = Logger; -------------------------------------------------------------------------------- /web/vue/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["es2015", { "modules": false }] 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /web/vue/README.md: -------------------------------------------------------------------------------- 1 | # vue 2 | 3 | > A Vue.js project 4 | 5 | ## Build Setup 6 | 7 | ``` bash 8 | # install dependencies 9 | npm install 10 | 11 | # serve with hot reload at localhost:8080 12 | npm run dev 13 | 14 | # build for production with minification 15 | npm run build 16 | ``` 17 | 18 | For detailed explanation on how things work, consult the [docs for vue-loader](http://vuejs.github.io/vue-loader). 19 | -------------------------------------------------------------------------------- /web/vue/UIconfig.js: -------------------------------------------------------------------------------- 1 | // This config is used in both the 2 | // frontend as well as the web server. 3 | 4 | // see https://github.com/askmike/gekko/blob/stable/docs/installing_gekko_on_a_server.md 5 | 6 | const CONFIG = { 7 | headless: false, 8 | api: { 9 | host: '127.0.0.1', 10 | port: 3000, 11 | timeout: 120000 // 2 minutes 12 | }, 13 | ui: { 14 | ssl: false, 15 | host: 'localhost', 16 | port: 3000, 17 | path: '/' 18 | }, 19 | adapter: 'sqlite' 20 | } 21 | 22 | if(typeof window === 'undefined') 23 | module.exports = CONFIG; 24 | else 25 | window.CONFIG = CONFIG; 26 | -------------------------------------------------------------------------------- /web/vue/assets/gekko.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magicdude4eva/MD4ECryptoBot/1812ec881d1ea08a11e162ae3774ff6083b6a535/web/vue/assets/gekko.jpg -------------------------------------------------------------------------------- /web/vue/assets/select-arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magicdude4eva/MD4ECryptoBot/1812ec881d1ea08a11e162ae3774ff6083b6a535/web/vue/assets/select-arrow.png -------------------------------------------------------------------------------- /web/vue/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Gekko 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /web/vue/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue", 3 | "version": "0.2.0", 4 | "description": "The frontend for the Gekko UI", 5 | "author": "Mike van Rossum ", 6 | "scripts": { 7 | "dev": "cross-env NODE_ENV=development webpack-dev-server --open --inline --hot", 8 | "build": "cross-env NODE_ENV=production webpack --progress --hide-modules" 9 | }, 10 | "devDependencies": { 11 | "babel-core": "^6.0.0", 12 | "babel-loader": "^6.0.0", 13 | "babel-preset-es2015": "^6.0.0", 14 | "cross-env": "^3.0.0", 15 | "css-loader": "^0.25.0", 16 | "file-loader": "^0.9.0", 17 | "json-loader": "^0.5.4", 18 | "marked": "^0.3.6", 19 | "superagent": "^2.3.0", 20 | "superagent-no-cache": "uditalias/superagent-no-cache", 21 | "vue": "^2.0.1", 22 | "vue-loader": "^9.7.0", 23 | "vue-router": "^2.0.3", 24 | "vuex": "^2.2.1", 25 | "webpack": "^2.1.0-beta.25", 26 | "webpack-dev-server": "^2.1.0-beta.0" 27 | }, 28 | "dependencies": { 29 | "jade": "^1.11.0" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /web/vue/src/App.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 25 | 26 | 94 | -------------------------------------------------------------------------------- /web/vue/src/components/backtester/backtester.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 67 | 68 | 75 | -------------------------------------------------------------------------------- /web/vue/src/components/backtester/result/chartWrapper.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 52 | 53 | 105 | -------------------------------------------------------------------------------- /web/vue/src/components/backtester/result/result.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 31 | 32 | 34 | -------------------------------------------------------------------------------- /web/vue/src/components/backtester/result/roundtripTable.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 44 | 45 | 76 | -------------------------------------------------------------------------------- /web/vue/src/components/backtester/result/summary.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 52 | 53 | 59 | -------------------------------------------------------------------------------- /web/vue/src/components/config/apiConfigBuilder.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 94 | 95 | 97 | -------------------------------------------------------------------------------- /web/vue/src/components/config/config.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | -------------------------------------------------------------------------------- /web/vue/src/components/data/import/importConfigBuilder.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 63 | 64 | 66 | -------------------------------------------------------------------------------- /web/vue/src/components/data/import/importer.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 81 | 82 | 84 | -------------------------------------------------------------------------------- /web/vue/src/components/global/blockSpinner.vue: -------------------------------------------------------------------------------- 1 | // http://tobiasahlin.com/spinkit/ 2 | 3 | 10 | 11 | 15 | 16 | 70 | -------------------------------------------------------------------------------- /web/vue/src/components/global/configbuilder/exchangepicker.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 65 | 66 | -------------------------------------------------------------------------------- /web/vue/src/components/global/configbuilder/papertrader.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 58 | 73 | -------------------------------------------------------------------------------- /web/vue/src/components/global/configbuilder/rangecreator.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 81 | 82 | 94 | -------------------------------------------------------------------------------- /web/vue/src/components/global/configbuilder/typepicker.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 41 | 42 | 49 | -------------------------------------------------------------------------------- /web/vue/src/components/global/mixins/dataset.js: -------------------------------------------------------------------------------- 1 | import { post } from '../../../tools/ajax' 2 | 3 | var mixin = { 4 | data: () => { 5 | return { 6 | datasets: [], 7 | datasetScanstate: 'idle', 8 | unscannableMakets: [] 9 | } 10 | }, 11 | methods: { 12 | scan: function() { 13 | this.datasetScanstate = 'scanning'; 14 | 15 | post('scansets', {}, (error, response) => { 16 | this.datasetScanstate = 'scanned'; 17 | 18 | this.unscannableMakets = response.errors; 19 | 20 | let sets = []; 21 | 22 | response.datasets.forEach(market => { 23 | market.ranges.forEach((range, i) => { 24 | sets.push({ 25 | exchange: market.exchange, 26 | currency: market.currency, 27 | asset: market.asset, 28 | from: moment.unix(range.from).utc(), 29 | to: moment.unix(range.to).utc(), 30 | id: market.exchange + market.asset + market.currency + i 31 | }); 32 | }); 33 | }); 34 | 35 | // for now, filter out sets smaller than 3 hours.. 36 | sets = sets.filter(set => { 37 | if(set.to.diff(set.from, 'hours') > 2) 38 | return true; 39 | }); 40 | 41 | sets = sets.sort((a, b) => { 42 | let adiff = a.to.diff(a.from); 43 | let bdiff = b.to.diff(b.from); 44 | 45 | if(adiff < bdiff) 46 | return -1; 47 | 48 | if(adiff > bdiff) 49 | return 1; 50 | 51 | return 0; 52 | }).reverse(); 53 | 54 | this.datasets = sets; 55 | }) 56 | } 57 | } 58 | } 59 | 60 | export default mixin; -------------------------------------------------------------------------------- /web/vue/src/components/global/paperTradeSummary.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 41 | 42 | 65 | -------------------------------------------------------------------------------- /web/vue/src/components/global/progressBar.vue: -------------------------------------------------------------------------------- 1 | // http://stackoverflow.com/questions/7190898/progress-bar-with-html-and-css 2 | 3 | 9 | 10 | 18 | 19 | -------------------------------------------------------------------------------- /web/vue/src/components/global/ws.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import Vue from 'vue' 3 | 4 | import { wsPath } from '../../tools/api' 5 | import initializeState from '../../store/init' 6 | 7 | var socket = null; 8 | 9 | export const bus = new Vue(); 10 | 11 | bus.$on('gekko_update', data => console.log(data)) 12 | bus.$on('gekko_error', data => { 13 | alert('GEKKO ERROR: ' + data.error); 14 | }) 15 | 16 | bus.$on('import_update', data => console.log(data)) 17 | bus.$on('import_error', data => { 18 | alert('IMPORT ERROR: ' + data.error); 19 | }); 20 | 21 | const info = { 22 | connected: false 23 | } 24 | 25 | 26 | export const connect = () => { 27 | socket = new ReconnectingWebSocket(wsPath); 28 | 29 | setTimeout(() => { 30 | // in case we cannot connect 31 | if(!info.connected) { 32 | initializeState(); 33 | bus.$emit('WS_STATUS_CHANGE', info); 34 | } 35 | }, 500); 36 | 37 | socket.onopen = () => { 38 | if(info.connected) 39 | return; 40 | 41 | info.connected = true; 42 | bus.$emit('WS_STATUS_CHANGE', info); 43 | initializeState(); 44 | } 45 | socket.onclose = () => { 46 | if(!info.connected) 47 | return; 48 | 49 | info.connected = false; 50 | bus.$emit('WS_STATUS_CHANGE', info); 51 | } 52 | socket.onerror = () => { 53 | if(!info.connected) 54 | return; 55 | 56 | info.connected = false; 57 | bus.$emit('WS_STATUS_CHANGE', info); 58 | } 59 | socket.onmessage = function(message) { 60 | let payload = JSON.parse(message.data); 61 | bus.$emit(payload.type, payload); 62 | }; 63 | } -------------------------------------------------------------------------------- /web/vue/src/components/layout/footer.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 24 | -------------------------------------------------------------------------------- /web/vue/src/components/layout/header.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 21 | 22 | 48 | -------------------------------------------------------------------------------- /web/vue/src/components/layout/home.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | -------------------------------------------------------------------------------- /web/vue/src/components/layout/modal.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 37 | 38 | 79 | -------------------------------------------------------------------------------- /web/vue/src/d3/message.js: -------------------------------------------------------------------------------- 1 | export const draw = function(message) { 2 | d3.select("#chart").append("text") 3 | .attr('class', 'message') 4 | .attr('x', 150) 5 | .attr('y', 150) 6 | .text(message); 7 | } 8 | 9 | export const clear = function() { 10 | d3.select("#chart").find('text').remove(); 11 | } -------------------------------------------------------------------------------- /web/vue/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | 4 | import VueRouter from 'vue-router' 5 | Vue.use(VueRouter); 6 | 7 | import store from './store' 8 | 9 | import backtester from './components/backtester/backtester.vue' 10 | import home from './components/layout/home.vue' 11 | 12 | import data from './components/data/data.vue' 13 | import importer from './components/data/import/importer.vue' 14 | import singleImport from './components/data/import/single.vue' 15 | import config from './components/config/config.vue' 16 | 17 | import gekkoList from './components/gekko/list.vue' 18 | import newGekko from './components/gekko/new.vue' 19 | import singleStratrunner from './components/gekko/singleStratrunner.vue' 20 | import singleWatcher from './components/gekko/singleWatcher.vue' 21 | import { connect as connectWS } from './components/global/ws' 22 | 23 | const router = new VueRouter({ 24 | mode: 'hash', 25 | base: __dirname, 26 | routes: [ 27 | { path: '/', redirect: '/home' }, 28 | { path: '/home', component: home }, 29 | { path: '/backtest', component: backtester }, 30 | { path: '/config', component: config }, 31 | { path: '/data', component: data }, 32 | { path: '/data/importer', component: importer }, 33 | { path: '/data/importer/import/:id', component: singleImport }, 34 | { path: '/live-gekkos', component: gekkoList }, 35 | { path: '/live-gekkos/new', component: newGekko }, 36 | { path: '/live-gekkos/stratrunner/:id', component: singleStratrunner }, 37 | { path: '/live-gekkos/watcher/:id', component: singleWatcher } 38 | ] 39 | }); 40 | 41 | // setup some stuff 42 | connectWS(); 43 | 44 | new Vue({ 45 | router, 46 | store, 47 | el: '#app', 48 | render: h => h(App) 49 | }) -------------------------------------------------------------------------------- /web/vue/src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | import _ from 'lodash' 4 | 5 | import * as importMutations from './modules/imports/mutations' 6 | import * as watchMutations from './modules/watchers/mutations' 7 | import * as stratrunnerMutations from './modules/stratrunners/mutations' 8 | import * as notificationMutations from './modules/notifications/mutations' 9 | import * as configMutations from './modules/config/mutations' 10 | 11 | Vue.use(Vuex); 12 | 13 | const debug = process.env.NODE_ENV !== 'production' 14 | 15 | let mutations = {}; 16 | 17 | _.merge(mutations, importMutations); 18 | _.merge(mutations, watchMutations); 19 | _.merge(mutations, stratrunnerMutations); 20 | _.merge(mutations, notificationMutations); 21 | _.merge(mutations, configMutations); 22 | 23 | export default new Vuex.Store({ 24 | state: { 25 | warnings: { 26 | connected: true, // assume we will connect 27 | }, 28 | imports: [], 29 | stratrunners: [], 30 | watchers: [], 31 | connection: { 32 | disconnected: false, 33 | reconnected: false 34 | }, 35 | apiKeys: [], 36 | exchanges: {} 37 | }, 38 | mutations, 39 | strict: debug 40 | }) -------------------------------------------------------------------------------- /web/vue/src/store/init.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | 4 | import syncImports from './modules/imports/sync' 5 | import syncWatchers from './modules/watchers/sync' 6 | import syncStratrunners from './modules/stratrunners/sync' 7 | import syncNotifications from './modules/notifications/sync' 8 | import syncConfig from './modules/config/sync' 9 | 10 | export default function() { 11 | syncImports(); 12 | syncWatchers(); 13 | syncStratrunners(); 14 | syncNotifications(); 15 | syncConfig(); 16 | } -------------------------------------------------------------------------------- /web/vue/src/store/modules/config/mutations.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | export const syncApiKeys = (state, apiKeys) => { 4 | Vue.set(state, 'apiKeys', apiKeys); 5 | return state; 6 | } 7 | 8 | export const syncExchanges = (state, exchanges) => { 9 | Vue.set(state, 'exchanges', exchanges); 10 | return state; 11 | } -------------------------------------------------------------------------------- /web/vue/src/store/modules/config/sync.js: -------------------------------------------------------------------------------- 1 | import { get } from '../../../tools/ajax' 2 | import store from '../../' 3 | import { bus } from '../../../components/global/ws' 4 | 5 | const transformMarkets = backendData => { 6 | var exchangesRaw = backendData; 7 | var exchangesTemp = {}; 8 | 9 | exchangesRaw.forEach(e => { 10 | exchangesTemp[e.slug] = exchangesTemp[e.slug] || {markets: {}}; 11 | 12 | e.markets.forEach( pair => { 13 | let [ currency, asset ] = pair['pair']; 14 | exchangesTemp[e.slug].markets[currency] = exchangesTemp[e.slug].markets[currency] || []; 15 | exchangesTemp[e.slug].markets[currency].push( asset ); 16 | }); 17 | 18 | exchangesTemp[e.slug].importable = e.providesFullHistory ? true : false; 19 | exchangesTemp[e.slug].tradable = e.tradable ? true : false; 20 | exchangesTemp[e.slug].requires = e.requires; 21 | }); 22 | 23 | return exchangesTemp; 24 | } 25 | 26 | 27 | const init = () => { 28 | get('apiKeys', (err, resp) => { 29 | store.commit('syncApiKeys', resp); 30 | }); 31 | 32 | get('exchanges', (err, resp) => { 33 | store.commit('syncExchanges', transformMarkets(resp)); 34 | }) 35 | } 36 | 37 | const sync = () => { 38 | bus.$on('apiKeys', data => { 39 | store.commit('syncApiKeys', data.exchanges); 40 | }); 41 | } 42 | 43 | export default function() { 44 | init(); 45 | sync(); 46 | } -------------------------------------------------------------------------------- /web/vue/src/store/modules/imports/mutations.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | export const addImport = (state, imp) => { 4 | state.imports.push(imp); 5 | return state; 6 | } 7 | 8 | export const syncImports = (state, imports) => { 9 | state.imports = imports; 10 | return state; 11 | } 12 | 13 | export const updateImport = (state, update) => { 14 | let index = state.imports.findIndex(i => i.id === update.import_id); 15 | let item = state.imports[index]; 16 | if(!item) 17 | return state; 18 | 19 | let updated = Vue.util.extend(item, update.updates); 20 | Vue.set(state.imports, index, updated); 21 | 22 | return state; 23 | } -------------------------------------------------------------------------------- /web/vue/src/store/modules/imports/sync.js: -------------------------------------------------------------------------------- 1 | import { get } from '../../../tools/ajax' 2 | import store from '../../' 3 | import { bus } from '../../../components/global/ws' 4 | 5 | const init = () => { 6 | get('imports', (err, resp) => { 7 | store.commit('syncImports', resp); 8 | }); 9 | } 10 | 11 | const sync = () => { 12 | bus.$on('import_update', data => { 13 | store.commit('updateImport', data); 14 | }); 15 | } 16 | 17 | export default function() { 18 | init(); 19 | sync(); 20 | } -------------------------------------------------------------------------------- /web/vue/src/store/modules/messages/mutations.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magicdude4eva/MD4ECryptoBot/1812ec881d1ea08a11e162ae3774ff6083b6a535/web/vue/src/store/modules/messages/mutations.js -------------------------------------------------------------------------------- /web/vue/src/store/modules/notifications/mutations.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import _ from 'lodash' 3 | 4 | export const setGlobalWarning = (state, warning) => { 5 | state.warnings[warning.key] = warning.value; 6 | return state; 7 | } -------------------------------------------------------------------------------- /web/vue/src/store/modules/notifications/sync.js: -------------------------------------------------------------------------------- 1 | import store from '../../' 2 | import { bus } from '../../../components/global/ws' 3 | 4 | const init = () => {} 5 | 6 | const sync = () => { 7 | bus.$on('WS_STATUS_CHANGE', ws => { 8 | return store.commit('setGlobalWarning', {key: 'connected', value: ws.connected}); 9 | }); 10 | } 11 | 12 | export default function() { 13 | init(); 14 | sync(); 15 | } -------------------------------------------------------------------------------- /web/vue/src/store/modules/stratrunners/mutations.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | export const addStratrunner = (state, runner) => { 4 | state.stratrunners.push(runner); 5 | return state; 6 | } 7 | 8 | export const syncStratrunners = (state, runners) => { 9 | state.stratrunners = runners; 10 | return state; 11 | } 12 | 13 | export const updateStratrunner = (state, update) => { 14 | let index = state.stratrunners.findIndex(i => i.id === update.gekko_id); 15 | let item = state.stratrunners[index]; 16 | if(!item) 17 | return state; 18 | 19 | let updated = Vue.util.extend(item, update.updates); 20 | Vue.set(state.stratrunners, index, updated); 21 | 22 | return state; 23 | } 24 | export const addTradeToStratrunner = (state, update) => { 25 | let index = state.stratrunners.findIndex(i => i.id === update.gekko_id); 26 | let item = state.stratrunners[index]; 27 | if(!item) 28 | return state; 29 | 30 | let updated = Vue.util.extend({}, item); 31 | updated.trades.push(update.trade); 32 | Vue.set(state.stratrunners, index, updated); 33 | 34 | return state; 35 | } 36 | 37 | export const addRoundtripToStratrunner = (state, update) => { 38 | let index = state.stratrunners.findIndex(i => i.id === update.gekko_id); 39 | let item = state.stratrunners[index]; 40 | if(!item) 41 | return state; 42 | 43 | let updated = Vue.util.extend({}, item); 44 | updated.roundtrips.push(update.roundtrip); 45 | Vue.set(state.stratrunners, index, updated); 46 | 47 | return state; 48 | } -------------------------------------------------------------------------------- /web/vue/src/store/modules/stratrunners/sync.js: -------------------------------------------------------------------------------- 1 | import { get } from '../../../tools/ajax' 2 | import store from '../../' 3 | import { bus } from '../../../components/global/ws' 4 | import _ from 'lodash' 5 | 6 | const init = () => { 7 | get('gekkos', (err, resp) => { 8 | let runners = _.filter(resp, {type: 'leech'}); 9 | store.commit('syncStratrunners', runners); 10 | }); 11 | } 12 | 13 | const sync = () => { 14 | 15 | bus.$on('new_gekko', data => { 16 | if(data.gekko.type === 'leech') 17 | store.commit('addStratrunner', data.gekko); 18 | }); 19 | 20 | const update = (data) => { 21 | store.commit('updateStratrunner', data); 22 | } 23 | 24 | const trade = (data) => { 25 | store.commit('addTradeToStratrunner', data); 26 | } 27 | 28 | const roundtrip = (data) => { 29 | store.commit('addRoundtripToStratrunner', data); 30 | } 31 | 32 | bus.$on('report', update); 33 | bus.$on('trade', trade); 34 | bus.$on('update', update); 35 | bus.$on('startAt', update); 36 | bus.$on('lastCandle', update); 37 | bus.$on('firstCandle', update); 38 | bus.$on('roundtrip', roundtrip); 39 | } 40 | 41 | export default function() { 42 | init(); 43 | sync(); 44 | } -------------------------------------------------------------------------------- /web/vue/src/store/modules/watchers/mutations.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | export const addWatcher = (state, watcher) => { 4 | state.watchers.push(watcher); 5 | return state; 6 | } 7 | 8 | export const syncWatchers = (state, watchers) => { 9 | state.watchers = watchers; 10 | return state; 11 | } 12 | 13 | export const updateWatcher = (state, update) => { 14 | let index = state.watchers.findIndex(i => i.id === update.gekko_id); 15 | let item = state.watchers[index]; 16 | if(!item) 17 | return state; 18 | 19 | let updated = Vue.util.extend(item, update.updates); 20 | Vue.set(state.watchers, index, updated); 21 | 22 | return state; 23 | } -------------------------------------------------------------------------------- /web/vue/src/store/modules/watchers/sync.js: -------------------------------------------------------------------------------- 1 | import { get } from '../../../tools/ajax' 2 | import store from '../../' 3 | import { bus } from '../../../components/global/ws' 4 | import _ from 'lodash' 5 | 6 | const init = () => { 7 | get('gekkos', (err, resp) => { 8 | let watchers = _.filter(resp, {type: 'watcher'}); 9 | store.commit('syncWatchers', watchers); 10 | }); 11 | } 12 | 13 | const sync = () => { 14 | 15 | bus.$on('new_gekko', data => { 16 | if(data.gekko.type === 'watcher') 17 | store.commit('addWatcher', data.gekko); 18 | }); 19 | 20 | const update = (data) => { 21 | store.commit('updateWatcher', data); 22 | } 23 | 24 | bus.$on('update', update); 25 | bus.$on('startAt', update); 26 | bus.$on('lastCandle', update); 27 | bus.$on('firstCandle', update); 28 | } 29 | 30 | export default function() { 31 | init(); 32 | sync(); 33 | } -------------------------------------------------------------------------------- /web/vue/src/tools/ajax.js: -------------------------------------------------------------------------------- 1 | import superagent from 'superagent' 2 | import noCache from 'superagent-no-cache' 3 | import { restPath } from './api.js' 4 | 5 | const processResponse = next => (err, res) => { 6 | if(err) 7 | return next(err); 8 | 9 | if(!res.text) 10 | return next('no data'); 11 | 12 | let data = JSON.parse(res.text); 13 | 14 | next(false, data); 15 | } 16 | 17 | export const post = (to, data, next) => { 18 | superagent 19 | .post(restPath + to) 20 | .use(noCache) 21 | .send(data) 22 | .end(processResponse(next)); 23 | } 24 | 25 | export const get = (to, next) => { 26 | superagent 27 | .get(restPath + to) 28 | .use(noCache) 29 | .end(processResponse(next)); 30 | } 31 | -------------------------------------------------------------------------------- /web/vue/src/tools/api.js: -------------------------------------------------------------------------------- 1 | // global window.CONFIG 2 | 3 | const config = window.CONFIG.ui; 4 | const host = `${config.host}${config.port === 80 ? '' : `:${config.port}`}${config.path}api/`; 5 | 6 | // rest API path 7 | if(config.ssl) { 8 | var restPath = `https://${host}`; 9 | } else { 10 | var restPath = `http://${host}`; 11 | } 12 | 13 | // ws API path 14 | if(config.ssl) { 15 | var wsPath = `wss://${host}`; 16 | } else { 17 | var wsPath = `ws://${host}`; 18 | } 19 | 20 | export {wsPath,restPath}; 21 | -------------------------------------------------------------------------------- /web/vue/src/tools/marked.js: -------------------------------------------------------------------------------- 1 | const marked = require('marked'); 2 | 3 | // add `target='_blank'` to outgoing links 4 | 5 | // https://github.com/chjj/marked/pull/451#issuecomment-49976076 6 | 7 | var myRenderer = new marked.Renderer(); 8 | myRenderer.link = function(href, title, text) { 9 | var external, newWindow, out; 10 | external = /^https?:\/\/.+$/.test(href); 11 | newWindow = external || title === 'newWindow'; 12 | out = ""; 20 | }; 21 | 22 | marked.setOptions({renderer: myRenderer}); 23 | 24 | export default marked; -------------------------------------------------------------------------------- /web/vue/webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var webpack = require('webpack') 3 | var toml = require('toml') 4 | var fs = require('fs') 5 | 6 | module.exports = { 7 | entry: './src/main.js', 8 | output: { 9 | path: path.resolve(__dirname, './dist'), 10 | publicPath: '/dist/', 11 | filename: 'build.js' 12 | }, 13 | module: { 14 | rules: [ 15 | { 16 | test: /\.vue$/, 17 | loader: 'vue-loader', 18 | options: { 19 | // vue-loader options go here 20 | } 21 | }, 22 | { 23 | test: /\.js$/, 24 | loader: 'babel-loader', 25 | exclude: /node_modules/ 26 | }, 27 | { 28 | test: /\.(png|jpg|gif|svg)$/, 29 | loader: 'file-loader', 30 | options: { 31 | name: '[name].[ext]?[hash]' 32 | } 33 | }, 34 | { 35 | test: /\.json$/, 36 | loader: 'json-loader' 37 | } 38 | ] 39 | }, 40 | devServer: { 41 | historyApiFallback: true, 42 | noInfo: true 43 | }, 44 | devtool: '#eval-source-map' 45 | } 46 | 47 | if (process.env.NODE_ENV === 'production') { 48 | module.exports.devtool = '#source-map' 49 | // http://vue-loader.vuejs.org/en/workflow/production.html 50 | module.exports.plugins = (module.exports.plugins || []).concat([ 51 | new webpack.DefinePlugin({ 52 | NODE_ENV: JSON.stringify('"production"'), 53 | }), 54 | new webpack.optimize.UglifyJsPlugin({ 55 | compress: { 56 | warnings: false 57 | } 58 | }), 59 | new webpack.LoaderOptionsPlugin({ 60 | minimize: true 61 | }) 62 | ]) 63 | } else { 64 | module.exports.plugins = [ 65 | new webpack.DefinePlugin({ 66 | NODE_ENV: JSON.stringify('"development"'), 67 | }) 68 | ]; 69 | } 70 | --------------------------------------------------------------------------------