├── .dockerignore ├── .editorconfig ├── .env ├── .github ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md └── stale.yml ├── .gitignore ├── .prettierrc ├── .travis.yml ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── README.md ├── Watch_Me_Build_a_Trading_Bot (1).ipynb ├── appveyor.yml ├── 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 │ ├── TMA.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 ├── emitter.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 │ ├── paper_trading.md │ └── trading_bot.md ├── gekko-broker │ ├── introduction.md │ ├── sticky_order.md │ └── wrapper_api.md ├── installation │ ├── configuring_gekko_on_a_server.md │ ├── installing_gekko.md │ ├── installing_gekko_on_raspberry_pi_2_or_3.md │ ├── installing_gekko_on_windows.md │ ├── installing_gekko_on_windows_with_bash_on_windows_10.md │ ├── installing_gekko_using_docker.md │ └── updating_gekko.md ├── internals │ ├── architecture.md │ ├── budfox.md │ ├── events.md │ ├── gekko_ui.md │ ├── plugins.md │ └── server_api.md ├── introduction │ ├── about_gekko.md │ ├── getting_help.md │ ├── roadmap.md │ ├── scope.md │ ├── supported_exchanges.md │ └── supporting_the_project.md └── strategies │ ├── creating_a_strategy.md │ ├── gekko_indicators.md │ ├── introduction.md │ ├── talib_indicators.md │ └── tulip_indicators.md ├── exchange ├── .npmignore ├── README.md ├── dependencyCheck.js ├── exchangeChecker.js ├── exchangeErrors.js ├── exchangeUtils.js ├── gekkoBroker.js ├── orders │ ├── index.js │ ├── limit.js │ ├── order.js │ ├── states.js │ └── sticky.js ├── package-lock.json ├── package.json ├── portfolioManager.js ├── trigger.js ├── triggers │ ├── index.js │ └── trailingStop.js ├── util │ └── genMarketFiles │ │ ├── update-binance.js │ │ ├── update-bitfinex.js │ │ ├── update-coinbase.js │ │ ├── update-coinfalcon.js │ │ └── update-kraken.js └── wrappers │ ├── DEBUG_exchange-simulator.js │ ├── binance-markets.json │ ├── binance.js │ ├── bitcoin-co-id.js.old │ ├── bitfinex-markets.json │ ├── bitfinex.js │ ├── bitfinex_v2.js.prep │ ├── bitstamp.js.old │ ├── bittrex.js │ ├── btc-markets.js.old │ ├── btcc.js.old │ ├── bx.in.th.js.old │ ├── cexio.js.old │ ├── coinbase-markets.json │ ├── coinfalcon-markets.json │ ├── coinfalcon.js │ ├── coingi.js │ ├── exmo-markets.json │ ├── exmo.js │ ├── gdax.js │ ├── gemini.js.old │ ├── kraken-markets.json │ ├── kraken.js │ ├── lakebtc.js │ ├── luno.js │ ├── mexbt.js.old │ ├── mtgox.js.old │ ├── okcoin.js.old │ ├── poloniex-markets.json │ ├── poloniex.js │ ├── quadriga-markets.json │ ├── quadriga.js.old │ ├── therocktrading-markets.json │ ├── therocktrading.js │ ├── wex.nz.js.old │ └── zaif.jp.js.old ├── gekko.js ├── importers └── exchanges │ ├── binance.js │ ├── bitfinex.js │ ├── btcc.js │ ├── coinfalcon.js │ ├── gdax.js │ ├── kraken.js │ ├── luno.js │ ├── poloniex.js │ └── therocktrading.js ├── logs └── .gitignore ├── package-lock.json ├── package.json ├── plugins.js ├── plugins ├── adviceLogger.js ├── backtestResultExporter.js ├── blotter.js ├── campfire.js ├── candleUploader.js ├── childToParent.js ├── eventLogger.js ├── ifttt.js ├── ircbot.js ├── kodi.js ├── mailer.js ├── mongodb │ ├── handle.js │ ├── reader.js │ ├── scanner.js │ ├── util.js │ └── writer.js ├── paperTrader │ └── paperTrader.js ├── performanceAnalyzer │ ├── 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 │ └── trader.js ├── tradingAdvisor │ ├── asyncIndicatorRunner.js │ ├── baseTradingMethod.js │ └── tradingAdvisor.js ├── twitter.js ├── webserver.js └── xmppbot.js ├── sample-config.js ├── strategies ├── CCI.js ├── DEBUG_single-advice.js ├── DEBUG_toggle-advice.js ├── DEMA.js ├── MACD.js ├── PPO.js ├── RSI.js ├── StochRSI.js ├── TMA.js ├── TSI.js ├── UO.js ├── custom.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 ├── test ├── _prepare.js ├── candleBatcher.js ├── exchanges │ ├── bitstamp.js │ └── data │ │ └── bitstamp_trades.json ├── indicators │ ├── cci.js │ ├── dema.js │ ├── ema.js │ ├── macd.js │ ├── ppo.js │ ├── rsi.js │ ├── sma.js │ └── smma.js ├── marketFetcher.js ├── plugins │ └── portfolioManager.js ├── test-config.json ├── tradeBatcher.js └── triggers │ └── trailingStop.js └── web ├── apiKeyManager.js ├── baseUIconfig.js ├── isWindows.js ├── routes ├── apiKeys.js ├── backtest.js ├── baseConfig.js ├── configPart.js ├── deleteGekko.js ├── exchanges.js ├── getCandles.js ├── import.js ├── info.js ├── list.js ├── scanDatasets.js ├── scanDateRange.js ├── startGekko.js ├── stopGekko.js └── strategies.js ├── server.js ├── state ├── cache.js ├── gekkoManager.js ├── listManager.js ├── logger.js └── reduceState.js └── vue ├── babel.config.js ├── dist ├── UIconfig.js ├── app.5e99ecf7.js ├── app.5e99ecf7.js.map ├── app.730569ff.css ├── chunk-vendors.b9a11975.js ├── chunk-vendors.b9a11975.js.map ├── index.html ├── static │ └── gekko.jpg └── vendor │ ├── d3.js │ ├── furtive.min.css │ ├── humanize-duration.js │ ├── moment.js │ ├── reconnecting-websocket.min.js │ ├── select-arrow.png │ └── toml.js ├── package-lock.json ├── package.json ├── public ├── UIconfig.js ├── index.html ├── static │ └── gekko.jpg └── vendor │ ├── d3.js │ ├── furtive.min.css │ ├── humanize-duration.js │ ├── moment.js │ ├── reconnecting-websocket.min.js │ ├── select-arrow.png │ └── toml.js ├── 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 │ │ ├── singleGekko.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 │ │ ├── gekkos │ │ ├── mutations.js │ │ └── sync.js │ │ ├── imports │ │ ├── mutations.js │ │ └── sync.js │ │ ├── messages │ │ └── mutations.js │ │ └── notifications │ │ ├── mutations.js │ │ └── sync.js └── tools │ ├── ajax.js │ ├── api.js │ └── marked.js └── vue.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 3 | USE_SSL=0 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | **Note: this is the technical bug tracker, please use other platforms for getting support and starting a (non technical) discussion. See the [getting help page](https://gekko.wizb.it/docs/introduction/getting-help.html) for details.** 2 | 3 | **I'm submitting a ...** 4 | [ ] bug report 5 | [ ] question about the decisions made in the repository 6 | 7 | **Action taken** (what you did) 8 | 9 | 10 | **Expected result** (what you hoped would happen) 11 | 12 | 13 | **Actual result** (unexpected outcome) 14 | 15 | 16 | **Other information** (e.g. detailed explanation, stacktraces, related issues, suggestions how to fix, links for us to have context, eg. stackoverflow, etc) 17 | 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 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 60 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 7 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - pinned 8 | - security 9 | - important 10 | # Label to use when marking an issue as stale 11 | staleLabel: wontfix 12 | # Comment to post when marking an issue as stale. Set to `false` to disable 13 | markComment: > 14 | This issue has been automatically marked as stale because it has not had 15 | recent activity. It will be closed if no further activity occurs. Thank you 16 | for your contributions. If you feel this is very a important issue please 17 | reach out the maintainer of this project directly via e-mail: gekko at mvr dot me. 18 | # Comment to post when closing a stale issue. Set to `false` to disable 19 | closeComment: false -------------------------------------------------------------------------------- /.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 | config.js 48 | config-*.js 49 | private-*.js 50 | private-*.toml 51 | SECRET-api-keys.json 52 | exchange/nusadua -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "singleQuote": true 4 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "8.11.2" -------------------------------------------------------------------------------- /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 Gekko dependencies 15 | COPY package.json . 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 | # Install Gekko Broker dependencies 21 | WORKDIR exchange 22 | COPY exchange/package.json . 23 | RUN npm install --production && \ 24 | npm cache clean --force 25 | WORKDIR ../ 26 | 27 | # Bundle app source 28 | COPY . /usr/src/app 29 | 30 | EXPOSE 3000 31 | RUN chmod +x /usr/src/app/docker-entrypoint.sh 32 | ENTRYPOINT ["/usr/src/app/docker-entrypoint.sh"] 33 | 34 | CMD ["--config", "config.js", "--ui"] 35 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ## Overview 3 | 4 | This is the code for [this](https://youtu.be/myydDX-us4o) video on Youtube by Siraj Raval called Watch me Build a Trading Bot. 5 | 6 | ## Dependencies 7 | 8 | * [gekko](https://github.com/askmike/gekko) 9 | * reinforcejs 10 | * Bitcoin API 11 | 12 | ## Usage 13 | 14 | ### Step 1 - Install dependencies with NPM 15 | 16 | 'npm install --only=production' 17 | 18 | ### Step 2 - Start the bot 19 | 20 | 'node gekko --ui' 21 | 22 | ### Step 3 - Follow the instructions [here](https://github.com/SirTificate/gekko-neuralnet) to install a custom neural strategy 23 | 24 | ### Step 4 - Once that works, create your own custom deep RL strategy using [this](https://github.com/karpathy/reinforcejs) library. 25 | 26 | 27 | ## Credits 28 | 29 | Credits go to [AskMike](https://github.com/askmike/gekko) for the starter code. 30 | 31 | 32 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | init: 2 | - git config --global core.autocrlf input 3 | 4 | environment: 5 | nodejs_version: "9" 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 -------------------------------------------------------------------------------- /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 | weight = 21 2 | 3 | [thresholds] 4 | down = -0.025 5 | up = 0.025 6 | -------------------------------------------------------------------------------- /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/TMA.toml: -------------------------------------------------------------------------------- 1 | short = 7 2 | medium = 25 3 | long = 99 4 | -------------------------------------------------------------------------------- /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 | // relay a marketUpdate event 34 | this.marketDataProvider.on( 35 | 'marketUpdate', 36 | e => this.emit('marketUpdate', e) 37 | ); 38 | 39 | // relay a marketStart event 40 | this.marketDataProvider.on( 41 | 'marketStart', 42 | e => this.emit('marketStart', e) 43 | ); 44 | 45 | // Output the candles 46 | this.candleManager.on( 47 | 'candles', 48 | this.pushCandles 49 | ); 50 | 51 | // on every `tick` retrieve trade data 52 | this.heart.on( 53 | 'tick', 54 | this.marketDataProvider.retrieve 55 | ); 56 | 57 | // on new trade data create candles 58 | this.marketDataProvider.on( 59 | 'trades', 60 | this.candleManager.processTrades 61 | ); 62 | 63 | this.heart.pump(); 64 | } 65 | 66 | var Readable = require('stream').Readable; 67 | 68 | BudFox.prototype = Object.create(Readable.prototype, { 69 | constructor: { value: BudFox } 70 | }); 71 | 72 | BudFox.prototype._read = function noop() {} 73 | 74 | BudFox.prototype.pushCandles = function(candles) { 75 | _.each(candles, this.push); 76 | } 77 | 78 | module.exports = BudFox; 79 | -------------------------------------------------------------------------------- /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 | 14 | var CandleCreator = require(dirs.budfox + 'candleCreator'); 15 | 16 | var Manager = function() { 17 | _.bindAll(this); 18 | 19 | this.candleCreator = new CandleCreator; 20 | 21 | this.candleCreator 22 | .on('candles', this.relayCandles); 23 | }; 24 | 25 | util.makeEventEmitter(Manager); 26 | Manager.prototype.processTrades = function(tradeBatch) { 27 | this.candleCreator.write(tradeBatch); 28 | } 29 | 30 | Manager.prototype.relayCandles = function(candles) { 31 | this.emit('candles', candles); 32 | } 33 | 34 | module.exports = Manager; 35 | -------------------------------------------------------------------------------- /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 | 14 | const Manager = function(config) { 15 | 16 | _.bindAll(this); 17 | 18 | // fetch trades 19 | this.source = new MarketFetcher(config); 20 | 21 | // relay newly fetched trades 22 | this.source 23 | .on('trades batch', this.relayTrades); 24 | } 25 | 26 | util.makeEventEmitter(Manager); 27 | 28 | // HANDLERS 29 | Manager.prototype.retrieve = function() { 30 | this.source.fetch(); 31 | } 32 | 33 | 34 | Manager.prototype.relayTrades = function(batch) { 35 | this.sendMarketStart(batch); 36 | this.emit('marketUpdate', batch.last.date); 37 | 38 | this.emit('trades', batch); 39 | } 40 | 41 | Manager.prototype.sendMarketStart = _.once(function(batch) { 42 | this.emit('marketStart', batch.first.date); 43 | }); 44 | 45 | module.exports = Manager; -------------------------------------------------------------------------------- /core/emitter.js: -------------------------------------------------------------------------------- 1 | // Gekko uses a custom event emitter within the GekkoStream (the plugins) to guarantee 2 | // the correct order of events that are triggered by eachother. Turns sync events from 3 | // LIFO into a FIFO stack based model. 4 | // 5 | // More details here: https://forum.gekko.wizb.it/thread-56579.html 6 | 7 | const util = require('util'); 8 | const events = require('events'); 9 | const NativeEventEmitter = events.EventEmitter; 10 | 11 | const GekkoEventEmitter = function() { 12 | NativeEventEmitter.call(this); 13 | this.defferedEvents = []; 14 | } 15 | 16 | util.inherits(GekkoEventEmitter, NativeEventEmitter); 17 | 18 | // push to stack 19 | GekkoEventEmitter.prototype.deferredEmit = function(name, payload) { 20 | this.defferedEvents.push({name, payload}); 21 | } 22 | 23 | // resolve FIFO 24 | GekkoEventEmitter.prototype.broadcastDeferredEmit = function() { 25 | if(this.defferedEvents.length === 0) 26 | return false; 27 | 28 | const event = this.defferedEvents.shift(); 29 | 30 | this.emit(event.name, event.payload); 31 | return true; 32 | } 33 | 34 | module.exports = GekkoEventEmitter; -------------------------------------------------------------------------------- /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: method, message: 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.write = _.noop; 79 | } 80 | 81 | module.exports = new Log; -------------------------------------------------------------------------------- /core/markets/realtime.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | 3 | const util = require('../util'); 4 | const dirs = util.dirs(); 5 | 6 | const exchangeChecker = require(dirs.gekko + 'exchange/exchangeChecker'); 7 | const 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 | const 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/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 | var os = require('os'); 5 | 6 | var util = require('../../util'); 7 | var dirs = util.dirs(); 8 | 9 | var dateRangeScan = require('../dateRangeScan/parent'); 10 | 11 | module.exports = function(config, done) { 12 | 13 | util.setConfig(config); 14 | 15 | var adapter = config[config.adapter]; 16 | var scan = require(dirs.gekko + adapter.path + '/scanner'); 17 | 18 | scan((err, markets) => { 19 | 20 | if(err) 21 | return done(err); 22 | 23 | let numCPUCores = os.cpus().length; 24 | if(numCPUCores === undefined) 25 | numCPUCores = 1; 26 | async.eachLimit(markets, numCPUCores, (market, next) => { 27 | 28 | let marketConfig = _.clone(config); 29 | marketConfig.watch = market; 30 | 31 | dateRangeScan(marketConfig, (err, ranges) => { 32 | if(err) 33 | return next(); 34 | 35 | market.ranges = ranges; 36 | 37 | next(); 38 | }); 39 | 40 | }, err => { 41 | let resp = { 42 | datasets: [], 43 | errors: [] 44 | } 45 | markets.forEach(market => { 46 | if(market.ranges) 47 | resp.datasets.push(market); 48 | else 49 | resp.errors.push(market); 50 | }) 51 | done(err, resp); 52 | }) 53 | }); 54 | } 55 | -------------------------------------------------------------------------------- /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 | var debug = typeof v8debug === 'object'; 6 | if (debug) { 7 | process.execArgv = []; 8 | } 9 | 10 | task = new ForkTask(fork(__dirname + '/child')); 11 | 12 | task.send('start', config); 13 | 14 | task.once('ranges', ranges => { 15 | return done(false, ranges); 16 | }); 17 | task.on('exit', code => { 18 | if(code !== 0) 19 | done('ERROR, unable to scan dateranges, please check the console.'); 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /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 | }); 25 | 26 | process.on('disconnect', function() { 27 | process.exit(0); 28 | }) 29 | -------------------------------------------------------------------------------- /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 | var debug = typeof v8debug === 'object'; 40 | if (debug) { 41 | process.execArgv = []; 42 | } 43 | 44 | const child = fork(__dirname + '/child'); 45 | 46 | const message = { 47 | what: 'start', 48 | config 49 | } 50 | 51 | const done = _.once(callback); 52 | 53 | child.on('message', function(m) { 54 | if(m === 'ready') 55 | return child.send(message); 56 | 57 | // else we are done and have candles! 58 | done(null, m); 59 | if (this.connected) { 60 | this.disconnect(); 61 | } 62 | }); 63 | 64 | child.on('exit', code => { 65 | if(code !== 0) 66 | done('ERROR, unable to load candles, please check the console.'); 67 | }); 68 | } 69 | -------------------------------------------------------------------------------- /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 | }) 56 | 57 | process 58 | .on('unhandledRejection', (message, p) => { 59 | console.error('unhandledRejection', message); 60 | process.send({type: 'error', message: message}); 61 | }) 62 | .on('uncaughtException', err => { 63 | console.error('uncaughtException', err); 64 | process.send({type: 'error', error: err}); 65 | process.exit(1); 66 | }); -------------------------------------------------------------------------------- /core/workers/pipeline/messageHandlers/backtestHandler.js: -------------------------------------------------------------------------------- 1 | // Relay the backtest message it when it comes in. 2 | 3 | module.exports = done => { 4 | let backtest; 5 | 6 | return { 7 | message: message => { 8 | if(message.type === 'error') { 9 | done(message.error); 10 | } 11 | 12 | if(message.backtest) { 13 | done(null, message.backtest); 14 | } 15 | }, 16 | exit: status => { 17 | if(status !== 0) { 18 | if(backtest) 19 | console.error('Child process died after finishing backtest'); 20 | else 21 | done('Child process has died.'); 22 | } 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /core/workers/pipeline/messageHandlers/importerHandler.js: -------------------------------------------------------------------------------- 1 | module.exports = cb => { 2 | 3 | return { 4 | message: message => { 5 | 6 | if(message.event === 'marketUpdate') 7 | cb(null, { 8 | done: false, 9 | latest: message.payload 10 | }) 11 | 12 | else if(message.type === 'error') { 13 | cb(message.error); 14 | } 15 | 16 | else if(message.type === 'log') 17 | console.log(message.log); 18 | }, 19 | exit: status => { 20 | if(status !== 0) 21 | return cb('Child process has died.'); 22 | else 23 | cb(null, { done: true }); 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /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 | } 12 | 13 | else 14 | cb(null, message); 15 | 16 | }, 17 | exit: status => { 18 | if(status !== 0) 19 | cb('Child process has died.'); 20 | else 21 | cb(null, { done: true }); 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /core/workers/pipeline/parent.js: -------------------------------------------------------------------------------- 1 | var fork = require('child_process').fork; 2 | 3 | module.exports = (mode, config, callback) => { 4 | var debug = typeof v8debug === 'object'; 5 | if (debug) { 6 | process.execArgv = []; 7 | } 8 | 9 | var child = fork(__dirname + '/child'); 10 | 11 | // How we should handle client messages depends 12 | // on the mode of the Pipeline that is being ran. 13 | var handle = require('./messageHandlers/' + mode + 'Handler')(callback); 14 | 15 | var message = { 16 | what: 'start', 17 | mode: mode, 18 | config: config 19 | }; 20 | 21 | child.on('message', function(m) { 22 | if(m === 'ready') 23 | return child.send(message); 24 | 25 | if(m === 'done') 26 | return child.send({what: 'exit'}); 27 | 28 | handle.message(m); 29 | }); 30 | 31 | child.on('exit', handle.exit); 32 | 33 | return child; 34 | } 35 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | gekko: 4 | build: ./ 5 | volumes: 6 | - ./volumes/gekko/history:/usr/src/app/history 7 | - ./config.js:/usr/src/app/config.js 8 | links: 9 | - redis 10 | # - postgresql 11 | environment: 12 | - HOST 13 | - PORT 14 | - USE_SSL 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/dist/UIconfig.js 4 | sed -i 's/localhost/'${HOST}'/g' /usr/src/app/web/vue/dist/UIconfig.js 5 | sed -i 's/3000/'${PORT}'/g' /usr/src/app/web/vue/dist/UIconfig.js 6 | if [[ "${USE_SSL:-0}" == "1" ]] ; then 7 | sed -i 's/ssl: false/ssl: true/g' /usr/src/app/web/vue/dist/UIconfig.js 8 | fi 9 | exec node gekko "$@" 10 | -------------------------------------------------------------------------------- /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 --config config.js --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 | - `performanceAnalyzer` - enable. 20 | 21 | Turn off the paperTrader (there can only be 1 trade plugin active per instance). 22 | 23 | Once done, run Gekko like so: 24 | 25 | node gekko --config your-config-file.js 26 | -------------------------------------------------------------------------------- /docs/extending/add_a_plugin.md: -------------------------------------------------------------------------------- 1 | # Plugins 2 | 3 | A plugin is a low level module or plugin that can act upon events bubbling 4 | through Gekko. If you want to have custom functionality so that your rocket 5 | flies to the moon as soon as the price hits X you should create a plugin for it. 6 | 7 | All plugins live in `gekko/plugins`. 8 | 9 | Note that in order to use custom plugins, you have to run Gekko over [the commandline](../commandline/about_the_commandline.md). 10 | 11 | ## Existing plugins: 12 | 13 | - Candle Store: save trades to disk. 14 | - Mailer: mail trading advice to your gmail account. 15 | - Pushbullet: send messages to Pushbullet devices. 16 | - Telegram: send messages over Telegram. 17 | - IRC bot: logs Gekko on in an irc channel and lets users communicate with it. 18 | - Paper Trader: simulates trades and calculates profit over these (and logs profit). 19 | - Trading advisor (internal): calculates advice based on market data. 20 | - Redis beacon (advanced): [see below!](#redis-beacon) 21 | 22 | *And more! Take a look in the `gekko/plugins folder.`* 23 | 24 | ## Implementing a new plugin 25 | 26 | If you want to add your own plugin you need to expose a constructor function inside 27 | `plugins/[slugname of plugin].js`. The object needs methods based on which event you want 28 | to listen to. All events can be found in [the events page](../architecture/events.md). 29 | 30 | You also need to add an entry for your plugin inside `plugins.js` which registers your plugin for use with Gekko. Finally you need to add a configuration object to `sample-config.js` with at least: 31 | 32 | config.[slug name of plugin] = { 33 | enabled: true 34 | } 35 | 36 | Besides enabled you can also add other configurables here which users can set themselves. 37 | 38 | That's it! Don't forget to create a pull request of the awesome plugin you've just created! 39 | -------------------------------------------------------------------------------- /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 | ### Backtesters 7 | - [japonicus](https://github.com/Gab0/japonicus) 8 | - [gekkoga](https://github.com/gekkowarez/gekkoga) 9 | - [Gekko-BacktestTool](https://github.com/xFFFFF/GekkoBacktestTool) 10 | - [Gekko Automated Backtest](https://github.com/tommiehansen/gab) 11 | - [Gekko Warez Bruteforce Backtester](https://github.com/gekkowarez/bruteforce) 12 | 13 | ### UI mode 14 | - [Unofficial Material UI](https://github.com/H256/gekko-quasar-ui) 15 | - [Controlling bots](https://github.com/CyborgDroid/gekko-python) 16 | 17 | ### CLI mode 18 | - [Batch generation of configuration files](https://github.com/bettimms/multi-gekko) 19 | 20 | ### Plugins 21 | - [Store trades in Google SpreadSheet](https://github.com/RJPGriffin/google-forms-gekko-plugin) 22 | -------------------------------------------------------------------------------- /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/features/paper_trading.md: -------------------------------------------------------------------------------- 1 | # Paper trading 2 | 3 | Gekko can automatically run a strategy over the live markets and simulate in realtime what happen if you would have traded on its signals. Paper trading and [backtesting](./backtesting.md) are the two simulation modes that come with Gekko. It's a great way to experiment with strategies without putting your money on the line. 4 | 5 | You can start a paper trader by going to live gekkos and clicking on "Start a new live Gekko". 6 | 7 | Keep in mind that a paper trader is a simulation, and the accuracy depends on the market you decide to run it on (you'll get pretty accurate results on big markets like USD/BTC). You can read more about the details and limitations of the simulation on [the backtesting page](./backtesting.md#Simplified-simulation). 8 | -------------------------------------------------------------------------------- /docs/features/trading_bot.md: -------------------------------------------------------------------------------- 1 | # Trading bot 2 | 3 | Once you have run enough simulations (using [backtesting](./backtesting.md) and [paper trading](./paper_trading.md)) and you are confident in your strategy you can use Gekko as a trading bot. 4 | 5 | Gekko will run your strategy on the live market and automatically trade on your exchange account when trade signals come out of your strategy. 6 | 7 | ## Preparation 8 | 9 | 1. Make sure you are fully confident in your strategy! If you want to play around use either the [paper trader](./paper_trading.md) or the [backtester](./backtesting.md). Once you are confident continue with this list. 10 | 2. Gekko will need to have API keys to your exchange account that have permissions to view balances and orders and create new orders. Keep in mind: 11 | - Gekko does NOT need withdrawal access, for your safety DO NOT create API keys that can withdraw. 12 | - Make sure you only use the API key for Gekko, and for nothing else. If in doubt create a new key (and remove stale ones). 13 | - If possible try to restrict the API key to the IP address you will run Gekko from (this makes moest sense in server environments) 14 | 3. Start your gekko through either the UI or the commandline interface! 15 | 16 | ## Notes 17 | 18 | Gekko will trade on the market you configured that consists of two currencies (for example USD/BTC): 19 | - Try to not trade either of these currencies on the account you use with Gekko. (in the example above: don't trade any USD nor any BTC). 20 | - Try to not withdraw or deposit more of either of these currencies. 21 | 22 | While Gekko will handle the situations above, all the profit calculations will be incorrect since your balances are taken into account while calculating profits. 23 | -------------------------------------------------------------------------------- /docs/gekko-broker/sticky_order.md: -------------------------------------------------------------------------------- 1 | # Sticky Order 2 | 3 | An advanced order that stays at the top of the book (until the optional limit). The order will automatically stick to the best BBO until the complete amount has been filled. 4 | 5 | TODO: 6 | 7 | - implement fallback for when this order is alone at the top, some spread before everyone else 8 | - finalize API 9 | - add more events / ways to debug 10 | - pull ticker data out of this order market data should flow from the broker (so we can easier move to at least public websocket streams). 11 | 12 | ## Example usage 13 | 14 | const Broker = require('gekko-broker'); 15 | 16 | const gdax = new Broker({ 17 | currency: 'EUR', 18 | asset: 'BTC', 19 | 20 | exchange: 'gdax', 21 | 22 | // Enables access to private endpoints. 23 | // Needed to create orders and fetch portfolio 24 | private: true, 25 | 26 | key: 'x', 27 | secret: 'y', 28 | passphrase: 'z' 29 | }); 30 | 31 | gdax.portfolio.setBalances(console.log); 32 | 33 | const type = 'sticky'; 34 | const amount = 0.5; 35 | const side = 'buy'; 36 | const limit = 6555; 37 | 38 | const order = gdax.createOrder(type, side, amount, { limit }); 39 | order.on('statusChange', status => console.log(status)); 40 | order.on('filled', result => console.log(result)); 41 | order.on('completed', () => { 42 | order.createSummary(summary => console.log) 43 | }); 44 | 45 | // mutate like so 46 | 47 | // order.moveAmount(1); 48 | // order.moveLimit(6666); -------------------------------------------------------------------------------- /docs/gekko-broker/wrapper_api.md: -------------------------------------------------------------------------------- 1 | # Wrapper API 2 | 3 | Gekko Broker is a library that sits between trading applications and Gekko Broker Exchange Wrappers. Which means it has two APIs to communicate with other code: 4 | 5 | ![diagram describing Gekko Broker API interface](https://user-images.githubusercontent.com/969743/41892153-566293a0-7941-11e8-9998-7a5b5b554ffd.png) 6 | 7 | This document descibres the API layer between the exchange wrappers and Gekko Broker. 8 | 9 | ## Wrapper API spec 10 | 11 | The current API documentation is currently located [here](../extending/add_an_exchange.md). 12 | 13 | ## Wrapper API Changelog 14 | 15 | ### Gekko 0.5.x to Gekko (Broker) 0.6.0 16 | 17 | NOTE: this API design might still have minor changes leading up to the release of Gekko 0.6. See [this thread](https://forum.gekko.wizb.it/thread-57279-post-59207.html) for more information. 18 | 19 | - The wrapper files are now nested different (from `gekko/exchanges` to `gekkobroker/wrappers` (which equals `gekko/exchange/wrappers` for Gekko users). 20 | - cancelOrder now requires a second parameter to be passed (that indicates whether the order was filled before it was canceled), see [details](https://github.com/askmike/gekko/commit/0e301f7d66e24ec97327f5f01380f691cc2d3725#diff-dbfe320ca090e208be32459d98fc11ed). 21 | - checkOrder now expects an object with a few properties to be returned, see [details](https://github.com/askmike/gekko/commit/e0d4a7362cd74b4b4f50759b1012ce489ea44a0c#diff-dbfe320ca090e208be32459d98fc11ed). 22 | - Error handling has gotten a lot more complex, with an updated error interface between a retry system (provided by Gekko) and the exchange wrapper. [Read more here](https://github.com/askmike/gekko/commit/e0d4a7362cd74b4b4f50759b1012ce489ea44a0c#diff-dbfe320ca090e208be32459d98fc11ed). 23 | -------------------------------------------------------------------------------- /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 | If running behind an SSL-terminating proxy, make sure to set `USE_SSL=1` to tell the Gekko to use the HTTPS and WSS protocols instead of the default HTTP and WS protocols. 24 | 25 | To see logs: `docker logs -f gekko_gekko_1`. View which dockers are running by executing `docker ps`. 26 | -------------------------------------------------------------------------------- /docs/installation/updating_gekko.md: -------------------------------------------------------------------------------- 1 | # Updating Gekko 2 | 3 | ## Prepare 4 | 5 | Before updating your local version of Gekko it's good to keep a few things in mind: 6 | 7 | - If you have configured a UIconfig (to run Gekko on a server for example) make a copy of the UIconfig file (stored in `gekko/web/vue/dist/`) first and call it something else (such as `my-UIconfig.js`). After that revert the file to it's original state (which can be found in `gekko/web/baseUIconfig.js/`) prior to updating (or git will complain). 8 | - Check the changelog (found in the [releases page](https://github.com/askmike/gekko/releases)) for breaking changes) regarding: 9 | - (in case you changed the UIconfig) for changes in the UIconfig. 10 | - (in case you were using non standard strategies) strategy API, to make sure your strategies will run in the new version. 11 | - any other breaking changes specified inside the release log (such as required node and or browser versions). 12 | 13 | ## Updating to a new stable release 14 | 15 | Run the following commands inside the Gekko directory: 16 | 17 | git checkout stable 18 | git pull 19 | npm install --only=production 20 | cd exchange 21 | npm install --only=production 22 | cd .. 23 | 24 | ## Updating the develop branch 25 | 26 | The develop branch contains the most recent code as we are working on Gekko. In most scenarios this should considered less stable. But note that once a bug is found and fixed the fixes are applied here immediately while it takes the release of a new version to have these changes available in the stable branch. 27 | 28 | git checkout develop 29 | git pull 30 | npm install --only=production 31 | cd exchange 32 | npm install --only=production 33 | cd .. 34 | 35 | ### Compiling the frontend on the develop branch 36 | 37 | If you are using the UI and it changed since the latest release you need to manually recompile it since (as of v0.6.2) Gekko only ships compiled frontends with new releases. 38 | 39 | cd web/vue 40 | npm install 41 | npm run build 42 | cd ../.. -------------------------------------------------------------------------------- /docs/introduction/getting_help.md: -------------------------------------------------------------------------------- 1 | # Getting Help 2 | 3 | Are you getting stuck installing or using Gekko? Are you struggling with understanding Gekko? Running into problems creating your own plugin or strategy? Do you want to discuss awesome things you want to do with Gekko? This page will explain what to do! 4 | 5 | ## Online community 6 | 7 | If you have trouble with anything specific or you want to provide any type of feedback the online community is the place to be. You can find the community at: 8 | 9 | 1. [The online webforum](https://forum.gekko.wizb.it) 10 | 2. [The discord channel](https://discord.gg/26wMygt) 11 | 12 | These communities are the place for things like: 13 | 14 | - Solving installation problems 15 | - Solving installation errors 16 | - Helping set up Gekko on a server or embedded device 17 | - Requesting/requesting new features and or changes such as: 18 | - New exchanges to add 19 | - New plugins to add 20 | - General new features 21 | - Getting clarity on what Gekko can do and how it works 22 | 23 | ## Technical discussion 24 | 25 | The links above are always the first place to go to when discussing new features. But if you've found a clear bug or you want to **keep the complete discussion 100% technical (meaning completely code and architecture related)** you can use the [bug tracker on github](https://github.com/askmike/gekko/issues). Note that we are trying to keep all other discussions out of the bug tracker to keep the whole thing manageable :) 26 | -------------------------------------------------------------------------------- /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 | - Improve stability of new event and backtest engine 10 | - Port the old supported exchanges to Gekko Broker 11 | - new UI, see [here](https://forum.gekko.wizb.it/thread-1429-post-58996.html) 12 | - advanced orders from your strategy (Take Profit and Stop Loss) -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /exchange/.npmignore: -------------------------------------------------------------------------------- 1 | nusadua -------------------------------------------------------------------------------- /exchange/README.md: -------------------------------------------------------------------------------- 1 | # Gekko Broker 2 | 3 | see [the docs](https://gekko.wizb.it/docs/gekko-broker/introduction.html). -------------------------------------------------------------------------------- /exchange/dependencyCheck.js: -------------------------------------------------------------------------------- 1 | const deps = require('./package.json').dependencies; 2 | 3 | const missing = []; 4 | 5 | Object.keys(deps).forEach(dep => { 6 | try { 7 | require(dep); 8 | } catch(e) { 9 | if(e.code === 'MODULE_NOT_FOUND') { 10 | missing.push(dep); 11 | } 12 | } 13 | }); 14 | 15 | if(missing.length) { 16 | console.error( 17 | '\nThe following Gekko Broker dependencies are not installed: [', 18 | missing.join(', '), 19 | '].\n\nYou need to install them first, read here how:', 20 | 'https://gekko.wizb.it/docs/installation/installing_gekko.html#Installing-Gekko-39-s-dependencies\n' 21 | ); 22 | process.exit(1); 23 | } 24 | -------------------------------------------------------------------------------- /exchange/exchangeErrors.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | 3 | const ExchangeError = function(message) { 4 | _.bindAll(this); 5 | 6 | this.name = "ExchangeError"; 7 | this.message = message; 8 | } 9 | ExchangeError.prototype = new Error(); 10 | 11 | const ExchangeAuthenticationError = function(message) { 12 | _.bindAll(this); 13 | 14 | this.name = "ExchangeAuthenticationError"; 15 | this.message = message; 16 | } 17 | ExchangeAuthenticationError.prototype = new Error(); 18 | 19 | const RetryError = function(message) { 20 | _.bindAll(this); 21 | 22 | this.name = "RetryError"; 23 | this.retry = 5; 24 | this.message = message; 25 | } 26 | RetryError.prototype = new Error(); 27 | 28 | const AbortError = function(message) { 29 | _.bindAll(this); 30 | 31 | this.name = "AbortError"; 32 | this.message = message; 33 | } 34 | AbortError.prototype = new Error(); 35 | 36 | module.exports = { 37 | ExchangeError, 38 | ExchangeAuthenticationError, 39 | RetryError, 40 | AbortError 41 | }; 42 | 43 | -------------------------------------------------------------------------------- /exchange/orders/index.js: -------------------------------------------------------------------------------- 1 | const sticky = require('./sticky'); 2 | 3 | module.exports = { 4 | sticky 5 | } -------------------------------------------------------------------------------- /exchange/orders/order.js: -------------------------------------------------------------------------------- 1 | const EventEmitter = require('events'); 2 | const _ = require('lodash'); 3 | 4 | const exchangeUtils = require('../exchangeUtils'); 5 | const bindAll = exchangeUtils.bindAll; 6 | const isValidOrder = exchangeUtils.isValidOrder; 7 | const states = require('./states'); 8 | 9 | // base order 10 | 11 | class BaseOrder extends EventEmitter { 12 | constructor(api) { 13 | super(); 14 | 15 | this.api = api; 16 | 17 | this.checkInterval = api.interval || 1500; 18 | this.status = states.INITIALIZING; 19 | 20 | this.completed = false; 21 | this.completing = false; 22 | 23 | bindAll(this); 24 | } 25 | 26 | submit({side, amount, price, alreadyFilled}) { 27 | const check = isValidOrder({ 28 | market: this.market, 29 | api: this.api, 30 | amount, 31 | price 32 | }); 33 | 34 | if(!check.valid) { 35 | if(alreadyFilled) { 36 | // partially filled, but the remainder is too 37 | // small. 38 | return this.filled(); 39 | } 40 | 41 | this.emit('invalidOrder', check.reason); 42 | this.rejected(check.reason); 43 | } 44 | 45 | this.api[this.side](amount, this.price, this.handleCreate); 46 | } 47 | 48 | setData(data) { 49 | this.data = data; 50 | } 51 | 52 | emitStatus() { 53 | this.emit('statusChange', this.status); 54 | } 55 | 56 | cancelled() { 57 | this.status = states.CANCELLED; 58 | this.emitStatus(); 59 | this.completed = true; 60 | this.finish(); 61 | } 62 | 63 | rejected(reason) { 64 | this.rejectedReason = reason; 65 | this.status = states.REJECTED; 66 | this.emitStatus(); 67 | console.log(new Date, 'sticky rejected', reason) 68 | this.finish(); 69 | } 70 | 71 | filled(price) { 72 | this.status = states.FILLED; 73 | this.emitStatus(); 74 | this.completed = true; 75 | console.log(new Date, 'sticky filled') 76 | this.finish(true); 77 | } 78 | 79 | finish(filled) { 80 | this.completed = true; 81 | this.emit('completed', { 82 | status: this.status, 83 | filled 84 | }) 85 | } 86 | } 87 | 88 | module.exports = BaseOrder; -------------------------------------------------------------------------------- /exchange/orders/states.js: -------------------------------------------------------------------------------- 1 | const states = { 2 | // Not created 3 | INITIALIZING: 'INITIALIZING', 4 | 5 | // Created and send to the exchange, but no acknowledgement received yet 6 | SUBMITTED: 'SUBMITTED', 7 | 8 | // In the process of moving the order 9 | MOVING: 'MOVING', 10 | 11 | // Order is open on the exchange 12 | OPEN: 'OPEN', 13 | 14 | 15 | CHECKING: 'CHECKING', 16 | 17 | CHECKED: 'CHECKED', 18 | 19 | // the orders below indicate a fully completed order 20 | 21 | 22 | // Order is completely filled 23 | FILLED: 'FILLED', 24 | 25 | // Order was succesfully cancelled 26 | CANCELLED: 'CANCELLED', 27 | 28 | // Order was rejected by the exchange 29 | REJECTED: 'REJECTED', 30 | 31 | ERROR: 'ERROR' 32 | } 33 | 34 | module.exports = states; -------------------------------------------------------------------------------- /exchange/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gekko-broker", 3 | "version": "0.6.8", 4 | "description": "Gekko's order execution library for bitcoin & crypto exchanges", 5 | "main": "gekkoBroker.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "github.com/askmike/gekko" 12 | }, 13 | "keywords": [ 14 | "crypto", 15 | "bitcoin", 16 | "exchange", 17 | "execution", 18 | "trade" 19 | ], 20 | "author": "Mike van Rossum ", 21 | "license": "MIT", 22 | "dependencies": { 23 | "async": "^2.6.0", 24 | "binance": "^1.3.3", 25 | "bitfinex-api-node": "^2.0.0-beta", 26 | "bitx": "^1.5.0", 27 | "bluebird": "^3.5.1", 28 | "coinfalcon": "^1.0.3", 29 | "crypto-js": "^3.1.9-1", 30 | "gdax": "^0.7.0", 31 | "gekko-bittrex": "^0.8.5", 32 | "gekko-broker-poloniex": "^0.0.12", 33 | "kraken-api": "askmike/npm-kraken-api#a69dfb3eb296b9c795cfa48dd31edcdc1b3d5398", 34 | "lodash": "^4.17.5", 35 | "moment": "^2.22.1", 36 | "request-promise": "^4.2.2", 37 | "retry": "^0.12.0", 38 | "therocktrading": "^0.9.0" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /exchange/portfolioManager.js: -------------------------------------------------------------------------------- 1 | /* 2 | The Portfolio class holds data about the portfolio 3 | */ 4 | 5 | const _ = require('lodash'); 6 | const async = require('async'); 7 | const errors = require('./exchangeErrors'); 8 | // const EventEmitter = require('events'); 9 | 10 | class Portfolio { 11 | constructor(config, api) { 12 | _.bindAll(this); 13 | this.config = config; 14 | this.api = api; 15 | this.balances = {}; 16 | this.fee = null; 17 | } 18 | 19 | getBalance(fund) { 20 | return this.getFund(fund).amount; 21 | } 22 | 23 | // return the [fund] based on the data we have in memory 24 | getFund(fund) { 25 | return _.find(this.balances, function(f) { return f.name === fund}); 26 | } 27 | 28 | // convert into the portfolio expected by the performanceAnalyzer 29 | convertBalances(asset,currency) { // rename? 30 | var asset = _.find(this.balances, a => a.name === this.config.asset).amount; 31 | var currency = _.find(this.balances, a => a.name === this.config.currency).amount; 32 | 33 | return { 34 | currency, 35 | asset, 36 | balance: currency + (asset * this.ticker.bid) 37 | } 38 | } 39 | 40 | setBalances(callback) { 41 | let set = (err, fullPortfolio) => { 42 | if(err) { 43 | console.log(err); 44 | throw new errors.ExchangeError(err); 45 | } 46 | 47 | // only include the currency/asset of this market 48 | const balances = [ this.config.currency, this.config.asset ] 49 | .map(name => { 50 | let item = _.find(fullPortfolio, {name}); 51 | 52 | if(!item) { 53 | // assume we have 0 54 | item = { name, amount: 0 }; 55 | } 56 | 57 | return item; 58 | }); 59 | 60 | this.balances = balances; 61 | 62 | if(_.isFunction(callback)) 63 | callback(); 64 | } 65 | 66 | this.api.getPortfolio(set); 67 | } 68 | 69 | setFee(callback) { 70 | this.api.getFee((err, fee) => { 71 | if(err) 72 | throw new errors.ExchangeError(err); 73 | 74 | this.fee = fee; 75 | 76 | if(_.isFunction(callback)) 77 | callback(); 78 | }); 79 | } 80 | 81 | setTicker(ticker) { 82 | this.ticker = ticker; 83 | } 84 | 85 | } 86 | 87 | module.exports = Portfolio -------------------------------------------------------------------------------- /exchange/trigger.js: -------------------------------------------------------------------------------- 1 | // wraps around a low level trigger and feeds 2 | // it live market data. 3 | 4 | const _ = require('lodash'); 5 | 6 | const exchangeUtils = require('./exchangeUtils'); 7 | const bindAll = exchangeUtils.bindAll; 8 | 9 | const triggers = require('./triggers'); 10 | 11 | // @param api: a gekko broker wrapper instance 12 | // @param type: type of trigger to wrap 13 | // @param props: properties to feed to trigger 14 | class Trigger { 15 | constructor({api, type, props, onTrigger}) { 16 | this.onTrigger = onTrigger; 17 | this.api = api; 18 | 19 | this.isLive = true; 20 | 21 | // note: we stay on the safe side and trigger 22 | // as soon as the bid goes below trail. 23 | this.tickerProp = 'bid'; 24 | 25 | if(!_.has(triggers, type)) { 26 | throw new Error('Gekko Broker does not know trigger ' + type); 27 | } 28 | 29 | this.CHECK_INTERVAL = this.api.interval * 10; 30 | 31 | bindAll(this); 32 | this.trigger = new triggers[type]({ 33 | onTrigger: this.propogateTrigger, 34 | ...props 35 | }) 36 | 37 | this.scheduleFetch(); 38 | } 39 | 40 | scheduleFetch() { 41 | this.timout = setTimeout(this.fetch, this.CHECK_INTERVAL); 42 | } 43 | 44 | fetch() { 45 | if(!this.isLive) { 46 | return; 47 | } 48 | this.api.getTicker(this.processTicker) 49 | } 50 | 51 | processTicker(err, ticker) { 52 | if(!this.isLive) { 53 | return; 54 | } 55 | 56 | if(err) { 57 | return console.log('[GB/trigger] failed to fetch ticker:', err); 58 | } 59 | 60 | this.price = ticker[this.tickerProp]; 61 | 62 | this.trigger.updatePrice(this.price); 63 | this.scheduleFetch(); 64 | } 65 | 66 | cancel() { 67 | this.isLive = false; 68 | clearTimeout(this.timout); 69 | } 70 | 71 | propogateTrigger(payload) { 72 | if(!this.isLive) { 73 | return; 74 | } 75 | this.isLive = false; 76 | this.onTrigger(payload); 77 | } 78 | } 79 | 80 | module.exports = Trigger; -------------------------------------------------------------------------------- /exchange/triggers/index.js: -------------------------------------------------------------------------------- 1 | const trailingStop = require('./trailingStop'); 2 | 3 | module.exports = { trailingStop }; -------------------------------------------------------------------------------- /exchange/triggers/trailingStop.js: -------------------------------------------------------------------------------- 1 | const EventEmitter = require('events'); 2 | 3 | // Note: as of now only supports trailing the price going up (after 4 | // a buy), on trigger (when the price moves down) you should sell. 5 | 6 | 7 | // @param initialPrice: initial price, preferably buy price 8 | // @param trail: fixed offset from the price 9 | // @param onTrigger: fn to call when the stop triggers 10 | class TrailingStop extends EventEmitter { 11 | constructor({trail, initialPrice, onTrigger}) { 12 | super(); 13 | 14 | this.trail = trail; 15 | this.isLive = true; 16 | this.onTrigger = onTrigger; 17 | 18 | this.previousPrice = initialPrice; 19 | this.trailingPoint = initialPrice - this.trail; 20 | } 21 | 22 | updatePrice(price) { 23 | if(!this.isLive) { 24 | return; 25 | } 26 | 27 | if(price > this.trailingPoint + this.trail) { 28 | this.trailingPoint = price - this.trail; 29 | } 30 | 31 | this.previousPrice = price; 32 | 33 | if(price <= this.trailingPoint) { 34 | this.trigger(); 35 | } 36 | } 37 | 38 | updateTrail(trail) { 39 | if(!this.isLive) { 40 | return; 41 | } 42 | 43 | this.trail = trail; 44 | this.trailingPoint = this.previousPrice - this.trail; 45 | // recheck whether moving the trail triggered. 46 | this.updatePrice(this.previousPrice); 47 | } 48 | 49 | trigger() { 50 | if(!this.isLive) { 51 | return; 52 | } 53 | 54 | this.isLive = false; 55 | if(this.onTrigger) { 56 | this.onTrigger(this.previousPrice); 57 | } 58 | this.emit('trigger', this.previousPrice); 59 | } 60 | } 61 | 62 | module.exports = TrailingStop; -------------------------------------------------------------------------------- /exchange/util/genMarketFiles/update-binance.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const fs = require('fs'); 3 | const request = require('request-promise'); 4 | const Promise = require('bluebird'); 5 | 6 | 7 | let getOrderMinSize = currency => { 8 | if (currency === 'BTC') return 0.001; 9 | else if (currency === 'ETH') return 0.01; 10 | else if (currency === 'USDT') return 10; 11 | else return 1; 12 | }; 13 | 14 | const options = { 15 | url: 'https://www.binance.com/exchange/public/product', 16 | headers: { 17 | Connection: 'keep-alive', 18 | 'User-Agent': 'Request-Promise', 19 | }, 20 | json: true, 21 | }; 22 | 23 | request(options) 24 | .then(body => { 25 | if (!body && !body.data) { 26 | throw new Error('Unable to fetch product list, response was empty'); 27 | } 28 | 29 | let assets = _.uniqBy(_.map(body.data, market => market.baseAsset)); 30 | let currencies = _.uniqBy(_.map(body.data, market => market.quoteAsset)); 31 | let pairs = _.map(body.data, market => { 32 | return { 33 | pair: [market.quoteAsset, market.baseAsset], 34 | minimalOrder: { 35 | amount: parseFloat(market.minTrade), 36 | price: parseFloat(market.tickSize), 37 | order: getOrderMinSize(market.quoteAsset), 38 | }, 39 | }; 40 | }); 41 | 42 | return { assets: assets, currencies: currencies, markets: pairs }; 43 | }) 44 | .then(markets => { 45 | fs.writeFileSync('../../wrappers/binance-markets.json', JSON.stringify(markets, null, 2)); 46 | console.log(`Done writing Binance market data`); 47 | }) 48 | .catch(err => { 49 | console.log(`Couldn't import products from Binance`); 50 | console.log(err); 51 | }); 52 | -------------------------------------------------------------------------------- /exchange/util/genMarketFiles/update-bitfinex.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const fs = require('fs'); 3 | const request = require('request-promise'); 4 | const Promise = require('bluebird'); 5 | 6 | request({ 7 | url: 'https://api.bitfinex.com/v1/symbols_details', 8 | headers: { 9 | Connection: 'keep-alive', 10 | 'User-Agent': 'Request-Promise', 11 | }, 12 | json: true, 13 | }) 14 | .then(body => { 15 | if (!body) { 16 | throw new Error('Unable to fetch list of assets, response was empty'); 17 | } 18 | 19 | return body; 20 | }) 21 | .then(results => { 22 | let assets = _.uniq(_.map(results, market => { 23 | return market.pair.substring(0, 3).toUpperCase(); 24 | })); 25 | 26 | let currencies = _.uniq(_.map(results, market => { 27 | return market.pair.substring(3, 6).toUpperCase(); 28 | })); 29 | 30 | let markets = _.map(results, market => { 31 | return { 32 | pair: [ 33 | market.pair.substring(3, 6).toUpperCase(), 34 | market.pair.substring(0, 3).toUpperCase() 35 | ], 36 | minimalOrder: { 37 | amount: market.minimum_order_size, 38 | unit: 'asset', 39 | }, 40 | }; 41 | }); 42 | 43 | return { assets: assets, currencies: currencies, markets: markets }; 44 | }) 45 | .then(markets => { 46 | fs.writeFileSync('../../wrappers/bitfinex-markets.json', JSON.stringify(markets, null, 2)); 47 | console.log(`Done writing Bitfinex market data`); 48 | }) 49 | .catch(err => { 50 | console.log(`Couldn't import products from Bitfinex`); 51 | console.log(err); 52 | }); 53 | 54 | 55 | -------------------------------------------------------------------------------- /exchange/util/genMarketFiles/update-coinbase.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const fs = require('fs'); 3 | const request = require('request-promise'); 4 | const Promise = require('bluebird'); 5 | 6 | request({ 7 | url: 'https://api.pro.coinbase.com/products', 8 | headers: { 9 | Connection: 'keep-alive', 10 | 'User-Agent': 'Request-Promise', 11 | }, 12 | json: true, 13 | }) 14 | .then(body => { 15 | if (!body) { 16 | throw new Error('Unable to fetch list of assets, response was empty'); 17 | } 18 | 19 | return body; 20 | }) 21 | .then(results => { 22 | let assets = _.uniq(_.map(results, market => { 23 | return market.base_currency.toUpperCase(); 24 | })); 25 | 26 | let currencies = _.uniq(_.map(results, market => { 27 | return market.quote_currency.toUpperCase(); 28 | })); 29 | 30 | let markets = _.map(results, market => { 31 | return { 32 | pair: [ 33 | market.quote_currency.toUpperCase(), 34 | market.base_currency.toUpperCase() 35 | ], 36 | minimalOrder: { 37 | amount: market.base_min_size, 38 | unit: 'asset', 39 | }, 40 | }; 41 | }); 42 | 43 | return { assets: assets, currencies: currencies, markets: markets }; 44 | }) 45 | .then(markets => { 46 | fs.writeFileSync('../../wrappers/coinbase-markets.json', JSON.stringify(markets, null, 2)); 47 | console.log(`Done writing Coinbase market data`); 48 | }) 49 | .catch(err => { 50 | console.log(`Couldn't import products from Coinbase`); 51 | console.log(err); 52 | }); 53 | 54 | 55 | -------------------------------------------------------------------------------- /exchange/util/genMarketFiles/update-coinfalcon.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const fs = require('fs'); 3 | const request = require('request-promise'); 4 | const Promise = require('bluebird'); 5 | 6 | const options = { 7 | url: 'https://coinfalcon.com/api/v1/markets', 8 | headers: { 9 | Connection: 'keep-alive', 10 | 'User-Agent': 'Request-Promise', 11 | }, 12 | json: true, 13 | }; 14 | 15 | request(options) 16 | .then(body => { 17 | if (!body && !body.data) { 18 | throw new Error('Unable to fetch product list, response was empty'); 19 | } 20 | 21 | let assets = _.uniq(_.map(body.data, market => market.name.split('-')[0])); 22 | let currencies = _.uniq(_.map(body.data, market => market.name.split('-')[1])); 23 | let pairs = _.map(body.data, market => { 24 | var currency = market.name.split('-')[1]; 25 | var asset = market.name.split('-')[0]; 26 | return { 27 | pair: [currency, asset], 28 | minimalOrder: { 29 | amount: parseFloat(market.min_volume), 30 | price: parseFloat(market.min_price), 31 | order: 0.0 32 | }, 33 | }; 34 | }); 35 | 36 | return { assets: assets, currencies: currencies, markets: pairs }; 37 | }) 38 | .then(markets => { 39 | fs.writeFileSync('../../wrappers/coinfalcon-markets.json', JSON.stringify(markets, null, 2)); 40 | console.log(`Done writing CoinFalcon market data`); 41 | }) 42 | .catch(err => { 43 | console.log(`Couldn't import products from CoinFalcon`); 44 | console.log(err); 45 | }); 46 | -------------------------------------------------------------------------------- /exchange/wrappers/DEBUG_exchange-simulator.js: -------------------------------------------------------------------------------- 1 | // Fake exchanges: used to test purposes to develop Gekko (works without internet). 2 | 3 | const _ = require('lodash'); 4 | const moment = require('moment'); 5 | 6 | const TREND_DURATION = 1000; 7 | 8 | const Trader = function() { 9 | this.name = 'Exchange Simulator'; 10 | this.at = moment().subtract(30, 'minutes'); 11 | 12 | 13 | // fake data 14 | this.price = 100; 15 | this.trend = 'up'; 16 | this.tid = 0; 17 | } 18 | 19 | Trader.prototype.getTrades = function(since, cb) { 20 | const amount = moment().diff(this.at, 'seconds'); 21 | 22 | const trades = _.range(amount).map(() => { 23 | 24 | this.tid++; 25 | 26 | if(this.tid % TREND_DURATION === 0) { 27 | if(this.trend === 'up') 28 | this.trend = 'down'; 29 | else 30 | this.trend = 'up'; 31 | } 32 | 33 | if(this.trend === 'up') 34 | this.price += Math.random(); 35 | else 36 | this.price -= Math.random(); 37 | 38 | return { 39 | date: this.at.add(1, 'seconds').unix(), 40 | price: this.price, 41 | amount: Math.random() * 100, 42 | tid: this.tid 43 | } 44 | }); 45 | 46 | console.log( 47 | `[EXCHANGE SIMULATOR] emitted ${amount} fake trades, up until ${this.at.format('YYYY-MM-DD HH:mm:ss')}.` 48 | ); 49 | 50 | cb(null, trades); 51 | } 52 | 53 | Trader.getCapabilities = function () { 54 | return { 55 | name: 'Exchange Simulator', 56 | slug: 'DEBUG_exchange-simulator', 57 | currencies: ['USD'], 58 | assets: ['BTC', 'BTC'], 59 | maxTradesAge: 60, 60 | maxHistoryFetch: null, 61 | markets: [ 62 | { pair: ['USD', 'BTC'], minimalOrder: { amount: 5, unit: 'currency' } }, 63 | ], 64 | requires: ['key', 'secret', 'username'], 65 | fetchTimespan: 60, 66 | tid: 'tid', 67 | tradable: false 68 | }; 69 | } 70 | 71 | module.exports = Trader; 72 | 73 | -------------------------------------------------------------------------------- /exchange/wrappers/exmo-markets.json: -------------------------------------------------------------------------------- 1 | { 2 | "assets": [ 3 | "BTC", 4 | "ETH" 5 | ], 6 | "currencies": [ 7 | "EUR", 8 | "USD", 9 | "LTC" 10 | ], 11 | "markets": [ 12 | { 13 | "pair": [ 14 | "USD", 15 | "BTC" 16 | ], 17 | "minimalOrder": { 18 | "amount": "0.001", 19 | "unit": "asset" 20 | } 21 | }, 22 | { 23 | "pair": [ 24 | "EUR", 25 | "BTC" 26 | ], 27 | "minimalOrder": { 28 | "amount": "0.001", 29 | "unit": "asset" 30 | } 31 | }, 32 | { 33 | "pair": [ 34 | "LTC", 35 | "ETH" 36 | ], 37 | "minimalOrder": { 38 | "amount": "0.01", 39 | "unit": "asset" 40 | } 41 | } 42 | ] 43 | } 44 | -------------------------------------------------------------------------------- /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://gekko.wizb.it/docs/internals/architecture.html 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 | 46 | const pipeline = require(dirs.core + 'pipeline'); 47 | const config = util.getConfig(); 48 | const mode = util.gekkoMode(); 49 | 50 | if( 51 | config.trader && 52 | config.trader.enabled && 53 | !config['I understand that Gekko only automates MY OWN trading strategies'] 54 | ) 55 | util.die('Do you understand what Gekko will do with your money? Read this first:\n\nhttps://github.com/askmike/gekko/issues/201'); 56 | 57 | // > Ever wonder why fund managers can't beat the S&P 500? 58 | // > 'Cause they're sheep, and sheep get slaughtered. 59 | pipeline({ 60 | config: config, 61 | mode: mode 62 | }); 63 | 64 | -------------------------------------------------------------------------------- /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 = (err, trades) => { 25 | if (err) { 26 | log.error(`There was an error importing from Binance ${err}`); 27 | fetcher.emit('done'); 28 | return fetcher.emit('trades', []); 29 | } 30 | 31 | if (trades.length > 0) { 32 | var last = moment.unix(_.last(trades).date).utc(); 33 | // Conversion to milliseconds epoch time means we have to compensate for possible leap seconds 34 | var next = from.clone().add(1, 'h').subtract(1, 's'); 35 | } else { 36 | // Conversion to milliseconds epoch time means we have to compensate for possible leap seconds 37 | var next = from.clone().add(1, 'h').subtract(1, 's'); 38 | log.debug('Import step returned no results, moving to the next 1h period'); 39 | } 40 | 41 | if (from.add(1, 'h') >= end) { 42 | fetcher.emit('done'); 43 | 44 | var endUnix = end.unix(); 45 | trades = _.filter(trades, t => t.date <= endUnix); 46 | } 47 | 48 | from = next.clone(); 49 | fetcher.emit('trades', trades); 50 | }; 51 | 52 | module.exports = function(daterange) { 53 | from = daterange.from.clone().utc(); 54 | end = daterange.to.clone().utc(); 55 | 56 | return { 57 | bus: fetcher, 58 | fetch: fetch, 59 | }; 60 | }; 61 | -------------------------------------------------------------------------------- /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/coinfalcon.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 + 'coinfalcon'); 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 | log.debug('[CoinFalcon] Getting trades from: ', from); 22 | fetcher.getTrades(from, handleFetch, true); 23 | }; 24 | 25 | var handleFetch = (unk, trades) => { 26 | if (trades.length > 0) { 27 | var last = moment.unix(_.last(trades).date).utc(); 28 | var next = last.clone(); 29 | } else { 30 | var next = from.clone().add(1, 'h'); 31 | log.debug('Import step returned no results, moving to the next 1h period'); 32 | } 33 | 34 | if (from.add(1, 'h') >= end) { 35 | fetcher.emit('done'); 36 | 37 | var endUnix = end.unix(); 38 | trades = _.filter(trades, t => t.date <= endUnix); 39 | } 40 | 41 | from = next.clone(); 42 | fetcher.emit('trades', trades); 43 | }; 44 | 45 | module.exports = function(daterange) { 46 | from = daterange.from.clone().utc(); 47 | end = daterange.to.clone().utc(); 48 | 49 | return { 50 | bus: fetcher, 51 | fetch: fetch, 52 | }; 53 | }; 54 | -------------------------------------------------------------------------------- /importers/exchanges/kraken.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | var moment = require('moment'); 3 | 4 | var util = require('../../core/util.js'); 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 + 'kraken'); 12 | 13 | util.makeEventEmitter(Fetcher); 14 | 15 | var end = false; 16 | var done = false; 17 | var from = false; 18 | 19 | var lastId = false; 20 | var prevLastId = false; 21 | 22 | var fetcher = new Fetcher(config.watch); 23 | 24 | var fetch = () => { 25 | fetcher.import = true; 26 | 27 | if (lastId) { 28 | var tidAsTimestamp = lastId / 1000000; 29 | setTimeout(() => { 30 | fetcher.getTrades(tidAsTimestamp, handleFetch) 31 | }, 500); 32 | } 33 | else 34 | fetcher.getTrades(from, handleFetch); 35 | } 36 | 37 | var handleFetch = (err, trades) => { 38 | if(!err && !trades.length) { 39 | console.log('no trades'); 40 | err = 'No trades'; 41 | } 42 | 43 | if (err) { 44 | log.error(`There was an error importing from Kraken ${err}`); 45 | fetcher.emit('done'); 46 | return fetcher.emit('trades', []); 47 | } 48 | 49 | var last = moment.unix(_.last(trades).date).utc(); 50 | lastId = _.last(trades).tid 51 | if(last < from) { 52 | log.debug('Skipping data, they are before from date', last.format()); 53 | return fetch(); 54 | } 55 | 56 | if (last > end || lastId === prevLastId) { 57 | fetcher.emit('done'); 58 | 59 | var endUnix = end.unix(); 60 | trades = _.filter( 61 | trades, 62 | t => t.date <= endUnix 63 | ) 64 | } 65 | 66 | prevLastId = lastId 67 | fetcher.emit('trades', trades); 68 | } 69 | 70 | module.exports = function (daterange) { 71 | 72 | from = daterange.from.clone(); 73 | end = daterange.to.clone(); 74 | 75 | return { 76 | bus: fetcher, 77 | fetch: fetch 78 | } 79 | } 80 | 81 | 82 | -------------------------------------------------------------------------------- /logs/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gekko", 3 | "version": "0.6.8", 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 | "async": "2.1.2", 19 | "bitfinex-api-node": "^1.2.1", 20 | "co-fs": "^1.2.0", 21 | "commander": "^2.13.0", 22 | "gekko": "0.0.9", 23 | "humanize-duration": "^3.10.0", 24 | "koa": "^1.2.0", 25 | "koa-bodyparser": "^2.2.0", 26 | "koa-cors": "0.0.16", 27 | "koa-logger": "^1.3.0", 28 | "koa-router": "^5.4.0", 29 | "koa-static": "^2.0.0", 30 | "lodash": "2.x", 31 | "moment": "^2.20.1", 32 | "opn": "^4.0.2", 33 | "promisify-node": "^0.5.0", 34 | "prompt-lite": "0.1.1", 35 | "relieve": "^2.1.3", 36 | "retry": "^0.10.1", 37 | "semver": "5.4.1", 38 | "sqlite3": "^4.0.6", 39 | "stats-lite": "^2.0.4", 40 | "tiny-promisify": "^0.1.1", 41 | "toml": "^2.3.0", 42 | "ws": "^6.0.0" 43 | }, 44 | "devDependencies": { 45 | "chai": "^4.1.2", 46 | "mocha": "^5.0.0", 47 | "proxyquire": "^1.7.10", 48 | "request": "^2.83.0", 49 | "request-promise": "^4.2.2", 50 | "sinon": "^4.2.0" 51 | }, 52 | "engines": { 53 | "node": ">=8.11.2" 54 | }, 55 | "license": "MIT", 56 | "repository": { 57 | "type": "git", 58 | "url": "https://github.com/askmike/gekko.git" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /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/candleUploader.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | const _ = require('lodash'); 3 | const log = require('../core/log.js'); 4 | const util = require('../core/util.js'); 5 | const config = util.getConfig(); 6 | 7 | const CandleUploader = function(done) { 8 | _.bindAll(this); 9 | 10 | done(); 11 | this.candles = []; 12 | this.schedule(); 13 | }; 14 | 15 | CandleUploader.prototype.processCandle = function(candle, done) { 16 | this.candles.push(candle); 17 | done(); 18 | }; 19 | 20 | CandleUploader.prototype.schedule = function() { 21 | this.timer = setTimeout(this.upload, 10 * 1000); 22 | } 23 | 24 | CandleUploader.prototype.rawUpload = function(candles, count, next) { 25 | 26 | const amount = candles.length; 27 | 28 | axios({ 29 | url: config.candleUploader.url, 30 | method: 'post', 31 | data: { 32 | apiKey: config.candleUploader.apiKey, 33 | watch: config.watch, 34 | candles: candles 35 | } 36 | }) 37 | .then(r => { 38 | if(r.data.success === false) { 39 | console.log('error uploading:', r.data); 40 | } 41 | console.log(new Date, 'uploaded', amount, 'candles'); 42 | 43 | next(); 44 | }) 45 | .catch(e => { 46 | console.log('error uploading:', e.message); 47 | 48 | count++; 49 | 50 | if(count > 10) { 51 | console.log('FINAL error uploading:', e.message); 52 | return next(); 53 | } 54 | 55 | setTimeout(() => this.rawUpload(candles, count, next), 2000); 56 | }); 57 | } 58 | 59 | CandleUploader.prototype.upload = function() { 60 | const amount = this.candles.length; 61 | if(!amount) { 62 | return this.schedule(); 63 | } 64 | 65 | this.rawUpload(this.candles, 0, () => { 66 | this.schedule(); 67 | }); 68 | 69 | this.candles = []; 70 | } 71 | 72 | CandleUploader.prototype.finish = function(next) { 73 | this.upload(); 74 | clearTimeout(this.timer); 75 | } 76 | 77 | module.exports = CandleUploader; 78 | -------------------------------------------------------------------------------- /plugins/childToParent.js: -------------------------------------------------------------------------------- 1 | // Small plugin that subscribes to some events, stores 2 | // them and sends it to the parent process. 3 | 4 | const log = require('../core/log'); 5 | const _ = require('lodash'); 6 | const subscriptions = require('../subscriptions'); 7 | const config = require('../core/util').getConfig(); 8 | 9 | const ChildToParent = function() { 10 | 11 | subscriptions 12 | // .filter(sub => config.childToParent.events.includes(sub.event)) 13 | .forEach(sub => { 14 | this[sub.handler] = (event, next) => { 15 | process.send({type: sub.event, payload: event}); 16 | if(_.isFunction(next)) { 17 | next(); 18 | } 19 | } 20 | }, this); 21 | 22 | } 23 | 24 | module.exports = ChildToParent; -------------------------------------------------------------------------------- /plugins/eventLogger.js: -------------------------------------------------------------------------------- 1 | const log = require('../core/log'); 2 | const _ = require('lodash'); 3 | const subscriptions = require('../subscriptions'); 4 | const config = require('../core/util').getConfig().eventLogger; 5 | 6 | const EventLogger = function() {} 7 | 8 | _.each(subscriptions, sub => { 9 | if(config.whitelist && !config.whitelist.includes(sub.event)) { 10 | return; 11 | } 12 | 13 | EventLogger.prototype[sub.handler] = (event, next) => { 14 | log.info(`\t\t\t\t[EVENT ${sub.event}]\n`, event); 15 | if(_.isFunction(next)) 16 | next(); 17 | } 18 | }); 19 | 20 | module.exports = EventLogger; -------------------------------------------------------------------------------- /plugins/kodi.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by billymcintosh on 24/12/17. 3 | */ 4 | 5 | var _ = require('lodash'); 6 | var request = require('request'); 7 | var log = require('../core/log.js'); 8 | var util = require('../core/util.js'); 9 | var config = util.getConfig(); 10 | var kodiConfig = config.kodi; 11 | 12 | var Kodi = function(done) { 13 | _.bindAll(this); 14 | 15 | this.exchange = config.watch.exchange.charAt().toUpperCase() + config.watch.exchange.slice(1) 16 | 17 | this.price = 'N/A'; 18 | this.done = done; 19 | this.setup(); 20 | }; 21 | 22 | Kodi.prototype.setup = function(done) { 23 | var setupKodi = function (err, result) { 24 | if(kodiConfig.sendMessageOnStart) { 25 | var currency = config.watch.currency; 26 | var asset = config.watch.asset; 27 | var title = "Gekko Started"; 28 | var message = `Watching ${this.exchange} - ${currency}/${asset}`; 29 | this.mail(title, message); 30 | } else { 31 | log.debug('Skipping Send message on startup') 32 | } 33 | } 34 | setupKodi.call(this) 35 | }; 36 | 37 | Kodi.prototype.processCandle = function(candle, done) { 38 | this.price = candle.close; 39 | 40 | done(); 41 | }; 42 | 43 | Kodi.prototype.processAdvice = function(advice) { 44 | var title = `Gekko: Going ${advice.recommendation} @ ${this.price}` 45 | var message = `${this.exchange} ${config.watch.currency}/${config.watch.asset}`; 46 | this.mail(title, message); 47 | }; 48 | 49 | Kodi.prototype.mail = function(title, message, done) { 50 | var options = { 51 | body: `{"jsonrpc":"2.0","method":"GUI.ShowNotification","params":{"title":"${title}","message":"${message}"},"id":1}`, 52 | headers: { 53 | 'Content-Type': 'application/json' 54 | }, 55 | method: 'POST', 56 | url: kodiConfig.host 57 | } 58 | 59 | request(options, (error, response, body) => { 60 | if (!error) { 61 | log.info('Kodi message sent') 62 | } else { 63 | log.debug(`Kodi ${error}`) 64 | } 65 | }) 66 | } 67 | 68 | module.exports = Kodi; 69 | -------------------------------------------------------------------------------- /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/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().replace(/\-/g,''); 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.replace(/\-/g,'') + '_' + name; 49 | } 50 | var fullName = [name, settings.pair.join('_')].join('_'); 51 | return useLowerCaseTableNames() ? fullName.toLowerCase() : fullName; 52 | }, 53 | 54 | startconstraint: function (name) { 55 | if (useSingleDatabase()) { 56 | name = watch.exchange.replace(/\-/g,'') + '_' + name; 57 | } 58 | var fullName = [name, settings.pair.join('_')].join('_'); 59 | return useLowerCaseTableNames() ? fullName.toLowerCase() + '_start_key' : fullName + '_start_key'; 60 | }, 61 | 62 | // postgres schema name. defaults to 'public' 63 | schema: function () { 64 | return config.postgresql.schema ? config.postgresql.schema : 'public'; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /plugins/postgresql/writer.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | const log = require('../../core/log'); 3 | const util = require('../../core/util'); 4 | const config = util.getConfig(); 5 | 6 | var handle = require('./handle'); 7 | var postgresUtil = require('./util'); 8 | 9 | var Store = function(done, pluginMeta) { 10 | _.bindAll(this); 11 | this.done = done; 12 | this.db = handle; 13 | this.cache = []; 14 | done(); 15 | } 16 | 17 | Store.prototype.writeCandles = function() { 18 | if(_.isEmpty(this.cache)){ 19 | return; 20 | } 21 | 22 | //log.debug('Writing candles to DB!'); 23 | _.each(this.cache, candle => { 24 | var stmt = ` 25 | BEGIN; 26 | LOCK TABLE ${postgresUtil.table('candles')} IN SHARE ROW EXCLUSIVE MODE; 27 | INSERT INTO ${postgresUtil.table('candles')} 28 | (start, open, high,low, close, vwp, volume, trades) 29 | VALUES 30 | (${candle.start.unix()}, ${candle.open}, ${candle.high}, ${candle.low}, ${candle.close}, ${candle.vwp}, ${candle.volume}, ${candle.trades}) 31 | ON CONFLICT ON CONSTRAINT ${postgresUtil.startconstraint('candles')} 32 | DO NOTHING; 33 | COMMIT; 34 | `; 35 | 36 | this.db.connect((err,client,done) => { 37 | if(err) { 38 | util.die(err); 39 | } 40 | client.query(stmt, (err, res) => { 41 | done(); 42 | if (err) { 43 | log.debug(err.stack) 44 | } else { 45 | //log.debug(res) 46 | } 47 | }); 48 | }); 49 | }); 50 | 51 | this.cache = []; 52 | } 53 | 54 | var processCandle = function(candle, done) { 55 | this.cache.push(candle); 56 | if (this.cache.length > 1) 57 | this.writeCandles(); 58 | 59 | done(); 60 | }; 61 | 62 | var finalize = function(done) { 63 | this.writeCandles(); 64 | this.db = null; 65 | done(); 66 | } 67 | 68 | if(config.candleWriter.enabled) { 69 | Store.prototype.processCandle = processCandle; 70 | Store.prototype.finalize = finalize; 71 | } 72 | 73 | module.exports = Store; 74 | -------------------------------------------------------------------------------- /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/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) util.die(cannotLoad); 19 | 20 | // should be good now 21 | if (config.debug) var sqlite3 = require('sqlite3').verbose(); 22 | else var sqlite3 = require('sqlite3'); 23 | 24 | var plugins = require(util.dirs().gekko + 'plugins'); 25 | 26 | var version = adapter.version; 27 | 28 | var dbName = config.watch.exchange.toLowerCase() + '_' + version + '.db'; 29 | var dir = dirs.gekko + adapter.dataDirectory; 30 | 31 | var fullPath = [dir, dbName].join('/'); 32 | 33 | var mode = util.gekkoMode(); 34 | if (mode === 'realtime' || mode === 'importer') { 35 | if (!fs.existsSync(dir)) fs.mkdirSync(dir); 36 | } else if (mode === 'backtest') { 37 | if (!fs.existsSync(dir)) util.die('History directory does not exist.'); 38 | 39 | if (!fs.existsSync(fullPath)) 40 | util.die( 41 | `History database does not exist for exchange ${ 42 | config.watch.exchange 43 | } at version ${version}.` 44 | ); 45 | } 46 | 47 | module.exports = { 48 | initDB: () => { 49 | var journalMode = config.sqlite.journalMode || 'PERSIST'; 50 | var syncMode = journalMode === 'WAL' ? 'NORMAL' : 'FULL'; 51 | 52 | var db = new sqlite3.Database(fullPath); 53 | db.run('PRAGMA synchronous = ' + syncMode); 54 | db.run('PRAGMA journal_mode = ' + journalMode); 55 | db.configure('busyTimeout', 10000); 56 | return db; 57 | } 58 | }; 59 | -------------------------------------------------------------------------------- /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/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/DEBUG_single-advice.js: -------------------------------------------------------------------------------- 1 | var settings = { 2 | wait: 0, 3 | // advice: 'short' 4 | advice: 'long' 5 | }; 6 | 7 | // ------- 8 | 9 | var _ = require('lodash'); 10 | var log = require('../core/log.js'); 11 | 12 | var i = 0; 13 | 14 | var method = { 15 | init: _.noop, 16 | update: _.noop, 17 | log: _.noop, 18 | check: function() { 19 | 20 | // log.info('iteration:', i); 21 | if(settings.wait === i) { 22 | console.log('trigger advice!'); 23 | this.advice({ 24 | direction: settings.advice, 25 | trigger: { 26 | type: 'trailingStop', 27 | trailPercentage: 0.5 28 | } 29 | }); 30 | } 31 | 32 | i++ 33 | 34 | } 35 | }; 36 | 37 | module.exports = method; -------------------------------------------------------------------------------- /strategies/DEBUG_toggle-advice.js: -------------------------------------------------------------------------------- 1 | var settings = { 2 | wait: 0, 3 | each: 6 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 | processTrade: function(trade) { 18 | log.debug('TRADE RECEIVED BY processTrade:', trade); 19 | }, 20 | check: function(candle) { 21 | 22 | if(settings.wait > i) 23 | return; 24 | 25 | log.info('iteration:', i); 26 | 27 | if(i % settings.each === 0) { 28 | log.debug('trigger SHORT'); 29 | this.advice('short'); 30 | } else if(i % settings.each === settings.each / 2) { 31 | log.debug('trigger LONG'); 32 | this.advice('long'); 33 | } 34 | 35 | // if(i % 2 === 0) 36 | // this.advice('long'); 37 | // else if(i % 2 === 1) 38 | // this.advice('short'); 39 | 40 | i++; 41 | 42 | } 43 | }; 44 | 45 | module.exports = method; 46 | -------------------------------------------------------------------------------- /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 | this.addIndicator('sma', 'SMA', this.settings.weight); 18 | } 19 | 20 | // what happens on every new candle? 21 | method.update = function(candle) { 22 | // nothing! 23 | } 24 | 25 | // for debugging purposes: log the last calculated 26 | // EMAs and diff. 27 | method.log = function() { 28 | let dema = this.indicators.dema; 29 | let sma = this.indicators.sma; 30 | 31 | log.debug('Calculated DEMA and SMA properties for candle:'); 32 | log.debug('\t Inner EMA:', dema.inner.result.toFixed(8)); 33 | log.debug('\t Outer EMA:', dema.outer.result.toFixed(8)); 34 | log.debug('\t DEMA:', dema.result.toFixed(5)); 35 | log.debug('\t SMA:', sma.result.toFixed(5)); 36 | log.debug('\t DEMA age:', dema.inner.age, 'candles'); 37 | } 38 | 39 | method.check = function(candle) { 40 | let dema = this.indicators.dema; 41 | let sma = this.indicators.sma; 42 | let resDEMA = dema.result; 43 | let resSMA = sma.result; 44 | let price = candle.close; 45 | let diff = resSMA - resDEMA; 46 | 47 | let message = '@ ' + price.toFixed(8) + ' (' + resDEMA.toFixed(5) + '/' + diff.toFixed(5) + ')'; 48 | 49 | if(diff > this.settings.thresholds.up) { 50 | log.debug('we are currently in uptrend', message); 51 | 52 | if(this.currentTrend !== 'up') { 53 | this.currentTrend = 'up'; 54 | this.advice('long'); 55 | } else 56 | this.advice(); 57 | 58 | } else if(diff < this.settings.thresholds.down) { 59 | log.debug('we are currently in a downtrend', message); 60 | 61 | if(this.currentTrend !== 'down') { 62 | this.currentTrend = 'down'; 63 | this.advice('short'); 64 | } else 65 | this.advice(); 66 | 67 | } else { 68 | log.debug('we are currently not in an up or down trend', message); 69 | this.advice(); 70 | } 71 | } 72 | 73 | module.exports = method; 74 | -------------------------------------------------------------------------------- /strategies/TMA.js: -------------------------------------------------------------------------------- 1 | var method = {}; 2 | 3 | method.init = function() { 4 | this.name = 'Triple Moving Average'; 5 | this.requiredHistory = this.settings.long; 6 | 7 | this.addIndicator('short', 'SMA', this.settings.short) 8 | this.addIndicator('medium', 'SMA', this.settings.medium) 9 | this.addIndicator('long', 'SMA', this.settings.long) 10 | } 11 | 12 | method.update = function(candle) { 13 | this.indicators.short.update(candle.close) 14 | this.indicators.medium.update(candle.close) 15 | this.indicators.long.update(candle.close) 16 | } 17 | 18 | method.check = function() { 19 | const short = this.indicators.short.result; 20 | const medium = this.indicators.medium.result; 21 | const long = this.indicators.long.result; 22 | 23 | if((short > medium) && (medium > long)) { 24 | this.advice('long') 25 | } else if((short < medium) && (medium > long)) { 26 | this.advice('short') 27 | } else if(((short > medium) && (medium < long))) { 28 | this.advice('short') 29 | } else { 30 | this.advice(); 31 | } 32 | } 33 | 34 | module.exports = method; 35 | -------------------------------------------------------------------------------- /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/indicators/CCI.js: -------------------------------------------------------------------------------- 1 | /* 2 | * CCI 3 | */ 4 | var log = require('../../core/log'); 5 | 6 | var Indicator = function(settings) { 7 | this.input = 'candle'; 8 | this.tp = 0.0; 9 | this.result = false; 10 | this.hist = []; // needed for mean? 11 | this.mean = 0.0; 12 | this.size = 0; 13 | this.constant = settings.constant; 14 | this.maxSize = settings.history; 15 | for (var i = 0; i < this.maxSize; i++) 16 | this.hist.push(0.0); 17 | } 18 | 19 | Indicator.prototype.update = function(candle) { 20 | 21 | // We need sufficient history to get the right result. 22 | 23 | var tp = (candle.high + candle.close + candle.low) / 3; 24 | if (this.size < this.maxSize) { 25 | this.hist[this.size] = tp; 26 | this.size++; 27 | } else { 28 | for (var i = 0; i < this.maxSize-1; i++) { 29 | this.hist[i] = this.hist[i+1]; 30 | } 31 | this.hist[this.maxSize-1] = tp; 32 | } 33 | 34 | if (this.size < this.maxSize) { 35 | this.result = false; 36 | } else { 37 | this.calculate(tp); 38 | } 39 | } 40 | 41 | /* 42 | * Handle calculations 43 | */ 44 | Indicator.prototype.calculate = function(tp) { 45 | 46 | var sumtp = 0.0 47 | 48 | for (var i = 0; i < this.size; i++) { 49 | sumtp = sumtp + this.hist[i]; 50 | } 51 | 52 | this.avgtp = sumtp / this.size; 53 | 54 | this.tp = tp; 55 | 56 | var sum = 0.0; 57 | // calculate tps 58 | for (var i = 0; i < this.size; i++) { 59 | 60 | var z = (this.hist[i] - this.avgtp); 61 | if (z < 0) z = z * -1.0; 62 | sum = sum + z; 63 | 64 | } 65 | 66 | this.mean = (sum / this.size); 67 | 68 | 69 | 70 | this.result = (this.tp - this.avgtp) / (this.constant * this.mean); 71 | 72 | // log.debug("===\t", this.mean, "\t", this.tp, '\t', this.TP.result, "\t", sum, "\t", avgtp, '\t', this.result.toFixed(2)); 73 | } 74 | 75 | module.exports = Indicator; 76 | -------------------------------------------------------------------------------- /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.inner = new EMA(config.weight); 8 | this.outer = new EMA(config.weight); 9 | } 10 | 11 | // add a price and calculate the EMAs and 12 | // the result 13 | Indicator.prototype.update = function(price) { 14 | this.inner.update(price); 15 | this.outer.update(this.inner.result); 16 | this.result = 2 * this.inner.result - this.outer.result; 17 | } 18 | 19 | module.exports = Indicator; 20 | -------------------------------------------------------------------------------- /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 = null; 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 | 18 | if (prevClose === null) { 19 | // Set initial price to prevent invalid change calculation 20 | this.lastClose = close; 21 | // Do not calculate TSI on first close 22 | return; 23 | } 24 | 25 | var momentum = close - prevClose; 26 | 27 | this.inner.update(momentum); 28 | this.outer.update(this.inner.result); 29 | 30 | this.absoluteInner.update(Math.abs(momentum)); 31 | this.absoluteOuter.update(this.absoluteInner.result); 32 | 33 | this.tsi = 100 * this.outer.result / this.absoluteOuter.result; 34 | 35 | this.lastClose = close; 36 | } 37 | 38 | module.exports = Indicator; 39 | -------------------------------------------------------------------------------- /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.thirdLow.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 | var method = {}; 2 | // Prepare everything our method needs 3 | method.init = function() { 4 | this.name = 'tulip-adx' 5 | this.trend = 'none'; 6 | this.requiredHistory = this.tradingAdvisor.historySize; 7 | this.addTulipIndicator('myadx', 'adx', this.settings); 8 | } 9 | // What happens on every new candle? 10 | method.update = function(candle) { 11 | // nothing! 12 | } 13 | method.log = function() { 14 | // nothing! 15 | } 16 | method.check = function(candle) { 17 | var price = candle.close; 18 | var adx = this.tulipIndicators.myadx.result.result; 19 | // console.dir(adx) 20 | 21 | if(this.settings.thresholds.down > adx && this.trend !== 'short') { 22 | this.trend = 'short'; 23 | this.advice('short'); 24 | } else if(this.settings.thresholds.up < adx && this.trend !== 'long'){ 25 | this.trend = 'long'; 26 | this.advice('long'); 27 | } 28 | } 29 | 30 | module.exports = method; 31 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | describe('indicators/DEMA', function() { 26 | 27 | var DEMA = require(INDICATOR_PATH + 'DEMA'); 28 | 29 | var verified_dema10results = [81,62.157024793388416,65.1412471825695,49.61361928829999,42.570707415663364,34.597495090487996,44.47997295246192,58.6168391922143,71.4979863711489,48.963944850563,60.095241192962106,66.41965473810654,69.77997604557987,49.75572911767095,61.08641574881719,65.56112611350791,54.65374623818491,57.51231851211959,51.73955057939423,36.941596820151815,27.434153499662216,24.890025210750593,33.14097982029734,30.163817870645254,40.330715478873344,45.63811915119551,36.045947422710505,53.12735486322183,64.78921092296176,72.9995704035162,83.22304838955624,58.85287916072168,62.841348382387075,42.93804766689409,36.82301007497254,26.454331684513562,46.374329400503385,53.28360623846342,38.891184741941984]; 30 | 31 | it('should correctly calculate DEMA with weight 10', function() { 32 | let dema = new DEMA({weight: 10}); 33 | _.each(prices, function(p, i) { 34 | dema.update(p); 35 | expect(dema.result).to.equal(verified_dema10results[i]); 36 | }); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /test/plugins/portfolioManager.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/llSourcell/Watch-Me-Build-a-Trading-Bot/5b1aafa46cf35c56ce328c5880bac2ff425a2eb7/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/baseUIconfig.js: -------------------------------------------------------------------------------- 1 | // Note: this file gets copied around, make sure you edit 2 | // the UIconfig located at `gekko/web/vue/dist/UIconfig.js`. 3 | 4 | // This config is used by both the frontend as well as the web server. 5 | // see https://gekko.wizb.it/docs/installation/installing_gekko_on_a_server.html#Configuring-Gekko 6 | 7 | const CONFIG = { 8 | headless: false, 9 | api: { 10 | host: '127.0.0.1', 11 | port: 3000, 12 | timeout: 120000 // 2 minutes 13 | }, 14 | ui: { 15 | ssl: false, 16 | host: 'localhost', 17 | port: 3000, 18 | path: '/' 19 | }, 20 | adapter: 'sqlite' 21 | } 22 | 23 | if(typeof window === 'undefined') 24 | module.exports = CONFIG; 25 | else 26 | window.CONFIG = CONFIG; 27 | -------------------------------------------------------------------------------- /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); 29 | 30 | this.body = yield pipelineRunner(mode, config); 31 | } -------------------------------------------------------------------------------- /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/deleteGekko.js: -------------------------------------------------------------------------------- 1 | const cache = require('../state/cache'); 2 | const gekkoManager = cache.get('gekkos'); 3 | 4 | // Deletes a gekko 5 | // requires a post body with an id 6 | module.exports = function *() { 7 | 8 | let id = this.request.body.id; 9 | 10 | if(!id) { 11 | this.body = { status: 'not ok' } 12 | return; 13 | } 14 | 15 | try { 16 | gekkoManager.delete(id); 17 | } catch(e) { 18 | this.body = { status: e.message } 19 | } 20 | 21 | this.body = { status: 'ok' }; 22 | } -------------------------------------------------------------------------------- /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 + 'exchange/wrappers/'); 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 + 'exchange/wrappers/' + 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/info.js: -------------------------------------------------------------------------------- 1 | const p = require('../../package.json'); 2 | 3 | // Retrieves API information 4 | module.exports = function *() { 5 | this.body = { 6 | version: p.version 7 | } 8 | } -------------------------------------------------------------------------------- /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/startGekko.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | 3 | const cache = require('../state/cache'); 4 | const Logger = require('../state/logger'); 5 | const apiKeyManager= cache.get('apiKeyManager'); 6 | const gekkoManager = cache.get('gekkos'); 7 | 8 | const base = require('./baseConfig'); 9 | 10 | // starts an import 11 | // requires a post body with a config object 12 | module.exports = function *() { 13 | const mode = this.request.body.mode; 14 | 15 | let config = {}; 16 | 17 | _.merge(config, base, this.request.body); 18 | 19 | // Attach API keys 20 | if(config.trader && config.trader.enabled && !config.trader.key) { 21 | 22 | const keys = apiKeyManager._getApiKeyPair(config.watch.exchange); 23 | 24 | if(!keys) { 25 | this.body = 'No API keys found for this exchange.'; 26 | return; 27 | } 28 | 29 | _.merge( 30 | config.trader, 31 | keys 32 | ); 33 | } 34 | 35 | const state = gekkoManager.add({config, mode}); 36 | 37 | this.body = state; 38 | } 39 | -------------------------------------------------------------------------------- /web/routes/stopGekko.js: -------------------------------------------------------------------------------- 1 | const cache = require('../state/cache'); 2 | const gekkoManager = cache.get('gekkos'); 3 | 4 | // stops a Gekko 5 | // requires a post body with an id 6 | module.exports = function *() { 7 | 8 | let id = this.request.body.id; 9 | 10 | if(!id) { 11 | this.body = { status: 'not ok' } 12 | return; 13 | } 14 | 15 | let stopped = gekkoManager.stop(id); 16 | 17 | if(!stopped) { 18 | this.body = { status: 'not ok' } 19 | return; 20 | } 21 | 22 | this.body = { status: 'ok' }; 23 | } -------------------------------------------------------------------------------- /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 | // - etc.. 5 | const _ = require('lodash'); 6 | 7 | var ListManager = function() { 8 | this._list = []; 9 | } 10 | 11 | // add an item to the list 12 | ListManager.prototype.add = function(obj) { 13 | if(!obj.id) 14 | return false; 15 | this._list.push(_.clone(obj)); 16 | return true; 17 | } 18 | 19 | // update some properties on an item 20 | ListManager.prototype.update = function(id, updates) { 21 | let item = this._list.find(i => i.id === id); 22 | if(!item) 23 | return false; 24 | _.merge(item, updates); 25 | return true; 26 | } 27 | 28 | // get an item from the list 29 | ListManager.prototype.get = function(id) { 30 | return this._list.find(i => i.id === id); 31 | } 32 | 33 | // push a value to a array property of an item 34 | ListManager.prototype.push = function(id, prop, value) { 35 | let item = this._list.find(i => i.id === id); 36 | if(!item) 37 | return false; 38 | 39 | item[prop].push(value); 40 | return true; 41 | } 42 | 43 | // delete an item from the list 44 | ListManager.prototype.delete = function(id) { 45 | let wasThere = this._list.find(i => i.id === id); 46 | this._list = this._list.filter(i => i.id !== id); 47 | 48 | if(wasThere) 49 | return true; 50 | else 51 | return false; 52 | } 53 | 54 | // getter 55 | ListManager.prototype.list = function() { 56 | return this._list; 57 | } 58 | 59 | module.exports = ListManager; -------------------------------------------------------------------------------- /web/state/logger.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const _ = require('lodash'); 3 | 4 | const BASEPATH = __dirname + '/../../logs/'; 5 | 6 | const Logger = function(id) { 7 | 8 | this.fileName = `${id}.log`; 9 | 10 | this.writing = false; 11 | this.queue = []; 12 | 13 | _.bindAll(this); 14 | } 15 | 16 | Logger.prototype.write = function(line) { 17 | if(!this.writing) { 18 | this.writing = true; 19 | fs.appendFile( 20 | BASEPATH + this.fileName, 21 | line + '\n', 22 | this.handleWriteCallback 23 | ); 24 | } else 25 | this.queue.push(line); 26 | } 27 | 28 | Logger.prototype.handleWriteCallback = function(err) { 29 | if(err) 30 | console.error(`ERROR WRITING LOG FILE ${this.fileName}:`, err); 31 | 32 | this.writing = false; 33 | 34 | if(_.size(this.queue)) 35 | this.write(this.queue.shift()) 36 | } 37 | 38 | module.exports = Logger; -------------------------------------------------------------------------------- /web/state/reduceState.js: -------------------------------------------------------------------------------- 1 | // Redux/vuex inspired reducer, reduces an event into a gekko state. 2 | // NOTE: this is used by the backend as well as the frontend. 3 | 4 | const skipInitialEvents = ['marketUpdate']; 5 | const skipLatestEvents = ['marketStart', 'stratWarmupCompleted']; 6 | const trackAllEvents = ['tradeCompleted', 'advice', 'roundtrip']; 7 | 8 | const reduce = (state, event) => { 9 | const type = event.type; 10 | const payload = event.payload; 11 | 12 | state = { 13 | ...state, 14 | latestUpdate: new Date() 15 | } 16 | 17 | if(trackAllEvents.includes(type)) { 18 | if(!state.events[type]) { 19 | state = { 20 | ...state, 21 | events: { 22 | ...state.events, 23 | [type]: [ payload ] 24 | } 25 | } 26 | } else { 27 | state = { 28 | ...state, 29 | events: { 30 | ...state.events, 31 | [type]: [ ...state.events[type], payload ] 32 | } 33 | } 34 | } 35 | } 36 | 37 | if(!state.events.initial[type] && !skipInitialEvents.includes(type)) { 38 | state = { 39 | ...state, 40 | events: { 41 | ...state.events, 42 | initial: { 43 | ...state.events.initial, 44 | [type]: payload 45 | } 46 | } 47 | } 48 | } 49 | 50 | if(!skipLatestEvents.includes(type)) { 51 | state = { 52 | ...state, 53 | events: { 54 | ...state.events, 55 | latest: { 56 | ...state.events.latest, 57 | [type]: payload 58 | } 59 | } 60 | } 61 | } 62 | 63 | return state; 64 | } 65 | 66 | // export default reduce; 67 | module.exports = reduce; -------------------------------------------------------------------------------- /web/vue/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/app' 4 | ], 5 | ignore: [ 6 | 'node_modules', 7 | ], 8 | plugins : ["transform-commonjs-es2015-modules"] 9 | } 10 | -------------------------------------------------------------------------------- /web/vue/dist/UIconfig.js: -------------------------------------------------------------------------------- 1 | // Note: this file gets copied around, make sure you edit 2 | // the UIconfig located at `gekko/web/vue/dist/UIconfig.js`. 3 | 4 | // This config is used by both the frontend as well as the web server. 5 | // see https://gekko.wizb.it/docs/installation/installing_gekko_on_a_server.html#Configuring-Gekko 6 | 7 | const CONFIG = { 8 | headless: false, 9 | api: { 10 | host: '127.0.0.1', 11 | port: 3000, 12 | timeout: 120000 // 2 minutes 13 | }, 14 | ui: { 15 | ssl: false, 16 | host: 'localhost', 17 | port: 3000, 18 | path: '/' 19 | }, 20 | adapter: 'sqlite' 21 | } 22 | 23 | if(typeof window === 'undefined') 24 | module.exports = CONFIG; 25 | else 26 | window.CONFIG = CONFIG; 27 | -------------------------------------------------------------------------------- /web/vue/dist/index.html: -------------------------------------------------------------------------------- 1 | Gekko
-------------------------------------------------------------------------------- /web/vue/dist/static/gekko.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/llSourcell/Watch-Me-Build-a-Trading-Bot/5b1aafa46cf35c56ce328c5880bac2ff425a2eb7/web/vue/dist/static/gekko.jpg -------------------------------------------------------------------------------- /web/vue/dist/vendor/select-arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/llSourcell/Watch-Me-Build-a-Trading-Bot/5b1aafa46cf35c56ce328c5880bac2ff425a2eb7/web/vue/dist/vendor/select-arrow.png -------------------------------------------------------------------------------- /web/vue/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gekko-vue-ui", 3 | "version": "0.2.3", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build" 8 | }, 9 | "dependencies": { 10 | "marked": "^0.4.0", 11 | "superagent": "^3.8.3", 12 | "superagent-no-cache": "github:uditalias/superagent-no-cache", 13 | "vue": "^2.5.16", 14 | "vue-router": "^3.0.1", 15 | "vuex": "^3.0.1" 16 | }, 17 | "devDependencies": { 18 | "@vue/cli-plugin-babel": "^3.0.0-beta.15", 19 | "@vue/cli-service": "^3.0.0-beta.15", 20 | "babel-plugin-transform-commonjs-es2015-modules": "^4.0.1", 21 | "copy-webpack-plugin": "^4.5.2", 22 | "pug": "^2.0.3", 23 | "pug-plain-loader": "^1.0.0", 24 | "vue-template-compiler": "^2.5.16" 25 | }, 26 | "postcss": { 27 | "plugins": { 28 | "autoprefixer": {} 29 | } 30 | }, 31 | "browserslist": [ 32 | "> 1%", 33 | "last 2 versions", 34 | "not ie <= 8" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /web/vue/public/UIconfig.js: -------------------------------------------------------------------------------- 1 | // Note: this file gets copied around, make sure you edit 2 | // the UIconfig located at `gekko/web/vue/dist/UIconfig.js`. 3 | 4 | // This config is used by both the frontend as well as the web server. 5 | // see https://gekko.wizb.it/docs/installation/installing_gekko_on_a_server.html#Configuring-Gekko 6 | 7 | const CONFIG = { 8 | headless: false, 9 | api: { 10 | host: '127.0.0.1', 11 | port: 3000, 12 | timeout: 120000 // 2 minutes 13 | }, 14 | ui: { 15 | ssl: false, 16 | host: 'localhost', 17 | port: 3000, 18 | path: '/' 19 | }, 20 | adapter: 'sqlite' 21 | } 22 | 23 | if(typeof window === 'undefined') 24 | module.exports = CONFIG; 25 | else 26 | window.CONFIG = CONFIG; 27 | -------------------------------------------------------------------------------- /web/vue/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Gekko 10 | 11 | 12 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /web/vue/public/static/gekko.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/llSourcell/Watch-Me-Build-a-Trading-Bot/5b1aafa46cf35c56ce328c5880bac2ff425a2eb7/web/vue/public/static/gekko.jpg -------------------------------------------------------------------------------- /web/vue/public/vendor/select-arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/llSourcell/Watch-Me-Build-a-Trading-Bot/5b1aafa46cf35c56ce328c5880bac2ff425a2eb7/web/vue/public/vendor/select-arrow.png -------------------------------------------------------------------------------- /web/vue/src/App.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 25 | 26 | 100 | -------------------------------------------------------------------------------- /web/vue/src/components/backtester/backtester.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 56 | -------------------------------------------------------------------------------- /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 | 39 | 40 | 42 | -------------------------------------------------------------------------------- /web/vue/src/components/backtester/result/summary.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 52 | 53 | 59 | -------------------------------------------------------------------------------- /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/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 | 13 | bus.$on('import_update', data => console.log(data)) 14 | bus.$on('import_error', data => { 15 | alert('IMPORT ERROR: ' + data.error); 16 | }); 17 | 18 | const info = { 19 | connected: false 20 | } 21 | 22 | export const connect = () => { 23 | socket = new ReconnectingWebSocket(wsPath, null, { maxReconnectInterval: 4000 }); 24 | 25 | setTimeout(() => { 26 | // in case we cannot connect 27 | if(!info.connected) { 28 | initializeState(); 29 | bus.$emit('WS_STATUS_CHANGE', info); 30 | } 31 | }, 500); 32 | 33 | socket.onopen = () => { 34 | if(info.connected) 35 | return; 36 | 37 | info.connected = true; 38 | bus.$emit('WS_STATUS_CHANGE', info); 39 | initializeState(); 40 | } 41 | socket.onclose = () => { 42 | if(!info.connected) 43 | return; 44 | 45 | info.connected = false; 46 | bus.$emit('WS_STATUS_CHANGE', info); 47 | } 48 | socket.onerror = () => { 49 | if(!info.connected) 50 | return; 51 | 52 | info.connected = false; 53 | bus.$emit('WS_STATUS_CHANGE', info); 54 | } 55 | socket.onmessage = function(message) { 56 | const payload = JSON.parse(message.data); 57 | // console.log('ws message:', payload); 58 | bus.$emit(payload.type, payload); 59 | }; 60 | } -------------------------------------------------------------------------------- /web/vue/src/components/layout/footer.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 24 | -------------------------------------------------------------------------------- /web/vue/src/components/layout/header.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 21 | 22 | 47 | -------------------------------------------------------------------------------- /web/vue/src/components/layout/home.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 35 | -------------------------------------------------------------------------------- /web/vue/src/components/layout/modal.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 39 | 40 | 81 | -------------------------------------------------------------------------------- /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 singleGekko from './components/gekko/singleGekko.vue' 20 | import { connect as connectWS } from './components/global/ws' 21 | 22 | const router = new VueRouter({ 23 | mode: 'hash', 24 | base: __dirname, 25 | routes: [ 26 | { path: '/', redirect: '/home' }, 27 | { path: '/home', component: home }, 28 | { path: '/backtest', component: backtester }, 29 | { path: '/config', component: config }, 30 | { path: '/data', component: data }, 31 | { path: '/data/importer', component: importer }, 32 | { path: '/data/importer/import/:id', component: singleImport }, 33 | { path: '/live-gekkos', component: gekkoList }, 34 | { path: '/live-gekkos/new', component: newGekko }, 35 | { path: '/live-gekkos/:id', component: singleGekko }, 36 | ] 37 | }); 38 | 39 | // setup some stuff 40 | connectWS(); 41 | 42 | new Vue({ 43 | router, 44 | store, 45 | el: '#app', 46 | render: h => h(App) 47 | }) -------------------------------------------------------------------------------- /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 gekkoMutations from './modules/gekkos/mutations' 7 | import * as notificationMutations from './modules/notifications/mutations' 8 | import * as configMutations from './modules/config/mutations' 9 | 10 | Vue.use(Vuex); 11 | 12 | const debug = process.env.NODE_ENV !== 'production' 13 | 14 | let mutations = {}; 15 | 16 | _.merge(mutations, importMutations); 17 | _.merge(mutations, gekkoMutations); 18 | _.merge(mutations, notificationMutations); 19 | _.merge(mutations, configMutations); 20 | 21 | export default new Vuex.Store({ 22 | state: { 23 | warnings: { 24 | connected: true, // assume we will connect 25 | }, 26 | imports: [], 27 | gekkos: {}, 28 | archivedGekkos: {}, 29 | connection: { 30 | disconnected: false, 31 | reconnected: false 32 | }, 33 | apiKeys: [], 34 | exchanges: {} 35 | }, 36 | mutations, 37 | strict: debug 38 | }) -------------------------------------------------------------------------------- /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 syncGekkos from './modules/gekkos/sync' 6 | import syncNotifications from './modules/notifications/sync' 7 | import syncConfig from './modules/config/sync' 8 | 9 | export default function() { 10 | syncImports(); 11 | syncGekkos(); 12 | syncNotifications(); 13 | syncConfig(); 14 | } -------------------------------------------------------------------------------- /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 | if(!backendData) { 7 | return {}; 8 | } 9 | 10 | var exchangesRaw = backendData; 11 | var exchangesTemp = {}; 12 | 13 | exchangesRaw.forEach(e => { 14 | exchangesTemp[e.slug] = exchangesTemp[e.slug] || {markets: {}}; 15 | 16 | e.markets.forEach( pair => { 17 | let [ currency, asset ] = pair['pair']; 18 | exchangesTemp[e.slug].markets[currency] = exchangesTemp[e.slug].markets[currency] || []; 19 | exchangesTemp[e.slug].markets[currency].push( asset ); 20 | }); 21 | 22 | if ("exchangeMaxHistoryAge" in e) { 23 | exchangesTemp[e.slug].exchangeMaxHistoryAge = e.exchangeMaxHistoryAge; 24 | } 25 | 26 | exchangesTemp[e.slug].importable = e.providesFullHistory ? true : false; 27 | exchangesTemp[e.slug].tradable = e.tradable ? true : false; 28 | exchangesTemp[e.slug].requires = e.requires; 29 | }); 30 | 31 | return exchangesTemp; 32 | } 33 | 34 | 35 | const init = () => { 36 | get('apiKeys', (err, resp) => { 37 | store.commit('syncApiKeys', resp); 38 | }); 39 | 40 | get('exchanges', (err, resp) => { 41 | store.commit('syncExchanges', transformMarkets(resp)); 42 | }) 43 | } 44 | 45 | const sync = () => { 46 | bus.$on('apiKeys', data => { 47 | store.commit('syncApiKeys', data.exchanges); 48 | }); 49 | } 50 | 51 | export default function() { 52 | init(); 53 | sync(); 54 | } 55 | -------------------------------------------------------------------------------- /web/vue/src/store/modules/gekkos/mutations.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import _ from 'lodash'; 3 | const reduceState = require('../../../../../state/reduceState'); 4 | 5 | export const syncGekkos = (state, data) => { 6 | if(!data) { 7 | return state; 8 | } 9 | 10 | state.gekkos = data.live; 11 | state.archivedGekkos = data.archive; 12 | return state; 13 | } 14 | 15 | export const addGekko = (state, gekko) => { 16 | state.gekkos = { 17 | ...state.gekkos, 18 | [gekko.id]: gekko 19 | } 20 | return state; 21 | } 22 | 23 | export const updateGekko = (state, update) => { 24 | if(!update.id || !_.has(state.gekkos, update.id)) { 25 | return console.error('cannot update unknown gekko..');; 26 | } 27 | 28 | state.gekkos = { 29 | ...state.gekkos, 30 | [update.id]: reduceState(state.gekkos[update.id], update.event) 31 | } 32 | return state; 33 | } 34 | 35 | export const archiveGekko = (state, id) => { 36 | if(!_.has(state.gekkos, id)) { 37 | return console.error('cannot archive unknown gekko..'); 38 | } 39 | 40 | state.archivedGekkos = { 41 | ...state.archivedGekkos, 42 | [id]: { 43 | ...state.gekkos[id], 44 | stopped: true, 45 | active: false 46 | } 47 | } 48 | 49 | state.gekkos = _.omit(state.gekkos, id); 50 | return state; 51 | } 52 | 53 | export const errorGekko = (state, data) => { 54 | if(!_.has(state.gekkos, data.id)) { 55 | return console.error('cannot error unknown gekko..'); 56 | } 57 | 58 | state.gekkos = { 59 | ...state.gekkos, 60 | [data.id]: { 61 | ...state.gekkos[data.id], 62 | errored: true, 63 | errorMessage: data.error 64 | } 65 | } 66 | 67 | return state; 68 | } 69 | 70 | export const deleteGekko = (state, id) => { 71 | if(!_.has(state.archivedGekkos, id)) { 72 | return console.error('cannot delete unknown gekko..'); 73 | } 74 | 75 | state.archivedGekkos = _.omit(state.archivedGekkos, id); 76 | return state; 77 | } -------------------------------------------------------------------------------- /web/vue/src/store/modules/gekkos/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 | const gekkos = resp; 9 | store.commit('syncGekkos', gekkos); 10 | }); 11 | } 12 | 13 | const sync = () => { 14 | bus.$on('gekko_new', data => store.commit('addGekko', data.state)); 15 | bus.$on('gekko_event', data => store.commit('updateGekko', data)); 16 | bus.$on('gekko_archived', data => store.commit('archiveGekko', data.id)); 17 | bus.$on('gekko_error', data => store.commit('errorGekko', data)); 18 | bus.$on('gekko_deleted', data => store.commit('deleteGekko', data.id)); 19 | 20 | // unused: 21 | // bus.$on('gekko_stopped', data => store.commit('x', data.id)); 22 | // bus.$on('gekko_deleted', data => store.commit('x', data.id)); 23 | } 24 | 25 | export default function() { 26 | init(); 27 | sync(); 28 | } -------------------------------------------------------------------------------- /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/llSourcell/Watch-Me-Build-a-Trading-Bot/5b1aafa46cf35c56ce328c5880bac2ff425a2eb7/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/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 endpoint = `${config.host}${config.port === 80 ? '' : `:${config.port}`}${config.path}`; 5 | 6 | let basePath, restPath, wsPath; 7 | 8 | // rest API path 9 | if(config.ssl) { 10 | basePath = `https://${endpoint}`; 11 | } else { 12 | basePath = `http://${endpoint}`; 13 | } 14 | 15 | restPath = basePath + 'api/'; 16 | 17 | // ws API path 18 | if(config.ssl) { 19 | wsPath = `wss://${endpoint}api`; 20 | } else { 21 | wsPath = `ws://${endpoint}api`; 22 | } 23 | 24 | export { 25 | wsPath, 26 | restPath, 27 | basePath 28 | }; 29 | -------------------------------------------------------------------------------- /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/vue.config.js: -------------------------------------------------------------------------------- 1 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 2 | 3 | module.exports = { 4 | configureWebpack: { 5 | plugins: [ 6 | new CopyWebpackPlugin([ 7 | { 8 | from: '../baseUIconfig.js', 9 | to: '../public/UIconfig.js' 10 | }, 11 | { 12 | from: '../baseUIconfig.js', 13 | to: 'UIconfig.js' 14 | }, 15 | ]) 16 | ] 17 | }, 18 | baseUrl: '' 19 | } --------------------------------------------------------------------------------