├── .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 | 
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 | [](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 | 
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 | 
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 |
123 | Start Server
124 |
125 |
126 | {/*
129 | Stop Server
130 | */}
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 |
--------------------------------------------------------------------------------