├── .dockerignore ├── .eslintrc ├── .gitattributes ├── .github └── workflows │ └── node.js.yml ├── .gitignore ├── .prettierrc ├── CODE_OF_CONDUCT.md ├── Dockerfile ├── LICENSE ├── README.md ├── bitbucket-pipelines.yml ├── bot.sql ├── conf.json.dist ├── docker-compose.yml ├── documentation ├── backtest_result.png ├── cryptobot.png ├── manual_order.png ├── slack_signals.png └── trades.png ├── index.js ├── instance.js.dist ├── instance.js.dist_trade.js ├── instance.js.dist_trade_binance_futures.js ├── instance.js.dist_trade_binance_futures_coin.js ├── instance.js.dist_trade_binance_margin.js ├── instance.js.dist_trade_bybit.js ├── package-lock.json ├── package.json ├── patches └── bitmex-realtime-api+0.4.3.patch ├── src ├── command │ ├── backfill.js │ ├── server.js │ └── trade.js ├── dict │ ├── candlestick.js │ ├── exchange_candlestick.js │ ├── exchange_order.js │ ├── exchange_position.js │ ├── order.js │ ├── order_capital.js │ ├── orderbook.js │ ├── pair_state.js │ ├── period.js │ ├── position.js │ ├── signal.js │ ├── strategy │ │ ├── single_target.js │ │ └── stop_loss.js │ ├── strategy_context.js │ └── ticker.js ├── event │ ├── candlestick_event.js │ ├── exchange_order_event.js │ ├── exchange_orders_event.js │ ├── order_event.js │ ├── orderbook_event.js │ ├── position_state_change_event.js │ └── ticker_event.js ├── exchange │ ├── binance.js │ ├── binance_futures.js │ ├── binance_futures_coin.js │ ├── binance_margin.js │ ├── bitfinex.js │ ├── bitmex.js │ ├── bitmex_testnet.js │ ├── bybit.js │ ├── bybit_unified.js │ ├── ccxt │ │ └── ccxt_exchange_order.js │ ├── coinbase_pro.js │ ├── noop.js │ └── utils │ │ ├── candles_from_trades.js │ │ ├── ccxt_util.js │ │ ├── order_bag.js │ │ └── trades_util.js ├── modules │ ├── backfill.js │ ├── backtest.js │ ├── exchange │ │ ├── exchange_candle_combine.js │ │ ├── exchange_manager.js │ │ └── exchange_position_watcher.js │ ├── http.js │ ├── listener │ │ ├── create_order_listener.js │ │ ├── exchange_order_watchdog_listener.js │ │ ├── tick_listener.js │ │ └── ticker_database_listener.js │ ├── order │ │ ├── order_calculator.js │ │ ├── order_executor.js │ │ ├── risk_reward_ratio_calculator.js │ │ └── stop_loss_calculator.js │ ├── orders │ │ └── orders_http.js │ ├── pairs │ │ ├── pair_config.js │ │ ├── pair_interval.js │ │ ├── pair_state_execution.js │ │ ├── pair_state_manager.js │ │ └── pairs_http.js │ ├── repository │ │ ├── candlestick_repository.js │ │ ├── logs_repository.js │ │ ├── signal_repository.js │ │ ├── ticker_log_repository.js │ │ └── ticker_repository.js │ ├── services.js │ ├── signal │ │ ├── signal_http.js │ │ └── signal_logger.js │ ├── strategy │ │ ├── dict │ │ │ ├── indicator_builder.js │ │ │ ├── indicator_period.js │ │ │ └── signal_result.js │ │ ├── strategies │ │ │ ├── awesome_oscillator_cross_zero.js │ │ │ ├── cci.js │ │ │ ├── cci_macd.js │ │ │ ├── dca_dipper │ │ │ │ ├── README.md │ │ │ │ ├── dca_dipper.js │ │ │ │ └── doc │ │ │ │ │ ├── crypto_dca_btc.png │ │ │ │ │ └── crypto_dca_eth.png │ │ │ ├── dip_catcher │ │ │ │ ├── README.md │ │ │ │ ├── dip_catcher.js │ │ │ │ └── doc │ │ │ │ │ ├── dip_catcher_backtest.png │ │ │ │ │ └── dip_catcher_tradingview.png │ │ │ ├── macd.js │ │ │ ├── noop.js │ │ │ ├── obv_pump_dump.js │ │ │ ├── parabolicsar.js │ │ │ ├── pivot_reversal_strategy.js │ │ │ └── trader.js │ │ └── strategy_manager.js │ ├── system │ │ ├── candle_export_http.js │ │ ├── candle_importer.js │ │ ├── candlestick_resample.js │ │ ├── logs_http.js │ │ └── system_util.js │ ├── ta.js │ └── trade.js ├── notify │ ├── mail.js │ ├── notify.js │ ├── slack.js │ └── telegram.js ├── storage │ └── tickers.js └── utils │ ├── common_util.js │ ├── indicators.js │ ├── instance_util.js │ ├── order_util.js │ ├── queue.js │ ├── request_client.js │ ├── resample.js │ ├── technical_analysis.js │ ├── technical_analysis_validator.js │ ├── technical_pattern.js │ ├── throttler.js │ └── winston_sqlite_transport.js ├── templates ├── backtest.html.twig ├── backtest_submit.html.twig ├── backtest_submit_multiple.html.twig ├── base.html.twig ├── candle_stick_export.html.twig ├── components │ ├── alert.html.twig │ ├── backtest_summary.html.twig │ └── backtest_table.html.twig ├── desks.html.twig ├── layout.html.twig ├── logs.html.twig ├── orders │ ├── index.html.twig │ ├── layout.html.twig │ └── orders.html.twig ├── pairs.html.twig ├── signals.html.twig ├── trades.html.twig ├── tradingview.html.twig └── tradingview_desk.html.twig ├── test ├── deploy.test.js ├── dict │ └── order.test.js ├── exchange │ ├── binance.test.js │ ├── binance │ │ ├── account-info.json │ │ ├── events.json │ │ └── orders.json │ ├── binance_futures.test.js │ ├── binance_futures │ │ ├── positions.json │ │ ├── websocket-orders.json │ │ ├── websocket_position_close.json │ │ └── websocket_position_open.json │ ├── binance_margin.test.js │ ├── binance_margin │ │ └── account_info.json │ ├── bitfinex.test.js │ ├── bitfinex │ │ ├── on-orders.json │ │ ├── on-ps.json │ │ └── on-req-reject.json │ ├── bitmex.test.js │ ├── bitmex │ │ ├── ws-orders.json │ │ ├── ws-positions-updates.json │ │ └── ws-positions.json │ ├── bybit.test.js │ ├── bybit │ │ ├── ws-orders.json │ │ └── ws-positions.json │ ├── coinbase_pro.test.js │ ├── ftx │ │ ├── orders.json │ │ └── positions.json │ └── utils │ │ ├── ccxt.json │ │ ├── order_bag.test.js │ │ └── trades_util.test.js ├── modules │ ├── exchange │ │ ├── exchange_candle_combine.test.js │ │ ├── exchange_manager.test.js │ │ └── exchange_position_watcher.test.js │ ├── listener │ │ ├── exchange_order_watchdog_listener.test.js │ │ └── tick_listener.test.js │ ├── order │ │ ├── order_calculator.test.js │ │ ├── order_executor.test.js │ │ ├── risk_reward_ratio_calculator.test.js │ │ └── stop_loss_calculator.test.js │ ├── pairs │ │ ├── pair_state_execution.test.js │ │ └── pair_state_manager.test.js │ └── strategy │ │ ├── dict │ │ ├── indicator_period.test.js │ │ └── signal_result.test.js │ │ └── strategies │ │ ├── awesome_oscillator_cross_zero.test.js │ │ ├── cci.test.js │ │ ├── obv_pump_dump.test.js │ │ └── strategy_manager.test.js ├── storage │ └── tickers.test.js ├── system │ └── system_util.test.js └── utils │ ├── fixtures │ ├── pattern │ │ ├── volume_pump_BNBUSDT.json │ │ └── volume_pump_BNBUSDT.png │ └── xbt-usd-5m.json │ ├── order_util.test.js │ ├── request_client.test.js │ ├── resample.test.js │ ├── technical_analysis.test.js │ ├── technical_analysis_validator.test.js │ └── technical_pattern.test.js ├── var ├── log │ └── .gitignore └── strategies │ ├── .gitignore │ └── README.md └── web └── static ├── css ├── adminlte.css ├── adminlte.css.map ├── adminlte.min.css ├── adminlte.min.css.map ├── backtest.css └── style.css ├── favicon.ico ├── img ├── bitcoin.svg └── exchanges │ ├── binance.png │ ├── binance_futures.png │ ├── binance_margin.png │ ├── bitfinex.png │ ├── bitmex.png │ ├── bybit.png │ ├── bybit_linear.png │ ├── bybit_unified.png │ ├── coinbase_pro.png │ └── ftx.png ├── js ├── adminlte.js ├── adminlte.js.map ├── adminlte.min.js ├── adminlte.min.js.map ├── backtest-form.js ├── backtest.js ├── orders.js ├── pairs.js ├── trades.js └── trades.vue └── robots.txt /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true, 4 | "es6": true, 5 | "jquery": true, 6 | "mocha": true 7 | }, 8 | "extends": ["airbnb", "prettier", "plugin:node/recommended"], 9 | "plugins": ["promise","prettier"], 10 | "parserOptions": { 11 | "ecmaVersion": 2022 12 | }, 13 | "rules": { 14 | "prettier/prettier": "error", 15 | "no-unused-vars": "warn", 16 | "no-console": "off", 17 | "func-names": "off", 18 | "no-process-exit": "off", 19 | "object-shorthand": "off", 20 | "class-methods-use-this": "off", 21 | "no-continue": "off", 22 | "no-await-in-loop": "warn", 23 | "comma-dangle": ["error", "never"], 24 | "arrow-parens": ["error", "as-needed"], 25 | "no-plusplus": "off", 26 | "no-multi-assign": "off", 27 | "no-restricted-syntax": "off", 28 | "guard-for-in": "off" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.css linguist-detectable=false 2 | *.js linguist-detectable=true -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ "master" ] 9 | pull_request: 10 | branches: [ "master" ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [20.x] 20 | 21 | steps: 22 | - uses: actions/checkout@v3 23 | - name: Use Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v3 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | cache: 'npm' 28 | - run: npm install 29 | - run: npm test 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ./node_modules/ 2 | ./idea/ 3 | ./vscode/ 4 | 5 | # Project config 6 | conf.json 7 | instance.js 8 | bot.db 9 | bot.db-shm 10 | bot.db-wal 11 | ./var/ -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "printWidth": 160, 4 | "endOfLine": "lf", 5 | "trailingComma": "none", 6 | "arrowParens": "avoid" 7 | } 8 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at daniel@espendiller.net. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:10 2 | 3 | MAINTAINER Daniel Espendiller 4 | 5 | # Install build-essential, sqlite in order 6 | RUN apt-get update && apt-get install -y \ 7 | sqlite \ 8 | && rm -rf /var/lib/apt/lists/* 9 | 10 | WORKDIR /usr/src/app 11 | 12 | # Install app dependencies 13 | COPY package.json /usr/src/app/ 14 | RUN npm install --production && \ 15 | npm cache clean --force 16 | 17 | # Bundle app source 18 | COPY . /usr/src/app 19 | 20 | # Apply all patches in app 21 | RUN npm run postinstall 22 | 23 | EXPOSE 8080 24 | CMD ["npm", "run", "start"] 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 Daniel Espendiller 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 14 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 15 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 16 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 17 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 18 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 19 | OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /bitbucket-pipelines.yml: -------------------------------------------------------------------------------- 1 | image: node:20 2 | pipelines: 3 | default: 4 | - step: 5 | script: 6 | - npm install 7 | - npm test -------------------------------------------------------------------------------- /bot.sql: -------------------------------------------------------------------------------- 1 | PRAGMA auto_vacuum = INCREMENTAL; 2 | 3 | CREATE TABLE IF NOT EXISTS candlesticks ( 4 | id INTEGER PRIMARY KEY AUTOINCREMENT, 5 | exchange VARCHAR(255) NULL, 6 | symbol VARCHAR(255) NULL, 7 | period VARCHAR(255) NULL, 8 | time INTEGER NULL, 9 | open REAL NULL, 10 | high REAL NULL, 11 | low REAL NULL, 12 | close REAL NULL, 13 | volume REAL NULL 14 | ); 15 | 16 | CREATE UNIQUE INDEX unique_candle 17 | ON candlesticks (exchange, symbol, period, time); 18 | 19 | CREATE INDEX time_idx ON candlesticks (time); 20 | CREATE INDEX exchange_symbol_idx ON candlesticks (exchange, symbol); 21 | 22 | CREATE TABLE IF NOT EXISTS candlesticks_log ( 23 | id INTEGER PRIMARY KEY AUTOINCREMENT, 24 | income_at BIGINT NULL, 25 | exchange VARCHAR(255) NULL, 26 | symbol VARCHAR(255) NULL, 27 | period VARCHAR(255) NULL, 28 | time INTEGER NULL, 29 | open REAL NULL, 30 | high REAL NULL, 31 | low REAL NULL, 32 | close REAL NULL, 33 | volume REAL NULL 34 | ); 35 | 36 | CREATE INDEX candle_idx ON candlesticks_log (exchange, symbol, period, time); 37 | 38 | CREATE TABLE IF NOT EXISTS ticker ( 39 | id INTEGER PRIMARY KEY AUTOINCREMENT, 40 | exchange VARCHAR(255) NULL, 41 | symbol VARCHAR(255) NULL, 42 | ask REAL NULL, 43 | bid REAL NULL, 44 | updated_at INT NULL 45 | ); 46 | 47 | CREATE UNIQUE INDEX ticker_unique 48 | ON ticker (exchange, symbol); 49 | 50 | CREATE TABLE IF NOT EXISTS ticker_log ( 51 | id INTEGER PRIMARY KEY AUTOINCREMENT, 52 | exchange VARCHAR(255) NULL, 53 | symbol VARCHAR(255) NULL, 54 | ask REAL NULL, 55 | bid REAL NULL, 56 | income_at BIGINT NULL 57 | ); 58 | CREATE INDEX ticker_log_idx ON ticker_log (exchange, symbol); 59 | CREATE INDEX ticker_log_time_idx ON ticker_log (exchange, symbol, income_at); 60 | 61 | CREATE TABLE IF NOT EXISTS signals ( 62 | id INTEGER PRIMARY KEY AUTOINCREMENT, 63 | exchange VARCHAR(255) NULL, 64 | symbol VARCHAR(255) NULL, 65 | ask REAL NULL, 66 | bid REAL NULL, 67 | options TEXT NULL, 68 | side VARCHAR(50) NULL, 69 | strategy VARCHAR(50) NULL, 70 | income_at BIGINT NULL, 71 | state VARCHAR(50) NULL 72 | ); 73 | CREATE INDEX symbol_idx ON signals (exchange, symbol); 74 | 75 | CREATE TABLE IF NOT EXISTS logs ( 76 | uuid VARCHAR(64) PRIMARY KEY, 77 | level VARCHAR(32) NOT NULL, 78 | message TEXT NULL, 79 | created_at INT NOT NULL 80 | ); 81 | 82 | CREATE INDEX created_at_idx ON logs (created_at); 83 | CREATE INDEX level_created_at_idx ON logs (level, created_at); 84 | CREATE INDEX level_idx ON logs (level); -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | 3 | services: 4 | bot: 5 | container_name: crypto-trading-bot 6 | image: node:10 7 | build: 8 | context: . 9 | dockerfile: Dockerfile 10 | volumes: 11 | - .:/usr/src/app 12 | - /usr/src/app/node_modules 13 | ports: 14 | - "8080:8080" 15 | command: npm start 16 | -------------------------------------------------------------------------------- /documentation/backtest_result.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Haehnchen/crypto-trading-bot/a5d92b153c6cbe8770a28c693309640bb30cfea1/documentation/backtest_result.png -------------------------------------------------------------------------------- /documentation/cryptobot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Haehnchen/crypto-trading-bot/a5d92b153c6cbe8770a28c693309640bb30cfea1/documentation/cryptobot.png -------------------------------------------------------------------------------- /documentation/manual_order.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Haehnchen/crypto-trading-bot/a5d92b153c6cbe8770a28c693309640bb30cfea1/documentation/manual_order.png -------------------------------------------------------------------------------- /documentation/slack_signals.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Haehnchen/crypto-trading-bot/a5d92b153c6cbe8770a28c693309640bb30cfea1/documentation/slack_signals.png -------------------------------------------------------------------------------- /documentation/trades.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Haehnchen/crypto-trading-bot/a5d92b153c6cbe8770a28c693309640bb30cfea1/documentation/trades.png -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const program = require('commander'); 2 | const TradeCommand = require('./src/command/trade.js'); 3 | const ServerCommand = require('./src/command/server.js'); 4 | const Backfill = require('./src/command/backfill.js'); 5 | 6 | // init 7 | const services = require('./src/modules/services'); 8 | 9 | program 10 | .command('trade') 11 | .description('start crypto trading bot') 12 | .option('-i, --instance ', 'Instance to start', 'instance.json') 13 | .action(async options => { 14 | await services.boot(__dirname); 15 | 16 | const cmd = new TradeCommand(options.instance); 17 | cmd.execute(); 18 | }); 19 | 20 | program 21 | .command('backfill') 22 | .description('process historical data collection') 23 | .option('-e, --exchange ') 24 | .option('-s, --symbol ') 25 | .option('-p, --period ', '1m 5m, 15m, 1h', '15m') 26 | .option('-d, --date ', 'days in past to collect start', '7') 27 | .action(async options => { 28 | if (!options.exchange || !options.symbol || !options.period || !options.date) { 29 | throw new Error('Not all options are given'); 30 | } 31 | 32 | await services.boot(__dirname); 33 | 34 | const cmd = new Backfill(); 35 | await cmd.execute(options.exchange, options.symbol, options.period, options.date); 36 | 37 | process.exit(); 38 | }); 39 | 40 | program 41 | .command('server') 42 | .description('') 43 | .option('-i, --instance ', 'Instance to start', 'instance.json') 44 | .action(options => { 45 | const cmd = new ServerCommand(options.instance); 46 | cmd.execute(); 47 | }); 48 | 49 | program.parse(process.argv); 50 | -------------------------------------------------------------------------------- /instance.js.dist_trade.js: -------------------------------------------------------------------------------- 1 | const c = (module.exports = {}); 2 | 3 | c.symbols = [ 4 | { 5 | symbol: 'ETHUSDT', 6 | exchange: 'binance_futures', 7 | periods: ['1m', '15m', '1h'], 8 | trade: { 9 | currency_capital: 10, 10 | strategies: [ 11 | { 12 | strategy: 'dip_catcher', 13 | interval: '15m', 14 | options: { 15 | period: '15m' 16 | } 17 | } 18 | ] 19 | }, 20 | watchdogs: [ 21 | { 22 | name: 'risk_reward_ratio', 23 | target_percent: 3.1, 24 | stop_percent: 2.1 25 | } 26 | ] 27 | } 28 | ]; -------------------------------------------------------------------------------- /instance.js.dist_trade_bybit.js: -------------------------------------------------------------------------------- 1 | const c = (module.exports = {}); 2 | 3 | c.symbols = []; 4 | 5 | c.init = async () => { 6 | const j = ['BTC/USDT:USDT', 'ETH/USDT:USDT', 'LTC/USDT:USDT', 'SOL/USDT:USDT', 'ETC/USDT:USDT', '1000PEPE/USDT:USDT', 'XRP/USDT:USDT']; 7 | 8 | j.forEach(pair => { 9 | c.symbols.push({ 10 | symbol: pair, 11 | periods: ['1m', '15m', '1h'], 12 | exchange: 'bybit_unified', 13 | state: 'watch' 14 | }); 15 | }); 16 | }; 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "crypto-trading-bot", 3 | "version": "0.0.0", 4 | "author": "Daniel Espendiller", 5 | "bugs": "https://github.com/Haehnchen/crypto-trading-bot/issues", 6 | "contributors": [ 7 | { 8 | "email": "daniel@espendiller.net", 9 | "name": "Daniel Espendiller", 10 | "url": "http://espend.de/" 11 | }, 12 | { 13 | "email": "phuc@nguyenminhphuc.com", 14 | "name": "Phuc Nguyen Minh", 15 | "url": "https://nguyenminhphuc.com/" 16 | } 17 | ], 18 | "dependencies": { 19 | "async": "^3.2.5", 20 | "basic-auth": "^2.0.1", 21 | "better-queue": "^3.8.12", 22 | "better-sqlite3": "^11.1.2", 23 | "bfx-api-node-models": "^1.6.3", 24 | "binance-api-node": "^0.12.0", 25 | "bitfinex-api-node": "^5.0.4", 26 | "bitmex-realtime-api": "^0.5.1", 27 | "bybit-api": "^3.2.0", 28 | "ccxt": "^4.3.80", 29 | "coinbase-pro": "^0.9.0", 30 | "colors": "^1.4.0", 31 | "commander": "^9.4.1", 32 | "compression": "^1.7.4", 33 | "cookie-parser": "^1.4.6", 34 | "express": "^4.18.2", 35 | "http": "^0.0.1-security", 36 | "lodash": "^4.17.21", 37 | "mocha": "^10.1.0", 38 | "moment": "^2.30.1", 39 | "nodemailer": "^6.8.0", 40 | "numbro": "^2.3.6", 41 | "p-queue": "^7.3.0", 42 | "patch-package": "^6.5.0", 43 | "percent": "^2.2.0", 44 | "queue": "^6.0.2", 45 | "queue-promise": "^2.2.1", 46 | "request": "^2.88.2", 47 | "sendmail": "^1.6.1", 48 | "talib": "^1.1.4", 49 | "technicalindicators": "^3.1.0", 50 | "telegraf": "^4.10.0", 51 | "ts-node": "^10.9.1", 52 | "tulind": "^0.8.20", 53 | "twig": "^1.17.1", 54 | "typescript": "^5.5.4", 55 | "winston": "^3.8.2", 56 | "winston-telegram": "^2.6.0", 57 | "ws": "^8.18.0", 58 | "zero-fill": "^2.2.4" 59 | }, 60 | "engines": { 61 | "node": ">=20.0" 62 | }, 63 | "homepage": "https://github.com/coinbase/coinbase-pro-node", 64 | "keywords": [ 65 | "cryptocurrency", 66 | "crypto", 67 | "exchange", 68 | "trading", 69 | "trading-bot", 70 | "trading-strategies", 71 | "websocket", 72 | "nodejs", 73 | "bitfinex", 74 | "bitmex", 75 | "binance", 76 | "tradingview", 77 | "bot", 78 | "javascript" 79 | ], 80 | "license": "MIT", 81 | "main": "index.js", 82 | "repository": { 83 | "type": "git", 84 | "url": "https://github.com/Haehnchen/crypto-trading-bot.git" 85 | }, 86 | "scripts": { 87 | "start": "node index.js trade", 88 | "postinstall": "patch-package", 89 | "test": "mocha 'test/**/*.test.js'", 90 | "setup": "pm2 deploy ecosystem.config.js production setup", 91 | "deploy": "pm2 deploy ecosystem.config.js production" 92 | }, 93 | "devDependencies": { 94 | "eslint": "^8.27.0", 95 | "eslint-config-airbnb": "^19.0.4", 96 | "eslint-config-node": "^4.1.0", 97 | "eslint-config-prettier": "^8.5.0", 98 | "eslint-plugin-import": "^2.26.0", 99 | "eslint-plugin-jsx-a11y": "^6.6.1", 100 | "eslint-plugin-node": "^11.1.0", 101 | "eslint-plugin-prettier": "^4.2.1", 102 | "eslint-plugin-promise": "^6.1.1", 103 | "eslint-plugin-react": "^7.31.10", 104 | "prettier": "^2.7.1" 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /patches/bitmex-realtime-api+0.4.3.patch: -------------------------------------------------------------------------------- 1 | patch-package 2 | --- a/node_modules/bitmex-realtime-api/lib/deltaParser.js 3 | +++ b/node_modules/bitmex-realtime-api/lib/deltaParser.js 4 | @@ -33,6 +33,16 @@ module.exports = { 5 | * @return {Array} Updated data. 6 | */ 7 | onAction(action, tableName, symbol, client, data) { 8 | + // temp fix for new orders when none existed previously 9 | + if ( 10 | + tableName === "order" && 11 | + action === "insert" && 12 | + !isInitialized(tableName, symbol, client) 13 | + ) { 14 | + data.keys = ["orderID"]; 15 | + return this._partial(tableName, symbol, client, data); 16 | + } 17 | + 18 | // Deltas before the getSymbol() call returns can be safely discarded. 19 | if (action !== 'partial' && !isInitialized(tableName, symbol, client)) return []; 20 | // Partials initialize the table, so there's a different signature. 21 | -------------------------------------------------------------------------------- /src/command/backfill.js: -------------------------------------------------------------------------------- 1 | const services = require('../modules/services'); 2 | 3 | module.exports = class BackfillCommand { 4 | constructor() {} 5 | 6 | async execute(exchangeName, symbol, period, date) { 7 | await services.getBackfill().backfill(exchangeName, symbol, period, date); 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /src/command/server.js: -------------------------------------------------------------------------------- 1 | const services = require('../modules/services'); 2 | 3 | module.exports = class ServerCommand { 4 | constructor() {} 5 | 6 | execute() { 7 | services.createWebserverInstance().start(); 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /src/command/trade.js: -------------------------------------------------------------------------------- 1 | const services = require('../modules/services'); 2 | 3 | module.exports = class TradeCommand { 4 | constructor() {} 5 | 6 | execute() { 7 | services.createTradeInstance().start(); 8 | services.createWebserverInstance().start(); 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /src/dict/candlestick.js: -------------------------------------------------------------------------------- 1 | module.exports = class Candlestick { 2 | constructor(time, open, high, low, close, volume) { 3 | this.time = time; 4 | this.open = open; 5 | this.high = high; 6 | this.low = low; 7 | this.close = close; 8 | this.volume = volume; 9 | } 10 | 11 | getArray() { 12 | return { 13 | time: this.time, 14 | open: this.open, 15 | high: this.high, 16 | low: this.low, 17 | close: this.close, 18 | volume: this.volume 19 | }; 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /src/dict/exchange_candlestick.js: -------------------------------------------------------------------------------- 1 | module.exports = class ExchangeCandlestick { 2 | constructor(exchange, symbol, period, time, open, high, low, close, volume) { 3 | if (!['m', 'h', 'd', 'y'].includes(period.slice(-1))) { 4 | throw `Invalid candlestick period: ${period} - ${JSON.stringify(Object.values(arguments))}`; 5 | } 6 | 7 | // simple time validation 8 | time = parseInt(time); 9 | if (time <= 631148400) { 10 | throw `Invalid candlestick time given: ${time} - ${JSON.stringify(Object.values(arguments))}`; 11 | } 12 | 13 | this.exchange = exchange; 14 | this.period = period; 15 | this.symbol = symbol; 16 | this.time = time; 17 | this.open = open; 18 | this.high = high; 19 | this.low = low; 20 | this.close = close; 21 | this.volume = volume; 22 | } 23 | 24 | static createFromCandle(exchange, symbol, period, candle) { 25 | return new ExchangeCandlestick( 26 | exchange, 27 | symbol, 28 | period, 29 | candle.time, 30 | candle.open, 31 | candle.high, 32 | candle.low, 33 | candle.close, 34 | candle.volume 35 | ); 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /src/dict/exchange_position.js: -------------------------------------------------------------------------------- 1 | const Position = require('./position'); 2 | 3 | module.exports = class ExchangePosition { 4 | constructor(exchange, position) { 5 | if (!(position instanceof Position)) { 6 | throw new Error(`TypeError: invalid position`); 7 | } 8 | 9 | this._exchange = exchange; 10 | this._position = position; 11 | } 12 | 13 | getKey() { 14 | return this._exchange + this._position.symbol; 15 | } 16 | 17 | getExchange() { 18 | return this._exchange; 19 | } 20 | 21 | getPosition() { 22 | return this._position; 23 | } 24 | 25 | getSymbol() { 26 | return this._position.getSymbol(); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /src/dict/order_capital.js: -------------------------------------------------------------------------------- 1 | module.exports = class OrderCapital { 2 | static get ASSET() { 3 | return 'asset'; 4 | } 5 | 6 | static get CURRENCY() { 7 | return 'currency'; 8 | } 9 | 10 | static get BALANCE() { 11 | return 'balance'; 12 | } 13 | 14 | static createAsset(asset) { 15 | const capital = new OrderCapital(); 16 | capital.type = OrderCapital.ASSET; 17 | capital.asset = asset; 18 | return capital; 19 | } 20 | 21 | static createCurrency(currency) { 22 | const capital = new OrderCapital(); 23 | capital.type = OrderCapital.CURRENCY; 24 | capital.currency = currency; 25 | return capital; 26 | } 27 | 28 | static createBalance(balance) { 29 | const capital = new OrderCapital(); 30 | capital.type = OrderCapital.BALANCE; 31 | capital.balance = balance; 32 | return capital; 33 | } 34 | 35 | getAmount() { 36 | if (this.type === OrderCapital.CURRENCY) { 37 | return this.getCurrency(); 38 | } 39 | 40 | if (this.type === OrderCapital.ASSET) { 41 | return this.getAsset(); 42 | } 43 | 44 | if (this.type === OrderCapital.BALANCE) { 45 | return this.getBalance(); 46 | } 47 | 48 | throw new Error(`Invalid capital type:${this.type}`); 49 | } 50 | 51 | getAsset() { 52 | return this.asset; 53 | } 54 | 55 | getCurrency() { 56 | return this.currency; 57 | } 58 | 59 | getBalance() { 60 | return this.balance; 61 | } 62 | }; 63 | -------------------------------------------------------------------------------- /src/dict/orderbook.js: -------------------------------------------------------------------------------- 1 | module.exports = class Orderbook { 2 | constructor(asks, bids) { 3 | this.asks = asks; 4 | this.bids = bids; 5 | } 6 | }; 7 | -------------------------------------------------------------------------------- /src/dict/period.js: -------------------------------------------------------------------------------- 1 | module.exports = class Ticker { 2 | constructor(lookbacks) { 3 | this.time = lookbacks; 4 | } 5 | }; 6 | -------------------------------------------------------------------------------- /src/dict/position.js: -------------------------------------------------------------------------------- 1 | module.exports = class Position { 2 | static get SIDE_LONG() { 3 | return 'long'; 4 | } 5 | 6 | static get SIDE_SHORT() { 7 | return 'short'; 8 | } 9 | 10 | /** 11 | * @param symbol 'BTCUSD' 12 | * @param side "long" or "short" 13 | * @param amount negative for short and positive for long entries 14 | * @param profit Current profit in percent: "23.56" 15 | * @param updatedAt Item last found or sync 16 | * @param entry The entry price 17 | * @param createdAt 18 | * @param raw 19 | */ 20 | constructor(symbol, side, amount, profit, updatedAt, entry, createdAt, raw = undefined) { 21 | if (![Position.SIDE_LONG, Position.SIDE_SHORT].includes(side)) { 22 | throw new Error(`Invalid position direction given:${side}`); 23 | } 24 | 25 | if (amount < 0 && side === Position.SIDE_LONG) { 26 | throw new Error(`Invalid direction amount:${side}`); 27 | } 28 | 29 | if (amount > 0 && side === Position.SIDE_SHORT) { 30 | throw new Error(`Invalid direction amount:${side}`); 31 | } 32 | 33 | this.symbol = symbol; 34 | this.side = side; 35 | this.amount = amount; 36 | this.profit = profit; 37 | this.updatedAt = updatedAt; 38 | this.entry = entry; 39 | this.createdAt = createdAt; 40 | this.raw = raw; 41 | } 42 | 43 | getSide() { 44 | return this.side; 45 | } 46 | 47 | isShort() { 48 | return this.getSide() === Position.SIDE_SHORT; 49 | } 50 | 51 | isLong() { 52 | return this.getSide() === Position.SIDE_LONG; 53 | } 54 | 55 | getAmount() { 56 | return this.amount; 57 | } 58 | 59 | getSymbol() { 60 | return this.symbol; 61 | } 62 | 63 | getProfit() { 64 | return this.profit; 65 | } 66 | 67 | getEntry() { 68 | return this.entry; 69 | } 70 | 71 | getCreatedAt() { 72 | return this.createdAt; 73 | } 74 | 75 | getUpdatedAt() { 76 | return this.updatedAt; 77 | } 78 | 79 | /** 80 | * For position based exchanges 81 | * 82 | * @returns {array} 83 | */ 84 | getRaw() { 85 | return this.raw; 86 | } 87 | 88 | static create(symbol, amount, updatedAt, createdAt, entry, profit, raw = undefined) { 89 | return new Position(symbol, amount < 0 ? 'short' : 'long', amount, profit, updatedAt, entry, createdAt, raw); 90 | } 91 | 92 | static createProfitUpdate(position, profit) { 93 | return new Position( 94 | position.symbol, 95 | position.side, 96 | position.amount, 97 | profit, 98 | position.updatedAt, 99 | position.entry, 100 | position.createdAt, 101 | position.raw 102 | ); 103 | } 104 | }; 105 | -------------------------------------------------------------------------------- /src/dict/signal.js: -------------------------------------------------------------------------------- 1 | module.exports = class Signal { 2 | constructor(id, exchange, symbol, side, income_at) { 3 | this.id = id; 4 | this.exchange = exchange; 5 | this.symbol = symbol; 6 | this.side = side; 7 | this.income_at = income_at; 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /src/dict/strategy/single_target.js: -------------------------------------------------------------------------------- 1 | module.exports = class SingleTarget { 2 | constructor(target) { 3 | this.target = target; 4 | } 5 | }; 6 | -------------------------------------------------------------------------------- /src/dict/strategy/stop_loss.js: -------------------------------------------------------------------------------- 1 | module.exports = class StopLoss { 2 | constructor(target) { 3 | this.target = target; 4 | } 5 | }; 6 | -------------------------------------------------------------------------------- /src/dict/strategy_context.js: -------------------------------------------------------------------------------- 1 | module.exports = class StrategyContext { 2 | constructor(options, ticker, isBacktest) { 3 | this.bid = ticker.bid; 4 | this.ask = ticker.ask; 5 | 6 | this.options = options; 7 | 8 | this.lastSignal = undefined; 9 | this.amount = undefined; 10 | this.entry = undefined; 11 | this.profit = undefined; 12 | 13 | this.backtest = isBacktest; 14 | } 15 | 16 | static createFromPosition(options, ticker, position, isBacktest = false) { 17 | const context = new StrategyContext(options, ticker, isBacktest); 18 | 19 | context.amount = position.getAmount(); 20 | context.lastSignal = position.getSide(); 21 | context.entry = position.getEntry(); 22 | context.profit = position.getProfit(); 23 | 24 | return context; 25 | } 26 | 27 | getAmount() { 28 | return this.amount; 29 | } 30 | 31 | getLastSignal() { 32 | return this.lastSignal; 33 | } 34 | 35 | getEntry() { 36 | return this.entry; 37 | } 38 | 39 | getProfit() { 40 | return this.profit; 41 | } 42 | 43 | /** 44 | * @returns {any} 45 | */ 46 | getOptions() { 47 | return this.options; 48 | } 49 | 50 | /** 51 | * @returns boolean 52 | */ 53 | isBacktest() { 54 | return this.backtest; 55 | } 56 | 57 | static create(options, ticker, isBacktest) { 58 | return new StrategyContext(options, ticker, isBacktest); 59 | } 60 | }; 61 | -------------------------------------------------------------------------------- /src/dict/ticker.js: -------------------------------------------------------------------------------- 1 | module.exports = class Ticker { 2 | constructor(exchange, symbol, time, bid, ask) { 3 | if (bid <= 0 || ask <= 0 || time <= 0 || !exchange || !symbol) { 4 | throw new Error(`Invalid Ticker bid/ask/time ${exchange} ${symbol}`); 5 | } 6 | 7 | this.exchange = exchange; 8 | this.symbol = symbol; 9 | this.time = time; 10 | this.bid = bid; 11 | this.ask = ask; 12 | this.createdAt = new Date(); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /src/event/candlestick_event.js: -------------------------------------------------------------------------------- 1 | module.exports = class CandlestickEvent { 2 | constructor(exchange, symbol, period, candles) { 3 | this.exchange = exchange; 4 | this.symbol = symbol; 5 | this.period = period; 6 | this.candles = candles; 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /src/event/exchange_order_event.js: -------------------------------------------------------------------------------- 1 | module.exports = class ExchangeOrderEvent { 2 | constructor(exchange, order) { 3 | this.exchange = exchange; 4 | this.order = order; 5 | } 6 | }; 7 | -------------------------------------------------------------------------------- /src/event/exchange_orders_event.js: -------------------------------------------------------------------------------- 1 | module.exports = class ExchangeOrdersEvent { 2 | constructor(exchange, orders) { 3 | this.exchange = exchange; 4 | this.orders = orders; 5 | } 6 | }; 7 | -------------------------------------------------------------------------------- /src/event/order_event.js: -------------------------------------------------------------------------------- 1 | module.exports = class OrderEvent { 2 | constructor(exchange, order) { 3 | this.exchange = exchange; 4 | this.order = order; 5 | } 6 | }; 7 | -------------------------------------------------------------------------------- /src/event/orderbook_event.js: -------------------------------------------------------------------------------- 1 | module.exports = class OrderbookEvent { 2 | constructor(exchange, symbol, orderbook) { 3 | this.exchange = exchange; 4 | this.symbol = symbol; 5 | this.orderbook = orderbook; 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /src/event/position_state_change_event.js: -------------------------------------------------------------------------------- 1 | const ExchangePosition = require('../dict/exchange_position'); 2 | 3 | module.exports = class PositionStateChangeEvent { 4 | static get EVENT_NAME() { 5 | return 'position_state_changed'; 6 | } 7 | 8 | constructor(state, exchangePosition) { 9 | if (!(exchangePosition instanceof ExchangePosition)) { 10 | throw 'TypeError: invalid exchangePosition'; 11 | } 12 | 13 | if (!['opened', 'closed'].includes(state)) { 14 | throw `TypeError: invalid state: ${state}`; 15 | } 16 | 17 | this._state = state; 18 | this._exchangePosition = exchangePosition; 19 | } 20 | 21 | isOpened() { 22 | return this._state === 'opened'; 23 | } 24 | 25 | isClosed() { 26 | return this._state === 'closed'; 27 | } 28 | 29 | getExchange() { 30 | return this._exchangePosition.getExchange(); 31 | } 32 | 33 | getPosition() { 34 | return this._exchangePosition.getPosition(); 35 | } 36 | 37 | getSymbol() { 38 | return this._exchangePosition.getSymbol(); 39 | } 40 | 41 | static createOpened(exchangePosition) { 42 | return new PositionStateChangeEvent('opened', exchangePosition); 43 | } 44 | 45 | static createClosed(exchangePosition) { 46 | return new PositionStateChangeEvent('closed', exchangePosition); 47 | } 48 | }; 49 | -------------------------------------------------------------------------------- /src/event/ticker_event.js: -------------------------------------------------------------------------------- 1 | module.exports = class TickerEvent { 2 | constructor(exchange, symbol, ticker) { 3 | this.exchange = exchange; 4 | this.symbol = symbol; 5 | this.ticker = ticker; 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /src/exchange/bitmex_testnet.js: -------------------------------------------------------------------------------- 1 | const Bitmex = require('./bitmex'); 2 | 3 | module.exports = class BitmexTestnet extends Bitmex { 4 | getName() { 5 | return 'bitmex_testnet'; 6 | } 7 | 8 | getBaseUrl() { 9 | return 'https://testnet.bitmex.com'; 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /src/exchange/noop.js: -------------------------------------------------------------------------------- 1 | /** 2 | * An dummy exchange 3 | * 4 | * @type {module.Noop} 5 | */ 6 | module.exports = class Noop { 7 | constructor() {} 8 | 9 | start(config, symbols) {} 10 | 11 | getName() { 12 | return 'noop'; 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /src/exchange/utils/ccxt_util.js: -------------------------------------------------------------------------------- 1 | const ExchangeOrder = require('../../dict/exchange_order'); 2 | 3 | module.exports = class CcxtUtil { 4 | static createExchangeOrders(orders) { 5 | return orders.map(CcxtUtil.createExchangeOrder); 6 | } 7 | 8 | static createExchangeOrder(order) { 9 | let retry = false; 10 | 11 | let status; 12 | const orderStatus = order.status.toLowerCase(); 13 | 14 | if (['new', 'open', 'partiallyfilled', 'pendingnew', 'doneforday', 'stopped'].includes(orderStatus)) { 15 | status = 'open'; 16 | } else if (orderStatus === 'filled') { 17 | status = 'done'; 18 | } else if (orderStatus === 'canceled') { 19 | status = 'canceled'; 20 | } else if (orderStatus === 'rejected' || orderStatus === 'expired') { 21 | status = 'rejected'; 22 | retry = true; 23 | } 24 | 25 | const ordType = order.type.toLowerCase().replace(/[\W_]+/g, ''); 26 | 27 | // secure the value 28 | let orderType; 29 | switch (ordType) { 30 | case 'limit': 31 | orderType = ExchangeOrder.TYPE_LIMIT; 32 | break; 33 | case 'stop': 34 | case 'stopmarket': // currently: binance_futures only 35 | orderType = ExchangeOrder.TYPE_STOP; 36 | break; 37 | case 'stoplimit': 38 | orderType = ExchangeOrder.TYPE_STOP_LIMIT; 39 | break; 40 | case 'market': 41 | orderType = ExchangeOrder.TYPE_MARKET; 42 | break; 43 | case 'trailingstop': 44 | case 'trailingstopmarket': // currently: binance_futures only 45 | orderType = ExchangeOrder.TYPE_TRAILING_STOP; 46 | break; 47 | default: 48 | orderType = ExchangeOrder.TYPE_UNKNOWN; 49 | break; 50 | } 51 | 52 | return new ExchangeOrder( 53 | order.id, 54 | order.symbol, 55 | status, 56 | order.price, 57 | order.amount, 58 | retry, 59 | null, 60 | order.side.toLowerCase() === 'sell' ? 'sell' : 'buy', // secure the value, 61 | orderType, 62 | new Date(), // no date? 63 | new Date(), 64 | JSON.parse(JSON.stringify(order)) 65 | ); 66 | } 67 | 68 | static createPositions(positions) { 69 | return positions.map(position => { 70 | let { unrealisedRoePcnt } = position; 71 | 72 | if (position.leverage && position.leverage > 1) { 73 | unrealisedRoePcnt /= position.leverage; 74 | } 75 | 76 | return new Position( 77 | position.symbol, 78 | position.currentQty < 0 ? 'short' : 'long', 79 | position.currentQty, 80 | parseFloat((unrealisedRoePcnt * 100).toFixed(2)), 81 | new Date(), 82 | position.avgEntryPrice, 83 | new Date(position.openingTimestamp) 84 | ); 85 | }); 86 | } 87 | }; 88 | -------------------------------------------------------------------------------- /src/exchange/utils/order_bag.js: -------------------------------------------------------------------------------- 1 | const ExchangeOrder = require('../../dict/exchange_order'); 2 | 3 | module.exports = class OrderBag { 4 | constructor() { 5 | this.orders = {}; 6 | } 7 | 8 | /** 9 | * Force an order update only if order is "not closed" for any reason already by exchange 10 | * 11 | * @param order 12 | */ 13 | triggerOrder(order) { 14 | if (!(order instanceof ExchangeOrder)) { 15 | throw Error('Invalid order given'); 16 | } 17 | 18 | // dont overwrite state closed order 19 | for (const [key] of Object.entries(this.orders)) { 20 | if (String(order.id) !== String(key)) { 21 | continue; 22 | } 23 | 24 | if ( 25 | [ExchangeOrder.STATUS_DONE, ExchangeOrder.STATUS_CANCELED, ExchangeOrder.STATUS_REJECTED].includes(order.status) 26 | ) { 27 | delete this.orders[order.id]; 28 | } 29 | break; 30 | } 31 | 32 | this.orders[String(order.id)] = order; 33 | } 34 | 35 | getOrders() { 36 | return new Promise(resolve => { 37 | const orders = []; 38 | 39 | for (const key in this.orders) { 40 | if (this.orders[key].status === 'open') { 41 | orders.push(this.orders[key]); 42 | } 43 | } 44 | 45 | resolve(orders); 46 | }); 47 | } 48 | 49 | findOrderById(id) { 50 | return new Promise(async resolve => { 51 | resolve((await this.getOrders()).find(order => order.id === id || order.id == id)); 52 | }); 53 | } 54 | 55 | getOrdersForSymbol(symbol) { 56 | return new Promise(async resolve => { 57 | resolve((await this.getOrders()).filter(order => order.symbol === symbol)); 58 | }); 59 | } 60 | 61 | delete(id) { 62 | delete this.orders[String(id)]; 63 | } 64 | 65 | set(orders) { 66 | const ourOrder = {}; 67 | 68 | orders.forEach(o => { 69 | if (!(o instanceof ExchangeOrder)) { 70 | throw Error('Invalid order given'); 71 | } 72 | 73 | ourOrder[String(o.id)] = o; 74 | }); 75 | 76 | this.orders = ourOrder; 77 | } 78 | 79 | get(id) { 80 | return this.orders[String(id)]; 81 | } 82 | 83 | all() { 84 | return Object.values(this.orders); 85 | } 86 | }; 87 | -------------------------------------------------------------------------------- /src/exchange/utils/trades_util.js: -------------------------------------------------------------------------------- 1 | const moment = require('moment'); 2 | 3 | module.exports = { 4 | findPositionEntryFromTrades: (trades, balance, side) => { 5 | if (trades.length === 0) { 6 | return undefined; 7 | } 8 | 9 | if (!['short', 'long'].includes(side)) { 10 | throw Error(`Invalid entry side: ${side}`); 11 | } 12 | 13 | const result = { 14 | size: 0, 15 | costs: 0 16 | }; 17 | 18 | const sideBlocker = side === 'short' ? 'sell' : 'buy'; 19 | for (const trade of trades) { 20 | // stop if last trade is a sell 21 | if (trade.side !== sideBlocker) { 22 | // stop if order is really old 23 | if (trade.time < new Date(moment().subtract(2, 'days'))) { 24 | break; 25 | } 26 | 27 | continue; 28 | } 29 | 30 | // stop if price out of range window 31 | const number = result.size + parseFloat(trade.size); 32 | if (number > balance * 1.15) { 33 | break; 34 | } 35 | 36 | // stop on old fills 37 | if (result.time) { 38 | const secDiff = Math.abs(new Date(trade.time).getTime() - new Date(result.time).getTime()); 39 | 40 | // out of 7 day range 41 | if (secDiff > 60 * 60 * 24 * 7 * 1000) { 42 | break; 43 | } 44 | } 45 | 46 | result.size += parseFloat(trade.size); 47 | const costs = parseFloat(trade.size) * parseFloat(trade.price) + parseFloat(trade.fee || 0); 48 | 49 | result.costs += costs; 50 | 51 | // first trade wins for open 52 | if (trade.time && !result.time) { 53 | result.time = trade.time; 54 | } 55 | } 56 | 57 | result.average_price = result.costs / result.size; 58 | 59 | if (result.size === 0 || result.costs === 0) { 60 | return undefined; 61 | } 62 | 63 | return result; 64 | } 65 | }; 66 | -------------------------------------------------------------------------------- /src/modules/backfill.js: -------------------------------------------------------------------------------- 1 | const moment = require('moment'); 2 | const _ = require('lodash'); 3 | const ExchangeCandlestick = require('../dict/exchange_candlestick'); 4 | 5 | module.exports = class Backfill { 6 | constructor(exchangesIterator, candleImporter) { 7 | this.exchangesIterator = exchangesIterator; 8 | this.candleImporter = candleImporter; 9 | } 10 | 11 | async backfill(exchangeName, symbol, period, date) { 12 | const exchange = this.exchangesIterator.find(e => e.getName() === exchangeName); 13 | if (!exchange) { 14 | throw `Exchange not found: ${exchangeName}`; 15 | } 16 | 17 | let start = moment().subtract(date, 'days'); 18 | let candles; 19 | 20 | do { 21 | console.log(`Since: ${new Date(start).toISOString()}`); 22 | candles = await exchange.backfill(symbol, period, start); 23 | 24 | const exchangeCandlesticks = candles.map(candle => { 25 | return ExchangeCandlestick.createFromCandle(exchangeName, symbol, period, candle); 26 | }); 27 | 28 | await this.candleImporter.insertCandles(exchangeCandlesticks); 29 | 30 | console.log(`Got: ${candles.length} candles`); 31 | 32 | start = new Date(_.max(candles.map(r => r.time)) * 1000); 33 | } while (candles.length > 10); 34 | 35 | console.log('finish'); 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /src/modules/exchange/exchange_candle_combine.js: -------------------------------------------------------------------------------- 1 | const Candlestick = require('../../dict/candlestick'); 2 | 3 | module.exports = class ExchangeCandleCombine { 4 | constructor(candlestickRepository) { 5 | this.candlestickRepository = candlestickRepository; 6 | } 7 | 8 | async fetchCombinedCandles(mainExchange, symbol, period, exchanges = [], olderThen = undefined) { 9 | return this.combinedCandles( 10 | this.candlestickRepository.getLookbacksForPair(mainExchange, symbol, period, 750, olderThen), 11 | mainExchange, 12 | symbol, 13 | period, 14 | exchanges 15 | ); 16 | } 17 | 18 | async fetchCombinedCandlesSince(mainExchange, symbol, period, exchanges = [], start) { 19 | return this.combinedCandles( 20 | this.candlestickRepository.getLookbacksSince(mainExchange, symbol, period, start), 21 | mainExchange, 22 | symbol, 23 | period, 24 | exchanges 25 | ); 26 | } 27 | 28 | async fetchCandlePeriods(mainExchange, symbol) { 29 | return this.candlestickRepository.getCandlePeriods(mainExchange, symbol); 30 | } 31 | 32 | async combinedCandles(candlesAwait, mainExchange, symbol, period, exchanges = []) { 33 | const currentTime = Math.round(new Date().getTime() / 1000); 34 | 35 | // we filter the current candle, be to able to use it later 36 | const candles = (await candlesAwait).filter(c => c.time <= currentTime); 37 | 38 | const result = { 39 | [mainExchange]: candles 40 | }; 41 | 42 | // no need for overhead 43 | if (exchanges.length === 0 || candles.length === 0) { 44 | return result; 45 | } 46 | 47 | const c = { 48 | [mainExchange]: {} 49 | }; 50 | 51 | candles.forEach(candle => { 52 | c[mainExchange][candle.time] = candle; 53 | }); 54 | 55 | const start = candles[candles.length - 1].time; 56 | 57 | await Promise.all( 58 | exchanges.map(exchange => { 59 | return new Promise(async resolve => { 60 | const candles = {}; 61 | 62 | const databaseCandles = await this.candlestickRepository.getLookbacksSince( 63 | exchange.name, 64 | exchange.symbol, 65 | period, 66 | start 67 | ); 68 | databaseCandles.forEach(c => { 69 | candles[c.time] = c; 70 | }); 71 | 72 | const myCandles = []; 73 | 74 | let timeMatchedOnce = false; 75 | for (const time of Object.keys(c[mainExchange])) { 76 | // time was matched 77 | if (candles[time]) { 78 | myCandles.push(candles[time]); 79 | timeMatchedOnce = true; 80 | continue; 81 | } 82 | 83 | // pipe the close prices from last known candle 84 | const previousCandle = myCandles[myCandles.length - 1]; 85 | 86 | const candle = previousCandle 87 | ? new Candlestick( 88 | parseInt(time), 89 | previousCandle.close, 90 | previousCandle.close, 91 | previousCandle.close, 92 | previousCandle.close, 93 | 0 94 | ) 95 | : new Candlestick(parseInt(time)); 96 | 97 | myCandles.push(candle); 98 | } 99 | 100 | if (timeMatchedOnce) { 101 | result[exchange.name + exchange.symbol] = myCandles.reverse(); 102 | } 103 | 104 | resolve(); 105 | }); 106 | }) 107 | ); 108 | 109 | return result; 110 | } 111 | }; 112 | -------------------------------------------------------------------------------- /src/modules/exchange/exchange_manager.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const ExchangePosition = require('../../dict/exchange_position'); 3 | 4 | module.exports = class ExchangeManager { 5 | constructor(exchangesIterator, logger, instances, config) { 6 | this.logger = logger; 7 | this.instances = instances; 8 | this.config = config; 9 | this.exchangesIterator = exchangesIterator; 10 | 11 | this.exchanges = []; 12 | } 13 | 14 | init() { 15 | const exchanges = this.exchangesIterator; 16 | 17 | const symbols = {}; 18 | 19 | exchanges 20 | .map(exchange => exchange.getName()) 21 | .forEach(exchangeName => { 22 | const pairs = this.instances.symbols.filter(symbol => { 23 | return symbol.exchange === exchangeName; 24 | }); 25 | 26 | if (pairs.length === 0) { 27 | return; 28 | } 29 | 30 | symbols[exchangeName] = pairs; 31 | }); 32 | 33 | const activeExchanges = exchanges.filter(exchange => exchange.getName() in symbols); 34 | 35 | activeExchanges.forEach(activeExchange => 36 | activeExchange.start( 37 | _.get(this.config, `exchanges.${activeExchange.getName()}`, {}), 38 | symbols[activeExchange.getName()] 39 | ) 40 | ); 41 | 42 | this.exchanges = activeExchanges; 43 | } 44 | 45 | all() { 46 | return this.exchanges; 47 | } 48 | 49 | get(name) { 50 | return this.exchanges.find(exchange => exchange.getName() === name); 51 | } 52 | 53 | async getPosition(exchangeName, symbol) { 54 | return this.get(exchangeName).getPositionForSymbol(symbol); 55 | } 56 | 57 | async getPositions() { 58 | const positions = []; 59 | 60 | for (const exchange of this.all()) { 61 | const exchangeName = exchange.getName(); 62 | 63 | const exchangePositions = (await exchange.getPositions()).map(pos => new ExchangePosition(exchangeName, pos)); 64 | 65 | positions.push(...exchangePositions); 66 | } 67 | 68 | return positions; 69 | } 70 | 71 | async getOrders(exchangeName, symbol) { 72 | return this.get(exchangeName).getOrdersForSymbol(symbol); 73 | } 74 | 75 | async findOrderById(exchangeName, id) { 76 | return this.get(exchangeName).findOrderById(id); 77 | } 78 | }; 79 | -------------------------------------------------------------------------------- /src/modules/exchange/exchange_position_watcher.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const PositionStateChangeEvent = require('../../event/position_state_change_event'); 3 | 4 | module.exports = class ExchangePositionWatcher { 5 | constructor(exchangeManager, eventEmitter, logger) { 6 | this.exchangeManager = exchangeManager; 7 | this.eventEmitter = eventEmitter; 8 | this.logger = logger; 9 | 10 | this.positions = {}; 11 | this.init = false; 12 | } 13 | 14 | async onPositionStateChangeTick() { 15 | const positions = await this.exchangeManager.getPositions(); 16 | 17 | // first run after start 18 | if (!this.init) { 19 | positions.forEach(position => { 20 | this.positions[position.getKey()] = position; 21 | }); 22 | 23 | this.init = true; 24 | } 25 | 26 | const currentOpen = []; 27 | 28 | for (const position of positions) { 29 | const key = position.getKey(); 30 | currentOpen.push(key); 31 | 32 | if (!(key in this.positions)) { 33 | // new position 34 | this.logger.info(`Position opened:${JSON.stringify([position.getExchange(), position.getSymbol(), position])}`); 35 | this.positions[position.getKey()] = position; 36 | this.eventEmitter.emit(PositionStateChangeEvent.EVENT_NAME, PositionStateChangeEvent.createOpened(position)); 37 | } 38 | } 39 | 40 | for (const [key, position] of Object.entries(this.positions)) { 41 | if (!currentOpen.includes(key)) { 42 | // closed position 43 | this.logger.info(`Position closed:${JSON.stringify([position.getExchange(), position.getSymbol(), position])}`); 44 | 45 | delete this.positions[key]; 46 | this.eventEmitter.emit(PositionStateChangeEvent.EVENT_NAME, PositionStateChangeEvent.createClosed(position)); 47 | } 48 | } 49 | } 50 | }; 51 | -------------------------------------------------------------------------------- /src/modules/listener/create_order_listener.js: -------------------------------------------------------------------------------- 1 | const Candlestick = require('../../dict/candlestick.js'); 2 | const ta = require('../../utils/technical_analysis'); 3 | 4 | module.exports = class CreateOrderListener { 5 | constructor(exchangeManager, logger) { 6 | this.exchangeManager = exchangeManager; 7 | this.logger = logger; 8 | } 9 | 10 | async onCreateOrder(orderEvent) { 11 | this.logger.debug(`Create Order:${JSON.stringify(orderEvent)}`); 12 | 13 | const exchange = this.exchangeManager.get(orderEvent.exchange); 14 | if (!exchange) { 15 | console.log(`order: unknown exchange:${orderEvent.exchange}`); 16 | return; 17 | } 18 | 19 | // filter same direction 20 | const ordersForSymbol = (await exchange.getOrdersForSymbol(orderEvent.order.symbol)).filter(order => { 21 | return order.side === orderEvent.order.side; 22 | }); 23 | 24 | if (ordersForSymbol.length === 0) { 25 | this.triggerOrder(exchange, orderEvent.order); 26 | return; 27 | } 28 | 29 | this.logger.debug(`Info Order update:${JSON.stringify(orderEvent)}`); 30 | 31 | const currentOrder = ordersForSymbol[0]; 32 | 33 | if (currentOrder.side !== orderEvent.order.side) { 34 | console.log('order side change'); 35 | return; 36 | } 37 | 38 | exchange 39 | .updateOrder(currentOrder.id, orderEvent.order) 40 | .then(order => { 41 | console.log(`OderUpdate:${JSON.stringify(order)}`); 42 | }) 43 | .catch(() => { 44 | console.log('order update error'); 45 | }); 46 | } 47 | 48 | triggerOrder(exchange, order, retry = 0) { 49 | if (retry > 3) { 50 | console.log(`Retry limit stop creating order: ${JSON.stringify(order)}`); 51 | return; 52 | } 53 | 54 | if (retry > 0) { 55 | console.log(`Retry (${retry}) creating order: ${JSON.stringify(order)}`); 56 | } 57 | 58 | exchange 59 | .order(order) 60 | .then(order => { 61 | if (order.status === 'rejected') { 62 | setTimeout(() => { 63 | console.log(`Order rejected: ${JSON.stringify(order)}`); 64 | this.triggerOrder(exchange, order, retry + 1); 65 | }, 1500); 66 | 67 | return; 68 | } 69 | 70 | console.log(`Order created: ${JSON.stringify(order)}`); 71 | }) 72 | .catch(e => { 73 | console.log(e); 74 | console.log(`Order create error: ${JSON.stringify(e)} - ${JSON.stringify(order)}`); 75 | }); 76 | } 77 | }; 78 | -------------------------------------------------------------------------------- /src/modules/listener/ticker_database_listener.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | 3 | module.exports = class TickerDatabaseListener { 4 | constructor(tickerRepository) { 5 | this.trottle = {}; 6 | 7 | setInterval(async () => { 8 | const tickers = Object.values(this.trottle); 9 | this.trottle = {}; 10 | 11 | if (tickers.length > 0) { 12 | for (const chunk of _.chunk(tickers, 100)) { 13 | await tickerRepository.insertTickers(chunk); 14 | } 15 | } 16 | }, 1000 * 15); 17 | } 18 | 19 | onTicker(tickerEvent) { 20 | const { ticker } = tickerEvent; 21 | this.trottle[ticker.symbol + ticker.exchange] = ticker; 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /src/modules/order/order_calculator.js: -------------------------------------------------------------------------------- 1 | module.exports = class OrderCalculator { 2 | /** 3 | * @param tickers {Tickers} 4 | * @param logger {Logger} 5 | * @param exchangeManager {ExchangeManager} 6 | * @param pairConfig {PairConfig} 7 | */ 8 | constructor(tickers, logger, exchangeManager, pairConfig) { 9 | this.tickers = tickers; 10 | this.logger = logger; 11 | this.exchangeManager = exchangeManager; 12 | this.pairConfig = pairConfig; 13 | } 14 | 15 | /** 16 | * @param exchangeName String 17 | * @param symbol String 18 | * @param capital {OrderCapital} 19 | * @returns {Promise} 20 | */ 21 | async calculateOrderSizeCapital(exchangeName, symbol, capital) { 22 | const balancePercent = capital.getBalance(); 23 | const exchange = this.exchangeManager.get(exchangeName); 24 | 25 | let amountAsset = capital.getAsset(); 26 | let amountCurrency = balancePercent 27 | ? (exchange.getTradableBalance() * balancePercent) / 100 28 | : capital.getCurrency(); 29 | 30 | if (!amountAsset && !amountCurrency) { 31 | throw new Error(`Invalid capital`); 32 | } 33 | if (!amountAsset) { 34 | amountAsset = await this.convertCurrencyToAsset(exchangeName, symbol, amountCurrency); 35 | } 36 | if (!amountCurrency) { 37 | amountCurrency = await this.convertAssetToCurrency(exchangeName, symbol, amountAsset); 38 | } 39 | return exchange.calculateAmount(exchange.isInverseSymbol(symbol) ? amountCurrency : amountAsset, symbol); 40 | } 41 | 42 | async calculateOrderSize(exchangeName, symbol) { 43 | const capital = this.pairConfig.getSymbolCapital(exchangeName, symbol); 44 | if (!capital) { 45 | this.logger.error(`No capital: ${JSON.stringify([exchangeName, symbol, capital])}`); 46 | return undefined; 47 | } 48 | 49 | return this.calculateOrderSizeCapital(exchangeName, symbol, capital); 50 | } 51 | 52 | /** 53 | * If you want to trade with 0.25 BTC this calculated the asset amount which are available to buy 54 | * 55 | * @param exchangeName 56 | * @param symbol 57 | * @param currencyAmount 58 | * @returns {Promise} 59 | */ 60 | async convertCurrencyToAsset(exchangeName, symbol, currencyAmount) { 61 | const ticker = this.tickers.get(exchangeName, symbol); 62 | if (!ticker || !ticker.bid) { 63 | this.logger.error( 64 | `Invalid ticker for calculate currency capital:${JSON.stringify([exchangeName, symbol, currencyAmount])}` 65 | ); 66 | return undefined; 67 | } 68 | 69 | return currencyAmount / ticker.bid; 70 | } 71 | 72 | /** 73 | * If you want to trade with 0.25 BTC this calculated the asset amount which are available to buy 74 | * 75 | * @param exchangeName 76 | * @param symbol 77 | * @param currencyAmount 78 | * @returns {Promise} 79 | */ 80 | async convertAssetToCurrency(exchangeName, symbol, currencyAmount) { 81 | const ticker = this.tickers.get(exchangeName, symbol); 82 | if (!ticker || !ticker.bid) { 83 | this.logger.error( 84 | `Invalid ticker for calculate currency capital:${JSON.stringify([exchangeName, symbol, currencyAmount])}` 85 | ); 86 | return undefined; 87 | } 88 | 89 | return ticker.bid * currencyAmount; 90 | } 91 | }; 92 | -------------------------------------------------------------------------------- /src/modules/order/stop_loss_calculator.js: -------------------------------------------------------------------------------- 1 | module.exports = class StopLossCalculator { 2 | constructor(tickers, logger) { 3 | this.tickers = tickers; 4 | this.logger = logger; 5 | } 6 | 7 | async calculateForOpenPosition(exchange, position, options = { percent: 3 }) { 8 | const { tickers } = this; 9 | 10 | return new Promise(resolve => { 11 | if (!position.entry) { 12 | this.logger.info(`Invalid position entry for stop loss:${JSON.stringify(position)}`); 13 | resolve(); 14 | 15 | return; 16 | } 17 | 18 | let price; 19 | if (position.side === 'long') { 20 | if (options.percent) { 21 | price = position.entry * (1 - options.percent / 100); 22 | } 23 | } else if (options.percent) { 24 | price = position.entry * (1 + options.percent / 100); 25 | } 26 | 27 | // invalid price no value 28 | if (!price) { 29 | this.logger.info(`Empty price for stop loss:${JSON.stringify(position)}`); 30 | 31 | return resolve(); 32 | } 33 | 34 | const ticker = tickers.get(exchange, position.symbol); 35 | 36 | if (!ticker) { 37 | this.logger.info(`Ticker not found for stop loss:${JSON.stringify(position)}`); 38 | 39 | resolve(); 40 | return; 41 | } 42 | 43 | if (position.side === 'long') { 44 | if (price > ticker.ask) { 45 | this.logger.info( 46 | `Ticker out of range stop loss (long): ${JSON.stringify(position)}${JSON.stringify(ticker)}` 47 | ); 48 | 49 | resolve(); 50 | return; 51 | } 52 | } else if (position.side === 'short') { 53 | if (price < ticker.bid) { 54 | this.logger.info( 55 | `Ticker out of range stop loss (short): ${JSON.stringify(position)}${JSON.stringify(ticker)}` 56 | ); 57 | 58 | resolve(); 59 | return; 60 | } 61 | } 62 | 63 | // inverse price for lose long position via sell 64 | if (position.side === 'long') { 65 | price *= -1; 66 | } 67 | 68 | resolve(price); 69 | }); 70 | } 71 | }; 72 | -------------------------------------------------------------------------------- /src/modules/orders/orders_http.js: -------------------------------------------------------------------------------- 1 | const Order = require('../../dict/order'); 2 | 3 | module.exports = class OrdersHttp { 4 | constructor(backtest, tickers, orderExecutor, exchangeManager, pairConfig) { 5 | this.backtest = backtest; 6 | this.tickers = tickers; 7 | this.orderExecutor = orderExecutor; 8 | this.exchangeManager = exchangeManager; 9 | this.pairConfig = pairConfig; 10 | } 11 | 12 | getPairs() { 13 | return this.pairConfig.getAllPairNames(); 14 | } 15 | 16 | getOrders(pair) { 17 | const res = pair.split('.'); 18 | return this.exchangeManager.getOrders(res[0], res[1]); 19 | } 20 | 21 | async cancel(pair, id) { 22 | const res = pair.split('.'); 23 | 24 | return this.orderExecutor.cancelOrder(res[0], id); 25 | } 26 | 27 | async cancelAll(pair) { 28 | const res = pair.split('.'); 29 | 30 | const orders = await this.exchangeManager.getOrders(res[0], res[1]); 31 | 32 | for (const order of orders) { 33 | await this.orderExecutor.cancelOrder(res[0], order.id); 34 | } 35 | } 36 | 37 | getTicker(pair) { 38 | const res = pair.split('.'); 39 | return this.tickers.get(res[0], res[1]); 40 | } 41 | 42 | async createOrder(pair, order) { 43 | const res = pair.split('.'); 44 | 45 | const exchangeInstance = this.exchangeManager.get(res[0]); 46 | 47 | let orderAmount = parseFloat(order.amount); 48 | 49 | // support inverse contracts 50 | if (exchangeInstance.isInverseSymbol(res[1])) { 51 | orderAmount = parseFloat(order.amount_currency); 52 | } 53 | 54 | const amount = exchangeInstance.calculateAmount(orderAmount, res[1]); 55 | if (amount) { 56 | orderAmount = parseFloat(amount); 57 | } 58 | 59 | let orderPrice = parseFloat(order.price); 60 | const price = exchangeInstance.calculatePrice(orderPrice, res[1]); 61 | if (price) { 62 | orderPrice = parseFloat(price); 63 | } 64 | 65 | let ourOrder; 66 | if (order.type && order.type === 'stop') { 67 | ourOrder = Order.createStopOrder(res[1], order.side, orderPrice, orderAmount); 68 | } else { 69 | ourOrder = Order.createLimitPostOnlyOrder(res[1], order.side, orderPrice, orderAmount); 70 | } 71 | 72 | return this.orderExecutor.executeOrder(res[0], ourOrder); 73 | } 74 | }; 75 | -------------------------------------------------------------------------------- /src/modules/pairs/pair_config.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const OrderCapital = require('../../dict/order_capital'); 3 | 4 | module.exports = class PairConfig { 5 | constructor(instances) { 6 | this.instances = instances; 7 | } 8 | 9 | /** 10 | * @param exchangeName string 11 | * @param symbol string 12 | * @returns OrderCapital 13 | */ 14 | getSymbolCapital(exchangeName, symbol) { 15 | const capital = this.instances.symbols.find( 16 | instance => 17 | instance.exchange === exchangeName && instance.symbol === symbol && _.get(instance, 'trade.capital', 0) > 0 18 | ); 19 | 20 | if (capital) { 21 | return OrderCapital.createAsset(capital.trade.capital); 22 | } 23 | 24 | const capitalCurrency = this.instances.symbols.find( 25 | instance => 26 | instance.exchange === exchangeName && 27 | instance.symbol === symbol && 28 | _.get(instance, 'trade.currency_capital', 0) > 0 29 | ); 30 | 31 | if (capitalCurrency) { 32 | return OrderCapital.createCurrency(capitalCurrency.trade.currency_capital); 33 | } 34 | 35 | const balancePercent = this.instances.symbols.find( 36 | instance => 37 | instance.exchange === exchangeName && 38 | instance.symbol === symbol && 39 | _.get(instance, 'trade.balance_percent', 0) > 0 40 | ); 41 | 42 | if (balancePercent) { 43 | return OrderCapital.createBalance(balancePercent.trade.balance_percent); 44 | } 45 | 46 | return undefined; 47 | } 48 | 49 | /** 50 | * Get all instance pairs sorted 51 | * 52 | * @returns string[] 53 | */ 54 | getAllPairNames() { 55 | const pairs = []; 56 | 57 | this.instances.symbols.forEach(symbol => { 58 | pairs.push(`${symbol.exchange}.${symbol.symbol}`); 59 | }); 60 | 61 | return pairs.sort(); 62 | } 63 | }; 64 | -------------------------------------------------------------------------------- /src/modules/pairs/pair_interval.js: -------------------------------------------------------------------------------- 1 | module.exports = class PairInterval { 2 | constructor() { 3 | this.intervals = {}; 4 | } 5 | 6 | /** 7 | * @param name {string} 8 | * @param func {Function} 9 | * @param delay {int} 10 | */ 11 | addInterval(name, delay, func) { 12 | if (name in this.intervals) { 13 | clearInterval(this.intervals[name]); 14 | } 15 | 16 | setTimeout(func, 1); 17 | this.intervals[name] = setInterval(func, delay); 18 | } 19 | 20 | /** 21 | * @param name {string} 22 | */ 23 | clearInterval(name) { 24 | if (name in this.intervals) { 25 | clearInterval(this.intervals[name]); 26 | delete this.intervals[name]; 27 | } 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /src/modules/pairs/pairs_http.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | 3 | module.exports = class PairsHttp { 4 | constructor(instances, exchangeManager, pairStateManager, eventEmitter) { 5 | this.instances = instances; 6 | this.exchangeManager = exchangeManager; 7 | this.pairStateManager = pairStateManager; 8 | this.eventEmitter = eventEmitter; 9 | } 10 | 11 | async getTradePairs() { 12 | const pairs = await Promise.all( 13 | this.instances.symbols.map(async symbol => { 14 | const position = await this.exchangeManager.getPosition(symbol.exchange, symbol.symbol); 15 | const state = await this.pairStateManager.get(symbol.exchange, symbol.symbol); 16 | 17 | const strategiesTrade = symbol.trade && symbol.trade.strategies ? symbol.trade.strategies : []; 18 | const strategies = symbol.strategies || []; 19 | 20 | const tradeCapital = _.get(symbol, 'trade.capital', 0); 21 | const tradeCurrencyCapital = _.get(symbol, 'trade.currency_capital', 0); 22 | const tradeBalancePercent = _.get(symbol, 'trade.balance_percent', 0); 23 | 24 | const item = { 25 | exchange: symbol.exchange, 26 | symbol: symbol.symbol, 27 | watchdogs: symbol.watchdogs, 28 | is_trading: 29 | strategiesTrade.length > 0 || tradeCapital > 0 || tradeCurrencyCapital > 0 || tradeBalancePercent > 0, 30 | has_position: position !== undefined, 31 | trade_capital: tradeCapital, 32 | trade_currency_capital: tradeCurrencyCapital, 33 | trade_balance_percent: tradeBalancePercent, 34 | strategies: strategies, 35 | strategies_trade: strategiesTrade, 36 | weight: 0, 37 | strategy_names: [...strategies, ...strategiesTrade].map(s => s.strategy) 38 | }; 39 | 40 | // open position wins over default state 41 | if (item.has_position) { 42 | item.weight += 1; 43 | } 44 | 45 | // processing items must win 46 | if (state && state.state) { 47 | item.process = state.state; 48 | item.weight += 2; 49 | } 50 | 51 | return item; 52 | }) 53 | ); 54 | 55 | return pairs 56 | .sort((a, b) => `${a.exchange}.${a.symbol}`.localeCompare(`${b.exchange}.${b.symbol}`)) 57 | .sort((a, b) => b.weight - a.weight); 58 | } 59 | 60 | async triggerOrder(exchangeName, symbol, action) { 61 | let side = action; 62 | const options = {}; 63 | if (['long_market', 'short_market', 'close_market'].includes(action)) { 64 | options.market = true; 65 | side = side.replace('_market', ''); 66 | } 67 | 68 | this.pairStateManager.update(exchangeName, symbol, side, options); 69 | 70 | this.eventEmitter.emit('tick_ordering'); 71 | } 72 | }; 73 | -------------------------------------------------------------------------------- /src/modules/repository/logs_repository.js: -------------------------------------------------------------------------------- 1 | const moment = require('moment'); 2 | 3 | module.exports = class LogsRepository { 4 | constructor(db) { 5 | this.db = db; 6 | } 7 | 8 | getLatestLogs(excludes = ['debug'], limit = 200) { 9 | return new Promise(resolve => { 10 | let sql = `SELECT * from logs order by created_at DESC LIMIT ${limit}`; 11 | 12 | const parameters = {}; 13 | 14 | if (excludes.length > 0) { 15 | sql = `SELECT * from logs WHERE level NOT IN (${excludes 16 | .map((exclude, index) => `$level_${index}`) 17 | .join(', ')}) order by created_at DESC LIMIT ${limit}`; 18 | 19 | excludes.forEach((exclude, index) => { 20 | parameters[`level_${index}`] = exclude; 21 | }); 22 | } 23 | 24 | const stmt = this.db.prepare(sql); 25 | resolve(stmt.all(parameters)); 26 | }); 27 | } 28 | 29 | getLevels() { 30 | return new Promise(resolve => { 31 | const stmt = this.db.prepare('SELECT level from logs GROUP BY level'); 32 | resolve(stmt.all().map(r => r.level)); 33 | }); 34 | } 35 | 36 | cleanOldLogEntries(days = 7) { 37 | return new Promise(resolve => { 38 | const stmt = this.db.prepare('DELETE FROM logs WHERE created_at < $created_at'); 39 | 40 | stmt.run({ 41 | created_at: moment() 42 | .subtract(days, 'days') 43 | .unix() 44 | }); 45 | 46 | resolve(); 47 | }); 48 | } 49 | }; 50 | -------------------------------------------------------------------------------- /src/modules/repository/signal_repository.js: -------------------------------------------------------------------------------- 1 | module.exports = class SignalRepository { 2 | constructor(db) { 3 | this.db = db; 4 | } 5 | 6 | getSignals(since) { 7 | return new Promise(resolve => { 8 | const stmt = this.db.prepare('SELECT * from signals where income_at > ? order by income_at DESC LIMIT 100'); 9 | resolve(stmt.all(since)); 10 | }); 11 | } 12 | 13 | insertSignal(exchange, symbol, options, side, strategy) { 14 | const stmt = this.db.prepare( 15 | 'INSERT INTO signals(exchange, symbol, options, side, strategy, income_at) VALUES ($exchange, $symbol, $options, $side, $strategy, $income_at)' 16 | ); 17 | 18 | stmt.run({ 19 | exchange: exchange, 20 | symbol: symbol, 21 | options: JSON.stringify(options || {}), 22 | side: side, 23 | strategy: strategy, 24 | income_at: Math.floor(Date.now() / 1000) 25 | }); 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /src/modules/repository/ticker_log_repository.js: -------------------------------------------------------------------------------- 1 | const moment = require('moment'); 2 | 3 | module.exports = class TickerLogRepository { 4 | constructor(db) { 5 | this.db = db; 6 | } 7 | 8 | cleanOldLogEntries(days = 14) { 9 | return new Promise(resolve => { 10 | const stmt = this.db.prepare('DELETE FROM ticker_log WHERE income_at < $income_at'); 11 | 12 | stmt.run({ 13 | income_at: moment() 14 | .subtract(days, 'days') 15 | .unix() 16 | }); 17 | 18 | resolve(); 19 | }); 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /src/modules/repository/ticker_repository.js: -------------------------------------------------------------------------------- 1 | module.exports = class TickerRepository { 2 | constructor(db, logger) { 3 | this.db = db; 4 | this.logger = logger; 5 | } 6 | 7 | insertTickers(tickers) { 8 | return new Promise(resolve => { 9 | const upsert = this.db.prepare( 10 | 'INSERT INTO ticker(exchange, symbol, ask, bid, updated_at) VALUES ($exchange, $symbol, $ask, $bid, $updated_at) ' + 11 | 'ON CONFLICT(exchange, symbol) DO UPDATE SET ask=$ask, bid=$bid, updated_at=$updated_at' 12 | ); 13 | 14 | this.db.transaction(() => { 15 | tickers.forEach(ticker => { 16 | const parameters = { 17 | exchange: ticker.exchange, 18 | symbol: ticker.symbol, 19 | ask: ticker.ask, 20 | bid: ticker.bid, 21 | updated_at: new Date().getTime() 22 | }; 23 | 24 | upsert.run(parameters); 25 | }); 26 | })(); 27 | 28 | resolve(); 29 | }); 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /src/modules/signal/signal_http.js: -------------------------------------------------------------------------------- 1 | module.exports = class SignalHttp { 2 | constructor(signalRepository) { 3 | this.signalRepository = signalRepository; 4 | } 5 | 6 | async getSignals(since) { 7 | return this.signalRepository.getSignals(since); 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /src/modules/signal/signal_logger.js: -------------------------------------------------------------------------------- 1 | module.exports = class SignalLogger { 2 | constructor(signalRepository) { 3 | this.signalRepository = signalRepository; 4 | } 5 | 6 | signal(exchange, symbol, options, side, strategy) { 7 | this.signalRepository.insertSignal(exchange, symbol, options, side, strategy); 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /src/modules/strategy/dict/indicator_builder.js: -------------------------------------------------------------------------------- 1 | module.exports = class IndicatorBuilder { 2 | constructor() { 3 | this.indicators = {}; 4 | } 5 | 6 | add(key, indicator, period, options = {}, source) { 7 | this.indicators[key] = { 8 | indicator: indicator, 9 | key: key, 10 | period: period, 11 | source: source, 12 | options: options 13 | }; 14 | } 15 | 16 | all() { 17 | return Object.keys(this.indicators).map(key => this.indicators[key]); 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /src/modules/strategy/dict/indicator_period.js: -------------------------------------------------------------------------------- 1 | module.exports = class IndicatorPeriod { 2 | constructor(strategyContext, indicators) { 3 | this.strategyContext = strategyContext; 4 | this.indicators = indicators; 5 | } 6 | 7 | getPrice() { 8 | return this.strategyContext.bid; 9 | } 10 | 11 | getLastSignal() { 12 | if (!this.strategyContext || !this.strategyContext.getLastSignal()) { 13 | return undefined; 14 | } 15 | 16 | return this.strategyContext.getLastSignal(); 17 | } 18 | 19 | getProfit() { 20 | return this.strategyContext.getProfit(); 21 | } 22 | 23 | isShort() { 24 | return this.getLastSignal() === 'short'; 25 | } 26 | 27 | isLong() { 28 | return this.getLastSignal() === 'long'; 29 | } 30 | 31 | /** 32 | * Context return for the current strategy, usable to get the previous strategy signals and current positions. 33 | * 34 | * Usable in a strategy by calling indicatorPeriod.getStrategyContext() --> then you can use the result to grab the 35 | * current entry, last signal, etc.. 36 | */ 37 | getStrategyContext() { 38 | return this.strategyContext; 39 | } 40 | 41 | getIndicator(key) { 42 | for (const k in this.indicators) { 43 | if (k === key) { 44 | return this.indicators[k]; 45 | } 46 | } 47 | 48 | return undefined; 49 | } 50 | 51 | /** 52 | * Generate to iterate over item, starting with latest one going to oldest. 53 | * You should "break" the iteration until you found what you needed 54 | * 55 | * @param limit 56 | * @returns {IterableIterator} 57 | */ 58 | *visitLatestIndicators(limit = 200) { 59 | for (let i = 1; i < limit; i++) { 60 | const result = {}; 61 | 62 | for (const key in this.indicators) { 63 | if (!this.indicators[key][this.indicators[key].length - i]) { 64 | continue; 65 | } 66 | 67 | result[key] = this.indicators[key][this.indicators[key].length - i]; 68 | } 69 | 70 | yield result; 71 | } 72 | 73 | return undefined; 74 | } 75 | 76 | /** 77 | * Get all indicator values from current candle 78 | */ 79 | getLatestIndicators() { 80 | const result = {}; 81 | 82 | for (const key in this.indicators) { 83 | result[key] = this.indicators[key][this.indicators[key].length - 1]; 84 | } 85 | 86 | return result; 87 | } 88 | 89 | /** 90 | * Get all indicator values from current candle 91 | */ 92 | getLatestIndicator(key) { 93 | for (const k in this.indicators) { 94 | if (k === key) { 95 | return this.indicators[key][this.indicators[key].length - 1]; 96 | } 97 | } 98 | 99 | return undefined; 100 | } 101 | }; 102 | -------------------------------------------------------------------------------- /src/modules/strategy/dict/signal_result.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | 3 | const Order = require('../../../dict/order'); 4 | 5 | module.exports = class SignalResult { 6 | constructor() { 7 | this._debug = {}; 8 | this._signal = undefined; 9 | this.placeOrders = []; 10 | } 11 | 12 | mergeDebug(debug) { 13 | this._debug = _.merge(this._debug, debug); 14 | } 15 | 16 | setSignal(signal) { 17 | if (!['long', 'short', 'close'].includes(signal)) { 18 | throw `Invalid signal:${signal}`; 19 | } 20 | 21 | this._signal = signal; 22 | } 23 | 24 | addDebug(key, value) { 25 | if (typeof key !== 'string') { 26 | throw 'Invalid key'; 27 | } 28 | 29 | this._debug[key] = value; 30 | } 31 | 32 | getDebug() { 33 | return this._debug; 34 | } 35 | 36 | getSignal() { 37 | return this._signal; 38 | } 39 | 40 | placeBuyOrder(amountCurrency, price) { 41 | this.placeOrders.push({ 42 | side: Order.SIDE_LONG, 43 | amount_currency: amountCurrency, 44 | price: price 45 | }); 46 | } 47 | 48 | /** 49 | * 50 | * @returns {[Order]} 51 | */ 52 | getPlaceOrder() { 53 | return this.placeOrders; 54 | } 55 | 56 | static createSignal(signal, debug = {}) { 57 | const result = new SignalResult(); 58 | 59 | result.setSignal(signal); 60 | result.mergeDebug(debug); 61 | 62 | return result; 63 | } 64 | 65 | static createEmptySignal(debug = {}) { 66 | const result = new SignalResult(); 67 | 68 | result.mergeDebug(debug); 69 | 70 | return result; 71 | } 72 | }; 73 | -------------------------------------------------------------------------------- /src/modules/strategy/strategies/awesome_oscillator_cross_zero.js: -------------------------------------------------------------------------------- 1 | const SignalResult = require('../dict/signal_result'); 2 | 3 | module.exports = class AwesomeOscillatorCrossZero { 4 | getName() { 5 | return 'awesome_oscillator_cross_zero'; 6 | } 7 | 8 | buildIndicator(indicatorBuilder, options) { 9 | if (!options.period) { 10 | throw 'Invalid period'; 11 | } 12 | 13 | indicatorBuilder.add('ao', 'ao', options.period, options); 14 | 15 | indicatorBuilder.add('sma200', 'sma', options.period, { 16 | length: 200 17 | }); 18 | } 19 | 20 | period(indicatorPeriod) { 21 | return this.macd( 22 | indicatorPeriod.getPrice(), 23 | indicatorPeriod.getIndicator('sma200'), 24 | indicatorPeriod.getIndicator('ao'), 25 | indicatorPeriod.getLastSignal() 26 | ); 27 | } 28 | 29 | macd(price, sma200Full, aoFull, lastSignal) { 30 | if (aoFull.length <= 2 || sma200Full.length < 2) { 31 | return; 32 | } 33 | 34 | // remove incomplete candle 35 | const sma200 = sma200Full.slice(0, -1); 36 | const ao = aoFull.slice(0, -1); 37 | 38 | const debug = { 39 | sma200: sma200.slice(-1)[0], 40 | ao: ao.slice(-1)[0], 41 | last_signal: lastSignal 42 | }; 43 | 44 | const before = ao.slice(-2)[0]; 45 | const last = ao.slice(-1)[0]; 46 | 47 | // trend change 48 | if ((lastSignal === 'long' && before > 0 && last < 0) || (lastSignal === 'short' && before < 0 && last > 0)) { 49 | return SignalResult.createSignal('close', debug); 50 | } 51 | 52 | // sma long 53 | const long = price >= sma200.slice(-1)[0]; 54 | 55 | if (long) { 56 | // long 57 | if (before < 0 && last > 0) { 58 | return SignalResult.createSignal('long', debug); 59 | } 60 | } else { 61 | // short 62 | 63 | if (before > 0 && last < 0) { 64 | return SignalResult.createSignal('short', debug); 65 | } 66 | } 67 | 68 | return SignalResult.createEmptySignal(debug); 69 | } 70 | 71 | getOptions() { 72 | return { 73 | period: '15m' 74 | }; 75 | } 76 | }; 77 | -------------------------------------------------------------------------------- /src/modules/strategy/strategies/cci.js: -------------------------------------------------------------------------------- 1 | const SignalResult = require('../dict/signal_result'); 2 | 3 | module.exports = class CCI { 4 | getName() { 5 | return 'cci'; 6 | } 7 | 8 | buildIndicator(indicatorBuilder, options) { 9 | if (!options.period) { 10 | throw 'Invalid period'; 11 | } 12 | 13 | indicatorBuilder.add('cci', 'cci', options.period); 14 | 15 | indicatorBuilder.add('sma200', 'sma', options.period, { 16 | length: 200 17 | }); 18 | 19 | indicatorBuilder.add('ema200', 'ema', options.period, { 20 | length: 200 21 | }); 22 | } 23 | 24 | period(indicatorPeriod) { 25 | return this.cci( 26 | indicatorPeriod.getPrice(), 27 | indicatorPeriod.getIndicator('sma200'), 28 | indicatorPeriod.getIndicator('ema200'), 29 | indicatorPeriod.getIndicator('cci'), 30 | indicatorPeriod.getLastSignal() 31 | ); 32 | } 33 | 34 | async cci(price, sma200Full, ema200Full, cciFull, lastSignal) { 35 | if ( 36 | !cciFull || 37 | !sma200Full || 38 | !ema200Full || 39 | cciFull.length <= 0 || 40 | sma200Full.length < 2 || 41 | ema200Full.length < 2 42 | ) { 43 | return; 44 | } 45 | 46 | // remove incomplete candle 47 | const sma200 = sma200Full.slice(0, -1); 48 | const ema200 = ema200Full.slice(0, -1); 49 | const cci = cciFull.slice(0, -1); 50 | 51 | const debug = { 52 | sma200: sma200.slice(-1)[0], 53 | ema200: ema200.slice(-1)[0], 54 | cci: cci.slice(-1)[0] 55 | }; 56 | 57 | const before = cci.slice(-2)[0]; 58 | const last = cci.slice(-1)[0]; 59 | 60 | // trend change 61 | if ( 62 | (lastSignal === 'long' && before > 100 && last < 100) || 63 | (lastSignal === 'short' && before < -100 && last > -100) 64 | ) { 65 | return SignalResult.createSignal('close', debug); 66 | } 67 | 68 | let long = price >= sma200.slice(-1)[0]; 69 | 70 | // ema long 71 | if (!long) { 72 | long = price >= ema200.slice(-1)[0]; 73 | } 74 | 75 | const count = cci.length - 1; 76 | 77 | if (long) { 78 | // long 79 | 80 | if (before <= -100 && last >= -100) { 81 | let rangeValues = []; 82 | 83 | for (let i = count - 1; i >= 0; i--) { 84 | if (cci[i] >= -100) { 85 | rangeValues = cci.slice(i, count); 86 | break; 87 | } 88 | } 89 | 90 | const min = Math.min(...rangeValues); 91 | if (min <= -200) { 92 | debug._trigger = min; 93 | return SignalResult.createSignal('long', debug); 94 | } 95 | } 96 | } else if (before >= 100 && last <= 100) { 97 | const count = cci.length - 1; 98 | let rangeValues = []; 99 | 100 | for (let i = count - 1; i >= 0; i--) { 101 | if (cci[i] <= 100) { 102 | rangeValues = cci.slice(i, count); 103 | break; 104 | } 105 | } 106 | 107 | const max = Math.max(...rangeValues); 108 | if (max >= 200) { 109 | debug._trigger = max; 110 | return SignalResult.createSignal('short', debug); 111 | } 112 | } 113 | 114 | return SignalResult.createEmptySignal(debug); 115 | } 116 | 117 | getBacktestColumns() { 118 | return [ 119 | { 120 | label: 'cci', 121 | value: 'cci', 122 | type: 'oscillator', 123 | range: [100, -100] 124 | } 125 | ]; 126 | } 127 | 128 | getOptions() { 129 | return { 130 | period: '15m' 131 | }; 132 | } 133 | }; 134 | -------------------------------------------------------------------------------- /src/modules/strategy/strategies/dca_dipper/README.md: -------------------------------------------------------------------------------- 1 | # Dollar-Cost Averaging (DCA) - Dip Investing Strategy 2 | 3 | Try to invest only on a dip with a fixed amount 4 | 5 | Long term strategy only **not sell singals** ;) 6 | 7 | ## TODO 8 | 9 | * provide capital invested control: daily, weekly limits, ... 10 | 11 | ## Entry (Buy) 12 | 13 | Using Bollinger Band and only invest when crossing lower band with a HMA line. 14 | 15 | ## Configuration 16 | 17 | As most exchanges have a minimum order amount of >10 USD you must reduce the signals. 18 | 19 | Default configuration is providing around a signal every 2 days. Which will be a monthly invest of 140 USD. 20 | 21 | * `amount_currency` just buying 12 USD (BTCUSD) for every signal given. 22 | * `percent_below_price` placing the order "0.1%" below the price. 23 | 24 | ```json 25 | { 26 | "period": "15m", 27 | "amount_currency": "12", 28 | "percent_below_price": 0.1, 29 | "hma_period": 9, 30 | "hma_source": "close" 31 | } 32 | ``` 33 | 34 | `hma_period` and `hma_source` can be used to generate more signals. 35 | 36 | ```json 37 | { 38 | "period": "15m", 39 | "amount_currency": "12", 40 | "percent_below_price": 0.1, 41 | "hma_period": 12, 42 | "hma_source": "low" 43 | } 44 | ``` 45 | 46 | ## Backtest 47 | 48 | ![Crypto BTC DCA](doc/crypto_dca_btc.png) 49 | 50 | ![Crypto ETH DCA](doc/crypto_dca_eth.png) 51 | 52 | -------------------------------------------------------------------------------- /src/modules/strategy/strategies/dca_dipper/dca_dipper.js: -------------------------------------------------------------------------------- 1 | const SignalResult = require('../../dict/signal_result'); 2 | 3 | module.exports = class DcaDipper { 4 | getName() { 5 | return 'dca_dipper'; 6 | } 7 | 8 | buildIndicator(indicatorBuilder, options) { 9 | // basic price normalizer 10 | indicatorBuilder.add('hma', 'hma', options.period, { 11 | length: options.hma_period || 9, 12 | source: options.hma_source || 'close' 13 | }); 14 | 15 | indicatorBuilder.add('bb', 'bb', options.period, { 16 | length: options.bb_length || 20, 17 | stddev: options.bb_stddev || 2 18 | }); 19 | } 20 | 21 | period(indicatorPeriod) { 22 | const currentValues = indicatorPeriod.getLatestIndicators(); 23 | 24 | const price = indicatorPeriod.getPrice(); 25 | if (!price) { 26 | throw new Error('No price given'); 27 | } 28 | 29 | const context = indicatorPeriod.getStrategyContext(); 30 | const options = context.getOptions(); 31 | 32 | if (!options.amount_currency) { 33 | throw new Error('No amount_currency given'); 34 | } 35 | 36 | const hma = indicatorPeriod.getIndicator('hma').slice(-2); 37 | const bb = indicatorPeriod.getIndicator('bb').slice(-2); 38 | 39 | if (bb.length < 2 || hma.length < 2) { 40 | return undefined; 41 | } 42 | 43 | let shouldBuy = false; 44 | if (hma[0] > bb[0].lower && hma[1] < bb[1].lower) { 45 | shouldBuy = true; 46 | } 47 | 48 | const emptySignal = SignalResult.createEmptySignal(currentValues); 49 | emptySignal.addDebug('buy', shouldBuy); 50 | 51 | if (shouldBuy) { 52 | // percent below current price 53 | const orderPrice = 54 | options.percent_below_price && options.percent_below_price > 0 55 | ? price * (1 - options.percent_below_price / 100) 56 | : price; 57 | 58 | emptySignal.addDebug('price', orderPrice); 59 | 60 | // give feedback on backtest via chart 61 | if (context.isBacktest()) { 62 | emptySignal.setSignal('long'); 63 | } 64 | 65 | emptySignal.placeBuyOrder(options.amount_currency, orderPrice); 66 | } 67 | 68 | return emptySignal; 69 | } 70 | 71 | getBacktestColumns() { 72 | return [ 73 | { 74 | label: 'buy', 75 | value: row => { 76 | if (row.buy) { 77 | return 'success'; 78 | } 79 | return undefined; 80 | }, 81 | type: 'icon' 82 | }, 83 | { 84 | label: 'price', 85 | value: 'price' 86 | } 87 | ]; 88 | } 89 | 90 | getOptions() { 91 | return { 92 | period: '15m', 93 | amount_currency: '12', 94 | percent_below_price: 0.1, 95 | hma_period: 9, 96 | hma_source: 'close', 97 | bb_length: 20, 98 | bb_stddev: 2 99 | }; 100 | } 101 | }; 102 | -------------------------------------------------------------------------------- /src/modules/strategy/strategies/dca_dipper/doc/crypto_dca_btc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Haehnchen/crypto-trading-bot/a5d92b153c6cbe8770a28c693309640bb30cfea1/src/modules/strategy/strategies/dca_dipper/doc/crypto_dca_btc.png -------------------------------------------------------------------------------- /src/modules/strategy/strategies/dca_dipper/doc/crypto_dca_eth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Haehnchen/crypto-trading-bot/a5d92b153c6cbe8770a28c693309640bb30cfea1/src/modules/strategy/strategies/dca_dipper/doc/crypto_dca_eth.png -------------------------------------------------------------------------------- /src/modules/strategy/strategies/dip_catcher/README.md: -------------------------------------------------------------------------------- 1 | # Dip / Retracement Catcher 2 | 3 | Try to catch a dip or retracement on the higher trend 4 | 5 | ## Trend Indicator 6 | 7 | Ichimoku Cloud "Lead2 Line" given the "long" or "short" trend based on higher timeframe (4x multipler of main period) 8 | 9 | ## Entry / Exit 10 | 11 | HMA (source: candle low) is cross from lower Bollinger for vice versa for opposite signal 12 | 13 | ## Configuration 14 | 15 | Default config is more secure given less signals. 16 | 17 | ```json 18 | { 19 | "period": "15m", 20 | "trend_cloud_multiplier": 4, 21 | "hma_high_period": 9, 22 | "hma_high_candle_source": "close", 23 | "hma_low_period": 9, 24 | "hma_low_candle_source": "close" 25 | } 26 | ``` 27 | 28 | More signals can be generate by change the candle source and higher the signal line 29 | 30 | ```json 31 | { 32 | "period": "15m", 33 | "trend_cloud_multiplier": 4, 34 | "hma_high_period": 12, 35 | "hma_high_candle_source": "high", 36 | "hma_low_period": 12, 37 | "hma_low_candle_source": "low" 38 | } 39 | ``` 40 | 41 | ## Tradingview 42 | 43 | ![Tradingview](doc/dip_catcher_tradingview.png) 44 | 45 | ## Backtest 46 | 47 | ![Backtest](doc/dip_catcher_backtest.png) 48 | 49 | -------------------------------------------------------------------------------- /src/modules/strategy/strategies/dip_catcher/dip_catcher.js: -------------------------------------------------------------------------------- 1 | const SignalResult = require('../../dict/signal_result'); 2 | 3 | module.exports = class DipCatcher { 4 | getName() { 5 | return 'dip_catcher'; 6 | } 7 | 8 | buildIndicator(indicatorBuilder, options) { 9 | // line for short entry or long exit 10 | indicatorBuilder.add('hma_high', 'hma', options.period, { 11 | length: options.hma_high_period || 12, 12 | source: options.hma_high_candle_source || 'high' 13 | }); 14 | 15 | // line for long entry or short exit 16 | indicatorBuilder.add('hma_low', 'hma', options.period, { 17 | length: options.hma_low_period || 12, 18 | source: options.hma_low_candle_source || 'low' 19 | }); 20 | 21 | // basic price normalizer 22 | indicatorBuilder.add('hma', 'hma', options.period, { 23 | length: 9 24 | }); 25 | 26 | // our main direction 27 | const trendCloudMultiplier = options.trend_cloud_multiplier || 4; 28 | indicatorBuilder.add('cloud', 'ichimoku_cloud', options.period, { 29 | conversionPeriod: 9 * trendCloudMultiplier, 30 | basePeriod: 26 * trendCloudMultiplier, 31 | spanPeriod: 52 * trendCloudMultiplier, 32 | displacement: 26 * trendCloudMultiplier 33 | }); 34 | 35 | indicatorBuilder.add('bb', 'bb', '15m'); 36 | } 37 | 38 | period(indicatorPeriod) { 39 | const currentValues = indicatorPeriod.getLatestIndicators(); 40 | 41 | const hma = indicatorPeriod.getIndicator('hma').slice(-2); 42 | const hmaLow = indicatorPeriod.getIndicator('hma_low').slice(-2); 43 | const hmaHigh = indicatorPeriod.getIndicator('hma_high').slice(-2); 44 | const bb = indicatorPeriod.getIndicator('bb').slice(-2); 45 | const cloud = indicatorPeriod.getIndicator('cloud').slice(-1); 46 | 47 | const emptySignal = SignalResult.createEmptySignal(currentValues); 48 | 49 | if (!cloud[0] || !hma[0]) { 50 | return emptySignal; 51 | } 52 | 53 | const lastSignal = indicatorPeriod.getLastSignal(); 54 | 55 | // follow the main trend with entries 56 | const isLong = hma[0] > cloud[0].spanB; 57 | emptySignal.addDebug('trend', isLong); 58 | 59 | if (hmaLow[0] > bb[0].lower && hmaLow[1] < bb[1].lower) { 60 | if (!lastSignal && isLong) { 61 | emptySignal.addDebug('message', 'long_lower_cross'); 62 | 63 | emptySignal.setSignal('long'); 64 | } else if (lastSignal) { 65 | emptySignal.setSignal('close'); 66 | } 67 | } 68 | 69 | if (hmaHigh[0] < bb[0].upper && hmaHigh[1] > bb[1].upper) { 70 | if (!lastSignal && !isLong) { 71 | emptySignal.addDebug('message', 'short_upper_cross'); 72 | 73 | emptySignal.setSignal('short'); 74 | } else if (lastSignal) { 75 | emptySignal.setSignal('close'); 76 | } 77 | } 78 | 79 | return emptySignal; 80 | } 81 | 82 | getBacktestColumns() { 83 | return [ 84 | { 85 | label: 'bb_hma', 86 | value: row => { 87 | if (!row.bb) { 88 | return undefined; 89 | } 90 | 91 | if (row.hma < row.bb.lower) { 92 | return 'success'; 93 | } 94 | 95 | if (row.hma > row.bb.upper) { 96 | return 'danger'; 97 | } 98 | 99 | return undefined; 100 | }, 101 | type: 'icon' 102 | }, 103 | { 104 | label: 'trend', 105 | value: row => { 106 | if (typeof row.trend !== 'boolean') { 107 | return undefined; 108 | } 109 | 110 | return row.trend === true ? 'success' : 'danger'; 111 | }, 112 | type: 'icon' 113 | }, 114 | { 115 | label: 'message', 116 | value: 'message' 117 | } 118 | ]; 119 | } 120 | 121 | getOptions() { 122 | return { 123 | period: '15m', 124 | trend_cloud_multiplier: 4, 125 | hma_high_period: 9, 126 | hma_high_candle_source: 'close', 127 | hma_low_period: 9, 128 | hma_low_candle_source: 'close' 129 | }; 130 | } 131 | }; 132 | -------------------------------------------------------------------------------- /src/modules/strategy/strategies/dip_catcher/doc/dip_catcher_backtest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Haehnchen/crypto-trading-bot/a5d92b153c6cbe8770a28c693309640bb30cfea1/src/modules/strategy/strategies/dip_catcher/doc/dip_catcher_backtest.png -------------------------------------------------------------------------------- /src/modules/strategy/strategies/dip_catcher/doc/dip_catcher_tradingview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Haehnchen/crypto-trading-bot/a5d92b153c6cbe8770a28c693309640bb30cfea1/src/modules/strategy/strategies/dip_catcher/doc/dip_catcher_tradingview.png -------------------------------------------------------------------------------- /src/modules/strategy/strategies/macd.js: -------------------------------------------------------------------------------- 1 | const SignalResult = require('../dict/signal_result'); 2 | 3 | module.exports = class Macd { 4 | getName() { 5 | return 'macd'; 6 | } 7 | 8 | buildIndicator(indicatorBuilder, options) { 9 | if (!options.period) { 10 | throw Error('Invalid period'); 11 | } 12 | 13 | indicatorBuilder.add('macd', 'macd_ext', options.period, options); 14 | 15 | indicatorBuilder.add('hma', 'hma', options.period, { 16 | length: 9 17 | }); 18 | 19 | indicatorBuilder.add('sma200', 'sma', options.period, { 20 | length: 200 21 | }); 22 | } 23 | 24 | period(indicatorPeriod) { 25 | const sma200Full = indicatorPeriod.getIndicator('sma200'); 26 | const macdFull = indicatorPeriod.getIndicator('macd'); 27 | const hmaFull = indicatorPeriod.getIndicator('hma'); 28 | 29 | if (!macdFull || !sma200Full || !hmaFull || macdFull.length < 2 || sma200Full.length < 2) { 30 | return undefined; 31 | } 32 | 33 | const hma = hmaFull.slice(-1)[0]; 34 | const sma200 = sma200Full.slice(-1)[0]; 35 | const macd = macdFull.slice(-2); 36 | 37 | // overall trend filter 38 | const long = hma >= sma200; 39 | 40 | const lastSignal = indicatorPeriod.getLastSignal(); 41 | 42 | const debug = { 43 | sma200: sma200[0], 44 | histogram: macd[0].histogram, 45 | last_signal: lastSignal, 46 | long: long 47 | }; 48 | 49 | const current = macd[0].histogram; 50 | const before = macd[1].histogram; 51 | 52 | // trend change 53 | if ((lastSignal === 'long' && before > 0 && current < 0) || (lastSignal === 'short' && before < 0 && current > 0)) { 54 | return SignalResult.createSignal('close', debug); 55 | } 56 | 57 | if (long) { 58 | // long 59 | if (before < 0 && current > 0) { 60 | return SignalResult.createSignal('long', debug); 61 | } 62 | } else { 63 | // short 64 | 65 | if (before > 0 && current < 0) { 66 | return SignalResult.createSignal('short', debug); 67 | } 68 | } 69 | 70 | return SignalResult.createEmptySignal(debug); 71 | } 72 | 73 | getBacktestColumns() { 74 | return [ 75 | { 76 | label: 'trend', 77 | value: row => { 78 | if (typeof row.long !== 'boolean') { 79 | return undefined; 80 | } 81 | 82 | return row.long === true ? 'success' : 'danger'; 83 | }, 84 | type: 'icon' 85 | }, 86 | { 87 | label: 'histogram', 88 | value: 'histogram', 89 | type: 'histogram' 90 | } 91 | ]; 92 | } 93 | 94 | getOptions() { 95 | return { 96 | period: '15m', 97 | default_ma_type: 'EMA', 98 | fast_period: 12, 99 | slow_period: 26, 100 | signal_period: 9 101 | }; 102 | } 103 | }; 104 | -------------------------------------------------------------------------------- /src/modules/strategy/strategies/obv_pump_dump.js: -------------------------------------------------------------------------------- 1 | const SignalResult = require('../dict/signal_result'); 2 | 3 | module.exports = class { 4 | getName() { 5 | return 'obv_pump_dump'; 6 | } 7 | 8 | buildIndicator(indicatorBuilder, options) { 9 | indicatorBuilder.add('obv', 'obv', '1m'); 10 | 11 | indicatorBuilder.add('ema', 'ema', '1m', { 12 | length: 200 13 | }); 14 | } 15 | 16 | async period(indicatorPeriod, options) { 17 | const triggerMultiplier = options.trigger_multiplier || 2; 18 | const triggerTimeWindows = options.trigger_time_windows || 3; 19 | 20 | const obv = indicatorPeriod.getIndicator('obv'); 21 | 22 | if (!obv || obv.length <= 20) { 23 | return; 24 | } 25 | 26 | const price = indicatorPeriod.getPrice(); 27 | const ema = indicatorPeriod.getIndicator('ema').slice(-1)[0]; 28 | 29 | const debug = { 30 | obv: obv.slice(-1)[0], 31 | ema: ema 32 | }; 33 | 34 | if (price > ema) { 35 | // long 36 | debug.trend = 'up'; 37 | 38 | const before = obv.slice(-20, triggerTimeWindows * -1); 39 | 40 | const highest = before.sort((a, b) => b - a).slice(0, triggerTimeWindows); 41 | const highestOverage = highest.reduce((a, b) => a + b, 0) / highest.length; 42 | 43 | const current = obv.slice(triggerTimeWindows * -1); 44 | 45 | const currentAverage = current.reduce((a, b) => a + b, 0) / current.length; 46 | 47 | debug.highest_overage = highestOverage; 48 | debug.current_average = currentAverage; 49 | 50 | if (currentAverage < highestOverage) { 51 | return SignalResult.createEmptySignal(debug); 52 | } 53 | 54 | const difference = Math.abs(currentAverage / highestOverage); 55 | 56 | debug.difference = difference; 57 | 58 | if (difference >= triggerMultiplier) { 59 | return SignalResult.createSignal('long', debug); 60 | } 61 | } else { 62 | // short 63 | debug.trend = 'down'; 64 | } 65 | 66 | return SignalResult.createEmptySignal(debug); 67 | } 68 | 69 | getOptions() { 70 | return { 71 | period: '15m', 72 | trigger_multiplier: 2, 73 | trigger_time_windows: 3 74 | }; 75 | } 76 | }; 77 | -------------------------------------------------------------------------------- /src/modules/strategy/strategies/parabolicsar.js: -------------------------------------------------------------------------------- 1 | const SignalResult = require('../dict/signal_result'); 2 | 3 | module.exports = class PARABOL { 4 | getName() { 5 | return 'parabol'; 6 | } 7 | 8 | buildIndicator(indicatorBuilder, options) { 9 | if (!options.period) { 10 | throw Error('Invalid period'); 11 | } 12 | 13 | indicatorBuilder.add('psar', 'PSAR', options.period, options); 14 | } 15 | 16 | period(indicatorPeriod) { 17 | const psar = indicatorPeriod.getIndicator('psar'); 18 | const psarVal = psar.result; 19 | const price = indicatorPeriod.getPrice(); 20 | const diff = price - psarVal; 21 | 22 | const lastSignal = indicatorPeriod.getLastSignal(); 23 | 24 | const long = diff > 0; 25 | 26 | const debug = { 27 | psar: psar[0], 28 | histogram: psar[0].histogram, 29 | last_signal: lastSignal, 30 | long: long 31 | }; 32 | 33 | const current = psar[0].histogram; 34 | const before = psar[1].histogram; 35 | 36 | // trend change 37 | if ((lastSignal === 'long' && before > 0 && current < 0) || (lastSignal === 'short' && before < 0 && current > 0)) { 38 | return SignalResult.createSignal('close', debug); 39 | } 40 | 41 | if (long) { 42 | // long 43 | if (before < 0 && current > 0) { 44 | return SignalResult.createSignal('long', debug); 45 | } 46 | } else { 47 | // short 48 | 49 | if (before > 0 && current < 0) { 50 | return SignalResult.createSignal('short', debug); 51 | } 52 | } 53 | 54 | return SignalResult.createEmptySignal(debug); 55 | } 56 | 57 | getBacktestColumns() { 58 | return [ 59 | { 60 | label: 'trend', 61 | value: row => { 62 | if (typeof row.long !== 'boolean') { 63 | return undefined; 64 | } 65 | 66 | return row.long === true ? 'success' : 'danger'; 67 | }, 68 | type: 'icon' 69 | }, 70 | { 71 | label: 'histogram', 72 | value: 'histogram', 73 | type: 'histogram' 74 | } 75 | ]; 76 | } 77 | 78 | getOptions() { 79 | return { 80 | period: '15m' 81 | }; 82 | } 83 | }; 84 | -------------------------------------------------------------------------------- /src/modules/strategy/strategies/trader.js: -------------------------------------------------------------------------------- 1 | const { SD } = require('technicalindicators'); 2 | const { SMA } = require('technicalindicators'); 3 | const { Lowest } = require('technicalindicators'); 4 | const { isTrendingUp } = require('technicalindicators'); 5 | const { isTrendingDown } = require('technicalindicators'); 6 | const SignalResult = require('../dict/signal_result'); 7 | const TA = require('../../../utils/technical_analysis'); 8 | const TechnicalPattern = require('../../../utils/technical_pattern'); 9 | const resample = require('../../../utils/resample'); 10 | const TechnicalAnalysis = require('../../../utils/technical_analysis'); 11 | 12 | module.exports = class { 13 | getName() { 14 | return 'trader'; 15 | } 16 | 17 | buildIndicator(indicatorBuilder, options) { 18 | indicatorBuilder.add('candles_1m', 'candles', '1m'); 19 | indicatorBuilder.add('bb', 'bb', '15m', { 20 | length: 40 21 | }); 22 | } 23 | 24 | async period(indicatorPeriod, options) { 25 | const currentValues = indicatorPeriod.getLatestIndicators(); 26 | 27 | const result = SignalResult.createEmptySignal(currentValues); 28 | 29 | const candles1m = indicatorPeriod.getIndicator('candles_1m'); 30 | if (!candles1m) { 31 | return result; 32 | } 33 | 34 | const candles3m = resample.resampleMinutes(candles1m.slice().reverse(), '3'); 35 | 36 | const foo = TechnicalAnalysis.getPivotPoints( 37 | candles1m.slice(-10).map(c => c.close), 38 | 3, 39 | 3 40 | ); 41 | 42 | const bb = indicatorPeriod.getLatestIndicator('bb'); 43 | 44 | const lastCandle = candles1m.slice(-1)[0]; 45 | result.addDebug('price2', lastCandle.close); 46 | 47 | if (bb && lastCandle && lastCandle.close > bb.upper) { 48 | result.addDebug('v', 'success'); 49 | 50 | const bb = indicatorPeriod.getIndicator('bb'); 51 | 52 | const values = bb 53 | .slice(-10) 54 | .reverse() 55 | .map(b => b.width); 56 | const value = Math.min(...values); 57 | 58 | if (currentValues.bb.width < 0.05) { 59 | result.addDebug('x', currentValues.bb.width); 60 | result.setSignal('long'); 61 | } 62 | } 63 | 64 | result.addDebug('pivot', foo); 65 | 66 | result.mergeDebug(TechnicalPattern.volumePump(candles3m.slice().reverse() || [])); 67 | 68 | return result; 69 | } 70 | 71 | getBacktestColumns() { 72 | return [ 73 | { 74 | label: 'price2', 75 | value: 'price2' 76 | }, 77 | { 78 | label: 'RSI', 79 | value: 'rsi' 80 | }, 81 | { 82 | label: 'roc', 83 | value: 'roc_1m' 84 | }, 85 | { 86 | label: 'roc_ma', 87 | value: 'roc_ma', 88 | type: 'icon' 89 | }, 90 | { 91 | label: 'Vol', 92 | value: 'candles_1m.volume' 93 | }, 94 | { 95 | label: 'VolSd', 96 | value: 'volume_sd' 97 | }, 98 | { 99 | label: 'VolV', 100 | value: 'volume_v' 101 | }, 102 | { 103 | label: 'hint', 104 | value: 'hint', 105 | type: 'icon' 106 | }, 107 | { 108 | label: 'v', 109 | value: 'v', 110 | type: 'icon' 111 | }, 112 | { 113 | label: 'x', 114 | value: 'x' 115 | }, 116 | { 117 | label: 'pivot', 118 | value: 'pivot' 119 | } 120 | ]; 121 | } 122 | 123 | getOptions() { 124 | return { 125 | period: '15m' 126 | }; 127 | } 128 | }; 129 | -------------------------------------------------------------------------------- /src/modules/system/candle_export_http.js: -------------------------------------------------------------------------------- 1 | module.exports = class CandleExportHttp { 2 | constructor(candlestickRepository, pairConfig) { 3 | this.candlestickRepository = candlestickRepository; 4 | this.pairConfig = pairConfig; 5 | } 6 | 7 | async getCandles(exchange, symbol, period, start, end) { 8 | return this.candlestickRepository.getCandlesInWindow(exchange, symbol, period, start, end); 9 | } 10 | 11 | async getPairs() { 12 | return this.pairConfig.getAllPairNames().map(p => { 13 | const split = p.split('.'); 14 | 15 | return { 16 | exchange: split[0], 17 | symbol: split[1] 18 | }; 19 | }); 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /src/modules/system/candle_importer.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | 3 | module.exports = class CandleImporter { 4 | constructor(candlestickRepository) { 5 | this.candlestickRepository = candlestickRepository; 6 | this.trottle = {}; 7 | this.promises = []; 8 | 9 | setInterval(async () => { 10 | const candles = Object.values(this.trottle); 11 | this.trottle = {}; 12 | 13 | const promises = this.promises.slice(); 14 | this.promises = []; 15 | 16 | // on init we can have a lot or REST api we can have a lot of candles 17 | // reduce database locking time by split them 18 | if (candles.length > 0) { 19 | for (const chunk of _.chunk(candles, 1000)) { 20 | await this.insertCandles(chunk); 21 | } 22 | } 23 | 24 | promises.forEach(resolve => { 25 | resolve(); 26 | }); 27 | }, 1000 * 5); 28 | } 29 | 30 | async insertCandles(candles) { 31 | return this.candlestickRepository.insertCandles(candles); 32 | } 33 | 34 | /** 35 | * We have spikes in each exchange on possible every full minute, collect them for a time range the candles and fire them at once 36 | * 37 | * @param candles 38 | * @returns {Promise} 39 | */ 40 | async insertThrottledCandles(candles) { 41 | for (const candle of candles) { 42 | this.trottle[candle.exchange + candle.symbol + candle.period + candle.time] = candle; 43 | } 44 | 45 | const { promise, resolve } = this.getPromise(); 46 | 47 | this.promises.push(resolve); 48 | 49 | return promise; 50 | } 51 | 52 | /** 53 | * @private 54 | */ 55 | getPromise() { 56 | let resolve; 57 | 58 | const promise = new Promise(res => { 59 | resolve = res; 60 | }); 61 | 62 | return { promise, resolve }; 63 | } 64 | }; 65 | -------------------------------------------------------------------------------- /src/modules/system/candlestick_resample.js: -------------------------------------------------------------------------------- 1 | const resample = require('../../utils/resample'); 2 | const ExchangeCandlestick = require('../../dict/exchange_candlestick'); 3 | 4 | module.exports = class CandlestickResample { 5 | constructor(candlestickRepository, candleImporter) { 6 | this.candlestickRepository = candlestickRepository; 7 | this.candleImporter = candleImporter; 8 | } 9 | 10 | /** 11 | * Resample a eg "15m" range to a "1h" 12 | * 13 | * @param exchange The change name to resample 14 | * @param symbol Pair for resample 15 | * @param periodFrom From "5m" must be lower then "periodTo" 16 | * @param periodTo To new candles eg "1h" 17 | * @param limitCandles For mass resample history provide a switch else calculate the candle window on resample periods 18 | * @returns {Promise} 19 | */ 20 | async resample(exchange, symbol, periodFrom, periodTo, limitCandles = false) { 21 | const toMinute = resample.convertPeriodToMinute(periodTo); 22 | const fromMinute = resample.convertPeriodToMinute(periodFrom); 23 | 24 | if (fromMinute > toMinute) { 25 | throw 'Invalid resample "from" must be geater then "to"'; 26 | } 27 | 28 | // we need some 29 | let wantCandlesticks = 750; 30 | 31 | // we can limit the candles in the range we should resample 32 | // but for mass resample history provide a switch 33 | if (limitCandles === true) { 34 | wantCandlesticks = Math.round((toMinute / fromMinute) * 5.6); 35 | } 36 | 37 | const candlestick = await this.candlestickRepository.getLookbacksForPair( 38 | exchange, 39 | symbol, 40 | periodFrom, 41 | wantCandlesticks 42 | ); 43 | if (candlestick.length === 0) { 44 | return; 45 | } 46 | 47 | const resampleCandlesticks = resample.resampleMinutes(candlestick, toMinute); 48 | 49 | const candles = resampleCandlesticks.map(candle => { 50 | return new ExchangeCandlestick( 51 | exchange, 52 | symbol, 53 | periodTo, 54 | candle.time, 55 | candle.open, 56 | candle.high, 57 | candle.low, 58 | candle.close, 59 | candle.volume 60 | ); 61 | }); 62 | 63 | await this.candleImporter.insertThrottledCandles(candles); 64 | } 65 | }; 66 | -------------------------------------------------------------------------------- /src/modules/system/logs_http.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | 3 | module.exports = class LogsHttp { 4 | constructor(logsRepository) { 5 | this.logsRepository = logsRepository; 6 | } 7 | 8 | async getLogsPageVariables(request, response) { 9 | let excludeLevels = request.query.exclude_levels || []; 10 | 11 | if (excludeLevels.length === 0 && !('filters' in request.cookies)) { 12 | excludeLevels = ['debug']; 13 | } 14 | 15 | response.cookie('filters', excludeLevels, { 16 | maxAge: 60 * 60 * 24 * 30 * 1000 17 | }); 18 | 19 | return { 20 | logs: await this.logsRepository.getLatestLogs(excludeLevels), 21 | levels: await this.logsRepository.getLevels(), 22 | form: { 23 | excludeLevels: excludeLevels 24 | } 25 | }; 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /src/modules/system/system_util.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | 3 | module.exports = class SystemUtil { 4 | constructor(config) { 5 | this.config = config; 6 | } 7 | 8 | /** 9 | * Provide the configuration inside "conf.json" with a comma separated access for array structures 10 | * 11 | * @param key eg "webserver.port" nested config supported 12 | * @param defaultValue value if config does not exists 13 | * @returns {*} 14 | */ 15 | getConfig(key, defaultValue = undefined) { 16 | const value = _.get(this.config, key, defaultValue); 17 | 18 | if (value === null) { 19 | return defaultValue; 20 | } 21 | 22 | return value; 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /src/notify/mail.js: -------------------------------------------------------------------------------- 1 | module.exports = class Mail { 2 | constructor(mailer, systemUtil, logger) { 3 | this.mailer = mailer; 4 | this.systemUtil = systemUtil; 5 | this.logger = logger; 6 | } 7 | 8 | send(message) { 9 | const to = this.systemUtil.getConfig('notify.mail.to'); 10 | if (!to) { 11 | this.logger.error('No mail "to" address given'); 12 | 13 | return; 14 | } 15 | 16 | this.mailer.sendMail( 17 | { 18 | to: to, 19 | subject: message, 20 | text: message 21 | }, 22 | err => { 23 | if (err) { 24 | this.logger.error(`Mailer: ${JSON.stringify(err)}`); 25 | } 26 | } 27 | ); 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /src/notify/notify.js: -------------------------------------------------------------------------------- 1 | module.exports = class Notify { 2 | constructor(notifier) { 3 | this.notifier = notifier; 4 | } 5 | 6 | send(message) { 7 | this.notifier.forEach(notify => notify.send(message)); 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /src/notify/slack.js: -------------------------------------------------------------------------------- 1 | const request = require('request'); 2 | 3 | module.exports = class Slack { 4 | constructor(config) { 5 | this.config = config; 6 | } 7 | 8 | send(message) { 9 | const postOptions = { 10 | uri: this.config.webhook, 11 | method: 'POST', 12 | headers: { 13 | 'Content-type': 'application/json' 14 | }, 15 | json: { 16 | text: message, 17 | username: this.config.username || 'crypto-bot', 18 | icon_emoji: this.config.icon_emoji || ':ghost:' 19 | } 20 | }; 21 | request(postOptions, (error, response, body) => { 22 | if (error) { 23 | console.log(error); 24 | } 25 | }); 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /src/notify/telegram.js: -------------------------------------------------------------------------------- 1 | module.exports = class Telegram { 2 | constructor(telegraf, config, logger) { 3 | this.telegraf = telegraf; 4 | this.config = config; 5 | this.logger = logger; 6 | } 7 | 8 | send(message) { 9 | const chatId = this.config.chat_id; 10 | if (!chatId) { 11 | console.log('Telegram: No chat id given'); 12 | this.logger.error('Telegram: No chat id given'); 13 | return; 14 | } 15 | this.telegraf.telegram.sendMessage(chatId, message).catch(err => { 16 | this.logger.error(`Mailer: ${JSON.stringify(err)}`); 17 | console.log(err); 18 | }); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /src/storage/tickers.js: -------------------------------------------------------------------------------- 1 | const moment = require('moment'); 2 | 3 | module.exports = class Tickers { 4 | constructor() { 5 | this.tickers = {}; 6 | } 7 | 8 | set(ticker) { 9 | this.tickers[`${ticker.exchange}.${ticker.symbol}`] = ticker; 10 | } 11 | 12 | get(exchange, symbol) { 13 | return this.tickers[`${exchange}.${symbol}`] || null; 14 | } 15 | 16 | getIfUpToDate(exchange, symbol, lastUpdatedSinceMs) { 17 | if (!lastUpdatedSinceMs) { 18 | throw 'Invalid ms argument given'; 19 | } 20 | 21 | if (!`${exchange}.${symbol}` in this.tickers) { 22 | return undefined; 23 | } 24 | 25 | return this.tickers[`${exchange}.${symbol}`] && 26 | this.tickers[`${exchange}.${symbol}`].createdAt > 27 | moment() 28 | .subtract(lastUpdatedSinceMs, 'ms') 29 | .toDate() 30 | ? this.tickers[`${exchange}.${symbol}`] 31 | : undefined; 32 | } 33 | 34 | all() { 35 | return this.tickers; 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /src/utils/common_util.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | /** 3 | * 4 | * @param {string} side 5 | * @param {number} currentPrice 6 | * @param {number} entryPrice 7 | * @returns {number} 8 | */ 9 | getProfitAsPercent: (side, currentPrice, entryPrice) => { 10 | switch (side) { 11 | case 'long': 12 | return parseFloat(((currentPrice / entryPrice - 1) * 100).toFixed(2)); 13 | case 'short': 14 | return parseFloat(((entryPrice / currentPrice - 1) * 100).toFixed(2)); 15 | default: 16 | throw new Error(`Invalid direction given for profit ${side}`); 17 | } 18 | }, 19 | 20 | camelToSnakeCase: (text) => { 21 | return text.replace(/(.)([A-Z][a-z]+)/, '$1_$2') 22 | .replace(/([a-z0-9])([A-Z])/, '$1_$2') 23 | .toLowerCase(); 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /src/utils/order_util.js: -------------------------------------------------------------------------------- 1 | const ExchangeOrder = require('../dict/exchange_order'); 2 | 3 | module.exports = { 4 | calculateOrderAmount: (price, capital) => { 5 | return capital / price; 6 | }, 7 | 8 | syncOrderByType: (position, orders, type) => { 9 | const stopOrders = orders.filter(order => order.type === type); 10 | if (stopOrders.length === 0) { 11 | return [ 12 | { 13 | amount: Math.abs(position.amount) 14 | } 15 | ]; 16 | } 17 | 18 | const stopOrder = stopOrders[0]; 19 | 20 | // only update if we 1 % out of range; to get not unit amount lot size issues 21 | if (module.exports.isPercentDifferentGreaterThen(position.amount, stopOrder.amount, 1)) { 22 | return [ 23 | { 24 | id: stopOrder.id, 25 | amount: position.amount 26 | } 27 | ]; 28 | } 29 | 30 | return []; 31 | }, 32 | 33 | syncStopLossOrder: (position, orders) => { 34 | return module.exports.syncOrderByType(position, orders, ExchangeOrder.TYPE_STOP); 35 | }, 36 | 37 | syncTrailingStopLossOrder: (position, orders) => { 38 | return module.exports.syncOrderByType(position, orders, ExchangeOrder.TYPE_TRAILING_STOP); 39 | }, 40 | 41 | /** 42 | * LTC: "0.008195" => "0.00820" 43 | * 44 | * @param num 0.008195 45 | * @param tickSize 0.00001 46 | * @returns {*} 47 | */ 48 | calculateNearestSize: (num, tickSize) => { 49 | const number = Math.trunc(num / tickSize) * tickSize; 50 | 51 | // fix float issues: 52 | // 0.0085696 => 0.00001 = 0.00857000...001 53 | const points = tickSize.toString().split('.'); 54 | if (points.length < 2) { 55 | return number; 56 | } 57 | 58 | return number.toFixed(points[1].length); 59 | }, 60 | 61 | isPercentDifferentGreaterThen: (value1, value2, percentDiff) => { 62 | // we dont care about negative values 63 | const value1Abs = Math.abs(value1); 64 | const value2Abs = Math.abs(value2); 65 | 66 | return Math.abs((value1Abs - value2Abs) / ((value1Abs + value2Abs) / 2)) * 100 > percentDiff; 67 | }, 68 | 69 | /** 70 | * Percent different between to values, independent of smaller or bigger 71 | * @param orderPrice 72 | * @param currentPrice 73 | * @returns {number} 74 | */ 75 | getPercentDifferent: (orderPrice, currentPrice) => { 76 | return orderPrice > currentPrice 77 | ? 100 - (currentPrice / orderPrice) * 100 78 | : 100 - (orderPrice / currentPrice) * 100; 79 | } 80 | }; 81 | -------------------------------------------------------------------------------- /src/utils/queue.js: -------------------------------------------------------------------------------- 1 | const Queue = require('queue-promise'); 2 | 3 | module.exports = class { 4 | constructor() { 5 | this.queue = new Queue({ 6 | concurrent: 1, 7 | interval: 1120, // every seconds; include some random ms 8 | start: true 9 | }); 10 | 11 | this.queue2 = new Queue({ 12 | concurrent: 2, 13 | interval: 1120, 14 | start: true 15 | }); 16 | 17 | this.queue3 = new Queue({ 18 | concurrent: 2, 19 | interval: 1180, 20 | start: true 21 | }); 22 | } 23 | 24 | add(promise) { 25 | return this.queue.enqueue(promise); 26 | } 27 | 28 | addQueue2(promise) { 29 | return this.queue2.enqueue(promise); 30 | } 31 | 32 | addQueue3(promise) { 33 | return this.queue3.enqueue(promise); 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /src/utils/request_client.js: -------------------------------------------------------------------------------- 1 | const request = require('request'); 2 | 3 | module.exports = class RequestClient { 4 | constructor(logger) { 5 | this.logger = logger; 6 | } 7 | 8 | executeRequest(options) { 9 | return new Promise(resolve => { 10 | request(options, (error, response, body) => { 11 | resolve({ 12 | error: error, 13 | response: response, 14 | body: body 15 | }); 16 | }); 17 | }); 18 | } 19 | 20 | executeRequestRetry(options, retryCallback, retryMs = 500, retries = 10) { 21 | const wait = time => { 22 | return new Promise(resolve => { 23 | setTimeout(() => { 24 | resolve(); 25 | }, time); 26 | }); 27 | }; 28 | 29 | return new Promise(async resolve => { 30 | let lastResult; 31 | 32 | for (let retry = 1; retry <= retries; retry++) { 33 | const result = (lastResult = await this.executeRequest(options)); 34 | 35 | const shouldRetry = retryCallback(result); 36 | 37 | if (shouldRetry !== true) { 38 | resolve(result); 39 | return; 40 | } 41 | 42 | if (this.logger) { 43 | const debug = JSON.stringify([ 44 | options.url ? options.url : '', 45 | result && result.response && result.response.statusCode ? result.response.statusCode : '', 46 | options.body ? options.body.substring(0, 50) : '' 47 | ]); 48 | 49 | this.logger.error(`Request: error retry (${retry}) in ${retryMs}ms ${debug}`); 50 | } 51 | 52 | await wait(retryMs); 53 | } 54 | 55 | resolve(lastResult); 56 | }); 57 | } 58 | }; 59 | -------------------------------------------------------------------------------- /src/utils/resample.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | /** 3 | * Resample eg 5m candle sticks into 15m or other minutes 4 | * 5 | * @param lookbackNewestFirst 6 | * @param minutes 7 | * @returns {Array} 8 | */ 9 | resampleMinutes: function(lookbackNewestFirst, minutes) { 10 | if (lookbackNewestFirst.length === 0) { 11 | return []; 12 | } 13 | 14 | if (lookbackNewestFirst.length > 1 && lookbackNewestFirst[0].time < lookbackNewestFirst[1].time) { 15 | throw 'Invalid candle stick order'; 16 | } 17 | 18 | // group candles by its higher resample time 19 | const resampleCandleGroup = []; 20 | 21 | const secs = minutes * 60; 22 | lookbackNewestFirst.forEach(candle => { 23 | const mod = candle.time % secs; 24 | 25 | const resampleCandleClose = 26 | mod === 0 27 | ? candle.time // we directly catch the window: eg full hour matched 28 | : candle.time - mod + secs; // we calculate the next full window in future where es candle is closing 29 | 30 | // store the candle inside the main candle close 31 | if (!resampleCandleGroup[resampleCandleClose]) { 32 | resampleCandleGroup[resampleCandleClose] = []; 33 | } 34 | 35 | resampleCandleGroup[resampleCandleClose].push(candle); 36 | }); 37 | 38 | const merge = []; 39 | 40 | for (const candleCloseTime in resampleCandleGroup) { 41 | const candles = resampleCandleGroup[candleCloseTime]; 42 | 43 | const x = { open: [], high: [], low: [], close: [], volume: [] }; 44 | 45 | candles.forEach(candle => { 46 | x.open.push(candle.open); 47 | x.high.push(candle.high); 48 | x.low.push(candle.low); 49 | x.close.push(candle.close); 50 | x.volume.push(candle.volume); 51 | }); 52 | 53 | const sortHighToLow = candles.slice().sort((a, b) => { 54 | return b.time - a.time; 55 | }); 56 | 57 | merge.push({ 58 | time: parseInt(candleCloseTime), 59 | open: sortHighToLow[sortHighToLow.length - 1].open, 60 | high: Math.max(...x.high), 61 | low: Math.min(...x.low), 62 | close: sortHighToLow[0].close, 63 | volume: x.volume.reduce((sum, a) => sum + Number(a), 0), 64 | _time: new Date(candleCloseTime * 1000), 65 | _candle_count: candles.length, 66 | _candles: sortHighToLow 67 | }); 68 | } 69 | 70 | // sort items and remove oldest item which can be incomplete 71 | return merge.sort((a, b) => b.time - a.time).splice(0, merge.length - 1); 72 | }, 73 | 74 | /** 75 | * Resample eg 5m candle sticks into 15m or other minutes 76 | * 77 | * @returns number 78 | * @param period 79 | */ 80 | convertPeriodToMinute: function(period) { 81 | const unit = period.slice(-1).toLowerCase(); 82 | 83 | switch (unit) { 84 | case 'm': 85 | return parseInt(period.substring(0, period.length - 1)); 86 | case 'h': 87 | return parseInt(period.substring(0, period.length - 1) * 60); 88 | case 'd': 89 | return parseInt(period.substring(0, period.length - 1) * 60 * 24); 90 | case 'w': 91 | return parseInt(period.substring(0, period.length - 1) * 60 * 24 * 7); 92 | case 'y': 93 | return parseInt(period.substring(0, period.length - 1) * 60 * 24 * 7 * 356); 94 | default: 95 | throw `Unsupported period unit: ${period}`; 96 | } 97 | }, 98 | 99 | convertMinuteToPeriod: function(period) { 100 | if (period < 60) { 101 | return `${period}m`; 102 | } 103 | 104 | if (period >= 60) { 105 | return `${period / 60}h`; 106 | } 107 | 108 | throw `Unsupported period: ${period}`; 109 | } 110 | }; 111 | -------------------------------------------------------------------------------- /src/utils/technical_analysis_validator.js: -------------------------------------------------------------------------------- 1 | const Resample = require('./resample'); 2 | 3 | module.exports = class TechnicalAnalysisValidator { 4 | isValidCandleStickLookback(lookbackNewestFirst, period) { 5 | if (lookbackNewestFirst.length === 0) { 6 | return false; 7 | } 8 | 9 | if (lookbackNewestFirst.length > 1 && lookbackNewestFirst[0].time < lookbackNewestFirst[1].time) { 10 | return false; 11 | } 12 | 13 | // check if candle to close no outside candle size with a little buffer 14 | let factor = 2; 15 | 16 | // we only get candles if we trades inside this range 17 | // as low timeframes can be silent allow some failings 18 | const minutes = Resample.convertPeriodToMinute(period); 19 | if (minutes === 1) { 20 | factor = 40; 21 | } else if (minutes === 2) { 22 | factor = 20; 23 | } else if (minutes < 10) { 24 | factor = 4; 25 | } 26 | 27 | const allowedOutdatedWindow = minutes * factor; 28 | const candleOpenToCurrentTime = Math.abs((Math.floor(Date.now() / 1000) - lookbackNewestFirst[0].time) / 60); 29 | 30 | if (candleOpenToCurrentTime > allowedOutdatedWindow) { 31 | return false; 32 | } 33 | 34 | // @TODO: check candles window "times" against the period 35 | 36 | return true; 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /src/utils/technical_pattern.js: -------------------------------------------------------------------------------- 1 | const { SMA } = require('technicalindicators'); 2 | 3 | module.exports = { 4 | /** 5 | * @param candles 6 | */ 7 | volumePump: candles => { 8 | if (candles.length < 20) { 9 | return {}; 10 | } 11 | 12 | if (candles.length > 1 && candles[0].time > candles[1].time) { 13 | throw 'Invalid candlestick order'; 14 | } 15 | 16 | const volSma = SMA.calculate({ 17 | period: 20, 18 | values: candles.slice(-40).map(b => b.volume) 19 | }); 20 | 21 | const candleSizeSma = SMA.calculate({ 22 | period: 20, 23 | values: candles.slice(-40).map(v => Math.abs(v.open - v.close)) 24 | }); 25 | 26 | const currentCandle = candles.slice(-1)[0]; 27 | const currentVolumeSma = volSma.slice(-1)[0]; 28 | 29 | return { 30 | volume_sd: volSma.slice(-1)[0], 31 | volume_v: currentCandle.volume / currentVolumeSma > 5 ? currentCandle.volume / currentVolumeSma : undefined, 32 | hint: currentCandle.volume / currentVolumeSma > 5 ? 'success' : undefined, 33 | price_trigger: currentCandle.high, 34 | roc_ma: 35 | Math.abs(Math.abs(candles.slice(-1)[0].open - candles.slice(-1)[0].close)) / candleSizeSma.slice(-1)[0] > 4 36 | ? 'success' 37 | : undefined 38 | }; 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /src/utils/throttler.js: -------------------------------------------------------------------------------- 1 | module.exports = class Throttler { 2 | constructor(logger) { 3 | this.logger = logger; 4 | this.tasks = {}; 5 | } 6 | 7 | addTask(key, func, timeout = 1000) { 8 | if (func.constructor.name !== 'AsyncFunction') { 9 | throw new Error(`Throttler no async function given: ${key}`); 10 | } 11 | 12 | if (key in this.tasks) { 13 | this.logger.debug(`Throttler already existing task: ${key} - ${timeout}ms`); 14 | return; 15 | } 16 | 17 | const me = this; 18 | this.tasks[key] = setTimeout(async () => { 19 | delete me.tasks[key]; 20 | try { 21 | await func(); 22 | } catch (e) { 23 | me.logger.error(`Task error: ${key} - ${String(e)}`); 24 | } 25 | }, timeout); 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /src/utils/winston_sqlite_transport.js: -------------------------------------------------------------------------------- 1 | const Transport = require('winston-transport'); 2 | 3 | module.exports = class WinstonSqliteTransport extends Transport { 4 | constructor(opts) { 5 | super(opts); 6 | 7 | if (!opts.database_connection) { 8 | throw new Error('database_connection is needed'); 9 | } 10 | 11 | if (!opts.table) { 12 | throw new Error('table is needed'); 13 | } 14 | 15 | this.db = opts.database_connection; 16 | this.table = opts.table; 17 | } 18 | 19 | log(info, callback) { 20 | setImmediate(() => { 21 | this.emit('logged', info); 22 | }); 23 | 24 | const parameters = { 25 | uuid: WinstonSqliteTransport.createUUID(), 26 | level: info.level, 27 | message: info.message, 28 | created_at: Math.floor(Date.now() / 1000) 29 | }; 30 | 31 | this.db 32 | .prepare( 33 | `INSERT INTO ${this.table}(uuid, level, message, created_at) VALUES ($uuid, $level, $message, $created_at)` 34 | ) 35 | .run(parameters); 36 | 37 | callback(); 38 | } 39 | 40 | static createUUID() { 41 | let dt = new Date().getTime(); 42 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { 43 | const r = (dt + Math.random() * 16) % 16 | 0; 44 | dt = Math.floor(dt / 16); 45 | return (c == 'x' ? r : (r & 0x3) | 0x8).toString(16); 46 | }); 47 | } 48 | }; 49 | -------------------------------------------------------------------------------- /templates/backtest_submit_multiple.html.twig: -------------------------------------------------------------------------------- 1 | {% extends './layout.html.twig' %} 2 | 3 | {% block title %}Backtesting Results | Crypto Bot{% endblock %} 4 | 5 | {% block stylesheet %} 6 | 7 | {% endblock %} 8 | 9 | {% block content %} 10 | 11 |
12 | 13 |
14 |
15 |
16 |
17 |

Backtesting Results

18 |
19 |
20 | 24 |
25 |
26 |
27 |
28 | 29 | 30 | 31 |
32 |
33 |
34 | {% for backtest in backtests %} 35 |
36 |
37 |
38 |

{{ backtest.pair }} - {{ backtest.result.strategy }} - {{ backtest.result.configuration.period }} - {{ backtest.result.start|date('y-m-d H:i:s') }} - {{ backtest.result.end|date('y-m-d H:i:s') }}

39 |
40 | 41 |
42 | {% include 'components/backtest_summary.html.twig' with { 43 | 'summary': backtest.result.summary 44 | } only %} 45 |
46 |
47 |
48 | {% endfor %} 49 |
50 | 51 |
52 |
53 | 54 |
55 | 56 | {% endblock %} 57 | 58 | {% block javascript %} 59 | 60 | 61 | 62 | {% endblock %} 63 | -------------------------------------------------------------------------------- /templates/components/alert.html.twig: -------------------------------------------------------------------------------- 1 |
2 | 3 |
{{ alert.title }}
4 | {% if alert.message is defined %}{{ alert.message }}{% endif %} 5 |
-------------------------------------------------------------------------------- /templates/components/backtest_summary.html.twig: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 | 8 | 11 | 12 | 13 | 16 | 19 | 20 | 21 | 24 | 27 | 28 | 29 | 32 | 35 | 36 |
6 | Initial Capital 7 | 9 | {{ summary.initialCapital }} $ 10 |
14 | Final Capital 15 | 17 | {{ summary.finalCapital }} $ 18 |
22 | Net Return on Investment 23 | 25 | {{ summary.netProfit }} % 26 |
30 | Sharpe Ratio 31 | 33 | {{ summary.sharpeRatio }} 34 |
37 |
38 | 39 |
40 | 41 | 42 | 45 | 48 | 49 | 50 | 53 | 56 | 57 | 58 | 61 | 64 | 65 | 66 | 69 | 72 | 73 |
43 | Average Return Per Trade 44 | 46 | {{ summary.averagePNLPercent }} % 47 |
51 | Total Number of Trades 52 | 54 | {{ summary.trades.total }} 55 |
59 | Number of Winning Trades 60 | 62 | {{ summary.trades.profitableCount }} 63 |
67 | Percentage of Winning Trades 68 | 70 | {{ summary.trades.profitabilityPercent }} 71 |
74 |
75 |
76 | -------------------------------------------------------------------------------- /templates/components/backtest_table.html.twig: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | {% for key, fields in extra_fields %} 10 | 11 | {% endfor %} 12 | 13 | 14 | 15 | 16 | 17 | 18 | {% for row in rows %} 19 | 20 | 21 | 22 | 31 | 42 | 43 | {% for column in row.columns|default([]) %} 44 | {% if column.type == 'cross' or column.type == 'histogram' %} 45 | {% set color = '' %} 46 | 47 | {% if column.state == 'over' %} 48 | {% set color = 'text-success' %} 49 | {% elseif column.state == 'below' %} 50 | {% set color = 'text-danger' %} 51 | {% endif %} 52 | 53 | 54 | {% elseif column.type == 'oscillator' %} 55 | {% set color = '' %} 56 | 57 | {% if column.state == 'over' %} 58 | {% set color = 'text-danger' %} 59 | {% elseif column.state == 'below' %} 60 | {% set color = 'text-success' %} 61 | {% endif %} 62 | 63 | 64 | {% elseif column.type == 'icon' %} 65 | 66 | {% else %} 67 | 68 | {% endif %} 69 | {% endfor %} 70 | 71 | 76 | 77 | {% endfor %} 78 | 79 |
TimePriceProfitSignal{{ fields.label|default(key) }}Debug
{{ row.time|date('y-m-d H:i:s') }}{{ row.price|default }} 23 | {% if row.profit is defined %} 24 | {{ row.profit|round(2) }} % 25 | {% endif %} 26 | 27 | {% if row.lastPriceClosed is defined %} 28 | {{ row.lastPriceClosed|round(2) }} % 29 | {% endif %} 30 | 32 | {% if row.result is defined %} 33 | {% if row.result.signal == 'long' %} 34 | 35 | {% elseif row.result.signal == 'short' %} 36 | 37 | {% elseif row.result.signal == 'close' %} 38 | 39 | {% endif %} 40 | {% endif %} 41 | {{ column.value }}{{ column.value }}{% if column.value is defined %}{% endif %}{{ column.value }} 72 | 73 | {{ row|json_encode }} 74 | 75 |
-------------------------------------------------------------------------------- /templates/orders/index.html.twig: -------------------------------------------------------------------------------- 1 | {% extends './layout.html.twig' %} 2 | -------------------------------------------------------------------------------- /templates/orders/layout.html.twig: -------------------------------------------------------------------------------- 1 | {% extends '../layout.html.twig' %} 2 | 3 | {% block title %}Orders | Crypto Bot{% endblock %} 4 | 5 | {% block content %} 6 |
7 |
8 |
9 |
10 |

Orders

11 |
12 |
13 |
14 |
15 | 16 |
17 |
18 |
19 |
20 | 21 |
22 |
23 |

24 | 25 | Pairs 26 |

27 | 28 | 29 |
30 |
31 |
    32 | {% for pair in pairs %} 33 |
  • {{ pair }}
  • 34 | {% endfor %} 35 |
36 |
37 |
38 |
39 | 40 | {% block page_content %}{% endblock %} 41 |
42 |
43 |
44 | {% endblock %} 45 | 46 | 47 | {% block javascript %} 48 | 78 | {% endblock javascript %} -------------------------------------------------------------------------------- /templates/trades.html.twig: -------------------------------------------------------------------------------- 1 | {% extends './layout.html.twig' %} 2 | 3 | {% block title %}Trades | Crypto Bot{% endblock %} 4 | 5 | {% block javascript %} 6 | 7 | 8 | 9 | {% endblock %} 10 | 11 | {% block content %} 12 | 13 |
14 | 15 |
16 |
17 |
18 |
19 |

Trades

20 |
21 |
22 | 26 |
27 |
28 |
29 |
30 | 31 | 32 | 33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 | 41 |
42 |
43 | 44 |
45 | 46 | {% endblock %} 47 | -------------------------------------------------------------------------------- /templates/tradingview.html.twig: -------------------------------------------------------------------------------- 1 | {% extends './layout.html.twig' %} 2 | 3 | {% block title %}{{ symbol }} | Trading View | Crypto Bot{% endblock %} 4 | 5 | {% block content %} 6 |
7 |
8 |
9 |
10 |
11 |

Trading View

12 |
13 |
14 | 18 |
19 |
20 |
21 |
22 | 23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 | {% endblock %} 37 | 38 | 39 | {% block javascript %} 40 | 41 | 42 | 43 | 92 | {% endblock javascript %} -------------------------------------------------------------------------------- /templates/tradingview_desk.html.twig: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Desk: {{ desk.name }} | Crypto Bot 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 23 | 24 | 25 | 26 | 27 | 39 | 40 |
41 |
42 | 43 | 62 |
63 | 64 | -------------------------------------------------------------------------------- /test/deploy.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const fs = require('fs'); 3 | 4 | describe('#validate pre deployment files', function() { 5 | it('test that config.json.dist file is valid', () => { 6 | const config = JSON.parse(fs.readFileSync(`${__dirname}/../conf.json.dist`, 'utf8')); 7 | 8 | assert.equal(config.webserver.ip, '0.0.0.0'); 9 | }); 10 | 11 | it('test that instance.js.dist file is valid', () => { 12 | const instances = require(`${__dirname}/../instance.js.dist`); 13 | 14 | assert.equal(instances.symbols.length > 0, true); 15 | assert.equal(instances.symbols.filter(i => i.symbol === 'ETHUSD').length, 1); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /test/dict/order.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const Order = require('../../src/dict/order'); 3 | 4 | describe('#order dict test', function() { 5 | it('test order dict creation (post only)', () => { 6 | const order = Order.createLimitPostOnlyOrder('BTCUSD', 'long', 12, 12, { foobar: 'test' }); 7 | 8 | assert.equal(order.options.foobar, 'test'); 9 | assert.equal(order.options.post_only, true); 10 | }); 11 | 12 | it('test order dict creation (post only + adjusted) [long]', () => { 13 | const order = Order.createLimitPostOnlyOrderAutoAdjustedPriceOrder('BTCUSD', 12); 14 | 15 | assert.equal(order.price, undefined); 16 | assert.equal(order.options.adjust_price, true); 17 | assert.equal(order.amount, 12); 18 | assert.equal(order.side, 'long'); 19 | 20 | assert.equal(order.hasAdjustedPrice(), true); 21 | }); 22 | 23 | it('test order dict creation (post only + adjusted) [short]', () => { 24 | const order = Order.createLimitPostOnlyOrderAutoAdjustedPriceOrder('BTCUSD', -12); 25 | 26 | assert.equal(order.price, undefined); 27 | assert.equal(order.options.adjust_price, true); 28 | assert.equal(order.amount, -12); 29 | assert.equal(order.side, 'short'); 30 | 31 | assert.equal(order.hasAdjustedPrice(), true); 32 | }); 33 | 34 | it('test order close creation', () => { 35 | const order = Order.createCloseOrderWithPriceAdjustment('BTCUSD', -12); 36 | 37 | assert.equal(order.price, undefined); 38 | assert.equal(order.options.adjust_price, true); 39 | assert.equal(order.options.close, true); 40 | 41 | assert.equal(order.side, 'short'); 42 | assert.equal(order.hasAdjustedPrice(), true); 43 | assert.deepEqual(order.options, { close: true, adjust_price: true, post_only: true }); 44 | 45 | assert.equal(Order.createCloseOrderWithPriceAdjustment('BTCUSD', 12).side, 'long'); 46 | }); 47 | 48 | it('test order close creation for closes', () => { 49 | const order = Order.createCloseLimitPostOnlyReduceOrder('BTCUSD', -12, 0.4); 50 | 51 | assert.equal(order.symbol, 'BTCUSD'); 52 | assert.equal(order.price, -12); 53 | assert.equal(order.amount, 0.4); 54 | 55 | assert.equal(order.side, 'short'); 56 | assert.deepEqual(order.options, { close: true, post_only: true }); 57 | }); 58 | 59 | it('test market order', () => { 60 | let order = Order.createMarketOrder('BTCUSD', -12); 61 | 62 | assert.equal(order.price < 0, true); 63 | assert.equal(order.side, 'short'); 64 | 65 | order = Order.createMarketOrder('BTCUSD', 12); 66 | 67 | assert.equal(order.price > 0, true); 68 | assert.equal(order.side, 'long'); 69 | }); 70 | 71 | it('test retry order', () => { 72 | const order = Order.createRetryOrder(Order.createMarketOrder('BTCUSD', 12)); 73 | 74 | assert.strictEqual(order.price > 0, true); 75 | assert.strictEqual(order.side, 'long'); 76 | assert.strictEqual(order.amount, 12); 77 | }); 78 | 79 | it('test retry order with amount [long]', () => { 80 | let order = Order.createRetryOrder(Order.createMarketOrder('BTCUSD', 12), -16); 81 | 82 | assert.strictEqual(order.price > 0, true); 83 | assert.strictEqual(order.side, 'long'); 84 | assert.strictEqual(order.amount, 16); 85 | 86 | order = Order.createRetryOrder(Order.createMarketOrder('BTCUSD', 12), 16); 87 | 88 | assert.strictEqual(order.price > 0, true); 89 | assert.strictEqual(order.side, 'long'); 90 | assert.strictEqual(order.amount, 16); 91 | }); 92 | 93 | it('test retry order with amount [short]', () => { 94 | let order = Order.createRetryOrder(Order.createMarketOrder('BTCUSD', -12), -16); 95 | 96 | assert.strictEqual(order.price > 0, false); 97 | assert.strictEqual(order.side, 'short'); 98 | assert.strictEqual(order.amount, -16); 99 | 100 | order = Order.createRetryOrder(Order.createMarketOrder('BTCUSD', -12), 16); 101 | 102 | assert.strictEqual(order.price > 0, false); 103 | assert.strictEqual(order.side, 'short'); 104 | assert.strictEqual(order.amount, -16); 105 | }); 106 | }); 107 | -------------------------------------------------------------------------------- /test/exchange/binance/account-info.json: -------------------------------------------------------------------------------- 1 | { 2 | "balances": [ 3 | { 4 | "asset": "BTC", 5 | "free": "0.00000380", 6 | "locked": "0.00000000" 7 | }, 8 | { 9 | "asset": "LTC", 10 | "free": "0.00000000", 11 | "locked": "0.00000000" 12 | }, 13 | { 14 | "asset": "USDT", 15 | "free": "169.50825986", 16 | "locked": "0.00000000" 17 | }, 18 | { 19 | "asset": "TNT", 20 | "free": "0.37100000", 21 | "locked": "2.00000000" 22 | } 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /test/exchange/binance_futures.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const fs = require('fs'); 3 | const BinanceFutures = require('../../src/exchange/binance_futures'); 4 | const Position = require('../../src/dict/position'); 5 | 6 | describe('#binance_futures exchange implementation', () => { 7 | const getJsonFixture = filename => { 8 | return JSON.parse(fs.readFileSync(`${__dirname}/binance_futures/${filename}`, 'utf8')); 9 | }; 10 | 11 | it('calculates positions', () => { 12 | const positions = BinanceFutures.createPositions(getJsonFixture('positions.json')); 13 | 14 | assert.strictEqual(positions[0].getSymbol(), 'ETHUSDT'); 15 | assert.strictEqual(positions[0].isShort(), true); 16 | assert.strictEqual(positions[0].getAmount(), -2.349); 17 | assert.strictEqual(positions[0].getEntry(), 170.24); 18 | assert.strictEqual(positions[0].getRaw().symbol, 'ETHUSDT'); 19 | 20 | // short 21 | assert.strictEqual(positions[0].getProfit(), -1.5); 22 | assert.strictEqual(positions[1].getProfit(), 10.07); 23 | 24 | // long 25 | assert.strictEqual(positions[2].getProfit(), 1.52); 26 | assert.strictEqual(positions[3].getProfit(), -9.15); 27 | }); 28 | 29 | it('websocket position close positions', async () => { 30 | const binanceFutures = new BinanceFutures({}, {}, {}, { info: () => {} }, {}, {}); 31 | 32 | binanceFutures.positions = { 33 | EOSUSDT: new Position('EOSUSDT', 'long', 1), 34 | ADAUSDT: new Position('ADAUSDT', 'long', 1) 35 | }; 36 | 37 | const json = getJsonFixture('websocket_position_close.json'); 38 | binanceFutures.accountUpdate(json); 39 | 40 | assert.strictEqual((await binanceFutures.getPositions()).length, 1); 41 | }); 42 | 43 | it('websocket position open positions', async () => { 44 | const binanceFutures = new BinanceFutures({}, {}, {}, { info: () => {} }, {}, {}); 45 | 46 | binanceFutures.positions = { 47 | ADAUSDT: new Position('ADAUSDT', 'long', 1) 48 | }; 49 | 50 | const json = getJsonFixture('websocket_position_open.json'); 51 | binanceFutures.accountUpdate(json); 52 | 53 | assert.strictEqual((await binanceFutures.getPositions()).length, 3); 54 | 55 | const ADAUSDT = await binanceFutures.getPositionForSymbol('ADAUSDT'); 56 | assert.strictEqual(ADAUSDT.getSymbol(), 'ADAUSDT'); 57 | assert.strictEqual(ADAUSDT.getAmount(), 1); 58 | 59 | const pos = await binanceFutures.getPositionForSymbol('EOSUSDT'); 60 | assert.strictEqual(pos.getSymbol(), 'EOSUSDT'); 61 | assert.strictEqual(pos.getAmount(), 1); 62 | assert.strictEqual(pos.getEntry(), 2.626); 63 | assert.strictEqual(pos.isLong(), true); 64 | 65 | const posShort = await binanceFutures.getPositionForSymbol('EOSUSDTSHORT'); 66 | assert.strictEqual(posShort.getSymbol(), 'EOSUSDTSHORT'); 67 | assert.strictEqual(posShort.getAmount(), -1); 68 | assert.strictEqual(posShort.getEntry(), 2.626); 69 | assert.strictEqual(posShort.isLong(), false); 70 | assert.strictEqual(posShort.isShort(), true); 71 | }); 72 | 73 | it('test converting websocket order to rest api order', async () => { 74 | const orders = getJsonFixture('websocket-orders.json').map(BinanceFutures.createRestOrderFromWebsocket); 75 | 76 | assert.strictEqual(orders[0].symbol, 'SXPUSDT'); 77 | assert.strictEqual(orders[0].clientOrderId, 'c6QlyAzYShGmuLe2Hu73ew'); 78 | assert.strictEqual(orders[0].side, 'SELL'); 79 | assert.strictEqual(orders[0].type, 'LIMIT'); 80 | assert.strictEqual(orders[0].timeInForce, 'GTC'); 81 | assert.strictEqual(orders[0].origQty, '374.4'); 82 | assert.strictEqual(orders[0].price, '1.6041'); 83 | assert.strictEqual(orders[0].stopPrice, '0'); 84 | assert.strictEqual(orders[0].status, 'PARTIALLY_FILLED'); 85 | assert.strictEqual(orders[0].orderId, 289977283); 86 | assert.strictEqual(orders[0].executedQty, '324.4'); 87 | assert.strictEqual(orders[0].updateTime > 1001371637215, true); 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /test/exchange/binance_futures/positions.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "symbol": "ETHUSDT", 4 | "positionAmt": "-2.349", 5 | "entryPrice": "170.24000", 6 | "markPrice": "172.82814882", 7 | "unRealizedProfit": "-6.07956158", 8 | "liquidationPrice": "203.10", 9 | "leverage": "5", 10 | "maxNotionalValue": "2000000", 11 | "marginType": "isolated", 12 | "isolatedMargin": "74.21191568", 13 | "isAutoAddMargin": "false", 14 | "positionSide": "BOTH" 15 | }, 16 | { 17 | "symbol": "ETHUSDT", 18 | "positionAmt": "-2.349", 19 | "entryPrice": "190.24000", 20 | "markPrice": "172.82814882", 21 | "unRealizedProfit": "-6.07956158", 22 | "liquidationPrice": "203.10", 23 | "leverage": "5", 24 | "maxNotionalValue": "2000000", 25 | "marginType": "isolated", 26 | "isolatedMargin": "74.21191568", 27 | "isAutoAddMargin": "false", 28 | "positionSide": "BOTH" 29 | }, 30 | { 31 | "symbol": "ETHUSDT", 32 | "positionAmt": "2.349", 33 | "entryPrice": "170.24000", 34 | "markPrice": "172.82814882", 35 | "unRealizedProfit": "-6.07956158", 36 | "liquidationPrice": "203.10", 37 | "leverage": "5", 38 | "maxNotionalValue": "2000000", 39 | "marginType": "isolated", 40 | "isolatedMargin": "74.21191568", 41 | "isAutoAddMargin": "false", 42 | "positionSide": "BOTH" 43 | }, 44 | { 45 | "symbol": "ETHUSDT", 46 | "positionAmt": "2.349", 47 | "entryPrice": "190.24000", 48 | "markPrice": "172.82814882", 49 | "unRealizedProfit": "-6.07956158", 50 | "liquidationPrice": "203.10", 51 | "leverage": "5", 52 | "maxNotionalValue": "2000000", 53 | "marginType": "isolated", 54 | "isolatedMargin": "74.21191568", 55 | "isAutoAddMargin": "false", 56 | "positionSide": "BOTH" 57 | } 58 | ] -------------------------------------------------------------------------------- /test/exchange/binance_futures/websocket-orders.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "s": "SXPUSDT", 4 | "c": "c6QlyAzYShGmuLe2Hu73ew", 5 | "S": "SELL", 6 | "o": "LIMIT", 7 | "f": "GTC", 8 | "q": "374.4", 9 | "p": "1.6041", 10 | "ap": "1.60410", 11 | "sp": "0", 12 | "x": "TRADE", 13 | "X": "PARTIALLY_FILLED", 14 | "i": 289977283, 15 | "l": "14.2", 16 | "z": "324.4", 17 | "L": "1.6041", 18 | "n": "0.00455564", 19 | "N": "USDT", 20 | "T": 1601371637215, 21 | "t": 19124533, 22 | "b": "0", 23 | "a": "80.23000", 24 | "m": true, 25 | "R": false, 26 | "wt": "CONTRACT_PRICE", 27 | "ot": "LIMIT", 28 | "ps": "BOTH", 29 | "cp": false, 30 | "rp": "0", 31 | "si": 0, 32 | "ss": 0 33 | } 34 | ] 35 | -------------------------------------------------------------------------------- /test/exchange/binance_futures/websocket_position_close.json: -------------------------------------------------------------------------------- 1 | { 2 | "e": "ACCOUNT_UPDATE", 3 | "T": 1594312160323, 4 | "E": 1594312160328, 5 | "a": { 6 | "B": [ 7 | { 8 | "a": "USDT", 9 | "wb": "1", 10 | "cw": "1" 11 | }, 12 | { 13 | "a": "BNB", 14 | "wb": "0", 15 | "cw": "0" 16 | } 17 | ], 18 | "P": [ 19 | { 20 | "s": "EOSUSDT", 21 | "pa": "0", 22 | "ep": "0.0000", 23 | "cr": "106.70419995", 24 | "up": "0", 25 | "mt": "cross", 26 | "iw": "0", 27 | "ps": "BOTH" 28 | } 29 | ], 30 | "m": "ORDER" 31 | } 32 | } -------------------------------------------------------------------------------- /test/exchange/binance_futures/websocket_position_open.json: -------------------------------------------------------------------------------- 1 | { 2 | "e": "ACCOUNT_UPDATE", 3 | "T": 1594312136242, 4 | "E": 1594312136246, 5 | "a": { 6 | "B": [ 7 | { 8 | "a": "USDT", 9 | "wb": "1", 10 | "cw": "1" 11 | }, 12 | { 13 | "a": "BNB", 14 | "wb": "0", 15 | "cw": "0" 16 | } 17 | ], 18 | "P": [ 19 | { 20 | "s": "EOSUSDT", 21 | "pa": "1", 22 | "ep": "2.6260", 23 | "cr": "106.70619995", 24 | "up": "-0.0014", 25 | "mt": "cross", 26 | "iw": "0", 27 | "ps": "BOTH" 28 | }, 29 | { 30 | "s": "EOSUSDTSHORT", 31 | "pa": "-1", 32 | "ep": "2.6260", 33 | "cr": "106.70619995", 34 | "up": "-0.0014", 35 | "mt": "cross", 36 | "iw": "0", 37 | "ps": "BOTH" 38 | }, 39 | { 40 | "s": "ADAUSDT", 41 | "pa": "7", 42 | "ep": "2.6260", 43 | "cr": "106.70619995", 44 | "up": "-0.0014", 45 | "mt": "cross", 46 | "iw": "0", 47 | "ps": "BOTH" 48 | } 49 | ], 50 | "m": "ORDER" 51 | } 52 | } -------------------------------------------------------------------------------- /test/exchange/bitfinex/on-orders.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 18233985719, 4 | "gid": null, 5 | "cid": 70300865307, 6 | "symbol": "tBCHBTC", 7 | "mtsCreate": 1539977500910, 8 | "mtsUpdate": 1539977500939, 9 | "amount": 0.2, 10 | "amountOrig": 0.2, 11 | "type": "LIMIT", 12 | "typePrev": null, 13 | "flags": 0, 14 | "status": "ACTIVE", 15 | "price": 0.067, 16 | "priceAvg": 0, 17 | "priceTrailing": 0, 18 | "priceAuxLimit": 0, 19 | "notify": false, 20 | "placedId": null 21 | } 22 | ] -------------------------------------------------------------------------------- /test/exchange/bitfinex/on-ps.json: -------------------------------------------------------------------------------- 1 | [ 2 | [ 3 | "tIOTUSD", 4 | "ACTIVE", 5 | -80, 6 | 0.54866, 7 | -0.00001689, 8 | null, 9 | null, 10 | null, 11 | null, 12 | null 13 | ], 14 | [ 15 | "tIOTUSD", 16 | "ACTIVE", 17 | 80, 18 | 0.54866, 19 | -0.00001689, 20 | null, 21 | null, 22 | null, 23 | null, 24 | null 25 | ] 26 | ] 27 | -------------------------------------------------------------------------------- /test/exchange/bitfinex/on-req-reject.json: -------------------------------------------------------------------------------- 1 | [ 15649951151, 2 | null, 3 | 1239527835507, 4 | "tEOSUSD", 5 | 1534676835099, 6 | 1534676835114, 7 | -10, 8 | -10, 9 | "LIMIT", 10 | null, 11 | null, 12 | null, 13 | 0, 14 | "POSTONLY CANCELED", 15 | null, 16 | null, 17 | 5.0882, 18 | 0, 19 | null, 20 | null, 21 | null, 22 | null, 23 | null, 24 | 0, 25 | 0, 26 | 0, 27 | null, 28 | null, 29 | "API>BFX", 30 | null, 31 | null, 32 | null 33 | ] -------------------------------------------------------------------------------- /test/exchange/bitmex/ws-positions.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "account": 294606, 4 | "symbol": "LTCZ18", 5 | "currency": "XBt", 6 | "underlying": "LTC", 7 | "quoteCurrency": "XBT", 8 | "commission": 0.0025, 9 | "initMarginReq": 0.1, 10 | "maintMarginReq": 0.015, 11 | "riskLimit": 5000000000, 12 | "leverage": 10, 13 | "crossMargin": false, 14 | "deleveragePercentile": 0.4, 15 | "rebalancedPnl": 0, 16 | "prevRealisedPnl": 0, 17 | "prevUnrealisedPnl": 0, 18 | "prevClosePrice": 0.00831, 19 | "openingTimestamp": "2018-10-19T17:00:00.000Z", 20 | "openingQty": -2, 21 | "openingCost": -1666000, 22 | "openingComm": -833, 23 | "openOrderBuyQty": 0, 24 | "openOrderBuyCost": 0, 25 | "openOrderBuyPremium": 0, 26 | "openOrderSellQty": 4, 27 | "openOrderSellCost": 3352000, 28 | "openOrderSellPremium": 0, 29 | "execBuyQty": 0, 30 | "execBuyCost": 0, 31 | "execSellQty": 2, 32 | "execSellCost": 1662000, 33 | "execQty": -2, 34 | "execCost": -1662000, 35 | "execComm": -831, 36 | "currentTimestamp": "2018-10-19T17:29:00.097Z", 37 | "currentQty": -4, 38 | "currentCost": -3328000, 39 | "currentComm": -1664, 40 | "realisedCost": 0, 41 | "unrealisedCost": -3328000, 42 | "grossOpenCost": 3352000, 43 | "grossOpenPremium": 0, 44 | "grossExecCost": 1662000, 45 | "isOpen": true, 46 | "markPrice": 0.00831, 47 | "markValue": -3324000, 48 | "riskValue": 6676000, 49 | "homeNotional": -4, 50 | "foreignNotional": 0.03324, 51 | "posState": "", 52 | "posCost": -3328000, 53 | "posCost2": -3328000, 54 | "posCross": 0, 55 | "posInit": 332800, 56 | "posComm": 9152, 57 | "posLoss": 0, 58 | "posMargin": 341952, 59 | "posMaint": 59072, 60 | "posAllowance": 0, 61 | "taxableMargin": 0, 62 | "initMargin": 352798, 63 | "maintMargin": 345952, 64 | "sessionMargin": 0, 65 | "targetExcessMargin": 0, 66 | "varMargin": 0, 67 | "realisedGrossPnl": 0, 68 | "realisedTax": 0, 69 | "realisedPnl": 1664, 70 | "unrealisedGrossPnl": 4000, 71 | "longBankrupt": 0, 72 | "shortBankrupt": 0, 73 | "taxBase": 4000, 74 | "indicativeTaxRate": 0, 75 | "indicativeTax": 0, 76 | "unrealisedTax": 0, 77 | "unrealisedPnl": 4000, 78 | "unrealisedPnlPcnt": 0.0012, 79 | "unrealisedRoePcnt": 0.012, 80 | "simpleQty": -4, 81 | "simpleCost": -0.03328, 82 | "simpleValue": -0.03324, 83 | "simplePnl": 0.00004, 84 | "simplePnlPcnt": 0.0012, 85 | "avgCostPrice": 0.00832, 86 | "avgEntryPrice": 0.00832, 87 | "breakEvenPrice": 0.00832, 88 | "marginCallPrice": 0.00902, 89 | "liquidationPrice": 0.00902, 90 | "bankruptPrice": 0.00915, 91 | "timestamp": "2018-10-19T17:29:00.097Z", 92 | "lastPrice": 0.00831, 93 | "lastValue": -3324000 94 | } 95 | ] -------------------------------------------------------------------------------- /test/exchange/bybit/ws-positions.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "symbol": "BTCUSD", 4 | "side": "None", 5 | "size": 0, 6 | "entry_price": 0, 7 | "liq_price": 0, 8 | "bust_price": 0, 9 | "take_profit": 0, 10 | "stop_loss": 0, 11 | "trailing_stop": 0, 12 | "position_value": 0, 13 | "leverage": 50, 14 | "position_status": "Normal", 15 | "auto_add_margin": 0, 16 | "cross_seq": 294556045, 17 | "position_seq": 96541317 18 | }, 19 | { 20 | "symbol": "BTCUSD", 21 | "side": "Buy", 22 | "size": 1, 23 | "entry_price": 7898.894154818327, 24 | "liq_price": 7782.5, 25 | "bust_price": 7744.5, 26 | "take_profit": 0, 27 | "stop_loss": 0, 28 | "trailing_stop": 0, 29 | "position_value": 0.0001266, 30 | "leverage": 50, 31 | "position_status": "Normal", 32 | "auto_add_margin": 0, 33 | "cross_seq": 294557049, 34 | "position_seq": 96542339 35 | }, 36 | { 37 | "id": 118105, 38 | "user_id": 500181, 39 | "risk_id": 21, 40 | "symbol": "EOSUSD", 41 | "side": "Sell", 42 | "size": 10, 43 | "position_value": 1.56568028, 44 | "entry_price": 6.387000032982468, 45 | "leverage": 25, 46 | "auto_add_margin": 0, 47 | "position_margin": 0.06262722, 48 | "liq_price": 6.618, 49 | "bust_price": 6.653, 50 | "occ_closing_fee": 0.00112732, 51 | "occ_funding_fee": 0, 52 | "ext_fields": null, 53 | "take_profit": 0, 54 | "stop_loss": 0, 55 | "trailing_stop": 0, 56 | "position_status": "Normal", 57 | "deleverage_indicator": 5, 58 | "oc_calc_data": "{\"blq\":10,\"blv\":\"1.66666666\",\"slq\":0,\"bmp\":6,\"smp\":0}", 59 | "order_margin": 0, 60 | "wallet_balance": 0.06843150999999956, 61 | "realised_pnl": 0.00039141999999955593, 62 | "cum_realised_pnl": -1.4315684900000005, 63 | "cum_commission": 0, 64 | "cross_seq": 85796995, 65 | "position_seq": 85847236, 66 | "created_at": "2019-04-22T08:34:06.000Z", 67 | "updated_at": "2019-06-09T12:56:30.000Z", 68 | "unrealised_pnl": 0.03201903000000006 69 | } 70 | ] -------------------------------------------------------------------------------- /test/exchange/ftx/orders.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 771250569, 3 | "clientId": null, 4 | "market": "ETH-PERP", 5 | "type": "limit", 6 | "side": "sell", 7 | "price": 184.85, 8 | "size": 0.005, 9 | "status": "new", 10 | "filledSize": 0, 11 | "remainingSize": 0.005, 12 | "reduceOnly": false, 13 | "avgFillPrice": null, 14 | "postOnly": false, 15 | "ioc": false 16 | } -------------------------------------------------------------------------------- /test/exchange/ftx/positions.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "collateralUsed": 0, 4 | "cost": 0, 5 | "entryPrice": null, 6 | "estimatedLiquidationPrice": null, 7 | "future": "ADA-PERP", 8 | "initialMarginRequirement": 0.05, 9 | "longOrderSize": 0, 10 | "maintenanceMarginRequirement": 0.03, 11 | "netSize": 0, 12 | "openSize": 0, 13 | "realizedPnl": -43.93447263, 14 | "shortOrderSize": 0, 15 | "side": "buy", 16 | "size": 0, 17 | "unrealizedPnl": 0 18 | }, 19 | { 20 | "collateralUsed": 9.9164026, 21 | "cost": -198.328052, 22 | "entryPrice": 0.002669, 23 | "estimatedLiquidationPrice": 0.0030896924356879473, 24 | "future": "DOGE-PERP", 25 | "initialMarginRequirement": 0.05, 26 | "longOrderSize": 0, 27 | "maintenanceMarginRequirement": 0.03, 28 | "netSize": -74308, 29 | "openSize": 74308, 30 | "realizedPnl": 1.77959695, 31 | "shortOrderSize": 0, 32 | "side": "sell", 33 | "size": 74308, 34 | "unrealizedPnl": 0 35 | } 36 | ] -------------------------------------------------------------------------------- /test/exchange/utils/ccxt.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "avgFillPrice": null, 4 | "clientId": null, 5 | "createdAt": null, 6 | "filledSize": 0, 7 | "future": "SHIT-PERP", 8 | "id": 759782359, 9 | "ioc": false, 10 | "market": "SHIT-PERP", 11 | "postOnly": true, 12 | "price": 543, 13 | "reduceOnly": true, 14 | "remainingSize": 0.803, 15 | "side": "buy", 16 | "size": 0.803, 17 | "status": "open", 18 | "type": "limit" 19 | }, 20 | "id": "759782359", 21 | "symbol": "SHIT-PERP", 22 | "type": "limit", 23 | "side": "buy", 24 | "price": 543, 25 | "amount": 0.803, 26 | "cost": 0, 27 | "filled": 0, 28 | "remaining": 0.803, 29 | "status": "open" 30 | } -------------------------------------------------------------------------------- /test/exchange/utils/order_bag.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const OrderBag = require('../../../src/exchange/utils/order_bag'); 3 | const ExchangeOrder = require('../../../src/dict/exchange_order'); 4 | 5 | describe('#order bag utils', function() { 6 | it('test non strict handling non id type', async () => { 7 | const orderBag = new OrderBag(); 8 | 9 | orderBag.triggerOrder( 10 | new ExchangeOrder( 11 | 12345, 12 | 'BCHBTC', 13 | 'open', 14 | undefined, 15 | undefined, 16 | undefined, 17 | undefined, 18 | 'sell', 19 | ExchangeOrder.TYPE_LIMIT 20 | ) 21 | ); 22 | 23 | const newVar = await orderBag.findOrderById('12345'); 24 | assert.strictEqual(12345, newVar.id); 25 | 26 | orderBag.triggerOrder( 27 | new ExchangeOrder( 28 | '12345', 29 | 'BCHBTC', 30 | ExchangeOrder.STATUS_CANCELED, 31 | undefined, 32 | undefined, 33 | undefined, 34 | undefined, 35 | 'sell', 36 | ExchangeOrder.TYPE_LIMIT 37 | ) 38 | ); 39 | 40 | assert.strictEqual(undefined, await orderBag.findOrderById('12345')); 41 | }); 42 | 43 | it('test non strict handling non id type get', async () => { 44 | const orderBag = new OrderBag(); 45 | 46 | orderBag.triggerOrder( 47 | new ExchangeOrder( 48 | 12345, 49 | 'BCHBTC', 50 | 'open', 51 | undefined, 52 | undefined, 53 | undefined, 54 | undefined, 55 | 'sell', 56 | ExchangeOrder.TYPE_LIMIT 57 | ) 58 | ); 59 | 60 | assert.strictEqual(12345, (await orderBag.findOrderById('12345')).id); 61 | assert.strictEqual(12345, (await orderBag.findOrderById(12345)).id); 62 | 63 | assert.strictEqual(12345, orderBag.get('12345').id); 64 | assert.strictEqual(12345, orderBag.get(12345).id); 65 | }); 66 | 67 | it('test non strict handling non id type set', async () => { 68 | const orderBag = new OrderBag(); 69 | 70 | orderBag.set([ 71 | new ExchangeOrder( 72 | 12345, 73 | 'BCHBTC', 74 | 'open', 75 | undefined, 76 | undefined, 77 | undefined, 78 | undefined, 79 | 'sell', 80 | ExchangeOrder.TYPE_LIMIT 81 | ), 82 | new ExchangeOrder( 83 | 12346, 84 | 'BCHBTC', 85 | 'open', 86 | undefined, 87 | undefined, 88 | undefined, 89 | undefined, 90 | 'sell', 91 | ExchangeOrder.TYPE_LIMIT 92 | ) 93 | ]); 94 | 95 | assert.strictEqual(12345, (await orderBag.findOrderById('12345')).id); 96 | assert.strictEqual(12345, (await orderBag.findOrderById(12345)).id); 97 | 98 | assert.strictEqual(12345, orderBag.get('12345').id); 99 | assert.strictEqual(12345, orderBag.get(12345).id); 100 | 101 | orderBag.delete(12345); 102 | assert.strictEqual(undefined, orderBag.get(12345)); 103 | 104 | assert.strictEqual(12346, orderBag.all()[0].id); 105 | }); 106 | }); 107 | -------------------------------------------------------------------------------- /test/modules/exchange/exchange_candle_combine.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const ExchangeCandleCombine = require('../../../src/modules/exchange/exchange_candle_combine'); 3 | 4 | const Candlestick = require('../../../src/dict/candlestick'); 5 | 6 | describe('#exchange candle combine', () => { 7 | it('test that times are combined for exchanges', async () => { 8 | const calls = []; 9 | 10 | const exchangeCandleCombine = new ExchangeCandleCombine({ 11 | getLookbacksForPair: async () => { 12 | return createCandles(); 13 | }, 14 | getLookbacksSince: async (exchange, symbol, period, start) => { 15 | calls.push([exchange, symbol, period, start]); 16 | 17 | switch (exchange) { 18 | case 'binance': 19 | return createCandles(); 20 | case 'gap': 21 | return createCandlesWithGap(); 22 | default: 23 | return []; 24 | } 25 | } 26 | }); 27 | 28 | const result = await exchangeCandleCombine.fetchCombinedCandles('bitmex', 'XTBUSD', '15m', [ 29 | { 30 | name: 'binance', 31 | symbol: 'BTCUSD' 32 | }, 33 | { 34 | name: 'gap', 35 | symbol: 'FOOUSD' 36 | }, 37 | { 38 | name: 'foobar', 39 | symbol: 'FOOUSD' 40 | } 41 | ]); 42 | 43 | assert.equal(result.bitmex.length, 22); 44 | assert.equal(result['binance' + 'BTCUSD'].length, 22); 45 | 46 | assert.equal(result.bitmex[0].open, 2); 47 | assert.equal(result['binance' + 'BTCUSD'][0].open, 2); 48 | 49 | assert.equal(result.bitmex[0].close, 2.1); 50 | assert.equal(result['binance' + 'BTCUSD'][0].close, 2.1); 51 | 52 | assert.equal(result.bitmex[0].time > result.bitmex[1].time, true); 53 | assert.equal(result['binance' + 'BTCUSD'][0].time > result['binance' + 'BTCUSD'][1].time, true); 54 | assert.equal(result['gap' + 'FOOUSD'][0].time > result['gap' + 'FOOUSD'][1].time, true); 55 | 56 | assert.equal(result.bitmex[result.bitmex.length - 1].close, 46.2); 57 | assert.equal(result['binance' + 'BTCUSD'][result['binance' + 'BTCUSD'].length - 1].close, 46.2); 58 | 59 | assert.equal(result['gap' + 'FOOUSD'].length, 22); 60 | assert.equal(calls.filter(c => c[3] === 1393473600).length, 3); 61 | 62 | assert.equal('foobar' in result['gap' + 'FOOUSD'], false); 63 | }); 64 | 65 | it('test that only main exchagne is given', async () => { 66 | const exchangeCandleCombine = new ExchangeCandleCombine({ 67 | getLookbacksForPair: async () => { 68 | return createCandles(); 69 | }, 70 | getLookbacksSince: async () => { 71 | return createCandles(); 72 | } 73 | }); 74 | 75 | const result = await exchangeCandleCombine.fetchCombinedCandles('bitmex', 'XTBUSD', '15m'); 76 | 77 | assert.equal(result.bitmex.length, 22); 78 | assert.equal(result.bitmex[0].close, 2.1); 79 | 80 | assert.equal(result.bitmex[0].time > result.bitmex[1].time, true); 81 | 82 | assert.equal(result.bitmex[result.bitmex.length - 1].close, 46.2); 83 | }); 84 | 85 | function createCandles() { 86 | const candles = []; 87 | 88 | // 2014-02-27T09:30:00.000Z 89 | const start = 1393493400; 90 | 91 | for (let i = 1; i < 23; i++) { 92 | candles.push(new Candlestick(start - 15 * i * 60, i * 2, i * 1.1, i * 0.9, i * 2.1, i * 100)); 93 | } 94 | 95 | return candles; 96 | } 97 | 98 | function createCandlesWithGap() { 99 | const candles = []; 100 | 101 | // 2014-02-27T09:30:00.000Z 102 | const start = 1393493400; 103 | 104 | for (let i = 1; i < 23; i++) { 105 | if (i % 2) { 106 | continue; 107 | } 108 | 109 | candles.push(new Candlestick(start - 15 * i * 60, i * 2, i * 1.1, i * 0.9, i * 2.1, i * 100)); 110 | } 111 | 112 | return candles; 113 | } 114 | }); 115 | -------------------------------------------------------------------------------- /test/modules/exchange/exchange_manager.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const ExchangeManager = require('../../../src/modules/exchange/exchange_manager'); 3 | const Noop = require('../../../src/exchange/noop'); 4 | const Position = require('../../../src/dict/position'); 5 | const ExchangeOrder = require('../../../src/dict/exchange_order'); 6 | 7 | describe('#exchange manager', () => { 8 | it('test that exchanges are initialized', () => { 9 | const symbols = [ 10 | { 11 | symbol: 'BTCUSD', 12 | periods: ['1m', '15m', '1h'], 13 | exchange: 'noop', 14 | state: 'watch' 15 | }, 16 | { 17 | symbol: 'BTCUSD', 18 | periods: ['1m', '15m', '1h'], 19 | exchange: 'FOOBAR', 20 | state: 'watch' 21 | } 22 | ]; 23 | 24 | const config = { 25 | noop: { 26 | key: 'foobar', 27 | secret: 'foobar' 28 | } 29 | }; 30 | 31 | const exchangeManager = new ExchangeManager([new Noop()], {}, { symbols: symbols }, { exchanges: config }); 32 | 33 | exchangeManager.init(); 34 | 35 | assert.deepEqual( 36 | exchangeManager 37 | .all() 38 | .map(exchange => exchange.getName()) 39 | .sort(), 40 | ['noop'] 41 | ); 42 | 43 | assert.equal(exchangeManager.get('noop').getName(), 'noop'); 44 | assert.equal(exchangeManager.get('UNKNOWN'), undefined); 45 | }); 46 | 47 | it('test positions and orders', async () => { 48 | const symbols = [ 49 | { 50 | symbol: 'BTCUSD', 51 | periods: ['1m', '15m', '1h'], 52 | exchange: 'noop', 53 | state: 'watch' 54 | } 55 | ]; 56 | 57 | const config = { 58 | noop: { 59 | key: 'foobar', 60 | secret: 'foobar' 61 | } 62 | }; 63 | 64 | const exchange = new Noop(); 65 | exchange.getPositionForSymbol = async symbol => 66 | new Position(symbol, 'long', 1, undefined, undefined, 100, { stop: 0.9 }); 67 | exchange.getPositions = async () => [new Position('BTCUSDT', 'long', 1, undefined, undefined, 100, { stop: 0.9 })]; 68 | exchange.getOrdersForSymbol = async symbol => 69 | new ExchangeOrder( 70 | '25035356', 71 | symbol, 72 | 'open', 73 | undefined, 74 | undefined, 75 | undefined, 76 | undefined, 77 | 'buy', 78 | ExchangeOrder.TYPE_LIMIT 79 | ); 80 | 81 | const exchangeManager = new ExchangeManager([exchange], {}, { symbols: symbols }, { exchanges: config }); 82 | 83 | exchangeManager.init(); 84 | 85 | const position = await exchangeManager.getPosition('noop', 'BTCUSD'); 86 | assert.strictEqual(position.symbol, 'BTCUSD'); 87 | 88 | const order = await exchangeManager.getOrders('noop', 'BTCUSD'); 89 | assert.strictEqual(order.symbol, 'BTCUSD'); 90 | 91 | const positions = await exchangeManager.getPositions('noop', 'BTCUSD'); 92 | assert.strictEqual(positions[0].getExchange(), 'noop'); 93 | assert.strictEqual(positions[0].getSymbol(), 'BTCUSDT'); 94 | assert.strictEqual(positions[0].getPosition().symbol, 'BTCUSDT'); 95 | }); 96 | }); 97 | -------------------------------------------------------------------------------- /test/modules/listener/tick_listener.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const TickListener = require('../../../src/modules/listener/tick_listener'); 3 | const Ticker = require('../../../src/dict/ticker'); 4 | const SignalResult = require('../../../src/modules/strategy/dict/signal_result'); 5 | 6 | describe('#tick listener for order', function() { 7 | it('test tick listener for live order', async () => { 8 | let updates = []; 9 | 10 | const listener = new TickListener( 11 | { get: () => new Ticker('unknown', 'BTC', 123456, 12, 12) }, 12 | {}, 13 | { send: () => {} }, 14 | { signal: () => {} }, 15 | { 16 | executeStrategy: async () => { 17 | return SignalResult.createSignal('short', {}); 18 | } 19 | }, 20 | { 21 | getPosition: async () => { 22 | return undefined; 23 | } 24 | }, 25 | { 26 | update: async (exchange, symbol, signal) => { 27 | updates.push(exchange, symbol, signal); 28 | return []; 29 | } 30 | }, 31 | { info: () => {} } 32 | ); 33 | 34 | await listener.visitTradeStrategy('foobar', { 35 | symbol: 'FOOUSD', 36 | exchange: 'FOOBAR' 37 | }); 38 | 39 | assert.deepEqual(['FOOBAR', 'FOOUSD', 'short'], updates); 40 | 41 | // reset; block for time window 42 | updates = []; 43 | await listener.visitTradeStrategy('foobar', { 44 | symbol: 'FOOUSD', 45 | exchange: 'FOOBAR' 46 | }); 47 | 48 | assert.deepEqual([], updates); 49 | }); 50 | 51 | it('test tick listener for notifier order', async () => { 52 | const calls = []; 53 | 54 | const listener = new TickListener( 55 | { get: () => new Ticker('unknown', 'BTC', 123456, 12, 12) }, 56 | {}, 57 | { send: () => {} }, 58 | { 59 | signal: (exchange, symbol, opts, signal, strategyKey) => { 60 | calls.push(exchange, symbol, opts, signal, strategyKey); 61 | return []; 62 | } 63 | }, 64 | { 65 | executeStrategy: async () => { 66 | return SignalResult.createSignal('short', {}); 67 | } 68 | }, 69 | { 70 | getPosition: async () => { 71 | return undefined; 72 | } 73 | }, 74 | {}, 75 | { info: () => {} } 76 | ); 77 | 78 | await listener.visitStrategy( 79 | { strategy: 'foobar' }, 80 | { 81 | symbol: 'FOOUSD', 82 | exchange: 'FOOBAR' 83 | } 84 | ); 85 | 86 | assert.deepEqual(calls, [ 87 | 'FOOBAR', 88 | 'FOOUSD', 89 | { price: 12, strategy: 'foobar', raw: '{"_debug":{},"_signal":"short","placeOrders":[]}' }, 90 | 'short', 91 | 'foobar' 92 | ]); 93 | }); 94 | }); 95 | -------------------------------------------------------------------------------- /test/modules/order/stop_loss_calculator.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const StopLossCalculator = require('../../../src/modules/order/stop_loss_calculator'); 3 | const Position = require('../../../src/dict/position'); 4 | const Ticker = require('../../../src/dict/ticker'); 5 | const Tickers = require('../../../src/storage/tickers'); 6 | 7 | describe('#stop loss order calculation', function() { 8 | const fakeLogger = { info: () => {} }; 9 | it('calculate stop lose for long', async () => { 10 | const tickers = new Tickers(); 11 | tickers.set(new Ticker('noop', 'BTCUSD', undefined, 6500.66, 6502.99)); 12 | const calculator = new StopLossCalculator(tickers, fakeLogger); 13 | 14 | const result = await calculator.calculateForOpenPosition( 15 | 'noop', 16 | new Position('BTCUSD', 'long', 0.15, 6500.66, new Date(), 6501.76) 17 | ); 18 | 19 | assert.equal(result.toFixed(1), -6306.7); 20 | 21 | const result2 = await calculator.calculateForOpenPosition( 22 | 'noop', 23 | new Position('BTCUSD', 'long', 0.15, 6500.66, new Date(), 6501.76), 24 | { percent: 5 } 25 | ); 26 | 27 | assert.equal(result2.toFixed(1), -6176.7); 28 | }); 29 | 30 | it('calculate stop lose for short', async () => { 31 | const tickers = new Tickers(); 32 | tickers.set(new Ticker('noop', 'BTCUSD', undefined, 6500.66, 6502.99)); 33 | 34 | const calculator = new StopLossCalculator(tickers, fakeLogger); 35 | 36 | const result = await calculator.calculateForOpenPosition( 37 | 'noop', 38 | new Position('BTCUSD', 'short', -0.15, 6500.66, new Date(), 6501.76) 39 | ); 40 | 41 | assert.equal(result.toFixed(1), 6696.8); 42 | }); 43 | 44 | it('calculate stop lose invalid option', async () => { 45 | const tickers = new Tickers(); 46 | tickers.set(new Ticker('noop', 'BTCUSD', undefined, 6500.66, 6502.99)); 47 | 48 | const calculator = new StopLossCalculator(tickers, fakeLogger); 49 | 50 | const result = await calculator.calculateForOpenPosition( 51 | 'noop', 52 | new Position('BTCUSD', 'short', -0.15, 6500.66, new Date(), 6501.76), 53 | {} 54 | ); 55 | 56 | assert.equal(result, undefined); 57 | }); 58 | 59 | it('calculate stop lose with higher ticker (long)', async () => { 60 | const tickers = new Tickers(); 61 | tickers.set(new Ticker('noop', 'BTCUSD', undefined, 6500.66, 6301)); 62 | 63 | const calculator = new StopLossCalculator(tickers, fakeLogger); 64 | 65 | const result = await calculator.calculateForOpenPosition( 66 | 'noop', 67 | new Position('BTCUSD', 'long', 0.15, 6500.66, new Date(), 6501.76) 68 | ); 69 | 70 | assert.equal(result, undefined); 71 | }); 72 | 73 | it('calculate stop lose with higher ticker (short)', async () => { 74 | const tickers = new Tickers(); 75 | tickers.set(new Ticker('noop', 'BTCUSD', undefined, 6796, 6502.99)); 76 | 77 | const calculator = new StopLossCalculator(tickers, fakeLogger); 78 | 79 | const result = await calculator.calculateForOpenPosition( 80 | 'noop', 81 | new Position('BTCUSD', 'short', -0.15, 6500.66, new Date(), 6501.76) 82 | ); 83 | 84 | assert.equal(result, undefined); 85 | }); 86 | }); 87 | -------------------------------------------------------------------------------- /test/modules/strategy/dict/indicator_period.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const IndicatorPeriod = require('../../../../src/modules/strategy/dict/indicator_period'); 3 | 4 | describe('#test indicator', function() { 5 | it('test that yield visiting is possible', () => { 6 | const ip = new IndicatorPeriod( 7 | {}, 8 | { 9 | macd: [ 10 | { 11 | test: 'test1' 12 | }, 13 | { 14 | test: 'test2' 15 | }, 16 | { 17 | test: 'test3' 18 | } 19 | ], 20 | sma: [1, 2, 3, 4, 5] 21 | } 22 | ); 23 | 24 | const calls = []; 25 | 26 | for (const value of ip.visitLatestIndicators()) { 27 | calls.push(value); 28 | 29 | if (calls.length > 1) { 30 | break; 31 | } 32 | } 33 | 34 | assert.deepEqual({ macd: { test: 'test3' }, sma: 5 }, calls[0]); 35 | assert.deepEqual({ macd: { test: 'test2' }, sma: 4 }, calls[1]); 36 | }); 37 | 38 | it('test that helper for latest elements are given', () => { 39 | const ip = new IndicatorPeriod( 40 | {}, 41 | { 42 | macd: [ 43 | { 44 | test: 'test1' 45 | }, 46 | { 47 | test: 'test2' 48 | }, 49 | { 50 | test: 'test3' 51 | } 52 | ], 53 | sma: [1, 2, 3, 4, 5] 54 | } 55 | ); 56 | 57 | assert.deepEqual({ macd: { test: 'test3' }, sma: 5 }, ip.getLatestIndicators()); 58 | assert.deepEqual(5, ip.getLatestIndicator('sma')); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /test/modules/strategy/dict/signal_result.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const SignalResult = require('../../../../src/modules/strategy/dict/signal_result'); 3 | 4 | describe('#test signal object', function() { 5 | it('test that signal state is correct', () => { 6 | const signal = new SignalResult(); 7 | 8 | assert.equal(signal.getSignal(), undefined); 9 | assert.deepEqual(signal.getDebug(), {}); 10 | 11 | signal.setSignal('short'); 12 | assert.equal(signal.getSignal(), 'short'); 13 | 14 | signal.mergeDebug({ test: 'foobar' }); 15 | signal.addDebug('test2', 'test'); 16 | signal.addDebug('test', 'foobar2'); 17 | signal.mergeDebug({ test3: 'foobar', test5: 'foobar' }); 18 | 19 | assert.deepEqual(signal.getDebug(), { 20 | test: 'foobar2', 21 | test2: 'test', 22 | test3: 'foobar', 23 | test5: 'foobar' 24 | }); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /test/modules/strategy/strategies/obv_pump_dump.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const OBVPumpDump = require('../../../../src/modules/strategy/strategies/obv_pump_dump'); 3 | const IndicatorBuilder = require('../../../../src/modules/strategy/dict/indicator_builder'); 4 | const IndicatorPeriod = require('../../../../src/modules/strategy/dict/indicator_period'); 5 | const StrategyContext = require('../../../../src/dict/strategy_context'); 6 | const Ticker = require('../../../../src/dict/ticker'); 7 | 8 | describe('#strategy obv_pump_dump', () => { 9 | it('obv_pump_dump strategy builder', async () => { 10 | const indicatorBuilder = new IndicatorBuilder(); 11 | const obv = new OBVPumpDump(); 12 | 13 | obv.buildIndicator(indicatorBuilder); 14 | assert.equal(2, indicatorBuilder.all().length); 15 | }); 16 | 17 | it('obv_pump_dump strategy long', async () => { 18 | const obv = new OBVPumpDump(); 19 | 20 | const result = await obv.period( 21 | new IndicatorPeriod(createStrategyContext(), { 22 | ema: [380, 370], 23 | obv: [ 24 | -2358, 25 | -2395, 26 | -2395, 27 | -2395, 28 | -2385, 29 | -2165, 30 | -1987, 31 | -1987, 32 | -1990, 33 | -1990, 34 | -1990, 35 | -1990, 36 | -1990, 37 | -1948, 38 | -1808, 39 | -1601, 40 | -1394, 41 | -1394, 42 | -1147, 43 | 988, 44 | 3627, 45 | 6607, 46 | 11467 47 | ] 48 | }), 49 | {} 50 | ); 51 | 52 | assert.equal('long', result.getSignal()); 53 | assert.equal('up', result.getDebug().trend); 54 | }); 55 | 56 | it('obv_pump_dump strategy long options', async () => { 57 | const obv = new OBVPumpDump(); 58 | 59 | const result = await obv.period( 60 | new IndicatorPeriod(createStrategyContext(), { 61 | ema: [380, 370], 62 | obv: [ 63 | -2358, 64 | -2395, 65 | -2395, 66 | -2395, 67 | -2385, 68 | -2165, 69 | -1987, 70 | -1987, 71 | -1990, 72 | -1990, 73 | -1990, 74 | -1990, 75 | -1990, 76 | -1948, 77 | -1808, 78 | -1601, 79 | -1394, 80 | -1394, 81 | -1147, 82 | 988, 83 | 3627, 84 | 6607, 85 | 11467 86 | ] 87 | }), 88 | { trigger_multiplier: 1000 } 89 | ); 90 | 91 | assert.equal(undefined, result.getSignal()); 92 | assert.equal('up', result.getDebug().trend); 93 | }); 94 | 95 | let createStrategyContext = () => { 96 | return new StrategyContext({}, new Ticker('goo', 'goo', 'goo', 394, 394)); 97 | }; 98 | }); 99 | -------------------------------------------------------------------------------- /test/modules/strategy/strategies/strategy_manager.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const fs = require('fs'); 3 | const StrategyManager = require('../../../../src/modules/strategy/strategy_manager'); 4 | const StrategyContext = require('../../../../src/dict/strategy_context'); 5 | const TechnicalAnalysisValidator = require('../../../../src/utils/technical_analysis_validator'); 6 | const Ticker = require('../../../../src/dict/ticker'); 7 | 8 | describe('#strategy manager', () => { 9 | it('strategy cci', async () => { 10 | const strategyManager = new StrategyManager(createTechnicalAnalysisValidator(), createCandlestickRepository()); 11 | 12 | const result = await strategyManager.executeStrategy('cci', createStrategyContext(), 'foobar', 'BTCUSD', { 13 | period: '15m' 14 | }); 15 | assert.equal(undefined, result.signal); 16 | }); 17 | 18 | it('strategy macd', async () => { 19 | const strategyManager = new StrategyManager(createTechnicalAnalysisValidator(), createCandlestickRepository()); 20 | 21 | const result = await strategyManager.executeStrategy('macd', createStrategyContext(), 'foobar', 'BTCUSD', { 22 | period: '15m' 23 | }); 24 | assert.equal(undefined, result.signal); 25 | }); 26 | 27 | let createCandlestickRepository = () => { 28 | return { 29 | fetchCombinedCandles: async exchange => { 30 | return { 31 | [exchange]: createCandleFixtures() 32 | }; 33 | } 34 | }; 35 | }; 36 | 37 | let createCandleFixtures = () => { 38 | return JSON.parse(fs.readFileSync(`${__dirname}/../../../utils/fixtures/xbt-usd-5m.json`, 'utf8')); 39 | }; 40 | 41 | let createStrategyContext = () => { 42 | return new StrategyContext({}, new Ticker('goo', 'goo', 'goo', 6000, 6000)); 43 | }; 44 | 45 | let createTechnicalAnalysisValidator = () => { 46 | const technicalAnalysisValidator = new TechnicalAnalysisValidator(); 47 | 48 | technicalAnalysisValidator.isValidCandleStickLookback = function() { 49 | return true; 50 | }; 51 | 52 | return technicalAnalysisValidator; 53 | }; 54 | }); 55 | -------------------------------------------------------------------------------- /test/storage/tickers.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const moment = require('moment'); 3 | const Tickers = require('../../src/storage/tickers'); 4 | const Ticker = require('../../src/dict/ticker'); 5 | 6 | describe('#tickers', function() { 7 | it('test getting update tickers', () => { 8 | const tickers = new Tickers(); 9 | const ticker = new Ticker('foobar', 'BTCUSD', 1234, 1337, 1338); 10 | 11 | tickers.set(ticker); 12 | ticker.createdAt = moment() 13 | .subtract(5000, 'ms') 14 | .toDate(); 15 | 16 | assert.equal(tickers.get('foobar', 'BTCUSD').ask, 1338); 17 | assert.equal(tickers.getIfUpToDate('foobar', 'BTCUSD', 1000), undefined); 18 | 19 | assert.equal(tickers.getIfUpToDate('foobar', 'BTCUSD', 7000).ask, 1338); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /test/system/system_util.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const SystemUtil = require('../../src/modules/system/system_util'); 3 | 4 | describe('#system util test', function() { 5 | it('test configuration extraction', () => { 6 | const systemUtil = new SystemUtil({ 7 | root: 'test123', 8 | root2: undefined, 9 | webserver: { 10 | test: 8080 11 | } 12 | }); 13 | 14 | assert.equal(systemUtil.getConfig('webserver.test'), 8080); 15 | assert.equal(systemUtil.getConfig('root'), 'test123'); 16 | assert.equal(systemUtil.getConfig('UNKONWN', 'test'), 'test'); 17 | assert.equal(systemUtil.getConfig('UNKONWN'), undefined); 18 | assert.equal(systemUtil.getConfig('root2', 'test'), 'test'); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /test/utils/fixtures/pattern/volume_pump_BNBUSDT.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Haehnchen/crypto-trading-bot/a5d92b153c6cbe8770a28c693309640bb30cfea1/test/utils/fixtures/pattern/volume_pump_BNBUSDT.png -------------------------------------------------------------------------------- /test/utils/order_util.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const orderUtil = require('../../src/utils/order_util'); 3 | const Position = require('../../src/dict/position'); 4 | const ExchangeOrder = require('../../src/dict/exchange_order'); 5 | 6 | describe('#order util', function() { 7 | it('calculate order amount', () => { 8 | assert.equal(0.0154012, orderUtil.calculateOrderAmount(6493, 100).toFixed(8)); 9 | }); 10 | 11 | it('sync stoploss exchange order (long)', () => { 12 | const position = new Position('LTCUSD', 'long', 4, 0, new Date()); 13 | 14 | // stop loss create 15 | assert.deepEqual([{ amount: 4 }], orderUtil.syncStopLossOrder(position, [])); 16 | 17 | // stop loss update 18 | assert.deepEqual( 19 | [{ id: 'foobar', amount: 4 }], 20 | orderUtil.syncStopLossOrder(position, [ 21 | new ExchangeOrder('foobar', 'BTUSD', 'open', 1337, 3, false, 'our_id', 'buy', 'stop') 22 | ]) 23 | ); 24 | 25 | // stop loss: missing value 26 | assert.deepEqual( 27 | [{ id: 'foobar', amount: 4 }], 28 | orderUtil.syncStopLossOrder(position, [ 29 | new ExchangeOrder('foobar', 'BTUSD', 'open', 1337, 3, false, 'our_id', 'buy', 'limit'), 30 | new ExchangeOrder('foobar', 'BTUSD', 'open', 1337, -2, false, 'our_id', 'buy', 'stop') 31 | ]) 32 | ); 33 | 34 | assert.deepEqual( 35 | [{ id: 'foobar', amount: 4 }], 36 | orderUtil.syncStopLossOrder(position, [ 37 | new ExchangeOrder('foobar', 'BTUSD', 'open', 1337, 3, false, 'our_id', 'buy', 'limit'), 38 | new ExchangeOrder('foobar', 'BTUSD', 'open', 1337, -5, false, 'our_id', 'buy', 'stop') 39 | ]) 40 | ); 41 | 42 | // stop loss correct 43 | assert.deepEqual( 44 | [], 45 | orderUtil.syncStopLossOrder(position, [ 46 | new ExchangeOrder('foobar', 'BTUSD', 'open', 1337, 3, false, 'our_id', 'buy', 'limit'), 47 | new ExchangeOrder('foobar', 'BTUSD', 'open', 1337, -4, false, 'our_id', 'buy', 'stop') 48 | ]) 49 | ); 50 | }); 51 | 52 | it('sync stoploss exchange order (short)', () => { 53 | const position = new Position('LTCUSD', 'short', -4, 0, new Date()); 54 | 55 | // stop loss update 56 | assert.deepEqual( 57 | [], 58 | orderUtil.syncStopLossOrder(position, [ 59 | new ExchangeOrder('foobar', 'BTUSD', 'open', 1337, 4, false, 'our_id', 'buy', 'stop') 60 | ]) 61 | ); 62 | 63 | // stop loss create 64 | assert.deepEqual([{ amount: 4 }], orderUtil.syncStopLossOrder(position, [])); 65 | }); 66 | 67 | it('calculate increment size', () => { 68 | assert.equal(orderUtil.calculateNearestSize(0.0085696, 0.00001), 0.00856); 69 | assert.equal(orderUtil.calculateNearestSize(50.55, 2.5), 50); 70 | 71 | assert.equal(orderUtil.calculateNearestSize(50.22, 1), 50); 72 | assert.equal(orderUtil.calculateNearestSize(50.88, 1), 50); 73 | 74 | assert.equal(orderUtil.calculateNearestSize(-149.87974, 0.01), -149.87); 75 | }); 76 | 77 | it('calculate percent change', () => { 78 | assert.strictEqual(50, orderUtil.getPercentDifferent(0.5, 1)); 79 | assert.strictEqual(50, orderUtil.getPercentDifferent(1, 0.5)); 80 | 81 | assert.strictEqual('1.20', orderUtil.getPercentDifferent(0.004036, 0.004085).toFixed(2)); 82 | assert.strictEqual('1.20', orderUtil.getPercentDifferent(0.004085, 0.004036).toFixed(2)); 83 | }); 84 | }); 85 | -------------------------------------------------------------------------------- /test/utils/request_client.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const RequestClient = require('../../src/utils/request_client'); 3 | 4 | describe('#request client', function() { 5 | it('test that retry is running with a final result', async () => { 6 | const client = new RequestClient({ error: () => {} }); 7 | 8 | const responses = []; 9 | 10 | for (let retry = 0; retry < 2; retry++) { 11 | responses.push({ 12 | error: undefined, 13 | response: { statusCode: 503 }, 14 | body: retry 15 | }); 16 | } 17 | 18 | responses.push({ 19 | error: undefined, 20 | response: { statusCode: 200 }, 21 | body: 'yes' 22 | }); 23 | 24 | let i = 0; 25 | client.executeRequest = () => { 26 | return new Promise(resolve => { 27 | resolve(responses[i++]); 28 | }); 29 | }; 30 | 31 | const result = await client.executeRequestRetry( 32 | {}, 33 | result => { 34 | return result && result.response && result.response.statusCode === 503; 35 | }, 36 | 5 37 | ); 38 | 39 | assert.equal(i, 3); 40 | assert.equal(result.body, 'yes'); 41 | }); 42 | 43 | it('test that retry limit is reached', async () => { 44 | const client = new RequestClient({ error: () => {} }); 45 | 46 | const responses = []; 47 | 48 | for (let retry = 0; retry < 15; retry++) { 49 | responses.push({ 50 | error: undefined, 51 | response: { statusCode: 503 }, 52 | body: retry 53 | }); 54 | } 55 | 56 | let i = 0; 57 | client.executeRequest = () => { 58 | return new Promise(resolve => { 59 | resolve(responses[i++]); 60 | }); 61 | }; 62 | 63 | const result = await client.executeRequestRetry( 64 | { url: 'http://test.de' }, 65 | result => { 66 | return result && result.response && result.response.statusCode === 503; 67 | }, 68 | 5 69 | ); 70 | 71 | assert.equal(i, 10); 72 | assert.equal(result.body, '9'); 73 | assert.equal(result.response.statusCode, 503); 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /test/utils/resample.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const fs = require('fs'); 3 | const Resample = require('../../src/utils/resample'); 4 | 5 | describe('#resample of candles', () => { 6 | it('should resample 1 hour candles', () => { 7 | const candles = Resample.resampleMinutes(createCandleFixtures(), 60); 8 | 9 | const firstFullCandle = candles[1]; 10 | 11 | assert.equal(12, firstFullCandle._candle_count); 12 | 13 | assert.equal(firstFullCandle.time, 1533142800); 14 | assert.equal(firstFullCandle.open, 7600); 15 | assert.equal(firstFullCandle.high, 7609.5); 16 | assert.equal(firstFullCandle.low, 7530); 17 | assert.equal(firstFullCandle.close, 7561.5); 18 | assert.equal(firstFullCandle.volume, 174464214); 19 | 20 | assert.equal(candles[2].time, 1533139200); 21 | }); 22 | 23 | it('should resample 15m candles', () => { 24 | const candles = Resample.resampleMinutes(createCandleFixtures(), 15); 25 | 26 | const firstFullCandle = candles[1]; 27 | 28 | assert.equal(3, firstFullCandle._candle_count); 29 | 30 | assert.equal(firstFullCandle.time, 1533142800); 31 | assert.equal(firstFullCandle.open, 7547.5); 32 | assert.equal(firstFullCandle.high, 7562); 33 | assert.equal(firstFullCandle.low, 7530); 34 | assert.equal(firstFullCandle.close, 7561.5); 35 | assert.equal(firstFullCandle.volume, 45596804); 36 | 37 | assert.equal(candles[2].time, 1533141900); 38 | }); 39 | 40 | it('should format period based on unit', () => { 41 | assert.strictEqual(Resample.convertPeriodToMinute('15m'), 15); 42 | assert.strictEqual(Resample.convertPeriodToMinute('30M'), 30); 43 | assert.strictEqual(Resample.convertPeriodToMinute('1H'), 60); 44 | assert.strictEqual(Resample.convertPeriodToMinute('2h'), 120); 45 | assert.strictEqual(Resample.convertPeriodToMinute('1w'), 10080); 46 | assert.strictEqual(Resample.convertPeriodToMinute('2w'), 20160); 47 | assert.strictEqual(Resample.convertPeriodToMinute('1y'), 3588480); 48 | }); 49 | 50 | it('test that resample starting time is matching given candle lookback', () => { 51 | const candles = []; 52 | 53 | // 2014-02-27T09:30:00.000Z 54 | const start = 1393493400; 55 | 56 | for (let i = 1; i < 23; i++) { 57 | candles.push({ 58 | time: start - 15 * i * 60, 59 | volume: i * 100, 60 | open: i * 2, 61 | close: i * 2.1, 62 | high: i * 1.1, 63 | low: i * 0.9 64 | }); 65 | } 66 | 67 | const resampleCandles = Resample.resampleMinutes(candles, 60); 68 | 69 | assert.equal(new Date(resampleCandles[0].time * 1000).getUTCHours(), 10); 70 | 71 | const firstFullCandle = resampleCandles[1]; 72 | assert.equal(firstFullCandle._candle_count, 4); 73 | assert.equal(firstFullCandle.time, 1393491600); 74 | 75 | assert.equal(resampleCandles.length, 6); 76 | 77 | assert.equal(resampleCandles[0].time, 1393495200); 78 | assert.equal(resampleCandles[4].time, 1393480800); 79 | assert.equal(resampleCandles[4]._candle_count, 4); 80 | }); 81 | 82 | let createCandleFixtures = function() { 83 | return JSON.parse(fs.readFileSync(`${__dirname}/fixtures/xbt-usd-5m.json`, 'utf8')); 84 | }; 85 | }); 86 | -------------------------------------------------------------------------------- /test/utils/technical_analysis_validator.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const moment = require('moment'); 3 | const TechnicalAnalysisValidator = require('../../src/utils/technical_analysis_validator'); 4 | 5 | describe('#technical analysis validation for candles', () => { 6 | it('test that last candle is up to date', async () => { 7 | const result = new TechnicalAnalysisValidator().isValidCandleStickLookback( 8 | [ 9 | { 10 | time: moment() 11 | .minute(Math.floor(moment().minute() / 15) * 15) 12 | .second(0) 13 | .unix() 14 | }, 15 | { 16 | time: moment() 17 | .minute(Math.floor(moment().minute() / 15) * 15) 18 | .second(0) 19 | .subtract(15, 'minutes') 20 | .unix() 21 | } 22 | ], 23 | '15m' 24 | ); 25 | 26 | assert.equal(result, true); 27 | }); 28 | 29 | it('test that last candle is outdated', async () => { 30 | const result = new TechnicalAnalysisValidator().isValidCandleStickLookback( 31 | [ 32 | { 33 | time: moment() 34 | .minute(Math.floor(moment().minute() / 15) * 15) 35 | .second(0) 36 | .subtract(1, 'hour') 37 | .unix() 38 | }, 39 | { 40 | time: moment() 41 | .minute(Math.floor(moment().minute() / 15) * 15) 42 | .second(0) 43 | .subtract(1, 'hour') 44 | .subtract(15, 'minutes') 45 | .unix() 46 | } 47 | ], 48 | '15m' 49 | ); 50 | 51 | assert.equal(result, false); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /test/utils/technical_pattern.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const fs = require('fs'); 3 | const TechnicalPattern = require('../../src/utils/technical_pattern'); 4 | 5 | describe('#technical pattern', () => { 6 | it('pump it with volume', () => { 7 | const candles = createCandleFixtures() 8 | .slice() 9 | .reverse(); 10 | 11 | const results = []; 12 | for (let i = 40; i < candles.length; i++) { 13 | results.push(TechnicalPattern.volumePump(candles.slice(0, i))); 14 | } 15 | 16 | const success = results.filter(r => r.hint === 'success'); 17 | 18 | assert.equal(success[0].price_trigger.toFixed(3), 15.022); 19 | }); 20 | 21 | let createCandleFixtures = function() { 22 | return JSON.parse(fs.readFileSync(`${__dirname}/fixtures/pattern/volume_pump_BNBUSDT.json`, 'utf8')); 23 | }; 24 | }); 25 | -------------------------------------------------------------------------------- /var/log/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /var/strategies/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | !README.md 4 | -------------------------------------------------------------------------------- /var/strategies/README.md: -------------------------------------------------------------------------------- 1 | # Strategy folder 2 | 3 | Put in your custom strategies here 4 | 5 | see `modules/strategy/strategies` for examples -------------------------------------------------------------------------------- /web/static/css/backtest.css: -------------------------------------------------------------------------------- 1 | path.candle { 2 | stroke: #000000; 3 | } 4 | 5 | path.candle.body { 6 | stroke-width: 0; 7 | } 8 | 9 | path.candle.up { 10 | fill: #00AA00; 11 | stroke: #00AA00; 12 | } 13 | 14 | path.candle.down { 15 | fill: #FF0000; 16 | stroke: #FF0000; 17 | } 18 | 19 | path.tradearrow { 20 | stroke: none; 21 | opacity: 1; 22 | } 23 | 24 | path.volume { 25 | fill: #cccccc; 26 | } 27 | 28 | path.tradearrow.long { 29 | fill: #00AA00; 30 | } 31 | 32 | path.tradearrow.short { 33 | fill: #FF0000; 34 | } 35 | 36 | path.tradearrow.close { 37 | fill: black; 38 | } 39 | 40 | .tradearrow path.highlight { 41 | fill: none; 42 | stroke-width: 2; 43 | } 44 | 45 | .tradearrow path.highlight.long { 46 | stroke: #01f015; 47 | } 48 | 49 | .tradearrow path.highlight.short { 50 | stroke: #ff0121; 51 | } 52 | 53 | .tradearrow path.highlight.close { 54 | fill: black; 55 | } 56 | 57 | .crosshair { 58 | cursor: crosshair; 59 | } 60 | 61 | .crosshair path.wire { 62 | stroke: #DDDDDD; 63 | stroke-dasharray: 1, 1; 64 | } 65 | 66 | .crosshair .axisannotation path { 67 | fill: #DDDDDD; 68 | } 69 | 70 | .close.annotation { 71 | font-size: 0.8em; 72 | color: black; 73 | text-shadow: none; 74 | opacity: 1; 75 | font-weight: normal; 76 | } 77 | 78 | .close.annotation.up path { 79 | fill: #00AA00; 80 | } 81 | 82 | .backtest-table .debug-toggle a.button-debug-toggle { 83 | display:none; 84 | } 85 | 86 | .backtest-table .debug-toggle.hide a.button-debug-toggle { 87 | display:inline; 88 | } 89 | 90 | 91 | .backtest-table .debug-toggle .debug-text { 92 | display:inline; 93 | } 94 | 95 | .backtest-table .debug-toggle.hide .debug-text { 96 | display:none; 97 | } 98 | 99 | -------------------------------------------------------------------------------- /web/static/css/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-size: 0.9rem; 3 | } 4 | 5 | nav { 6 | font-size: initial; 7 | } 8 | 9 | .content-wrapper > .content { 10 | padding: 0; 11 | } 12 | 13 | /* Medium devices (tablets, 768px and up) */ 14 | @media (min-width: 768px) { 15 | .span-md { 16 | word-break: break-all; 17 | } 18 | } 19 | 20 | 21 | /*base*/ 22 | .text-orange { 23 | color: #b35900 24 | } 25 | 26 | .trend-up { 27 | transform: rotate(45deg); 28 | } 29 | 30 | .trend-down { 31 | transform: rotate(135deg); 32 | } 33 | 34 | @keyframes blink { 35 | 50% { 36 | opacity: 0.5; 37 | } 38 | } 39 | 40 | .blink { 41 | animation: blink 1s step-start 0s infinite; 42 | animation-timing-function: ease-in-out; 43 | } 44 | 45 | .period-start { 46 | border-left: 1px solid #dee2e6 47 | } 48 | 49 | .th-hidden { 50 | border: 0 !important; 51 | -webkit-user-select: none; 52 | -moz-user-select: none; 53 | -ms-user-select: none; 54 | user-select: none; 55 | } 56 | /*end base*/ 57 | 58 | .col-rotate { 59 | vertical-align:bottom; 60 | } 61 | 62 | .col-rotate span { 63 | writing-mode: vertical-rl; 64 | transform: rotate(180deg); 65 | display: inline-block; 66 | } 67 | 68 | .no-wrap { 69 | white-space: nowrap; 70 | } 71 | -------------------------------------------------------------------------------- /web/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Haehnchen/crypto-trading-bot/a5d92b153c6cbe8770a28c693309640bb30cfea1/web/static/favicon.ico -------------------------------------------------------------------------------- /web/static/img/exchanges/binance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Haehnchen/crypto-trading-bot/a5d92b153c6cbe8770a28c693309640bb30cfea1/web/static/img/exchanges/binance.png -------------------------------------------------------------------------------- /web/static/img/exchanges/binance_futures.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Haehnchen/crypto-trading-bot/a5d92b153c6cbe8770a28c693309640bb30cfea1/web/static/img/exchanges/binance_futures.png -------------------------------------------------------------------------------- /web/static/img/exchanges/binance_margin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Haehnchen/crypto-trading-bot/a5d92b153c6cbe8770a28c693309640bb30cfea1/web/static/img/exchanges/binance_margin.png -------------------------------------------------------------------------------- /web/static/img/exchanges/bitfinex.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Haehnchen/crypto-trading-bot/a5d92b153c6cbe8770a28c693309640bb30cfea1/web/static/img/exchanges/bitfinex.png -------------------------------------------------------------------------------- /web/static/img/exchanges/bitmex.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Haehnchen/crypto-trading-bot/a5d92b153c6cbe8770a28c693309640bb30cfea1/web/static/img/exchanges/bitmex.png -------------------------------------------------------------------------------- /web/static/img/exchanges/bybit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Haehnchen/crypto-trading-bot/a5d92b153c6cbe8770a28c693309640bb30cfea1/web/static/img/exchanges/bybit.png -------------------------------------------------------------------------------- /web/static/img/exchanges/bybit_linear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Haehnchen/crypto-trading-bot/a5d92b153c6cbe8770a28c693309640bb30cfea1/web/static/img/exchanges/bybit_linear.png -------------------------------------------------------------------------------- /web/static/img/exchanges/bybit_unified.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Haehnchen/crypto-trading-bot/a5d92b153c6cbe8770a28c693309640bb30cfea1/web/static/img/exchanges/bybit_unified.png -------------------------------------------------------------------------------- /web/static/img/exchanges/coinbase_pro.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Haehnchen/crypto-trading-bot/a5d92b153c6cbe8770a28c693309640bb30cfea1/web/static/img/exchanges/coinbase_pro.png -------------------------------------------------------------------------------- /web/static/img/exchanges/ftx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Haehnchen/crypto-trading-bot/a5d92b153c6cbe8770a28c693309640bb30cfea1/web/static/img/exchanges/ftx.png -------------------------------------------------------------------------------- /web/static/js/backtest-form.js: -------------------------------------------------------------------------------- 1 | $(function() { 2 | $('form.backtest-form #form-strategies').change(function() { 3 | // get data as string 4 | const options = $(this) 5 | .find('option:selected') 6 | .attr('data-options'); 7 | 8 | if (options) { 9 | $(this) 10 | .closest('form') 11 | .find('#form-options') 12 | .val(options); 13 | } 14 | }); 15 | 16 | $('.chosen-select').chosen(); 17 | 18 | $('form.backtest-form #form-pair').change(function() { 19 | // get data as string 20 | const options = $(this) 21 | .find('option:selected') 22 | .attr('data-options'); 23 | 24 | if (options) { 25 | const optionTag = $(this) 26 | .closest('form') 27 | .find('#form-candle-period'); 28 | 29 | optionTag.html(''); 30 | $.each(JSON.parse(options), function(key, value) { 31 | optionTag.append($('