├── .dockerignore ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .prettierrc ├── .sample_env ├── .vscode └── settings.json ├── Dockerfile ├── LICENSE ├── README.md ├── ccxt_config.json ├── ccxt_config_sample.json ├── jest.config.js ├── package.json ├── renovate.json ├── src ├── __test__ │ ├── emulator │ │ ├── backtest_emulator.spec.ts │ │ ├── emulator.spec.ts │ │ ├── stategies.spec.ts │ │ └── strategy_optimizer.spec.ts │ ├── fixtures │ │ ├── candleData.ts │ │ └── candleOHLCV.ts │ ├── gridbot │ │ └── gridbot.spec.ts │ └── utils │ │ └── candlestick_generator.ts ├── constants.ts ├── database │ └── index.ts ├── emitter │ ├── emitter.ts │ └── index.ts ├── emulator │ ├── backtest_emulator.ts │ ├── emulator.ts │ ├── live_emulator.ts │ └── strategy_optimizer.ts ├── exchange │ └── ccxt_controller.ts ├── grid_bot │ └── index.ts ├── httpserver │ ├── controllers │ │ ├── backtest.controller.ts │ │ ├── gridbot.controller.ts │ │ ├── strategy.controller.ts │ │ └── tradepairs.controller.ts │ ├── index.ts │ ├── main.module.ts │ └── services │ │ ├── backtest.service.ts │ │ ├── gridbot.service.ts │ │ ├── strategy.service.ts │ │ └── tradepairs.service.ts ├── index.ts ├── indicators │ ├── custom │ │ ├── ATR.js │ │ ├── BB.js │ │ ├── CCI.js │ │ ├── CROSS_SMMA.js │ │ ├── DEMA.js │ │ ├── DONCHIAN.js │ │ ├── EMA.js │ │ ├── LRC.js │ │ ├── MACD.js │ │ ├── MOME.js │ │ ├── OBI.js │ │ ├── OHCL4.js │ │ ├── PPO.js │ │ ├── RISK.js │ │ ├── RSI.js │ │ ├── SMA.js │ │ ├── SMMA.js │ │ ├── STOPLOSS.js │ │ ├── TRIX.js │ │ ├── TSI.js │ │ ├── UO.js │ │ ├── WA.js │ │ ├── WF.js │ │ └── WF_SMA.js │ └── index.js ├── logger │ └── index.ts ├── redis │ ├── channels │ │ └── candlestick_redis.ts │ ├── index.ts │ └── redis.ts ├── strategies │ ├── abstract_strategy │ │ └── index.ts │ ├── bb_pure │ │ └── index.js │ ├── index.ts │ ├── ml_train │ │ └── index.js │ ├── rsi_macd │ │ └── index.js │ └── utils │ │ └── ml_api │ │ └── index.ts ├── tradepairs │ └── tradepairs.ts ├── traderbot │ ├── trade_emulator.ts │ ├── trade_instance.ts │ ├── trade_utils.ts │ └── traderbot.ts ├── types.ts └── utils │ └── index.ts ├── tsconfig.json └── types └── mysql2 └── index.d.ts /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .dockerignore 3 | Dockerfile 4 | npm-debug.log 5 | .git 6 | logs -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | logs 4 | types 5 | SQL 6 | *__test__ 7 | *indicators 8 | strategies 9 | jest.config.js -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true 4 | }, 5 | "root": true, 6 | "parser": "@typescript-eslint/parser", 7 | "parserOptions": { 8 | "project": "./tsconfig.json" 9 | }, 10 | "plugins": ["@typescript-eslint", "prettier"], 11 | "extends": [ 12 | "airbnb-typescript", 13 | "plugin:@typescript-eslint/eslint-recommended", 14 | "plugin:@typescript-eslint/recommended", 15 | "plugin:prettier/recommended" 16 | ], 17 | "rules": { 18 | "no-console": "error", 19 | "camelcase": "off", 20 | "import/prefer-default-export": "off", 21 | "import/no-default-export": "off", 22 | "no-bitwise": "off", 23 | "no-plusplus": "off", 24 | "import/no-cycle": "off", 25 | "no-underscore-dangle": "off", 26 | "consistent-return": "off", 27 | "no-restricted-syntax": "off", 28 | "no-await-in-loop": "off", 29 | "no-return-assign": "warn", 30 | "class-methods-use-this": "off", 31 | "import/no-dynamic-require": "off", 32 | "global-require": "off", 33 | "lines-between-class-members": "off", 34 | "array-element-newline": ["error", "consistent"], 35 | "array-bracket-newline": ["error", "consistent"] 36 | }, 37 | "settings": { 38 | "react": { 39 | "version": "999.999.999" 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # parcel-bundler cache (https://parceljs.org/) 61 | .cache 62 | 63 | # next.js build output 64 | .next 65 | 66 | # nuxt.js build output 67 | .nuxt 68 | 69 | # vuepress build output 70 | .vuepress/dist 71 | 72 | # Serverless directories 73 | .serverless 74 | 75 | # FuseBox cache 76 | .fusebox/ 77 | 78 | # 79 | package-lock.json 80 | 81 | # Tensorflow train datas 82 | *.tf 83 | ccxtConfig.json 84 | .docker_scripts 85 | .next 86 | table_creator.js 87 | ccxtConfig.json 88 | build -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "trailingComma": "all", 4 | "singleQuote": true 5 | } 6 | -------------------------------------------------------------------------------- /.sample_env: -------------------------------------------------------------------------------- 1 | # 2 | ################## Application Database 3 | # 4 | # low performance database 5 | # 6 | MYSQL_HOST= 7 | MYSQL_PORT=3306 8 | MYSQL_USER= 9 | MYSQL_PASS= 10 | MYSQL_DB=stockml 11 | 12 | # 13 | ################## Exchange Database 14 | # Storage of exchange datas / high performance database 15 | # 16 | MYSQL_HOST_EXCHANGE= 17 | MYSQL_PORT_EXCHANGE=3306 18 | MYSQL_USER_EXCHANGE= 19 | MYSQL_PASS_EXCHANGE= 20 | MYSQL_DB_EXCHANGE=stockml_exchange 21 | 22 | 23 | # 24 | ################## Redis Database 25 | # Redis can be used to increase reliability and reduce latency between services 26 | # 27 | REDIS_HOST = 28 | REDIS_PORT = 6379 29 | REDIS_AUTH = 30 | REDIS_DB_ID = 0 31 | 32 | # Log Level verbose,info 33 | log_level = info 34 | 35 | # Application settings 36 | # 37 | # On / Off 38 | traderbot = 1 39 | liveEmulator = 1 40 | backtestEmulator = 1 41 | 42 | 43 | #HTTP API 44 | httpAPI = 1 45 | httpPort = 3100 -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true 3 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:12-alpine 2 | 3 | # Install all build dependencies 4 | RUN apk update \ 5 | && apk add --virtual build-dependencies \ 6 | build-base \ 7 | dos2unix \ 8 | python2-dev \ 9 | && python2 \ 10 | && apk add bash \ 11 | && apk add libc6-compat 12 | 13 | # Create app directory 14 | RUN mkdir -p /home/node/app/node_modules && chown -R node:node /home/node/app 15 | 16 | WORKDIR /home/node/app 17 | 18 | 19 | 20 | # Install GYP dependencies globally, will be used to code build other dependencies 21 | RUN npm install -g --production node-gyp \ 22 | && npm install -g --production node-pre-gyp \ 23 | && npm cache clean --force \ 24 | && npm install -g nodemon 25 | 26 | 27 | # Install dependencies 28 | COPY package.json . 29 | 30 | USER node 31 | 32 | RUN npm install \ 33 | && npm cache clean --force 34 | 35 | # Bundle app source 36 | COPY --chown=node:node . . 37 | 38 | EXPOSE 3001 39 | 40 | 41 | CMD [ "nodemon", "index.js" ] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Valamidev 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 | # TraderCore 2 | [![DeepScan grade](https://deepscan.io/api/teams/6761/projects/8874/branches/145556/badge/grade.svg)](https://deepscan.io/dashboard#view=project&tid=6761&pid=8874&bid=145556) 3 | 4 | TradeCore made for Trading, Signaling and Backtesting Strategies based on datas provided by DataSynchronizer (https://github.com/stockmlbot/DataSynchronizer/). 5 | 6 | 7 | ### Install: 8 | 9 | Rename `.sample_env` -> `.env` and configure required variables 10 | 11 | ``` 12 | npm install && npm run build && npm start 13 | ``` 14 | 15 | Windows(only): 16 | 17 | - Talib will build only with `--vs2015` build tools. 18 | 19 | ``` 20 | npm install --vs2015 --global windows-build-tools 21 | ``` 22 | 23 | ### API Endpoints: 24 | 25 | #### Tradepairs: 26 | 27 | `/all` 28 | 29 | Get all available Tradepairs from database. 30 | ``` 31 | curl --location --request GET 'http://localhost:3100/tradepairs/all' 32 | ``` 33 | 34 | #### Strategy: 35 | 36 | `/all` 37 | 38 | Get all available Strategy and configuration schema from (https://github.com/stockmlbot/TraderCore/blob/master/src/strategies/index.ts). 39 | ``` 40 | curl --location --request GET 'http://localhost:3100/strategy/all' 41 | ``` 42 | 43 | 44 | #### Backtest: 45 | 46 | `/optimize` 47 | 48 | Run Backtest optimize process against given Tradepair with various strategy configuration (random generated). 49 | ``` 50 | curl --location --request POST 'http://localhost:3100/backtest/optimize' \ 51 | --data-raw '{ 52 | "exchange": '\''binance'\'', 53 | "symbol": '\''BTC/USDT'\'', 54 | "strategy": '\''bb_pure'\'', 55 | "candleLimit": 3000, 56 | "numberOfExecution": 30 57 | };' 58 | ``` 59 | 60 | ### Additional features: 61 | 62 | #### Live strategy evaluation: 63 | 64 | It allow to load/update your pre-configured strategies and save trading advices into the database also can be used for Tradebot. 65 | 66 | #### Tradebot (experimental): 67 | 68 | Interact with Exchanges to execute,update,follow Orders and manage balances. 69 | 70 | -------------------------------------------------------------------------------- /ccxt_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "binance" :{ 3 | "apiKey": "", 4 | "secret": "" 5 | }, 6 | "kucoin" :{ 7 | "apiKey": "", 8 | "secret": "" 9 | } 10 | } -------------------------------------------------------------------------------- /ccxt_config_sample.json: -------------------------------------------------------------------------------- 1 | { 2 | "binance" :{ 3 | "apiKey": "", 4 | "secret": "" 5 | }, 6 | "kucoin" :{ 7 | "apiKey": "", 8 | "secret": "" 9 | } 10 | } -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | collectCoverage: false, 3 | coveragePathIgnorePatterns: ['/node_modules|dist/'], 4 | collectCoverageFrom: ['src/**/*.ts'], 5 | transform: { 6 | '.(ts|tsx)': 'ts-jest', 7 | }, 8 | testRegex: '(/__tests__/.*|\\.(test|spec))\\.(ts|tsx|js)$', 9 | moduleFileExtensions: ['ts', 'tsx', 'js'], 10 | }; 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stockmlbot-tradercore", 3 | "version": "4.0.0", 4 | "description": "", 5 | "main": "./build/src/index.js", 6 | "scripts": { 7 | "start": "node ./build/src/index.js", 8 | "dev": "ts-node ./src/index.ts", 9 | "debug": "tsc && node --experimental-worker --inspect=5858 -r ts-node/register ./src/index.ts", 10 | "test": "jest", 11 | "prebuild": "rimraf build", 12 | "build": "tsc", 13 | "format": "prettier --write \"src/**/*.ts\" \"src/**/*.js\"", 14 | "lint": "eslint . --ext .ts", 15 | "lint:fix": "eslint . --ext .ts --fix", 16 | "precommit": "eslint . --ext .ts --fix", 17 | "prepublishOnly": "npm test && npm run lint", 18 | "preversion": "npm run lint", 19 | "version": "npm run format && git add -A src", 20 | "postversion": "git push && git push --tags" 21 | }, 22 | "keywords": [], 23 | "author": "", 24 | "license": "ISC", 25 | "dependencies": { 26 | "@koa/cors": "^2.2.3", 27 | "@nestjs/common": "^7.0.6", 28 | "@nestjs/core": "^7.0.6", 29 | "@nestjs/platform-express": "^7.0.8", 30 | "@types/koa__cors": "^3.0.1", 31 | "axios": ">=0.19.0", 32 | "bfx-hf-indicators": "^1.0.2", 33 | "bigint-money": "^1.1.1", 34 | "candlestick-convert": "^5.1.3", 35 | "ccxt": "^1.18.995", 36 | "co-body": "^6.0.0", 37 | "dotenv": "^8.0.0", 38 | "eventemitter3": "^4.0.0", 39 | "ioredis": "^4.14.0", 40 | "koa": "^2.7.0", 41 | "koa-router": "^7.4.0", 42 | "lodash": "^4.17.15", 43 | "math": "0.0.3", 44 | "mathjs": "^5.10.0", 45 | "mysql2": "^1.6.5", 46 | "reflect-metadata": "^0.1.13", 47 | "rimraf": "^3.0.2", 48 | "rxjs": "^6.5.4", 49 | "sand-ex": "^0.1.8", 50 | "synaptic": "^1.1.4", 51 | "talib": "^1.1.3", 52 | "tulind": "^0.8.18", 53 | "winston": "^3.2.1" 54 | }, 55 | "devDependencies": { 56 | "@types/co-body": "0.0.3", 57 | "@types/ioredis": "^4.14.6", 58 | "@types/jest": "^24.0.18", 59 | "@types/koa": "^2.11.2", 60 | "@types/koa-router": "^7.4.0", 61 | "@types/lodash": "^4.14.138", 62 | "@types/node": "^12.12.26", 63 | "@types/request": "^2.48.4", 64 | "@typescript-eslint/eslint-plugin": "^2.26.0", 65 | "@typescript-eslint/parser": "^2.21.0", 66 | "eslint": "^6.8.0", 67 | "eslint-config-airbnb-typescript": "^7.2.0", 68 | "eslint-config-prettier": "^6.10.1", 69 | "eslint-plugin-import": "^2.20.2", 70 | "eslint-plugin-jsx-a11y": "^6.2.3", 71 | "eslint-plugin-prettier": "^3.1.2", 72 | "eslint-plugin-react": "^7.19.0", 73 | "eslint-plugin-react-hooks": "^2.5.1", 74 | "husky": "^4.2.3", 75 | "jest": "^24.9.0", 76 | "lint-staged": "^10.1.1", 77 | "prettier": "^1.18.2", 78 | "ts-jest": "^24.0.2", 79 | "ts-node": "^8.4.1", 80 | "typescript": "^3.8.0" 81 | }, 82 | "husky": { 83 | "hooks": { 84 | "pre-commit": "lint-staged" 85 | } 86 | }, 87 | "lint-staged": { 88 | "*.{js,ts}": "eslint --fix" 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /src/__test__/emulator/backtest_emulator.spec.ts: -------------------------------------------------------------------------------- 1 | import { BacktestEmulator } from '../../emulator/backtest_emulator'; 2 | import { batchedOHLCV } from '../../types'; 3 | 4 | import candleData from '../fixtures/candleData'; 5 | import { DEFAULT_TRADER_CONFIG } from '../../constants'; 6 | 7 | const Backtest = new BacktestEmulator(); 8 | 9 | describe('BackTest Emulator', () => { 10 | it('should give back Actions and Performance', async () => { 11 | // Act 12 | await Backtest.start({ 13 | exchange: 'binance', 14 | symbol: 'BTC/USDT', 15 | strategy: 'bb_pure', 16 | strategyConfig: { intervals: [60, 300] }, 17 | intervals: [60, 300], 18 | traderConfig: DEFAULT_TRADER_CONFIG, 19 | candledata: (candleData as any) as batchedOHLCV, 20 | }); 21 | 22 | // Assert 23 | expect(Backtest.historyOrders).toHaveLength(0); 24 | expect(Backtest).toHaveProperty('performance', 1000); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /src/__test__/emulator/emulator.spec.ts: -------------------------------------------------------------------------------- 1 | import { batchedOHLCV } from '../../types'; 2 | import { Emulator } from '../../emulator/emulator'; 3 | import { EmulatorStates, DEFAULT_TRADER_CONFIG } from '../../constants'; 4 | 5 | import candleData from '../fixtures/candleData'; 6 | 7 | describe('Emulator', () => { 8 | let emulator: Emulator; 9 | 10 | it('should after Start update the properties ', async () => { 11 | // Arrange 12 | const lastCandleTick = candleData[Object.keys(candleData)[Object.keys(candleData).length - 1]]; 13 | 14 | emulator = new Emulator({ 15 | exchange: 'binance', 16 | symbol: 'BTC/USDT', 17 | strategy: 'bb_pure', 18 | strategyConfig: { intervals: [60, 300] }, 19 | intervals: [60, 300], 20 | traderConfig: DEFAULT_TRADER_CONFIG, 21 | }); 22 | 23 | // Act 24 | await emulator.start((candleData as any) as batchedOHLCV); 25 | 26 | // Assert 27 | expect(emulator).toHaveProperty('lastUpdateTime', lastCandleTick['60'].time); 28 | expect(emulator).toHaveProperty('lastUpdate', lastCandleTick); 29 | expect(emulator.state).toBe(EmulatorStates.READY); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /src/__test__/emulator/stategies.spec.ts: -------------------------------------------------------------------------------- 1 | import { AbstractStrategy } from '../../strategies/abstract_strategy'; 2 | import { fakeBatchedCandlestickMap } from '../utils/candlestick_generator'; 3 | // const baseIntervals = [60]; 4 | 5 | describe('Strategy', () => { 6 | let strategy: AbstractStrategy; 7 | 8 | it('should after Start update the properties ', async () => { 9 | // Arrange 10 | const candleData = fakeBatchedCandlestickMap([60, 300], 1000); 11 | const updateTimeStamps = Object.keys(candleData); 12 | 13 | strategy = new AbstractStrategy(); 14 | strategy.addNeWTA({ label: 'SMA_5_5min', updateInterval: 300, nameTA: 'SMA', params: 5, params2: 'ohlc/4' }); 15 | strategy.addNeWTA({ label: 'SMA_5_2min', updateInterval: 120, nameTA: 'SMA', params: 5, params2: 'ohlc/4' }); 16 | strategy.addNeWTA({ label: 'SMA_5_1min', updateInterval: 60, nameTA: 'SMA', params: 5, params2: 'ohlc/4' }); 17 | 18 | // Act 19 | for (const timeStamp of updateTimeStamps) { 20 | await strategy.update(candleData[timeStamp]); 21 | } 22 | 23 | // Assert 24 | const updateSteps = strategy.step; 25 | const SMA5min = (strategy.TA_BUFFER as any).SMA_5_5min; 26 | const SMA1min = (strategy.TA_BUFFER as any).SMA_5_1min; 27 | 28 | const SMA2min = (strategy.TA_BUFFER as any).SMA_5_2min; 29 | 30 | expect(updateSteps).toBe(1006); 31 | expect(strategy.isStrategyReady()).toBe(true); 32 | expect(SMA5min[updateSteps]).toBeDefined; 33 | expect(SMA1min[updateSteps]).toBeDefined; 34 | expect(SMA2min[updateSteps]).toBe(-1); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /src/__test__/emulator/strategy_optimizer.spec.ts: -------------------------------------------------------------------------------- 1 | import { StrategyOptimizer } from '../../emulator/strategy_optimizer'; 2 | import { fakeBatchedCandlestickMap } from '../utils/candlestick_generator'; 3 | import { DEFAULT_TRADER_CONFIG } from '../../constants'; 4 | // const baseIntervals = [60]; 5 | 6 | describe('Strategy Optimizer', () => { 7 | const candledata = fakeBatchedCandlestickMap([60, 300, 1200], 1000); 8 | const optimizer = new StrategyOptimizer({ 9 | exchange: 'Test', 10 | symbol: 'BTC/USDT', 11 | numberOfExecution: 10, 12 | strategy: 'bb_pure', 13 | traderConfig: DEFAULT_TRADER_CONFIG, 14 | candledata, 15 | }); 16 | 17 | it('should create StrategyOptimizer object ', async () => { 18 | // Assert 19 | expect(optimizer).toStrictEqual(expect.any(Object)); 20 | }); 21 | 22 | it('should execute and return results', async () => { 23 | // Act 24 | 25 | const result = await optimizer.execute(); 26 | 27 | // Assert 28 | expect(result).toHaveLength(10); 29 | expect(result[0]).toMatchObject({ 30 | strategy: 'bb_pure', 31 | config: expect.any(Object), 32 | historyOrders: expect.any(Array), 33 | performance: expect.any(Number), 34 | numOfOrders: expect.any(Number), 35 | }); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /src/__test__/fixtures/candleData.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | '1583925360000': { 3 | '60': { 4 | time: 1583925360000, 5 | open: 7820.33, 6 | high: 7821.58, 7 | low: 7817.17, 8 | close: 7820.05, 9 | volume: 12.868774, 10 | }, 11 | }, 12 | '1583925420000': { 13 | '60': { 14 | time: 1583925420000, 15 | open: 7820.12, 16 | high: 7828.15, 17 | low: 7819.3, 18 | close: 7824.62, 19 | volume: 18.25686900000001, 20 | }, 21 | }, 22 | '1583925480000': { 23 | '60': { 24 | time: 1583925480000, 25 | open: 7824.69, 26 | high: 7828.99, 27 | low: 7822.54, 28 | close: 7822.8, 29 | volume: 34.182644, 30 | }, 31 | }, 32 | '1583925540000': { 33 | '60': { 34 | time: 1583925540000, 35 | open: 7822.93, 36 | high: 7828.97, 37 | low: 7821.31, 38 | close: 7826.87, 39 | volume: 20.02194700000005, 40 | }, 41 | }, 42 | '1583925600000': { 43 | '60': { 44 | time: 1583925600000, 45 | open: 7827.86, 46 | high: 7827.95, 47 | low: 7817.98, 48 | close: 7819.5, 49 | volume: 39.462627000000005, 50 | }, 51 | '300': { 52 | time: 1583925600000, 53 | open: 7820.33, 54 | high: 7828.99, 55 | low: 7817.17, 56 | close: 7819.5, 57 | volume: 124.79286100000007, 58 | }, 59 | }, 60 | '1583925660000': { 61 | '60': { 62 | time: 1583925660000, 63 | open: 7819.46, 64 | high: 7823.33, 65 | low: 7819.29, 66 | close: 7820.84, 67 | volume: 15.599552999999991, 68 | }, 69 | }, 70 | '1583925720000': { 71 | '60': { 72 | time: 1583925720000, 73 | open: 7820.72, 74 | high: 7828, 75 | low: 7819, 76 | close: 7826.11, 77 | volume: 30.833984, 78 | }, 79 | }, 80 | '1583925780000': { 81 | '60': { 82 | time: 1583925780000, 83 | open: 7826.74, 84 | high: 7839, 85 | low: 7826, 86 | close: 7835.46, 87 | volume: 48.099201, 88 | }, 89 | }, 90 | '1583925840000': { 91 | '60': { 92 | time: 1583925840000, 93 | open: 7835.56, 94 | high: 7838.99, 95 | low: 7830.13, 96 | close: 7830.68, 97 | volume: 17.512491999999998, 98 | }, 99 | }, 100 | '1583925900000': { 101 | '60': { 102 | time: 1583925900000, 103 | open: 7830.77, 104 | high: 7832.69, 105 | low: 7825.37, 106 | close: 7831.77, 107 | volume: 21.85953, 108 | }, 109 | '300': { 110 | time: 1583925900000, 111 | open: 7819.46, 112 | high: 7839, 113 | low: 7819, 114 | close: 7831.77, 115 | volume: 133.90475999999998, 116 | }, 117 | }, 118 | }; 119 | -------------------------------------------------------------------------------- /src/__test__/fixtures/candleOHLCV.ts: -------------------------------------------------------------------------------- 1 | export default [ 2 | [1569160500000, 9977.09, 9992.3, 9972.63, 9986.24, 56.127912], 3 | [1569160800000, 9985.95, 9986.71, 9972.37, 9981.83, 131.1298], 4 | [1569161100000, 9981.65, 9981.65, 9956.62, 9965.68, 138.566688], 5 | [1569161400000, 9966.4, 9969, 9952.69, 9960.58, 69.024538], 6 | [1569161700000, 9961.77, 9968.16, 9945.01, 9951.75, 85.007832], 7 | [1569162000000, 9951.75, 9964.05, 9941.33, 9951.41, 94.306792], 8 | [1569162300000, 9950.56, 9977.7, 9950, 9975.01, 78.896676], 9 | [1569162600000, 9975.46, 9978.1, 9949.33, 9974.05, 79.544889], 10 | [1569162900000, 9975.23, 9978.56, 9966.38, 9966.43, 38.531468], 11 | [1569163200000, 9966.41, 9976.5, 9957.67, 9969.5, 49.047956], 12 | [1569163500000, 9969.5, 9982.38, 9968.28, 9975, 46.836256], 13 | [1569163800000, 9975, 9992.86, 9972.23, 9985.02, 49.518444], 14 | [1569164100000, 9985.17, 10004.28, 9980.18, 9987.46, 73.729233], 15 | [1569164400000, 9987.38, 9987.38, 9971.86, 9981.72, 49.469497], 16 | [1569164700000, 9982.13, 9988.98, 9978.08, 9985.99, 29.684564], 17 | [1569165000000, 9986, 9992.34, 9985.08, 9985.75, 24.680755], 18 | [1569165300000, 9985.21, 9990.56, 9976.01, 9977.42, 27.993789], 19 | [1569165600000, 9977.42, 9988.07, 9973, 9976.76, 38.247194], 20 | [1569165900000, 9976.61, 9978.99, 9969.47, 9974.36, 32.936238], 21 | [1569166200000, 9974.36, 9980.48, 9971.25, 9974.41, 60.678558], 22 | [1569166500000, 9974.43, 9975.41, 9958.16, 9965.97, 123.160978], 23 | [1569166800000, 9967.88, 9967.88, 9957, 9963.99, 66.292489], 24 | [1569167100000, 9962.72, 9974.12, 9962.05, 9973.54, 33.30088], 25 | [1569167400000, 9974.05, 9979.87, 9971.84, 9976.68, 47.000296], 26 | [1569167700000, 9976.68, 9992.31, 9976.07, 9990.64, 41.082629], 27 | [1569168000000, 9989.87, 10029.13, 9989.81, 10020.44, 294.680292], 28 | [1569168300000, 10020.45, 10023.23, 9999.98, 10016.39, 111.010599], 29 | [1569168600000, 10017.62, 10019.19, 9992.74, 9995, 149.356775], 30 | [1569168900000, 9998.23, 9999.22, 9962.97, 9978.98, 355.256005], 31 | [1569169200000, 9979.88, 10001.1, 9977.58, 9988.82, 393.577349], 32 | [1569169500000, 9988.82, 10014, 9981.46, 10010.01, 347.63874], 33 | [1569169800000, 10010.49, 10018.74, 9996.12, 10006.66, 151.043527], 34 | [1569170100000, 10004.81, 10006.14, 9965.86, 9991.75, 492.622205], 35 | [1569170400000, 9990.72, 9995.73, 9966.23, 9990.02, 295.876907], 36 | [1569170700000, 9988.47, 10020, 9983.76, 10020, 283.521446], 37 | [1569171000000, 10019.99, 10020, 9990.83, 9997.83, 201.544617], 38 | [1569171300000, 9997.85, 10014.35, 9990.86, 10013.93, 131.743334], 39 | [1569171600000, 10013.95, 10014, 9997.52, 9997.74, 92.921018], 40 | [1569171900000, 9999.15, 10003.07, 9995.32, 10001.08, 63.013161], 41 | [1569172200000, 10001.51, 10012.76, 10000, 10002.66, 60.606419], 42 | [1569172500000, 10003.41, 10013.95, 10001.95, 10013.9, 61.075028], 43 | [1569172800000, 10012.76, 10013.95, 10007.12, 10010.69, 61.135623], 44 | [1569173100000, 10010.69, 10024.97, 10010.69, 10017.69, 111.586792], 45 | [1569173400000, 10017.97, 10027.43, 10007.87, 10009.89, 104.628293], 46 | [1569173700000, 10009.89, 10022.59, 10005.37, 10022.21, 53.273785], 47 | [1569174000000, 10022.21, 10022.21, 10001.91, 10004.53, 61.938857], 48 | [1569174300000, 10004.51, 10014.93, 9999.02, 10014.24, 54.775051], 49 | [1569174600000, 10014.65, 10017.4, 9988.5, 9995.34, 72.36145], 50 | [1569174900000, 9997.36, 10003.19, 9985, 9993.95, 51.645971], 51 | [1569175200000, 9993.42, 9994.97, 9975.67, 9977, 85.189472], 52 | [1569175500000, 9976.16, 9994.69, 9973, 9979.16, 75.771394], 53 | [1569175800000, 9978.19, 9984.38, 9970, 9979.98, 39.4644], 54 | [1569176100000, 9977.49, 9989.98, 9977.48, 9981.68, 27.814701], 55 | [1569176400000, 9982.15, 9982.15, 9965, 9968.85, 47.559983], 56 | [1569176700000, 9968.2, 9987.5, 9964.76, 9982.81, 55.03325], 57 | [1569177000000, 9983.13, 9998.59, 9978.4, 9992.71, 31.224711], 58 | [1569177300000, 9992.7, 10003.89, 9985.65, 9986.31, 37.164782], 59 | [1569177600000, 9986.73, 9995, 9980.11, 9992.38, 41.478021], 60 | [1569177900000, 9993.18, 9994.42, 9965.01, 9965.4, 78.712463], 61 | [1569178200000, 9965.01, 9975.31, 9945.61, 9965.11, 116.804223], 62 | [1569178500000, 9966.07, 9978.85, 9965.14, 9974.26, 36.95313], 63 | [1569178800000, 9972.93, 9983.88, 9970.22, 9975.08, 36.567075], 64 | [1569179100000, 9975.08, 9977.29, 9970.43, 9976.5, 34.725379], 65 | [1569179400000, 9975.6, 9985.16, 9975.32, 9982.85, 20.75081], 66 | [1569179700000, 9984.31, 9988.85, 9981.38, 9986.41, 24.550114], 67 | [1569180000000, 9986.58, 9989.13, 9981.28, 9986.83, 24.182104], 68 | [1569180300000, 9986.25, 9991, 9977.77, 9978.68, 24.172568], 69 | [1569180600000, 9978.65, 9992.62, 9978.51, 9992.53, 20.900339], 70 | [1569180900000, 9992.24, 9992.53, 9970, 9972.08, 35.700914], 71 | [1569181200000, 9972.39, 9990, 9968.33, 9989.07, 45.336494], 72 | [1569181500000, 9988.35, 9995, 9975.37, 9975.37, 51.269654], 73 | [1569181800000, 9975.51, 9995, 9975.41, 9986.06, 37.946446], 74 | [1569182100000, 9985.47, 10000, 9983.24, 9990.04, 36.640409], 75 | [1569182400000, 9989.45, 10008.84, 9982.32, 9999.78, 81.718573], 76 | [1569182700000, 10000.58, 10009.99, 9998.64, 9999, 45.965454], 77 | [1569183000000, 9999.58, 10007.23, 9991.94, 10000.25, 39.585367], 78 | [1569183300000, 10000.57, 10006.83, 9993.52, 9998.52, 36.301146], 79 | [1569183600000, 9998.5, 10015.58, 9996.66, 10004.55, 52.968946], 80 | [1569183900000, 10004.93, 10016.79, 10003, 10007.08, 28.2871], 81 | [1569184200000, 10007.03, 10023.61, 10005.64, 10020.26, 49.232449], 82 | [1569184500000, 10018.74, 10026, 10009.61, 10024.74, 91.785192], 83 | [1569184800000, 10023.49, 10038, 10019.54, 10022.36, 91.51808], 84 | [1569185100000, 10021.44, 10022.68, 10011.68, 10011.98, 34.000808], 85 | [1569185400000, 10011.98, 10012, 9996.74, 10010.13, 47.693509], 86 | [1569185700000, 10010.11, 10017.51, 10009.38, 10013.71, 25.88148], 87 | [1569186000000, 10013.26, 10022.96, 10010.29, 10021.42, 24.718276], 88 | [1569186300000, 10021.55, 10023.14, 10015, 10016.18, 18.956986], 89 | [1569186600000, 10015.13, 10017.94, 10014.9, 10016.71, 17.960617], 90 | [1569186900000, 10016.46, 10017.42, 10008.45, 10008.54, 19.233083], 91 | [1569187200000, 10008.54, 10009.14, 9990.61, 9996.54, 36.134403], 92 | [1569187500000, 9997.33, 10017.57, 9996.69, 10013.42, 38.753674], 93 | [1569187800000, 10014, 10023.51, 10014, 10022.61, 41.245953], 94 | [1569188100000, 10022.21, 10027, 10020.07, 10026.97, 44.058153], 95 | [1569188400000, 10026.97, 10045, 10025.68, 10044.99, 121.190385], 96 | [1569188700000, 10045, 10045, 10034.21, 10036.58, 61.927495], 97 | [1569189000000, 10035.97, 10065.01, 10031.03, 10065.01, 78.283652], 98 | [1569189300000, 10064.97, 10065.98, 10046.63, 10054.32, 120.274509], 99 | [1569189600000, 10054.84, 10066, 10050.63, 10063, 49.798054], 100 | [1569189900000, 10063.93, 10070.2, 10038.81, 10040.59, 93.004095], 101 | [1569190200000, 10040.59, 10047.4, 10031.18, 10040.41, 55.564993], 102 | ]; 103 | -------------------------------------------------------------------------------- /src/__test__/gridbot/gridbot.spec.ts: -------------------------------------------------------------------------------- 1 | import { GridBot, GridBotConfig } from '../../grid_bot'; 2 | import candleData from '../fixtures/candleOHLCV'; 3 | import { OHLCV } from 'sand-ex/build/types'; 4 | 5 | const FEE = 0.00075; // 0.0075% / 100 6 | 7 | const gridBotConfig: GridBotConfig = { 8 | priceLow: 9950, 9 | priceHigh: 10050, 10 | gridQuantity: 4, 11 | balanceQuote: 1000, 12 | fee: FEE, 13 | }; 14 | 15 | const gridBotPerfect: GridBotConfig = { 16 | priceLow: 9000, 17 | priceHigh: 11000, 18 | gridQuantity: 4, 19 | balanceQuote: 1000, 20 | fee: FEE, 21 | }; 22 | 23 | const perfectUpTrendChart = [ 24 | [1, 9000, 9000, 9000, 9000, 100], 25 | [2, 9000, 9000, 9000, 9000, 100], 26 | [3, 9500, 9500, 9500, 9500, 100], 27 | [4, 9500, 9500, 9500, 9500, 100], 28 | [5, 10000, 10000, 10000, 10000, 100], 29 | [6, 10000, 10000, 10000, 10000, 100], 30 | [7, 10500, 10500, 10500, 10500, 100], 31 | [8, 10500, 10500, 10500, 10500, 100], 32 | [9, 11000, 11000, 11000, 11000, 100], 33 | ]; 34 | 35 | const perfectDownTrendChart = [ 36 | [1, 11000, 11000, 11000, 11000, 100], 37 | [2, 10500, 10500, 10500, 10500, 100], 38 | [3, 10500, 10500, 10500, 10500, 100], 39 | [4, 10000, 10000, 10000, 10000, 100], 40 | [5, 10000, 10000, 10000, 10000, 100], 41 | [6, 9500, 9500, 9500, 9500, 100], 42 | [7, 9500, 9500, 9500, 9500, 100], 43 | [8, 9000, 9000, 9000, 9000, 100], 44 | [9, 9000, 9000, 9000, 9000, 100], 45 | ]; 46 | 47 | const perfectGridChart = [ 48 | [1, 9000, 9000, 9000, 9000, 100], 49 | [2, 9000, 9000, 9000, 9000, 100], 50 | [3, 9500, 9500, 9500, 9500, 100], 51 | [4, 9500, 9500, 9500, 9500, 100], 52 | [5, 10000, 10000, 10000, 10000, 100], 53 | [6, 10000, 10000, 10000, 10000, 100], 54 | [7, 10500, 10500, 10500, 10500, 100], 55 | [8, 10500, 10500, 10500, 10500, 100], 56 | [9, 11000, 11000, 11000, 11000, 100], 57 | [10, 11000, 11000, 11000, 11000, 100], 58 | [11, 10500, 10500, 10500, 10500, 100], 59 | [12, 10500, 10500, 10500, 10500, 100], 60 | [13, 10000, 10000, 10000, 10000, 100], 61 | [14, 10000, 10000, 10000, 10000, 100], 62 | [15, 9500, 9500, 9500, 9500, 100], 63 | [16, 9500, 9500, 9500, 9500, 100], 64 | [17, 9000, 9000, 9000, 9000, 100], 65 | [18, 9000, 9000, 9000, 9000, 100], 66 | [19, 9000, 9000, 9000, 9000, 100], 67 | [20, 9000, 9000, 9000, 9000, 100], 68 | [21, 9500, 9500, 9500, 9500, 100], 69 | [22, 9500, 9500, 9500, 9500, 100], 70 | [23, 10000, 10000, 10000, 10000, 100], 71 | [24, 10000, 10000, 10000, 10000, 100], 72 | [25, 10500, 10500, 10500, 10500, 100], 73 | [26, 10500, 10500, 10500, 10500, 100], 74 | [27, 11000, 11000, 11000, 11000, 100], 75 | ]; 76 | 77 | describe('#Gridbot', () => { 78 | let gridBot: GridBot; 79 | 80 | beforeEach(() => { 81 | gridBot = new GridBot(gridBotConfig); 82 | }); 83 | 84 | it('should Constructor set the properties', async () => { 85 | 86 | let sumGridQuote = 0; 87 | 88 | gridBot.grids.forEach(e => { 89 | sumGridQuote += e.maxQuantity * e.priceLow; 90 | }); 91 | 92 | expect(sumGridQuote).toBeLessThanOrEqual(gridBotConfig.balanceQuote); 93 | expect(gridBot.grids).toHaveLength(gridBotConfig.gridQuantity); 94 | }); 95 | 96 | it('should Update set initial orders', async () => { 97 | // Act 98 | gridBot.update(candleData[0] as OHLCV); 99 | 100 | // Assert 101 | const ExchangeOrders = gridBot.exchange.getOrders(); 102 | 103 | expect(ExchangeOrders).toHaveLength(2); 104 | expect(gridBot.grids[0].maxQuantity).toBe(ExchangeOrders[0].origQty); 105 | expect(gridBot.grids[1].maxQuantity).toBe(ExchangeOrders[1].origQty); 106 | }); 107 | }); 108 | 109 | describe('#Perfect Gridbot', () => { 110 | let gridBot: GridBot; 111 | 112 | beforeEach(() => { 113 | gridBot = new GridBot(gridBotPerfect); 114 | }); 115 | 116 | it('should increase Quote up-trend', async () => { 117 | // Arrange 118 | 119 | // Act 120 | for (let i = 0; i < perfectUpTrendChart.length; i++) { 121 | gridBot.update(perfectUpTrendChart[i] as OHLCV); 122 | } 123 | 124 | // Assert 125 | const ExchangeOrders = gridBot.exchange.getOrders(); 126 | 127 | 128 | ExchangeOrders.forEach(order => { 129 | gridBot.exchange.cancelOrder(order.orderId); 130 | }); 131 | 132 | 133 | 134 | const ExchangeBalance = gridBot.exchange.getBalance(); 135 | 136 | 137 | gridBot.grids.forEach(grid => { 138 | expect(grid.ownedQuantity).toBe(0); 139 | expect(grid.activeOrderId).toBeGreaterThan(1); 140 | }); 141 | 142 | expect(ExchangeBalance).toMatchObject({ balanceQuote: 1047.280742845 }); 143 | expect(gridBot).toMatchObject({ 144 | balanceAsset: 0, 145 | balanceQuote: 1047.2807428449998, 146 | }); 147 | }); 148 | 149 | it('should increase Asset down-trend', async () => { 150 | // Arrange 151 | 152 | // Act 153 | for (let i = 0; i < perfectDownTrendChart.length; i++) { 154 | gridBot.update(perfectDownTrendChart[i] as OHLCV); 155 | } 156 | 157 | // Assert 158 | const ExchangeOrders = gridBot.exchange.getOrders(); 159 | 160 | ExchangeOrders.forEach(order => { 161 | gridBot.exchange.cancelOrder(order.orderId); 162 | }); 163 | 164 | const ExchangeBalance = gridBot.exchange.getBalance(); 165 | 166 | gridBot.grids.forEach(grid => { 167 | expect(grid.ownedQuantity).toBeGreaterThan(0); 168 | expect(grid.activeOrderId).toBeGreaterThan(1); 169 | }); 170 | 171 | 172 | expect(ExchangeBalance).toMatchObject({ balanceAsset: 0.09748778928000001 }); 173 | expect(gridBot).toMatchObject({ 174 | balanceAsset: 0.09748776, 175 | }); 176 | }); 177 | 178 | it('should generate Profit perfectGridChart', async () => { 179 | // Arrange 180 | 181 | // Act 182 | for (let i = 0; i < perfectGridChart.length; i++) { 183 | gridBot.update(perfectGridChart[i] as OHLCV); 184 | } 185 | 186 | // Assert 187 | const ExchangeOrders = gridBot.exchange.getOrders(); 188 | 189 | ExchangeOrders.forEach(order => { 190 | gridBot.exchange.cancelOrder(order.orderId); 191 | }); 192 | 193 | const ExchangeBalance = gridBot.exchange.getBalance(); 194 | 195 | gridBot.grids.forEach(grid => { 196 | expect(grid.ownedQuantity).toBe(0); 197 | expect(grid.activeOrderId).toBeGreaterThan(1); 198 | }); 199 | 200 | 201 | expect(ExchangeBalance).toMatchObject({ balanceQuote: 1094.56148569 }); 202 | 203 | }); 204 | 205 | 206 | 207 | }); 208 | -------------------------------------------------------------------------------- /src/__test__/utils/candlestick_generator.ts: -------------------------------------------------------------------------------- 1 | import CandleConvert, { OHLCV } from 'candlestick-convert'; 2 | import { batchedOHLCV } from '../../types'; 3 | import { EXCHANGE_BASE_INTERVAL_IN_SEC } from '../../constants'; 4 | 5 | const timeStep = 60 * 1000; 6 | const defaultTime = 1583925360000; 7 | 8 | export const fakeBatchedCandlestickMap = (intervalsTimeInSec: number[], limit: number): batchedOHLCV => { 9 | const limitCandlestick = ~~(limit + ((Math.max(...intervalsTimeInSec) as number) / EXCHANGE_BASE_INTERVAL_IN_SEC) * 1.5); 10 | 11 | const batch = {}; 12 | const result = new Map(); 13 | 14 | const candledata = [...Array(limitCandlestick)].map((_e, i) => { 15 | const priceMove = ~~(100 * Math.random() - 50); 16 | 17 | return { 18 | time: defaultTime + i * timeStep, 19 | open: 7820.12 + priceMove, 20 | high: 7828.15 + priceMove, 21 | low: 7819.3 + priceMove, 22 | close: 7824.62 + priceMove, 23 | volume: 10 + 10 * Math.random(), 24 | }; 25 | }); 26 | 27 | batch[EXCHANGE_BASE_INTERVAL_IN_SEC] = candledata; 28 | 29 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 30 | batch[EXCHANGE_BASE_INTERVAL_IN_SEC].map((elem: any) => { 31 | result[elem.time] = {}; 32 | result[elem.time][EXCHANGE_BASE_INTERVAL_IN_SEC] = elem; 33 | }); 34 | 35 | if (intervalsTimeInSec.length != 0) { 36 | for (let i = 0; i < intervalsTimeInSec.length; i++) { 37 | const interval = intervalsTimeInSec[i]; 38 | 39 | batch[interval] = CandleConvert.json(candledata as OHLCV[], EXCHANGE_BASE_INTERVAL_IN_SEC, interval); 40 | 41 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 42 | batch[interval].map((elem: any) => { 43 | result[elem.time][interval] = elem; 44 | }); 45 | } 46 | } 47 | 48 | return result as batchedOHLCV; 49 | }; 50 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | export const DEFAULT_BACKTEST_ARRAY_LIMIT = 30000000; 2 | 3 | export const DEFAULT_STRATEGY_OPTIMIZER_INTERVALS = [ 4 | 60, 5 | 120, 6 | 180, 7 | 300, 8 | 600, 9 | 900, 10 | 1200, 11 | 1800, 12 | 3600, 13 | 3600 * 3, 14 | 3600 * 6, 15 | 3600 * 12, 16 | 3600 * 24, 17 | 3600 * 48, 18 | 3600 * 72, 19 | 3600 * 24 * 7, 20 | ]; 21 | 22 | export const DEFAULT_LIVE_STRATEGY_HOT_START_CANDLE_SIZE = 3000; 23 | 24 | export const DEFAULT_TRADERBOT_UPDATELOOP_TIMEOUT = 10 * 1000; 25 | 26 | export const DEFAULT_TRADER_CONFIG = { 27 | stopLossLimit: 0.98, 28 | trailingLimit: 0.02, 29 | portionPct: 50, 30 | balanceAsset: 0, 31 | balanceQuote: 1000, 32 | fee: 0.002, 33 | }; 34 | 35 | export enum EmulatorStates { 36 | LOADED = 'Loaded', 37 | READY = 'Ready', 38 | } 39 | 40 | export const EXCHANGE_BASE_INTERVAL_IN_SEC = 60; 41 | -------------------------------------------------------------------------------- /src/database/index.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | 3 | import { createPool } from 'mysql2/promise'; 4 | 5 | export const BaseDB = createPool({ 6 | host: process.env.MYSQL_HOST, 7 | port: process.env.MYSQL_PORT === undefined ? 3306 : parseInt(process.env.MYSQL_PORT, 10), 8 | user: process.env.MYSQL_USER, 9 | password: process.env.MYSQL_PASS, 10 | database: process.env.MYSQL_DB, 11 | charset: 'utf8mb4', 12 | waitForConnections: true, 13 | multipleStatements: true, 14 | connectionLimit: 10, 15 | queueLimit: 0, 16 | }); 17 | 18 | export const ExchangeDB = createPool({ 19 | host: process.env.MYSQL_HOST_EXCHANGE, 20 | port: process.env.MYSQL_PORT_EXCHANGE === undefined ? 3306 : parseInt(process.env.MYSQL_PORT_EXCHANGE, 10), 21 | user: process.env.MYSQL_USER_EXCHANGE, 22 | password: process.env.MYSQL_PASS_EXCHANGE, 23 | database: process.env.MYSQL_DB_EXCHANGE, 24 | charset: 'utf8mb4', 25 | waitForConnections: true, 26 | multipleStatements: true, 27 | connectionLimit: 10, 28 | queueLimit: 0, 29 | }); 30 | -------------------------------------------------------------------------------- /src/emitter/emitter.ts: -------------------------------------------------------------------------------- 1 | import EventEmitter from 'eventemitter3'; 2 | 3 | export const Emitter = new EventEmitter(); 4 | -------------------------------------------------------------------------------- /src/emitter/index.ts: -------------------------------------------------------------------------------- 1 | export * from './emitter'; 2 | -------------------------------------------------------------------------------- /src/emulator/backtest_emulator.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable prefer-destructuring */ 2 | import { logger } from '../logger'; 3 | 4 | import { Emulator } from './emulator'; 5 | import { BacktestEmulatorInit, Simulation } from '../types'; 6 | import { DEFAULT_BACKTEST_ARRAY_LIMIT } from '../constants'; 7 | 8 | export class BacktestEmulator { 9 | simulation?: Simulation; 10 | performance: number; 11 | historyOrders: any[]; 12 | backTestArrayLimit: number; 13 | config?: BacktestEmulatorInit; 14 | 15 | constructor(backTestArrayLimit?: number) { 16 | this.backTestArrayLimit = backTestArrayLimit ?? DEFAULT_BACKTEST_ARRAY_LIMIT; 17 | 18 | this.performance = 0; 19 | this.historyOrders = []; 20 | } 21 | 22 | // Start backtest instances 23 | async start(config: BacktestEmulatorInit): Promise { 24 | try { 25 | this.config = config; 26 | const symbol = config.symbol; // 'BTC/USDT'; 27 | const exchange = config.exchange; // 'binance'; 28 | const strategy = config.strategy; // 'bb_pure'; 29 | const strategyConfig = config.strategyConfig; // {}; 30 | const traderConfig = config.traderConfig; // {}; 31 | const intervals = config.intervals; // [60,300] 32 | const candledata = config.candledata; // []; 33 | 34 | this.simulation = { 35 | symbol, 36 | exchange, 37 | strategy, 38 | emulator: new Emulator({ 39 | exchange, 40 | symbol, 41 | strategy, 42 | strategyConfig, 43 | intervals, 44 | traderConfig, 45 | }), 46 | }; 47 | 48 | // Start and load first chunk of candle datas into the strategy 49 | await this.simulation.emulator.start(candledata); 50 | 51 | logger.verbose( 52 | `Backtest emulator loaded successfully, Strategy: ${strategy}, Candledata length: ${candledata.size}`, 53 | ); 54 | 55 | this.historyOrders = this.simulation.emulator.TradeEmulator?.historyOrders ?? []; 56 | 57 | this.performance = this.simulation.emulator.TradeEmulator?.getFullBalance() || 0; 58 | 59 | return; 60 | } catch (e) { 61 | logger.error('Backtest Emulator error ', e); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/emulator/emulator.ts: -------------------------------------------------------------------------------- 1 | import { logger } from '../logger'; 2 | import { TradeEmulator } from '../traderbot/trade_emulator'; 3 | import { EmulatorConfig, batchedOHLCV, OHLCVMapFlat } from '../types'; 4 | import { AbstractStrategy } from '../strategies/abstract_strategy'; 5 | import { EmulatorStates } from '../constants'; 6 | 7 | export class Emulator { 8 | intervals: number[]; 9 | config: EmulatorConfig; 10 | strategy: AbstractStrategy; 11 | lastAdvice: any; 12 | lastAction: any; 13 | nextAction: any; 14 | lastUpdateTime: number; 15 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 16 | lastUpdate?: OHLCVMapFlat; 17 | actionList: unknown[]; 18 | TradeEmulator: TradeEmulator | undefined; 19 | backtest: number; 20 | state: EmulatorStates; 21 | balanceQuote: any; 22 | 23 | constructor(config: EmulatorConfig) { 24 | this.config = config; 25 | this.intervals = this.config.intervals; 26 | 27 | this.strategy = new (require('../strategies/' + this.config.strategy + '/'))(this.config.strategyConfig); 28 | 29 | this.lastAdvice; 30 | this.lastAction; 31 | this.nextAction; 32 | this.lastUpdateTime = 0; 33 | 34 | this.actionList = []; 35 | 36 | this.backtest = 0; 37 | 38 | if (this.config.traderConfig) { 39 | this.TradeEmulator = new TradeEmulator(this.config.traderConfig); 40 | this.backtest = 1; 41 | } 42 | 43 | this.state = EmulatorStates.LOADED; 44 | } 45 | 46 | async start(candledata: batchedOHLCV): Promise { 47 | try { 48 | // Hot-start strategy 49 | await this.update(candledata); 50 | 51 | this.state = EmulatorStates.READY; 52 | 53 | return; 54 | } catch (e) { 55 | logger.error('Emulator error ', e); 56 | } 57 | } 58 | 59 | // Price ticker update / Cannot be used for back testing! 60 | async updatePrice(): Promise { 61 | return; 62 | } 63 | 64 | // Candledata / Orderbook update 65 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 66 | async update(candledata: batchedOHLCV, _orderbook?: any): Promise { 67 | try { 68 | let updateTick = 0; 69 | 70 | const updateTimeStamps = Object.keys(candledata); 71 | 72 | for (const timeStamp of updateTimeStamps) { 73 | // Strategy update! 74 | 75 | await this.strategy.update(candledata[timeStamp]); 76 | 77 | // Strategy update! 78 | 79 | if (this.TradeEmulator) { 80 | // Price update 81 | this.TradeEmulator.update(candledata[timeStamp][60]); 82 | } 83 | 84 | if (this.nextAction !== this.lastAction) { 85 | if (this.TradeEmulator) { 86 | this.TradeEmulator.action({ action: this.nextAction, price: candledata[timeStamp][60].open, time: candledata[timeStamp][60].time }); 87 | } 88 | 89 | this.lastAction = this.nextAction; 90 | } 91 | 92 | if (this.strategy.advice !== this.lastAdvice) { 93 | this.nextAction = this.strategy.advice; 94 | 95 | this.lastAdvice = this.strategy.advice; 96 | } 97 | 98 | // Set last update time avoid multiple update 99 | this.lastUpdateTime = parseInt(timeStamp); 100 | this.lastUpdate = candledata[timeStamp]; 101 | updateTick++; 102 | } 103 | 104 | return updateTick as number; 105 | } catch (e) { 106 | logger.error('Emulator error ', e); 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/emulator/live_emulator.ts: -------------------------------------------------------------------------------- 1 | import { logger } from '../logger'; 2 | import _ from 'lodash'; 3 | import tradePairs from '../tradepairs/tradepairs'; 4 | import { Emulator } from './emulator'; 5 | import { BaseDB } from '../database'; 6 | import { RowDataPacket } from 'mysql2'; 7 | import { LiveSimulation } from '../types'; 8 | import { DEFAULT_LIVE_STRATEGY_HOT_START_CANDLE_SIZE } from '../constants'; 9 | 10 | class LiveEmulator { 11 | simulations: LiveSimulation[]; 12 | performance: any[]; 13 | constructor() { 14 | this.simulations = []; 15 | this.performance = []; 16 | } 17 | 18 | // Load all strategies 19 | async start(): Promise { 20 | try { 21 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 22 | this.simulations = (await this.loadStrategiesFromDB()) as LiveSimulation[]; 23 | 24 | const promises = []; 25 | 26 | for (const simulation of this.simulations) { 27 | const candledata = await tradePairs.getBatchedCandlestickMap(simulation.exchange, simulation.symbol, simulation.intervals, DEFAULT_LIVE_STRATEGY_HOT_START_CANDLE_SIZE); 28 | 29 | simulation.emulator = new Emulator(simulation); 30 | 31 | if (candledata && simulation.emulator) { 32 | promises.push(simulation.emulator.start(candledata)); 33 | } 34 | } 35 | 36 | await Promise.all(promises); 37 | 38 | logger.info(`Live emulators started, count: ${this.simulations.length}`); 39 | 40 | // Set update loop every 5 sec 41 | setInterval(async () => { 42 | await this.updateLoop(); 43 | }, 5000); 44 | // Set update loop every 5 sec 45 | } catch (e) { 46 | logger.error('Live Emulator start error ', e); 47 | } 48 | } 49 | 50 | async reloadStrategiesFromDB(): Promise { 51 | try { 52 | // Only add new strategies never delete old ones avoid overwrites /* TODO it need an other level of abstraction */ 53 | const promises = []; 54 | const allSimulations = (await this.loadStrategiesFromDB()) as LiveSimulation[]; 55 | let count = 0; 56 | 57 | for (const newSimulation of allSimulations) { 58 | // If this -1 strategy is not loaded (new) 59 | if (_.findIndex(this.simulations, { guid: newSimulation.guid }) == -1) { 60 | const candledata = await tradePairs.getBatchedCandlestickMap( 61 | newSimulation.exchange, 62 | newSimulation.symbol, 63 | newSimulation.intervals, 64 | DEFAULT_LIVE_STRATEGY_HOT_START_CANDLE_SIZE, 65 | ); 66 | 67 | this.simulations.push(newSimulation); 68 | 69 | const currentIndex = this.simulations.length - 1; 70 | 71 | this.simulations[currentIndex].emulator = new Emulator(this.simulations[currentIndex]); 72 | 73 | this.simulations[currentIndex].firstAdvice = true; 74 | 75 | if (candledata) { 76 | promises.push(this.simulations[currentIndex].emulator?.start(candledata)); 77 | } 78 | 79 | count++; 80 | } 81 | } 82 | 83 | if (count > 0) { 84 | await Promise.all(promises); 85 | logger.verbose(`New live strategy(s) loaded count : ${count}`); 86 | } 87 | } catch (e) { 88 | logger.error('Live Emulator reload error ', e); 89 | } 90 | } 91 | 92 | /* Mass update on all strategy */ 93 | async updateLoop(): Promise { 94 | try { 95 | const promises = []; 96 | const time = Date.now(); 97 | 98 | for (let i = 0; i < this.simulations.length; i++) { 99 | promises.push(this.updateSingle(i)); 100 | } 101 | 102 | await Promise.all(promises); 103 | 104 | logger.verbose(`Live strategies updated, count: ${this.simulations.length} , time: ${time} lastCandleUpdate: ${this.simulations[0]?.emulator?.lastUpdate || 'undefined'} `); 105 | 106 | this.reloadStrategiesFromDB(); 107 | 108 | return; 109 | } catch (e) { 110 | logger.error('Live Emulator update loop error ', e); 111 | } 112 | } 113 | 114 | /* Helper function for Update loop */ 115 | async updateSingle(strategiesId: number): Promise { 116 | try { 117 | const simulation = this.simulations[strategiesId]; 118 | 119 | // Be sure that emulator is Ready for update 120 | if (simulation?.emulator?.state != 'Ready') { 121 | return; 122 | } 123 | 124 | const candledata = await tradePairs.getBatchedCandlestickMap(simulation.exchange, simulation.symbol, simulation.intervals, 10); 125 | 126 | // Get count of the updated candledatas 127 | if (!candledata) { 128 | return; 129 | } 130 | 131 | const updateTick = (await simulation.emulator?.update(candledata)) as number; 132 | 133 | // Validate that strategy got updated 134 | if (updateTick > 0) { 135 | // Save new advice otherwise Idle 136 | if (simulation.emulator?.lastAdvice !== simulation.lastAdvice) { 137 | simulation.lastAdvice = simulation.emulator.lastAdvice; 138 | // Do not save first advice after load solve problem when restart advice buy/sell 139 | if (simulation.firstAdvice == false) { 140 | this.saveAdviceToDB(simulation, simulation.emulator.lastAdvice); 141 | } else { 142 | simulation.firstAdvice = true; 143 | } 144 | } else { 145 | this.saveAdviceToDB(simulation, 'IDLE'); 146 | } 147 | } 148 | } catch (e) { 149 | logger.error('Live Emulator single update error ', e); 150 | } 151 | } 152 | 153 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 154 | getStrategiesPerformance(): any { 155 | return (this.performance = this.simulations.map(simulation => [ 156 | { 157 | symbol: simulation.symbol, 158 | exchange: simulation.exchange, 159 | config: simulation.strategyConfig, 160 | virtualBalance: simulation.emulator?.balanceQuote || 0, 161 | tradeHistory: simulation.emulator?.actionList || [], 162 | }, 163 | ])); 164 | } 165 | 166 | async loadStrategiesFromDB(): Promise { 167 | try { 168 | const [rows] = await BaseDB.query('SELECT `guid`,`symbol`,`exchange`,`strategy`,`strategy_config`,`interval_sec` FROM `trade_strategies` ORDER BY `guid` ASC;'); 169 | 170 | if (rows) { 171 | return (rows as RowDataPacket[]).map((elem: any) => ({ 172 | exchange: elem.exchange, 173 | symbol: elem.symbol, 174 | strategy: elem.strategy, 175 | strategyConfig: elem.strategy_config, 176 | intervals: elem.interval_sec as number[], 177 | })) as LiveSimulation[]; 178 | } 179 | } catch (e) { 180 | logger.error('SQL error', e); 181 | } 182 | } 183 | 184 | async saveAdviceToDB(simulation: LiveSimulation, action: string): Promise { 185 | try { 186 | await BaseDB.query( 187 | 'INSERT INTO `trade_advice` (`strategy`, `strategy_guid`, `strategy_config`, `symbol`, `exchange`, `action` , `time`, `close` ) VALUES (?, ?,? ,? ,? ,? ,? , ?);', 188 | [ 189 | simulation.strategy, 190 | simulation.guid, 191 | JSON.stringify(simulation.strategyConfig), // JSON 192 | simulation.symbol, 193 | simulation.exchange, 194 | action, 195 | simulation.emulator.lastUpdateTime ?? 0, 196 | simulation.emulator.lastUpdate?.['60'].close ?? 0, 197 | ], 198 | ); 199 | 200 | return; 201 | } catch (e) { 202 | logger.error('SQL error', e); 203 | } 204 | } 205 | } 206 | 207 | export default new LiveEmulator(); 208 | -------------------------------------------------------------------------------- /src/emulator/strategy_optimizer.ts: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import { logger } from '../logger'; 3 | import { BacktestEmulator } from './backtest_emulator'; 4 | import { STRATEGIES } from '../strategies/index'; 5 | import { StrategyOptimizerConfig } from '../types'; 6 | import { DEFAULT_STRATEGY_OPTIMIZER_INTERVALS } from '../constants'; 7 | 8 | export class StrategyOptimizer { 9 | constructor(public config: StrategyOptimizerConfig) {} 10 | 11 | private _loadStrategyConfigSchema(name: string): unknown | undefined { 12 | const strategyInfo = STRATEGIES.find(elem => elem.name === name); 13 | 14 | if (strategyInfo?.config) { 15 | return strategyInfo.config; 16 | } 17 | throw new Error(`Strategy config schema not exist, for strategy: ${name} `); 18 | } 19 | 20 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 21 | private _strategyConfigRandomizer(config: any): any { 22 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 23 | const newConfig: any = {}; 24 | 25 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 26 | Object.entries(config).forEach((elem: any) => { 27 | /* 28 | elem[1][0] = min 29 | elem[1][1]elem[1] = max 30 | elem[1][2] = type 31 | elem[1][3] = round (if type == float) */ 32 | 33 | let value = elem[1][0] + (elem[1][1] - elem[1][0]) * Math.random(); 34 | 35 | if (elem[1][2] === 'float') { 36 | value = _.floor(value, elem[1][3]); 37 | } 38 | 39 | if (elem[1][2] === 'int') { 40 | value = parseInt(value, elem[1][3]); 41 | } 42 | 43 | newConfig[elem[0]] = value; 44 | }); 45 | 46 | return newConfig; 47 | } 48 | 49 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 50 | async execute(): Promise { 51 | try { 52 | const result = []; 53 | const promises: Array> = []; 54 | const backtestEmulatorList = []; 55 | const backtestStrategyConfig = []; 56 | const baseStrategyConfig = this._loadStrategyConfigSchema(this.config.strategy); 57 | 58 | // Load default intervals 59 | const intervals = DEFAULT_STRATEGY_OPTIMIZER_INTERVALS; 60 | 61 | for (let i = 0; i < this.config.numberOfExecution; i++) { 62 | backtestEmulatorList[i] = new BacktestEmulator(); 63 | 64 | // Create randomized config 65 | backtestStrategyConfig[i] = this._strategyConfigRandomizer(baseStrategyConfig); 66 | 67 | promises.push( 68 | backtestEmulatorList[i].start({ 69 | symbol: this.config.symbol, 70 | exchange: this.config.exchange, 71 | strategy: this.config.strategy, 72 | strategyConfig: backtestStrategyConfig[i], 73 | traderConfig: this.config.traderConfig, 74 | candledata: this.config.candledata, 75 | intervals, 76 | }), 77 | ); 78 | } 79 | 80 | await Promise.all(promises); 81 | 82 | for (const backtestEmulator of backtestEmulatorList) { 83 | result.push({ 84 | strategy: this.config.strategy, 85 | config: backtestEmulator?.config?.strategyConfig ?? {}, 86 | historyOrders: backtestEmulator?.historyOrders ?? [], 87 | performance: backtestEmulator?.performance ?? 0, 88 | numOfOrders: backtestEmulator?.historyOrders.length ?? 0, 89 | }); 90 | } 91 | 92 | return _.orderBy(result, ['performance']); 93 | } catch (e) { 94 | logger.error('Strategy StrategyOptimizer error ', e); 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/exchange/ccxt_controller.ts: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import * as ccxt from 'ccxt'; 3 | import { logger } from '../logger'; 4 | 5 | import * as ccxtConfig from '../../ccxt_config.json'; 6 | 7 | class ExchangeAPI { 8 | exchanges: any[]; 9 | constructor() { 10 | this.exchanges = []; 11 | } 12 | 13 | /* CCXT API STUFF */ 14 | _isExchangeLoaded(exchange: string): boolean { 15 | const exchangeName = exchange.toLowerCase(); 16 | 17 | if (this.exchanges.find(e => e.exchangeName === exchangeName)) { 18 | return true; 19 | } 20 | 21 | return false; 22 | } 23 | 24 | loadExchangeAPI(exchange: string): any { 25 | try { 26 | const exchangeName = exchange.toLowerCase(); 27 | 28 | // Check if CCXT API already loaded 29 | const exchangeData = this.exchanges.find(e => e.exchangeName === exchangeName); 30 | 31 | if (exchangeData?.api) { 32 | return exchangeData.api; 33 | } 34 | 35 | return this.initNewExchanges(exchangeName).api; 36 | } catch (e) { 37 | logger.error('CCXT load API error ', e); 38 | } 39 | } 40 | 41 | initNewExchanges(exchange: string): any { 42 | const exchangeName = exchange.toLowerCase(); 43 | 44 | const config = ccxtConfig[exchange]; 45 | 46 | if (_.isObject(ccxt[exchangeName]) && _.isObject(config)) { 47 | const api = new ccxt[exchangeName](config); 48 | 49 | if (!this._isExchangeLoaded(exchange)) { 50 | this.exchanges.push({ exchangeName, api }); 51 | } 52 | 53 | return { exchangeName, api }; 54 | } 55 | throw new Error(`Invalid Exchange ${exchangeName}`); 56 | } 57 | 58 | /* CCXT API STUFF */ 59 | } 60 | 61 | export default new ExchangeAPI(); 62 | -------------------------------------------------------------------------------- /src/grid_bot/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Grid bot abstract: 3 | 4 | - should able to set unlimited grid level 5 | - calculate with fee 6 | - able to follow the grid instance fund 7 | - execute trades in order 8 | 9 | */ 10 | import SandExchange from 'sand-ex'; 11 | import { floor } from 'lodash'; 12 | 13 | import { OHLCV, OrderStatus, OrderType, OrderSide } from 'sand-ex/build/types'; 14 | import { logger } from '../logger'; 15 | 16 | const DEFAULT_PRECISION = 8; 17 | 18 | export type GridBotConfig = { 19 | priceLow: number; 20 | priceHigh: number; 21 | gridQuantity: number; 22 | balanceQuote: number; 23 | fee: number; 24 | precision?: number; 25 | }; 26 | 27 | type Grid = { 28 | readonly priceLow: number; 29 | readonly priceHigh: number; 30 | readonly maxQuantity: number; 31 | ownedQuantity: number; 32 | activeOrderId: number | null; 33 | }; 34 | 35 | export class GridBot { 36 | balanceQuote: number; 37 | balanceAsset: number; 38 | assetPerGrid: number; 39 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 40 | orderHistory: any[]; 41 | grids: Grid[]; 42 | exchange: SandExchange; 43 | syncedOrders: Set; 44 | private readonly fee: number; 45 | private readonly precision: number; 46 | 47 | constructor(config: GridBotConfig) { 48 | this.orderHistory = []; 49 | this.syncedOrders = new Set(); 50 | this.balanceQuote = config.balanceQuote; 51 | this.balanceAsset = 0; 52 | this.fee = config.fee; 53 | this.precision = config.precision || DEFAULT_PRECISION; 54 | 55 | this.exchange = new SandExchange({ 56 | balanceAsset: this.balanceAsset, 57 | balanceQuote: this.balanceQuote, 58 | fee: this.fee, 59 | precision: this.precision, 60 | }); 61 | 62 | const pricePerStep = (config.priceHigh - config.priceLow) / config.gridQuantity; 63 | const totalPriceStep = (config.gridQuantity ** 2 + config.gridQuantity) / 2; // n * n + n / 2 64 | 65 | this.assetPerGrid = this.balanceQuote / (config.gridQuantity * config.priceLow + totalPriceStep * pricePerStep); 66 | this.assetPerGrid = floor(this.assetPerGrid, DEFAULT_PRECISION); 67 | 68 | this.grids = [...Array(config.gridQuantity)].map((_, i) => { 69 | const priceLow = config.priceLow + ((config.priceHigh - config.priceLow) / config.gridQuantity) * i; 70 | const priceHigh = config.priceLow + ((config.priceHigh - config.priceLow) / config.gridQuantity) * (i + 1); 71 | 72 | const ownedQuantity = 0; 73 | 74 | return { 75 | priceLow, 76 | priceHigh, 77 | maxQuantity: this.assetPerGrid, 78 | ownedQuantity, 79 | activeOrderId: null, 80 | }; 81 | }); 82 | } 83 | 84 | update(candle: OHLCV): void { 85 | const currentPrice = candle[1]; 86 | 87 | this.exchange.update(candle); 88 | 89 | // Check Orders 90 | this.exchange.getOrders().forEach(order => { 91 | if (this.syncedOrders.has(order.orderId)) { 92 | return; 93 | } 94 | 95 | if (order.status === OrderStatus.FILLED) { 96 | const grindIndex = this.grids.findIndex(grid => grid.activeOrderId === order.orderId); 97 | 98 | if (order.side === OrderSide.BUY) { 99 | const bookedQuantity = floor(order.executedQty * (1 - this.fee), this.precision); 100 | this.balanceAsset += bookedQuantity; 101 | this.balanceQuote -= order.executedQty * order.price; 102 | 103 | this.grids[grindIndex] = { 104 | ...this.grids[grindIndex], 105 | ...{ activeOrderId: null, ownedQuantity: bookedQuantity }, 106 | }; 107 | } 108 | 109 | if (order.side === OrderSide.SELL) { 110 | this.balanceQuote += order.executedQty * order.price * (1 - this.fee); 111 | this.balanceAsset -= order.executedQty; 112 | 113 | this.grids[grindIndex] = { 114 | ...this.grids[grindIndex], 115 | ...{ activeOrderId: null, ownedQuantity: 0 }, 116 | }; 117 | } 118 | 119 | this.orderHistory.push(order); 120 | this.syncedOrders.add(order.orderId); 121 | } 122 | }); 123 | 124 | // Actions 125 | this.grids = this.grids.map(grid => { 126 | const { priceLow, priceHigh, ownedQuantity, maxQuantity, activeOrderId } = grid; 127 | 128 | // Buy 129 | if (currentPrice >= priceLow && activeOrderId === null && ownedQuantity === 0) { 130 | const requiredQuantity = maxQuantity; 131 | 132 | try { 133 | const orderInfo = this.exchange.createNewOrder({ 134 | side: OrderSide.BUY, 135 | type: OrderType.LIMIT, 136 | price: priceLow, 137 | quantity: requiredQuantity, 138 | }); 139 | 140 | return { ...grid, ...{ activeOrderId: orderInfo.orderId } }; 141 | } catch (err) { 142 | logger.error( 143 | `Gridbot createNewOrder error, details: ${JSON.stringify({ 144 | side: OrderSide.BUY, 145 | type: OrderType.LIMIT, 146 | price: priceLow, 147 | quantity: requiredQuantity, 148 | })} `, 149 | ); 150 | } 151 | } 152 | 153 | // Sell 154 | if (currentPrice <= priceHigh && activeOrderId === null && ownedQuantity !== 0) { 155 | try { 156 | const orderInfo = this.exchange.createNewOrder({ 157 | side: OrderSide.SELL, 158 | type: OrderType.LIMIT, 159 | price: priceHigh, 160 | quantity: ownedQuantity, 161 | }); 162 | 163 | return { ...grid, ...{ activeOrderId: orderInfo.orderId } }; 164 | } catch (err) { 165 | logger.error( 166 | `Gridbot createNewOrder error, details: ${JSON.stringify({ 167 | side: OrderSide.SELL, 168 | type: OrderType.LIMIT, 169 | price: priceHigh, 170 | quantity: ownedQuantity, 171 | })} `, 172 | ); 173 | } 174 | } 175 | 176 | return { ...grid }; 177 | }); 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/httpserver/controllers/backtest.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Post, InternalServerErrorException, Body } from '@nestjs/common'; 2 | 3 | import { logger } from '../../logger'; 4 | import { OptimizeConfig, BacktestService } from '../services/backtest.service'; 5 | 6 | @Controller('backtest') 7 | export class BacktestController { 8 | constructor(private backtestService: BacktestService) {} 9 | 10 | @Post('optimize') 11 | async optimize(@Body() optimizeConfig: OptimizeConfig): Promise { 12 | try { 13 | return await this.backtestService.optimize(optimizeConfig); 14 | } catch (err) { 15 | logger.error(`NestJS API error, ${err}`); 16 | 17 | throw new InternalServerErrorException(); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/httpserver/controllers/gridbot.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Post, InternalServerErrorException, Body } from '@nestjs/common'; 2 | 3 | import { logger } from '../../logger'; 4 | import { GridbotService, GridbotConfig } from '../services/gridbot.service'; 5 | 6 | @Controller('gridbot') 7 | export class GridbotController { 8 | constructor(private gridbotService: GridbotService) {} 9 | 10 | @Post('backtest') 11 | async backtest(@Body() gridbotConfig: GridbotConfig): Promise { 12 | try { 13 | return await this.gridbotService.backtest(gridbotConfig); 14 | } catch (err) { 15 | logger.error(`NestJS API error, ${err} , Body: ${JSON.stringify(gridbotConfig)}`); 16 | 17 | throw new InternalServerErrorException(); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/httpserver/controllers/strategy.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, InternalServerErrorException } from '@nestjs/common'; 2 | 3 | import { StrategyInfo } from '../../strategies'; 4 | import { logger } from '../../logger'; 5 | import { StrategyService } from '../services/strategy.service'; 6 | 7 | @Controller('strategy') 8 | export class StrategyController { 9 | constructor(private strategyService: StrategyService) {} 10 | 11 | @Get('all') 12 | getAll(): StrategyInfo[] { 13 | try { 14 | return this.strategyService.getAll(); 15 | } catch (err) { 16 | logger.error(`NestJS API error, ${err}`); 17 | 18 | throw new InternalServerErrorException(); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/httpserver/controllers/tradepairs.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, InternalServerErrorException, Query } from '@nestjs/common'; 2 | import { RowDataPacket } from 'mysql2'; 3 | import { IOHLCV } from 'candlestick-convert'; 4 | import { TradepairsService } from '../services/tradepairs.service'; 5 | import { logger } from '../../logger'; 6 | 7 | @Controller('tradepairs') 8 | export class TradepairsController { 9 | constructor(private tradepairsService: TradepairsService) { } 10 | 11 | @Get('all') 12 | async getAll(): Promise { 13 | try { 14 | return await this.tradepairsService.getAll(); 15 | } catch (err) { 16 | logger.error(`NestJS API error, ${err}`); 17 | 18 | throw new InternalServerErrorException(); 19 | } 20 | } 21 | 22 | @Get('candlestick') 23 | async getCandleStick(@Query() params: any): Promise { 24 | try { 25 | const { exchange, symbol, interval, limit } = params; 26 | 27 | const candleStick = await this.tradepairsService.getCandleStick(exchange, symbol, interval, limit); 28 | 29 | if (!candleStick) { 30 | return []; 31 | } 32 | 33 | return candleStick; 34 | } catch (err) { 35 | logger.error(`NestJS API error, ${err}`); 36 | 37 | throw new InternalServerErrorException(); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/httpserver/index.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { LoggerService } from '@nestjs/common'; 3 | import { logger } from '../logger'; 4 | import { MainModule } from './main.module'; 5 | 6 | class NestLogger implements LoggerService { 7 | log(message: string): void { 8 | logger.verbose(message); 9 | } 10 | error(message: string, trace: string): void { 11 | logger.error(`error: ${message}, trace: ${trace}`); 12 | } 13 | warn(message: string): void { 14 | logger.error(message); 15 | } 16 | debug(message: string): void { 17 | logger.verbose(message); 18 | } 19 | verbose(message: string): void { 20 | logger.verbose(message); 21 | } 22 | } 23 | 24 | export async function bootstrap(port: number): Promise { 25 | const app = await NestFactory.create(MainModule, { 26 | logger: new NestLogger(), 27 | }); 28 | app.enableCors(); 29 | await app.listen(port); 30 | } 31 | -------------------------------------------------------------------------------- /src/httpserver/main.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { TradepairsService } from './services/tradepairs.service'; 3 | import { TradepairsController } from './controllers/tradepairs.controller'; 4 | import { StrategyService } from './services/strategy.service'; 5 | import { StrategyController } from './controllers/strategy.controller'; 6 | import { BacktestService } from './services/backtest.service'; 7 | import { BacktestController } from './controllers/backtest.controller'; 8 | import { GridbotService } from './services/gridbot.service'; 9 | import { GridbotController } from './controllers/gridbot.controller'; 10 | 11 | @Module({ 12 | providers: [TradepairsService, StrategyService, BacktestService, GridbotService], 13 | controllers: [TradepairsController, StrategyController, BacktestController, GridbotController], 14 | }) 15 | export class MainModule { } 16 | -------------------------------------------------------------------------------- /src/httpserver/services/backtest.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | import _ from 'lodash'; 4 | 5 | import { StrategyOptimizer } from '../../emulator/strategy_optimizer'; 6 | import { DEFAULT_STRATEGY_OPTIMIZER_INTERVALS, DEFAULT_TRADER_CONFIG } from '../../constants'; 7 | import tradePairs from '../../tradepairs/tradepairs'; 8 | 9 | export type OptimizeConfig = { 10 | exchange: string; 11 | symbol: string; 12 | strategy: string; 13 | candleLimit: number; 14 | numberOfExecution: number; 15 | }; 16 | 17 | @Injectable() 18 | export class BacktestService { 19 | async optimize(config: OptimizeConfig): Promise { 20 | const exchange = config.exchange ?? 'binance'; 21 | const symbol = config.symbol ?? 'BTC/USDT'; 22 | const strategy = config.strategy ?? 'bb_pure'; 23 | const candleLimit = Number(config.candleLimit) || 3000; 24 | const numberOfExecution = Number(config.numberOfExecution) || 10; 25 | 26 | const candleData = await tradePairs.getBatchedCandlestickMap( 27 | exchange, 28 | symbol, 29 | DEFAULT_STRATEGY_OPTIMIZER_INTERVALS, 30 | candleLimit, 31 | ); 32 | 33 | if (candleData) { 34 | // Strategy optimizer, helper function 35 | const optimizer = new StrategyOptimizer({ 36 | exchange, 37 | symbol, 38 | numberOfExecution, 39 | strategy, 40 | traderConfig: DEFAULT_TRADER_CONFIG, 41 | candledata: candleData, 42 | }); 43 | 44 | const optimizerResult = await optimizer.execute(); 45 | 46 | const candleDataForChart = Object.keys(candleData) 47 | .map((time): any => { 48 | return candleData[time][300]; 49 | }) 50 | .filter(elem => elem !== undefined); 51 | 52 | const response = { 53 | testResults: _.reverse(optimizerResult), 54 | candledata: candleDataForChart, 55 | }; 56 | 57 | return response; 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/httpserver/services/gridbot.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { OHLCV } from 'sand-ex/build/types'; 3 | // import _ from 'lodash'; 4 | 5 | import tradePairs from '../../tradepairs/tradepairs'; 6 | import { GridBotConfig, GridBot } from '../../grid_bot'; 7 | 8 | export interface GridbotConfig { 9 | exchange: string; 10 | symbol: string; 11 | priceLow: number; 12 | priceHigh: number; 13 | gridQuantity: number; 14 | fee: number; 15 | rangeInDays: number; 16 | } 17 | 18 | @Injectable() 19 | export class GridbotService { 20 | async backtest(gridbotConfig: GridbotConfig): Promise { 21 | const exchange = gridbotConfig.exchange ?? 'binance'; 22 | const symbol = gridbotConfig.symbol ?? 'BTC/USDT'; 23 | const { rangeInDays, priceLow, priceHigh, gridQuantity } = gridbotConfig; 24 | const fee = gridbotConfig.fee ?? 0.00075; // 0.0075% / 100, 25 | 26 | const candleLimit = 1440 * rangeInDays; 27 | 28 | const testQuote = 100000; 29 | 30 | const candleData = await tradePairs.getCandlestickFromDB(exchange, symbol, 60, candleLimit); 31 | 32 | if (candleData) { 33 | const candleSticks = candleData.map(c => [c.time, c.open, c.high, c.low, c.close, c.volume]); 34 | 35 | // Strategy optimizer, helper function 36 | const gridBotConfig: GridBotConfig = { 37 | priceLow, 38 | priceHigh, 39 | gridQuantity, 40 | balanceQuote: testQuote, 41 | fee, 42 | }; 43 | 44 | const gridBot = new GridBot(gridBotConfig); 45 | 46 | const profits = []; 47 | 48 | for (let i = 0; i < candleSticks.length; i++) { 49 | gridBot.update((candleSticks[i] as unknown) as OHLCV); 50 | 51 | profits.push(gridBot.balanceQuote + gridBot.balanceAsset * candleSticks[candleSticks.length - 1][4]); 52 | } 53 | 54 | const totalEndBalance = gridBot.balanceQuote + gridBot.balanceAsset * candleSticks[candleSticks.length - 1][4]; 55 | 56 | return { 57 | balanceAsset: gridBot.balanceAsset, 58 | balanceQuote: gridBot.balanceQuote, 59 | totalEndBalance, 60 | profitPct: (totalEndBalance / 100000 - 1) * 100, 61 | orderHistory: gridBot.orderHistory, 62 | }; 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/httpserver/services/strategy.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { STRATEGIES, StrategyInfo } from '../../strategies'; 3 | 4 | @Injectable() 5 | export class StrategyService { 6 | getAll(): StrategyInfo[] { 7 | return STRATEGIES; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/httpserver/services/tradepairs.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { RowDataPacket } from 'mysql2'; 3 | 4 | import { IOHLCV } from 'candlestick-convert'; 5 | import tradePairs from '../../tradepairs/tradepairs'; 6 | 7 | @Injectable() 8 | export class TradepairsService { 9 | async getAll(): Promise { 10 | try { 11 | return await tradePairs.loadAvailableTradePairs(); 12 | } catch (err) { 13 | throw new Error(err); 14 | } 15 | } 16 | async getCandleStick( 17 | exchange: string, 18 | symbol: string, 19 | interval: number, 20 | limit: number, 21 | ): Promise { 22 | try { 23 | return await tradePairs.getCandlestickFromDB(exchange, symbol, interval, limit); 24 | } catch (err) { 25 | throw new Error(err); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/first */ 2 | require('dotenv').config(); 3 | 4 | require('./emitter'); 5 | require('./redis'); 6 | require('./exchange/ccxt_controller'); 7 | 8 | import { logger } from './logger'; 9 | 10 | import liveEmulator from './emulator/live_emulator'; 11 | import Traderbot from './traderbot/traderbot'; 12 | 13 | const { httpPort } = process.env; 14 | 15 | import { bootstrap } from './httpserver'; 16 | 17 | // const httpServer = new HTTPServer(Number(httpPort ?? 3000)); 18 | 19 | async function main(): Promise { 20 | try { 21 | logger.info('Trader Bot Service loading...'); 22 | 23 | liveEmulator.start(); 24 | await Traderbot.start(); 25 | 26 | await bootstrap(Number(httpPort ?? 3000)); 27 | 28 | logger.info('Trader Bot Service started!'); 29 | } catch (err) { 30 | throw new Error(err); 31 | } 32 | } 33 | 34 | main().catch(err => { 35 | logger.error(`'Startup error, reason: ${err}`); 36 | }); 37 | -------------------------------------------------------------------------------- /src/indicators/custom/ATR.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const SMA = require("./SMA"); 3 | /* 4 | Average True Range (ATR) 5 | https://pipbear.com/indicator/average-true-range/ 6 | */ 7 | 8 | const Indicator = function(period = 14) { 9 | this.input = "candle"; 10 | 11 | this.sma = new SMA(period); 12 | 13 | this.buffer = []; 14 | 15 | this.result = 0; 16 | }; 17 | 18 | Indicator.prototype.update = function(candle) { 19 | this.buffer.push(candle); 20 | 21 | // Keep buffer as long as we need 22 | if (this.buffer.length > 2) { 23 | this.buffer.shift(); 24 | 25 | let c1 = this.buffer[0]; 26 | let c2 = this.buffer[1]; 27 | 28 | // True Range = Max(High[1]-Low[1]; High[1] — Close[2]; Close[2]-Low[1]) 29 | let true_range = Math.max( 30 | c1.high - c1.low, 31 | c1.high - c2.close, 32 | c2.close - c1.low 33 | ); 34 | 35 | this.sma.update(true_range); 36 | 37 | this.result = this.sma.result; 38 | } else { 39 | this.result = 0; 40 | } 41 | 42 | return; 43 | }; 44 | 45 | module.exports = Indicator; 46 | -------------------------------------------------------------------------------- /src/indicators/custom/BB.js: -------------------------------------------------------------------------------- 1 | var Indicator = function(BBSettings) { 2 | this.input = 'price'; 3 | this.settings = BBSettings; 4 | // Settings: 5 | // TimePeriod: The amount of samples used for the average. 6 | // NbDevUp: The distance in stdev of the upper band from the SMA. 7 | // NbDevDn: The distance in stdev of the lower band from the SMA. 8 | this.prices = []; 9 | this.diffs = []; 10 | this.age = 0; // age = Warm Up Period 11 | this.sum = 0; 12 | this.sumsq = 0; 13 | this.upper = 0; 14 | this.middle = 0; 15 | this.lower = 0; 16 | this.result = { upper: -1, lower: -1 }; 17 | }; 18 | 19 | Indicator.prototype.update = function(price) { 20 | var tail = this.prices[this.age] || 0; // oldest price in window 21 | var diffsTail = this.diffs[this.age] || 0; // oldest average in window 22 | 23 | this.prices[this.age] = price; 24 | this.sum += price - tail; 25 | this.middle = this.sum / this.prices.length; // SMA value 26 | 27 | // your code: 28 | // this.diffs[this.age] = (price - this.middle); 29 | // customized code (see formula), we have to build a math.pow: 30 | this.diffs[this.age] = Math.pow(price - this.middle, 2); 31 | 32 | // your code: 33 | // this.sumsq += this.diffs[this.age] ** 2 - diffsTail ** 2; 34 | // customized code: 35 | this.sumsq += this.diffs[this.age] - diffsTail; 36 | 37 | // your code: 38 | // var stdev = Math.sqrt(this.sumsq) / this.prices.length; 39 | // customized code (see formula), we have to build a math.sqrt over the whole expression: 40 | var stdev = Math.sqrt(this.sumsq / this.prices.length); 41 | 42 | this.upper = this.middle + this.settings.NbDevUp * stdev; 43 | this.lower = this.middle - this.settings.NbDevDn * stdev; 44 | 45 | this.age = (this.age + 1) % this.settings.TimePeriod; 46 | 47 | this.result = { upper: this.upper, lower: this.lower }; 48 | }; 49 | module.exports = Indicator; 50 | -------------------------------------------------------------------------------- /src/indicators/custom/CCI.js: -------------------------------------------------------------------------------- 1 | /* 2 | * CCI 3 | */ 4 | 5 | var Indicator = function(settings) { 6 | this.input = 'candle'; 7 | this.tp = 0.0; 8 | this.result = false; 9 | this.hist = []; // needed for mean? 10 | this.mean = 0.0; 11 | this.size = 0; 12 | this.constant = settings.constant; 13 | this.maxSize = settings.history; 14 | for (let i = 0; i < this.maxSize; i++) this.hist.push(0.0); 15 | }; 16 | 17 | Indicator.prototype.update = function(candle) { 18 | // We need sufficient history to get the right result. 19 | let tp = (candle.high + candle.close + candle.low) / 3; 20 | if (this.size < this.maxSize) { 21 | this.hist[this.size] = tp; 22 | this.size++; 23 | } else { 24 | for (let i = 0; i < this.maxSize - 1; i++) { 25 | this.hist[i] = this.hist[i + 1]; 26 | } 27 | this.hist[this.maxSize - 1] = tp; 28 | } 29 | 30 | if (this.size < this.maxSize) { 31 | this.result = false; 32 | } else { 33 | this.calculate(tp); 34 | } 35 | }; 36 | 37 | /* 38 | * Handle calculations 39 | */ 40 | Indicator.prototype.calculate = function(tp) { 41 | let sumtp = 0.0; 42 | 43 | for (let i = 0; i < this.size; i++) { 44 | sumtp = sumtp + this.hist[i]; 45 | } 46 | 47 | this.avgtp = sumtp / this.size; 48 | 49 | this.tp = tp; 50 | 51 | let sum = 0.0; 52 | // calculate tps 53 | for (let i = 0; i < this.size; i++) { 54 | let z = this.hist[i] - this.avgtp; 55 | if (z < 0) z = z * -1.0; 56 | sum = sum + z; 57 | } 58 | 59 | this.mean = sum / this.size; 60 | 61 | this.result = (this.tp - this.avgtp) / (this.constant * this.mean); 62 | 63 | // log.debug("===\t", this.mean, "\t", this.tp, '\t', this.TP.result, "\t", sum, "\t", avgtp, '\t', this.result.toFixed(2)); 64 | }; 65 | 66 | module.exports = Indicator; 67 | -------------------------------------------------------------------------------- /src/indicators/custom/CROSS_SMMA.js: -------------------------------------------------------------------------------- 1 | // Awesome oscilator custom with percent! 2 | 3 | var SMMA = require('./SMMA'); 4 | 5 | var Indicator = function(settings) { 6 | this.input = 'price'; 7 | this.result = 0; 8 | this.short_smma = new SMMA(settings.short); 9 | this.long_smma = new SMMA(settings.long); 10 | }; 11 | 12 | Indicator.prototype.update = function(price) { 13 | this.short_smma.update(price); 14 | this.long_smma.update(price); 15 | 16 | this.result = (this.short_smma.result - this.long_smma.result) / this.short_smma.result; 17 | }; 18 | 19 | module.exports = Indicator; 20 | -------------------------------------------------------------------------------- /src/indicators/custom/DEMA.js: -------------------------------------------------------------------------------- 1 | // required indicators 2 | var EMA = require('./EMA.js'); 3 | 4 | var Indicator = function(config) { 5 | this.input = 'price'; 6 | this.result = false; 7 | this.inner = new EMA(config.weight); 8 | this.outer = new EMA(config.weight); 9 | } 10 | 11 | // add a price and calculate the EMAs and 12 | // the result 13 | Indicator.prototype.update = function(price) { 14 | this.inner.update(price); 15 | this.outer.update(this.inner.result); 16 | this.result = 2 * this.inner.result - this.outer.result; 17 | } 18 | 19 | module.exports = Indicator; 20 | -------------------------------------------------------------------------------- /src/indicators/custom/DONCHIAN.js: -------------------------------------------------------------------------------- 1 | /* 2 | Azzx_donchian 3 | https://pipbear.com/price-action-pattern/turtle-strategy/ 4 | */ 5 | 6 | const Indicator = function(period) { 7 | this.input = 'candle'; 8 | 9 | this.period = period; 10 | 11 | this.buffer = []; 12 | 13 | this.result = { 14 | min: 0, 15 | middle: 0, 16 | max: 0, 17 | }; 18 | }; 19 | 20 | Indicator.prototype.update = function(candle) { 21 | this.buffer.push(candle); 22 | 23 | this.result.min = candle.low; 24 | this.result.max = candle.high; 25 | 26 | // Keep buffer as long as we need 27 | if (this.buffer.length > this.period) { 28 | this.buffer.shift(); 29 | } 30 | 31 | for (let i = 0; i < this.buffer.length; i++) { 32 | const elem = this.buffer[i]; 33 | 34 | if (elem.low < this.result.min) { 35 | this.result.min = elem.low; 36 | } 37 | if (elem.high > this.result.max) { 38 | this.result.max = elem.high; 39 | } 40 | } 41 | 42 | this.result.middle = (this.result.min + this.result.max) / 2; 43 | 44 | return; 45 | }; 46 | 47 | module.exports = Indicator; 48 | -------------------------------------------------------------------------------- /src/indicators/custom/EMA.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | // @link http://en.wikipedia.org/wiki/Exponential_moving_average#Exponential_moving_average 3 | 4 | var Indicator = function(weight) { 5 | this.input = "price"; 6 | this.weight = weight; 7 | this.result = false; 8 | this.age = 0; 9 | }; 10 | 11 | Indicator.prototype.update = function(price) { 12 | // The first time we can't calculate based on previous 13 | // ema, because we haven't calculated any yet. 14 | if (this.result === false) this.result = price; 15 | 16 | this.age++; 17 | this.calculate(price); 18 | 19 | return this.result; 20 | }; 21 | 22 | // calculation (based on tick/day): 23 | // EMA = Price(t) * k + EMA(y) * (1 – k) 24 | // t = today, y = yesterday, N = number of days in EMA, k = 2 / (N+1) 25 | Indicator.prototype.calculate = function(price) { 26 | // weight factor 27 | var k = 2 / (this.weight + 1); 28 | 29 | // yesterday 30 | var y = this.result; 31 | 32 | // calculation 33 | this.result = price * k + y * (1 - k); 34 | }; 35 | 36 | module.exports = Indicator; 37 | -------------------------------------------------------------------------------- /src/indicators/custom/LRC.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Linear regression curve 3 | */ 4 | 5 | var Indicator = function(settings) { 6 | this.input = 'price'; 7 | this.depth = settings; 8 | this.result = false; 9 | this.age = 0; 10 | this.history = []; 11 | this.x = []; 12 | /* 13 | * Do not use array(depth) as it might not be implemented 14 | */ 15 | for (var i = 0; i < this.depth; i++) { 16 | this.history.push(0.0); 17 | this.x.push(i); 18 | } 19 | 20 | // log.debug("Created LRC indicator with h: ", this.depth); 21 | }; 22 | 23 | Indicator.prototype.update = function(price) { 24 | // We need sufficient history to get the right result. 25 | if (this.result === false && this.age < this.depth) { 26 | this.history[this.age] = price; 27 | this.age++; 28 | this.result = false; 29 | // log.debug("Waiting for sufficient age: ", this.age, " out of ", this.depth); 30 | // 31 | return; 32 | } 33 | 34 | this.age++; 35 | // shift history 36 | for (var i = 0; i < this.depth - 1; i++) { 37 | this.history[i] = this.history[i + 1]; 38 | } 39 | this.history[this.depth - 1] = price; 40 | 41 | this.calculate(price); 42 | 43 | // log.debug("Checking LRC: ", this.result.toFixed(8), "\tH: ", this.age); 44 | return; 45 | }; 46 | 47 | /* 48 | * Least squares linear regression fitting. 49 | */ 50 | function linreg(values_x, values_y) { 51 | var sum_x = 0; 52 | var sum_y = 0; 53 | var sum_xy = 0; 54 | var sum_xx = 0; 55 | var count = 0; 56 | 57 | /* 58 | * We'll use those variables for faster read/write access. 59 | */ 60 | var x = 0; 61 | var y = 0; 62 | var values_length = values_x.length; 63 | 64 | if (values_length != values_y.length) { 65 | throw new Error('The parameters values_x and values_y need to have same size!'); 66 | } 67 | 68 | /* 69 | * Nothing to do. 70 | */ 71 | if (values_length === 0) { 72 | return [[], []]; 73 | } 74 | 75 | /* 76 | * Calculate the sum for each of the parts necessary. 77 | */ 78 | for (var v = 0; v < values_length; v++) { 79 | x = values_x[v]; 80 | y = values_y[v]; 81 | sum_x += x; 82 | sum_y += y; 83 | sum_xx += x * x; 84 | sum_xy += x * y; 85 | count++; 86 | } 87 | 88 | /* 89 | * Calculate m and b for the formular: 90 | * y = x * m + b 91 | */ 92 | var m = (count * sum_xy - sum_x * sum_y) / (count * sum_xx - sum_x * sum_x); 93 | var b = sum_y / count - (m * sum_x) / count; 94 | 95 | return [m, b]; 96 | } 97 | 98 | /* 99 | * Handle calculations 100 | */ 101 | Indicator.prototype.calculate = function(price) { 102 | // get the reg 103 | var reg = linreg(this.x, this.history); 104 | 105 | // y = a * x + b 106 | this.result = (this.depth - 1) * reg[0] + reg[1]; 107 | }; 108 | 109 | module.exports = Indicator; 110 | -------------------------------------------------------------------------------- /src/indicators/custom/MACD.js: -------------------------------------------------------------------------------- 1 | // required indicators 2 | var EMA = require('./EMA.js'); 3 | 4 | var Indicator = function(config) { 5 | this.input = 'price'; 6 | this.diff = false; 7 | this.short = new EMA(config.short); 8 | this.long = new EMA(config.long); 9 | this.signal = new EMA(config.signal); 10 | } 11 | 12 | Indicator.prototype.update = function(price) { 13 | this.short.update(price); 14 | this.long.update(price); 15 | this.calculateEMAdiff(); 16 | this.signal.update(this.diff); 17 | this.result = this.diff - this.signal.result; 18 | } 19 | 20 | Indicator.prototype.calculateEMAdiff = function() { 21 | var shortEMA = this.short.result; 22 | var longEMA = this.long.result; 23 | 24 | this.diff = shortEMA - longEMA; 25 | } 26 | 27 | module.exports = Indicator; 28 | -------------------------------------------------------------------------------- /src/indicators/custom/MOME.js: -------------------------------------------------------------------------------- 1 | // required indicators 2 | // Momentum Oscillator = (Price today / Price n periods ago) x 100 3 | 4 | var Indicator = function(windowLength) { 5 | this.input = 'price'; 6 | this.windowLength = windowLength; 7 | this.prices = []; 8 | this.result = 0; 9 | }; 10 | 11 | Indicator.prototype.update = function(price) { 12 | this.prices.push(price); 13 | 14 | if (this.prices.length >= this.windowLength) { 15 | this.result = price / this.prices[0] - 1; 16 | this.prices.shift(); 17 | } 18 | 19 | if (this.result > 1) this.result = 1; 20 | }; 21 | 22 | module.exports = Indicator; 23 | -------------------------------------------------------------------------------- /src/indicators/custom/OBI.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | // @link https://www.investopedia.com/terms/o/onbalancevolume.asp 3 | 4 | const _ = require("lodash"); 5 | 6 | var Indicator = function(weight) { 7 | this.input = "price"; 8 | this.weight = weight; 9 | this.result = false; 10 | this.volume_buffer = []; 11 | this.age = 0; 12 | this.last_close = 0; 13 | }; 14 | 15 | Indicator.prototype.update = function(candle) { 16 | this.calculate(candle); 17 | }; 18 | 19 | Indicator.prototype.calculate = function(candle) { 20 | this.volume_buffer.push(candle.volume); 21 | 22 | if (this.volume_buffer.length <= this.weight) { 23 | this.result = 0; 24 | return; 25 | } else { 26 | this.volume_buffer.shift(); 27 | } 28 | 29 | if (candle.close > this.last_close) { 30 | this.result += candle.volume / _.sum(this.volume_buffer); 31 | } 32 | 33 | if (candle.close == this.last_close) { 34 | this.result = 0; 35 | } 36 | 37 | if (candle.close < this.last_close) { 38 | this.result -= candle.volume / _.sum(this.volume_buffer); 39 | } 40 | 41 | this.result *= 0.1; 42 | 43 | if (this.result > 1) this.result = 1; 44 | 45 | if (this.result < -1) this.result = -1; 46 | 47 | this.last_close = candle.close; 48 | }; 49 | 50 | module.exports = Indicator; 51 | -------------------------------------------------------------------------------- /src/indicators/custom/OHCL4.js: -------------------------------------------------------------------------------- 1 | // OPEN + HIGH + LOW + CLOSE / 4 2 | 3 | var Indicator = function() { 4 | this.input = 'candle'; 5 | this.result = 0; 6 | }; 7 | 8 | // the result 9 | Indicator.prototype.update = function(candle) { 10 | this.result = (candle.open + candle.high + candle.low + candle.close) / 4; 11 | }; 12 | 13 | module.exports = Indicator; 14 | -------------------------------------------------------------------------------- /src/indicators/custom/PPO.js: -------------------------------------------------------------------------------- 1 | // required indicators 2 | var EMA = require('./EMA.js'); 3 | 4 | var Indicator = function(config) { 5 | this.result = {}; 6 | this.input = 'price'; 7 | this.macd = 0; 8 | this.ppo = 0; 9 | this.short = new EMA(config.short); 10 | this.long = new EMA(config.long); 11 | this.MACDsignal = new EMA(config.signal); 12 | this.PPOsignal = new EMA(config.signal); 13 | } 14 | 15 | Indicator.prototype.update = function(price) { 16 | this.short.update(price); 17 | this.long.update(price); 18 | this.calculatePPO(); 19 | this.MACDsignal.update(this.result.macd); 20 | this.MACDhist = this.result.macd - this.MACDsignal.result; 21 | this.PPOsignal.update(this.result.ppo); 22 | this.PPOhist = this.result.ppo - this.PPOsignal.result; 23 | 24 | this.result.MACDsignal = this.MACDsignal.result; 25 | this.result.MACDhist = this.MACDhist; 26 | this.result.PPOsignal = this.PPOsignal.result; 27 | this.result.PPOhist = this.PPOhist; 28 | } 29 | 30 | Indicator.prototype.calculatePPO = function() { 31 | this.result.shortEMA = this.short.result; 32 | this.result.longEMA = this.long.result; 33 | this.result.macd = this.result.shortEMA - this.result.longEMA; 34 | this.result.ppo = 100 * (this.result.macd / this.result.longEMA); 35 | } 36 | 37 | module.exports = Indicator; 38 | -------------------------------------------------------------------------------- /src/indicators/custom/RISK.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // stop loss as an indicator 4 | // originally created by scraqz. Thanks! 5 | 6 | const Indicator = function(variance) { 7 | this.input = "candle"; 8 | this.candle = null; 9 | this.price = 0; 10 | this.risk = 0; 11 | this.variance = variance; 12 | }; 13 | 14 | Indicator.prototype.update = function(candle) { 15 | this.candle = candle; 16 | const stoploss = this.price * this.threshold; 17 | if (candle.close < stoploss) { 18 | if (!["stoploss", "freefall"].includes(this.action)) { 19 | // new trend 20 | this.action = "stoploss"; // sell 21 | } else { 22 | this.updatePrice(); // lower our standards 23 | this.action = "freefall"; // strategy should do nothing 24 | } 25 | } else { 26 | if (this.price < candle.close) this.updatePrice(); // trailing 27 | this.action = "continue"; // safe to continue with rest of strategy 28 | } 29 | }; 30 | Indicator.prototype.updatePrice = function() { 31 | this.price = this.candle.close; 32 | }; 33 | Indicator.prototype.long = function(price) { 34 | this.price = price; 35 | this.action = "continue"; // reset in case we are in freefall before a buy 36 | }; 37 | 38 | module.exports = Indicator; 39 | -------------------------------------------------------------------------------- /src/indicators/custom/RSI.js: -------------------------------------------------------------------------------- 1 | // required indicators 2 | var SMMA = require("./SMMA.js"); 3 | 4 | var Indicator = function(interval) { 5 | this.input = "candle"; 6 | this.lastClose = null; 7 | this.weight = interval; 8 | this.avgU = new SMMA(this.weight); 9 | this.avgD = new SMMA(this.weight); 10 | this.u = 0; 11 | this.d = 0; 12 | this.rs = 0; 13 | this.result = 0; 14 | this.age = 0; 15 | }; 16 | 17 | Indicator.prototype.update = function(candle) { 18 | var currentClose = candle.close; 19 | 20 | if (this.lastClose === null) { 21 | // Set initial price to prevent invalid change calculation 22 | this.lastClose = currentClose; 23 | 24 | // Do not calculate RSI for this reason - there's no change! 25 | this.age++; 26 | return; 27 | } 28 | 29 | if (currentClose > this.lastClose) { 30 | this.u = currentClose - this.lastClose; 31 | this.d = 0; 32 | } else { 33 | this.u = 0; 34 | this.d = this.lastClose - currentClose; 35 | } 36 | 37 | this.avgU.update(this.u); 38 | this.avgD.update(this.d); 39 | 40 | this.rs = this.avgU.result / this.avgD.result; 41 | this.result = 100 - 100 / (1 + this.rs); 42 | 43 | if (this.avgD.result === 0 && this.avgU.result !== 0) { 44 | this.result = 100; 45 | } else if (this.avgD.result === 0) { 46 | this.result = 0; 47 | } 48 | 49 | if (this.result > 100) { 50 | this.result = 100; 51 | } 52 | 53 | this.lastClose = currentClose; 54 | this.age++; 55 | }; 56 | 57 | module.exports = Indicator; 58 | -------------------------------------------------------------------------------- /src/indicators/custom/SMA.js: -------------------------------------------------------------------------------- 1 | // required indicators 2 | // Simple Moving Average - O(1) implementation 3 | 4 | var Indicator = function(windowLength) { 5 | this.input = 'price'; 6 | this.windowLength = windowLength; 7 | this.prices = []; 8 | this.result = 0; 9 | this.age = 0; 10 | this.sum = 0; 11 | }; 12 | 13 | Indicator.prototype.update = function(price) { 14 | var tail = this.prices[this.age] || 0; // oldest price in window 15 | this.prices[this.age] = price; 16 | this.sum += price - tail; 17 | this.result = this.sum / this.prices.length; 18 | this.age = (this.age + 1) % this.windowLength; 19 | }; 20 | 21 | module.exports = Indicator; 22 | -------------------------------------------------------------------------------- /src/indicators/custom/SMMA.js: -------------------------------------------------------------------------------- 1 | // required indicators 2 | var SMA = require('./SMA'); 3 | 4 | var Indicator = function (weight) { 5 | this.input = 'price'; 6 | this.sma = new SMA(weight); 7 | this.weight = weight; 8 | this.prices = []; 9 | this.result = 0; 10 | this.age = 0; 11 | } 12 | 13 | Indicator.prototype.update = function (price) { 14 | this.prices[this.age] = price; 15 | 16 | if(this.prices.length < this.weight) { 17 | this.sma.update(price); 18 | } else if(this.prices.length === this.weight) { 19 | this.sma.update(price); 20 | this.result = this.sma.result; 21 | } else { 22 | this.result = (this.result * (this.weight - 1) + price) / this.weight; 23 | } 24 | 25 | this.age++; 26 | } 27 | 28 | module.exports = Indicator; 29 | -------------------------------------------------------------------------------- /src/indicators/custom/STOPLOSS.js: -------------------------------------------------------------------------------- 1 | // stop loss as an indicator 2 | // originally created by scraqz. Thanks! 3 | 4 | const Indicator = function(threshold) { 5 | this.input = 'candle'; 6 | this.candle = null; 7 | this.price = 0; 8 | this.result = 'continue'; // continue 9 | this.threshold = threshold; 10 | }; 11 | 12 | Indicator.prototype.update = function(candle) { 13 | this.candle = candle; 14 | const stoploss = this.price * this.threshold; 15 | if (candle.close < stoploss) { 16 | // new trend 17 | this.result = 'stoploss'; // sell 18 | } else { 19 | if (this.price < candle.close) this.updatePrice(); // trailing 20 | this.result = 'continue'; // safe to continue with rest of strategy 21 | } 22 | }; 23 | Indicator.prototype.updatePrice = function() { 24 | this.price = this.candle.close; 25 | }; 26 | Indicator.prototype.long = function(price) { 27 | this.price = price; 28 | this.result = 'continue'; // reset in case we are in freefall before a buy 29 | }; 30 | 31 | module.exports = Indicator; 32 | -------------------------------------------------------------------------------- /src/indicators/custom/TRIX.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | // required indicators 3 | // TRIX(i) = (EMA3(i) - EMA3(i - 1))/ EMA3(i-1) 4 | // The final formula can be simplified to: 100 * (ema3 / ema3(-1) - 1) 5 | 6 | var EMA = require("./EMA.js"); 7 | 8 | var Indicator = function(weight) { 9 | this.input = "price"; 10 | this.result = 0; 11 | this.ema1 = new EMA(weight); 12 | this.ema2 = new EMA(weight); 13 | this.ema3 = new EMA(weight); 14 | this.BUF = []; 15 | }; 16 | 17 | Indicator.prototype.update = function(price) { 18 | this.ema1.update(price); 19 | this.ema2.update(this.ema1.result); 20 | this.ema3.update(this.ema2.result); 21 | 22 | this.BUF.push(this.ema3.result); 23 | 24 | if (this.BUF.length > 2) { 25 | this.BUF.shift(); 26 | } 27 | 28 | if (this.BUF.length == 2) { 29 | this.result = 100 * (this.BUF[1] / this.BUF[0] - 1); 30 | } 31 | 32 | if (this.result > 1) { 33 | this.result = 1; 34 | } 35 | }; 36 | 37 | module.exports = Indicator; 38 | -------------------------------------------------------------------------------- /src/indicators/custom/TSI.js: -------------------------------------------------------------------------------- 1 | // required indicators 2 | var EMA = require('./EMA.js'); 3 | 4 | var Indicator = function(settings) { 5 | this.input = 'candle'; 6 | this.lastClose = null; 7 | this.tsi = 0; 8 | this.inner = new EMA(settings.long); 9 | this.outer = new EMA(settings.short); 10 | this.absoluteInner = new EMA(settings.long); 11 | this.absoluteOuter = new EMA(settings.short); 12 | } 13 | 14 | Indicator.prototype.update = function(candle) { 15 | var close = candle.close; 16 | var prevClose = this.lastClose; 17 | 18 | if (prevClose === null) { 19 | // Set initial price to prevent invalid change calculation 20 | this.lastClose = close; 21 | // Do not calculate TSI on first close 22 | return; 23 | } 24 | 25 | var momentum = close - prevClose; 26 | 27 | this.inner.update(momentum); 28 | this.outer.update(this.inner.result); 29 | 30 | this.absoluteInner.update(Math.abs(momentum)); 31 | this.absoluteOuter.update(this.absoluteInner.result); 32 | 33 | this.tsi = 100 * this.outer.result / this.absoluteOuter.result; 34 | 35 | this.lastClose = close; 36 | } 37 | 38 | module.exports = Indicator; 39 | -------------------------------------------------------------------------------- /src/indicators/custom/UO.js: -------------------------------------------------------------------------------- 1 | // required indicators 2 | var SMA = require('./SMA.js'); 3 | 4 | var Indicator = function(settings) { 5 | this.input = 'candle'; 6 | this.lastClose = 0; 7 | this.uo = 0; 8 | this.firstWeight = settings.first.weight; 9 | this.secondWeight = settings.second.weight; 10 | this.thirdWeight = settings.third.weight; 11 | this.firstLow = new SMA(settings.first.period); 12 | this.firstHigh = new SMA(settings.first.period); 13 | this.secondLow = new SMA(settings.second.period); 14 | this.secondHigh = new SMA(settings.second.period); 15 | this.thirdLow = new SMA(settings.third.period); 16 | this.thirdHigh = new SMA(settings.third.period); 17 | } 18 | 19 | Indicator.prototype.update = function(candle) { 20 | var close = candle.close; 21 | var prevClose = this.lastClose; 22 | var low = candle.low; 23 | var high = candle.high; 24 | 25 | var bp = close - Math.min(low, prevClose); 26 | var tr = Math.max(high, prevClose) - Math.min(low, prevClose); 27 | 28 | this.firstLow.update(tr); 29 | this.secondLow.update(tr); 30 | this.thirdLow.update(tr); 31 | 32 | this.firstHigh.update(bp); 33 | this.secondHigh.update(bp); 34 | this.thirdHigh.update(bp); 35 | 36 | var first = this.firstHigh.result / this.firstLow.result; 37 | var second = this.secondHigh.result / this.secondLow.result; 38 | var third = this.thirdHigh.result / this.thirdLow.result; 39 | 40 | this.uo = 100 * (this.firstWeight * first + this.secondWeight * second + this.thirdWeight * third) / (this.firstWeight + this.secondWeight + this.thirdWeight); 41 | 42 | this.lastClose = close; 43 | } 44 | 45 | module.exports = Indicator; 46 | -------------------------------------------------------------------------------- /src/indicators/custom/WA.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /* 3 | The Alligator’s Jaw, the “Blue” line, is a 13-period Smoothed Moving Average, moved into the future by 8 bars; 4 | 5 | The Alligator’s Teeth, the “Red” line, is an 8-period Smoothed Moving Average, moved by 5 bars into the future; 6 | 7 | The Alligator’s Lips, the “Green” line, is a 5-period Smoothed Moving Average, moved by 3 bars into the future. 8 | 9 | 10 | // Standard usage: 11 | BUY: 12 | this.BUF.wa[this.step].lips > this.BUF.wa[this.step].jaw && 13 | this.BUF.wa[this.step].lips > this.BUF.wa[this.step].teeth && 14 | this.BUF.wa[this.step].teeth > this.BUF.wa[this.step].jaw 15 | 16 | SELL: 17 | this.BUF.wa[this.step].jaw > this.BUF.wa[this.step].lips && 18 | this.BUF.wa[this.step].jaw > this.BUF.wa[this.step].teeth && 19 | this.BUF.wa[this.step].teeth > this.BUF.wa[this.step].lips 20 | 21 | 22 | */ 23 | // required indicators 24 | var SMMA = require("./SMMA.js"); 25 | 26 | var Indicator = function( 27 | settings = { 28 | jawLength: 13, 29 | teethLength: 8, 30 | lipsLength: 5, 31 | jawOffset: 8, 32 | teethOffset: 5, 33 | lipsOffset: 3 34 | } 35 | ) { 36 | this.input = "candle"; 37 | 38 | this.signal_smma = new SMMA(5); 39 | 40 | this.jawLength = new SMMA(settings.jawLength); // Blue 41 | this.teethLength = new SMMA(settings.teethLength); // Teeth 42 | this.lipsLength = new SMMA(settings.lipsLength); // Lips 43 | 44 | this.jawOffset = new SMMA(settings.jawOffset); 45 | this.teethOffset = new SMMA(settings.teethOffset); 46 | this.lipsOffset = new SMMA(settings.lipsOffset); 47 | 48 | this.result = { jaw: 0, teeth: 0, lips: 0, signal: 0 }; 49 | }; 50 | 51 | Indicator.prototype.update = function(candle) { 52 | let price = (candle.high + candle.low) / 2; 53 | 54 | this.signal_smma.update(price); 55 | this.jawLength.update(price); 56 | this.teethLength.update(price); 57 | this.lipsLength.update(price); 58 | this.jawOffset.update(price); 59 | this.teethOffset.update(price); 60 | this.lipsOffset.update(price); 61 | 62 | this.signal = this.signal_smma.result; 63 | 64 | this.jaw = this.signal + (this.jawLength.result - this.jawOffset.result); 65 | this.teeth = 66 | this.signal + (this.teethLength.result - this.teethOffset.result); 67 | this.lips = this.signal + (this.lipsLength.result - this.lipsOffset.result); 68 | 69 | this.result = { 70 | jaw: this.jaw, 71 | teeth: this.teeth, 72 | lips: this.lips, 73 | signal: this.signal 74 | }; 75 | }; 76 | 77 | module.exports = Indicator; 78 | -------------------------------------------------------------------------------- /src/indicators/custom/WF.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /* 3 | Williams fractal 4 | http://fxcodebase.com/wiki/index.php/Fractal_Indicator 5 | */ 6 | 7 | var Indicator = function(period = 2) { 8 | this.input = "candle"; 9 | 10 | this.period = period; 11 | 12 | this.buffer = []; 13 | 14 | this.result = "none"; 15 | }; 16 | 17 | Indicator.prototype.update = function(candle) { 18 | this.result = "none"; 19 | this.buffer.push(candle); 20 | 21 | if (this.buffer.length > this.period * 2 + 1) { 22 | this.buffer.shift(); 23 | } 24 | 25 | if (this.buffer.length == this.period * 2 + 1) { 26 | let low = 0; 27 | let high = 0; 28 | 29 | for (let i = 0; i <= this.period * 2; i++) { 30 | if (this.buffer[i].low > this.buffer[this.period].low) { 31 | low++; 32 | } 33 | if (this.buffer[i].high < this.buffer[this.period].high) { 34 | high++; 35 | } 36 | } 37 | 38 | if (low == 2 * this.period) { 39 | this.result = "down"; 40 | } 41 | 42 | if (high == 2 * this.period) { 43 | this.result = "up"; 44 | } 45 | } 46 | 47 | return; 48 | }; 49 | 50 | module.exports = Indicator; 51 | -------------------------------------------------------------------------------- /src/indicators/custom/WF_SMA.js: -------------------------------------------------------------------------------- 1 | /* 2 | Williams fractal 3 | http://fxcodebase.com/wiki/index.php/Fractal_Indicator 4 | */ 5 | 6 | var Indicator = function(period = 2) { 7 | this.input = 'price'; 8 | 9 | this.period = period; 10 | 11 | this.buffer = []; 12 | 13 | this.result = 'none'; 14 | }; 15 | 16 | Indicator.prototype.update = function(price) { 17 | this.result = 'none'; 18 | this.buffer.push(price); 19 | 20 | if (this.buffer.length > this.period * 2 + 1) { 21 | this.buffer.shift(); 22 | } 23 | 24 | if (this.buffer.length == this.period * 2 + 1) { 25 | let low = 0; 26 | let high = 0; 27 | 28 | for (let i = 0; i <= this.period * 2; i++) { 29 | if (this.buffer[i] > this.buffer[this.period]) { 30 | low++; 31 | } 32 | if (this.buffer[i] < this.buffer[this.period]) { 33 | high++; 34 | } 35 | } 36 | 37 | if (low == 2 * this.period) { 38 | this.result = 'down'; 39 | } 40 | 41 | if (high == 2 * this.period) { 42 | this.result = 'up'; 43 | } 44 | } 45 | 46 | return; 47 | }; 48 | 49 | module.exports = Indicator; 50 | -------------------------------------------------------------------------------- /src/indicators/index.js: -------------------------------------------------------------------------------- 1 | class TAIndicators { 2 | /* 3 | label = @string unique name to indentify in the strategy 4 | updateInterval = @number Candlestick/Tickchart update interval 60,120,180,300 5 | nameTA = @string Name of the TA script or Talib or Tulipb 6 | params = @number/object Required parameters 7 | params2 = @Optional parametes like O,H,L,C,V values for price update 8 | */ 9 | 10 | constructor(config = { label: 'label', updateInterval: 60, nameTA: 'ema', params: {}, params2: 'ohlcv/4' }) { 11 | Object.assign(this, config); 12 | 13 | const indicator_base = require(`./custom/${this.nameTA}`); 14 | 15 | this.indicator = new indicator_base(this.params); 16 | 17 | this.result = -1; 18 | this.lastUpdate = -1; 19 | } 20 | 21 | update(candle, step) { 22 | this.lastUpdate = step; 23 | 24 | // Price update 25 | if (this.indicator.input === 'price') { 26 | switch (this.params2) { 27 | case 'open': 28 | this.indicator.update(candle.open); 29 | break; 30 | case 'close': 31 | this.indicator.update(candle.close); 32 | break; 33 | case 'high': 34 | this.indicator.update(candle.high); 35 | break; 36 | case 'low': 37 | this.indicator.update(candle.low); 38 | break; 39 | default: 40 | // ohlcv/4 41 | this.indicator.update((candle.open + candle.close + candle.high + candle.low) / 4); 42 | break; 43 | } 44 | } // Candle 45 | else { 46 | this.indicator.update(candle); 47 | } 48 | this.result = this.indicator.result; 49 | return this.result; 50 | } 51 | } 52 | 53 | module.exports = TAIndicators; 54 | -------------------------------------------------------------------------------- /src/logger/index.ts: -------------------------------------------------------------------------------- 1 | import winston from 'winston'; 2 | 3 | const logsDir = './logs/'; 4 | 5 | const loggerLevel = process.env.logLevel || 'info'; 6 | 7 | export const logger = winston.createLogger({ 8 | level: loggerLevel, 9 | format: winston.format.json(), 10 | transports: [ 11 | new winston.transports.Console({ 12 | level: loggerLevel, 13 | format: winston.format.simple(), 14 | }), 15 | new winston.transports.File({ 16 | filename: `${logsDir}error.log`, 17 | level: 'error', 18 | }), 19 | ], 20 | }); 21 | -------------------------------------------------------------------------------- /src/redis/channels/candlestick_redis.ts: -------------------------------------------------------------------------------- 1 | import { Emitter } from '../../emitter'; 2 | 3 | import { RedisPub } from '../redis'; 4 | 5 | // Broadcast final candle updates 6 | Emitter.on('CandleUpdateFinal', (exchange, interval, candle) => { 7 | setImmediate(() => { 8 | RedisPub.publish('CandleUpdateFinal', JSON.stringify({ exchange, interval, candle })); 9 | }); 10 | }); 11 | 12 | /* Receive code */ 13 | /* 14 | const { Redis } = require("../index") 15 | 16 | 17 | Redis.subscribe("CandleUpdate", function(err, count) { 18 | 19 | }) 20 | 21 | Redis.on("message", function(channel, message) { 22 | 23 | }) 24 | */ 25 | -------------------------------------------------------------------------------- /src/redis/index.ts: -------------------------------------------------------------------------------- 1 | require('./channels/candlestick_redis'); 2 | -------------------------------------------------------------------------------- /src/redis/redis.ts: -------------------------------------------------------------------------------- 1 | import IORedis, { RedisOptions } from 'ioredis'; 2 | 3 | const redisConfig: RedisOptions = { 4 | host: process.env.REDIS_HOST, // Redis host 5 | port: process.env.REDIS_PORT === undefined ? 6379 : parseInt(process.env.REDIS_PORT, 10), // Redis port 6 | family: 4, // 4 (IPv4) or 6 (IPv6) 7 | password: process.env.REDIS_AUTH, 8 | db: process.env.REDIS_DB_ID === undefined ? 0 : parseInt(process.env.REDIS_DB_ID, 10), 9 | retryStrategy: (times: number) => { 10 | const delay = Math.min(times * 50, 2000); 11 | return delay; 12 | }, 13 | }; 14 | 15 | // 16 | // https://github.com/luin/ioredis#pubsub 17 | // 18 | // Subscription connection it cannot be used for Publish! 19 | export const Redis = new IORedis(redisConfig); 20 | // eslint-disable-next-line @typescript-eslint/explicit-function-return-type 21 | Redis.publish = () => { 22 | throw new Error('Subscription connection cannot be used for publish!'); 23 | }; 24 | // Publish connection it can be used only for Publish! 25 | export const RedisPub = new IORedis(redisConfig); 26 | // eslint-disable-next-line @typescript-eslint/explicit-function-return-type 27 | RedisPub.subscribe = () => { 28 | throw new Error('Publisher connection cannot be used for subscribe!'); 29 | }; 30 | -------------------------------------------------------------------------------- /src/strategies/abstract_strategy/index.ts: -------------------------------------------------------------------------------- 1 | import { logger } from '../../logger'; 2 | import TAIndicators from '../../indicators'; 3 | import { OHLCV } from 'candlestick-convert'; 4 | import { OHLCVMap, configTA } from '../../types'; 5 | 6 | export class AbstractStrategy { 7 | intervals: number[]; 8 | advice: any; 9 | tradeHistory: any; 10 | TA_BUFFER: {}; 11 | TA: {}; 12 | step: number; 13 | minimumPreLoadHistory: number; 14 | currentTrade: { priceBuy: number; priceSell: number; candlePatternSnapshot: OHLCV[] }; 15 | candleBuffer: OHLCVMap; 16 | constructor() { 17 | this.advice = ''; 18 | this.TA_BUFFER = {}; 19 | this.TA = {}; 20 | this.advice; 21 | this.step = -1; 22 | this.minimumPreLoadHistory = 100; 23 | 24 | this.intervals = []; 25 | 26 | this.candleBuffer = new Map() as OHLCVMap; 27 | 28 | // ML part 29 | this.tradeHistory = []; 30 | this.currentTrade = { 31 | priceBuy: 0, 32 | priceSell: 0, 33 | candlePatternSnapshot: [], 34 | }; 35 | } 36 | 37 | getTAValueByLabel(label: string): any { 38 | return this.TA_BUFFER[label][this.step]; 39 | } 40 | 41 | getTAAgeByLabel(label: string): any { 42 | return this.TA[label].lastUpdate; 43 | } 44 | 45 | isStrategyReady(): boolean { 46 | return this.step > this.minimumPreLoadHistory; 47 | } 48 | 49 | updateWithCandle(candledata: OHLCVMap): void { 50 | this.updateTA(candledata); 51 | return; 52 | } 53 | 54 | async update(candledata: OHLCVMap) { 55 | try { 56 | // Update buffers and indicators 57 | this.updateWithCandle(candledata); 58 | 59 | if (this.isStrategyReady()) { 60 | // 61 | // Strategy logic 62 | // 63 | return; 64 | } 65 | } catch (e) { 66 | logger.error(`Strategy update error: ${e}`); 67 | } 68 | } 69 | 70 | updateTA(candledata: OHLCVMap): void { 71 | try { 72 | Object.keys(candledata).forEach(interval => { 73 | Object.keys(this.TA).forEach(label => { 74 | if (Number(this.TA[label].updateInterval) === Number(interval)) { 75 | this.TA[label].update(candledata[interval], this.step); 76 | } 77 | }); 78 | }); 79 | 80 | this.updateTA_BUFFER(); 81 | 82 | return; 83 | } catch (e) { 84 | logger.error('AbstractStrategy TA Update error ', e); 85 | } 86 | } 87 | 88 | // eslint-disable-next-line @typescript-eslint/camelcase 89 | updateTA_BUFFER(): void { 90 | Object.keys(this.TA).forEach(label => { 91 | this.TA_BUFFER[label].push(this.TA[label].result); 92 | }); 93 | this.step++; // SUPER IMPORTANT!!!! 94 | 95 | return; 96 | } 97 | 98 | addNewCandleInterval(interval: number): void { 99 | if (this.intervals.indexOf(interval) == -1) { 100 | this.intervals.push(interval); 101 | this.candleBuffer[interval] = []; 102 | } 103 | } 104 | 105 | addNeWTA(config: configTA): void { 106 | try { 107 | const label = config.label; 108 | this.addNewCandleInterval(config.updateInterval); 109 | 110 | if (typeof this.TA_BUFFER[label] == 'undefined' && typeof this.TA[label] == 'undefined') { 111 | this.TA[label] = new TAIndicators(config); 112 | 113 | this.addTA_BUFFER(label); 114 | } 115 | } catch (e) { 116 | logger.error('AbstractStrategy Add TA error ', e); 117 | } 118 | } 119 | 120 | // eslint-disable-next-line @typescript-eslint/camelcase 121 | addTA_BUFFER(name: string): void { 122 | if (typeof this.TA_BUFFER[name] == 'undefined') { 123 | this.TA_BUFFER[name] = []; 124 | } 125 | } 126 | 127 | // eslint-disable-next-line @typescript-eslint/camelcase 128 | snapshotTA_BUFFER(snapshotLength = 10): any { 129 | try { 130 | const snapshot: any = []; 131 | 132 | for (let k = snapshotLength; k >= 0; k--) { 133 | Object.keys(this.TA).forEach(label => { 134 | if (this.TA[label].nameTA == 'BB') { 135 | snapshot.push(this.TA_BUFFER[label][this.step - k].upper); 136 | snapshot.push(this.TA_BUFFER[label][this.step - k].lower); 137 | } else if (this.TA[label].nameTA == 'DONCHIAN') { 138 | snapshot.push(this.TA_BUFFER[label][this.step - k].min); 139 | snapshot.push(this.TA_BUFFER[label][this.step - k].max); 140 | snapshot.push(this.TA_BUFFER[label][this.step - k].middle); 141 | } else { 142 | snapshot.push(this.TA_BUFFER[label][this.step - k]); 143 | } 144 | }); 145 | } 146 | 147 | return snapshot; 148 | } catch (e) { 149 | logger.error('AbstractStrategy ML_data_snapshot', e); 150 | } 151 | } 152 | 153 | resetCurrentTrade(): void { 154 | this.currentTrade = { 155 | candlePatternSnapshot: [], 156 | priceBuy: 0, 157 | priceSell: 0, 158 | }; 159 | } 160 | 161 | BUY(price: number /*, amount = "all"*/): void { 162 | if (this.advice == 'BUY') return; 163 | 164 | // ML /* TODO add config! */ 165 | this.currentTrade.priceBuy = price; 166 | this.currentTrade.candlePatternSnapshot = this.snapshotTA_BUFFER(7); 167 | 168 | this.advice = 'BUY'; 169 | } 170 | 171 | SELL(price: number /*, amount = "all"*/): void { 172 | if (this.advice == 'SELL') return; 173 | 174 | if (this.currentTrade.candlePatternSnapshot.length > 0) { 175 | this.advice = 'SELL'; 176 | 177 | this.currentTrade.priceSell = price; 178 | this.tradeHistory.push(this.currentTrade); 179 | this.resetCurrentTrade(); 180 | } 181 | } 182 | 183 | IDLE(): void { 184 | if (this.advice == 'IDLE') return; 185 | 186 | this.advice = 'IDLE'; 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /src/strategies/bb_pure/index.js: -------------------------------------------------------------------------------- 1 | const logger = require('../../logger'); 2 | const { AbstractStrategy } = require('../abstract_strategy'); 3 | 4 | class Strategy extends AbstractStrategy { 5 | constructor(config = {}) { 6 | super(); 7 | 8 | const bb_period = config.bb_period || 21; 9 | const bb_up = config.bb_up || 2.15; 10 | const bb_down = config.bb_down || 2.15; 11 | 12 | // General Strategy config 13 | this.predict_on = 0; 14 | this.learn = 1; 15 | // General Strategy config 16 | 17 | // TA Indicators 18 | this.addNeWTA({ 19 | label: 'BB', 20 | updateInterval: 1200, 21 | nameTA: 'BB', 22 | params: { 23 | TimePeriod: bb_period, 24 | NbDevUp: bb_up, 25 | NbDevDn: bb_down, 26 | }, 27 | params2: 'ohlcv/4', 28 | }); 29 | } 30 | 31 | async update(candledata) { 32 | try { 33 | // Update buffers and indicators 34 | this.updateWithCandle(candledata); 35 | 36 | if (this.isStrategyReady()) { 37 | let candle = candledata[60]; 38 | 39 | if (candle.low < this.TA_BUFFER.BB[this.step].lower) { 40 | this.BUY(candle.close); 41 | } 42 | 43 | // Sell 44 | if (candle.high > this.TA_BUFFER.BB[this.step].upper) { 45 | this.SELL(candle.close); 46 | } 47 | } 48 | } catch (e) { 49 | logger.error('Strategy error ', e); 50 | } 51 | } 52 | } 53 | 54 | module.exports = Strategy; 55 | -------------------------------------------------------------------------------- /src/strategies/index.ts: -------------------------------------------------------------------------------- 1 | // Store strategies name, description and config range! 2 | 3 | export type StrategyInfo = { 4 | guid: number; 5 | name: string; 6 | desc: string; 7 | config: Record; 8 | }; 9 | 10 | export const STRATEGIES: StrategyInfo[] = [ 11 | { 12 | guid: 1, 13 | name: 'bb_pure', 14 | desc: 'Bband strategy', 15 | config: { 16 | bb_period: [21, 21, 'int'], 17 | bb_up: [1.4, 2.2, 'float', 2], 18 | bb_down: [1.4, 2.2, 'float', 2], 19 | }, 20 | }, 21 | { 22 | guid: 2, 23 | name: 'rsi_macd', 24 | desc: 'RSI / MACD', 25 | config: { 26 | rsi_buy: [0, 100, 'float', 2], 27 | rsi_sell: [0, 100, 'float', 2], 28 | }, 29 | }, 30 | ]; 31 | -------------------------------------------------------------------------------- /src/strategies/ml_train/index.js: -------------------------------------------------------------------------------- 1 | const logger = require('../../logger'); 2 | const { AbstractStrategy } = require('../abstract_strategy'); 3 | 4 | class Strategy extends AbstractStrategy { 5 | constructor(config = {}) { 6 | super(); 7 | 8 | this.rsi_buy = config.rsi_buy || 30; 9 | this.rsi_sell = config.rsi_sell || 70; 10 | 11 | // General Strategy config 12 | this.predict_on = 0; 13 | this.learn = 1; 14 | // General Strategy config 15 | 16 | // TA Indicators 17 | this.addNeWTA({ label: 'RSI', updateInterval: 60, nameTA: 'RSI', params: 15, params2: '' }); 18 | this.addNeWTA({ label: 'MACD', updateInterval: 60, nameTA: 'MACD', params: { short: 12, long: 26, signal: 9 }, params2: '' }); 19 | this.addNeWTA({ label: 'CCI', updateInterval: 60, nameTA: 'CCI', params: { constant: 0.015, history: 14 }, params2: '' }); 20 | this.addNeWTA({ label: 'CROSS_SMMA', updateInterval: 60, nameTA: 'CROSS_SMMA', params: { short: 8, long: 13 }, params2: '' }); 21 | this.addNeWTA({ label: 'MOME', updateInterval: 60, nameTA: 'MOME', params: 100, params2: '' }); 22 | this.addNeWTA({ label: 'DONCHIAN', updateInterval: 60, nameTA: 'DONCHIAN', params: 20, params2: '' }); 23 | } 24 | 25 | async update(candledata) { 26 | try { 27 | // Update buffers and incidators 28 | this.updateWithCandle(candledata); 29 | 30 | if (this.isStrategyReady()) { 31 | // Stop loss sell 32 | if (this.advice == 'BUY' && this.STOP_LOSS == 'stoploss') { 33 | this.SELL(); 34 | } 35 | 36 | // Buy 37 | if (this.step % 11 == 0) { 38 | this.BUY(); 39 | } 40 | 41 | // Sell 42 | if (this.step % 21 == 0) { 43 | this.SELL(); 44 | } 45 | } 46 | } catch (e) { 47 | logger.error('Strategy error ', e); 48 | } 49 | } 50 | } 51 | 52 | module.exports = Strategy; 53 | -------------------------------------------------------------------------------- /src/strategies/rsi_macd/index.js: -------------------------------------------------------------------------------- 1 | const logger = require('../../logger'); 2 | const { AbstractStrategy } = require('../abstract_strategy'); 3 | 4 | class Strategy extends AbstractStrategy { 5 | constructor(config = {}) { 6 | super(); 7 | 8 | this.rsi_buy = config.rsi_buy || 30; 9 | this.rsi_sell = config.rsi_sell || 70; 10 | 11 | // General Strategy config 12 | this.predict_on = 0; 13 | this.learn = 1; 14 | // General Strategy config 15 | 16 | // TA Indicators 17 | this.addNeWTA({ label: 'SMA_5_5min', updateInterval: 300, nameTA: 'SMA', params: 5, params2: 'ohlc/4' }); 18 | this.addNeWTA({ label: 'RSI_5', updateInterval: 900, nameTA: 'RSI', params: 15, params2: '' }); 19 | this.addNeWTA({ label: 'RSI_60', updateInterval: 3600, nameTA: 'RSI', params: 15, params2: '' }); 20 | } 21 | 22 | async update(candledata) { 23 | try { 24 | // Update buffers and incidators 25 | this.updateWithCandle(candledata); 26 | 27 | if (this.isStrategyReady()) { 28 | let candle = candledata[60]; 29 | 30 | if (this.TA_BUFFER.RSI_5[this.step] < this.rsi_buy && this.TA_BUFFER.RSI_60[this.step] < this.rsi_buy) { 31 | this.BUY(candle.close); 32 | } 33 | 34 | if (this.TA_BUFFER.RSI_5[this.step] > this.rsi_sell && this.TA_BUFFER.RSI_60[this.step] > this.rsi_sell) { 35 | this.SELL(candle.close); 36 | } 37 | } 38 | } catch (e) { 39 | logger.error('Strategy error ', e); 40 | } 41 | } 42 | } 43 | 44 | module.exports = Strategy; 45 | -------------------------------------------------------------------------------- /src/strategies/utils/ml_api/index.ts: -------------------------------------------------------------------------------- 1 | // Used for Tensorflow service communication 2 | // TODO: Use Websocket 3 | 4 | const axios = require('axios'); 5 | 6 | export const mlAPI = { 7 | predict: async (input: any, name: any) => { 8 | let result = [0, 0]; 9 | 10 | let predict = await axios.post('http://localhost:3000/' + name + '/predict', { 11 | input, 12 | }); 13 | 14 | if (predict) { 15 | result = predict.data; 16 | } 17 | 18 | return result; 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /src/tradepairs/tradepairs.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-extra-semi */ 2 | 3 | import _ from 'lodash'; 4 | import CandleConvert, { Trade, OHLCV, IOHLCV } from 'candlestick-convert'; 5 | import { RowDataPacket } from 'mysql2'; 6 | 7 | import { batchedOHLCV } from '../types'; 8 | 9 | import { Utils } from '../utils'; 10 | import { logger } from '../logger'; 11 | import { BaseDB, ExchangeDB } from '../database'; 12 | import { EXCHANGE_BASE_INTERVAL_IN_SEC } from '../constants'; 13 | 14 | class TradePairs { 15 | public async getBatchedCandlestickMap( 16 | exchange: string, 17 | symbol: string, 18 | intervalsTimeInSec: number[] = [EXCHANGE_BASE_INTERVAL_IN_SEC], 19 | limit: number, 20 | ): Promise { 21 | try { 22 | const limitCandlestick = ~~( 23 | limit + 24 | ((_.max(intervalsTimeInSec) as number) / EXCHANGE_BASE_INTERVAL_IN_SEC) * 1.5 25 | ); 26 | 27 | const batch = {}; 28 | const result = new Map(); 29 | 30 | if (exchange && symbol && intervalsTimeInSec && limitCandlestick) { 31 | const candledata = await this.getCandlestickFromDB( 32 | exchange, 33 | symbol, 34 | EXCHANGE_BASE_INTERVAL_IN_SEC, 35 | limitCandlestick, 36 | ); 37 | 38 | batch[EXCHANGE_BASE_INTERVAL_IN_SEC] = candledata; 39 | 40 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 41 | batch[EXCHANGE_BASE_INTERVAL_IN_SEC].map((elem: any) => { 42 | result[elem.time] = {}; 43 | result[elem.time][EXCHANGE_BASE_INTERVAL_IN_SEC] = elem; 44 | }); 45 | 46 | if (intervalsTimeInSec.length != 0) { 47 | for (let i = 0; i < intervalsTimeInSec.length; i++) { 48 | const interval = intervalsTimeInSec[i]; 49 | 50 | batch[interval] = CandleConvert.json(candledata as IOHLCV[], EXCHANGE_BASE_INTERVAL_IN_SEC, interval); 51 | 52 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 53 | batch[interval].map((elem: any) => { 54 | result[elem.time][interval] = elem; 55 | }); 56 | } 57 | } 58 | } 59 | 60 | return result as batchedOHLCV; 61 | } catch (e) { 62 | logger.error('Batched Candlestick error', e); 63 | } 64 | } 65 | 66 | public async getCandlestickFromDB( 67 | exchange: string, 68 | symbol: string, 69 | interval: number, 70 | limit: number, 71 | ): Promise { 72 | try { 73 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 74 | let rows: any = []; 75 | 76 | // TODO add proper support Tick Chart values 77 | if ([16, 32, 64, 128, 256, 512, 1024].indexOf(interval) >= 0) { 78 | rows = await this._getTickchartFromDB(exchange, symbol, interval, limit); 79 | 80 | // Sort by time asc 81 | rows = _.sortBy(rows, ['time']); 82 | 83 | return rows; 84 | } 85 | 86 | // Converted Candles 87 | if (interval !== EXCHANGE_BASE_INTERVAL_IN_SEC && limit !== 0) { 88 | // eslint-disable-next-line no-param-reassign 89 | limit *= interval / EXCHANGE_BASE_INTERVAL_IN_SEC; 90 | 91 | // Limit should be always higher than convert ratio * 1,5 + 1 92 | // eslint-disable-next-line no-param-reassign 93 | limit += (interval / EXCHANGE_BASE_INTERVAL_IN_SEC) * 1.5; 94 | } 95 | 96 | const tableName = Utils.candlestickName(exchange, symbol, EXCHANGE_BASE_INTERVAL_IN_SEC); 97 | 98 | [rows] = await ExchangeDB.query('SELECT * FROM ?? ORDER BY `time` DESC LIMIT ?;', [tableName, ~~limit + 1]); 99 | 100 | // Convert into new time frame 101 | if (interval !== EXCHANGE_BASE_INTERVAL_IN_SEC) { 102 | rows = CandleConvert.json(rows, EXCHANGE_BASE_INTERVAL_IN_SEC, interval); 103 | } 104 | 105 | // Sort by time asc 106 | rows = _.sortBy(rows, ['time']); 107 | 108 | return rows; 109 | } catch (e) { 110 | logger.error('SQL error ', e); 111 | } 112 | } 113 | 114 | private async _getTickchartFromDB( 115 | exchange: string, 116 | symbol: string, 117 | tickLength: number, 118 | limit: number, 119 | time = 0, 120 | ): Promise { 121 | try { 122 | const tableName = Utils.tradesName(exchange, symbol); 123 | 124 | const [rows] = await ExchangeDB.query('SELECT * FROM ?? WHERE time > ? ORDER BY `time` DESC LIMIT ?;', [ 125 | tableName, 126 | time, 127 | limit, 128 | ]); 129 | 130 | return CandleConvert.tick_chart(rows as Trade[], tickLength); 131 | } catch (e) { 132 | logger.error('SQL error', e); 133 | } 134 | } 135 | 136 | public async loadAvailableTradePairs(): Promise { 137 | try { 138 | const [rows] = await BaseDB.query('SELECT * FROM `tradepairs`;'); 139 | 140 | return rows as RowDataPacket[]; 141 | } catch (e) { 142 | logger.error('SQL error', e); 143 | } 144 | } 145 | } 146 | 147 | export default new TradePairs(); 148 | -------------------------------------------------------------------------------- /src/traderbot/trade_emulator.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-param-reassign */ 2 | import { IOHLCV } from 'candlestick-convert'; 3 | import { logger } from '../logger'; 4 | import { Utils } from '../utils'; 5 | import { TradeEmulatorConfig, AdviceSchema, OrderSchema, createOrderSchema } from '../types'; 6 | 7 | export class TradeEmulator { 8 | orders: any[]; 9 | balanceAsset: any; 10 | balanceQuote: any; 11 | fee: any; 12 | stopLossLimit: any; 13 | trailingLimit: any; 14 | portionPct: any; 15 | orderSize: number; 16 | historyOrders: OrderSchema[]; 17 | price: number; 18 | constructor(config: TradeEmulatorConfig) { 19 | this.balanceAsset = config.balanceAsset || 0; 20 | this.balanceQuote = config.balanceQuote || 1000; 21 | this.fee = config.fee || 0.001; 22 | this.stopLossLimit = config.stopLossLimit || 0.9; // -1 disable 23 | this.trailingLimit = config.trailingLimit || 0.01; // -1 disable 24 | this.portionPct = config.portionPct || 10; 25 | 26 | this.orderSize = (this.balanceQuote / 100) * this.portionPct; 27 | 28 | this.orders = []; 29 | this.historyOrders = []; 30 | 31 | this.price = -1; 32 | } 33 | 34 | action(advice: AdviceSchema): void { 35 | try { 36 | /* { 37 | action: 38 | price: 39 | time: 40 | } */ 41 | 42 | if (advice.action === 'BUY') { 43 | this.buy(advice.price, advice.time); 44 | } else if (advice.action === 'SELL') { 45 | this.sell(advice.price, advice.time); 46 | } 47 | 48 | return; 49 | } catch (e) { 50 | logger.error('TradeEmulator action error ', e); 51 | } 52 | } 53 | 54 | sell(price: number, time: number): void { 55 | try { 56 | this.orders = this.orders 57 | .map(order => { 58 | if (order.closed === 0) { 59 | this.balanceQuote += (order.quantity * price) / (1 + this.fee * 2); 60 | this.balanceAsset -= order.quantity; 61 | 62 | order.sold = price; 63 | order.closed = time; 64 | order.closeType = 'Sell'; 65 | order.balance = this.getFullBalance(); 66 | 67 | this.historyOrders.push(order); 68 | } 69 | 70 | return order; 71 | }) 72 | .filter(order => order.closed === 0); 73 | } catch (e) { 74 | logger.error('Trade emulator Sell error ', e); 75 | } 76 | } 77 | 78 | buy(price: number, time: number): void { 79 | try { 80 | if (this.balanceQuote < this.orderSize) { 81 | return; 82 | } 83 | 84 | this.createOrder({ 85 | type: 'BUY', 86 | time, 87 | price, 88 | size: this.orderSize, 89 | stopLossLimit: this.stopLossLimit, 90 | trailingLimit: this.trailingLimit, 91 | }); 92 | 93 | return; 94 | } catch (e) { 95 | logger.error('Trade emulator Buy error ', e); 96 | } 97 | } 98 | 99 | createOrder(config: createOrderSchema): void { 100 | try { 101 | let quantity = config.size; 102 | 103 | if (config.type === 'BUY') { 104 | quantity = Utils.buyQuantityBySymbol(config.size, config.price); 105 | } 106 | 107 | let stopLossPrice = 0; 108 | let trailingPrice = 0; 109 | let trailingLimit = 0; 110 | 111 | if (config.stopLossLimit > 0) { 112 | stopLossPrice = config.price * config.stopLossLimit; 113 | } 114 | 115 | if (config.trailingLimit > 0) { 116 | trailingPrice = config.price + config.price * config.trailingLimit * 2; 117 | trailingLimit = config.trailingLimit; 118 | } 119 | 120 | const order = { 121 | price: config.price, 122 | time: config.time, 123 | quantity, 124 | stopLossPrice, 125 | trailingPrice, 126 | trailingLimit, 127 | closed: 0, 128 | balance: this.getFullBalance(), 129 | }; 130 | 131 | this.orders.push(order); 132 | 133 | if (config.type === 'BUY') { 134 | this.balanceQuote -= config.size; 135 | this.balanceAsset += quantity; 136 | } 137 | 138 | return; 139 | } catch (e) { 140 | logger.error('Trade emulator createOrder error ', e); 141 | } 142 | } 143 | 144 | update(candle: IOHLCV): OrderSchema | undefined { 145 | try { 146 | this.price = candle.close; 147 | 148 | this.orders = this.orders.map(order => { 149 | if (order?.stopLossPrice > 0 && order.stopLossPrice >= this.price && order.closed && order.closed == 0) { 150 | this.balanceQuote += (order.quantity * this.price) / (1 + this.fee * 2); 151 | this.balanceAsset -= order.quantity; 152 | 153 | this.historyOrders.push(order); 154 | 155 | order.sold = this.price; 156 | order.closed = candle.time; 157 | order.closeType = 'Stop-loss'; 158 | order.balance = this.getFullBalance(); 159 | } 160 | 161 | if (order?.trailingLimit > 0 && this.price >= order?.trailingPrice && order.closed && order.closed == 0) { 162 | order.stopLossPrice = this.price - this.price * order.trailingLimit; 163 | order.trailingPrice = this.price + this.price * order.trailingLimit; 164 | } 165 | 166 | return order; 167 | }); 168 | 169 | return; 170 | } catch (e) { 171 | logger.error('Trade emulator update error ', e); 172 | } 173 | } 174 | 175 | getFullBalance(): number { 176 | this.orderSize = (this.balanceQuote / 100) * this.portionPct; 177 | return this.balanceQuote + this.balanceAsset * this.price; 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/traderbot/trade_instance.ts: -------------------------------------------------------------------------------- 1 | import { tradeUtils } from './trade_utils'; 2 | import { Utils } from '../utils'; 3 | import { logger } from '../logger'; 4 | 5 | import ccxtController from '../exchange/ccxt_controller'; 6 | import { TradeInstanceConfig, AdviceSchema, ExchangeOrderInfoSchema } from '../types'; 7 | 8 | // const fee = 1.001 9 | 10 | const TOTAL_BALLANCE = 'ALL'; 11 | 12 | export class TradeInstance { 13 | instanceID: number; 14 | symbol: string; 15 | asset: string; 16 | quote: string; 17 | balanceAsset: number; 18 | balanceQuote: number; 19 | orderBalanceAsset: number; 20 | orderBalanceQuote: number; 21 | strategyGuid: number; 22 | orderLimit: number; 23 | exchange: string; 24 | exchangeAPI: any; 25 | constructor(config: TradeInstanceConfig) { 26 | // Config parse 27 | this.instanceID = config.instanceID; 28 | this.symbol = config.symbol; 29 | this.asset = config.asset; 30 | this.quote = config.quote; 31 | this.balanceAsset = config.balanceAsset; 32 | this.balanceQuote = config.balanceQuote; 33 | this.orderBalanceAsset = config.orderBalanceAsset; 34 | this.orderBalanceQuote = config.orderBalanceQuote; 35 | 36 | this.strategyGuid = config.strategyGuid; 37 | this.orderLimit = config.orderLimit; 38 | this.exchange = config.exchange; 39 | 40 | // Config parse 41 | 42 | // WARNING EVERY FIELD SYNCED FROM DATABASE DO NOT USE ANY NON DB LOADED VARIABLE! 43 | // Load Exchange API 44 | this.exchangeAPI = ccxtController.loadExchangeAPI(this.exchange); 45 | } 46 | 47 | async update(advice: AdviceSchema): Promise { 48 | try { 49 | logger.info(`Trade instance update ${advice.action} , ${advice.close}`); 50 | 51 | // Check values 52 | if (advice.action !== undefined && advice.close !== undefined) { 53 | // All in trades can have undefined or 0 quantities 54 | if (typeof advice.quantity == 'undefined') { 55 | advice.quantity = 0; 56 | } 57 | 58 | const quantity = advice.quantity ?? TOTAL_BALLANCE; 59 | 60 | if (advice.action == 'BUY') { 61 | await this.buy(advice.close, quantity); 62 | } else if (advice.action == 'SELL') { 63 | await this.sell(advice.close, quantity); 64 | } 65 | } 66 | 67 | await this.checkOrder(); 68 | 69 | return; 70 | } catch (e) { 71 | logger.error('Trade instance error ', e); 72 | } 73 | } 74 | 75 | async sell(price: number, quantity: number | string): Promise { 76 | try { 77 | // If quantity is not set use Asset Balance 78 | if (quantity === 0 || quantity === TOTAL_BALLANCE) { 79 | quantity = this.balanceAsset; 80 | } 81 | 82 | logger.info(`SYMBOL: ${this.symbol} SELL QUANTITY: ${quantity}`); 83 | 84 | if (typeof quantity === 'number' && quantity > 0 && this.balanceAsset - quantity < 0) { 85 | const response = await this.exchangeAPI.create_limit_sell_order(this.symbol, quantity, price); 86 | 87 | if (response) { 88 | await tradeUtils.insertAccountOrdersToDB(response, this.instanceID); 89 | 90 | /* 91 | this.balanceAsset 92 | this.balanceQuote 93 | this.orderBalanceAsset 94 | this.orderBalanceQuote 95 | */ 96 | 97 | this.balanceAsset -= response.amount; 98 | this.orderBalanceAsset += response.amount; 99 | 100 | await this.syncTradeInstanceBalance(); 101 | } 102 | } 103 | } catch (e) { 104 | logger.error('Trade instance error sell ', e); 105 | } 106 | } 107 | 108 | async buy(price: number, quoteLimit: number | string): Promise { 109 | try { 110 | if (quoteLimit === 0 || quoteLimit === TOTAL_BALLANCE) { 111 | quoteLimit = this.balanceQuote; 112 | } 113 | 114 | if (typeof quoteLimit === 'number') { 115 | // Calculate quantity 116 | const quantity = quoteLimit >= this.balanceQuote ? Utils.buyQuantityBySymbol(this.balanceQuote, price) : Utils.buyQuantityBySymbol(quoteLimit, price); 117 | // Round quantity 118 | 119 | logger.info(`SYMBOL: ${this.symbol} BUY QUANTITY: ${quantity}`); 120 | 121 | if (quantity > 0 || this.balanceQuote - quoteLimit < 0) { 122 | const response = await this.exchangeAPI.create_limit_buy_order(this.symbol, quantity, price); 123 | 124 | if (response) { 125 | await tradeUtils.insertAccountOrdersToDB(response, this.instanceID); 126 | 127 | /* 128 | this.balanceAsset 129 | this.balanceQuote 130 | this.orderBalanceAsset 131 | this.orderBalanceQuote 132 | */ 133 | 134 | this.balanceQuote -= response.amount * response.price; 135 | this.orderBalanceQuote += response.amount * response.price; 136 | 137 | await this.syncTradeInstanceBalance(); 138 | } 139 | } 140 | } 141 | } catch (e) { 142 | logger.error('Trade instance error ', e); 143 | } 144 | } 145 | 146 | async syncTradeInstanceBalance(): Promise { 147 | try { 148 | /* 149 | this.balanceAsset 150 | this.balanceQuote 151 | this.orderBalanceAsset 152 | this.orderBalanceQuote 153 | */ 154 | await tradeUtils.setTradeInstanceBalance(this.instanceID, this.balanceAsset, this.balanceQuote, this.orderBalanceAsset, this.orderBalanceQuote); 155 | } catch (e) { 156 | logger.error('Trade instance sync balance error ', e); 157 | } 158 | } 159 | 160 | async checkOrder(): Promise { 161 | try { 162 | const order = await tradeUtils.getLastTradesByInstanceToDB(this.instanceID); 163 | 164 | if (order) { 165 | order.forEach(async order => { 166 | const orderInfo = await this.exchangeAPI.fetchOrder(order.id, order.symbol); 167 | 168 | logger.verbose(orderInfo); 169 | 170 | /* 171 | info: {} 172 | type: 'limit', 173 | side: 'sell', 174 | price: 0.0003186, 175 | amount: 3.13, 176 | cost: 0.00099721, 177 | average: 0.00031859744408945683, 178 | filled: 3.13, 179 | remaining: 1, 180 | status: 'open', / 'closed', / 'canceled' 181 | */ 182 | 183 | if (orderInfo.status == 'open') { 184 | logger.verbose(`Open order ${orderInfo.id} , ${orderInfo.amount}/${orderInfo.filled} , ${orderInfo.price}`); 185 | } 186 | 187 | if (orderInfo.status == 'closed') { 188 | logger.verbose(`Order filled ${orderInfo.id} , ${orderInfo.filled} , ${orderInfo.price}`); 189 | 190 | await this.bookTradeInstanceOrder(orderInfo); 191 | } 192 | 193 | if (orderInfo.status == 'canceled') { 194 | await this.bookTradeInstanceOrder(orderInfo); 195 | } 196 | }); 197 | } 198 | } catch (e) { 199 | logger.error('Trade instance order check error ', e); 200 | } 201 | } 202 | 203 | async bookTradeInstanceOrder(orderInfo: ExchangeOrderInfoSchema): Promise { 204 | /* 205 | this.balanceAsset 206 | this.balanceQuote 207 | this.orderBalanceAsset 208 | this.orderBalanceQuote 209 | */ 210 | 211 | if (orderInfo.side == 'sell') { 212 | // Add non sold assets to the balance 213 | this.balanceAsset += orderInfo.remaining; 214 | // Remove order assets from the order asset balance 215 | this.orderBalanceAsset -= orderInfo.amount; 216 | // Add quotes gain after sold assets into the quote balance 217 | this.balanceQuote += orderInfo.cost; 218 | } 219 | 220 | if (orderInfo.side == 'buy') { 221 | // Add non spent quotes back to balance 222 | this.balanceQuote += orderInfo.remaining * orderInfo.price; 223 | // Remove quotes from the order quote balance 224 | this.orderBalanceQuote -= orderInfo.amount * orderInfo.price; 225 | // Add assets bought from order 226 | this.balanceAsset += orderInfo.filled; 227 | } 228 | 229 | // Avoid negative Order balance 230 | if (this.orderBalanceAsset < 0) this.orderBalanceAsset = 0; 231 | if (this.orderBalanceQuote < 0) this.orderBalanceQuote = 0; 232 | 233 | await this.syncTradeInstanceBalance(); 234 | await tradeUtils.closeAccountTradesDB(orderInfo.id); 235 | } 236 | // End of class 237 | } 238 | -------------------------------------------------------------------------------- /src/traderbot/trade_utils.ts: -------------------------------------------------------------------------------- 1 | import { logger } from '../logger'; 2 | import { BaseDB } from '../database'; 3 | import { RowDataPacket } from 'mysql2'; 4 | 5 | export const tradeUtils = { 6 | insertAccountOrdersToDB: async (resp: any, instanceId: number): Promise => { 7 | try { 8 | const time = Date.now(); 9 | 10 | /* 11 | info: {}, 12 | id: '31865059', 13 | timestamp: 1563975044422, 14 | datetime: '2019-07-24T13:30:44.422Z', 15 | lastTradeTimestamp: undefined, 16 | symbol: 'HC/BTC', 17 | type: 'limit', 18 | side: 'sell', 19 | price: 0.000308, 20 | amount: 1, 21 | cost: 0, 22 | average: undefined, 23 | filled: 0, 24 | remaining: 1, 25 | status: 'open', 26 | fee: undefined, 27 | trades: undefined 28 | */ 29 | const closed = 0; 30 | 31 | const data = [ 32 | resp.id, 33 | instanceId, 34 | time, 35 | resp.timestamp, 36 | resp.datetime, 37 | resp.lastTradeTimestamp, 38 | resp.symbol, 39 | resp.type, 40 | resp.side, 41 | resp.price, 42 | resp.amount, 43 | resp.cost, 44 | resp.average, 45 | resp.filled, 46 | resp.remaining, 47 | resp.status, 48 | resp.fee, 49 | resp.trades, 50 | JSON.stringify(resp.info), 51 | closed, 52 | ]; 53 | 54 | await BaseDB.query( 55 | 'INSERT INTO `account_orders` (`id`, `instance_id`, `time`, `timestamp`, `datetime`, `lastTradeTimestamp`, `symbol`, `type`, `side`, `price`, `amount`, `cost`, `average`, `filled`, `remaining`, `status`, `fee`, `trades`, `info`,`closed`) VALUES ?;', 56 | [[data]], 57 | ); 58 | 59 | return; 60 | } catch (e) { 61 | logger.error('SQL error', e); 62 | } 63 | }, 64 | 65 | closeAccountTradesDB: async (orderId: string): Promise => { 66 | try { 67 | await BaseDB.query('UPDATE `account_orders` SET `closed` = 1 WHERE `id` = ? LIMIT 1;', [orderId]); 68 | } catch (e) { 69 | logger.error('SQL error', e); 70 | } 71 | }, 72 | 73 | getLastTradesByInstanceToDB: async (instanceId: number): Promise => { 74 | try { 75 | const [rows] = await BaseDB.query("SELECT * FROM `account_orders` WHERE `instance_id` = ? AND `type` LIKE 'LIMIT' AND `closed` = 0 ORDER BY `account_orders`.`time` DESC;", [ 76 | instanceId, 77 | ]); 78 | 79 | return rows as RowDataPacket[]; 80 | } catch (e) { 81 | logger.error('SQL error', e); 82 | } 83 | }, 84 | 85 | setTradeInstanceBalance: async (instanceId: number, balanceAsset: number, balanceQuote: number, orderBalanceAsset: number, orderBalanceQuote: number): Promise => { 86 | try { 87 | await BaseDB.query( 88 | 'UPDATE `account_trader_instances` SET `asset_balance` = ?, `quote_balance` = ? , `order_asset_balance` = ?, `order_quote_balance` = ? WHERE `guid` = ?;', 89 | [balanceAsset, balanceQuote, orderBalanceAsset, orderBalanceQuote, instanceId], 90 | ); 91 | } catch (e) { 92 | logger.error('SQL error', e); 93 | } 94 | }, 95 | 96 | getTradeAdviceFromDB: async (algoName: string, time = 0): Promise => { 97 | try { 98 | const [ 99 | rows, 100 | ] = await BaseDB.query( 101 | 'SELECT DISTINCT `trade_advice`.`symbol`, `action`, `prevActionIfNotIdle`, `time`, `asset`, `quote`, `close` FROM `trade_advice` JOIN tradepairs ON trade_advice.symbol = tradepairs.symbol WHERE algo = ? and time > ? ORDER BY `time` DESC LIMIT 500;', 102 | [algoName, time], 103 | ); 104 | 105 | return rows as RowDataPacket[]; 106 | } catch (e) { 107 | logger.error('SQL error', e); 108 | } 109 | }, 110 | }; 111 | -------------------------------------------------------------------------------- /src/traderbot/traderbot.ts: -------------------------------------------------------------------------------- 1 | import { logger } from '../logger'; 2 | import { BaseDB } from '../database'; 3 | 4 | import { TradeInstance } from './trade_instance'; 5 | import { DEFAULT_TRADERBOT_UPDATELOOP_TIMEOUT } from '../constants'; 6 | import { RowDataPacket } from 'mysql2'; 7 | 8 | class Traderbot { 9 | strategyAdvices: any[]; 10 | lastAdviceTime: any; 11 | tradeInstanceList: TradeInstance[]; 12 | 13 | constructor() { 14 | this.strategyAdvices = []; 15 | this.tradeInstanceList = []; 16 | } 17 | 18 | async start(): Promise { 19 | try { 20 | // Update trade advice timer 21 | this.lastAdviceTime = await this.getLastAdviceTimeFromDB(); 22 | 23 | // Load/Reload instances 24 | await this.loadTradeInstances(); 25 | 26 | // Start update loop 27 | await this.updateLoop(); 28 | } catch (e) { 29 | logger.error('Traderbot start error!', e); 30 | } 31 | } 32 | 33 | async updateLoop(): Promise { 34 | try { 35 | await this.updateTradeBotInstanceList(); 36 | await this.loadTradeInstances(); 37 | } catch (e) { 38 | logger.error('Traderbot update loop error!', e); 39 | } finally { 40 | setTimeout(() => { 41 | this.updateLoop(); 42 | }, DEFAULT_TRADERBOT_UPDATELOOP_TIMEOUT); 43 | } 44 | } 45 | 46 | async loadTradeInstances(): Promise { 47 | try { 48 | // Only load new instances, re-load is not an option anymore 49 | let instances = await this.loadTradeInstanceListFromDB(); 50 | 51 | if (!instances) { 52 | return; 53 | } 54 | 55 | if (this.tradeInstanceList) { 56 | const oldInstanceIDs = this.tradeInstanceList.map((e: TradeInstance) => e.instanceID); 57 | 58 | instances = instances.filter(e => oldInstanceIDs.indexOf(e.guid) === -1); 59 | } 60 | 61 | instances.forEach(e => { 62 | const newTradeInstance = new TradeInstance({ 63 | exchange: e.exchange, 64 | orderLimit: e.orderLimit, 65 | instanceID: e.guid, 66 | strategyGuid: e.strategyGuid, 67 | symbol: e.symbol, 68 | asset: e.asset, 69 | quote: e.quote, 70 | balanceAsset: e.balanceAsset, 71 | balanceQuote: e.balanceQuote, 72 | orderBalanceAsset: e.orderBalanceAsset, 73 | orderBalanceQuote: e.orderBalanceQuote, 74 | }); 75 | 76 | this.tradeInstanceList.push(newTradeInstance); 77 | logger.verbose(`Tradebot new instances loaded, guid: ${e.strategyGuid}`); 78 | }); 79 | 80 | return; 81 | } catch (e) { 82 | logger.error('Tradebot error!', e); 83 | } 84 | } 85 | 86 | async updateTradeBotInstanceList(): Promise { 87 | try { 88 | // Get fresh Advices from DB 89 | 90 | this.strategyAdvices = (await this.getTradeAdviceFromDB(this.lastAdviceTime)) as RowDataPacket[]; 91 | 92 | logger.verbose(`Trade advice length: ${this.strategyAdvices.length} Last advice time: ${this.lastAdviceTime}`); 93 | 94 | // New advices 95 | if (this.strategyAdvices.length != 0) { 96 | // Update Trade instances 97 | for (const traderInstance of this.tradeInstanceList) { 98 | const strategyAdvice = this.strategyAdvices.find(elem => elem.strategyGuid == traderInstance.strategyGuid); 99 | 100 | // Update Trader instances 101 | if (strategyAdvice) { 102 | traderInstance.update(strategyAdvice); 103 | } 104 | } 105 | } 106 | 107 | logger.verbose(`Tradebot instances updated, count: ${this.tradeInstanceList.length}`); 108 | 109 | // Update advice time to avoid double actions 110 | this.lastAdviceTime = await this.getLastAdviceTimeFromDB(); 111 | } catch (e) { 112 | logger.error('Traderbot Trade Error', e); 113 | } 114 | } 115 | 116 | async loadTradeInstanceListFromDB(): Promise { 117 | try { 118 | const [rows] = await BaseDB.query('SELECT * FROM `account_trader_instances`;'); 119 | 120 | return rows as RowDataPacket[]; 121 | } catch (e) { 122 | logger.error('SQL error', e); 123 | } 124 | } 125 | 126 | async getTradeAdviceFromDB(time = 0): Promise { 127 | try { 128 | const [ 129 | rows, 130 | ] = await BaseDB.query( 131 | 'SELECT DISTINCT `trade_advice`.`symbol`, `trade_advice`.`exchange`, `trade_advice`.`strategy_guid`, `trade_advice`.`strategy`, `trade_advice`.`strategy_config`, `action`, trade_advice.`time`, `asset`, `quote`, `close` FROM `trade_advice` JOIN tradepairs ON trade_advice.symbol = tradepairs.symbol WHERE trade_advice.time > ? ORDER BY trade_advice.`time` DESC;', 132 | [time], 133 | ); 134 | 135 | return rows as RowDataPacket[]; 136 | } catch (e) { 137 | logger.error('SQL error', e); 138 | } 139 | } 140 | 141 | async getLastAdviceTimeFromDB(): Promise { 142 | try { 143 | const [rows] = await BaseDB.query('SELECT time FROM `trade_advice` ORDER BY `trade_advice`.`time` DESC LIMIT 1;'); 144 | 145 | if (rows && rows[0]?.time) { 146 | return rows[0].time; 147 | } 148 | 149 | return Date.now(); 150 | } catch (e) { 151 | logger.error('SQL error', e); 152 | } 153 | } 154 | } 155 | 156 | export default new Traderbot(); 157 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import { OHLCV } from 'candlestick-convert'; 2 | import { Emulator } from './emulator/emulator'; 3 | 4 | export type batchedOHLCV = Map; 5 | 6 | export type OHLCVMapFlat = Map; 7 | 8 | export type OHLCVMap = Map; 9 | 10 | export type AdviceSchema = { 11 | action: string; 12 | price: number; 13 | time: number; 14 | quantity?: number; 15 | close?: number; 16 | }; 17 | 18 | export type OrderSchema = { 19 | closed: number; 20 | sold: number; 21 | closeType: string; 22 | balance: number; 23 | }; 24 | 25 | export type ExchangeOrderInfoSchema = { 26 | id: string; 27 | side: string; 28 | remaining: number; 29 | amount: number; 30 | price: number; 31 | filled: number; 32 | cost: number; 33 | }; 34 | 35 | export type createOrderSchema = { 36 | size: number; 37 | type: string; 38 | price: number; 39 | time: number; 40 | stopLossLimit: number; 41 | trailingLimit: number; 42 | }; 43 | 44 | export interface BacktestEmulatorInit { 45 | symbol: string; 46 | exchange: string; 47 | strategy: string; 48 | strategyConfig: StrategyConfig; 49 | traderConfig: TradeEmulatorConfig; 50 | intervals: number[]; 51 | candledata: batchedOHLCV; 52 | } 53 | 54 | export interface Simulation { 55 | exchange: string; 56 | symbol: string; 57 | emulator: Emulator; 58 | [key: string]: unknown; 59 | } 60 | 61 | export interface EmulatorConfig { 62 | exchange: string; 63 | symbol: string; 64 | strategy: string; 65 | strategyConfig: StrategyConfig; 66 | intervals: number[]; 67 | traderConfig?: TradeEmulatorConfig; 68 | } 69 | 70 | export interface LiveSimulation { 71 | exchange: string; 72 | symbol: string; 73 | strategy: string; 74 | strategyConfig: StrategyConfig; 75 | intervals: number[]; 76 | emulator: Emulator; 77 | [key: string]: unknown; 78 | } 79 | 80 | export interface StrategyOptimizerConfig { 81 | exchange: string; 82 | symbol: string; 83 | numberOfExecution: number; 84 | strategy: string; 85 | traderConfig: TradeEmulatorConfig; 86 | candledata: batchedOHLCV; 87 | } 88 | 89 | export interface TradeEmulatorConfig { 90 | balanceAsset: number; 91 | balanceQuote: number; 92 | fee: number; 93 | stopLossLimit: number; 94 | trailingLimit: number; 95 | portionPct: number; 96 | } 97 | 98 | export interface TradeInstanceConfig { 99 | instanceID: number; 100 | symbol: string; 101 | asset: string; 102 | quote: string; 103 | balanceAsset: number; 104 | balanceQuote: number; 105 | orderBalanceAsset: number; 106 | orderBalanceQuote: number; 107 | strategyGuid: number; 108 | orderLimit: number; 109 | exchange: string; 110 | } 111 | 112 | export interface StrategyConfig { 113 | readonly [key: string]: unknown; 114 | } 115 | 116 | export interface TraderConfig { 117 | readonly [key: string]: unknown; 118 | } 119 | 120 | export type configTA = { 121 | label: string; 122 | updateInterval: number; 123 | nameTA: string; 124 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 125 | params: any; 126 | params2: string; 127 | }; 128 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export const Utils = { 2 | minMaxScaler: (data: number[]): number[] => { 3 | const min = Math.min(...data); 4 | const max = Math.max(...data); 5 | 6 | const scaledData = data.map(value => { 7 | return (value - min) / (max - min); 8 | }); 9 | 10 | return scaledData; 11 | }, 12 | 13 | round: (value: number, decimals = 2): number => { 14 | const coeff = 10 ** decimals; 15 | 16 | return Math.round(value * coeff) / coeff; 17 | }, 18 | 19 | // Twitter time standardizer 20 | stringtoTimestamp: (string: string): number => { 21 | return new Date(string).getTime(); 22 | }, 23 | 24 | // Get buy_quantity 25 | buyQuantityBySymbol: (spendableBalance: number, price: number): number => { 26 | let quantity = 0; 27 | 28 | quantity = Number(spendableBalance) / Number(price); 29 | 30 | return quantity; 31 | }, 32 | 33 | // Intervals: 1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,1M 34 | intervalToString: (interval: number): string => { 35 | let string = ''; 36 | switch (interval) { 37 | case 60: 38 | string = '1m'; 39 | break; 40 | case 180: 41 | string = '3m'; 42 | break; 43 | case 300: 44 | string = '5m'; 45 | break; 46 | case 900: 47 | string = '15m'; 48 | break; 49 | case 1800: 50 | string = '30m'; 51 | break; 52 | case 3600: 53 | string = '1h'; 54 | break; 55 | case 7200: 56 | string = '2h'; 57 | break; 58 | case 14400: 59 | string = '4h'; 60 | break; 61 | case 28800: 62 | string = '8h'; 63 | break; 64 | case 43200: 65 | string = '12h'; 66 | break; 67 | case 86400: 68 | string = '24h'; 69 | break; 70 | default: 71 | string = '1m'; 72 | break; 73 | } 74 | 75 | return string; 76 | }, 77 | 78 | /* StockML generic naming */ 79 | 80 | tradesName: (exchange: string, symbol: string): string => { 81 | const cleanSymbol = symbol 82 | .replace('/', '') 83 | .replace('-', '') 84 | .replace('_', ''); 85 | 86 | const name = `${exchange}_${cleanSymbol}_trades`; 87 | 88 | // Lowercase only 89 | return name.toLowerCase(); 90 | }, 91 | 92 | orderbookName: (exchange: string, symbol: string): string => { 93 | const cleanSymbol = symbol 94 | .replace('/', '') 95 | .replace('-', '') 96 | .replace('_', ''); 97 | 98 | const name = `${exchange}_${cleanSymbol}_orderbook`; 99 | 100 | // Lowercase only 101 | return name.toLowerCase(); 102 | }, 103 | 104 | candlestickName: (exchange: string, symbol: string, interval: string | number): string => { 105 | const cleanSymbol = symbol 106 | .replace('/', '') 107 | .replace('-', '') 108 | .replace('_', ''); 109 | 110 | if (typeof interval === 'number') { 111 | return `${exchange}_${cleanSymbol}_${Utils.intervalToString(interval)}`.toLowerCase(); 112 | } 113 | 114 | // Lowercase only 115 | return `${exchange}_${cleanSymbol}_${interval}`.toLowerCase(); 116 | }, 117 | }; 118 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "experimentalDecorators": true, 4 | "module": "commonjs", 5 | "resolveJsonModule": true, 6 | "emitDecoratorMetadata": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "suppressImplicitAnyIndexErrors": true, 10 | "target": "es2017", 11 | "noImplicitAny": true, 12 | "moduleResolution": "node", 13 | "sourceMap": true, 14 | "outDir": "build", 15 | "baseUrl": ".", 16 | "skipLibCheck": true, 17 | "allowJs": true, 18 | "strict": true, 19 | "pretty": true, 20 | "removeComments": true, 21 | "typeRoots": ["./node_modules/@types", "./types"] 22 | }, 23 | "include": ["src", "types"], 24 | "exclude": ["node_modules", "src/__test__"] 25 | } 26 | -------------------------------------------------------------------------------- /types/mysql2/index.d.ts: -------------------------------------------------------------------------------- 1 | // Generated by typings 2 | // Source: https://raw.githubusercontent.com/types/npm-mysql2/66662aa2098481bceb334529f884712fd2632c52/promise.d.ts 3 | declare module '~mysql2/promise' { 4 | 5 | import {RowDataPacket, OkPacket, FieldPacket, QueryOptions, ConnectionOptions, PoolOptions} from '~mysql2/index'; 6 | import {EventEmitter} from 'events'; 7 | export * from '~mysql2/index'; 8 | 9 | export interface Connection extends EventEmitter { 10 | 11 | config: ConnectionOptions; 12 | threadId: number; 13 | 14 | connect(): Promise; 15 | 16 | beginTransaction(): Promise; 17 | commit(): Promise; 18 | rollback(): Promise; 19 | 20 | changeUser(options: ConnectionOptions): Promise; 21 | 22 | query(sql: string): Promise<[T, FieldPacket[]]>; 23 | query(sql: string, values: any | any[] | { [param: string]: any }): Promise<[T, FieldPacket[]]>; 24 | query(options: QueryOptions): Promise<[T, FieldPacket[]]>; 25 | query(options: QueryOptions, values: any | any[] | { [param: string]: any }): Promise<[T, FieldPacket[]]>; 26 | 27 | execute(sql: string): Promise<[T, FieldPacket[]]>; 28 | execute(sql: string, values: any | any[] | { [param: string]: any }): Promise<[T, FieldPacket[]]>; 29 | execute(options: QueryOptions): Promise<[T, FieldPacket[]]>; 30 | execute(options: QueryOptions, values: any | any[] | { [param: string]: any }): Promise<[T, FieldPacket[]]>; 31 | 32 | end(options?: any): Promise; 33 | 34 | destroy(): void; 35 | 36 | pause(): void; 37 | 38 | resume(): void; 39 | 40 | escape(value: any): string; 41 | 42 | escapeId(value: string): string; 43 | escapeId(values: string[]): string; 44 | 45 | format(sql: string, values?: any | any[] | { [param: string]: any }): string; 46 | } 47 | 48 | export interface PoolConnection extends Connection { 49 | release(): void; 50 | } 51 | 52 | export interface Pool extends EventEmitter { 53 | query(sql: string): Promise<[T, FieldPacket[]]>; 54 | query(sql: string, values: any | any[] | { [param: string]: any }): Promise<[T, FieldPacket[]]>; 55 | query(options: QueryOptions): Promise<[T, FieldPacket[]]>; 56 | query(options: QueryOptions, values: any | any[] | { [param: string]: any }): Promise<[T, FieldPacket[]]>; 57 | 58 | execute(sql: string): Promise<[T, FieldPacket[]]>; 59 | execute(sql: string, values: any | any[] | { [param: string]: any }): Promise<[T, FieldPacket[]]>; 60 | execute(options: QueryOptions): Promise<[T, FieldPacket[]]>; 61 | execute(options: QueryOptions, values: any | any[] | { [param: string]: any }): Promise<[T, FieldPacket[]]>; 62 | 63 | getConnection(): Promise; 64 | on(event: 'connection', listener: (connection: PoolConnection) => any): this; 65 | } 66 | 67 | export function createConnection(connectionUri: string): Connection; 68 | export function createConnection(config: ConnectionOptions): Connection; 69 | export function createPool(config: PoolOptions): Pool; 70 | } 71 | declare module 'mysql2/promise' { 72 | export * from '~mysql2/promise'; 73 | } 74 | 75 | // Generated by typings 76 | // Source: https://raw.githubusercontent.com/types/npm-mysql/c3a7c0bf94ecf886d5ce7b5e4e5e7d17cf5b9668/lib/Connection.d.ts 77 | declare module '~mysql2~mysql/lib/Connection' { 78 | 79 | import Query = require('~mysql2~mysql/lib/protocol/sequences/Query'); 80 | import {OkPacket, FieldPacket, RowDataPacket} from '~mysql2~mysql/lib/protocol/packets/index'; 81 | import {EventEmitter} from 'events'; 82 | 83 | namespace Connection { 84 | 85 | export interface ConnectionOptions { 86 | /** 87 | * The MySQL user to authenticate as 88 | */ 89 | user?: string; 90 | 91 | /** 92 | * The password of that MySQL user 93 | */ 94 | password?: string; 95 | 96 | /** 97 | * Name of the database to use for this connection 98 | */ 99 | database?: string; 100 | 101 | /** 102 | * The charset for the connection. This is called 'collation' in the SQL-level of MySQL (like utf8_general_ci). 103 | * If a SQL-level charset is specified (like utf8mb4) then the default collation for that charset is used. 104 | * (Default: 'UTF8_GENERAL_CI') 105 | */ 106 | charset?: string; 107 | 108 | /** 109 | * The hostname of the database you are connecting to. (Default: localhost) 110 | */ 111 | host?: string; 112 | 113 | /** 114 | * The port number to connect to. (Default: 3306) 115 | */ 116 | port?: number; 117 | 118 | /** 119 | * The source IP address to use for TCP connection 120 | */ 121 | localAddress?: string; 122 | 123 | /** 124 | * The path to a unix domain socket to connect to. When used host and port are ignored 125 | */ 126 | socketPath?: string; 127 | 128 | /** 129 | * The timezone used to store local dates. (Default: 'local') 130 | */ 131 | timezone?: string | 'local'; 132 | 133 | /** 134 | * The milliseconds before a timeout occurs during the initial connection to the MySQL server. (Default: 10 seconds) 135 | */ 136 | connectTimeout?: number; 137 | 138 | /** 139 | * Stringify objects instead of converting to values. (Default: 'false') 140 | */ 141 | stringifyObjects?: boolean; 142 | 143 | /** 144 | * Allow connecting to MySQL instances that ask for the old (insecure) authentication method. (Default: false) 145 | */ 146 | insecureAuth?: boolean; 147 | 148 | /** 149 | * Determines if column values should be converted to native JavaScript types. It is not recommended (and may go away / change in the future) 150 | * to disable type casting, but you can currently do so on either the connection or query level. (Default: true) 151 | * 152 | * You can also specify a function (field: any, next: () => void) => {} to do the type casting yourself. 153 | * 154 | * WARNING: YOU MUST INVOKE the parser using one of these three field functions in your custom typeCast callback. They can only be called once. 155 | * 156 | * field.string() 157 | * field.buffer() 158 | * field.geometry() 159 | * 160 | * are aliases for 161 | * 162 | * parser.parseLengthCodedString() 163 | * parser.parseLengthCodedBuffer() 164 | * parser.parseGeometryValue() 165 | * 166 | * You can find which field function you need to use by looking at: RowDataPacket.prototype._typeCast 167 | */ 168 | typeCast?: boolean | ((field: any, next: () => void) => any); 169 | 170 | /** 171 | * A custom query format function 172 | */ 173 | queryFormat?: (query: string, values: any) => void; 174 | 175 | /** 176 | * When dealing with big numbers (BIGINT and DECIMAL columns) in the database, you should enable this option 177 | * (Default: false) 178 | */ 179 | supportBigNumbers?: boolean; 180 | 181 | /** 182 | * Enabling both supportBigNumbers and bigNumberStrings forces big numbers (BIGINT and DECIMAL columns) to be 183 | * always returned as JavaScript String objects (Default: false). Enabling supportBigNumbers but leaving 184 | * bigNumberStrings disabled will return big numbers as String objects only when they cannot be accurately 185 | * represented with [JavaScript Number objects] (http://ecma262-5.com/ELS5_HTML.htm#Section_8.5) 186 | * (which happens when they exceed the [-2^53, +2^53] range), otherwise they will be returned as Number objects. 187 | * This option is ignored if supportBigNumbers is disabled. 188 | */ 189 | bigNumberStrings?: boolean; 190 | 191 | /** 192 | * Force date types (TIMESTAMP, DATETIME, DATE) to be returned as strings rather then inflated into JavaScript Date 193 | * objects. (Default: false) 194 | */ 195 | dateStrings?: boolean; 196 | 197 | /** 198 | * This will print all incoming and outgoing packets on stdout. 199 | * You can also restrict debugging to packet types by passing an array of types (strings) to debug; 200 | * 201 | * (Default: false) 202 | */ 203 | debug?: any; 204 | 205 | /** 206 | * Generates stack traces on Error to include call site of library entrance ('long stack traces'). Slight 207 | * performance penalty for most calls. (Default: true) 208 | */ 209 | trace?: boolean; 210 | 211 | /** 212 | * Allow multiple mysql statements per query. Be careful with this, it exposes you to SQL injection attacks. (Default: false) 213 | */ 214 | multipleStatements?: boolean; 215 | 216 | /** 217 | * List of connection flags to use other than the default ones. It is also possible to blacklist default ones 218 | */ 219 | flags?: Array; 220 | 221 | /** 222 | * object with ssl parameters or a string containing name of ssl profile 223 | */ 224 | ssl?: string | SslOptions; 225 | } 226 | 227 | export interface SslOptions { 228 | /** 229 | * A string or buffer holding the PFX or PKCS12 encoded private key, certificate and CA certificates 230 | */ 231 | pfx?: string; 232 | 233 | /** 234 | * A string holding the PEM encoded private key 235 | */ 236 | key?: string; 237 | 238 | /** 239 | * A string of passphrase for the private key or pfx 240 | */ 241 | passphrase?: string; 242 | 243 | /** 244 | * A string holding the PEM encoded certificate 245 | */ 246 | cert?: string; 247 | 248 | /** 249 | * Either a string or list of strings of PEM encoded CA certificates to trust. 250 | */ 251 | ca?: string | string[]; 252 | 253 | /** 254 | * Either a string or list of strings of PEM encoded CRLs (Certificate Revocation List) 255 | */ 256 | crl?: string | string[]; 257 | 258 | /** 259 | * A string describing the ciphers to use or exclude 260 | */ 261 | ciphers?: string; 262 | 263 | /** 264 | * You can also connect to a MySQL server without properly providing the appropriate CA to trust. You should not do this. 265 | */ 266 | rejectUnauthorized?: boolean; 267 | } 268 | } 269 | 270 | class Connection extends EventEmitter { 271 | 272 | config: Connection.ConnectionOptions; 273 | threadId: number; 274 | 275 | static createQuery(sql: string, callback?: (err: Query.QueryError | null, result: T, fields: FieldPacket[]) => any): Query; 276 | static createQuery(sql: string, values: any | any[] | { [param: string]: any }, callback?: (err: Query.QueryError | null, result: T, fields: FieldPacket[]) => any): Query; 277 | 278 | beginTransaction(callback: (err: Query.QueryError | null) => void): void; 279 | 280 | connect(callback?: (err: Query.QueryError | null) => void): void; 281 | 282 | commit(callback?: (err: Query.QueryError | null) => void): void; 283 | 284 | changeUser(options: Connection.ConnectionOptions, callback?: (err: Query.QueryError | null) => void): void; 285 | 286 | query(sql: string, callback?: (err: Query.QueryError | null, result: T, fields: FieldPacket[]) => any): Query; 287 | query(sql: string, values: any | any[] | { [param: string]: any }, callback?: (err: Query.QueryError | null, result: T, fields: FieldPacket[]) => any): Query; 288 | query(options: Query.QueryOptions, callback?: (err: Query.QueryError | null, result: T, fields?: FieldPacket[]) => any): Query; 289 | query(options: Query.QueryOptions, values: any | any[] | { [param: string]: any }, callback?: (err: Query.QueryError | null, result: T, fields: FieldPacket[]) => any): Query; 290 | 291 | end(callback?: (err: Query.QueryError | null) => void): void; 292 | end(options: any, callback?: (err: Query.QueryError | null) => void): void; 293 | 294 | destroy(): void; 295 | 296 | pause(): void; 297 | 298 | resume(): void; 299 | 300 | escape(value: any): string; 301 | 302 | escapeId(value: string): string; 303 | escapeId(values: string[]): string; 304 | 305 | format(sql: string, values?: any | any[] | { [param: string]: any }): string; 306 | 307 | on(event: string, listener: Function): this; 308 | 309 | rollback(callback: () => void): void; 310 | } 311 | 312 | export = Connection; 313 | } 314 | 315 | // Generated by typings 316 | // Source: https://raw.githubusercontent.com/types/npm-mysql/c3a7c0bf94ecf886d5ce7b5e4e5e7d17cf5b9668/lib/PoolConnection.d.ts 317 | declare module '~mysql2~mysql/lib/PoolConnection' { 318 | 319 | import Connection = require('~mysql2~mysql/lib/Connection'); 320 | 321 | class PoolConnection extends Connection { 322 | release(): void; 323 | } 324 | 325 | export = PoolConnection; 326 | } 327 | 328 | // Generated by typings 329 | // Source: https://raw.githubusercontent.com/types/npm-mysql/c3a7c0bf94ecf886d5ce7b5e4e5e7d17cf5b9668/lib/Pool.d.ts 330 | declare module '~mysql2~mysql/lib/Pool' { 331 | 332 | import Query = require('~mysql2~mysql/lib/protocol/sequences/Query'); 333 | import {OkPacket, RowDataPacket, FieldPacket} from '~mysql2~mysql/lib/protocol/packets/index'; 334 | import Connection = require('~mysql2~mysql/lib/Connection'); 335 | import PoolConnection = require('~mysql2~mysql/lib/PoolConnection'); 336 | import {EventEmitter} from 'events'; 337 | 338 | namespace Pool { 339 | 340 | export interface PoolOptions extends Connection.ConnectionOptions { 341 | /** 342 | * The milliseconds before a timeout occurs during the connection acquisition. This is slightly different from connectTimeout, 343 | * because acquiring a pool connection does not always involve making a connection. (Default: 10 seconds) 344 | */ 345 | acquireTimeout?: number; 346 | 347 | /** 348 | * Determines the pool's action when no connections are available and the limit has been reached. If true, the pool will queue 349 | * the connection request and call it when one becomes available. If false, the pool will immediately call back with an error. 350 | * (Default: true) 351 | */ 352 | waitForConnections?: boolean; 353 | 354 | /** 355 | * The maximum number of connections to create at once. (Default: 10) 356 | */ 357 | connectionLimit?: number; 358 | 359 | /** 360 | * The maximum number of connection requests the pool will queue before returning an error from getConnection. If set to 0, there 361 | * is no limit to the number of queued connection requests. (Default: 0) 362 | */ 363 | queueLimit?: number; 364 | } 365 | } 366 | 367 | class Pool extends EventEmitter { 368 | 369 | config: Pool.PoolOptions; 370 | 371 | getConnection(callback: (err: NodeJS.ErrnoException, connection: PoolConnection) => any): void; 372 | 373 | query(sql: string, callback?: (err: Query.QueryError | null, result: T, fields: FieldPacket[]) => any): Query; 374 | query(sql: string, values: any | any[] | { [param: string]: any }, callback?: (err: Query.QueryError | null, result: T, fields: FieldPacket[]) => any): Query; 375 | query(options: Query.QueryOptions, callback?: (err: Query.QueryError | null, result: T, fields?: FieldPacket[]) => any): Query; 376 | query(options: Query.QueryOptions, values: any | any[] | { [param: string]: any }, callback?: (err: Query.QueryError | null, result: T, fields: FieldPacket[]) => any): Query; 377 | 378 | end(callback?: (err: NodeJS.ErrnoException | null, ...args: any[]) => any): void; 379 | 380 | on(event: string, listener: Function): this; 381 | on(event: 'connection', listener: (connection: PoolConnection) => any): this; 382 | } 383 | 384 | export = Pool; 385 | } 386 | 387 | // Generated by typings 388 | // Source: https://raw.githubusercontent.com/types/npm-mysql/c3a7c0bf94ecf886d5ce7b5e4e5e7d17cf5b9668/lib/PoolCluster.d.ts 389 | declare module '~mysql2~mysql/lib/PoolCluster' { 390 | 391 | import Connection = require('~mysql2~mysql/lib/Connection'); 392 | import PoolConnection = require('~mysql2~mysql/lib/PoolConnection'); 393 | import {EventEmitter} from 'events'; 394 | 395 | namespace PoolCluster { 396 | 397 | export interface PoolClusterOptions { 398 | /** 399 | * If true, PoolCluster will attempt to reconnect when connection fails. (Default: true) 400 | */ 401 | canRetry?: boolean; 402 | 403 | /** 404 | * If connection fails, node's errorCount increases. When errorCount is greater than removeNodeErrorCount, 405 | * remove a node in the PoolCluster. (Default: 5) 406 | */ 407 | removeNodeErrorCount?: number; 408 | 409 | /** 410 | * If connection fails, specifies the number of milliseconds before another connection attempt will be made. 411 | * If set to 0, then node will be removed instead and never re-used. (Default: 0) 412 | */ 413 | restoreNodeTimeout?: number; 414 | 415 | /** 416 | * The default selector. (Default: RR) 417 | * RR: Select one alternately. (Round-Robin) 418 | * RANDOM: Select the node by random function. 419 | * ORDER: Select the first node available unconditionally. 420 | */ 421 | defaultSelector?: string; 422 | } 423 | } 424 | 425 | class PoolCluster extends EventEmitter { 426 | 427 | config: PoolCluster.PoolClusterOptions; 428 | 429 | add(config: PoolCluster.PoolClusterOptions): void; 430 | add(group: string, config: PoolCluster.PoolClusterOptions): void; 431 | 432 | end(): void; 433 | 434 | getConnection(callback: (err: NodeJS.ErrnoException | null, connection: PoolConnection) => void): void; 435 | getConnection(group: string, callback: (err: NodeJS.ErrnoException | null, connection: PoolConnection) => void): void; 436 | getConnection(group: string, selector: string, callback: (err: NodeJS.ErrnoException | null, connection: PoolConnection) => void): void; 437 | 438 | of(pattern: string, selector?: string): PoolCluster; 439 | 440 | on(event: string, listener: Function): this; 441 | on(event: 'remove', listener: (nodeId: number) => void): this; 442 | on(event: 'connection', listener: (connection: PoolConnection) => void): this; 443 | } 444 | 445 | export = PoolCluster; 446 | } 447 | 448 | // Generated by typings 449 | // Source: https://raw.githubusercontent.com/types/npm-mysql/c3a7c0bf94ecf886d5ce7b5e4e5e7d17cf5b9668/lib/protocol/sequences/Sequence.d.ts 450 | declare module '~mysql2~mysql/lib/protocol/sequences/Sequence' { 451 | 452 | import {EventEmitter} from 'events'; 453 | 454 | class Sequence extends EventEmitter { } 455 | export = Sequence; 456 | } 457 | 458 | // Generated by typings 459 | // Source: https://raw.githubusercontent.com/types/npm-mysql/c3a7c0bf94ecf886d5ce7b5e4e5e7d17cf5b9668/lib/protocol/sequences/Query.d.ts 460 | declare module '~mysql2~mysql/lib/protocol/sequences/Query' { 461 | 462 | import Sequence = require('~mysql2~mysql/lib/protocol/sequences/Sequence'); 463 | import {OkPacket, RowDataPacket, FieldPacket} from '~mysql2~mysql/lib/protocol/packets/index'; 464 | import {Readable} from 'stream'; 465 | 466 | namespace Query { 467 | 468 | export interface QueryOptions { 469 | /** 470 | * The SQL for the query 471 | */ 472 | sql: string; 473 | 474 | /** 475 | * The values for the query 476 | */ 477 | values?: any | any[] | { [param: string]: any }; 478 | 479 | /** 480 | * Every operation takes an optional inactivity timeout option. This allows you to specify appropriate timeouts for 481 | * operations. It is important to note that these timeouts are not part of the MySQL protocol, and rather timeout 482 | * operations through the client. This means that when a timeout is reached, the connection it occurred on will be 483 | * destroyed and no further operations can be performed. 484 | */ 485 | timeout?: number; 486 | 487 | /** 488 | * Either a boolean or string. If true, tables will be nested objects. If string (e.g. '_'), tables will be 489 | * nested as tableName_fieldName 490 | */ 491 | nestTables?: any; 492 | 493 | /** 494 | * Determines if column values should be converted to native JavaScript types. It is not recommended (and may go away / change in the future) 495 | * to disable type casting, but you can currently do so on either the connection or query level. (Default: true) 496 | * 497 | * You can also specify a function (field: any, next: () => void) => {} to do the type casting yourself. 498 | * 499 | * WARNING: YOU MUST INVOKE the parser using one of these three field functions in your custom typeCast callback. They can only be called once. 500 | * 501 | * field.string() 502 | * field.buffer() 503 | * field.geometry() 504 | * 505 | * are aliases for 506 | * 507 | * parser.parseLengthCodedString() 508 | * parser.parseLengthCodedBuffer() 509 | * parser.parseGeometryValue() 510 | * 511 | * You can find which field function you need to use by looking at: RowDataPacket.prototype._typeCast 512 | */ 513 | typeCast?: any; 514 | } 515 | 516 | export interface StreamOptions { 517 | /** 518 | * Sets the max buffer size in objects of a stream 519 | */ 520 | highWaterMark?: number; 521 | 522 | /** 523 | * The object mode of the stream (Default: true) 524 | */ 525 | objectMode?: any; 526 | } 527 | 528 | export interface QueryError extends NodeJS.ErrnoException { 529 | /** 530 | * Either a MySQL server error (e.g. 'ER_ACCESS_DENIED_ERROR'), 531 | * a node.js error (e.g. 'ECONNREFUSED') or an internal error 532 | * (e.g. 'PROTOCOL_CONNECTION_LOST'). 533 | */ 534 | code: string; 535 | 536 | /** 537 | * The sql state marker 538 | */ 539 | sqlStateMarker?: string; 540 | 541 | /** 542 | * The sql state 543 | */ 544 | sqlState?: string; 545 | 546 | /** 547 | * The field count 548 | */ 549 | fieldCount?: number; 550 | 551 | /** 552 | * Boolean, indicating if this error is terminal to the connection object. 553 | */ 554 | fatal: boolean; 555 | } 556 | } 557 | 558 | class Query extends Sequence { 559 | 560 | /** 561 | * The SQL for a constructed query 562 | */ 563 | sql: string; 564 | 565 | /** 566 | * Emits a query packet to start the query 567 | */ 568 | start(): void; 569 | 570 | /** 571 | * Determines the packet class to use given the first byte of the packet. 572 | * 573 | * @param firstByte The first byte of the packet 574 | * @param parser The packet parser 575 | */ 576 | determinePacket(firstByte: number, parser: any): any; 577 | 578 | /** 579 | * Creates a Readable stream with the given options 580 | * 581 | * @param options The options for the stream. 582 | */ 583 | stream(options: Query.StreamOptions): Readable; 584 | 585 | on(event: string, listener: Function): this; 586 | on(event: 'error', listener: (err: Query.QueryError) => any): this; 587 | on(event: 'fields', listener: (fields: FieldPacket, index: number) => any): this; 588 | on(event: 'result', listener: (result: RowDataPacket | OkPacket, index: number) => any): this; 589 | on(event: 'end', listener: () => any): this; 590 | } 591 | 592 | export = Query; 593 | } 594 | 595 | // Generated by typings 596 | // Source: https://raw.githubusercontent.com/types/npm-mysql/c3a7c0bf94ecf886d5ce7b5e4e5e7d17cf5b9668/lib/protocol/packets/OkPacket.d.ts 597 | declare module '~mysql2~mysql/lib/protocol/packets/OkPacket' { 598 | 599 | interface OkPacket { 600 | constructor: { 601 | name: 'OkPacket' 602 | }; 603 | fieldCount: number; 604 | affectedRows: number; 605 | changedRows: number; 606 | insertId: number; 607 | serverStatus: number; 608 | warningCount: number; 609 | message: string; 610 | procotol41: boolean; 611 | } 612 | 613 | export = OkPacket; 614 | } 615 | 616 | // Generated by typings 617 | // Source: https://raw.githubusercontent.com/types/npm-mysql/c3a7c0bf94ecf886d5ce7b5e4e5e7d17cf5b9668/lib/protocol/packets/RowDataPacket.d.ts 618 | declare module '~mysql2~mysql/lib/protocol/packets/RowDataPacket' { 619 | 620 | interface RowDataPacket { 621 | constructor: { 622 | name: 'RowDataPacket' 623 | }; 624 | [column: string]: any; 625 | [column: number]: any; 626 | } 627 | 628 | export = RowDataPacket; 629 | } 630 | 631 | // Generated by typings 632 | // Source: https://raw.githubusercontent.com/types/npm-mysql/c3a7c0bf94ecf886d5ce7b5e4e5e7d17cf5b9668/lib/protocol/packets/FieldPacket.d.ts 633 | declare module '~mysql2~mysql/lib/protocol/packets/FieldPacket' { 634 | 635 | interface FieldPacket { 636 | constructor: { 637 | name: 'FieldPacket' 638 | }; 639 | catalog: string; 640 | charsetNr: number; 641 | db: string; 642 | decimals: number; 643 | default: any; 644 | flags: number; 645 | length: number; 646 | name: string; 647 | orgName: string; 648 | orgTable: string; 649 | protocol41: boolean; 650 | table: string; 651 | type: number; 652 | zerofill: boolean; 653 | } 654 | 655 | export = FieldPacket; 656 | } 657 | 658 | // Generated by typings 659 | // Source: https://raw.githubusercontent.com/types/npm-mysql/c3a7c0bf94ecf886d5ce7b5e4e5e7d17cf5b9668/lib/protocol/packets/index.d.ts 660 | declare module '~mysql2~mysql/lib/protocol/packets/index' { 661 | 662 | import OkPacket = require('~mysql2~mysql/lib/protocol/packets/OkPacket'); 663 | import RowDataPacket = require('~mysql2~mysql/lib/protocol/packets/RowDataPacket'); 664 | import FieldPacket = require('~mysql2~mysql/lib/protocol/packets/FieldPacket'); 665 | 666 | export { 667 | OkPacket, 668 | RowDataPacket, 669 | FieldPacket 670 | } 671 | } 672 | 673 | // Generated by typings 674 | // Source: https://raw.githubusercontent.com/types/npm-mysql/c3a7c0bf94ecf886d5ce7b5e4e5e7d17cf5b9668/index.d.ts 675 | declare module '~mysql2~mysql/index' { 676 | 677 | import Connection = require('~mysql2~mysql/lib/Connection'); 678 | import {ConnectionOptions, SslOptions} from '~mysql2~mysql/lib/Connection'; 679 | import PoolConnection = require('~mysql2~mysql/lib/PoolConnection'); 680 | import Pool = require('~mysql2~mysql/lib/Pool'); 681 | import {PoolOptions} from '~mysql2~mysql/lib/Pool'; 682 | import PoolCluster = require('~mysql2~mysql/lib/PoolCluster'); 683 | import {PoolClusterOptions} from '~mysql2~mysql/lib/PoolCluster'; 684 | import Query = require('~mysql2~mysql/lib/protocol/sequences/Query'); 685 | import {QueryOptions, StreamOptions, QueryError} from '~mysql2~mysql/lib/protocol/sequences/Query'; 686 | 687 | export function createConnection(connectionUri: string): Connection; 688 | export function createConnection(config: Connection.ConnectionOptions): Connection; 689 | export function createPool(config: Pool.PoolOptions): Pool; 690 | export function createPoolCluster(config?: PoolCluster.PoolClusterOptions): PoolCluster; 691 | export function escape(value: any): string; 692 | export function format(sql: string): string; 693 | export function format(sql: string, values: any[]): string; 694 | export function format(sql: string, values: any): string; 695 | 696 | export { 697 | ConnectionOptions, 698 | SslOptions, 699 | PoolOptions, 700 | PoolClusterOptions, 701 | QueryOptions, 702 | QueryError 703 | } 704 | export * from '~mysql2~mysql/lib/protocol/packets/index' 705 | 706 | // Expose class interfaces 707 | export interface Connection extends Connection {} 708 | export interface PoolConnection extends PoolConnection {} 709 | export interface Pool extends Pool {} 710 | export interface PoolCluster extends PoolCluster {} 711 | export interface Query extends Query {} 712 | } 713 | 714 | // Generated by typings 715 | // Source: https://raw.githubusercontent.com/types/npm-mysql2/66662aa2098481bceb334529f884712fd2632c52/index.d.ts 716 | declare module '~mysql2/index' { 717 | 718 | import '~mysql2/promise'; 719 | import * as mysql from '~mysql2~mysql/index'; 720 | export * from '~mysql2~mysql/index'; 721 | 722 | export interface Connection extends mysql.Connection { 723 | execute(sql: string, callback?: (err: mysql.QueryError | null, result: T, fields: mysql.FieldPacket[]) => any): mysql.Query; 724 | execute(sql: string, values: any | any[] | { [param: string]: any }, callback?: (err: mysql.QueryError | null, result: T, fields: mysql.FieldPacket[]) => any): mysql.Query; 725 | execute(options: mysql.QueryOptions, callback?: (err: mysql.QueryError | null, result: T, fields?: mysql.FieldPacket[]) => any): mysql.Query; 726 | execute(options: mysql.QueryOptions, values: any | any[] | { [param: string]: any }, callback?: (err: mysql.QueryError | null, result: T, fields: mysql.FieldPacket[]) => any): mysql.Query; 727 | } 728 | 729 | export interface PoolConnection extends mysql.PoolConnection, Connection {} 730 | 731 | export interface Pool extends mysql.Connection { 732 | execute(sql: string, callback?: (err: mysql.QueryError | null, result: T, fields: mysql.FieldPacket[]) => any): mysql.Query; 733 | execute(sql: string, values: any | any[] | { [param: string]: any }, callback?: (err: mysql.QueryError | null, result: T, fields: mysql.FieldPacket[]) => any): mysql.Query; 734 | execute(options: mysql.QueryOptions, callback?: (err: mysql.QueryError | null, result: T, fields?: mysql.FieldPacket[]) => any): mysql.Query; 735 | execute(options: mysql.QueryOptions, values: any | any[] | { [param: string]: any }, callback?: (err: mysql.QueryError | null, result: T, fields: mysql.FieldPacket[]) => any): mysql.Query; 736 | getConnection(callback: (err: NodeJS.ErrnoException, connection: PoolConnection) => any): void; 737 | on(event: 'connection', listener: (connection: PoolConnection) => any): this; 738 | } 739 | 740 | export function createConnection(connectionUri: string): Connection; 741 | export function createConnection(config: mysql.ConnectionOptions): Connection; 742 | export function createPool(config: mysql.PoolOptions): Pool; 743 | } 744 | declare module 'mysql2/index' { 745 | export * from '~mysql2/index'; 746 | } 747 | declare module 'mysql2' { 748 | export * from '~mysql2/index'; 749 | } 750 | --------------------------------------------------------------------------------