├── .babelrc ├── .dockerignore ├── .editorconfig ├── .gitignore ├── Dockerfile ├── HOW_IT_WORKS.md ├── LICENSE ├── README.md ├── app.json ├── docker-compose.yml ├── docs └── CUSTOM_STRATEGIES.md ├── electron ├── .gitignore ├── .travis.yml ├── app │ ├── app.html │ ├── app.icns │ ├── main.development.js │ └── package.json ├── package.json ├── resources │ ├── icon.icns │ ├── icon.ico │ ├── icon.png │ └── icons │ │ ├── 1024x1024.png │ │ ├── 128x128.png │ │ ├── 16x16.png │ │ ├── 24x24.png │ │ ├── 256x256.png │ │ ├── 32x32.png │ │ ├── 48x48.png │ │ ├── 512x512.png │ │ ├── 64x64.png │ │ └── 96x96.png ├── server.js ├── src │ ├── app.html │ ├── components │ │ ├── Home.tsx │ │ └── style.sass │ └── index.tsx ├── tsconfig.json └── webpack │ ├── config.base.js │ ├── config.development.js │ ├── config.electron.js │ └── config.production.js ├── index.js ├── package.json ├── postman ├── Autotrader.postman_collection.json └── Autotrader.postman_environment.json ├── screenshots ├── gui3.png ├── setup2.png └── stoploss3.png ├── scripts └── docker-publish.sh ├── src ├── budfox │ ├── candleCreator.ts │ ├── heart.ts │ ├── index.ts │ ├── marketDataProvider.ts │ └── tradeBatcher.ts ├── database │ ├── config.js │ ├── index.ts │ ├── migrations │ │ ├── 20190314194535-create-candles.js │ │ ├── 20190314194907-create-trades.js │ │ ├── 20190314195103-create-user-exchanges.js │ │ ├── 20190316154850-create-triggers.js │ │ ├── 20190320183253-create-plugins.js │ │ └── 20190334194900-create-advices.js │ ├── models │ │ ├── advices.ts │ │ ├── candles.ts │ │ ├── plugins.ts │ │ ├── trades.ts │ │ ├── triggers.ts │ │ └── userexchanges.ts │ └── seeders │ │ └── .gitkeep ├── errors │ ├── BadRequestError.ts │ ├── HttpError.ts │ ├── InvalidAPIKeyError.ts │ ├── InvalidJWTError.ts │ ├── NotAuthorizedError.ts │ ├── NotFoundError.ts │ └── RateLimitExceeded.ts ├── exchanges │ ├── BinanceExchange.ts │ ├── BitfinexExchange.ts │ ├── BitmexExchange.ts │ ├── BtcmarketsExchange.ts │ ├── HitbtcExchange.ts │ ├── Independentreserve.ts │ └── core │ │ ├── BacktestableExchange.ts │ │ ├── BaseExchange.ts │ │ └── CCXTExchange.ts ├── iguana.ts ├── index.ts ├── indicators │ ├── Indicator.ts │ ├── RSI.ts │ ├── SMA.ts │ └── SMMA.ts ├── interfaces.ts ├── managers │ ├── AdviceManager.ts │ ├── BudfoxManager.ts │ ├── ExchangeManager.ts │ ├── PluginsManager.ts │ └── TriggerManager.ts ├── package.json ├── plugins │ ├── BasePlugin.ts │ ├── slack.ts │ └── telegram.ts ├── server │ ├── controllers │ │ ├── advices.ts │ │ ├── keys.ts │ │ ├── plugins.ts │ │ └── triggers.ts │ ├── index.ts │ └── routes │ │ ├── advices.ts │ │ ├── index.ts │ │ ├── keys.ts │ │ ├── plugins.ts │ │ └── triggers.ts ├── strategies │ ├── BaseStrategy.ts │ ├── RSIStrategy.ts │ └── StopLossStrategy.ts ├── triggers │ ├── BaseTrigger.ts │ ├── CancelOrderTrigger.ts │ ├── DynamicTieredTakeProfitTrigger.ts │ ├── FutureOrderTrigger.ts │ ├── StopLossTakeProfitTrigger.ts │ ├── StopLossTrigger.ts │ ├── TakeProfitTrigger.ts │ ├── TieredTakeProfitTrigger.ts │ └── TrailingStopTrigger.ts └── utils │ ├── index.ts │ └── log.ts ├── test ├── Trigger │ ├── cancelOrderTriggerTests.ts │ ├── dynamicTieredTakeProfitTriggerTests.ts │ ├── futureOrderTriggerTests.ts │ ├── stopLossTakeProfitTests.ts │ ├── stopLossTriggerTests.ts │ ├── takeProfitTriggerTests.ts │ ├── tieredTakeProfitTriggerTests.ts │ └── trailingStopTriggerTests.ts ├── data.js └── test.ts ├── tsconfig.json ├── tslint.json ├── webpack.config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["env"] 3 | } 4 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .editorconfig 3 | .eslint* 4 | .idea/ 5 | .nyc_output/ 6 | .vscode/ 7 | *.sublime-project 8 | *.sublime-workspace 9 | node_modules/ 10 | nodemon.json 11 | npm-debug.log 12 | storage 13 | storage_prod 14 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_size = 2 8 | indent_style = space 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | .eslintcache 3 | .grunt 4 | .lock-wscript 5 | .next 6 | .node_repl_history 7 | .npm 8 | .nyc_output 9 | .sqlite 10 | .vscode 11 | .yarn-integrity 12 | *.log 13 | *.pid 14 | *.pid.lock 15 | *.seed 16 | *.sqlite 17 | *.tgz 18 | bower_components 19 | build/Release 20 | coverage 21 | jspm_packages/ 22 | lib-cov 23 | logs 24 | node_modules/ 25 | npm-debug.log* 26 | pids 27 | dist 28 | storage 29 | storage_prod 30 | typings/ 31 | yarn-debug.log* 32 | yarn-error.log* 33 | 34 | 35 | electron/build 36 | electron/release 37 | electron/app/main.js 38 | electron/app/main.js.map 39 | electron/app/bundle.js 40 | electron/app/bundle.js.map 41 | electron/app/style.css 42 | electron/app/style.css.map 43 | electron/dist 44 | electron/main.js 45 | electron/main.js.map 46 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:10.13.0 2 | 3 | ENV NODE_ENV production 4 | 5 | # Create app directory 6 | WORKDIR /app 7 | 8 | # Install app dependencies 9 | COPY package.json ./ 10 | COPY yarn.lock ./ 11 | RUN yarn install 12 | 13 | # Bundle app source 14 | COPY . . 15 | 16 | # Copy sensitive files 17 | # COPY .env . 18 | 19 | # Final configuration and then run! 20 | EXPOSE 8080 21 | CMD [ "npm", "run", "start" ] 22 | -------------------------------------------------------------------------------- /HOW_IT_WORKS.md: -------------------------------------------------------------------------------- 1 | How it works 2 | ============ 3 | 4 | Iguana is heavily insipired by the popular open-source bitcoin trading bot, [Gekko](https://github.com/askmike/gekko). Hence Iguana has a couple of different components that follow the same layout of Gekko. 5 | 6 | All data, including candle data, list of trades streamed from an exchange, list of trades made by the bot is stored in a SQL-compatiable database (MySQL, Postgres, Sqlite etc...). The connection details can be found in [`src/database/config.json`](./src/database/config.json). 7 | 8 | 9 | Understanding the code 10 | ====================== 11 | 12 | 13 | Contributing 14 | ============ 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 CryptoControl 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Iguana - Algo Trading Server for Advanced Orders 2 | ================================================ 3 | 4 | This server is meant to be used by the CryptoControl Terminal to execute advanced orders like stop-losses, trailing stop-losses, take profit (and more) on exchanges that don't support advanced orders. 5 | 6 | Iguana is heavily insipired by the popular open-source bitcoin trading bot, [Gekko](https://github.com/askmike/gekko). Iguana is also a 7 | better version of Gekko because it supports more exchanges, has a better interface and uses websocket/FIX apis hence it is **truly real-time**. 8 | 9 | In the screenshot below, users have the ability to execute stop-loss and take-profit orders from the trading screen within the CryptoControl Terminal, on an exchange that doesn't support these kinds of orders. 10 | 11 | ![Stop Loss Screenshot](./screenshots/stoploss3.png) 12 | 13 | The CryptoControl Terminal allows users to host their own trading servers so that they can execute advanced orders from within the terminal itself but never expose their API keys to CryptoControl. Since the trading server is open-source, everything is transparent. 14 | 15 | 16 | ## Disclaimer 17 | **USE THE SOFTWARE AT YOUR OWN RISK. YOU ARE RESPONSIBLE FOR YOUR OWN MONEY.** 18 | 19 | **THE AUTHORS AND ALL AFFILIATES ASSUME NO RESPONSIBILITY FOR ANY DAMAGE OR LOSS CAUSED BY THIS SOFTWARE.** 20 | 21 | **THERE CAN BE BUGS AND THE BOT MAY NOT PERFORM AS EXPECTED OR SPECIFIED. PLEASE CONSIDER TESTING IT FIRST WITH PAPER TRADING AND/OR BACKTESTING ON HISTORICAL DATA. ALSO LOOK AT THE CODE TO SEE HOW IT IS WORKS.** 22 | 23 | 24 | ## Features 25 | - Support for over 120 exchanges 26 | - Realtime streaming of trades via Websocket/FIX with select exchanges 27 | - Place advanced kinds of orders (stop-loss, take-profit etc..) 28 | - Create and run your own trading strategies (coming soon) 29 | - Backtesting and Paper trading (coming soon) 30 | - Plug in any kind of notification system (Slack, Telegram, Discord etc..) 31 | 32 | 33 | ## Quick Start 34 | [![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/cryptocontrol/algo-trading-server 35 | ) 36 | 37 | For a quick start with docker-compose run 38 | ``` 39 | docker-compose up 40 | ``` 41 | or via docker, run 42 | ``` 43 | docker run -p 8080:8080 -e SERVER_SECRET=set_random_password_here cryptocontrol/iguana 44 | ``` 45 | 46 | 47 | ## Usage 48 | Host this server in your own machine and enter in the server's ip and password in the CryptoControl's trading's settings screen as shown below. 49 | 50 | ![Insert Server Details](./screenshots/setup2.png) 51 | 52 | Once set, you'll be able to execute advanced orders straight from your terminal. 53 | 54 | 55 | ## Using in your local machine 56 | The server can also be downloaded as an executable and run from your own machine locally. 57 | 58 | Simply download the executable, enter in a password and click on the button to start the server. Once the server has started, copy the details back into the terminal. 59 | 60 | ![Desktop GUI](./screenshots/gui3.png) 61 | 62 | 66 | 67 | ## Authentication 68 | Iguana uses a password to encrypt/decrypt all API keys and to authenticate users. 69 | 70 | If you are setting up the bot on your own cloud, then the password is taken from the environment variable `SERVER_PASSWORD`. If you're setting up the bot via the GUI, then simply enter in the password in the password field before you start the bot. 71 | 72 | Once set, all users can simply connect to the bot from the CryptoControl terminal with the right password. 73 | 74 | 75 | ## Signals/Triggers 76 | Iguana supports triggers, which are one-time actions that execute when a price meets a certain condition. 77 | 78 | Supported Signals/Triggers: 79 | - Stop Loss 80 | - Take Profit 81 | - Trailing Stop 82 | 83 | ## Strategies (Coming Soon) 84 | Iguana supports auto-trading with various trading strategies. Strategies keep running forever and execute trades on the basis of certain conditions (like Technical Indicators). 85 | 86 | Strategies can also be backtested for performance with historic data from an exchange. 87 | 88 | Supported Strategies: 89 | - RSI Strategy 90 | 91 | You can also build your own strategies and use it with the CryptoControl terminal. For more info view [building custom strategies](./docs/CUSTOM_STRATEGIES.md). 92 | 93 | ## Supported Exchanges 94 | The following exchanges are supported: 95 | 96 | _1btcxe, acx, allcoin, anxpro, anybits, bcex, bibox, bigone, binance, bit2c, bitbank, bitbay, bitfinex, bitflyer, bitforex, bithumb, bitibu, bitkk, bitlish, bitmarket, bitmex, bitsane, bitso, bitstamp, bittrex, bitz, bl3p, bleutrade, braziliex, btcalpha, btcbox, btcchina, btcexchange, btcmarkets, btctradeim, btctradeua, btcturk, buda, bxinth, ccex, cex, chbtc, chilebit, cobinhood, coinbase, coinbaseprime, coinbasepro, coincheck, coinegg, coinex, coinexchange, coinfalcon, coinfloor, coingi, coinmarketcap, coinmate, coinnest, coinone, coinspot, cointiger, coolcoin, coss, crex24, crypton, cryptopia, deribit, dsx, ethfinex, exmo, exx, fcoin, fcoinjp, flowbtc, foxbit, fybse, fybsg, gatecoin, gateio, gdax, gemini, getbtc, hadax, hitbtc, huobipro, huobiru, ice3x, independentreserve, indodax, itbit, jubi, kkex, kraken, kucoin, kuna, lakebtc, lbank, liqui, liquid, livecoin, luno, lykke, mercado, mixcoins, negociecoins, nova, okcoincny, okcoinusd, okex, paymium, poloniex, quadrigacx, rightbtc, southxchange, stronghold, surbitcoin, theocean, therock, tidebit, tidex, uex, upbit, urdubit, vaultoro, vbtc, virwox, xbtce, yobit, yunbi, zaif, zb 97 | 98 | The following exchanges are supported with real-time data (ie; prices & trades are streamed real-time): 99 | 100 | binance 101 | 102 | ## Plugins 103 | Plugins allow Iguana to communicate to the user via multiple channels in realtime. 104 | - Slack 105 | - SMS Notifications (coming soon) 106 | 107 | 108 | ## Upcoming Features 109 | For any suggestions on features that you'd like to see, let us know by either submitting an issue or writing to us at contact@cryptocontrol.io 110 | 111 | Some of the upcoming features that we're working on include: 112 | 113 | - AI Integration 114 | - HFT (High Frequency Trading) 115 | - Custom Strategies 116 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "iguana-trading-server", 3 | "description": "Trading Cloud for the CryptoControl Terminal", 4 | "repository": "https://github.com/cryptocontrol/algo-trading-server", 5 | "logo": "https://node-js-sample.herokuapp.com/node.png", 6 | "keywords": ["node", "express", "static"], 7 | "env": { 8 | "SERVER_SECRET": { 9 | "description": "A password to access the server" 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | iguana: 4 | build: . 5 | ports: 6 | - "8080:8080" 7 | environment: 8 | - SERVER_SECRET=secret_keyboard_cat1 9 | volumes: 10 | - ./storage_prod:/app/storage 11 | -------------------------------------------------------------------------------- /docs/CUSTOM_STRATEGIES.md: -------------------------------------------------------------------------------- 1 | Custom Strategies with Iguana 2 | ============================= 3 | 4 | documentation coming soon. 5 | -------------------------------------------------------------------------------- /electron/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | app/node_modules 29 | 30 | # OSX 31 | .DS_Store 32 | 33 | # App packaged 34 | release 35 | app/main.js 36 | app/main.js.map 37 | app/bundle.js 38 | app/bundle.js.map 39 | app/style.css 40 | app/style.css.map 41 | dist 42 | main.js 43 | main.js.map 44 | 45 | .idea 46 | -------------------------------------------------------------------------------- /electron/.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | language: node_js 4 | 5 | node_js: 6 | - 8 7 | - 7 8 | 9 | cache: 10 | yarn: true 11 | directories: 12 | - node_modules 13 | - app/node_modules 14 | 15 | addons: 16 | apt: 17 | sources: 18 | - ubuntu-toolchain-r-test 19 | packages: 20 | - g++-4.8 21 | - icnsutils 22 | - graphicsmagick 23 | - xz-utils 24 | - xorriso 25 | 26 | before_install: yarn global add greenkeeper-lockfile@1 27 | 28 | install: 29 | - export CXX="g++-4.8" 30 | - yarn 31 | - cd app && yarn && cd .. 32 | - /sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile 33 | --background --exec /usr/bin/Xvfb -- :99 -ac -screen 0 1280x1024x16 34 | 35 | before_script: 36 | - greenkeeper-lockfile-update 37 | - export DISPLAY=:99.0 38 | - sh -e /etc/init.d/xvfb start & 39 | - sleep 3 40 | 41 | script: 42 | - node --version 43 | - yarn package 44 | - yarn test 45 | - yarn test-e2e 46 | 47 | after_script: greenkeeper-lockfile-upload 48 | 49 | env: 50 | global: 51 | secure: hH+MmHE3WhyyBflZoOZLqqr501OOnvA1VtaqF3WU9rJCpjnKb4fZvHejYtVw65Ho2C40G2a34oBGxnT6INF1BfClYz4IMijL689xuR8dgStQzvcpMTwEQkrLsaCTSqXJR6nzqbRRGmaRfjSLdF6vhd3bGmtVNMEtoEky5EsXeBVjn8D5WMrme17m4F4D/qjHty6Xr5PjZL6eInVKlPZEbALYBhnIWqE5Xw6LcGa8JkqsWw80sLNtz4yWQSwkjrZ1lzCNd/GSUDN5K/BFK6kUZB5HpJZUzUoIQyXmEft+ALZFE4p4tX/H044JguPN9sKsP4SNkz2+3tOMevz/x5of3ssLMqB7gFtd5SGKwjdn6sWYNHKeM0MALyXAARhilyzY0v+xf226ZLRb++8Ih/vAZNymvQjn0idBfwC8ffUSXEx786Zysot1AqWrxaktKFHkBHkiQ4BGkAXVT4FLCgLtdMta+NLYYmuNrmXoU2iEhSzwvZmw94wcBxZie+TvuXE76TgrOiopRPbGXw/0iDm8J1MVWpoPmqSLUG5Vbq9fSNZnR/6j89j7Ws83fdnfZby+qOcV5WMJetQs/EHTxfWJu/AM6bxCYSSmXEJUb8dHpk8el8T+MGLI9pCTiiyy1ozOeo1WKpcOdmF6GU64EOxRWn2837+a9W0kXXAj6tPCW6A= 52 | -------------------------------------------------------------------------------- /electron/app/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CryptoControl Trading Server 6 | 17 | 18 | 19 |
20 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /electron/app/app.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cryptocontrol/algo-trading-server/72cbbe3c9a9c2a147fbe2e046a1f7fec319bff86/electron/app/app.icns -------------------------------------------------------------------------------- /electron/app/main.development.js: -------------------------------------------------------------------------------- 1 | const { app, BrowserWindow, Menu } = require('electron') 2 | 3 | let mainWindow = null 4 | 5 | if (process.env.NODE_ENV === 'production') { 6 | const sourceMapSupport = require('source-map-support') // eslint-disable-line 7 | sourceMapSupport.install() 8 | } 9 | 10 | if (process.env.NODE_ENV === 'development') { 11 | require('electron-debug')() // eslint-disable-line global-require 12 | const path = require('path') // eslint-disable-line 13 | const p = path.join(__dirname, '..', 'app', 'node_modules') // eslint-disable-line 14 | require('module').globalPaths.push(p) // eslint-disable-line 15 | } 16 | 17 | app.on('window-all-closed', () => { 18 | if (process.platform !== 'darwin') app.quit() 19 | }) 20 | 21 | 22 | const installExtensions = () => { 23 | if (process.env.NODE_ENV === 'development') { 24 | const installer = require('electron-devtools-installer') // eslint-disable-line global-require 25 | 26 | const extensions = [ 27 | 'REACT_DEVELOPER_TOOLS', 28 | 'REDUX_DEVTOOLS' 29 | ] 30 | const forceDownload = !!process.env.UPGRADE_EXTENSIONS 31 | return Promise.all(extensions.map(name => installer.default(installer[name], forceDownload))) 32 | } 33 | 34 | return Promise.resolve([]) 35 | } 36 | 37 | app.on('ready', () => 38 | installExtensions() 39 | .then(() => { 40 | mainWindow = new BrowserWindow({ 41 | show: false, 42 | width: 500, 43 | height: 500 44 | }) 45 | 46 | mainWindow.loadURL(`file://${__dirname}/app.html`) 47 | 48 | mainWindow.webContents.on('did-finish-load', () => { 49 | mainWindow.show() 50 | mainWindow.focus() 51 | }) 52 | 53 | mainWindow.on('closed', () => { 54 | mainWindow = null 55 | }) 56 | 57 | if (process.env.NODE_ENV === 'development') { 58 | mainWindow.webContents.on('context-menu', (e, props) => { 59 | const { x, y } = props 60 | 61 | Menu.buildFromTemplate([{ 62 | label: 'Inspect element', 63 | click() { 64 | mainWindow.inspectElement(x, y) 65 | } 66 | }]).popup(mainWindow) 67 | }) 68 | } 69 | })) 70 | -------------------------------------------------------------------------------- /electron/app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cryptocontrol-trading-server", 3 | "productName": "CryptoControl Trading Server", 4 | "version": "1.0.0", 5 | "description": "GUI for the CryptoControl Server", 6 | "main": "./main.js", 7 | "author": { 8 | "name": "CryptoControl", 9 | "email": "contact@cryptocontrol.io" 10 | }, 11 | "scripts": { 12 | "postinstall": "npm rebuild --runtime=electron --target=4.0.8 --disturl=https://atom.io/download/atom-shell --build-from-source" 13 | }, 14 | "license": "MIT" 15 | } 16 | -------------------------------------------------------------------------------- /electron/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "electron-trading-app", 3 | "private": true, 4 | "version": "0.0.1", 5 | "main": "main.js", 6 | "scripts": { 7 | "hot-server": "cross-env NODE_ENV=development node --max_old_space_size=2096 server.js", 8 | "build-main": "cross-env NODE_ENV=production node ./node_modules/webpack/bin/webpack --config webpack/config.electron.js --progress --profile --colors", 9 | "build-renderer": "cross-env NODE_ENV=production node ./node_modules/webpack/bin/webpack --config webpack/config.production.js --progress --profile --colors", 10 | "build": "npm run build-main && npm run build-renderer", 11 | "start": "cross-env NODE_ENV=production electron ./src", 12 | "start-hot": "cross-env HOT=1 NODE_ENV=development electron ./src/main.development", 13 | "dev": "npm run hot-server -- --start-hot", 14 | "package": "npm run build && build --publish never", 15 | "package-win": "npm run build && build --win --x64", 16 | "package-linux": "npm run build && build --linux", 17 | "package-all": "npm run build && build -mwl", 18 | "cleanup": "mop -v" 19 | }, 20 | "build": { 21 | "productName": "CryptoControl Algo Server", 22 | "appId": "io.cryptocontrol.tradingserver", 23 | "dmg": { 24 | "contents": [ 25 | { 26 | "x": 410, 27 | "y": 150, 28 | "type": "link", 29 | "path": "/Applications" 30 | }, 31 | { 32 | "x": 130, 33 | "y": 150, 34 | "type": "file" 35 | } 36 | ] 37 | }, 38 | "files": [ 39 | "dist/", 40 | "node_modules/", 41 | "app.html", 42 | "main.js", 43 | "main.js.map", 44 | "package.json" 45 | ], 46 | "directories": { 47 | "buildResources": "resources", 48 | "output": "release" 49 | }, 50 | "win": { 51 | "target": "nsis" 52 | }, 53 | "linux": { 54 | "target": [ 55 | "deb", 56 | "AppImage" 57 | ] 58 | } 59 | }, 60 | "bin": { 61 | "electron": "./node_modules/.bin/electron" 62 | }, 63 | "devEngines": { 64 | "node": ">=6.x", 65 | "npm": ">=3.x" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /electron/resources/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cryptocontrol/algo-trading-server/72cbbe3c9a9c2a147fbe2e046a1f7fec319bff86/electron/resources/icon.icns -------------------------------------------------------------------------------- /electron/resources/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cryptocontrol/algo-trading-server/72cbbe3c9a9c2a147fbe2e046a1f7fec319bff86/electron/resources/icon.ico -------------------------------------------------------------------------------- /electron/resources/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cryptocontrol/algo-trading-server/72cbbe3c9a9c2a147fbe2e046a1f7fec319bff86/electron/resources/icon.png -------------------------------------------------------------------------------- /electron/resources/icons/1024x1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cryptocontrol/algo-trading-server/72cbbe3c9a9c2a147fbe2e046a1f7fec319bff86/electron/resources/icons/1024x1024.png -------------------------------------------------------------------------------- /electron/resources/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cryptocontrol/algo-trading-server/72cbbe3c9a9c2a147fbe2e046a1f7fec319bff86/electron/resources/icons/128x128.png -------------------------------------------------------------------------------- /electron/resources/icons/16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cryptocontrol/algo-trading-server/72cbbe3c9a9c2a147fbe2e046a1f7fec319bff86/electron/resources/icons/16x16.png -------------------------------------------------------------------------------- /electron/resources/icons/24x24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cryptocontrol/algo-trading-server/72cbbe3c9a9c2a147fbe2e046a1f7fec319bff86/electron/resources/icons/24x24.png -------------------------------------------------------------------------------- /electron/resources/icons/256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cryptocontrol/algo-trading-server/72cbbe3c9a9c2a147fbe2e046a1f7fec319bff86/electron/resources/icons/256x256.png -------------------------------------------------------------------------------- /electron/resources/icons/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cryptocontrol/algo-trading-server/72cbbe3c9a9c2a147fbe2e046a1f7fec319bff86/electron/resources/icons/32x32.png -------------------------------------------------------------------------------- /electron/resources/icons/48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cryptocontrol/algo-trading-server/72cbbe3c9a9c2a147fbe2e046a1f7fec319bff86/electron/resources/icons/48x48.png -------------------------------------------------------------------------------- /electron/resources/icons/512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cryptocontrol/algo-trading-server/72cbbe3c9a9c2a147fbe2e046a1f7fec319bff86/electron/resources/icons/512x512.png -------------------------------------------------------------------------------- /electron/resources/icons/64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cryptocontrol/algo-trading-server/72cbbe3c9a9c2a147fbe2e046a1f7fec319bff86/electron/resources/icons/64x64.png -------------------------------------------------------------------------------- /electron/resources/icons/96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cryptocontrol/algo-trading-server/72cbbe3c9a9c2a147fbe2e046a1f7fec319bff86/electron/resources/icons/96x96.png -------------------------------------------------------------------------------- /electron/server.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Setup and run the development server for Hot-Module-Replacement 3 | * https://webpack.github.io/docs/hot-module-replacement-with-webpack.html 4 | */ 5 | 6 | const express = require('express'); 7 | const webpack = require('webpack'); 8 | const webpackDevMiddleware = require('webpack-dev-middleware'); 9 | const webpackHotMiddleware = require('webpack-hot-middleware'); 10 | const { spawn } = require('child_process'); 11 | 12 | const config = require('./webpack/config.development'); 13 | 14 | const argv = require('minimist')(process.argv.slice(2)); 15 | 16 | const app = express(); 17 | const compiler = webpack(config); 18 | const PORT = process.env.PORT || 3000; 19 | 20 | const wdm = webpackDevMiddleware(compiler, { 21 | publicPath: config.output.publicPath, 22 | stats: { 23 | colors: true 24 | } 25 | }); 26 | 27 | app.use(wdm); 28 | 29 | app.use(webpackHotMiddleware(compiler)); 30 | 31 | const server = app.listen(PORT, 'localhost', serverError => { 32 | if (serverError) { 33 | return console.error(serverError); 34 | } 35 | 36 | if (argv['start-hot']) { 37 | spawn('npm', ['run', 'start-hot'], { shell: true, env: process.env, stdio: 'inherit' }) 38 | .on('close', code => process.exit(code)) 39 | .on('error', spawnError => console.error(spawnError)); 40 | } 41 | 42 | console.log(`Listening at http://localhost:${PORT}`); 43 | }); 44 | 45 | process.on('SIGTERM', () => { 46 | console.log('Stopping dev server'); 47 | wdm.close(); 48 | server.close(() => { 49 | process.exit(0); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /electron/src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CryptoControl Trading Server 6 | 17 | 18 | 19 |
20 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /electron/src/components/Home.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import server from '../../../src/server' 3 | import './style.sass' 4 | 5 | 6 | interface IState { 7 | error?: string 8 | loading: boolean 9 | messages: string[] 10 | password: string 11 | isServerRunning: boolean 12 | } 13 | 14 | 15 | export default class Home extends React.Component<{}, IState> { 16 | instance: any 17 | 18 | 19 | state: IState = { 20 | loading: false, 21 | password: '', 22 | messages: [], 23 | isServerRunning: false 24 | } 25 | 26 | 27 | runServer = () => { 28 | this.setState({ loading: true }) 29 | 30 | try { 31 | const port = process.env.PORT || 8080 32 | this.addMessage('starting server on port ' + port) 33 | 34 | process.env.SERVER_SECRET = this.state.password 35 | 36 | server.set('secret', this.state.password) 37 | this.instance = server.listen(port) 38 | 39 | this.addMessage('listening on port ' + port) 40 | this.setState({ isServerRunning: true, loading: false }) 41 | } catch (e) { 42 | this.addMessage('Error! ' + e.message) 43 | this.setState({ error: e.message, loading: false }) 44 | } 45 | } 46 | 47 | 48 | closeServer = () => { 49 | this.addMessage('shutting down server') 50 | this.setState({ loading: true }) 51 | 52 | try { 53 | // this.instance. 54 | this.addMessage('server shut down') 55 | this.setState({ isServerRunning: false, loading: false }) 56 | } catch(e) { 57 | this.setState({ error: e.message, loading: false }) 58 | this.addMessage('Error! ' + e.message) 59 | } 60 | } 61 | 62 | 63 | addMessage = (text: string) => { 64 | this.setState({ messages: [...this.state.messages, text] }) 65 | } 66 | 67 | 68 | renderConnectInstructions () { 69 | if (!this.state.isServerRunning) return 70 | return ( 71 |
72 |

73 | Great! the server is running. In the CryptoControl terminal enter in 74 | the following details 75 |

76 | 77 |
78 | Server Url: http://127.0.0.1:8080 79 |
80 |
81 | Password: {process.env.SERVER_SECRET} 82 |
83 |
84 | ) 85 | } 86 | 87 | render() { 88 | const { isServerRunning, loading, password } = this.state 89 | 90 | return ( 91 |
92 |

CryptoControl Trading Server

93 |
94 |

95 | This server is meant to be used by the CryptoControl Terminal to execute advanced orders like stop-losses, trailing stop-losses, take profit (and more) on exchanges that don't support advanced orders. 96 |

97 |

98 | To start the server, please enter a password and 99 | then press the start button. 100 |

101 |
102 | 103 |
104 | 105 |
106 |
107 |
108 | Password (required): this.setState({ password: e.target.value })} 110 | type="text" 111 | placeholder="something secretive" /> 112 |
113 |

114 | The password is used to encrypt all your API keys and is used to authenticate 115 | you from the CryptoControl terminal 116 |

117 |
118 | 119 |
120 | 125 | 126 | {/* */} 131 |
132 | 133 | {this.renderConnectInstructions()} 134 | 135 |
Server Status:   136 | {isServerRunning ? 137 | Running : 138 | Not Running} 139 |
140 |
141 | 142 |
143 | 151 | 152 | {/*
*/} 153 | 154 | {/*
155 | Message Console: 156 | {this.state.messages.map((message, index) => { 157 | return (
{message}
) 158 | })} 159 |
*/} 160 |
161 | ); 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /electron/src/components/style.sass: -------------------------------------------------------------------------------- 1 | body, html 2 | background: #1b1b1b 3 | margin: 0 4 | padding: 9 5 | font-family: Arial, Helvetica, sans-serif 6 | font-size: 12px 7 | line-height: 1.5 8 | 9 | #page-home 10 | padding: 15px 11 | color: #ddd 12 | 13 | footer, header 14 | color: #999 15 | 16 | .home 17 | height: 100vh 18 | position: relative 19 | 20 | .container 21 | padding-top: 30% 22 | text-align: center 23 | 24 | .password-section 25 | margin-bottom: 10px 26 | 27 | input 28 | outline: none 29 | background: #000 30 | color: #fff 31 | border: 1px solid #333 32 | padding: 5px 33 | 34 | .divider 35 | height: 1px 36 | background: rgba(#fff, 0.1) 37 | margin: 10px 0 38 | 39 | h1 40 | margin: 0 41 | 42 | a 43 | text-decoration: none 44 | color: #fff 45 | border-bottom: 1px dotted #fff 46 | 47 | button 48 | background: rgba(#fff, 0.1) 49 | border: 1px solid rgba(#fff, 0.2) 50 | color: #fff 51 | cursor: pointer 52 | margin-bottom: 10px 53 | margin-right: 5px 54 | outline: none 55 | padding: 4px 10px 56 | 57 | &:disabled 58 | opacity: 0.5 59 | 60 | .messages 61 | background: #000 62 | padding: 10px 63 | border: 1px solid #333 64 | 65 | .message 66 | margin-top: 5px 67 | color: rgba(#fff, 0.5) 68 | -------------------------------------------------------------------------------- /electron/src/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { render } from 'react-dom' 3 | import { AppContainer } from 'react-hot-loader' 4 | 5 | import Home from './components/Home' 6 | 7 | 8 | render( 9 | 10 | 11 | , 12 | document.getElementById('root') 13 | ) 14 | -------------------------------------------------------------------------------- /electron/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "moduleResolution": "node", 5 | "module": "commonjs", 6 | "jsx": "react", 7 | "lib": [ 8 | "dom", 9 | "es6", 10 | "dom.iterable", 11 | "scripthost" 12 | ], 13 | "types": [], 14 | 15 | "alwaysStrict": true, 16 | "strictNullChecks": true, 17 | "noImplicitAny": false, 18 | "noImplicitReturns": true, 19 | "noImplicitThis": true, 20 | "noUnusedLocals": true, 21 | 22 | "experimentalDecorators": true, 23 | "emitDecoratorMetadata": true, 24 | 25 | "sourceMap": true, 26 | 27 | "outDir": "dist" 28 | }, 29 | "files": [ 30 | "src/index.tsx" 31 | ], 32 | "include": [ 33 | "src/**/*.ts", 34 | "src/**/*.tsx", 35 | "test/**/*.ts", 36 | "test/**/*.tsx", 37 | "node_modules/@types/**/*.d.ts" 38 | ], 39 | "exclude": [ 40 | "dist" 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /electron/webpack/config.base.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Base webpack config used across other specific configs 3 | */ 4 | 5 | const path = require('path') 6 | const { dependencies: externals } = require('../app/package.json') 7 | 8 | module.exports = { 9 | module: { 10 | loaders: [{ 11 | test: /\.tsx?$/, 12 | loaders: ['react-hot-loader/webpack', 'ts-loader'], 13 | exclude: /node_modules/ 14 | }, { 15 | test: /\.json$/, 16 | loader: 'json-loader' 17 | }] 18 | }, 19 | 20 | output: { 21 | path: path.join(__dirname, '../app'), 22 | filename: 'bundle.js', 23 | 24 | // https://github.com/webpack/webpack/issues/1114 25 | libraryTarget: 'commonjs2' 26 | }, 27 | 28 | // https://webpack.github.io/docs/configuration.html#resolve 29 | resolve: { 30 | extensions: ['.js', '.ts', '.tsx', '.json'], 31 | modules: [ 32 | path.join(__dirname, 'app'), 33 | 'node_modules', 34 | ] 35 | }, 36 | 37 | plugins: [], 38 | 39 | externals: Object.keys(externals || {}) 40 | }; 41 | -------------------------------------------------------------------------------- /electron/webpack/config.development.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-len */ 2 | /** 3 | * Build config for development process that uses Hot-Module-Replacement 4 | * https://webpack.github.io/docs/hot-module-replacement-with-webpack.html 5 | */ 6 | 7 | const webpack = require('webpack') 8 | const merge = require('webpack-merge') 9 | const baseConfig = require('./config.base') 10 | const path = require('path') 11 | 12 | const port = process.env.PORT || 3000; 13 | 14 | module.exports = merge(baseConfig, { 15 | devtool: 'inline-source-map', 16 | 17 | entry: [ 18 | 'react-hot-loader/patch', 19 | `webpack-hot-middleware/client?path=http://localhost:${port}/__webpack_hmr&reload=true`, 20 | path.join(__dirname, '../src/index'), 21 | ], 22 | 23 | output: { 24 | publicPath: `http://localhost:${port}/dist/` 25 | }, 26 | 27 | module: { 28 | // preLoaders: [ 29 | // { 30 | // test: /\.js$/, 31 | // loader: 'eslint-loader', 32 | // exclude: /node_modules/ 33 | // } 34 | // ], 35 | loaders: [ 36 | // Add SASS support - compile all other .scss files and pipe it to style.css 37 | { 38 | test: /\.sass$/, 39 | loaders: [ 40 | 'style-loader', 41 | { loader: 'css-loader', options: { importLoaders: 1 } }, 42 | 'sass-loader' 43 | ] 44 | }, 45 | 46 | // WOFF Font 47 | { 48 | test: /\.woff(\?v=\d+\.\d+\.\d+)?$/, 49 | use: { 50 | loader: 'url-loader', 51 | options: { 52 | limit: 10000, 53 | mimetype: 'application/font-woff', 54 | } 55 | }, 56 | }, 57 | // WOFF2 Font 58 | { 59 | test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/, 60 | use: { 61 | loader: 'url-loader', 62 | options: { 63 | limit: 10000, 64 | mimetype: 'application/font-woff', 65 | } 66 | } 67 | }, 68 | // TTF Font 69 | { 70 | test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, 71 | use: { 72 | loader: 'url-loader', 73 | options: { 74 | limit: 10000, 75 | mimetype: 'application/octet-stream' 76 | } 77 | } 78 | }, 79 | // EOT Font 80 | { 81 | test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, 82 | use: 'file-loader', 83 | }, 84 | // SVG Font 85 | { 86 | test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, 87 | use: { 88 | loader: 'url-loader', 89 | options: { 90 | limit: 10000, 91 | mimetype: 'image/svg+xml', 92 | } 93 | } 94 | }, 95 | // Common Image Formats 96 | { 97 | test: /\.(?:ico|gif|png|jpg|jpeg|webp)$/, 98 | use: 'url-loader', 99 | } 100 | ] 101 | }, 102 | 103 | plugins: [ 104 | // https://webpack.github.io/docs/hot-module-replacement-with-webpack.html 105 | new webpack.HotModuleReplacementPlugin(), 106 | 107 | new webpack.NoEmitOnErrorsPlugin(), 108 | 109 | // NODE_ENV should be production so that modules do not perform certain development checks 110 | new webpack.DefinePlugin({ 111 | 'process.env.NODE_ENV': JSON.stringify('development') 112 | }), 113 | 114 | new webpack.LoaderOptionsPlugin({ 115 | debug: true 116 | }), 117 | ], 118 | 119 | // https://github.com/chentsulin/webpack-target-electron-renderer#how-this-module-works 120 | target: 'electron-renderer' 121 | }); 122 | -------------------------------------------------------------------------------- /electron/webpack/config.electron.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Build config for electron 'Main Process' file 3 | */ 4 | 5 | const webpack = require('webpack') 6 | const merge = require('webpack-merge') 7 | const path = require('path') 8 | const baseConfig = require('./config.base') 9 | 10 | module.exports = merge(baseConfig, { 11 | devtool: 'source-map', 12 | 13 | entry: [path.join(__dirname, '../app/main.development')], 14 | 15 | // 'main.js' in root 16 | output: { 17 | path: path.join(__dirname, '../app'), 18 | filename: 'main.js' 19 | }, 20 | 21 | plugins: [ 22 | // Add source map support for stack traces in node 23 | // https://github.com/evanw/node-source-map-support 24 | // new webpack.BannerPlugin( 25 | // 'require("source-map-support").install();', 26 | // { raw: true, entryOnly: false } 27 | // ), 28 | new webpack.DefinePlugin({ 29 | 'process.env': { 30 | NODE_ENV: JSON.stringify('production') 31 | } 32 | }) 33 | ], 34 | 35 | /** 36 | * Set target to Electron specific node.js env. 37 | * https://github.com/chentsulin/webpack-target-electron-renderer#how-this-module-works 38 | */ 39 | target: 'electron-main', 40 | 41 | /** 42 | * Disables webpack processing of __dirname and __filename. 43 | * If you run the bundle in node.js it falls back to these values of node.js. 44 | * https://github.com/webpack/webpack/issues/2010 45 | */ 46 | node: { 47 | __dirname: false, 48 | __filename: false 49 | }, 50 | }); 51 | -------------------------------------------------------------------------------- /electron/webpack/config.production.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Build config for electron 'Renderer Process' file 3 | */ 4 | 5 | const path = require('path'); 6 | const webpack = require('webpack'); 7 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 8 | const merge = require('webpack-merge'); 9 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 10 | const baseConfig = require('./config.base'); 11 | 12 | module.exports = merge(baseConfig, { 13 | devtool: 'cheap-module-source-map', 14 | 15 | entry: [ 16 | path.join(__dirname, '../src/index') 17 | ], 18 | 19 | output: { 20 | path: path.join(__dirname, '../app/dist'), 21 | publicPath: '../dist/' 22 | }, 23 | 24 | module: { 25 | loaders: [ 26 | // Extract all .global.css to style.css as is 27 | { 28 | test: /\.(scss|sass)$/, 29 | use: ExtractTextPlugin.extract({ 30 | use: [{ 31 | loader: 'css-loader', 32 | options: { 33 | //modules: true, 34 | importLoaders: 1, 35 | localIdentName: '[name]__[local]__[hash:base64:5]', 36 | } 37 | }, 38 | { 39 | loader: 'sass-loader' 40 | }] 41 | }) 42 | }, 43 | 44 | // WOFF Font 45 | { 46 | test: /\.woff(\?v=\d+\.\d+\.\d+)?$/, 47 | use: { 48 | loader: 'url-loader', 49 | options: { 50 | limit: 10000, 51 | mimetype: 'application/font-woff', 52 | } 53 | }, 54 | }, 55 | // WOFF2 Font 56 | { 57 | test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/, 58 | use: { 59 | loader: 'url-loader', 60 | options: { 61 | limit: 10000, 62 | mimetype: 'application/font-woff', 63 | } 64 | } 65 | }, 66 | // TTF Font 67 | { 68 | test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, 69 | use: { 70 | loader: 'url-loader', 71 | options: { 72 | limit: 10000, 73 | mimetype: 'application/octet-stream' 74 | } 75 | } 76 | }, 77 | // EOT Font 78 | { 79 | test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, 80 | use: 'file-loader', 81 | }, 82 | // SVG Font 83 | { 84 | test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, 85 | use: { 86 | loader: 'url-loader', 87 | options: { 88 | limit: 10000, 89 | mimetype: 'image/svg+xml', 90 | } 91 | } 92 | }, 93 | // Common Image Formats 94 | { 95 | test: /\.(?:ico|gif|png|jpg|jpeg|webp)$/, 96 | use: 'url-loader', 97 | } 98 | ] 99 | }, 100 | 101 | plugins: [ 102 | // https://webpack.github.io/docs/list-of-plugins.html#occurrenceorderplugin 103 | // https://github.com/webpack/webpack/issues/864 104 | new webpack.optimize.OccurrenceOrderPlugin(), 105 | 106 | // NODE_ENV should be production so that modules do not perform certain development checks 107 | new webpack.DefinePlugin({ 108 | 'process.env.NODE_ENV': JSON.stringify('production') 109 | }), 110 | 111 | new ExtractTextPlugin('style.css'), 112 | 113 | new HtmlWebpackPlugin({ 114 | filename: path.resolve(__dirname, '../app/app.html'), 115 | template: path.resolve(__dirname, '../src/app.html'), 116 | inject: false 117 | }) 118 | ], 119 | 120 | // https://github.com/chentsulin/webpack-target-electron-renderer#how-this-module-works 121 | target: 'electron-renderer' 122 | }); 123 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Entry Script 3 | */ 4 | require('dotenv').config() 5 | 6 | process.env.ROOT_PATH = __dirname 7 | 8 | 9 | if (process.env.NODE_ENV == 'production' || process.env.NODE_ENV == 'test') { 10 | if (!process.env.SERVER_SECRET || process.env.SERVER_SECRET === 'secret_keyboard_cat') { 11 | console.error('you need to set the SERVER_SECRET environment variable to something secretive') 12 | return process.exit(1) 13 | } 14 | 15 | require('./dist') 16 | return 17 | } 18 | 19 | require('./src') 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "iguana-trading-bot", 3 | "version": "2.0.0", 4 | "description": "Trading bot for advanced orders & strategies", 5 | "main": "index.js", 6 | "dependencies": { 7 | "@slack/client": "^4.11.0", 8 | "binance-api-node": "^0.8.18", 9 | "bitfinex-api-node": "^2.0.5", 10 | "body-parser": "^1.18.3", 11 | "ccxt": "^1.18.456", 12 | "cors": "^2.8.5", 13 | "cross-env": "^5.0.1", 14 | "debug": "^4.1.1", 15 | "dotenv": "^7.0.0", 16 | "electron-debug": "^1.1.0", 17 | "events": "^3.0.0", 18 | "express": "^4.16.4", 19 | "font-awesome": "^4.7.0", 20 | "global": "^4.4.0", 21 | "hat": "^0.0.3", 22 | "history": "^4.6.1", 23 | "i": "^0.3.6", 24 | "jsonfile": "^5.0.0", 25 | "jsonwebtoken": "^8.5.0", 26 | "morgan": "^1.9.1", 27 | "mysql2": "^1.6.5", 28 | "node-binance-api": "^0.9.0", 29 | "node-json-db": "^0.10.0", 30 | "node-telegram-bot-api": "^0.30.0", 31 | "pg-hstore": "^2.3.2", 32 | "react": "^16.0.0", 33 | "react-dom": "^16.0.0", 34 | "react-redux": "^5.0.1", 35 | "react-router": "^4.1.1", 36 | "react-router-dom": "^4.1.1", 37 | "redux": "^3.6.0", 38 | "redux-thunk": "^2.1.0", 39 | "reflect-metadata": "^0.1.13", 40 | "sequelize": "^5.1.0", 41 | "sequelize-cli": "^5.5.0", 42 | "sequelize-typescript": "^1.0.0-alpha.9", 43 | "source-map-support": "^0.5.0", 44 | "sqlite3": "^4.0.9", 45 | "underscore": "^1.9.1", 46 | "utf-8-validate": "^5.0.2", 47 | "web3": "^1.0.0-beta.52", 48 | "ws": "^7.1.1" 49 | }, 50 | "devDependencies": { 51 | "@types/chai": "^4.1.7", 52 | "@types/enzyme": "^3.1.1", 53 | "@types/express": "^4.11.1", 54 | "@types/hat": "0.0.0", 55 | "@types/history": "^4.5.2", 56 | "@types/mocha": "^5.2.7", 57 | "@types/node": "^11.9.5", 58 | "@types/node-json-db": "^0.0.1", 59 | "@types/node-telegram-bot-api": "^0.30.4", 60 | "@types/react": "^16.0.5", 61 | "@types/react-dom": "16.0.3", 62 | "@types/react-hot-loader": "^3.0.4", 63 | "@types/react-redux": "^5.0.4", 64 | "@types/react-router": "^4.0.11", 65 | "@types/react-router-dom": "^4.0.7", 66 | "@types/react-router-redux": "^5.0.2", 67 | "@types/redux-logger": "^3.0.0", 68 | "@types/sequelize": "^4.27.41", 69 | "@types/sinon": "^4.0.0", 70 | "@types/underscore": "^1.8.13", 71 | "asar": "^0.14.0", 72 | "awesome-typescript-loader": "^5.2.1", 73 | "babel-loader": "7", 74 | "babel-preset-es2015": "^6.24.1", 75 | "boiler-room-custodian": "^0.6.2", 76 | "chai": "^4.2.0", 77 | "concurrently": "^3.1.0", 78 | "css-loader": "^0.28.4", 79 | "css-modules-require-hook": "^4.0.6", 80 | "devtron": "^1.4.0", 81 | "electron": "4.0.8", 82 | "electron-builder": "^19.8.0", 83 | "electron-builder-http": "^19.15.0", 84 | "electron-devtools-installer": "^2.0.1", 85 | "enzyme": "^3.0.0", 86 | "enzyme-adapter-react-16": "^1.0.0", 87 | "express": "^4.16.4", 88 | "extract-text-webpack-plugin": "^3.0.0", 89 | "file-loader": "^1.1.5", 90 | "html-webpack-plugin": "^2.24.1", 91 | "identity-obj-proxy": "^3.0.0", 92 | "jest": "^22.0.4", 93 | "json-loader": "^0.5.4", 94 | "mocha": "^6.1.4", 95 | "node-sass": "^4.1.1", 96 | "react-hot-loader": "^3.0.0-beta.6", 97 | "react-test-renderer": "^16.0.0", 98 | "redux-logger": "^3.0.6", 99 | "sass-loader": "^6.0.6", 100 | "sinon": "^4.0.0", 101 | "source-map-loader": "^0.2.4", 102 | "spectron": "^3.4.1", 103 | "style-loader": "^0.19.0", 104 | "ts-loader": "^3.1.0", 105 | "ts-node": "^8.0.3", 106 | "tsconfig-paths": "^3.8.0", 107 | "tslint": "^5.18.0", 108 | "tslint-loader": "^3.5.4", 109 | "typescript": "^3.4.3", 110 | "url-loader": "^0.6.1", 111 | "webpack": "^4.39.1", 112 | "webpack-cli": "^3.3.0" 113 | }, 114 | "scripts": { 115 | "db:up": "sequelize db:migrate --config src/database/config.js --migrations-path src/database/migrations", 116 | "db:down": "sequelize db:migrate:undo --config src/database/config.js --migrations-path src/database/migrations", 117 | "db:down:all": "sequelize db:migrate:undo:all --config src/database/config.js --migrations-path src/database/migrations", 118 | "deploy": "npm run build; npm run build-docker; docker push cryptocontrol/iguana", 119 | "test": "./node_modules/mocha/bin/mocha -r ts-node/register test/test.ts", 120 | "build": "rimraf dist; tsc", 121 | "build-docker": "docker build -t cryptocontrol/iguana .", 122 | "start": "cross-env NODE_ENV=production npm run db:up; npm run start-prod", 123 | "start-prod": "NODE_ENV=production node index.js", 124 | "start-dev": "ts-node -r tsconfig-paths/register index.js", 125 | "start-app": "cross-env NODE_ENV=production electron ./electron/app", 126 | "start-app-hot": "cross-env HOT=1 NODE_ENV=development electron ./electron/app/main.development", 127 | "start-app-server": "cross-env NODE_ENV=development node --max_old_space_size=2096 ./electron/server.js", 128 | "hot-server": "cross-env NODE_ENV=development node --max_old_space_size=2096 server.js", 129 | "build-app-main": "cross-env NODE_ENV=production node ./node_modules/webpack/bin/webpack --config electron/webpack/config.electron.js --progress --profile --colors", 130 | "build-app-renderer": "cross-env NODE_ENV=production node ./node_modules/webpack/bin/webpack --config electron/webpack/config.production.js --progress --profile --colors", 131 | "build-app": "npm run build-app-main && npm run build-app-renderer" 132 | }, 133 | "repository": { 134 | "type": "git", 135 | "url": "git+ssh://git@github.com/cryptocontrol/algo-trading-server.git" 136 | }, 137 | "author": "", 138 | "license": "ISC", 139 | "bugs": { 140 | "url": "https://github.com/cryptocontrol/algo-trading-server/issues" 141 | }, 142 | "keywords": [ 143 | "cryptocontrol", 144 | "trading", 145 | "cryptocurrency", 146 | "algo-trading" 147 | ], 148 | "homepage": "https://github.com/cryptocontrol/algo-trading-server#README" 149 | } 150 | -------------------------------------------------------------------------------- /postman/Autotrader.postman_environment.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "a40b3341-5b53-4877-9870-102b12a17ddb", 3 | "name": "Autotrader", 4 | "values": [ 5 | { 6 | "key": "jwt", 7 | "value": "", 8 | "description": "", 9 | "enabled": true 10 | }, 11 | { 12 | "key": "url", 13 | "value": "", 14 | "description": "", 15 | "enabled": true 16 | } 17 | ], 18 | "_postman_variable_scope": "environment", 19 | "_postman_exported_at": "2019-03-25T11:55:18.788Z", 20 | "_postman_exported_using": "Postman/6.7.4" 21 | } -------------------------------------------------------------------------------- /screenshots/gui3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cryptocontrol/algo-trading-server/72cbbe3c9a9c2a147fbe2e046a1f7fec319bff86/screenshots/gui3.png -------------------------------------------------------------------------------- /screenshots/setup2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cryptocontrol/algo-trading-server/72cbbe3c9a9c2a147fbe2e046a1f7fec319bff86/screenshots/setup2.png -------------------------------------------------------------------------------- /screenshots/stoploss3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cryptocontrol/algo-trading-server/72cbbe3c9a9c2a147fbe2e046a1f7fec319bff86/screenshots/stoploss3.png -------------------------------------------------------------------------------- /scripts/docker-publish.sh: -------------------------------------------------------------------------------- 1 | docker build -t cryptocontrol/iguana ../ -f ../Dockerfile 2 | docker push cryptocontrol/iguana:latest 3 | -------------------------------------------------------------------------------- /src/budfox/candleCreator.ts: -------------------------------------------------------------------------------- 1 | import * as moment from 'moment' 2 | import * as _ from 'underscore' 3 | import { EventEmitter } from 'events' 4 | import { Trade } from 'ccxt' 5 | 6 | import { ICandle } from 'src/interfaces' 7 | 8 | 9 | /** 10 | * The CandleCreator creates one minute candles based on trade batches. Note that it 11 | * also adds empty candles to fill gaps with no volume. 12 | * 13 | * Emits `candles` event with a list of new candles 14 | * Emits `candle` event with the last candle 15 | */ 16 | export default class CandleCreator extends EventEmitter { 17 | private lastTrade: Trade 18 | private threshold: number = 0 19 | 20 | // This also holds the leftover between fetches 21 | private buckets: { [minute: string]: Trade[] } = {} 22 | 23 | 24 | write = (batch: Trade[]) => { 25 | const trades = this.filter(batch) 26 | 27 | if(_.isEmpty(trades)) return 28 | this.fillBuckets(trades) 29 | 30 | let candles = this.calculateCandles() 31 | candles = this.addEmptyCandles(candles) 32 | 33 | if (_.isEmpty(candles)) return 34 | 35 | // the last candle is not complete 36 | this.threshold = candles.pop().start.unix() 37 | 38 | this.emit('candles', candles) 39 | this.emit('candle', _.last(candles)) 40 | } 41 | 42 | 43 | /** 44 | * make sure we only include trades more recent than the previous emitted candle 45 | * @param trades 46 | */ 47 | private filter = (trades: Trade[]) => _.filter(trades, t => t.timestamp > this.threshold) 48 | 49 | 50 | /** 51 | * put each trade in a per minute bucket 52 | * @param trades 53 | */ 54 | private fillBuckets (trades: Trade[]) { 55 | _.each(trades, trade => { 56 | const minute = moment(trade.timestamp).format('YYYY-MM-DD HH:mm') 57 | 58 | if(!(minute in this.buckets)) this.buckets[minute] = [] 59 | this.buckets[minute].push(trade) 60 | }) 61 | 62 | this.lastTrade = _.last(trades) 63 | } 64 | 65 | 66 | /** 67 | * convert each bucket into a candle 68 | */ 69 | private calculateCandles () { 70 | let lastMinute, candles: ICandle[] = [] 71 | 72 | // catch error from high volume getTrades 73 | if (this.lastTrade !== undefined) 74 | // create a string referencing the minute this trade happened in 75 | lastMinute = moment(this.lastTrade.timestamp).format('YYYY-MM-DD HH:mm') 76 | 77 | _.mapObject(this.buckets, (bucket, name) => { 78 | const candle = this.calculateCandle(bucket) 79 | 80 | // clean all buckets, except the last one: 81 | // this candle is not complete 82 | if (name !== lastMinute) delete this.buckets[name] 83 | 84 | candles.push(candle) 85 | }) 86 | 87 | return candles 88 | } 89 | 90 | 91 | private calculateCandle (trades: Trade[]) { 92 | const first = _.first(trades) 93 | 94 | const candle: ICandle = { 95 | start: moment(first.timestamp).clone().startOf('minute'), 96 | open: first.price, 97 | high: first.price, 98 | low: first.price, 99 | close: _.last(trades).price, 100 | vwp: 0, 101 | volume: 0, 102 | trades: _.size(trades) 103 | } 104 | 105 | _.each(trades, function(trade) { 106 | candle.high = _.max([candle.high, trade.price]) 107 | candle.low = _.min([candle.low, trade.price]) 108 | candle.volume += trade.amount 109 | candle.vwp += trade.price * trade.amount 110 | }) 111 | 112 | candle.vwp /= candle.volume 113 | 114 | return candle 115 | } 116 | 117 | 118 | /** 119 | * Iguana expects a candle every minute, if nothing happened during a particilar 120 | * minute we will add empty candles with: 121 | * 122 | * - open, high, close, low, vwp are the same as the close of the previous candle. 123 | * - trades, volume are 0 124 | * 125 | * @param candles 126 | */ 127 | private addEmptyCandles (candles: ICandle[]) { 128 | const amount = _.size(candles) 129 | if (!amount) return candles 130 | 131 | // iterator 132 | const start = _.first(candles).start.clone() 133 | const end = _.last(candles).start 134 | let i, j = -1 135 | 136 | const minutes = _.map(candles, candle => +candle.start) 137 | 138 | while (start < end) { 139 | start.add(1, 'm') 140 | i = +start 141 | j++ 142 | 143 | // if we have a candle for this minute 144 | if (_.contains(minutes, i)) continue 145 | 146 | const lastPrice = candles[j].close 147 | 148 | candles.splice(j + 1, 0, { 149 | start: start.clone(), 150 | open: lastPrice, 151 | high: lastPrice, 152 | low: lastPrice, 153 | close: lastPrice, 154 | vwp: lastPrice, 155 | volume: 0, 156 | trades: 0 157 | }) 158 | } 159 | 160 | return candles 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /src/budfox/heart.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'underscore' 2 | import { EventEmitter } from 'events' 3 | 4 | import { die } from '../utils' 5 | import log from '../utils/log' 6 | 7 | 8 | /** 9 | * The heart schedules and emit ticks every 20 seconds. 10 | */ 11 | export default class Heart extends EventEmitter { 12 | private lastTick = 0 13 | private tickrate: number 14 | private interval: any 15 | 16 | 17 | constructor (tickrate: number = 20) { 18 | super() 19 | this.tickrate = tickrate 20 | } 21 | 22 | 23 | public pump () { 24 | log.debug('scheduling ticks') 25 | this.scheduleTicks() 26 | } 27 | 28 | 29 | public attack () { 30 | log.debug('stopping ticks') 31 | clearInterval(this.interval) 32 | } 33 | 34 | 35 | private tick = () => { 36 | if (this.lastTick) { 37 | // make sure the last tick happened not too long ago 38 | // @link https://github.com/askmike/gekko/issues/514 39 | if (this.lastTick < Date.now() - this.tickrate * 3000) 40 | die('Failed to tick in time, see https://github.com/askmike/gekko/issues/514 for details', true) 41 | } 42 | 43 | this.lastTick = Date.now() 44 | this.emit('tick') 45 | } 46 | 47 | 48 | private scheduleTicks () { 49 | this.interval = setInterval(this.tick, this.tickrate * 1000) 50 | _.defer(this.tick) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/budfox/index.ts: -------------------------------------------------------------------------------- 1 | import { Trade } from 'ccxt' 2 | import * as _ from 'underscore' 3 | 4 | import { EventEmitter } from 'events' 5 | import { ICandle } from '../interfaces' 6 | import BaseExchange from '../exchanges/core/BaseExchange' 7 | import CandleCreator from './candleCreator' 8 | import Candles from '../database/models/candles' 9 | import log from '../utils/log' 10 | import MarketDataProvider from './marketDataProvider' 11 | import Trades from '../database/models/trades' 12 | 13 | 14 | /** 15 | * Budfox is the realtime market for Iguana! It was initially built by the team 16 | * that built Gekko but was modified to support CCXT exchanges and websocket connections. 17 | * 18 | * Budfox takes an exchange and a symbol, and tracks all new trades and emits out candles. 19 | * 20 | * Read more here what Budfox does (Gekko's version): 21 | * @link https://github.com/askmike/gekko/blob/stable/docs/internals/budfox.md 22 | */ 23 | export default class BudFox extends EventEmitter { 24 | private readonly marketDataProvider: MarketDataProvider 25 | private readonly candlesCreator: CandleCreator 26 | public readonly exchange: BaseExchange 27 | public readonly symbol: string 28 | 29 | 30 | constructor (exchange: BaseExchange, symbol: string) { 31 | super() 32 | log.debug('init budfox for', exchange.id, symbol) 33 | this.exchange = exchange 34 | this.symbol = symbol 35 | 36 | // init the different components 37 | this.marketDataProvider = new MarketDataProvider(exchange, symbol) 38 | this.candlesCreator = new CandleCreator 39 | 40 | // connect them together 41 | 42 | // on new trade data create candles and stream it 43 | this.marketDataProvider.on('trades', this.candlesCreator.write) 44 | this.candlesCreator.on('candles', this.processCandles) 45 | 46 | // relay a market-start, market-update and trade events 47 | this.marketDataProvider.on('market-start', e => this.emit('market-start', e)) 48 | this.marketDataProvider.on('market-update', e => this.emit('market-update', e)) 49 | this.marketDataProvider.on('trades', this.processTrades) 50 | 51 | // once everything is connected, we start the market data provider 52 | this.marketDataProvider.start() 53 | } 54 | 55 | 56 | private processCandles = (candles: ICandle[]) => { 57 | candles.forEach(c => { 58 | // write to stream 59 | this.emit('candle', c) 60 | 61 | // save into the DB 62 | const candle = new Candles({ 63 | open: c.open, 64 | high: c.high, 65 | low: c.low, 66 | volume: c.volume, 67 | close: c.close, 68 | vwp: c.vwp, 69 | start: c.start, 70 | trades: c.trades, 71 | exchange: this.exchange.id, 72 | symbol: this.symbol 73 | }) 74 | 75 | candle.save().catch(_.noop) 76 | }) 77 | } 78 | 79 | 80 | private processTrades = (trades: Trade[]) => { 81 | trades.forEach(t => { 82 | this.emit('trade', t) 83 | 84 | // disable saving trades 85 | 86 | // const trade = new Trades({ 87 | // exchange: this.exchange.id, 88 | // price: t.price, 89 | // symbol: this.symbol, 90 | // tradedAt: new Date(t.timestamp), 91 | // side: t.side, 92 | // tradeId: String(t.id), 93 | // volume: t.amount 94 | // }) 95 | 96 | // trade.save().catch(_.noop) 97 | }) 98 | } 99 | 100 | 101 | public _read () { 102 | // do nothing 103 | } 104 | 105 | 106 | /** 107 | * Stop budfox 108 | */ 109 | public murder () { 110 | log.debug('murdered budfox for', this.exchange.id, this.symbol) 111 | this.marketDataProvider.stop() 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/budfox/marketDataProvider.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'underscore' 2 | import { EventEmitter } from 'events' 3 | import { Trade } from 'ccxt' 4 | 5 | import TradeBatcher, { ITradesBatchEvent } from './tradeBatcher' 6 | import BaseExchange from '../exchanges/core/BaseExchange' 7 | import Heart from './heart' 8 | import log from '../utils/log' 9 | 10 | 11 | /** 12 | * The market data provider will fetch data from a datasource on tick. It emits: 13 | * 14 | * - `trades`: batch of newly detected trades 15 | * - `trade`: the last new trade 16 | * - `market-update`: after Igunana fetched new trades, this will be the most recent one. 17 | * - `market-start`: contains the time timestamp of the first trade (a market start event)... 18 | */ 19 | export default class MarketDataProvider extends EventEmitter { 20 | private exchange: BaseExchange 21 | private heart: Heart 22 | private marketStarted: boolean = false 23 | private symbol: string 24 | private firstFetch: boolean = true 25 | private batcher: TradeBatcher 26 | 27 | 28 | constructor (exchange: BaseExchange, symbol: string) { 29 | super() 30 | 31 | this.exchange = exchange 32 | this.symbol = symbol 33 | 34 | this.heart = new Heart 35 | this.batcher = new TradeBatcher 36 | 37 | // relay newly fetched trades 38 | this.batcher.on('new batch', this.relayTrades) 39 | } 40 | 41 | 42 | public start () { 43 | // first fetch the first set of trades 44 | // this.fetch() // don't do this; 45 | 46 | // then we start streaming trades in real-time 47 | this.exchange.on('trade', (trade: Trade) => { 48 | if (trade.symbol === this.symbol) this.processTrades([trade]) 49 | }) 50 | 51 | log.debug('Streaming', this.symbol, 'trades from', this.exchange.id, '...') 52 | this.exchange.streamTrades(this.symbol) 53 | } 54 | 55 | 56 | public stop () { 57 | this.exchange.stopStreamingTrades(this.symbol) 58 | } 59 | 60 | 61 | private relayTrades = (e: ITradesBatchEvent) => { 62 | if (!e.trades) return 63 | 64 | if (this.marketStarted) { 65 | this.marketStarted = true 66 | this.emit('market-start', e.first.timestamp) 67 | } 68 | 69 | this.emit('market-update', e.last.timestamp) 70 | this.emit('trades', e.trades) 71 | this.emit('trade', e.last) 72 | } 73 | 74 | 75 | private processTrades (trades: Trade[]) { 76 | if (_.isEmpty(trades)) { 77 | log.debug('trade fetch came back empty, refetching...') 78 | // setTimeout(this._fetch, +moment.duration(1, 's')) 79 | return 80 | } 81 | 82 | console.log(trades) 83 | this.batcher.write(trades) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/budfox/tradeBatcher.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'underscore' 2 | import * as moment from 'moment' 3 | import { EventEmitter } from 'events' 4 | import { Trade } from 'ccxt' 5 | 6 | import log from '../utils/log' 7 | 8 | 9 | export interface ITradesBatchEvent { 10 | count: number 11 | start: number 12 | end: number 13 | last: Trade 14 | first: Trade 15 | trades: Trade[] 16 | } 17 | 18 | 19 | /** 20 | * Small wrapper that only propagates new trades. 21 | * 22 | * - Emits `new batch` - all new trades. 23 | */ 24 | export default class TradeBatcher extends EventEmitter { 25 | private lastTrade: Trade 26 | 27 | 28 | write (trades: Trade[] = []) { 29 | if (trades.length === 0) return log.debug('Trade fetch came back empty.') 30 | 31 | // filter and count trades 32 | const filteredBatch = this.filter(trades) 33 | const count = filteredBatch.length 34 | if (count === 0) return // log.debug('No new trades.') 35 | 36 | // pick first & last trades 37 | const last = _.last(filteredBatch) 38 | const first = _.first(filteredBatch) 39 | 40 | const firstDate = moment(first.timestamp), lastDate = moment(last.timestamp) 41 | log.debug(`Processing ${count} new trades from ${first.timestamp} UTC to ${last.timestamp} UTC ` + 42 | `(${firstDate.from(lastDate, true)})`) 43 | 44 | // log.debug( 45 | // 'Processing', amount, 'new trades.', 46 | // 'From', 47 | // first.date.format('YYYY-MM-DD HH:mm:ss'), 48 | // 'UTC to', 49 | // last.date.format('YYYY-MM-DD HH:mm:ss'), 50 | // 'UTC.', 51 | // '(' + first.date.from(last.date, true) + ')' 52 | // ) 53 | 54 | this.emit('new batch', { 55 | count, 56 | start: first.timestamp, 57 | end: last.timestamp, 58 | last, 59 | first, 60 | trades: filteredBatch 61 | }) 62 | 63 | this.lastTrade = last 64 | } 65 | 66 | 67 | private filter (batch: Trade[]) { 68 | // remove trades that have zero amount 69 | // see @link 70 | // https://github.com/askmike/gekko/issues/486 71 | batch = _.filter(batch, trade => trade.amount > 0) 72 | 73 | // weed out known trades 74 | // TODO: optimize by stopping as soon as the 75 | // first trade is too old (reverse first) 76 | if (this.lastTrade) return _.filter(batch, trade => this.lastTrade.timestamp < trade.timestamp) 77 | return batch 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/database/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | development: { 3 | username: 'root', 4 | password: 'root', 5 | database: 'cctrader_dev', 6 | host: '127.0.0.1', 7 | dialect: 'mysql', 8 | logging: false 9 | }, 10 | test: { 11 | username: 'root', 12 | password: null, 13 | database: 'cctrader_test', 14 | host: '127.0.0.1', 15 | dialect: 'mysql' 16 | }, 17 | production: { 18 | dialect: 'sqlite', 19 | storage: 'database.sqlite' 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/database/index.ts: -------------------------------------------------------------------------------- 1 | import { Sequelize } from 'sequelize-typescript' 2 | 3 | import Advices from './models/advices' 4 | import Candles from './models/candles' 5 | import log from '../utils/log' 6 | import Plugins from './models/plugins' 7 | import Trades from './models/trades' 8 | import Triggers from './models/triggers' 9 | import UserExchanges from './models/userexchanges' 10 | 11 | 12 | const env = process.env.NODE_ENV || 'development' 13 | const config = require('./config.js')[env] 14 | 15 | 16 | export const init = () => { 17 | log.info('init database') 18 | 19 | const sequelize = new Sequelize(config) 20 | sequelize.addModels([UserExchanges, Candles, Trades, Triggers, Plugins, Advices]) 21 | 22 | return sequelize 23 | } 24 | -------------------------------------------------------------------------------- /src/database/migrations/20190314194535-create-candles.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | module.exports = { 3 | up: async (queryInterface, Sequelize) => { 4 | await queryInterface.createTable('Candles', { 5 | id: { 6 | allowNull: false, 7 | autoIncrement: true, 8 | primaryKey: true, 9 | type: Sequelize.INTEGER 10 | }, 11 | start: { 12 | allowNull: false, 13 | type: Sequelize.DATE 14 | }, 15 | open: { 16 | allowNull: false, 17 | type: Sequelize.DOUBLE 18 | }, 19 | high: { 20 | allowNull: false, 21 | type: Sequelize.DOUBLE 22 | }, 23 | low: { 24 | allowNull: false, 25 | type: Sequelize.DOUBLE 26 | }, 27 | close: { 28 | allowNull: false, 29 | type: Sequelize.DOUBLE 30 | }, 31 | vwp: { 32 | allowNull: false, 33 | type: Sequelize.DOUBLE 34 | }, 35 | volume: { 36 | allowNull: false, 37 | type: Sequelize.DOUBLE 38 | }, 39 | trades: { 40 | allowNull: false, 41 | type: Sequelize.INTEGER 42 | }, 43 | exchange: { 44 | allowNull: false, 45 | type: Sequelize.STRING 46 | }, 47 | symbol: { 48 | allowNull: false, 49 | type: Sequelize.STRING 50 | }, 51 | createdAt: { 52 | allowNull: false, 53 | type: Sequelize.DATE 54 | }, 55 | updatedAt: { 56 | allowNull: false, 57 | type: Sequelize.DATE 58 | } 59 | }) 60 | 61 | await queryInterface.addIndex('Candles', ['symbol', 'exchange', 'start'], { 62 | type: 'unique', 63 | name: 'symbol_exchange_start' 64 | }) 65 | }, 66 | 67 | down: (queryInterface, Sequelize) => { 68 | return queryInterface.dropTable('Candles') 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/database/migrations/20190314194907-create-trades.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | module.exports = { 3 | up: async (queryInterface, Sequelize) => { 4 | await queryInterface.createTable('Trades', { 5 | id: { 6 | allowNull: false, 7 | autoIncrement: true, 8 | primaryKey: true, 9 | type: Sequelize.INTEGER 10 | }, 11 | price: { 12 | allowNull: false, 13 | type: Sequelize.DOUBLE 14 | }, 15 | volume: { 16 | allowNull: false, 17 | type: Sequelize.DOUBLE 18 | }, 19 | exchange: { 20 | allowNull: false, 21 | type: Sequelize.STRING 22 | }, 23 | symbol: { 24 | allowNull: false, 25 | type: Sequelize.STRING 26 | }, 27 | tradeId: { 28 | allowNull: false, 29 | type: Sequelize.STRING 30 | }, 31 | side: { 32 | allowNull: false, 33 | type: Sequelize.STRING 34 | }, 35 | tradedAt: { 36 | allowNull: false, 37 | type: Sequelize.DATE 38 | }, 39 | createdAt: { 40 | allowNull: false, 41 | type: Sequelize.DATE 42 | }, 43 | updatedAt: { 44 | allowNull: false, 45 | type: Sequelize.DATE 46 | } 47 | }) 48 | 49 | 50 | await queryInterface.addIndex('Trades', ['symbol', 'exchange', 'tradeId'], { 51 | type: 'unique', 52 | name: 'symbol_exchange_tid' 53 | }) 54 | }, 55 | down: (queryInterface, Sequelize) => { 56 | return queryInterface.dropTable('Trades') 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/database/migrations/20190314195103-create-user-exchanges.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | module.exports = { 3 | up: async (queryInterface, Sequelize) => { 4 | await queryInterface.createTable('UserExchanges', { 5 | id: { 6 | allowNull: false, 7 | autoIncrement: true, 8 | primaryKey: true, 9 | type: Sequelize.INTEGER 10 | }, 11 | uid: { 12 | allowNull: false, 13 | type: Sequelize.STRING 14 | }, 15 | exchange: { 16 | allowNull: false, 17 | type: Sequelize.STRING 18 | }, 19 | apiKey: { 20 | allowNull: false, 21 | type: Sequelize.STRING 22 | }, 23 | apiSecret: { 24 | allowNull: false, 25 | type: Sequelize.STRING 26 | }, 27 | apiPassword: { 28 | type: Sequelize.STRING 29 | }, 30 | createdAt: { 31 | allowNull: false, 32 | type: Sequelize.DATE 33 | }, 34 | updatedAt: { 35 | allowNull: false, 36 | type: Sequelize.DATE 37 | } 38 | }) 39 | 40 | await queryInterface.addIndex('UserExchanges', ['uid', 'exchange'], { 41 | type: 'unique', 42 | name: 'uid_exchange' 43 | }) 44 | }, 45 | down: (queryInterface, Sequelize) => { 46 | return queryInterface.dropTable('UserExchanges') 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/database/migrations/20190316154850-create-triggers.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | module.exports = { 3 | up: (queryInterface, Sequelize) => { 4 | return queryInterface.createTable('Triggers', { 5 | id: { 6 | allowNull: false, 7 | autoIncrement: true, 8 | primaryKey: true, 9 | type: Sequelize.INTEGER 10 | }, 11 | uid: { 12 | allowNull: false, 13 | type: Sequelize.STRING 14 | }, 15 | exchange: { 16 | allowNull: false, 17 | type: Sequelize.STRING 18 | }, 19 | symbol: { 20 | allowNull: false, 21 | type: Sequelize.STRING 22 | }, 23 | kind: { 24 | allowNull: false, 25 | type: Sequelize.STRING 26 | }, 27 | isActive: { 28 | allowNull: false, 29 | type: Sequelize.BOOLEAN, 30 | defaultValue: true 31 | }, 32 | hasTriggered: { 33 | allowNull: false, 34 | type: Sequelize.BOOLEAN, 35 | defaultValue: false 36 | }, 37 | closedAt: { type: Sequelize.DATE }, 38 | lastTriggeredAt: { type: Sequelize.DATE }, 39 | params: { type: Sequelize.STRING, }, 40 | createdAt: { 41 | allowNull: false, 42 | type: Sequelize.DATE 43 | }, 44 | updatedAt: { 45 | allowNull: false, 46 | type: Sequelize.DATE 47 | } 48 | }) 49 | }, 50 | down: (queryInterface, Sequelize) => { 51 | return queryInterface.dropTable('Triggers') 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/database/migrations/20190320183253-create-plugins.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | module.exports = { 3 | up: async (queryInterface, Sequelize) => { 4 | await queryInterface.createTable('Plugins', { 5 | id: { 6 | allowNull: false, 7 | autoIncrement: true, 8 | primaryKey: true, 9 | type: Sequelize.INTEGER 10 | }, 11 | uid: { 12 | allowNull: false, 13 | type: Sequelize.STRING 14 | }, 15 | config: { 16 | allowNull: false, 17 | type: Sequelize.STRING 18 | }, 19 | kind: { 20 | allowNull: false, 21 | type: Sequelize.STRING 22 | }, 23 | isActive: { 24 | allowNull: false, 25 | type: Sequelize.BOOLEAN, 26 | defaultValue: true 27 | }, 28 | createdAt: { 29 | allowNull: false, 30 | type: Sequelize.DATE 31 | }, 32 | updatedAt: { 33 | allowNull: false, 34 | type: Sequelize.DATE 35 | } 36 | }) 37 | 38 | await queryInterface.addIndex('Plugins', ['uid', 'kind'], { 39 | type: 'unique', 40 | name: 'uid_kind' 41 | }) 42 | }, 43 | down: (queryInterface, Sequelize) => { 44 | return queryInterface.dropTable('Plugins') 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/database/migrations/20190334194900-create-advices.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | module.exports = { 3 | up: (queryInterface, Sequelize) => { 4 | return queryInterface.createTable('Advices', { 5 | id: { 6 | allowNull: false, 7 | autoIncrement: true, 8 | primaryKey: true, 9 | type: Sequelize.INTEGER 10 | }, 11 | uid: { 12 | allowNull: false, 13 | type: Sequelize.STRING 14 | }, 15 | exchange: { 16 | allowNull: false, 17 | type: Sequelize.STRING 18 | }, 19 | symbol: { 20 | allowNull: false, 21 | type: Sequelize.STRING 22 | }, 23 | advice: { 24 | allowNull: false, 25 | type: Sequelize.STRING 26 | }, 27 | price: { 28 | allowNull: false, 29 | type: Sequelize.DOUBLE 30 | }, 31 | amount: { 32 | allowNull: false, 33 | type: Sequelize.DOUBLE, 34 | defaultValue: false 35 | }, 36 | mode: { 37 | allowNull: false, 38 | type: Sequelize.STRING 39 | }, 40 | trigger_id: { 41 | type: Sequelize.INTEGER 42 | }, 43 | createdAt: { 44 | allowNull: false, 45 | type: Sequelize.DATE 46 | }, 47 | updatedAt: { 48 | allowNull: false, 49 | type: Sequelize.DATE 50 | }, 51 | order_id: { 52 | allowNull: false, 53 | type: Sequelize.INTEGER, 54 | defaultValue: false 55 | }, 56 | error_msg: { 57 | allowNull: false, 58 | type: Sequelize.STRING, 59 | defaultValue: false 60 | } 61 | }) 62 | }, 63 | down: (queryInterface, Sequelize) => { 64 | return queryInterface.dropTable('Advices') 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/database/models/advices.ts: -------------------------------------------------------------------------------- 1 | import { Table, Column, Model } from 'sequelize-typescript' 2 | import { IAdvice } from 'src/interfaces' 3 | 4 | 5 | @Table({ timestamps: true }) 6 | export default class Advices extends Model { 7 | @Column 8 | symbol: string 9 | 10 | @Column 11 | exchange: string 12 | 13 | @Column 14 | uid: string 15 | 16 | @Column 17 | volume: number 18 | 19 | @Column 20 | price: number 21 | 22 | @Column 23 | advice: IAdvice 24 | 25 | @Column 26 | mode: 'realtime' | 'backtest' | 'paper' 27 | 28 | @Column 29 | trigger_id?: number 30 | 31 | @Column 32 | order_id: number 33 | 34 | @Column 35 | error_msg: string 36 | } 37 | -------------------------------------------------------------------------------- /src/database/models/candles.ts: -------------------------------------------------------------------------------- 1 | import { Table, Column, Model } from 'sequelize-typescript' 2 | 3 | 4 | @Table({ timestamps: true }) 5 | export default class Candles extends Model { 6 | @Column 7 | symbol: string 8 | 9 | @Column 10 | exchange: string 11 | 12 | @Column 13 | open: number 14 | 15 | @Column 16 | high: number 17 | 18 | @Column 19 | low: number 20 | 21 | @Column 22 | volume: number 23 | 24 | @Column 25 | close: number 26 | 27 | @Column 28 | vwp: number 29 | 30 | @Column 31 | start: Date 32 | 33 | @Column 34 | trades: number 35 | } 36 | -------------------------------------------------------------------------------- /src/database/models/plugins.ts: -------------------------------------------------------------------------------- 1 | import { Table, Column, Model } from 'sequelize-typescript' 2 | 3 | 4 | @Table({ timestamps: true }) 5 | export default class Plugins extends Model { 6 | @Column 7 | uid: string 8 | 9 | @Column 10 | kind: string 11 | 12 | @Column 13 | config: string 14 | 15 | @Column 16 | isActive: boolean 17 | 18 | @Column 19 | params: string 20 | } 21 | -------------------------------------------------------------------------------- /src/database/models/trades.ts: -------------------------------------------------------------------------------- 1 | import { Table, Column, Model } from 'sequelize-typescript' 2 | 3 | 4 | @Table({ timestamps: true }) 5 | export default class Trades extends Model { 6 | @Column 7 | symbol: string 8 | 9 | @Column 10 | exchange: string 11 | 12 | @Column 13 | price: number 14 | 15 | @Column 16 | volume: number 17 | 18 | @Column 19 | tradedAt: Date 20 | 21 | @Column 22 | tradeId: string 23 | 24 | @Column 25 | side: string 26 | } 27 | -------------------------------------------------------------------------------- /src/database/models/triggers.ts: -------------------------------------------------------------------------------- 1 | import { Table, Column, Model } from 'sequelize-typescript' 2 | 3 | 4 | @Table({ timestamps: true }) 5 | export default class Triggers extends Model { 6 | @Column 7 | symbol: string 8 | 9 | @Column 10 | exchange: string 11 | 12 | @Column 13 | uid: string 14 | 15 | @Column 16 | kind: string 17 | 18 | @Column 19 | lastTriggeredAt: Date 20 | 21 | @Column 22 | params: string 23 | 24 | @Column 25 | hasTriggered: boolean 26 | 27 | @Column 28 | closedAt: Date 29 | 30 | @Column 31 | isActive: boolean 32 | } 33 | -------------------------------------------------------------------------------- /src/database/models/userexchanges.ts: -------------------------------------------------------------------------------- 1 | import { Table, Column, Model } from 'sequelize-typescript' 2 | 3 | 4 | @Table({ timestamps: true }) 5 | export default class UserExchanges extends Model { 6 | @Column 7 | uid: string 8 | 9 | @Column 10 | exchange: string 11 | 12 | @Column 13 | apiKey: string 14 | 15 | @Column 16 | apiSecret: string 17 | 18 | @Column 19 | apiPassword: string 20 | } 21 | -------------------------------------------------------------------------------- /src/database/seeders/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cryptocontrol/algo-trading-server/72cbbe3c9a9c2a147fbe2e046a1f7fec319bff86/src/database/seeders/.gitkeep -------------------------------------------------------------------------------- /src/errors/BadRequestError.ts: -------------------------------------------------------------------------------- 1 | import HttpError from './HttpError' 2 | 3 | 4 | export default class BadRequestError extends HttpError { 5 | constructor (message?: string) { 6 | super(message || 'Bad Request') 7 | this.status = 400 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/errors/HttpError.ts: -------------------------------------------------------------------------------- 1 | export default class HttpError extends Error { 2 | status: number = 500 3 | } -------------------------------------------------------------------------------- /src/errors/InvalidAPIKeyError.ts: -------------------------------------------------------------------------------- 1 | import HttpError from './HttpError' 2 | 3 | 4 | export default class InvalidAPIKeyError extends HttpError { 5 | constructor(message?: string) { 6 | super(message || 'Invalid API Key') 7 | this.status = 401 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/errors/InvalidJWTError.ts: -------------------------------------------------------------------------------- 1 | import HttpError from './HttpError' 2 | 3 | 4 | export default class InvalidJWTError extends HttpError { 5 | constructor(message?: string) { 6 | super(message || 'Invalid JWT; Please check your server secret') 7 | this.status = 401 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/errors/NotAuthorizedError.ts: -------------------------------------------------------------------------------- 1 | import BadRequestError from './BadRequestError' 2 | 3 | 4 | export default class NotAuthorizedError extends BadRequestError { 5 | constructor(message?: string) { 6 | super('You are not authorised to access this page: ' + message) 7 | this.status = 401 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/errors/NotFoundError.ts: -------------------------------------------------------------------------------- 1 | import HttpError from './HttpError' 2 | 3 | 4 | export default class NotFoundError extends HttpError { 5 | constructor (message?: string) { 6 | super(message || 'Page not found') 7 | this.status = 404 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/errors/RateLimitExceeded.ts: -------------------------------------------------------------------------------- 1 | import HttpError from './HttpError' 2 | 3 | 4 | export default class RateLimitExceededError extends HttpError { 5 | constructor (message?: string) { 6 | super(message || 'Rate Limit Exceeded') 7 | this.status = 403 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/exchanges/BinanceExchange.ts: -------------------------------------------------------------------------------- 1 | import * as ccxt from 'ccxt' 2 | import * as WebSocket from 'ws' 3 | 4 | import CCXTExchange from './core/CCXTExchange' 5 | import { IOrder } from 'src/interfaces' 6 | 7 | 8 | interface ISocketTradeMessage { 9 | e: string 10 | E: number 11 | s: string 12 | t: number 13 | p: string 14 | q: string 15 | b: number 16 | a: number 17 | T: number 18 | m: boolean 19 | M: boolean 20 | } 21 | 22 | 23 | 24 | interface ISocketDiffDepthMessage { 25 | a: [string, string][] 26 | b: [string, string][] 27 | E: number 28 | e: string 29 | s: string 30 | U: number 31 | u: number 32 | } 33 | 34 | 35 | export default class BinanceExchange extends CCXTExchange { 36 | private readonly streamingTrades: { 37 | [symbol: string]: WebSocket 38 | } 39 | private readonly streamingOrderbookSymbol: { 40 | [symbol: string]: WebSocket 41 | } 42 | 43 | 44 | constructor (exchange: ccxt.Exchange) { 45 | super(exchange) 46 | 47 | this.streamingTrades = {} 48 | this.streamingOrderbookSymbol = {} 49 | } 50 | 51 | 52 | public streamTrades (symbol: string): void { 53 | if (!symbol) return 54 | 55 | // check if we are already streaming this symbol or not 56 | if (this.streamingTrades[symbol]) return 57 | 58 | // first download all the recent trades 59 | super.getTrades(symbol) 60 | .then(trades => this.emit(`trade:full:${symbol}`, trades)) 61 | 62 | // then start streaming from websockets 63 | const wsSymbol = symbol.replace('/', '').toLowerCase() 64 | 65 | const url = `wss://stream.binance.com:9443/ws/${wsSymbol}@trade` 66 | const socket = new WebSocket(url) 67 | 68 | socket.on('message', (event: any) => { 69 | try { 70 | const data: ISocketTradeMessage = JSON.parse(event) 71 | 72 | const ccxtTrade: ccxt.Trade = { 73 | amount: Number(data.q), 74 | cost: Number(data.p) * Number(data.q), 75 | datetime: (new Date(data.E)).toISOString(), 76 | fee: undefined, 77 | id: String(data.t), 78 | info: {}, 79 | price: Number(data.p), 80 | side: data.m ? 'sell' : 'buy', 81 | symbol, 82 | takerOrMaker: data.m ? 'maker' : 'taker', 83 | timestamp: Number(data.E) 84 | } 85 | 86 | this.emit('trade', ccxtTrade) 87 | } catch (e) { 88 | // do nothing 89 | } 90 | }) 91 | 92 | socket.on('close', (_event: any) => { delete this.streamingTrades[symbol] }) 93 | this.streamingTrades[symbol] = socket 94 | } 95 | 96 | 97 | public async stopStreamingTrades (symbol: string) { 98 | if (!this.streamingTrades[symbol]) return 99 | this.streamingTrades[symbol].close() 100 | delete this.streamingTrades[symbol] 101 | } 102 | 103 | 104 | public streamOrderbook(symbol: string): void { 105 | if (!symbol) return 106 | 107 | // check if we are already streaming this symbol or not 108 | if (this.streamingOrderbookSymbol[symbol]) return 109 | 110 | // first emit a full orderbook 111 | super.getOrderbook(symbol) 112 | .then(orderbook => this.emit(`orderbook:full:${symbol}`, orderbook)) 113 | 114 | // then start streaming the changes using websockets 115 | const wsSymbol = symbol.replace('/', '').toLowerCase() 116 | const url = `wss://stream.binance.com:9443/ws/${wsSymbol}@depth` 117 | const socket = new WebSocket(url) 118 | 119 | socket.on('message', (event: any) => { 120 | try { 121 | const data: ISocketDiffDepthMessage = JSON.parse(event) 122 | 123 | const bids: IOrder[] = data.b.map(bid => { 124 | return { price: Number(bid[0]), amount: Number(bid[1]) } 125 | }) 126 | 127 | const asks: IOrder[] = data.a.map(ask => { 128 | return { price: Number(ask[0]), amount: Number(ask[1]) } 129 | }) 130 | 131 | this.emit(`orderbook:${symbol}`, { bids, asks }) 132 | } catch (e) { 133 | // do nothing 134 | } 135 | }) 136 | 137 | socket.on('close', (_event: any) => { delete this.streamingOrderbookSymbol[symbol] }) 138 | this.streamingOrderbookSymbol[symbol] = socket 139 | } 140 | 141 | 142 | public async stopStreamingOrderbook (symbol: string) { 143 | if (!this.streamingOrderbookSymbol[symbol]) return 144 | this.streamingOrderbookSymbol[symbol].close() 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/exchanges/BitmexExchange.ts: -------------------------------------------------------------------------------- 1 | import * as ccxt from 'ccxt' 2 | import * as WebSocket from 'ws' 3 | 4 | import { IOrder, IOrderBook } from 'src/interfaces' 5 | import CCXTExchange from './core/CCXTExchange' 6 | 7 | 8 | export default class BitmexExchange extends CCXTExchange { 9 | private readonly clientws: WebSocket 10 | 11 | 12 | constructor (exchange: ccxt.Exchange) { 13 | super(exchange) 14 | 15 | const client = new WebSocket('wss://www.bitmex.com/realtime') 16 | this.clientws = client 17 | } 18 | 19 | 20 | public canStreamTrades (_symbol: string): boolean { 21 | return true 22 | } 23 | 24 | 25 | public streamTrades (symbol: string): void { 26 | // check if we are already streaming this symbol or not 27 | // if (this.streamingTradesSymbol.indexOf(symbol) >= 0) return 28 | // this.streamingTradesSymbol.push(symbol) 29 | 30 | const wsSymbol = symbol.replace('/', '').toUpperCase() 31 | 32 | this.clientws.on('open', () => { 33 | console.log('ws opened') 34 | this.clientws.send(`{'op': 'subscribe', 'args': 'trade:${wsSymbol}'}`) 35 | // this.clientws.send('{'op': 'subscribe', 'args': 'trade'}') //for all symbols 36 | }) 37 | 38 | this.clientws.on('message', (trade: any) => { 39 | const parsedJSON = JSON.parse(trade) 40 | try { 41 | const data = parsedJSON.data 42 | if (data) { 43 | data.forEach(obj => { 44 | var price = obj.price 45 | var size = obj.size 46 | var ts = obj.timestamp 47 | var symbol2 = obj.symbol // this will be needed for all symbols 48 | var timestamp = Date.parse(ts) 49 | var grossValue = obj.grossValue 50 | 51 | const ccxtTrade: ccxt.Trade = { 52 | amount: Number(size), 53 | datetime: (new Date(timestamp)).toISOString(), 54 | id: String(obj.trdMatchID), 55 | price: Number(price), 56 | info: {}, 57 | timestamp: timestamp, 58 | side: obj.side, 59 | symbol: symbol2, 60 | takerOrMaker: trade.maker ? 'maker' : 'taker', 61 | cost: Number(price) * Number(size), 62 | fee: undefined 63 | } 64 | console.log(ccxtTrade) 65 | this.emit('trade', ccxtTrade) 66 | }) 67 | } 68 | } catch (e) { 69 | // test 70 | } 71 | }) 72 | } 73 | 74 | 75 | public streamOrderbook (symbol: string) { 76 | const wsSymbol = symbol.replace('/', '').toUpperCase() 77 | 78 | this.clientws.on('open', () => { 79 | console.log('ws opened') 80 | this.clientws.send(`{'op': 'subscribe', 'args': 'orderBook10:${wsSymbol}'}`) 81 | }) 82 | 83 | this.clientws.on('message', (orders: any) => { 84 | const parsedJSON = JSON.parse(orders) 85 | try { 86 | const data = parsedJSON.data 87 | data.forEach(obj => { 88 | const bids: IOrder[] = obj.bids.map(bid => { 89 | return { 90 | asset: wsSymbol, 91 | price: bid[0], 92 | amount: bid[1] 93 | } 94 | }) 95 | 96 | const asks: IOrder[] = obj.asks.map(ask => { 97 | return { 98 | asset: wsSymbol, 99 | price: ask[0], 100 | amount: ask[1] 101 | } 102 | }) 103 | 104 | 105 | const orderBook: IOrderBook = { 106 | bids: bids, 107 | asks: asks 108 | } 109 | console.log(orderBook) 110 | this.emit('orderbook', orderBook) 111 | }) 112 | } catch (e) { 113 | // console.log(e) 114 | } 115 | }) 116 | } 117 | 118 | 119 | public async getTrades (symbol: string, since: number, _descending: boolean): Promise { 120 | return await this.exchange.fetchTrades(symbol, since) 121 | } 122 | } 123 | 124 | 125 | // const main = async () => { 126 | // const bitmex = new BitmexExchange() 127 | // // bitmex.streamTrades('ETHUSD') 128 | // bitmex.streamOrderbook('ETHUSD') 129 | // } 130 | 131 | // main() 132 | -------------------------------------------------------------------------------- /src/exchanges/BtcmarketsExchange.ts: -------------------------------------------------------------------------------- 1 | import * as ccxt from 'ccxt' 2 | 3 | import CCXTExchange from './core/CCXTExchange' 4 | 5 | 6 | interface IOrder { 7 | asset: string 8 | price: number 9 | amount: number 10 | } 11 | 12 | 13 | interface IOrderBook { 14 | bids: IOrder[] 15 | asks: IOrder[] 16 | } 17 | 18 | 19 | export default class BtcmarketsExchange extends CCXTExchange { 20 | private readonly clientws: WebSocket 21 | private readonly streamingTradesSymbol: string[] 22 | 23 | constructor (exchange: ccxt.Exchange) { 24 | super(exchange) 25 | 26 | const client = 'btcmarketsWeb' 27 | // this.clientws = 'btcmarketsWeb' 28 | this.streamingTradesSymbol = [] 29 | } 30 | 31 | 32 | public canStreamTrades (_symbol: string): boolean { 33 | return true 34 | } 35 | 36 | 37 | public streamTrades(symbol: string): void { 38 | 39 | // check if we are already streaming this symbol or not 40 | // if (this.streamingTradesSymbol.indexOf(symbol) >= 0) return 41 | // this.streamingTradesSymbol.push(symbol) 42 | // 43 | // const wsSymbol = symbol.replace('/', '').toUpperCase() 44 | // 45 | // this.clientws.open() 46 | // this.clientws.on('open', () => { 47 | // console.log('ws opened') 48 | // this.clientws.subscribeTrades(`t${wsSymbol}`) 49 | // }) 50 | // 51 | // this.clientws.onTrades({ symbol: `t${wsSymbol}` }, trade => { 52 | // trade.forEach((result:any) => { 53 | // const ccxtTrade: ccxt.Trade = { 54 | // amount: Number(result.amount), 55 | // datetime: (new Date(result.mts)).toISOString(), 56 | // id: String(result.id), 57 | // price: Number(result.price), 58 | // info: {}, 59 | // timestamp: result.mts, 60 | // side: trade.isBuyerMaker ? 'sell' : 'buy', 61 | // symbol: undefined, 62 | // takerOrMaker: trade.maker ? 'maker' : 'taker', 63 | // cost: Number(result.price) * Number(result.amount), 64 | // fee: undefined 65 | // } 66 | // console.log(ccxtTrade) 67 | // this.emit('trade', ccxtTrade) 68 | // }) 69 | // }) 70 | } 71 | 72 | 73 | public streamOrderbook (symbol: string) { 74 | // const wsSymbol = symbol.replace('/', '').toUpperCase() 75 | // 76 | // this.clientws.open() 77 | // this.clientws.on('open', () => { 78 | // console.log('ws opened') 79 | // this.clientws.subscribeOrderBook(`${wsSymbol}`) 80 | // }) 81 | // 82 | // this.clientws.onOrderBook({ symbol: `t${wsSymbol}` }, (orders) => { 83 | // 84 | // const bids: IOrder[] = orders.bids.map(bid => { 85 | // return { 86 | // asset: wsSymbol, 87 | // price: bid[0], 88 | // amount: bid[2] 89 | // } 90 | // }) 91 | // 92 | // const asks: IOrder[] = orders.asks.map(ask => { 93 | // return { 94 | // asset: wsSymbol, 95 | // price: ask[0], 96 | // amount: ask[2] 97 | // } 98 | // }) 99 | // 100 | // 101 | // const orderBook:IOrderBook = { 102 | // bids: bids, 103 | // asks: asks 104 | // } 105 | // this.emit('orderbook', orderBook) 106 | // console.log(orderBook) 107 | // }) 108 | } 109 | 110 | public async loadMarkets () { 111 | const markets = await this.exchange.loadMarkets() 112 | console.log(markets) 113 | return await this.exchange.loadMarkets() 114 | } 115 | 116 | 117 | public async fetchMarkets () { 118 | const markets = await this.exchange.fetchMarkets() 119 | console.log(markets) 120 | return await this.exchange.loadMarkets() 121 | } 122 | 123 | 124 | // public async fetchTickers (symbol) { 125 | // const wsSymbol = symbol.replace('/', '').toUpperCase() 126 | // const ticker = await this.exchange.has['fetchTickers'] 127 | // if(ticker == false){ 128 | // const fakeTicker = {} 129 | // fakeTicker['symbol'] = wsSymbol 130 | // fakeTicker['timestamp'] = '1553862530261.5288' 131 | // fakeTicker['datetime'] = 'datetime' 132 | // fakeTicker['high'] = 9.3e-7 133 | // fakeTicker['bid'] = 8.5e-7 134 | // fakeTicker['bidVolume'] = undefined 135 | // fakeTicker['ask'] = 0.00000103 136 | // fakeTicker['askVolume'] = undefined 137 | // fakeTicker['vwap'] = undefined 138 | // fakeTicker['open'] = undefined 139 | // fakeTicker['close'] = 8.5e-7 140 | // fakeTicker['last'] = 8.5e-7 141 | // fakeTicker['previousClose'] = undefined 142 | // fakeTicker['change'] = undefined 143 | // fakeTicker['percentage'] = undefined 144 | // fakeTicker['average'] = 9.4e-7 145 | // fakeTicker['baseVolume'] = 179063.63874 146 | // fakeTicker['quoteVolume'] = undefined 147 | // fakeTicker['info'] = 148 | // { mid: '0.00000094', 149 | // bid: '0.00000085', 150 | // ask: '0.00000103', 151 | // last_price: '0.00000085', 152 | // low: '0.00000072', 153 | // high: '0.00000093', 154 | // volume: '179063.63874', 155 | // timestamp: '1553862530.261528837', 156 | // pair: wsSymbol } 157 | 158 | // console.log('fetchTickers is not supported','\n', fakeTicker) 159 | // } else { 160 | // const ticker = await this.exchange.fetchTickers(wsSymbol) 161 | // console.log(ticker) 162 | // } 163 | // } 164 | 165 | // public async getTrades (symbol: string, since: number, _descending: boolean): Promise { 166 | // return await this.exchange.fetchTrades(symbol, since) 167 | // } 168 | } 169 | -------------------------------------------------------------------------------- /src/exchanges/HitbtcExchange.ts: -------------------------------------------------------------------------------- 1 | import * as ccxt from 'ccxt' 2 | import * as WebSocket from 'ws' 3 | 4 | import CCXTExchange from './core/CCXTExchange' 5 | import { IOrder, IOrderBook } from 'src/interfaces' 6 | 7 | 8 | export default class HitbtcExchange extends CCXTExchange { 9 | private readonly clientws: WebSocket 10 | private readonly streamingTradesSymbol: string[] 11 | 12 | constructor (exchange: ccxt.Exchange) { 13 | super(exchange) 14 | 15 | const client = new WebSocket('wss://api.hitbtc.com/api/2/ws') 16 | this.clientws = client 17 | this.streamingTradesSymbol = [] 18 | } 19 | 20 | 21 | public canStreamTrades (_symbol: string): boolean { 22 | return true 23 | } 24 | 25 | 26 | public streamTrades(symbol: string): void { 27 | 28 | // check if we are already streaming this symbol or not 29 | // if (this.streamingTradesSymbol.indexOf(symbol) >= 0) return 30 | // this.streamingTradesSymbol.push(symbol) 31 | // 32 | const wsSymbol = symbol.replace('/', '').toUpperCase() 33 | 34 | this.clientws.on('open', () => { 35 | console.log('ws opened') 36 | this.clientws.send(`{ 'method':'subscribeTrades','params': { 'symbol': '${wsSymbol}' }, 'id':123 }`) 37 | }) 38 | 39 | this.clientws.on('message', (trade: any) => { 40 | const parsedJSON = JSON.parse(trade) 41 | const params = parsedJSON.params 42 | try { 43 | const data = params.data 44 | data.forEach(obj => { 45 | const price = obj.price 46 | const quantity = obj.quantity 47 | const timestamp = Date.parse(obj.timestamp) 48 | // console.log(price, quantity, timestamp) 49 | 50 | const ccxtTrade: ccxt.Trade = { 51 | amount: Number(quantity), 52 | datetime: (new Date(timestamp)).toISOString(), 53 | id: String(obj.id), 54 | price: Number(price), 55 | info: {}, 56 | timestamp: timestamp, 57 | side: obj.side, 58 | symbol: undefined, 59 | takerOrMaker: trade.maker ? 'maker' : 'taker', 60 | cost: Number(price) * Number(quantity), 61 | fee: undefined 62 | } 63 | console.log(ccxtTrade) 64 | this.emit('trade', ccxtTrade) 65 | }) 66 | } catch (e) { 67 | // test 68 | } 69 | }) 70 | } 71 | 72 | 73 | public streamOrderbook (symbol: string) { 74 | const wsSymbol = symbol.replace('/', '').toUpperCase() 75 | 76 | this.clientws.on('open', () => { 77 | console.log('ws opened') 78 | this.clientws.send(`{'method': 'subscribeOrderbook','params': { 'symbol': '${wsSymbol}'}, 'id': 123}`) 79 | }) 80 | 81 | this.clientws.on('message', (orders: any) => { 82 | const parsedJSON = JSON.parse(orders) 83 | const params = parsedJSON.params 84 | try { 85 | const bids: IOrder[] = params.bid.map(bid => { 86 | return { 87 | asset: wsSymbol, 88 | price: bid.price, 89 | amount: bid.size 90 | } 91 | }) 92 | 93 | const asks: IOrder[] = params.ask.map(ask => { 94 | return { 95 | asset: wsSymbol, 96 | price: ask.price, 97 | amount: ask.size 98 | } 99 | }) 100 | 101 | const orderBook: IOrderBook = { 102 | bids: bids, 103 | asks: asks 104 | } 105 | this.emit('orderbook', orderBook) 106 | console.log(orderBook) 107 | } catch (e) { 108 | // test 109 | } 110 | }) 111 | } 112 | 113 | 114 | public async getTrades (symbol: string, since: number, _descending: boolean): Promise { 115 | return await this.exchange.fetchTrades(symbol, since) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/exchanges/Independentreserve.ts: -------------------------------------------------------------------------------- 1 | import * as ccxt from 'ccxt' 2 | 3 | import BaseExchange from './core/BaseExchange' 4 | import CCXTExchange from './core/CCXTExchange' 5 | 6 | 7 | export default class IndependentreserveExchange extends CCXTExchange { 8 | private readonly clientws: WebSocket 9 | private readonly streamingTradesSymbol: string[] 10 | 11 | constructor () { 12 | const independentreserve = new ccxt.independentreserve({ enableRateLimit: true }) 13 | super(independentreserve) 14 | 15 | const client = 'independentreserve' 16 | // this.clientws = 'client.ws(2, { transform: true })' 17 | this.streamingTradesSymbol = [] 18 | } 19 | 20 | 21 | public canStreamTrades (_symbol: string): boolean { 22 | return true 23 | } 24 | 25 | 26 | public streamTrades(symbol: string): void { 27 | // check if we are already streaming this symbol or not 28 | // if (this.streamingTradesSymbol.indexOf(symbol) >= 0) return 29 | // this.streamingTradesSymbol.push(symbol) 30 | // 31 | // const wsSymbol = symbol.replace('/', '').toUpperCase() 32 | // 33 | // this.clientws.open() 34 | // this.clientws.on('open', () => { 35 | // console.log('ws opened') 36 | // this.clientws.subscribeTrades(`t${wsSymbol}`) 37 | // }) 38 | // 39 | // this.clientws.onTrades({ symbol: `t${wsSymbol}` }, trade => { 40 | // trade.forEach((result:any) => { 41 | // const ccxtTrade: ccxt.Trade = { 42 | // amount: Number(result.amount), 43 | // datetime: (new Date(result.mts)).toISOString(), 44 | // id: String(result.id), 45 | // price: Number(result.price), 46 | // info: {}, 47 | // timestamp: result.mts, 48 | // side: trade.isBuyerMaker ? 'sell' : 'buy', 49 | // symbol: undefined, 50 | // takerOrMaker: trade.maker ? 'maker' : 'taker', 51 | // cost: Number(result.price) * Number(result.amount), 52 | // fee: undefined 53 | // } 54 | // console.log(ccxtTrade) 55 | // this.emit('trade', ccxtTrade) 56 | // }) 57 | // }) 58 | } 59 | 60 | 61 | public streamOrderbook (symbol: string) { 62 | // const wsSymbol = symbol.replace('/', '').toUpperCase() 63 | // 64 | // this.clientws.open() 65 | // this.clientws.on('open', () => { 66 | // console.log('ws opened') 67 | // this.clientws.subscribeOrderBook(`${wsSymbol}`) 68 | // }) 69 | // 70 | // this.clientws.onOrderBook({ symbol: `t${wsSymbol}` }, (orders) => { 71 | // 72 | // const bids: IOrder[] = orders.bids.map(bid => { 73 | // return { 74 | // asset: wsSymbol, 75 | // price: bid[0], 76 | // amount: bid[2] 77 | // } 78 | // }) 79 | // 80 | // const asks: IOrder[] = orders.asks.map(ask => { 81 | // return { 82 | // asset: wsSymbol, 83 | // price: ask[0], 84 | // amount: ask[2] 85 | // } 86 | // }) 87 | // 88 | // 89 | // const orderBook:IOrderBook = { 90 | // bids: bids, 91 | // asks: asks 92 | // } 93 | // this.emit('orderbook', orderBook) 94 | // console.log(orderBook) 95 | // }) 96 | } 97 | 98 | 99 | public async loadMarkets () { 100 | const markets = await this.exchange.loadMarkets() 101 | console.log(markets) 102 | return await this.exchange.loadMarkets() 103 | } 104 | 105 | 106 | public async fetchMarkets () { 107 | const markets = await this.exchange.fetchMarkets() 108 | console.log(markets) 109 | return await this.exchange.loadMarkets() 110 | } 111 | 112 | 113 | public async fetchTickers (symbol: string) { 114 | const wsSymbol = symbol.replace('/', '').toUpperCase() 115 | const ticker = await this.exchange.has.fetchTickers 116 | if (ticker === false) { 117 | const fakeTicker: any = {} 118 | fakeTicker.symbol = wsSymbol 119 | fakeTicker.timestamp = '1553862530261.5288' 120 | fakeTicker.datetime = 'datetime' 121 | fakeTicker.high = 9.3e-7 122 | fakeTicker.bid = 8.5e-7 123 | fakeTicker.bidVolume = undefined 124 | fakeTicker.ask = 0.00000103 125 | fakeTicker.askVolume = undefined 126 | fakeTicker.vwap = undefined 127 | fakeTicker.open = undefined 128 | fakeTicker.close = 8.5e-7 129 | fakeTicker.last = 8.5e-7 130 | fakeTicker.previousClose = undefined 131 | fakeTicker.change = undefined 132 | fakeTicker.percentage = undefined 133 | fakeTicker.average = 9.4e-7 134 | fakeTicker.baseVolume = 179063.63874 135 | fakeTicker.quoteVolume = undefined 136 | fakeTicker.info = { mid: '0.00000094', 137 | bid: '0.00000085', 138 | ask: '0.00000103', 139 | last_price: '0.00000085', 140 | low: '0.00000072', 141 | high: '0.00000093', 142 | volume: '179063.63874', 143 | timestamp: '1553862530.261528837', 144 | pair: wsSymbol } 145 | 146 | console.log('fetchTickers is not supported', '\n', fakeTicker) 147 | } else { 148 | // const ticker2 = await this.exchange.fetchTickers(symbol) 149 | // console.log(ticker2) 150 | } 151 | } 152 | 153 | 154 | public async getTrades (symbol: string, since: number, _descending: boolean): Promise { 155 | return await this.exchange.fetchTrades(symbol, since) 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/exchanges/core/BacktestableExchange.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'underscore' 2 | 3 | import BaseExchange from './BaseExchange' 4 | 5 | 6 | export default abstract class BacktestableExchange extends BaseExchange { 7 | public abstract getHistoricTrades() 8 | } 9 | -------------------------------------------------------------------------------- /src/exchanges/core/BaseExchange.ts: -------------------------------------------------------------------------------- 1 | import * as ccxt from 'ccxt' 2 | import * as _ from 'underscore' 3 | 4 | import { EventEmitter } from 'events' 5 | import { IOrderBook, IAdvanceOrderTypes, IOrderRequest } from 'src/interfaces' 6 | 7 | 8 | export type IExchangeFeature = 'view_deposits' | 'view_withdrawals' | 'get_deposit_address' | 'margin_trading' 9 | export type IChartInterval = '1m' | '3m' | '5m' | '15m' | '30m' | '1h' | '2h' | '4h' 10 | | '6h' | '8h' | '12h' | '1d' | '3d' | '1w' | '1M' | '1Y' | 'YTD' 11 | 12 | export default abstract class BaseExchange extends EventEmitter { 13 | public readonly id: string 14 | public readonly name: string 15 | 16 | constructor (id: string, name: string) { 17 | super() 18 | this.id = id 19 | this.name = name 20 | } 21 | 22 | 23 | public abstract hasFeature (id: IExchangeFeature, symbolOrCurrency?: string): Boolean 24 | 25 | 26 | public abstract streamTrades (symbol: string): void 27 | public abstract streamOrderbook (symbol: string): void 28 | 29 | public abstract stopStreamingTrades (symbol: string): void 30 | public abstract stopStreamingOrderbook (symbol: string): void 31 | 32 | // public abstract loadMarkets (): void 33 | // public abstract fetchMarkets (): void 34 | // public abstract fetchTickers (symbol: string): void 35 | 36 | public supportedOrderTypes (): IAdvanceOrderTypes[] { return [] } 37 | 38 | /* Margin trading functions */ 39 | public abstract allowsMarginTrading (symbol: string): boolean 40 | 41 | 42 | // public abstract executeSpotOrder (order: IOrderRequest): ccxt.Order 43 | // public abstract executePaperOrder (order: IOrderRequest): ccxt.Order 44 | // public abstract cancelOrder (orderId: string): ccxt.Order 45 | 46 | public abstract getTrades (symbol: string, since?: number, descending?: boolean): Promise 47 | public abstract getOrderbook (symbol: string): Promise 48 | // public abstract createMarginOrder (symbol: string) 49 | // public abstract createSpotOrder (symbol: string) 50 | // public abstract createPaperOrder (symbol: string) 51 | 52 | /* private methods */ 53 | public abstract getUserBalance (): Promise 54 | public abstract getOpenOrders (symbol: string): Promise 55 | public abstract getClosedOrders (symbol: string): Promise 56 | 57 | public abstract executeOrder (symbol: string, order: IOrderRequest): Promise 58 | public abstract executeMarginOrder (order: IOrderRequest): ccxt.Order 59 | public abstract cancelOrder (symbol: string, orderId: string): Promise 60 | 61 | 62 | public abstract getMarkets (): Promise<{[symbol: string]: ccxt.Market}> 63 | public abstract getTickers (): Promise<{[x: string]: ccxt.Ticker}> 64 | 65 | public abstract getOHLVC(symbol: string, interval: IChartInterval, from?: number, to?: number): Promise 66 | 67 | public abstract getSpotBalance (): Promise 68 | public abstract getMarginBalance (): Promise 69 | 70 | public abstract getDepositTxs (currency?: string, since?: number): Promise 71 | public abstract getWithdrawTxs (currency?: string, since?: number): Promise 72 | public abstract getDepositAddress (currency: string): Promise 73 | 74 | 75 | public toString () { 76 | return this.id 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/iguana.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This is the starting point of the application. Here we initialize the database and 3 | * start the bot.. 4 | */ 5 | import TriggerManger from './managers/TriggerManager' 6 | import PluginsManager from './managers/PluginsManager' 7 | 8 | 9 | 10 | export const start = () => { 11 | // connect all plugins \ 12 | 13 | // init plugins 14 | const pluginManager = PluginsManager.getInstance() 15 | pluginManager.loadPlugins() 16 | 17 | // If there are any existing triggers, we load them right away; (new triggers will get added 18 | // by the express.js server) 19 | const manager = TriggerManger.getInstance() 20 | manager.loadTriggers() 21 | } 22 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This is the starting point of the application. Here we initialize the database and start the server.. 3 | */ 4 | import server from './server' 5 | import * as Iguana from './iguana' 6 | import * as Database from './database' 7 | import log from './utils/log' 8 | 9 | // init the database 10 | Database.init() 11 | 12 | // start iguana 13 | Iguana.start() 14 | 15 | // start the servers 16 | const port = process.env.PORT || 8080 17 | server.listen(port, () => log.info('listening on port', port)) 18 | -------------------------------------------------------------------------------- /src/indicators/Indicator.ts: -------------------------------------------------------------------------------- 1 | export default class Indicator { 2 | public readonly input: 'candle' | 'price' | 'trade' 3 | public age: number = 0 4 | public result: number = 0 5 | 6 | constructor (input: 'candle' | 'price' | 'trade') { 7 | this.input = input 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/indicators/RSI.ts: -------------------------------------------------------------------------------- 1 | // required indicators 2 | import SMMA from './SMMA' 3 | import Indicator from './Indicator' 4 | import { ICandle } from 'src/interfaces' 5 | 6 | 7 | export default class RSI extends Indicator { 8 | private avgD: SMMA 9 | private avgU: SMMA 10 | private d: number = 0 11 | private lastClose: number = null 12 | private rs: number = 0 13 | private u: number = 0 14 | private weight: number 15 | 16 | 17 | 18 | constructor (interval: number) { 19 | super('candle') 20 | 21 | this.weight = interval 22 | this.avgU = new SMMA(this.weight) 23 | this.avgD = new SMMA(this.weight) 24 | } 25 | 26 | 27 | update (candle: ICandle) { 28 | const currentClose = candle.close 29 | 30 | if (this.lastClose === null) { 31 | // Set initial price to prevent invalid change calculation 32 | this.lastClose = currentClose 33 | 34 | // Do not calculate RSI for this reason - there's no change! 35 | this.age++ 36 | return 37 | } 38 | 39 | if (currentClose > this.lastClose) { 40 | this.u = currentClose - this.lastClose 41 | this.d = 0 42 | } else { 43 | this.u = 0 44 | this.d = this.lastClose - currentClose 45 | } 46 | 47 | this.avgU.update(this.u) 48 | this.avgD.update(this.d) 49 | 50 | this.rs = this.avgU.result / this.avgD.result 51 | this.result = 100 - (100 / (1 + this.rs)) 52 | 53 | if (this.avgD.result === 0 && this.avgU.result !== 0) this.result = 100 54 | else if (this.avgD.result === 0) this.result = 0 55 | 56 | this.lastClose = currentClose 57 | this.age++ 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/indicators/SMA.ts: -------------------------------------------------------------------------------- 1 | // required indicators 2 | // Simple Moving Average - O(1) implementation 3 | 4 | import Indicator from './Indicator' 5 | 6 | export default class SMA extends Indicator { 7 | private windowLength: number 8 | private prices: number[] = [] 9 | private sum: number = 0 10 | 11 | 12 | constructor (windowLength) { 13 | super('price') 14 | 15 | this.windowLength = windowLength 16 | } 17 | 18 | 19 | update (price: number) { 20 | var tail = this.prices[this.age] || 0 // oldest price in window 21 | this.prices[this.age] = price 22 | this.sum += price - tail 23 | this.result = this.sum / this.prices.length 24 | this.age = (this.age + 1) % this.windowLength 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/indicators/SMMA.ts: -------------------------------------------------------------------------------- 1 | // required indicators 2 | import SMA from './SMA' 3 | import Indicator from './Indicator' 4 | 5 | export default class SMMA extends Indicator { 6 | private weight: number 7 | private prices: number[] = [] 8 | private sma: SMA 9 | 10 | 11 | constructor (weight: number) { 12 | super('price') 13 | 14 | this.sma = new SMA(weight) 15 | this.weight = weight 16 | } 17 | 18 | 19 | update (price: number) { 20 | this.prices[this.age] = price 21 | 22 | if (this.prices.length < this.weight) this.sma.update(price) 23 | else if(this.prices.length === this.weight) { 24 | this.sma.update(price) 25 | this.result = this.sma.result 26 | } else this.result = (this.result * (this.weight - 1) + price) / this.weight 27 | 28 | this.age++ 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/interfaces.ts: -------------------------------------------------------------------------------- 1 | import { Request } from 'express' 2 | import { Moment } from 'moment' 3 | 4 | 5 | export interface ICandle { 6 | open: number 7 | high: number 8 | low: number 9 | volume: number 10 | close: number 11 | trades: number 12 | vwp: number 13 | start: Moment 14 | } 15 | 16 | 17 | export type IAdvice = 'long' | 'short' | 'soft' | 'close-position' | 'do-nothing' | 'market-buy' | 'market-sell' 18 | | 'limit-buy' | 'limit-sell' | 'cancel-order' | 'tiered-profit' 19 | // 'long' | 'short' | 'close-position' | 'do-nothing' 20 | 21 | // https://support.bitfinex.com/hc/en-us/articles/115003451049-Bitfinex-Order-Types 22 | // http://www.online-stock-trading-guide.com/sell-stop-order.html 23 | export type IAdvanceOrderTypes = 'stop-loss' | 'take-profit' | 'trailing-stop' | 'stop-limit' | 24 | 'buy-stop' | 'sell-stop' | 'bracket-order' | 'scaled-order' | 'hidden' | 'post-only-limit' | 25 | 'immediate-or-cancel' | 'reduce-only' | 'one-cancels-other' | 'fill-or-kill' | 26 | 'cancel-order' | 'tiered-profit' 27 | 28 | 29 | export interface IOrderRequest { 30 | side: 'buy' | 'sell' 31 | kind: 'market' | 'limit' | 'stop' | 'advanced' 32 | exchange: 'spot' | 'margin' | 'paper' 33 | amount: number 34 | price?: number 35 | // symbol: string 36 | 37 | takeProfitEnabled: boolean 38 | takeProfitPrice?: number 39 | 40 | stopLossEnabled: boolean 41 | stopLossPrice?: number 42 | 43 | leverageMultiplier: number 44 | 45 | advancedOrders: { 46 | [kind: string]: any[] 47 | } 48 | } 49 | 50 | 51 | export interface IAppRequest extends Request { 52 | uid: string 53 | } 54 | 55 | 56 | 57 | export interface IOrder { 58 | price: number 59 | amount?: number 60 | } 61 | 62 | 63 | export interface IOrderBook { 64 | bids: IOrder[] 65 | asks: IOrder[] 66 | } 67 | -------------------------------------------------------------------------------- /src/managers/AdviceManager.ts: -------------------------------------------------------------------------------- 1 | import * as ccxt from 'ccxt' 2 | 3 | import { IAdvice } from '../interfaces' 4 | import Advices from '../database/models/advices' 5 | import BaseTrigger from '../triggers/BaseTrigger' 6 | import PluginsManager from './PluginsManager' 7 | import UserExchanges from '../database/models/userexchanges' 8 | 9 | 10 | /** 11 | * The advice manager is where all the advices get executed. An advice is an action for a trade or some kind of action. 12 | * When an advice is added (from a trigger/strategy that makes an advice) a couple of things happen. 13 | * 14 | * - An entry gets added into the DB 15 | * - All the plugins get notified about the advice 16 | * - according the advice (buy/sell etc..) an order is created and executed 17 | * 18 | * This class is a singleton. 19 | */ 20 | export default class AdviceManager { 21 | private static readonly instance = new AdviceManager() 22 | 23 | 24 | public async addAdvice (t: BaseTrigger, advice: IAdvice, price: number, amount: number, extras: any) { 25 | // create and save the advice into the DB 26 | const adviceDB = new Advices({ 27 | uid: t.getUID(), 28 | symbol: t.getSymbol(), 29 | exchange: t.getExchange(), 30 | advice, 31 | price, 32 | mode: 'realtime', 33 | amount, 34 | trigger_id: t.getDBId() 35 | }) 36 | await adviceDB.save() 37 | 38 | // notify all the plugins about the advice made... 39 | PluginsManager.getInstance().onAdvice(t, advice, price, amount) 40 | 41 | // find the user's credentials and execute the advice... 42 | await UserExchanges.findOne({ where: { uid: t.getUID(), exchange: t.getExchange() } }) 43 | .then(async data => { 44 | // create the exchange instance 45 | // todo: decrypt the keys 46 | const exchange: ccxt.Exchange = new ccxt[t.getExchange()]({ apiKey: data.apiKey, secret: data.apiSecret, password: data.apiPassword }) 47 | 48 | /* execute the advice */ 49 | 50 | try { 51 | console.log('Out side condition check ', adviceDB.advice) 52 | // market buy 53 | if (adviceDB.advice === 'market-buy') { 54 | const res = await exchange.createOrder(t.getSymbol(), 'market', 'buy', amount) 55 | 56 | adviceDB.order_id = res.info.orderId 57 | adviceDB.save() 58 | } 59 | 60 | // market sell 61 | if (adviceDB.advice === 'market-sell') { 62 | const res = await exchange.createOrder(t.getSymbol(), 'market', 'sell', amount) 63 | 64 | adviceDB.order_id = res.info.orderId 65 | adviceDB.save() 66 | } 67 | 68 | // limit sell 69 | if (adviceDB.advice === 'limit-sell') { 70 | const res = await exchange.createOrder(t.getSymbol(), 'limit', 'sell', amount, price) 71 | 72 | adviceDB.order_id = res.info.orderId 73 | adviceDB.save() 74 | } 75 | 76 | // limit buy 77 | if (adviceDB.advice === 'limit-buy') { 78 | const res = await exchange.createOrder(t.getSymbol(), 'limit', 'buy', amount, price) 79 | 80 | adviceDB.order_id = res.info.orderId 81 | adviceDB.save() 82 | } 83 | 84 | // cancel order 85 | if (adviceDB.advice === 'cancel-order') { 86 | await exchange.cancelOrder(extras.orderId, t.getSymbol()) 87 | adviceDB.order_id = extras.orderId 88 | adviceDB.save() 89 | } 90 | } catch (e) { 91 | adviceDB.error_msg = e.message 92 | adviceDB.save() 93 | // TODO: if we encounter some kind of error we notify the plugins about it 94 | PluginsManager.getInstance().onError(e, t, advice, price, amount) 95 | } 96 | }) 97 | } 98 | 99 | 100 | static getInstance () { 101 | return AdviceManager.instance 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/managers/BudfoxManager.ts: -------------------------------------------------------------------------------- 1 | import BudFox from '../budfox' 2 | import ExchangeManger from './ExchangeManager' 3 | import log from '../utils/log' 4 | 5 | 6 | interface IBudfoxes { 7 | [exchangeSymbol: string]: BudFox 8 | } 9 | 10 | 11 | /** 12 | * The Budfox Manager is responsible for managing all the budfox instances. All other mangers 13 | * fetch budfox instances using this manager. 14 | * 15 | * This class is a singleton. 16 | */ 17 | export default class BudfoxManger { 18 | private readonly budfoxes: IBudfoxes = {} 19 | private readonly manager = ExchangeManger.getInstance() 20 | private static readonly instance = new BudfoxManger() 21 | 22 | 23 | getBudfox (exchangeId: string, symbol: string): BudFox { 24 | const exchangeSymbol = `${exchangeId}-${symbol}` 25 | 26 | if (this.budfoxes[exchangeSymbol]) return this.budfoxes[exchangeSymbol] 27 | 28 | const exchange = this.manager.getExchange(exchangeId) 29 | 30 | log.debug('creating budfox for', exchange.id, symbol) 31 | 32 | const budfox = new BudFox(exchange, symbol) 33 | this.budfoxes[exchangeSymbol] = budfox 34 | 35 | return budfox 36 | } 37 | 38 | 39 | removeBudfox (budfox: BudFox) { 40 | log.debug('removing budfox for', budfox.exchange.id, budfox.symbol) 41 | 42 | const exchangeSymbol = `${budfox.exchange.id}-${budfox.symbol}` 43 | if (this.budfoxes[exchangeSymbol]) delete this.budfoxes[exchangeSymbol] 44 | budfox.murder() 45 | } 46 | 47 | 48 | static getInstance () { 49 | return BudfoxManger.instance 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/managers/ExchangeManager.ts: -------------------------------------------------------------------------------- 1 | import * as ccxt from 'ccxt' 2 | 3 | import BaseExchange from '../exchanges/core/BaseExchange' 4 | import CCXTExchange from '../exchanges/core/CCXTExchange' 5 | import BinanceExchange from '../exchanges/BinanceExchange' 6 | 7 | 8 | interface IExchanges { 9 | [exchangeId: string]: BaseExchange 10 | } 11 | 12 | 13 | export default class ExchangeManger { 14 | private readonly exchanges: IExchanges = {} 15 | private static readonly instance = new ExchangeManger() 16 | 17 | 18 | getExchange (exchangeId: string): BaseExchange { 19 | // if the exchange is already instanstiated, then we return that 20 | if (this.exchanges[exchangeId]) return this.exchanges[exchangeId] 21 | 22 | // create a CCXT instance for each exchange; (note that the enableRateLimit should be enabled) 23 | const ccxtExchange = new ccxt[exchangeId]({ enableRateLimit: true }) 24 | 25 | const exchange = this.createBaseExchangeInstance(ccxtExchange) 26 | this.exchanges[exchangeId] = exchange 27 | 28 | return exchange 29 | } 30 | 31 | 32 | createBaseExchangeInstance (exchange: ccxt.Exchange) { 33 | if (exchange.id === 'binance') return new BinanceExchange(exchange) 34 | return new CCXTExchange(exchange) 35 | } 36 | 37 | 38 | static getInstance () { 39 | return ExchangeManger.instance 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/managers/PluginsManager.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'underscore' 2 | 3 | import { IAdvice } from '../interfaces' 4 | import BasePlugin from '../plugins/BasePlugin' 5 | import BaseTrigger from '../triggers/BaseTrigger' 6 | import Plugins from '../database/models/plugins' 7 | import SlackPlugin from '../plugins/slack' 8 | import TelegramPlugin from '../plugins/telegram' 9 | 10 | 11 | interface IPlugins { 12 | [uid: string]: { 13 | [pluginKind: string]: BasePlugin 14 | } 15 | } 16 | 17 | 18 | export default class PluginsManager { 19 | private readonly plugins: IPlugins = {} 20 | private static readonly instance = new PluginsManager() 21 | 22 | 23 | public async loadPlugins () { 24 | const plugins = await Plugins.findAll({ where: { isActive: true }}) 25 | plugins.forEach(p => this.registerPlugin(p)) 26 | } 27 | 28 | 29 | public onAdvice (trigger: BaseTrigger, advice: IAdvice, price?: number, amount?: number) { 30 | const plugins = this.plugins[trigger.getUID()] || [] 31 | 32 | const pluginKeys = _.keys(plugins) 33 | pluginKeys.forEach(key => plugins[key].onTriggered(trigger, advice, price, amount)) 34 | } 35 | 36 | 37 | public onError (error: Error, trigger: BaseTrigger, advice: IAdvice, price?: number, amount?: number) { 38 | const plugins = this.plugins[trigger.getUID()] || [] 39 | 40 | const pluginKeys = _.keys(plugins) 41 | pluginKeys.forEach(key => plugins[key].onError(error, trigger, advice, price, amount)) 42 | } 43 | 44 | 45 | public registerPlugin (p: Plugins) { 46 | const plugin = this.getPlugin(p) 47 | if (!plugin) return 48 | 49 | const userplugins = this.plugins[p.uid] || {} 50 | 51 | // delete the old plugin (if it exists) 52 | if (userplugins[plugin.name]) { 53 | userplugins[plugin.name].kill() 54 | delete userplugins[plugin.name] 55 | } 56 | 57 | // added plugins 58 | userplugins[plugin.name] = plugin 59 | this.plugins[p.uid] = userplugins 60 | } 61 | 62 | 63 | private getPlugin (plugin: Plugins) { 64 | if (plugin.kind === 'slack') return new SlackPlugin(plugin) 65 | if (plugin.kind === 'telegram') return new TelegramPlugin(plugin) 66 | } 67 | 68 | 69 | static getInstance () { 70 | return PluginsManager.instance 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/managers/TriggerManager.ts: -------------------------------------------------------------------------------- 1 | import AdviceManager from './AdviceManager' 2 | import BaseTrigger from '../triggers/BaseTrigger' 3 | import BudfoxManger from './BudfoxManager' 4 | import CancelOrderTrigger from '../triggers/CancelOrderTrigger' 5 | import StopLossTakeProfitTrigger from '../triggers/StopLossTakeProfitTrigger' 6 | import StopLossTrigger from '../triggers/StopLossTrigger' 7 | import TakeProfitTrigger from '../triggers/TakeProfitTrigger' 8 | import TieredTakeProfitTrigger from '../triggers/TieredTakeProfitTrigger' 9 | import Triggers from '../database/models/triggers' 10 | 11 | 12 | interface IExchangeTriggers { 13 | [exchangeSymbol: string]: BaseTrigger[] 14 | } 15 | 16 | 17 | /** 18 | * The triggers manager is a speical class that handles all the triggers in one place. This 19 | * class listens to all the new candles emitted by the Budfox managers and sends each candle 20 | * to the trigger. Once a trigger has been executed, the class removes it completely and marks 21 | * it as executed. 22 | * 23 | * Whenever a trigger gives an advice, the class also informs the AdviceManager of the advice. 24 | * 25 | * This class is a singleton. 26 | */ 27 | export default class TriggerManger { 28 | private readonly triggers: IExchangeTriggers = {} 29 | private readonly manager = BudfoxManger.getInstance() 30 | 31 | private static readonly instance = new TriggerManger() 32 | 33 | 34 | /** 35 | * Loads any tirggers which exist in the DB onto the server. 36 | */ 37 | async loadTriggers () { 38 | const activeTriggers = await Triggers.findAll({ where: { isActive: true }}) 39 | activeTriggers.forEach(t => this.addTrigger(t)) 40 | } 41 | 42 | 43 | /** 44 | * Adds a new trigger. This trigger will remain active until we kill it. 45 | * 46 | * @param t The trigger DB model to be tracked. 47 | */ 48 | public addTrigger (t: Triggers) { 49 | const trigger = this._getTrigger(t) 50 | if (!trigger) return 51 | 52 | const exchangeSymbol = this._getExchangeSymbol(t) 53 | const budfox = this.manager.getBudfox(t.exchange, t.symbol) 54 | 55 | // add the trigger into our array of triggers 56 | const exchangeSymbolTriggers = this.triggers[exchangeSymbol] || [] 57 | exchangeSymbolTriggers.push(trigger) 58 | this.triggers[exchangeSymbol] = exchangeSymbolTriggers 59 | 60 | budfox.on('candle', candle => trigger.onCandle(candle)) 61 | budfox.on('trade', trade => trigger.onTrade(trade)) 62 | 63 | // whenever a trigger executes 64 | trigger.on('advice', ({ advice, price, amount, ...extras }) => { 65 | AdviceManager.getInstance().addAdvice(trigger, advice, price, amount, extras) 66 | }) 67 | 68 | // once a trigger has finished 69 | trigger.on('close', () => { 70 | // we remove the trigger from the array of triggers 71 | const exchangeSymbolTriggers = this.triggers[exchangeSymbol] || [] 72 | const index = exchangeSymbolTriggers.indexOf(trigger) 73 | if (index === -1) return 74 | this.triggers[exchangeSymbol].splice(index, 1) 75 | 76 | // and remove budfox if there are no more triggers left for this symbol 77 | if (this.triggers[exchangeSymbol].length === 0) this.manager.removeBudfox(budfox) 78 | }) 79 | } 80 | 81 | 82 | public async removeTrigger (t: Triggers) { 83 | const exchangeSymbol = this._getExchangeSymbol(t) 84 | 85 | const exchangeSymbolTriggers = this.triggers[exchangeSymbol] || [] 86 | const newTriggers = exchangeSymbolTriggers.filter(e => e.getDBId() !== t.id) 87 | this.triggers[exchangeSymbol] = newTriggers 88 | 89 | // delete trigger from db 90 | // No need to delete the trigger from db only a flag is changed 91 | // await Triggers.destroy({ where: { id: t.id }}) 92 | } 93 | 94 | 95 | /** 96 | * Return the Trigger instantiated in a class. 97 | * 98 | * Each trigger has a Trigger class that implements it with the logic. 99 | * 100 | * @param triggerDB The trigger DB model to be instantiated 101 | */ 102 | private _getTrigger (triggerDB: Triggers) { 103 | // stop losses 104 | if (triggerDB.kind === 'stop-loss') return new StopLossTrigger(triggerDB) 105 | if (triggerDB.kind === 'take-profit') return new TakeProfitTrigger(triggerDB) 106 | if (triggerDB.kind === 'cancel-order') return new CancelOrderTrigger(triggerDB) 107 | if (triggerDB.kind === 'tiered-profit') return new TieredTakeProfitTrigger(triggerDB) 108 | if (triggerDB.kind === 'stop-loss-take-profit') return new StopLossTakeProfitTrigger(triggerDB) 109 | 110 | // tiered take-profits etc.. etc.. 111 | } 112 | 113 | 114 | private _getExchangeSymbol (t: Triggers) { 115 | return `${t.exchange}-${t.symbol}`.toUpperCase().trim() 116 | } 117 | 118 | 119 | /** 120 | * Returns the current active instance of this class. 121 | */ 122 | static getInstance () { 123 | return TriggerManger.instance 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "src" 3 | } 4 | -------------------------------------------------------------------------------- /src/plugins/BasePlugin.ts: -------------------------------------------------------------------------------- 1 | import { IAdvice } from '../interfaces' 2 | import BaseTrigger from '../triggers/BaseTrigger' 3 | import Plugins from '../database/models/plugins' 4 | 5 | 6 | /** 7 | * A plugin is something that integrates with the trader; It can't be used 8 | * to influence the decision of a trade, but it can be used to trigger 3rd 9 | * party applications. 10 | * 11 | * For code that is used to influnce the decision of a trade; see Strategies. 12 | */ 13 | export default abstract class BasePlugin { 14 | public readonly name: string 15 | public readonly description: string 16 | public readonly version: number 17 | public readonly pluginDB: Plugins 18 | public readonly modes: 'realtime' | 'backtest' [] 19 | 20 | protected options: T 21 | 22 | 23 | /** 24 | * @param pluginDB The plugin DB instance 25 | */ 26 | constructor (pluginDB: Plugins) { 27 | this.pluginDB = pluginDB 28 | this.options = JSON.parse(pluginDB.config) 29 | } 30 | 31 | 32 | /** 33 | * This fn. is called whenever a trigger has given an advice 34 | * 35 | * @param trigger The trigger object 36 | * @param advice The advice given by the trigger 37 | * @param price The price at which it was triggered 38 | * @param amount The amount adviced by the trigger 39 | */ 40 | abstract onTriggerAdvice (trigger: BaseTrigger, advice: IAdvice, price?: number, amount?: number): void 41 | 42 | 43 | /** 44 | * This fn. is called whenever an advice encounters an error 45 | * 46 | * @param trigger The trigger object 47 | * @param advice The advice given by the trigger 48 | * @param price The price at which it was triggered 49 | * @param amount The amount adviced by the trigger 50 | */ 51 | abstract onError (error: Error, trigger: BaseTrigger, advice: IAdvice, price?: number, amount?: number): void 52 | 53 | 54 | /** 55 | * Called whenever a plugin is going to be killed 56 | */ 57 | abstract kill () 58 | 59 | 60 | public getUID () { 61 | return this.pluginDB.uid 62 | } 63 | 64 | 65 | public getConfig (): T { 66 | return JSON.parse(this.pluginDB.config) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/plugins/slack.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'underscore' 2 | 3 | import { IAdvice } from '../interfaces' 4 | import BasePlugin from './BasePlugin' 5 | import BaseTrigger from '../triggers/BaseTrigger' 6 | import log from '../utils/log' 7 | import Plugins from '../database/models/plugins' 8 | 9 | const WebClient = require('@slack/client').WebClient 10 | 11 | 12 | interface ISlackOptions { 13 | token: string 14 | sendMessageOnStart: boolean 15 | channel: string 16 | muteSoft: boolean 17 | } 18 | 19 | 20 | export default class SlackPlugin extends BasePlugin { 21 | public readonly name = 'Slack' 22 | public readonly description = 'Sends notifications to slack channel.' 23 | public readonly version = 1 24 | 25 | private readonly slack: any 26 | 27 | 28 | constructor (pluginDB: Plugins) { 29 | super(pluginDB) 30 | this.slack = new WebClient(this.options.token) 31 | 32 | if (this.options.sendMessageOnStart){ 33 | const body = this._createResponse('#439FE0', 'Gekko started!') 34 | this._send(body) 35 | } else log.debug('Skipping Send message on startup') 36 | } 37 | 38 | 39 | kill () { 40 | 41 | } 42 | 43 | 44 | onTriggerAdvice (trigger: BaseTrigger, advice: IAdvice, price?: number) { 45 | if (advice == 'soft' && this.options.muteSoft) return 46 | 47 | const color = advice === 'long' ? 'good' : 48 | advice === 'short' ? 'danger' : 49 | 'warning' 50 | 51 | const body = this._createResponse( 52 | color, 53 | `There is a new trend! The advice is to go ${advice}! Current price is ${price}` 54 | ) 55 | 56 | this._send(body) 57 | } 58 | 59 | 60 | onError (error: Error) { 61 | const body = this._createResponse( 62 | 'danger', 63 | `Error: ` + error 64 | ) 65 | 66 | this._send(body) 67 | } 68 | 69 | 70 | checkResults (error) { 71 | if (error) log.warn('error sending slack', error) 72 | else log.info('Send advice via slack.') 73 | } 74 | 75 | 76 | private _send (content) { 77 | this.slack.chat.postMessage(this.options.channel, '', content, (error, response) => { 78 | if (error || !response) log.error('Slack ERROR:', error) 79 | else log.info('Slack Message Sent') 80 | }) 81 | } 82 | 83 | 84 | private _createResponse (color: string, text: string) { 85 | const template = { 86 | // username: `${this.exchange.toUpperCase()}-${this.symbol}`, 87 | // icon_url: this.createIconUrl(), 88 | attachments: [ 89 | { 90 | fallback: '', 91 | color, 92 | text, 93 | mrkdwn_in: ['text'] 94 | } 95 | ] 96 | } 97 | 98 | return template 99 | } 100 | 101 | 102 | // createIconUrl () { 103 | // const asset = config.watch.asset === 'XBT' ? 'btc' :config.watch.asset.toLowerCase() 104 | // return 'https://github.com/cjdowner/cryptocurrency-icons/raw/master/128/icon/' + asset + '.png' 105 | // } 106 | } 107 | -------------------------------------------------------------------------------- /src/plugins/telegram.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'underscore' 2 | import * as Telegram from 'node-telegram-bot-api' 3 | 4 | import { IAdvice } from '../interfaces' 5 | import BasePlugin from './BasePlugin' 6 | import BaseTrigger from '../triggers/BaseTrigger' 7 | import Plugins from '../database/models/plugins' 8 | 9 | 10 | interface ITelegramOptions { 11 | chatId?: string 12 | token: string 13 | } 14 | 15 | 16 | export default class TelegramPlugin extends BasePlugin { 17 | public readonly name = 'Telegram' 18 | public readonly description = 'Sends notifications to a Telegram bot.' 19 | public readonly version = 1 20 | 21 | private bot: Telegram 22 | 23 | 24 | constructor (pluginDB: Plugins) { 25 | super(pluginDB) 26 | 27 | this.bot = new Telegram(this.options.token, { polling: { interval: 1000 }}) 28 | 29 | if (this.options.chatId) this.send('I\'m now connected to the trading server!') 30 | 31 | this.bot.onText(/(.+)/, this.onText) 32 | } 33 | 34 | 35 | kill () { 36 | this.bot.stopPolling() 37 | } 38 | 39 | 40 | onTriggerAdvice (trigger: BaseTrigger, advice: IAdvice, price?: number, amount?: number) { 41 | const message = `${trigger.name} triggered! and adviced to \`${advice}\` ` + 42 | `on \`${trigger.getExchange().toUpperCase()}\` \`${trigger.getSymbol()}\` with a ` + 43 | `amount of \`${amount}\`! Current price is \`${price}\`` 44 | 45 | this.send(message) 46 | } 47 | 48 | 49 | onError (error: Error) { 50 | this.send(`Error: ` + error) 51 | } 52 | 53 | 54 | private send (message: string, _chatId?: string) { 55 | const chatId = _chatId || this.options.chatId 56 | if (chatId) this.bot.sendMessage(chatId, message, { parse_mode: 'Markdown' }) 57 | } 58 | 59 | 60 | private onText = (msg: any, _text: string[]) =>{ 61 | const text = _text[0] 62 | const chatId = msg.chat.id 63 | 64 | switch (text.toLowerCase()) { 65 | case '/start': 66 | this.send( 67 | `Hello! your chat id is: \`${chatId}\`. Enter the chat id in the CryptoControl ` + 68 | `terminal to recieve all kinds of trading notifications.`, chatId 69 | ) 70 | return 71 | 72 | case '/help': 73 | this.send( 74 | `Your chat id is: \`${chatId}\`. \n\nCurrently this bot does support commands :( ` + 75 | `\n\nEnter the chat id in the CryptoControl terminal to recieve all kinds of trading notifications.`, 76 | chatId 77 | ) 78 | return 79 | } 80 | 81 | this.send(`use /help to see a list of commands`, chatId) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/server/controllers/advices.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'underscore' 2 | 3 | import Advices from '../../database/models/advices' 4 | 5 | 6 | 7 | /** 8 | * get all advices for a user 9 | */ 10 | export const getAdvices = async (uid: string) => { 11 | const advices = await Advices.findAll({ where: { uid } }) 12 | return advices 13 | } 14 | -------------------------------------------------------------------------------- /src/server/controllers/keys.ts: -------------------------------------------------------------------------------- 1 | import * as ccxt from 'ccxt' 2 | import * as _ from 'underscore' 3 | 4 | 5 | import UserExchanges from '../../database/models/userexchanges' 6 | 7 | 8 | /** 9 | * Set the API key for an exchange for the logged in user 10 | */ 11 | export const setAPIKey = async (uid: string, exchange: string, data: any) => { 12 | // first authenticate with the exchange and see if the API key works 13 | const ccxtExchange = new ccxt[exchange]({ 14 | apiKey: data.apiKey, 15 | secret: data.secret, 16 | password: data.password, 17 | useServerTime: true, 18 | enableRateLimit: true 19 | }) 20 | 21 | // Make a sample request to fetch the user's balance; If this fn returns successfully 22 | // then we save the API keys and continue 23 | await ccxtExchange.fetchBalance() 24 | 25 | // find or update logic 26 | const found = await UserExchanges.findOne({ where: { uid, exchange } }) 27 | 28 | if (found) { 29 | // todo: encrypt the keys 30 | found.apiKey = data.apiKey 31 | found.apiSecret = data.secret 32 | found.apiPassword = data.password 33 | await found.save() 34 | return found 35 | } 36 | 37 | // todo: encrypt the keys 38 | const row = new UserExchanges({ 39 | uid, 40 | exchange, 41 | apiKey: data.apiKey, 42 | apiSecret: data.secret, 43 | apiPassword: data.password 44 | }) 45 | 46 | await row.save() 47 | return row 48 | } 49 | 50 | 51 | /** 52 | * Get all the API keys saved for the logged in user 53 | */ 54 | export const getAllUserApiKeys = async (uid: string) => { 55 | return await UserExchanges.findAll({ where: { uid } }) 56 | .then(data => { 57 | 58 | // hide the secret keys 59 | const parsedKeys = data.map(row => { 60 | const json = row.toJSON() 61 | return _.mapObject(json, (val, key) => { 62 | if (key !== 'apiSecret' && key !== 'apiPassword' || !val) return val 63 | return val.replace(/./gi, '*') 64 | }) 65 | }) 66 | 67 | return parsedKeys 68 | }) 69 | } 70 | 71 | 72 | export const deleteApiKey = async (uid: string, exchange: string) => { 73 | return await UserExchanges.destroy({ where: { uid, exchange }}) 74 | } 75 | -------------------------------------------------------------------------------- /src/server/controllers/plugins.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as _ from 'underscore' 3 | import PluginsManager from 'src/managers/PluginsManager' 4 | import Plugins from 'src/database/models/plugins' 5 | import TelegramPlugin from 'src/plugins/telegram'; 6 | 7 | /** 8 | * create a new plugin for a user 9 | */ 10 | export const regsiterPlugin = async (uid: string, kind: string, config: any) => { 11 | const plugin = new Plugins({ 12 | uid, 13 | kind, 14 | config: JSON.stringify(config), 15 | isActive: true 16 | }) 17 | 18 | await plugin.save() 19 | 20 | // once the plugin is register, we start tracking it in our DB 21 | PluginsManager.getInstance().registerPlugin(plugin) 22 | 23 | return plugin 24 | } 25 | 26 | 27 | export const updatePlugin = async (uid: string, id: string, config: any) => { 28 | const plugin = await Plugins.findOne({ where: { uid, id } }) 29 | if (plugin) { 30 | plugin.config = JSON.stringify(config) 31 | plugin.save() 32 | 33 | PluginsManager.getInstance().registerPlugin(plugin) 34 | } 35 | } 36 | 37 | 38 | /** 39 | * get all existing plugins for a user 40 | */ 41 | export const getPlugins = async (uid: string) => { 42 | const plugins = await Plugins.findAll({ where: { uid } }) 43 | return plugins 44 | } 45 | 46 | 47 | /** 48 | * Delete a specific plugin 49 | */ 50 | export const deleteplugin = async (uid: string, id: number) => { 51 | const plugin = await Plugins.findOne({ where: { uid, id } }) 52 | if (plugin) plugin.destroy() 53 | } 54 | 55 | /** 56 | * Enable / Disable a plugin 57 | */ 58 | export const enableDisablePlugin = async ( 59 | uid: string, action: 'enable' | 'disable') => { 60 | const plugin = await Plugins.findOne({ where: { uid } }) 61 | 62 | if (!plugin) return // TODO: code on failure 63 | 64 | if (!action) throw new Error("Missing action in req body") 65 | 66 | if (action === 'enable') plugin.isActive = true 67 | 68 | if (action === 'disable') plugin.isActive = false 69 | 70 | plugin.save() 71 | 72 | return plugin; 73 | } 74 | 75 | /** 76 | * To set telegram params 77 | */ 78 | 79 | export const setTelegramParams = async ( 80 | uid: string, chatId: string) => { 81 | const plugin = await Plugins.findOne({ 82 | where: { uid, kind: "telegram", isActive: true } }) 83 | 84 | if (!plugin) return // TODO: code on failure 85 | 86 | const config = JSON.parse(plugin.config) 87 | 88 | plugin.config = JSON.stringify({ 89 | ...config, 90 | chatId 91 | }) 92 | 93 | plugin.save(); 94 | 95 | return plugin 96 | } 97 | -------------------------------------------------------------------------------- /src/server/controllers/triggers.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'underscore' 2 | 3 | import Triggers from '../../database/models/triggers' 4 | import TriggerManger from '../../managers/TriggerManager' 5 | 6 | 7 | /** 8 | * create a new trigger for a user 9 | */ 10 | export const createTrigger = async (uid: string, exchange: string, symbol: string, kind: string, params: any) => { 11 | const trigger = new Triggers({ 12 | uid, 13 | symbol, 14 | exchange, 15 | kind, 16 | isActive: true, 17 | params: JSON.stringify(params) 18 | }) 19 | 20 | await trigger.save() 21 | 22 | // once the trigger is created, we start tracking it in our DB 23 | TriggerManger.getInstance().addTrigger(trigger) 24 | 25 | return trigger 26 | } 27 | 28 | 29 | /** 30 | * get all existing triggers for a user 31 | */ 32 | export const getTriggers = async (uid: string) => { 33 | const triggers = await Triggers.findAll({ where: { uid, isActive: true } }) 34 | return triggers 35 | } 36 | 37 | 38 | /** 39 | * Delete a specific trigger 40 | */ 41 | export const deleteTrigger = async (uid: string, id: number) => { 42 | const trigger = await Triggers.findOne({ where: { uid, id } }) 43 | if (trigger) { 44 | TriggerManger.getInstance().removeTrigger(trigger) 45 | 46 | trigger.isActive = false 47 | trigger.save() 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/server/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This is where the HTTP Json server runs. It is what is used by the CryptoControl terminal to 3 | * communicate with the trading sever. 4 | */ 5 | import * as bodyParser from 'body-parser' 6 | import * as express from 'express' 7 | import * as _ from 'underscore' 8 | import * as cors from 'cors' 9 | import * as jwt from 'jsonwebtoken' 10 | import * as morgan from 'morgan' 11 | 12 | import router from './routes' 13 | import { IAppRequest } from '../interfaces' 14 | import InvalidJWTError from '../errors/InvalidJWTError' 15 | 16 | 17 | const app = express() 18 | app.use(bodyParser.json({ limit: '2mb' })) 19 | app.use(bodyParser.urlencoded({ limit: '2mb', extended: false })) 20 | app.use(morgan()) 21 | 22 | 23 | // enable all cors 24 | app.use(cors()) 25 | 26 | 27 | import * as ccxt from 'ccxt' 28 | const cache = {} 29 | app.get('/balance/:exchange', (req: IAppRequest, res, next) => { 30 | const { exchange } = req.params 31 | const binance = new ccxt.binance({ 32 | apiKey: 'LgbBdOWwDXSOmu28JOJp64qJA6zJXph6uBmG1snlffGCzCHQMmK1uXKPUTDPY1Uc', 33 | secret: 'ikT1GLusBcOYgqHJO9fgyyuKDuEhVuHrxQXO0LCpspSAFHKCvoQO2Bb67PoYkwuQ' 34 | }) 35 | 36 | if (cache[exchange]) return res.json(cache[exchange]) 37 | 38 | binance.fetchBalance() 39 | .then(balance => { 40 | cache[exchange] = balance 41 | setTimeout(() => delete cache[exchange], 5000) 42 | 43 | res.json(balance) 44 | }) 45 | .catch(next) 46 | }) 47 | 48 | // authenticate the user using JWT tokens 49 | app.use((req: IAppRequest, _res, next) => { 50 | const token = req.header('x-jwt') 51 | 52 | const jwtSecret = app.get('secret') || process.env.SERVER_SECRET || 'secret_keyboard_cat' 53 | 54 | if (!token) return next() 55 | 56 | // verify the jwt token 57 | jwt.verify(token, jwtSecret, (err, decoded) => { 58 | if (err) return next(new InvalidJWTError) 59 | req.uid = decoded.uid 60 | next() 61 | }) 62 | }) 63 | 64 | 65 | // install routes 66 | app.use(router) 67 | 68 | 69 | // error handler 70 | app.use((error: any, _req, res, _next) => { 71 | res.status(error.status || 500) 72 | res.json({ error: error.message }) 73 | }) 74 | 75 | export default app 76 | -------------------------------------------------------------------------------- /src/server/routes/advices.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express' 2 | 3 | import { IAppRequest } from 'src/interfaces' 4 | import * as Controllers from '../controllers/advices' 5 | 6 | const router = Router() 7 | 8 | 9 | router.get('/', async (req: IAppRequest, res) => res.json(await Controllers.getAdvices(req.uid))) 10 | 11 | 12 | export default router 13 | -------------------------------------------------------------------------------- /src/server/routes/index.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express' 2 | import * as _ from 'underscore' 3 | 4 | import { IAppRequest } from 'src/interfaces' 5 | import NotAuthorizedError from '../../errors/NotAuthorizedError' 6 | 7 | import advices from './advices' 8 | import keys from './keys' 9 | import plugins from './plugins' 10 | import triggers from './triggers' 11 | 12 | const packageJson = require('../../../package.json') 13 | const router = Router() 14 | 15 | 16 | /** 17 | * Gets the status of the server. A great way for the terminal to check if the 18 | * trading server is of the latest version or not. 19 | */ 20 | router.get('/', (_req: IAppRequest, res) => { 21 | res.json({ 22 | sourceCode: 'https://github.com/cryptocontrol/algo-trading-server', 23 | version: packageJson.version, 24 | uptime: process.uptime() 25 | }) 26 | }) 27 | 28 | 29 | // For every route henceforth; require the user to be logged in 30 | router.use((req: IAppRequest, _res, next) => { 31 | if (!req.uid) return next(new NotAuthorizedError) 32 | next() 33 | }) 34 | 35 | 36 | // Gets the current user's id 37 | router.get('/me', (req: IAppRequest, res) => res.json({ uid: req.uid })) 38 | 39 | 40 | // init all the different routes 41 | router.use('/advices', advices) 42 | router.use('/keys', keys) 43 | router.use('/plugins', plugins) 44 | router.use('/triggers', triggers) 45 | 46 | 47 | /** 48 | * Error handler 49 | */ 50 | router.use((err, _req, res, _next) => { 51 | console.log(err) 52 | res.status(err.status || 500) 53 | res.json({ error: err.message }) 54 | }) 55 | 56 | 57 | export default router 58 | -------------------------------------------------------------------------------- /src/server/routes/keys.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express' 2 | import * as _ from 'underscore' 3 | import * as Bluebird from 'bluebird' 4 | 5 | import { IAppRequest } from 'src/interfaces' 6 | import * as Controllers from '../controllers/keys' 7 | 8 | const router = Router() 9 | 10 | 11 | // add an api key for an exchange 12 | router.post('/:exchange', (req: IAppRequest, res, next) => { 13 | Controllers.setAPIKey(req.uid, req.params.exchange, req.body) 14 | .then(data => res.json(data)) 15 | .catch(next) 16 | }) 17 | 18 | 19 | // add api keys for multiple exchanges 20 | router.post('/', (req: IAppRequest, res, next) => { 21 | Bluebird.mapSeries(req.body, (data: any) => Controllers.setAPIKey(req.uid, data.exchange, data.keys)) 22 | .then(data => res.json(data)) 23 | .catch(next) 24 | }) 25 | 26 | 27 | // get all user's api keys 28 | router.get('/', async (req: IAppRequest, res, next) => { 29 | Controllers.getAllUserApiKeys(req.uid) 30 | .then(data => res.json(data)) 31 | .catch(next) 32 | }) 33 | 34 | 35 | // delete an api for an exchange 36 | router.delete('/:exchange', (req: IAppRequest, res, next) => { 37 | Controllers.deleteApiKey(req.uid, req.params.exchange) 38 | .then(data => res.json(data)) 39 | .catch(next) 40 | }) 41 | 42 | 43 | export default router 44 | -------------------------------------------------------------------------------- /src/server/routes/plugins.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express' 2 | import * as _ from 'underscore' 3 | 4 | import * as Controllers from '../controllers/plugins' 5 | import { IAppRequest } from 'src/interfaces' 6 | 7 | const router = Router() 8 | 9 | 10 | // get all plugins 11 | router.get('/', async (req: IAppRequest, res) => res.json(await Controllers.getPlugins(req.uid))) 12 | 13 | 14 | // register a plugin 15 | router.post('/:kind', async (req: IAppRequest, res) => { 16 | const uid = req.uid 17 | const kind = req.params.kind 18 | const params = req.body 19 | 20 | const plugin = await Controllers.regsiterPlugin(uid, kind, params) 21 | res.json({ plugin, success: true }) 22 | }) 23 | 24 | 25 | router.delete('/:id', async (req: IAppRequest, res) => { 26 | const uid = req.uid 27 | const id = Number(req.params.id) 28 | 29 | await Controllers.deleteplugin(uid, id) 30 | res.json({ success: true }) 31 | }) 32 | 33 | 34 | router.put('/:id', async (req: IAppRequest, res) => { 35 | const uid = req.uid 36 | const id = req.params.id 37 | 38 | await Controllers.updatePlugin(uid, id, req.body) 39 | res.json({ success: true }) 40 | }) 41 | 42 | router.post('/enabelDisable', async (req: IAppRequest, res) => res.json(await Controllers.enableDisablePlugin(req.uid, req.body.action))) 43 | router.post('/setParams', async (req: IAppRequest, res) => res.json(await Controllers.setTelegramParams(req.uid, req.body.chatId))) 44 | 45 | export default router 46 | -------------------------------------------------------------------------------- /src/server/routes/triggers.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express' 2 | import * as _ from 'underscore' 3 | 4 | import { IAppRequest } from 'src/interfaces' 5 | import * as Controllers from '../controllers/triggers' 6 | 7 | const router = Router() 8 | 9 | 10 | /** 11 | * create a new trigger for a user 12 | */ 13 | router.post('/:kind', async (req: IAppRequest, res) => { 14 | const uid = req.uid 15 | 16 | const { symbol, exchange, params } = req.body 17 | const kind = req.params.kind 18 | 19 | const trigger = await Controllers.createTrigger(uid, exchange, symbol, kind, params) 20 | res.json({ trigger, success: true }) 21 | }) 22 | 23 | 24 | /** 25 | * get all existing triggers for a user 26 | */ 27 | router.get('/', async (req: IAppRequest, res) => res.json(await Controllers.getTriggers(req.uid))) 28 | 29 | 30 | /** 31 | * Delete a specific trigger 32 | */ 33 | router.delete('/:id', async (req: IAppRequest, res) => { 34 | const uid = req.uid 35 | const id = Number(req.params.id) 36 | 37 | await Controllers.deleteTrigger(uid, id) 38 | 39 | res.json({ success: true }) 40 | }) 41 | 42 | 43 | export default router 44 | -------------------------------------------------------------------------------- /src/strategies/BaseStrategy.ts: -------------------------------------------------------------------------------- 1 | import * as hat from 'hat' 2 | import BaseExchange from '../exchanges/core/BaseExchange' 3 | 4 | 5 | /** 6 | * A strategy is a trading logic that keeps executing trades based on certain conditions. Unlike triggers, 7 | * strategies need to be stopped for it to stop executing trades. 8 | * 9 | * Useful for creating simple strategies like RSI-based strategy, MACD strategy etc.. 10 | */ 11 | export default abstract class BaseStrategy { 12 | public readonly name: string 13 | public readonly uid: string 14 | public readonly symbol: string 15 | public readonly exchange: BaseExchange 16 | 17 | 18 | constructor (name: string) { 19 | this.uid = hat() 20 | this.name = name 21 | } 22 | 23 | 24 | advice (reason: 'long' | 'short' | 'close-position' | 'do-nothing') { 25 | // do nothing 26 | } 27 | 28 | abstract process (lastprice: number): void 29 | } 30 | -------------------------------------------------------------------------------- /src/strategies/RSIStrategy.ts: -------------------------------------------------------------------------------- 1 | import BaseStrategy from './BaseStrategy' 2 | import RSI from '../indicators/RSI' 3 | import log from '../utils/log' 4 | 5 | 6 | interface ITrend { 7 | direction: 'none' | 'high' | 'low', 8 | duration: number 9 | persisted: boolean 10 | adviced: boolean 11 | } 12 | 13 | 14 | export default class RSIStrategy extends BaseStrategy { 15 | private readonly rsi: RSI 16 | 17 | private readonly thresholdHigh: number = 70 18 | private readonly thresholdLow: number = 20 19 | private readonly persistence: number = 0 20 | 21 | private trend: ITrend = { 22 | direction: 'none', 23 | duration: 0, 24 | persisted: false, 25 | adviced: false 26 | } 27 | 28 | private constructor (id: string, trigger: any) { 29 | super('RSI') 30 | 31 | this.rsi = new RSI(15) 32 | 33 | // this.requiredHistory = this.tradingAdvisor.historySize 34 | 35 | // // define the indicators we need 36 | // this.addIndicator('rsi', 'RSI', this.settings) 37 | } 38 | 39 | 40 | static create (id: string, trigger: any) { 41 | return new RSIStrategy(id, trigger) 42 | } 43 | 44 | 45 | // for debugging purposes log the last 46 | // calculated parameters. 47 | log (candle) { 48 | const digits = 8 49 | const rsi = this.rsi 50 | 51 | log.debug('calculated RSI properties for candle:') 52 | log.debug('\t', 'rsi:', rsi.result.toFixed(digits)) 53 | log.debug('\t', 'price:', candle.close.toFixed(digits)) 54 | } 55 | 56 | 57 | process (lastprice: number) { 58 | 59 | } 60 | 61 | 62 | check () { 63 | const rsi = this.rsi 64 | const rsiVal = rsi.result 65 | 66 | if (rsiVal > this.thresholdHigh) { 67 | // new trend detected 68 | if (this.trend.direction !== 'high') 69 | this.trend = { 70 | duration: 0, 71 | persisted: false, 72 | direction: 'high', 73 | adviced: false 74 | } 75 | 76 | this.trend.duration++ 77 | log.debug(`in high since ${this.trend.duration} candle(s)`) 78 | 79 | if (this.trend.duration >= this.persistence) this.trend.persisted = true 80 | 81 | if (this.trend.persisted && !this.trend.adviced) { 82 | this.trend.adviced = true 83 | this.advice('short') 84 | } else this.advice('do-nothing') 85 | 86 | } else if (rsiVal < this.thresholdLow) { 87 | // new trend detected 88 | if (this.trend.direction !== 'low') 89 | this.trend = { 90 | duration: 0, 91 | persisted: false, 92 | direction: 'low', 93 | adviced: false 94 | } 95 | 96 | this.trend.duration++ 97 | log.debug(`in low since ${this.trend.duration} candle(s)`) 98 | 99 | if (this.trend.duration >= this.persistence) this.trend.persisted = true 100 | 101 | if (this.trend.persisted && !this.trend.adviced) { 102 | this.trend.adviced = true 103 | this.advice('long') 104 | } else this.advice('do-nothing') 105 | } else { 106 | log.debug('in no trend') 107 | this.advice('do-nothing') 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/strategies/StopLossStrategy.ts: -------------------------------------------------------------------------------- 1 | import BaseStrategy from './BaseStrategy' 2 | 3 | 4 | export default class StopLossStrategy extends BaseStrategy { 5 | private constructor (id: string, trigger: any) { 6 | // do nothing... for now 7 | super('stop-loss') 8 | } 9 | 10 | 11 | static create (id: string, trigger: any) { 12 | return new StopLossStrategy(id, trigger) 13 | } 14 | 15 | 16 | process (lastprice: number) { 17 | // process the trigger 18 | 19 | // triggers.forEach(trigger => { 20 | // let strategy = trigger.strategy 21 | // let params = trigger.params 22 | // let stopLossPrice:number = params.stopLossPrice 23 | // let takeProfitPrice:number = params.takeProfitPrice 24 | // //let exchange:any = ccxt.Exchange 25 | // //console.log( stopLossPrice) 26 | // //strategy == 'stop-loss' 27 | // if (strategy == 'stop-loss' && stopLossPrice < last ) { 28 | // try { 29 | // console.log('less') 30 | // Controller.deleteTriggers('123') 31 | // } catch (error) { 32 | // console.log(error) 33 | // } 34 | // } 35 | 36 | // if ( strategy == 'take-profit' && last >= takeProfitPrice ) { 37 | // //if( last >= takeProfitPrice){ 38 | // //console.log('last', last, 'stopLossPrice', stopLossPrice ) 39 | // exchange.createMarketSellOrder() 40 | // //} 41 | // } 42 | // }) 43 | //console.log('trigger',triggers) 44 | // loop through all the avaialbe triggers for this exchange and symbol 45 | // check if the trigger conditions are met 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/triggers/BaseTrigger.ts: -------------------------------------------------------------------------------- 1 | import { Trade } from 'ccxt' 2 | import * as EventEmitter from 'events' 3 | 4 | import { ICandle, IAdvice } from 'src/interfaces' 5 | import Triggers from '../database/models/triggers' 6 | import log from '../utils/log' 7 | 8 | 9 | /** 10 | * A trigger is an event that is executed once (or multiple times) when certain 11 | * conditions are met. 12 | * 13 | * The most common examples of triggers are stop-losses and take profit triggers. 14 | * 15 | * This is an abstract, any trigger that follows this layout needs to implement the fns. below 16 | */ 17 | export default abstract class BaseTrigger extends EventEmitter { 18 | public readonly name: string 19 | protected readonly triggerDB: Triggers 20 | 21 | 22 | constructor (triggerDB: Triggers, name: string = 'Unkown') { 23 | super() 24 | this.name = name 25 | this.triggerDB = triggerDB 26 | } 27 | 28 | 29 | /** 30 | * This fn recieves every new trade made for a particular symbol & exchange. Every trigger instance 31 | * needs to have this fn implemented with it's business logic. 32 | * 33 | * Trades are sent by Budfox 34 | * 35 | * @param trade the new trade made 36 | */ 37 | public abstract onTrade (trade: Trade): void 38 | 39 | 40 | /** 41 | * This fn recieves every new candle made for a particular symbol & exchange. Every trigger instance 42 | * needs to have this fn implemented with it's business logic. 43 | * 44 | * Candles are emitted by Budfox 45 | * 46 | * @param candle the new candle emitted 47 | */ 48 | public abstract onCandle (candle: ICandle): void 49 | 50 | 51 | public getExchange () { 52 | return this.triggerDB.exchange 53 | } 54 | 55 | 56 | public getSymbol () { 57 | return this.triggerDB.symbol 58 | } 59 | 60 | 61 | public getUID () { 62 | return this.triggerDB.uid 63 | } 64 | 65 | 66 | public getDBId () { 67 | return this.triggerDB.id 68 | } 69 | 70 | 71 | public isLive () { 72 | return this.triggerDB.isActive 73 | } 74 | 75 | 76 | protected advice (advice: IAdvice, params?: { price?: number, amount?: number, orderId?: string }) { 77 | if (!this.isLive()) return 78 | 79 | // do nothing 80 | const trigger = this.triggerDB 81 | log.info( 82 | `${trigger.kind} trigger for user ${trigger.uid} on ${trigger.exchange} ${trigger.symbol} ` + 83 | `adviced to ${advice} at ${params.price} for an amount of ${params.amount}` 84 | ) 85 | 86 | // mark the trigger as triggered 87 | trigger.hasTriggered = true 88 | trigger.lastTriggeredAt = new Date 89 | trigger.save() 90 | 91 | this.emit('advice', { advice, ...params }) 92 | } 93 | 94 | 95 | protected close () { 96 | if (!this.isLive()) return 97 | 98 | const trigger = this.triggerDB 99 | log.info(`${trigger.kind} trigger for user ${trigger.uid} on ${trigger.exchange} ${trigger.symbol} closed`) 100 | 101 | this.emit('close') 102 | 103 | // mark the trigger as closed in the DB 104 | this.triggerDB.isActive = false 105 | this.triggerDB.closedAt = new Date 106 | this.triggerDB.save() 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/triggers/CancelOrderTrigger.ts: -------------------------------------------------------------------------------- 1 | import { Trade } from 'ccxt' 2 | 3 | import { ICandle } from '../interfaces' 4 | import BaseTrigger from './BaseTrigger' 5 | import Triggers from '../database/models/triggers' 6 | import { isNumber } from 'util' 7 | 8 | 9 | /** 10 | * Cancel order trigger enables to cancel an order at various conditons which 11 | * would enable the user to have buy and sell walls 12 | */ 13 | 14 | export default class CancelOrderTrigger extends BaseTrigger { 15 | private readonly orderId: string 16 | private readonly condition: 'greater-than'| 'greater-than-equal' | 'less-than' | 'less-than-equal' 17 | private readonly targetPrice: number 18 | 19 | 20 | constructor(trigger: Triggers) { 21 | super(trigger, 'Cancel Order') 22 | 23 | const params = JSON.parse(trigger.params) 24 | this.orderId = params.orderId 25 | this.condition = params.condition 26 | this.targetPrice = params.price 27 | 28 | 29 | // check for missing condition 30 | if (['greater-than', 'greater-than-equal', 'less-than', 'less-than-equal'].indexOf(this.condition) === -1) 31 | throw new Error('bad/missing condition') 32 | 33 | if (!this.orderId) throw new Error('bad/missing order id') 34 | if (!this.targetPrice || !isNumber(this.targetPrice)) throw new Error('bad/missing price') 35 | } 36 | 37 | 38 | onTrade(trade: Trade) { 39 | // if the prices are not live than return 40 | if (!this.isLive()) return 41 | 42 | // get current price 43 | const { price } = trade 44 | 45 | // if the price satisfies the target price and condition pair then 46 | // trigger an cancel order request 47 | if ( 48 | price <= this.targetPrice && this.condition.toString() === 'less-than-equal' || 49 | price < this.targetPrice && this.condition.toString() === 'less-than' || 50 | price >= this.targetPrice && this.condition.toString() === 'greater-than-equal' || 51 | price > this.targetPrice && this.condition.toString() === 'greater-than' 52 | ) { 53 | // emit a cancel order adivce for advice manager 54 | this.advice('cancel-order', { orderId: this.orderId }) 55 | 56 | // emit a close event for the trigger 57 | this.close() 58 | } 59 | } 60 | 61 | 62 | onCandle(candle: ICandle) { 63 | // do nothing 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/triggers/DynamicTieredTakeProfitTrigger.ts: -------------------------------------------------------------------------------- 1 | import { Trade } from "ccxt"; 2 | import { ICandle } from "../interfaces"; 3 | import BaseTrigger from "./BaseTrigger"; 4 | import Triggers from "../database/models/triggers"; 5 | import TrailingStopTrigger from "./TrailingStopTrigger"; 6 | 7 | /** 8 | * Tiered take profit trigger enables the user to create triggers to sell on 9 | * profit at multiple tiers at n steps to take 1/n part of profit of target 10 | */ 11 | 12 | export default class DynamicTieredTakeProfitTrigger extends BaseTrigger { 13 | private readonly action: "sell"; 14 | private readonly type: "limit" | "market"; 15 | private readonly steps: number; 16 | private readonly params: any; 17 | 18 | constructor(trigger: Triggers) { 19 | super(trigger, "Dynamic tiered Trigger"); 20 | 21 | const params = JSON.parse(trigger.params); 22 | this.action = params.action; 23 | 24 | this.type = params.type; 25 | // tiers for take profit 26 | this.steps = params.steps; 27 | this.params = params; 28 | 29 | if (this.action !== "sell") throw new Error('bad/missing action'); 30 | if (this.type !== "market" && this.type !== "limit") 31 | throw new Error('bad/missing type'); 32 | if (!this.steps && typeof this.steps !== "number") 33 | throw new Error('bad/missing steps or invalid type'); 34 | } 35 | 36 | // Update params after partial execution is achived 37 | onPartialExecution(data) { 38 | this.triggerDB.params = JSON.stringify({ 39 | ...this.params, 40 | executedSteps: { 41 | ...data 42 | } 43 | }); 44 | } 45 | 46 | onTrade(trade: Trade) { 47 | if (!this.isLive()) return; 48 | 49 | const { price } = trade; 50 | 51 | // Amount to be traded after reaching a tier 52 | const amount = this.triggerDB.amount / this.steps; 53 | // Amount left for the last tier 54 | // TODO: What will be remaning amount 55 | const remainingAmount = this.triggerDB.amount - ( 56 | amount * (this.steps - 1)) 57 | // The amount between targer price and price at which trigger was created 58 | const priceDelta = this.triggerDB.targetPrice - this.triggerDB.createdAtPrice 59 | // Price for the first step or tier 60 | const firstStep = this.triggerDB.createdAtPrice + (priceDelta / this.steps) 61 | 62 | // trigger a maket or limit sell when price crosses the first tier 63 | // and this condition is achieved for the first time 64 | if (price >= this.triggerDB.targetPrice) { 65 | if (this.type === "market") this.advice('market-sell', price, remainingAmount); 66 | if (this.type === "limit") this.advice('limit-sell', price, remainingAmount); 67 | this.close(); 68 | } else { 69 | // other conditions 70 | if (price >= firstStep && price < (this.triggerDB.targetPrice - (priceDelta / this.steps))) { 71 | const priceDelta = price - this.triggerDB.createdAtPrice 72 | const currentStep = Math.floor(priceDelta / amount) 73 | // check if current step was previously executed 74 | 75 | if (this.params.executedSteps[currentStep]) return 76 | 77 | if (this.type === "market") this.advice('market-sell', price, amount); 78 | if (this.type === "limit") this.advice('limit-sell', price, amount); 79 | 80 | // Update params on partial execution 81 | this.onPartialExecution({ ...this.params.executedSteps, [currentStep]: true }); 82 | } 83 | } 84 | } 85 | 86 | onCandle(_candel: ICandle) { 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/triggers/FutureOrderTrigger.ts: -------------------------------------------------------------------------------- 1 | import { Trade } from "ccxt"; 2 | import { ICandle } from "../interfaces"; 3 | import BaseTrigger from "./BaseTrigger"; 4 | import Triggers from "../database/models/triggers"; 5 | import * as _ from "underscore"; 6 | 7 | /** 8 | * Future order trigger to enable users to execute an order after certain 9 | * amount of time and on certian condition 10 | */ 11 | 12 | export default class FutureOrderTrigger extends BaseTrigger { 13 | private readonly action: "buy" | "sell"; 14 | private readonly type: "market" | "limit"; 15 | private readonly condition: [{ 16 | [s: string]: number | string, 17 | type: "less-than" | "greater-than" 18 | }] 19 | 20 | constructor(trigger: Triggers) { 21 | super(trigger, "future order"); 22 | 23 | const params = JSON.parse(trigger.params); 24 | 25 | this.action = params.action; 26 | this.type = params.type; 27 | this.condition = params.condition; 28 | 29 | if (this.action !== "sell" && this.action !== "buy") 30 | throw new Error('bad/missing action'); 31 | 32 | if (this.type !== "market" && this.type !== "limit") 33 | throw new Error('bad/missing type'); 34 | 35 | if (!this.condition || !(this.condition.length > 0)) 36 | throw new Error('bad/missing condition') 37 | 38 | // check type 39 | const typeCheck = this.condition.map( 40 | item => item['type'] === 'less-than' || 41 | item['type'] === 'greater-than').reduce((a, c) => a && c); 42 | 43 | if (!typeCheck) throw new Error('bad/bad type in condition'); 44 | 45 | // check values 46 | const valueCheck = this.condition.map( 47 | item => typeof item['Date'] === 'number' || 48 | typeof item['Price'] === 'number' || 49 | typeof item['Volume'] === 'number').reduce((a, c) => a && c); 50 | 51 | if (!valueCheck) throw new Error('bad/bad value in condition'); 52 | 53 | } 54 | 55 | onTrade(trade: Trade) { 56 | if (!this.isLive()) return 57 | 58 | const { price } = trade 59 | const volume = this.triggerDB.amount * price 60 | const time = new Date().getTime() 61 | 62 | // handle less than condition 63 | const lessThan = this.condition.filter(item => item['type'] === 'less-than'); 64 | const greaterThan = this.condition.filter(item => item['type'] === 'greater-than'); 65 | 66 | // for date condition 67 | const dateCondtion = this.condition.filter(item => item['Date']) 68 | .map(item => item['Date'])[0] 69 | // get price conditon 70 | const priceCondtion = this.condition.filter(item => item['Price']) 71 | .map(item => item['Price'])[0] 72 | // get volume condtion 73 | const volumeCondtion = this.condition.filter(item => item['Volume']) 74 | .map(item => item['Volume'])[0] 75 | 76 | if (lessThan.length > 0) { 77 | // When any of the conditon arrives create order 78 | if (dateCondtion && time <= dateCondtion || 79 | priceCondtion && price <= priceCondtion || 80 | volumeCondtion && volume <= volumeCondtion) { 81 | if (this.type === 'limit') this.advice('limit-buy', price, this.triggerDB.amount) 82 | if (this.type === 'market') this.advice('market-buy', price, this.triggerDB.amount) 83 | this.close() 84 | } 85 | } 86 | 87 | // handle greater than condition 88 | 89 | if (greaterThan.length > 0) { 90 | // When any of the conditon arrives create order 91 | if (dateCondtion && time >= dateCondtion || 92 | priceCondtion && price >= priceCondtion || 93 | volumeCondtion && volume >= volumeCondtion) { 94 | if (this.type === 'limit') this.advice('limit-buy', price, this.triggerDB.amount) 95 | if (this.type === 'market') this.advice('market-buy', price, this.triggerDB.amount) 96 | this.close() 97 | } 98 | } 99 | } 100 | 101 | onCandle(_candel: ICandle) {} 102 | } 103 | 104 | -------------------------------------------------------------------------------- /src/triggers/StopLossTakeProfitTrigger.ts: -------------------------------------------------------------------------------- 1 | import { Trade } from 'ccxt' 2 | 3 | import { ICandle } from '../interfaces' 4 | import BaseTrigger from './BaseTrigger' 5 | import Triggers from '../database/models/triggers' 6 | 7 | 8 | /** 9 | * A stop loss take profit trigger, triggers a buy/sell order when the price exceeds above or drops below a 10 | * certain point. 11 | */ 12 | export default class StopLossTakeProfitTrigger extends BaseTrigger { 13 | private readonly action: 'market-buy' | 'market-sell' | 'limit-buy' | 'limit-sell' 14 | private readonly amount: number 15 | private readonly stopLossPrice: number 16 | private readonly takeProfitPrice: number 17 | 18 | 19 | constructor (trigger: Triggers) { 20 | super(trigger, 'Stop Loss & Take Proft') 21 | 22 | const params = JSON.parse(trigger.params) 23 | this.action = params.action 24 | this.amount = params.amount 25 | this.stopLossPrice = params.stopPrice 26 | this.takeProfitPrice = params.takePrice 27 | 28 | if (this.action !== 'market-buy' && this.action !== 'market-sell' && 29 | this.action !== 'limit-buy' && this.action !== 'limit-sell') 30 | throw new Error('bad/missing action') 31 | if (!this.stopLossPrice) throw new Error('bad/missing stoploss price') 32 | if (!this.takeProfitPrice) throw new Error('bad/missing take profit price') 33 | } 34 | 35 | 36 | onTrade (trade: Trade) { 37 | const { price } = trade 38 | console.log(price, this.action) 39 | if (!this.isLive()) return 40 | 41 | if (this.action.endsWith('buy')) { 42 | // if we go short, then we place buy orders to close our position 43 | 44 | // buy for stoploss 45 | 46 | // if price reaches or goes below the stop loss price, then 47 | // we close the position with a buy order 48 | if (price >= this.stopLossPrice) { 49 | this.advice(this.action, { price, amount: this.amount }) 50 | this.close() 51 | } 52 | 53 | // buy for take profit 54 | 55 | // if price reaches or goes below the take profit price, then 56 | // we close the position with a sell order 57 | if (price <= this.takeProfitPrice) { 58 | this.advice(this.action, { price, amount: this.amount }) 59 | this.close() 60 | } 61 | } else if (this.action.endsWith('sell')) { 62 | // if we go long, then we place sell orders to close our position 63 | 64 | console.log(price, 'stop', price <= this.stopLossPrice, this.stopLossPrice, 'take', price >= this.takeProfitPrice, this.takeProfitPrice) 65 | // sell for stoploss 66 | 67 | // if price reaches or goes above the stop loss price, then 68 | // we close the position with a sell order 69 | if (price <= this.stopLossPrice) { 70 | this.advice(this.action, { price, amount: this.amount }) 71 | this.close() 72 | } 73 | 74 | // sell for take profit 75 | 76 | // if price reaches or goes above the take profit price, then 77 | // we close the position with a sell order 78 | if (price >= this.takeProfitPrice) { 79 | this.advice(this.action, { price, amount: this.amount }) 80 | this.close() 81 | } 82 | } 83 | } 84 | 85 | 86 | onCandle (_candle: ICandle) { 87 | // do nothing 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/triggers/StopLossTrigger.ts: -------------------------------------------------------------------------------- 1 | import { Trade } from 'ccxt' 2 | 3 | import { ICandle } from '../interfaces' 4 | import BaseTrigger from './BaseTrigger' 5 | import Triggers from '../database/models/triggers' 6 | import { isNumber } from 'util' 7 | 8 | 9 | /** 10 | * A stop loss trigger, triggers a buy/sell order when the price exceeds above or drops below a 11 | * certain point. 12 | */ 13 | export default class StopLossTrigger extends BaseTrigger { 14 | private readonly action: 'market-buy' | 'market-sell' | 'limit-buy' | 'limit-sell' 15 | private readonly amount: number 16 | private readonly price: number 17 | 18 | 19 | constructor (trigger: Triggers) { 20 | super(trigger, 'Stop Loss') 21 | 22 | const params = JSON.parse(trigger.params) 23 | this.action = params.action 24 | this.amount = params.amount 25 | this.price = params.price 26 | 27 | if (this.action !== 'market-buy' && this.action !== 'market-sell' && 28 | this.action !== 'limit-buy' && this.action !== 'limit-sell') 29 | throw new Error('bad/missing action') 30 | if (this.price && !isNumber(this.price)) throw new Error('bad price') 31 | if (!this.amount || !isNumber(this.amount)) throw new Error('bad/missing amount') 32 | } 33 | 34 | 35 | onTrade (trade: Trade) { 36 | if (!this.isLive()) return 37 | const { price } = trade 38 | 39 | // if price reaches or goes below the stop loss price, then 40 | // we close the position with a buy order 41 | if (this.action.endsWith('buy') && price >= this.price) { 42 | this.advice(this.action, { price, amount: this.amount }) 43 | this.close() 44 | } 45 | 46 | // if price reaches or goes above the stop loss price, then 47 | // we close the position with a sell order 48 | if (this.action.endsWith('sell') && price <= this.price) { 49 | this.advice(this.action, { price, amount: this.amount }) 50 | this.close() 51 | } 52 | } 53 | 54 | 55 | onCandle (_candle: ICandle) { 56 | // do nothing 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/triggers/TakeProfitTrigger.ts: -------------------------------------------------------------------------------- 1 | import { Trade } from 'ccxt' 2 | 3 | import { ICandle } from '../interfaces' 4 | import BaseTrigger from './BaseTrigger' 5 | import Triggers from '../database/models/triggers' 6 | import { isNumber } from 'util' 7 | 8 | 9 | export default class TakeProfitTrigger extends BaseTrigger { 10 | private readonly action: 'market-buy' | 'market-sell' | 'limit-buy' | 'limit-sell' 11 | private readonly amount: number 12 | private readonly price: number 13 | 14 | 15 | constructor (trigger: Triggers) { 16 | super(trigger, 'Take Profit') 17 | 18 | const params = JSON.parse(trigger.params) 19 | this.action = params.action 20 | this.amount = params.amount 21 | this.price = params.price 22 | 23 | if (this.action !== 'market-buy' && this.action !== 'market-sell' && 24 | this.action !== 'limit-buy' && this.action !== 'limit-sell') 25 | throw new Error('bad/missing action') 26 | if (this.price && !isNumber(this.price)) throw new Error('bad price') 27 | if (!this.amount || !isNumber(this.amount)) throw new Error('bad/missing amount') 28 | } 29 | 30 | 31 | onTrade (trade: Trade) { 32 | if (!this.isLive) return 33 | const { price } = trade 34 | 35 | // if price reaches or goes below the take profit price, then 36 | // we close the position with a sell order 37 | if (this.action.endsWith('buy') && price <= this.price) { 38 | this.advice(this.action, { price, amount: this.amount }) 39 | this.close() 40 | } 41 | 42 | // if price reaches or goes above the take profit price, then 43 | // we close the position with a sell order 44 | if (this.action.endsWith('sell') && price >= this.price) { 45 | this.advice(this.action, { price, amount: this.amount }) 46 | this.close() 47 | } 48 | } 49 | 50 | 51 | onCandle (_candle: ICandle) { 52 | // do nothing 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/triggers/TieredTakeProfitTrigger.ts: -------------------------------------------------------------------------------- 1 | import { Trade } from "ccxt"; 2 | import { ICandle } from "../interfaces"; 3 | import BaseTrigger from "./BaseTrigger"; 4 | import Triggers from "../database/models/triggers"; 5 | 6 | /** 7 | * Tiered take profit trigger enables the user to create triggers to sell on 8 | * profit at multiple tiers at 33 % steps 9 | */ 10 | 11 | interface Params { 12 | action: string, 13 | type: string, 14 | steps: number, 15 | executedSteps: { 16 | [key: string]: boolean } }; 17 | 18 | export default class TieredTakeProfitTrigger extends BaseTrigger { 19 | private readonly action: "sell"; 20 | private readonly type: "market" | "limit"; 21 | private readonly params: Params; 22 | 23 | constructor(trigger: Triggers) { 24 | super(trigger, "Tiered Profits"); 25 | 26 | const params = JSON.parse(trigger.params); 27 | 28 | this.action = params.action; 29 | this.type = params.type; 30 | this.params = params; 31 | 32 | if (this.action !== "sell") throw new Error('bad/missing action'); 33 | if (this.type !== "market" && this.type !== "limit") 34 | throw new Error('bad/missing type'); 35 | } 36 | 37 | onPartialExecution(data) { 38 | this.triggerDB.params = JSON.stringify({ 39 | ...this.params, 40 | executedSteps: { 41 | ...data 42 | } 43 | }); 44 | } 45 | 46 | onTrade(trade: Trade) { 47 | if (!this.isLive()) return; 48 | 49 | const { price } = trade; 50 | const { createdAtPrice, targetPrice } = this.triggerDB 51 | const priceDelta = targetPrice - createdAtPrice 52 | 53 | // price for the first tier or step 54 | const firstStep = createdAtPrice + (0.33 * priceDelta); 55 | // price for the second tier or step 56 | const secondStep = createdAtPrice + (0.66 * priceDelta); 57 | // the profit amount for for the first & second tier or step 58 | const amount = 0.33 * this.triggerDB.amount; 59 | // the profit amount when target priced is achived 60 | const remainingAmount = this.triggerDB.amount - (2 * amount); 61 | 62 | // trigger a maket or limit sell when price crosses the first tier 63 | // and this condition is achieved for the first time 64 | if (price >= firstStep && price < secondStep 65 | && this.params.executedSteps[1] === false) { 66 | if (this.type === "market") this.advice('market-sell', price, amount); 67 | if (this.type === "limit") this.advice('limit-sell', price, amount); 68 | // TODO: add fields to check weather the trigger was partiall executed 69 | 70 | this.onPartialExecution({ ...this.params.executedSteps, 1: true }); 71 | } 72 | 73 | // trigger a maket or limit sell when price crosses the second tier 74 | // and this condition is achieved for the first time 75 | if (price >= secondStep && price < this.triggerDB.targetPrice 76 | && this.params.executedSteps[2] === false 77 | && this.params.executedSteps[1] === true) { 78 | if (this.type === "market") this.advice('market-sell', price, amount); 79 | if (this.type === "limit") this.advice('limit-sell', price, amount); 80 | // TODO: add fields to check weather the trigger was partiall executed 81 | 82 | this.onPartialExecution({ ...this.params.executedSteps, 2: true }) 83 | } 84 | 85 | // trigger a maket or limit sell when target Price is achived 86 | // and this condition is achieved for the first time 87 | if (price >= this.triggerDB.targetPrice 88 | && this.params.executedSteps[3] === false 89 | && this.params.executedSteps[1] === true 90 | && this.params.executedSteps[2] === true) { 91 | if (this.type === "market") this.advice('market-sell', price, remainingAmount); 92 | if (this.type === "limit") this.advice('limit-sell', price, remainingAmount); 93 | 94 | this.onPartialExecution({ ...this.params.executedSteps, 3: true }) 95 | 96 | this.close(); 97 | } 98 | } 99 | 100 | onCandle(_candle: ICandle) { 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/triggers/TrailingStopTrigger.ts: -------------------------------------------------------------------------------- 1 | import { ICandle } from '../interfaces' 2 | import BaseTrigger from './BaseTrigger' 3 | import Triggers from '../database/models/triggers' 4 | import { Trade } from 'ccxt' 5 | 6 | 7 | export default class TrailingStopTrigger extends BaseTrigger { 8 | private readonly amount: number 9 | private readonly trail: number 10 | private trailingPoint: number 11 | 12 | 13 | /** 14 | * Note: as of now only supports trailing the price going up (after 15 | * a buy), on trigger (when the price moves down) you should sell. 16 | * 17 | * @param trail fixed offset from the price 18 | * @param initialPrice initial price, preferably buy price 19 | */ 20 | constructor (trigger: Triggers) { 21 | super(trigger, 'Trailing Stop') 22 | 23 | const params = JSON.parse(trigger.params) 24 | this.trail = params.trail 25 | this.amount = params.amount 26 | this.trailingPoint = params.initialPrice - this.trail 27 | } 28 | 29 | 30 | onTrade (trade: Trade) { 31 | if (!this.isLive()) return 32 | 33 | const { price } = trade 34 | if (price > this.trailingPoint + this.trail) this.trailingPoint = price - this.trail 35 | 36 | if (price <= this.trailingPoint) { 37 | this.advice('close-position', { price, amount: this.amount }) 38 | this.close() 39 | } 40 | } 41 | 42 | 43 | onCandle (_candle: ICandle) { 44 | // do nothing 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export const die = (m: string, soft: boolean = false) => { 2 | // if(_gekkoEnv === 'child-process') { 3 | // return process.send({ type: 'error', error: '\n ERROR: ' + m + '\n' }) 4 | // } 5 | 6 | const log = console.log.bind(console) 7 | 8 | if(m) { 9 | if (soft) log('\n ERROR: ' + m + '\n\n') 10 | else { 11 | log(`\nGekko encountered an error and can\'t continue`) 12 | log('\nError:\n') 13 | log(m, '\n\n') 14 | log('\nMeta debug info:\n') 15 | // log(util.logVersion()) 16 | log('') 17 | } 18 | } 19 | 20 | process.exit(1) 21 | } 22 | -------------------------------------------------------------------------------- /src/utils/log.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Lightweight logger, print everything that is send to error, warn 3 | * and messages to stdout (the terminal). If config.debug is set in config 4 | * also print out everything send to debug. 5 | */ 6 | import * as _ from 'underscore' 7 | 8 | const moment = require('moment') 9 | const fmt = require('util').format 10 | 11 | // todo: get from config 12 | const debug = true 13 | const silent = false 14 | 15 | class Log { 16 | private env: string 17 | private output: any 18 | 19 | constructor () { 20 | this.env = 'standalone' // util.gekkoEnv() 21 | 22 | if (this.env === 'standalone') this.output = console 23 | else if (this.env === 'child-process') this.output = this.sendToParent() 24 | } 25 | 26 | 27 | public error (...args) { 28 | this._write('error', args) 29 | } 30 | 31 | 32 | public warn (...args) { 33 | this._write('warn', args) 34 | } 35 | 36 | 37 | public info (...args) { 38 | this._write('info', args) 39 | } 40 | 41 | 42 | public debug (...args) { 43 | if (!debug) return 44 | this._write('debug', args) 45 | } 46 | 47 | 48 | private _write (method: string, args: any[], name?: string) { 49 | if (silent) return 50 | if (!name) name = method.toUpperCase() 51 | 52 | let message = moment().format('YYYY-MM-DD HH:mm:ss') 53 | message += ' (' + name + '):\t' 54 | message += fmt.apply(null, args) 55 | 56 | this.output[method](message) 57 | } 58 | 59 | 60 | private sendToParent = function() { 61 | const send = method => (...args) => { 62 | process.send({ log: method, message: args.join(' ') }) 63 | } 64 | 65 | return { 66 | error: send('error'), 67 | warn: send('warn'), 68 | info: send('info'), 69 | write: send('write') 70 | } 71 | } 72 | } 73 | 74 | 75 | export default new Log 76 | -------------------------------------------------------------------------------- /test/Trigger/cancelOrderTriggerTests.ts: -------------------------------------------------------------------------------- 1 | import * as data from "../data"; 2 | import { expect } from "chai"; 3 | import CancelOrderTrigger from "../../src/triggers/CancelOrderTrigger"; 4 | 5 | export default describe("Cancel Order Trigger Tests", async function() { 6 | let trigger, trade; 7 | 8 | it("check & validate for cancel profit trigger for less-than-equal", done => { 9 | trigger = { 10 | ...data.default.trigger, 11 | params: '{ "action": "cancel", "condition": "less-than-equal" }' }; 12 | trade = { 13 | ...data.default.trade, 14 | price: 0.09 }; 15 | 16 | const cancelOrder = new CancelOrderTrigger(trigger); 17 | 18 | cancelOrder.on("advice", data => { 19 | expect(data).to.deep.equal( 20 | { advice: "cancel-order", price: 0.09, amount: 5 }) 21 | done(); 22 | }) 23 | 24 | cancelOrder.onTrade(trade); 25 | }) 26 | 27 | it("check & validate for cancel profit trigger for less-than", done => { 28 | trigger = { 29 | ...data.default.trigger, 30 | params: '{ "action": "cancel", "condition": "less-than" }' }; 31 | trade = { 32 | ...data.default.trade, 33 | price: 0.09 }; 34 | 35 | const cancelOrder = new CancelOrderTrigger(trigger); 36 | 37 | cancelOrder.on("advice", data => { 38 | console.log("in advice") 39 | expect(data).to.deep.equal( 40 | { advice: "cancel-order", price: 0.09, amount: 5 }) 41 | done(); 42 | }) 43 | 44 | cancelOrder.onTrade(trade); 45 | }) 46 | 47 | it("check & validate for cancel profit trigger for greater-than-equal", done => { 48 | trigger = { 49 | ...data.default.trigger, 50 | params: '{ "action": "cancel", "condition": "greater-than-equal" }' }; 51 | trade = { 52 | ...data.default.trade, 53 | price: 0.11 }; 54 | 55 | const cancelOrder = new CancelOrderTrigger(trigger); 56 | 57 | cancelOrder.on("advice", data => { 58 | expect(data).to.deep.equal( 59 | { advice: "cancel-order", price: 0.11, amount: 5 }) 60 | done(); 61 | }) 62 | 63 | cancelOrder.onTrade(trade); 64 | }) 65 | 66 | it("check & validate for cancel profit trigger for greater-than", done => { 67 | trigger = { 68 | ...data.default.trigger, 69 | params: '{ "action": "cancel", "condition": "greater-than" }' }; 70 | trade = { 71 | ...data.default.trade, 72 | price: 0.11 }; 73 | 74 | const cancelOrder = new CancelOrderTrigger(trigger); 75 | 76 | cancelOrder.on("advice", data => { 77 | expect(data).to.deep.equal( 78 | { advice: "cancel-order", price: 0.11, amount: 5 }) 79 | done(); 80 | }) 81 | 82 | cancelOrder.onTrade(trade); 83 | }) 84 | }) 85 | -------------------------------------------------------------------------------- /test/Trigger/dynamicTieredTakeProfitTriggerTests.ts: -------------------------------------------------------------------------------- 1 | import * as data from "../data"; 2 | import { expect } from "chai"; 3 | import DynamicTieredTakeProfitTrigger from "../../src/triggers/DynamicTieredTakeProfitTrigger"; 4 | 5 | export default describe("Dynamic Tiered Take Profit Trigger Tests", async function() { 6 | let trigger, trade; 7 | 8 | it(`Should check & validate dynamic tiered take profit trigger 9 | when price is 1/nth of target price for sell market order`, done => { 10 | trigger = { 11 | ...data.default.trigger, 12 | params: JSON.stringify({ 13 | action: "sell", 14 | type: "market", 15 | steps: 5, 16 | executedSteps: { 17 | 1: false, 18 | 2: false, 19 | 3: false, 20 | 4: false, 21 | 5: false } 22 | })}; 23 | 24 | const priceDelta = trigger.targetPrice - trigger.createdAtPrice 25 | 26 | const price = trigger.createdAtPrice + (2 * (priceDelta / 5)) 27 | const amount = 0.2 * trigger.amount; 28 | 29 | trade = { 30 | ...data.default.trade, 31 | price 32 | } 33 | 34 | const tieredTakeProfit = new DynamicTieredTakeProfitTrigger(trigger); 35 | 36 | tieredTakeProfit.on("advice", data => { 37 | expect(data).to.deep.equal( 38 | { advice: "market-sell", price, amount }) 39 | done(); 40 | }) 41 | 42 | tieredTakeProfit.onTrade(trade); 43 | }) 44 | 45 | it(`Should check & validate dynamic tiered take profit trigger 46 | when price is greater than target price for sell limit order`, done => { 47 | trigger = { 48 | ...data.default.trigger, 49 | params: JSON.stringify({ 50 | action: "sell", 51 | type: "limit", 52 | steps: 5, 53 | executedSteps: { 54 | 1: false, 55 | 2: false, 56 | 3: false, 57 | 4: false, 58 | 5: false } 59 | })}; 60 | 61 | const priceDelta = trigger.targetPrice - trigger.createdAtPrice 62 | 63 | const price = trigger.createdAtPrice + (2 * (priceDelta / 5)) 64 | const amount = 0.2 * trigger.amount; 65 | 66 | trade = { 67 | ...data.default.trade, 68 | price 69 | } 70 | 71 | const tieredTakeProfit = new DynamicTieredTakeProfitTrigger(trigger); 72 | 73 | tieredTakeProfit.on("advice", data => { 74 | expect(data).to.deep.equal( 75 | { advice: "limit-sell", price, amount }) 76 | done(); 77 | }) 78 | 79 | tieredTakeProfit.onTrade(trade); 80 | }) 81 | 82 | it(`Should check & validate dynamic tiered take profit trigger 83 | when price is greater than target price for sell limit order`, done => { 84 | trigger = { 85 | ...data.default.trigger, 86 | params: '{ "action": "sell", "type": "limit", "steps": 5 }'}; 87 | 88 | const price = trigger.targetPrice; 89 | const steps = JSON.parse(trigger.params).steps; 90 | const amount = trigger.amount / steps 91 | 92 | const remainingAmount = trigger.amount - (amount * (steps - 1)) 93 | 94 | trade = { 95 | ...data.default.trade, 96 | price }; 97 | 98 | const tieredTakeProfit = new DynamicTieredTakeProfitTrigger(trigger); 99 | 100 | tieredTakeProfit.on("advice", data => { 101 | expect(data).to.deep.equal( 102 | { advice: "limit-sell", price, amount: remainingAmount }) 103 | done(); 104 | }) 105 | 106 | tieredTakeProfit.onTrade(trade); 107 | }) 108 | 109 | it(`Should check & validate dynamic tiered take profit trigger 110 | when price is greater than target price for sell market order`, done => { 111 | trigger = { 112 | ...data.default.trigger, 113 | params: '{ "action": "sell", "type": "market", "steps": 5 }'}; 114 | 115 | const price = trigger.targetPrice; 116 | const steps = JSON.parse(trigger.params).steps; 117 | const amount = trigger.amount / steps 118 | 119 | const remainingAmount = trigger.amount - (amount * (steps - 1)) 120 | 121 | trade = { 122 | ...data.default.trade, 123 | price }; 124 | 125 | const tieredTakeProfit = new DynamicTieredTakeProfitTrigger(trigger); 126 | 127 | tieredTakeProfit.on("advice", data => { 128 | expect(data).to.deep.equal( 129 | { advice: "market-sell", price, amount: remainingAmount }) 130 | done(); 131 | }) 132 | 133 | tieredTakeProfit.onTrade(trade); 134 | }) 135 | }) 136 | -------------------------------------------------------------------------------- /test/Trigger/futureOrderTriggerTests.ts: -------------------------------------------------------------------------------- 1 | import * as data from "../data"; 2 | import { expect } from "chai"; 3 | import FutureOrderTrigger from '../../src/triggers/FutureOrderTrigger'; 4 | 5 | export default describe("Future Order Trigger Tests", async function() { 6 | let trigger, trade 7 | 8 | it(`check & validate for future order with greater than date condtion market 9 | buy advice`, done => { 10 | // get dummy data to check trigger for 11 | trade = { ...data.default.trade }; 12 | trigger = { 13 | ...data.default.trigger, 14 | params: `{ 15 | "action": "buy", 16 | "type": "market", 17 | "condition": [{ 18 | "Date": 1566466231883, 19 | "type": "greater-than" 20 | }] 21 | }` 22 | }; 23 | 24 | // Get instance of trigger for dummy data 25 | const futureOrder = new FutureOrderTrigger(trigger); 26 | 27 | // check if advice event is emitted and check its value 28 | futureOrder.on("advice", data => { 29 | expect(data).to.deep.equal( 30 | { advice: 'market-buy', price: 1, amount: 5 }) 31 | done(); 32 | }); 33 | 34 | // Call an trade event 35 | futureOrder.onTrade(trade); 36 | }) 37 | 38 | it(`check & validate for future order with greater than date condtion market 39 | buy advice`, done => { 40 | // get dummy data to check trigger for 41 | trade = { ...data.default.trade }; 42 | trigger = { 43 | ...data.default.trigger, 44 | params: `{ 45 | "action": "buy", 46 | "type": "limit", 47 | "condition": [{ 48 | "Date": 1666476451883, 49 | "type": "less-than" 50 | }] 51 | }` 52 | }; 53 | 54 | // Get instance of trigger for dummy data 55 | const futureOrder = new FutureOrderTrigger(trigger); 56 | 57 | // check if advice event is emitted and check its value 58 | futureOrder.on("advice", data => { 59 | expect(data).to.deep.equal( 60 | { advice: 'limit-buy', price: 1, amount: 5 }) 61 | done(); 62 | }); 63 | 64 | // Call an trade event 65 | futureOrder.onTrade(trade); 66 | }) 67 | 68 | it(`check & validate for future order with greater than price condtion market 69 | buy advice`, done => { 70 | // get dummy data to check trigger for 71 | trade = { ...data.default.trade }; 72 | trigger = { 73 | ...data.default.trigger, 74 | params: `{ 75 | "action": "buy", 76 | "type": "market", 77 | "condition": [{ 78 | "Price": 0.1, 79 | "type": "greater-than" 80 | }] 81 | }` 82 | }; 83 | 84 | // Get instance of trigger for dummy data 85 | const futureOrder = new FutureOrderTrigger(trigger); 86 | 87 | // check if advice event is emitted and check its value 88 | futureOrder.on("advice", data => { 89 | expect(data).to.deep.equal( 90 | { advice: 'market-buy', price: 1, amount: 5 }) 91 | done(); 92 | }); 93 | 94 | // Call an trade event 95 | futureOrder.onTrade(trade); 96 | }) 97 | 98 | it(`check & validate for future order with greater than price condtion market 99 | buy advice`, done => { 100 | // get dummy data to check trigger for 101 | trade = { ...data.default.trade }; 102 | trigger = { 103 | ...data.default.trigger, 104 | params: `{ 105 | "action": "buy", 106 | "type": "limit", 107 | "condition": [{ 108 | "Price": 1.5, 109 | "type": "less-than" 110 | }] 111 | }` 112 | }; 113 | 114 | // Get instance of trigger for dummy data 115 | const futureOrder = new FutureOrderTrigger(trigger); 116 | 117 | // check if advice event is emitted and check its value 118 | futureOrder.on("advice", data => { 119 | expect(data).to.deep.equal( 120 | { advice: 'limit-buy', price: 1, amount: 5 }) 121 | done(); 122 | }); 123 | 124 | // Call an trade event 125 | futureOrder.onTrade(trade); 126 | }) 127 | 128 | it(`check & validate for future order with greater than price condtion market 129 | buy advice`, done => { 130 | // get dummy data to check trigger for 131 | trade = { ...data.default.trade }; 132 | trigger = { 133 | ...data.default.trigger, 134 | params: `{ 135 | "action": "buy", 136 | "type": "market", 137 | "condition": [{ 138 | "Volume": 4, 139 | "type": "greater-than" 140 | }] 141 | }` 142 | }; 143 | 144 | // Get instance of trigger for dummy data 145 | const futureOrder = new FutureOrderTrigger(trigger); 146 | 147 | // check if advice event is emitted and check its value 148 | futureOrder.on("advice", data => { 149 | expect(data).to.deep.equal( 150 | { advice: 'market-buy', price: 1, amount: 5 }) 151 | done(); 152 | }); 153 | 154 | // Call an trade event 155 | futureOrder.onTrade(trade); 156 | }) 157 | 158 | it(`check & validate for future order with greater than price condtion market 159 | buy advice`, done => { 160 | // get dummy data to check trigger for 161 | trade = { ...data.default.trade }; 162 | trigger = { 163 | ...data.default.trigger, 164 | params: `{ 165 | "action": "buy", 166 | "type": "limit", 167 | "condition": [{ 168 | "Volume": 6, 169 | "type": "less-than" 170 | }] 171 | }` 172 | }; 173 | 174 | // Get instance of trigger for dummy data 175 | const futureOrder = new FutureOrderTrigger(trigger); 176 | 177 | // check if advice event is emitted and check its value 178 | futureOrder.on("advice", data => { 179 | expect(data).to.deep.equal( 180 | { advice: 'limit-buy', price: 1, amount: 5 }) 181 | done(); 182 | }); 183 | 184 | // Call an trade event 185 | futureOrder.onTrade(trade); 186 | }) 187 | 188 | it("Check for close on ") 189 | }) 190 | -------------------------------------------------------------------------------- /test/Trigger/stopLossTakeProfitTests.ts: -------------------------------------------------------------------------------- 1 | import * as data from "../data"; 2 | import { expect } from "chai"; 3 | import StopLossTakeProfitTrigger from "../../src/triggers/StopLossTakeProfitTrigger"; 4 | 5 | export default describe("Stop Loss Take Profit Trigger tests", async function() { 6 | let trade, trigger; 7 | 8 | it("check & validate for stop loss market buy advice", done => { 9 | // get dummy data to check trigger for 10 | trade = { ...data.default.trade }; 11 | trigger = { 12 | ...data.default.trigger, 13 | params: `{ 14 | "action": "buy", 15 | "type": "market", 16 | "stopLossPrice": 0.5, 17 | "takeProfitPrice": 0.1 18 | }` 19 | }; 20 | 21 | // Get instance of trigger for dummy data 22 | const stopLoss = new StopLossTakeProfitTrigger(trigger); 23 | 24 | // check if advice event is emitted and check its value 25 | stopLoss.on("advice", data => { 26 | expect(data).to.deep.equal( 27 | { advice: 'market-buy', price: 1, amount: 5 }) 28 | done(); 29 | }); 30 | 31 | // Call an trade event 32 | stopLoss.onTrade(trade); 33 | }) 34 | 35 | it("check & validate for stop loss market sell advice", done => { 36 | trade = { 37 | ...data.default.trade 38 | }; 39 | trigger = { 40 | ...data.default.trigger, 41 | params: `{ 42 | "action": "sell", 43 | "type": "market", 44 | "stopLossPrice": 2, 45 | "takeProfitPrice": 0.1 46 | }` 47 | }; 48 | 49 | const stopLoss = new StopLossTakeProfitTrigger(trigger); 50 | 51 | stopLoss.on("advice", data => { 52 | expect(data).to.deep.equal( 53 | { advice: 'market-sell', price: 1, amount: 5 }) 54 | done(); 55 | }) 56 | 57 | stopLoss.onTrade(trade); 58 | }) 59 | 60 | it("check for stop loss close", done => { 61 | trade = { ...data.default.trade } 62 | trigger = { 63 | ...data.default.trigger, 64 | params: `{ 65 | "action": "buy", 66 | "type": "market", 67 | "stopLossPrice": 0.5, 68 | "takeProfitPrice": 0.1 69 | }` 70 | } 71 | 72 | const stopLoss = new StopLossTakeProfitTrigger(trigger) 73 | 74 | stopLoss.on("close", () => { 75 | done() 76 | }) 77 | 78 | stopLoss.onTrade(trade); 79 | }) 80 | 81 | it("check & validate for take profit limit buy advice", done => { 82 | trade = { ...data.default.trade }; 83 | trigger = { 84 | ...data.default.trigger, 85 | params: `{ 86 | "action": "buy", 87 | "type": "limit", 88 | "stopLossPrice": 0.5, 89 | "takeProfitPrice": 2 90 | }` 91 | }; 92 | 93 | const takeProfit = new StopLossTakeProfitTrigger(trigger); 94 | 95 | takeProfit.on("advice", data => { 96 | expect(data).to.deep.equal( 97 | { advice: 'limit-buy', price: 1, amount: 5 }) 98 | done(); 99 | }); 100 | 101 | takeProfit.onTrade(trade); 102 | }) 103 | 104 | it("check & validate for take profit limit sell advice", done => { 105 | trade = { ...data.default.trade }; 106 | trigger = { 107 | ...data.default.trigger, 108 | params: `{ 109 | "action": "sell", 110 | "type": "limit", 111 | "stopLossPrice": 0.3, 112 | "takeProfitPrice": 0.5 113 | }` 114 | }; 115 | 116 | const takeProfit = new StopLossTakeProfitTrigger(trigger); 117 | 118 | takeProfit.on("advice", data => { 119 | expect(data).to.deep.equal( 120 | { advice: 'limit-sell', price: 1, amount: 5 }) 121 | done(); 122 | }); 123 | 124 | takeProfit.onTrade(trade); 125 | }) 126 | }) 127 | -------------------------------------------------------------------------------- /test/Trigger/stopLossTriggerTests.ts: -------------------------------------------------------------------------------- 1 | import * as data from "../data"; 2 | import { expect } from "chai"; 3 | import StopLossTrigger from "../../src/triggers/StopLossTrigger"; 4 | // import * as database from "../src/database"; 5 | // import Triggers from ""../src/database/models/triggers"; 6 | // import Trades from "../src/database/models/trades"; 7 | 8 | 9 | export default describe("Stop Loss Trigger tests", async function() { 10 | let trade, trigger; 11 | 12 | // before(async function() { 13 | // database.init() 14 | // const triggers = await Triggers.findAll({ where: 15 | // { isActive: false }, raw: true}) 16 | // const trades = await Trades.findAll({ where: { 17 | // exchange: "binance", 18 | // CreatedAt: "2019-03-21 09:07:48" 19 | // }, raw: true }) 20 | 21 | // trigger = triggers[0] || {} 22 | // trade = trades[0] || {} 23 | 24 | // // console.log(triggers[0]) 25 | // }) 26 | 27 | it("check & validate for stop loss market buy advice", done => { 28 | // get dummy data to check trigger for 29 | trade = { ...data.default.trade }; 30 | trigger = { ...data.default.trigger }; 31 | 32 | // Get instance of trigger for dummy data 33 | const stopLoss = new StopLossTrigger(trigger); 34 | 35 | // check if advice event is emitted and check its value 36 | stopLoss.on("advice", data => { 37 | expect(data).to.deep.equal( 38 | { advice: 'market-buy', price: 1, amount: 5 }) 39 | done(); 40 | }); 41 | 42 | // Call an trade event 43 | stopLoss.onTrade(trade); 44 | }) 45 | 46 | it("check & validate for stop loss market sell advice", done => { 47 | trade = { 48 | ...data.default.trade, 49 | price: 1 50 | }; 51 | trigger = { 52 | ...data.default.trigger, 53 | targetPrice: 2, 54 | params: '{ "action": "sell", "type": "market" }' 55 | }; 56 | 57 | const stopLoss = new StopLossTrigger(trigger); 58 | 59 | stopLoss.on("advice", data => { 60 | expect(data).to.deep.equal( 61 | { advice: 'market-sell', price: 1, amount: 5 }) 62 | done(); 63 | }) 64 | 65 | stopLoss.onTrade(trade); 66 | }) 67 | 68 | it("check & validate stop loss limit sell advice", done => { 69 | trade = { 70 | ...data.default.trade, 71 | price: 1 72 | }; 73 | trigger = { 74 | ...data.default.trigger, 75 | targetPrice: 2, 76 | params: '{ "action": "sell", "type": "limit" }' 77 | }; 78 | 79 | const stopLoss = new StopLossTrigger(trigger); 80 | 81 | stopLoss.on("advice", data => { 82 | expect(data).to.deep.equal( 83 | { advice: 'limit-sell', price: 1, amount: 5 }) 84 | done(); 85 | }); 86 | 87 | stopLoss.onTrade(trade); 88 | }) 89 | 90 | it("check & validate for stop loss limit buy advice", done => { 91 | trade = { 92 | ...data.default.trade 93 | }; 94 | trigger = { 95 | ...data.default.trigger, 96 | params: '{ "action": "buy", "type": "limit" }' 97 | }; 98 | 99 | const stopLoss = new StopLossTrigger(trigger); 100 | 101 | stopLoss.on("advice", data => { 102 | expect(data).to.deep.equal( 103 | { advice: 'limit-buy', price: 1, amount: 5 }) 104 | done(); 105 | }); 106 | 107 | 108 | stopLoss.onTrade(trade); 109 | }) 110 | 111 | it("check for stop loss close", done => { 112 | trade = { ...data.default.trade } 113 | trigger = { ...data.default.trigger } 114 | 115 | const stopLoss = new StopLossTrigger(trigger) 116 | 117 | stopLoss.on("close", () => { 118 | done() 119 | }) 120 | 121 | stopLoss.onTrade(trade); 122 | }) 123 | }) 124 | -------------------------------------------------------------------------------- /test/Trigger/takeProfitTriggerTests.ts: -------------------------------------------------------------------------------- 1 | import * as data from "../data"; 2 | import { expect } from "chai"; 3 | import TakeProfitTrigger from "../../src/triggers/TakeProfitTrigger"; 4 | 5 | export default describe("Take Profit Trigger tests", async function() { 6 | let trigger, trade 7 | 8 | it("check & validate for take profit limit buy advice", done => { 9 | trade = { 10 | ...data.default.trade, 11 | price: 1 12 | }; 13 | trigger = { 14 | ...data.default.trigger, 15 | targetPrice: 2, 16 | params: '{ "action": "buy", "type": "limit" }' }; 17 | 18 | const takeProfit = new TakeProfitTrigger(trigger); 19 | 20 | takeProfit.on("advice", data => { 21 | expect(data).to.deep.equal( 22 | { advice: 'limit-buy', price: 1, amount: 5 }) 23 | done(); 24 | }); 25 | 26 | takeProfit.onTrade(trade); 27 | }) 28 | 29 | it("check & validate for take profit market buy advice", done => { 30 | trade = { 31 | ...data.default.trade, 32 | price: 1 33 | }; 34 | trigger = { 35 | ...data.default.trigger, 36 | targetPrice: 2 37 | }; 38 | 39 | const takeProfit = new TakeProfitTrigger(trigger); 40 | 41 | takeProfit.on("advice", data => { 42 | expect(data).to.deep.equal( 43 | { advice: 'market-buy', price: 1, amount: 5 }) 44 | done(); 45 | }); 46 | 47 | takeProfit.onTrade(trade); 48 | }) 49 | 50 | it("check & validate for take profit limit sell advice", done => { 51 | trade = { ...data.default.trade }; 52 | trigger = { 53 | ...data.default.trigger, 54 | params: '{ "action": "sell", "type": "limit" }' }; 55 | 56 | const takeProfit = new TakeProfitTrigger(trigger); 57 | 58 | takeProfit.on("advice", data => { 59 | expect(data).to.deep.equal( 60 | { advice: 'limit-sell', price: 1, amount: 5 }) 61 | done(); 62 | }); 63 | 64 | takeProfit.onTrade(trade); 65 | }) 66 | 67 | it("check & validate for take profit markte sell advice", done => { 68 | trade = { ...data.default.trade }; 69 | trigger = { 70 | ...data.default.trigger, 71 | params: '{ "action": "sell", "type": "market" }' }; 72 | 73 | const takeProfit = new TakeProfitTrigger(trigger); 74 | 75 | takeProfit.on("advice", data => { 76 | expect(data).to.deep.equal( 77 | { advice: 'market-sell', price: 1, amount: 5 }) 78 | done(); 79 | }); 80 | 81 | takeProfit.onTrade(trade); 82 | }) 83 | 84 | it("check for take profit close", done => { 85 | trade = { 86 | ...data.default.trade, 87 | price: 1 88 | } 89 | trigger = { 90 | ...data.default.trigger, 91 | targetPrice: 2 92 | } 93 | 94 | const takeProfit = new TakeProfitTrigger(trigger); 95 | 96 | takeProfit.on("close", () => { 97 | done() 98 | }) 99 | 100 | takeProfit.onTrade(trade); 101 | }) 102 | }) 103 | -------------------------------------------------------------------------------- /test/Trigger/trailingStopTriggerTests.ts: -------------------------------------------------------------------------------- 1 | import * as data from "../data"; 2 | import { expect } from "chai"; 3 | 4 | 5 | // describe("Trailing Stop Trigger Tests", async function() { 6 | // let trigger, trade; 7 | 8 | // it("check & validate for trailing stop trigger", done => { 9 | // trigger = { 10 | // ...data.default.trigger, 11 | // params: '{ "intialPrice": "0.9", "trail": "0.1" }' }; 12 | // trade = { 13 | // ...data.default.trade, 14 | // price: 0.01 }; 15 | 16 | // const trailingStopTrigger = new TrailingStopTrigger(trigger); 17 | 18 | // trailingStopTrigger.on("close", data => { 19 | // done(); 20 | // }); 21 | 22 | // trailingStopTrigger.onTrade(trade); 23 | // }) 24 | 25 | // it("check & validate for trailing stop trigger ", done => { 26 | // trigger = { 27 | // ...data.default.trigger, 28 | // params: '{ "intialPrice": "0.9", "trail": "0.1" }' }; 29 | // trade = { 30 | // ...data.default.trade, 31 | // price: 0.01 }; 32 | 33 | // const trailingStopTrigger = new TrailingStopTrigger(trigger); 34 | 35 | // trailingStopTrigger.on("close", data => { 36 | // expect(data).to.deep.equal( 37 | // { advice: 'market-sell', price: 1, amount: 0.24 }) 38 | // done(); 39 | // }); 40 | 41 | // trailingStopTrigger.onTrade(trade); 42 | // }) 43 | // }) 44 | -------------------------------------------------------------------------------- /test/data.js: -------------------------------------------------------------------------------- 1 | export default { 2 | trigger: { 3 | symbol: "LTC/BTC", 4 | exchange: "binance", 5 | targetPrice: 0.1, 6 | targetVolume: 0.25, 7 | createdAtPrice: 0.05, 8 | amount: 5, 9 | uid: 123456, 10 | kind: "stop-loss-buy", 11 | lastTriggeredAt: null, 12 | params: `{ 13 | "action": "buy", 14 | "type": "market", 15 | "steps": 1 }`, 16 | hasTriggered: false, 17 | closedAt: new Date("2019-05-23 08:41:51"), 18 | isActive: true, 19 | orderId: 123 20 | }, 21 | trade: { 22 | amount: 1, 23 | datetime: "2019-05-23 08:41:51", 24 | id: 1, 25 | info: {}, 26 | price: 1, 27 | timestamp: 1558581111000, 28 | side: "sell", 29 | symbol: 'BTC/USDT', 30 | 31 | takerOrMaker: 'taker', 32 | cost: 1000, 33 | fee: { 34 | type: 'taker', 35 | currency: 'BTC', 36 | rate: 10, 37 | cost: 100 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /test/test.ts: -------------------------------------------------------------------------------- 1 | import stopLossTriggerTests from "./Trigger/stopLossTriggerTests"; 2 | import takeProfitTriggerTests from "./Trigger/takeProfitTriggerTests"; 3 | import cancelOrderTriggerTests from "./Trigger/cancelOrderTriggerTests"; 4 | import tieredTakeProfitTriggerTests from "./Trigger/tieredTakeProfitTriggerTests"; 5 | import dynamicTieredTakeProfitTriggerTests from "./Trigger/dynamicTieredTakeProfitTriggerTests"; 6 | import stopLossTakeProfitTriggerTests from "./Trigger/stopLossTakeProfitTests"; 7 | import futureOrderTriggerTests from "./Trigger/futureOrderTriggerTests"; 8 | 9 | stopLossTriggerTests; 10 | takeProfitTriggerTests; 11 | cancelOrderTriggerTests; 12 | tieredTakeProfitTriggerTests; 13 | dynamicTieredTakeProfitTriggerTests; 14 | stopLossTakeProfitTriggerTests; 15 | futureOrderTriggerTests; 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "baseUrl": ".", 5 | "target": "es6", 6 | "types": [ 7 | "node", 8 | "express", 9 | "mocha" 10 | ], 11 | "noImplicitAny": false, 12 | "sourceMap": false, 13 | "lib": [ 14 | "es5", 15 | "es6", 16 | "es2016", 17 | "es2017", 18 | "dom" 19 | ], 20 | "emitDecoratorMetadata": true, 21 | "experimentalDecorators": true, 22 | "allowJs": true, 23 | "jsx": "react", 24 | "outDir": "dist" 25 | }, 26 | "include": [ 27 | "node_modules/ccxt", 28 | "src" 29 | ], 30 | "exclude": [ 31 | "**/*test.tsx", 32 | "node_modules", 33 | "dist", 34 | "config", 35 | "electron", 36 | "release" 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "tslint-react" 4 | ], 5 | "linterOptions": { 6 | "exclude": [ 7 | ] 8 | }, 9 | "rules": { 10 | "align": [ 11 | true, 12 | "parameters", 13 | "arguments", 14 | "statements" 15 | ], 16 | "no-unused-variable":false, 17 | "ban": false, 18 | "class-name": true, 19 | "comment-format": [ 20 | true, 21 | "check-space" 22 | ], 23 | "curly": false, 24 | "eofline": false, 25 | "forin": true, 26 | "indent": [ 27 | true, 28 | "spaces" 29 | ], 30 | "interface-name": [ 31 | true, 32 | "always-prefix" 33 | ], 34 | "jsdoc-format": true, 35 | "jsx-no-string-ref": false, 36 | "jsx-no-lambda": false, 37 | "jsx-no-multiline-js": false, 38 | "label-position": true, 39 | "max-line-length": [ 40 | true, 41 | 120 42 | ], 43 | "member-ordering": [ 44 | true, 45 | "public-before-private", 46 | "static-before-instance", 47 | "variables-before-functions" 48 | ], 49 | // "no-any": true, 50 | "no-arg": true, 51 | "no-bitwise": true, 52 | "no-consecutive-blank-lines": false, 53 | "no-construct": true, 54 | "no-debugger": true, 55 | "no-duplicate-variable": true, 56 | "no-empty": true, 57 | "no-eval": true, 58 | "jsx-alignment": false, 59 | "no-shadowed-variable": true, 60 | "no-string-literal": true, 61 | "no-switch-case-fall-through": true, 62 | "no-trailing-whitespace": false, 63 | "no-unused-expression": true, 64 | "no-use-before-declare": true, 65 | "one-line": [ 66 | true, 67 | "check-catch", 68 | "check-else", 69 | "check-open-brace", 70 | "check-whitespace" 71 | ], 72 | "quotemark": [ 73 | true, 74 | "single", 75 | "jsx-double" 76 | ], 77 | "radix": true, 78 | "semicolon": [ 79 | true, 80 | "never" 81 | ], 82 | "switch-default": true, 83 | "trailing-comma": [ 84 | false 85 | ], 86 | "triple-equals": [ 87 | true, 88 | "allow-null-check" 89 | ], 90 | "typedef": [ 91 | true, 92 | "parameter", 93 | "property-declaration" 94 | ], 95 | "typedef-whitespace": [ 96 | true, 97 | { 98 | "call-signature": "nospace", 99 | "index-signature": "nospace", 100 | "parameter": "nospace", 101 | "property-declaration": "nospace", 102 | "variable-declaration": "nospace" 103 | } 104 | ], 105 | "variable-name": [ 106 | true, 107 | "ban-keywords", 108 | "check-format", 109 | "allow-leading-underscore", 110 | "allow-pascal-case" 111 | ], 112 | "whitespace": [ 113 | true, 114 | "check-branch", 115 | "check-decl", 116 | "check-module", 117 | "check-operator", 118 | "check-separator", 119 | "check-type", 120 | "check-typecast" 121 | ] 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const TsConfigPathsPlugin = require('awesome-typescript-loader').TsConfigPathsPlugin 3 | const webpack = require('webpack') 4 | 5 | 6 | module.exports = { 7 | entry: { 8 | app: path.resolve('./src/index.ts') 9 | }, 10 | 11 | output: { 12 | path: path.resolve('./dist'), 13 | filename: '[name].bundle.js' 14 | }, 15 | 16 | // context: path.resolve('./src'), 17 | 18 | target: 'node', 19 | mode: 'production', 20 | 21 | module: { 22 | rules: [ 23 | { 24 | // enforce: 'pre', 25 | test: /\.js$/, 26 | use: 'babel-loader' 27 | }, 28 | // { 29 | // test: /node_modules\/ccxt\/(.+)\.js$/, 30 | // loader: "babel-loader", 31 | // options: { 32 | // babelrc: false, 33 | // cacheDirectory: false, 34 | // presets: ["es2015"], 35 | // plugins: ["syntax-async-functions", "transform-regenerator"] 36 | // } 37 | // }, 38 | // { 39 | // enforce: 'pre', 40 | // test: /\.ts$/, 41 | // exclude: /node_modules/, 42 | // use: 'tslint-loader' 43 | // }, 44 | { 45 | test: /\.tsx?$/, 46 | exclude: [ /node_modules/ ], 47 | use: ['babel-loader', 'awesome-typescript-loader'], 48 | } 49 | ] 50 | }, 51 | 52 | resolve: { 53 | extensions: ['.js', '.ts', '.tsx', '.json'], 54 | modules: [ 55 | path.join(__dirname, 'src'), 56 | // path.join(__dirname, 'app/node_modules'), 57 | 'node_modules', 58 | ], 59 | plugins: [ 60 | new TsConfigPathsPlugin({ configFileName: path.resolve('./tsconfig.json') }) 61 | ] 62 | }, 63 | 64 | plugins: [ 65 | new webpack.DefinePlugin({ 66 | 'process.env': { NODE_ENV: JSON.stringify('production') } 67 | }) 68 | ], 69 | } 70 | --------------------------------------------------------------------------------