├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── dependabot.yml └── workflows │ └── codeql-analysis.yml ├── .dockerignore ├── .env ├── Dockerfile ├── .gitignore ├── utils ├── LINE.js └── index.js ├── databases └── bitkub.js ├── docker-compose.yml ├── LICENSE ├── package.json ├── models ├── TradingViewLogs.js └── TradingLogs.js ├── app.js ├── database.sql ├── CODE_OF_CONDUCT.md ├── routes └── tradingview.js └── README.md /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | custom: https://commerce.coinbase.com/checkout/c0ce98d2-0093-4dc3-b075-3cf801d46885 3 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .env 3 | .git 4 | docker-compose.yml 5 | database.sql 6 | .gitignore 7 | .Dockerfile 8 | .github 9 | .dockerignore 10 | README.md 11 | LICENSE 12 | Dockerfile 13 | CODE_OF_CONDUCT.md -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | NODE_ENV=Development 2 | API_KEY= 3 | API_SECRET= 4 | DB_SQL_HOST= 5 | DB_SQL_PORT=3306 6 | DB_SQL_DB= 7 | DB_SQL_USER= 8 | DB_SQL_PASS= 9 | SERVER_PORT=3000 10 | BITKUB_ROOT_URL=https://api.bitkub.com 11 | LINE_TOKEN= 12 | BUY_RATIO=0.01 13 | SELL_RATIO=1 -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:12 2 | 3 | # Create app directory 4 | WORKDIR /usr/src/app 5 | 6 | # Install app dependencies 7 | # A wildcard is used to ensure both package.json AND package-lock.json are copied 8 | # where available (npm@5+) 9 | COPY package*.json ./ 10 | 11 | RUN npm install 12 | # If you are building your code for production 13 | # RUN npm ci --only=production 14 | 15 | # Bundle app source 16 | COPY . . 17 | 18 | EXPOSE 3000 19 | CMD [ "npm", "run", "start" ] 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "22:00" 8 | open-pull-requests-limit: 10 9 | reviewers: 10 | - nanmcpe 11 | assignees: 12 | - nanmcpe 13 | ignore: 14 | - dependency-name: date-fns 15 | versions: 16 | - 2.17.0 17 | - 2.18.0 18 | - 2.20.0 19 | - 2.20.1 20 | - 2.20.2 21 | - 2.20.3 22 | - 2.21.0 23 | - dependency-name: console-stamp 24 | versions: 25 | - 3.0.1 26 | - dependency-name: sequelize 27 | versions: 28 | - 6.5.0 29 | - 6.5.1 30 | - 6.6.1 31 | - dependency-name: qs 32 | versions: 33 | - 6.10.0 34 | - 6.9.6 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | .DS_Store 5 | 6 | npm-debug.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | 13 | # Directory for instrumented libs generated by jscoverage/JSCover 14 | lib-cov 15 | 16 | # Coverage directory used by tools like istanbul 17 | coverage 18 | 19 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 20 | .grunt 21 | 22 | # node-waf configuration 23 | .lock-wscript 24 | 25 | # Compiled binary addons (http://nodejs.org/api/addons.html) 26 | build/Release 27 | 28 | # Dependency directory 29 | node_modules 30 | 31 | # Optional npm cache directory 32 | .npm 33 | 34 | # Optional REPL history 35 | .node_repl_history 36 | 37 | .idea 38 | 39 | 40 | -------------------------------------------------------------------------------- /utils/LINE.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | const qs = require('qs'); 3 | 4 | function LINE(message) { 5 | return new Promise(async (resolve, reject) => { 6 | console.log('Sent notification', message); 7 | try { 8 | const response = await axios({ 9 | method: 'post', 10 | url: 'https://notify-api.line.me/api/notify', 11 | headers: { 12 | 'Authorization': 'Bearer ' + process.env.LINE_TOKEN, 13 | 'Content-Type': 'application/x-www-form-urlencoded', 14 | }, 15 | data: qs.stringify({ message }) 16 | }).then(res => res.data);; 17 | resolve(response); 18 | } catch (err) { 19 | reject(err); 20 | } 21 | }); 22 | }; 23 | 24 | module.exports = { 25 | LINE 26 | }; 27 | -------------------------------------------------------------------------------- /databases/bitkub.js: -------------------------------------------------------------------------------- 1 | const Sequelize = require('sequelize'); 2 | const sequelize = new Sequelize(process.env.DB_SQL_DB, process.env.DB_SQL_USER, process.env.DB_SQL_PASS, { 3 | host: process.env.DB_SQL_HOST, 4 | port: process.env.DB_SQL_PORT, 5 | dialect: 'mariadb', 6 | pool: { 7 | max: 5, 8 | min: 0, 9 | acquire: 30000, 10 | idle: 10000 11 | }, 12 | dialectOptions: { 13 | collate: 'utf8_general_ci', 14 | useUTC: false, 15 | timezone: 'Etc/GMT+7' 16 | }, 17 | timezone: 'Etc/GMT+7', 18 | logging: false, 19 | query: { raw: true }, 20 | }); 21 | 22 | const db = {}; 23 | db.Sequelize = Sequelize; 24 | db.sequelize = sequelize; 25 | 26 | //import model 27 | db.TradingViewLogs = require('../models/TradingViewLogs')(sequelize, Sequelize); 28 | db.TradingLogs = require('../models/TradingLogs')(sequelize, Sequelize); 29 | module.exports = db; -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.7" 2 | services: 3 | bitkub-optimize: 4 | image: /bitkub-optimize:latest 5 | container_name: bitkub-optimize 6 | build: . 7 | environment: 8 | - NODE_ENV=Production 9 | - TZ=Asia/Bangkok 10 | - API_KEY= 11 | - API_SECRET= 12 | - DB_SQL_HOST= 13 | - DB_SQL_PORT=3306 14 | - DB_SQL_DB= 15 | - DB_SQL_USER= 16 | - DB_SQL_PASS= 17 | - SERVER_PORT=3000 18 | - BITKUB_ROOT_URL=https://api.bitkub.com 19 | - LINE_TOKEN= 20 | - BUY_RATIO=0.9 21 | - SELL_RATIO=1 22 | networks: 23 | internal-network: 24 | ipv4_address: 172.50.0.2 25 | restart: always 26 | 27 | # proxy_pass from nginx to this network 28 | networks: 29 | internal-network: 30 | external: 31 | name: internal-network 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Nan Thanwa 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bitkub-optimize", 3 | "version": "0.0.3", 4 | "description": "Node.JS trading bot for Bitkub.com using TradingView webhook", 5 | "main": "app.js", 6 | "dependencies": { 7 | "axios": "^1.8.2", 8 | "body-parser": "^1.20.0", 9 | "console-stamp": "^3.0.6", 10 | "cors": "^2.8.5", 11 | "date-fns": "^2.29.3", 12 | "dotenv": "^16.4.7", 13 | "express": "^4.21.2", 14 | "mariadb": "^3.0.0", 15 | "morgan": "^1.10.0", 16 | "qs": "^6.14.0", 17 | "sequelize": "^6.29.3" 18 | }, 19 | "devDependencies": {}, 20 | "scripts": { 21 | "test": "npm run test", 22 | "start": "node app.js" 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "git+https://github.com/nanmcpe/bitkub-optimize.git" 27 | }, 28 | "keywords": [ 29 | "cryptocurrentcy", 30 | "trading", 31 | "bot", 32 | "bitkub", 33 | "webhook" 34 | ], 35 | "author": "Thanwa Jindarattana", 36 | "license": "MIT", 37 | "bugs": { 38 | "url": "https://github.com/nanmcpe/bitkub-optimize/issues" 39 | }, 40 | "homepage": "https://github.com/nanmcpe/bitkub-optimize#readme" 41 | } 42 | -------------------------------------------------------------------------------- /models/TradingViewLogs.js: -------------------------------------------------------------------------------- 1 | module.exports = (sequelize, Sequelize) => { 2 | const TradingViewLogs = sequelize.define( 3 | 'log_tradingview', 4 | { 5 | id: { 6 | type: Sequelize.INTEGER, 7 | field: 'ID', 8 | autoIncrement: true, 9 | primaryKey: true, 10 | }, 11 | timestamp: { 12 | type: Sequelize.DATE, 13 | field: 'Timestamp' 14 | }, 15 | exchange: { 16 | type: Sequelize.STRING, 17 | field: 'Exchange' 18 | }, 19 | ticker: { 20 | type: Sequelize.STRING, 21 | field: 'Ticker' 22 | }, 23 | tf: { 24 | type: Sequelize.ENUM('4h', '6h', '8h', '12h', '1d'), 25 | field: 'Timeframe' 26 | }, 27 | side: { 28 | type: Sequelize.ENUM('buy', 'sell'), 29 | field: 'Side' 30 | }, 31 | open: { 32 | type: Sequelize.NUMBER, 33 | field: 'Open' 34 | }, 35 | close: { 36 | type: Sequelize.NUMBER, 37 | field: 'Close' 38 | }, 39 | high: { 40 | type: Sequelize.NUMBER, 41 | field: 'High' 42 | }, 43 | low: { 44 | type: Sequelize.NUMBER, 45 | field: 'Low' 46 | }, 47 | volume: { 48 | type: Sequelize.NUMBER, 49 | field: 'Volume' 50 | }, 51 | }, 52 | { 53 | timestamps: false, 54 | freezeTableName: true 55 | } 56 | ); 57 | return TradingViewLogs; 58 | }; -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | const cors = require('cors'); 3 | const bodyParser = require('body-parser'); 4 | const express = require('express'); 5 | const { format } = require('date-fns'); 6 | const app = express(); 7 | const { sequelize } = require('./databases/bitkub'); 8 | const morgan = require('morgan'); 9 | const { LINE } = require('./utils/LINE'); 10 | 11 | require('console-stamp')(console, { 12 | pattern: 'dd/mm/yyyy HH:MM:ss.l', 13 | colors: { 14 | stamp: 'green', 15 | label: 'white', 16 | } 17 | }); 18 | 19 | const isProduction = process.env.NODE_ENV === 'Production'; 20 | 21 | if (isProduction) { 22 | // Normal express config defaults 23 | morgan.token('datetime', () => format(new Date(), 'dd/MM/yyyy HH:mm:ss.SSS')); 24 | morgan.token('remoteIP', (req) => req.headers['x-forwarded-for']); // for display real host instead of container host, need to add more header in nginx 25 | app.use(morgan('[:datetime] [:method] :url :remoteIP :status :response-time ms - :res[content-length]')); 26 | } else { 27 | app.use(cors({ 28 | origin: ['http://localhost', 'http://localhost:3000'], 29 | optionsSuccessStatus: 200, 30 | credentials: true, 31 | })); 32 | app.use(morgan('dev')); 33 | } 34 | 35 | app.use(bodyParser.json()); 36 | app.use(bodyParser.urlencoded({ extended: true })); 37 | app.use(bodyParser.text()); 38 | 39 | app.use('/api', require('./routes/tradingview')); 40 | 41 | app.listen(process.env.SERVER_PORT || 3000, async () => { 42 | try { 43 | await sequelize.authenticate(); 44 | console.log('Database connection has been established successfully.'); 45 | console.log(`Server has started at port ${process.env.SERVER_PORT}`); 46 | if (isProduction) LINE(`Server has started at port ${process.env.SERVER_PORT}`); 47 | } catch (error) { 48 | console.error('Unable to connect to the database:', error); 49 | process.exit(1); 50 | } 51 | }); 52 | -------------------------------------------------------------------------------- /models/TradingLogs.js: -------------------------------------------------------------------------------- 1 | module.exports = (sequelize, Sequelize) => { 2 | const TradingLogs = sequelize.define( 3 | 'log_trading', 4 | { 5 | id: { 6 | type: Sequelize.INTEGER, 7 | field: 'ID', 8 | primaryKey: true, 9 | }, 10 | hash: { 11 | type: Sequelize.STRING, 12 | field: 'Hash' 13 | }, 14 | sym: { 15 | type: Sequelize.ENUM('THB_BTC', 'THB_ETH', 'THB_WAN', 'THB_ADA', 'THB_OMG', 'THB_BCH', 'THB_USDT', 'THB_LTC', 'THB_XRP', 'THB_BSV', 'THB_ZIL', 'THB_SNT', 'THB_CVC', 'THB_LINK', 'THB_GNT', 'THB_IOST', 'THB_ZRX', 'THB_KNC', 'THB_ENG', 'THB_RDN', 'THB_ABT', 'THB_MANA', 'THB_INF', 'THB_CTXC', 'THB_XLM', 'THB_SIX', 'THB_JFIN', 'THB_EVX', 'THB_BNB', 'THB_POW', 'THB_DOGE', 'THB_DAI', 'THB_USDC', 'THB_BAT', 'THB_BAND', 'THB_KSM', 'THB_DOT', 'THB_NEAR'), 16 | field: 'Symbol' 17 | }, 18 | side: { 19 | type: Sequelize.ENUM('buy', 'sell'), 20 | field: 'Side' 21 | }, 22 | typ: { 23 | type: Sequelize.ENUM('limit', 'market'), 24 | field: 'Type' 25 | }, 26 | amt: { 27 | type: Sequelize.NUMBER, 28 | field: 'Amount' 29 | }, 30 | rat: { 31 | type: Sequelize.NUMBER, 32 | field: 'Rate' 33 | }, 34 | fee: { 35 | type: Sequelize.NUMBER, 36 | field: 'Fee' 37 | }, 38 | cre: { 39 | type: Sequelize.NUMBER, 40 | field: 'Credit' 41 | }, 42 | rec: { 43 | type: Sequelize.NUMBER, 44 | field: 'Receive' 45 | }, 46 | ts: { 47 | type: Sequelize.DATE, 48 | field: 'Timestamp' 49 | }, 50 | }, 51 | { 52 | timestamps: false, 53 | freezeTableName: true 54 | } 55 | ); 56 | return TradingLogs; 57 | }; -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ main ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ main ] 20 | schedule: 21 | - cron: '29 3 * * 3' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | 28 | strategy: 29 | fail-fast: false 30 | matrix: 31 | language: [ 'javascript' ] 32 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 33 | # Learn more: 34 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 35 | 36 | steps: 37 | - name: Checkout repository 38 | uses: actions/checkout@v2 39 | 40 | # Initializes the CodeQL tools for scanning. 41 | - name: Initialize CodeQL 42 | uses: github/codeql-action/init@v1 43 | with: 44 | languages: ${{ matrix.language }} 45 | # If you wish to specify custom queries, you can do so here or in a config file. 46 | # By default, queries listed here will override any specified in a config file. 47 | # Prefix the list here with "+" to use these queries and those in the config file. 48 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 49 | 50 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 51 | # If this step fails, then you should remove it and run the build manually (see below) 52 | - name: Autobuild 53 | uses: github/codeql-action/autobuild@v1 54 | 55 | # ℹ️ Command-line programs to run using the OS shell. 56 | # 📚 https://git.io/JvXDl 57 | 58 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 59 | # and modify them (or add more) to build your code if your project 60 | # uses a compiled language 61 | 62 | #- run: | 63 | # make bootstrap 64 | # make release 65 | 66 | - name: Perform CodeQL Analysis 67 | uses: github/codeql-action/analyze@v1 68 | -------------------------------------------------------------------------------- /database.sql: -------------------------------------------------------------------------------- 1 | -- phpMyAdmin SQL Dump 2 | -- version 5.0.4 3 | -- https://www.phpmyadmin.net/ 4 | -- 5 | -- Host: x.x.x.x:3306 6 | -- Generation Time: Dec 09, 2020 at 12:22 PM 7 | -- Server version: 10.3.27-MariaDB 8 | -- PHP Version: 7.4.11 9 | 10 | SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO"; 11 | START TRANSACTION; 12 | SET time_zone = "+00:00"; 13 | 14 | 15 | /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; 16 | /*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; 17 | /*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; 18 | /*!40101 SET NAMES utf8mb4 */; 19 | 20 | -- 21 | -- Database: `bitkub` 22 | -- 23 | 24 | -- -------------------------------------------------------- 25 | 26 | -- 27 | -- Table structure for table `log_trading` 28 | -- 29 | 30 | CREATE TABLE `log_trading` ( 31 | `ID` bigint(50) NOT NULL, 32 | `Hash` varchar(30) NOT NULL, 33 | `Symbol` enum('THB_BTC','THB_ETH','THB_WAN','THB_ADA','THB_OMG','THB_BCH','THB_USDT','THB_LTC','THB_XRP','THB_BSV','THB_ZIL','THB_SNT','THB_CVC','THB_LINK','THB_GNT','THB_IOST','THB_ZRX','THB_KNC','THB_ENG','THB_RDN','THB_ABT','THB_MANA','THB_INF','THB_CTXC','THB_XLM','THB_SIX','THB_JFIN','THB_EVX','THB_BNB','THB_POW','THB_DOGE','THB_DAI','THB_USDC','THB_BAT','THB_BAND','THB_KSM','THB_DOT','THB_NEAR') NOT NULL, 34 | `Side` enum('buy','sell') NOT NULL, 35 | `Type` enum('limit','market') NOT NULL, 36 | `Amount` float NOT NULL, 37 | `Rate` float NOT NULL, 38 | `Fee` float NOT NULL, 39 | `Credit` float NOT NULL, 40 | `Receive` float NOT NULL, 41 | `Timestamp` timestamp NOT NULL 42 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 43 | 44 | -- -------------------------------------------------------- 45 | 46 | -- 47 | -- Table structure for table `log_tradingview` 48 | -- 49 | 50 | CREATE TABLE `log_tradingview` ( 51 | `ID` int(11) NOT NULL, 52 | `Exchange` varchar(10) NOT NULL, 53 | `Ticker` varchar(10) NOT NULL, 54 | `Timeframe` enum('4h','6h','8h','12h','1d') NOT NULL, 55 | `Side` enum('buy','sell') NOT NULL, 56 | `Open` float NOT NULL, 57 | `Close` float NOT NULL, 58 | `High` float NOT NULL, 59 | `Low` float NOT NULL, 60 | `Volume` float NOT NULL, 61 | `Timestamp` datetime NOT NULL 62 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 63 | 64 | -- 65 | -- Indexes for dumped tables 66 | -- 67 | 68 | -- 69 | -- Indexes for table `log_trading` 70 | -- 71 | ALTER TABLE `log_trading` 72 | ADD PRIMARY KEY (`ID`); 73 | 74 | -- 75 | -- Indexes for table `log_tradingview` 76 | -- 77 | ALTER TABLE `log_tradingview` 78 | ADD PRIMARY KEY (`ID`); 79 | 80 | -- 81 | -- AUTO_INCREMENT for dumped tables 82 | -- 83 | 84 | -- 85 | -- AUTO_INCREMENT for table `log_tradingview` 86 | -- 87 | ALTER TABLE `log_tradingview` 88 | MODIFY `ID` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=1; 89 | COMMIT; 90 | 91 | /*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; 92 | /*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; 93 | /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; 94 | -------------------------------------------------------------------------------- /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 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at nanmcpe[at]gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /routes/tradingview.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router(); 2 | const { LINE } = require('../utils/LINE'); 3 | const { TradingViewLogs } = require('../databases/bitkub'); 4 | const { parseObject, getWallet, placeBid, placeAsk, cancelOrder } = require('../utils'); 5 | 6 | router.post('/tradingview/btcusd', async (req, res, next) => { 7 | try { 8 | console.log('TradingView has hooked'); 9 | console.log('body', req.body); 10 | const obj = parseObject(req.body); 11 | console.log('obj', obj); 12 | await TradingViewLogs.create(obj); 13 | if (obj.side === 'buy') { 14 | const wallet = await getWallet(); 15 | const thb = wallet.result.THB; 16 | const buyRatio = parseFloat(process.env.BUY_RATIO); 17 | const amountToBuy = parseFloat((thb * buyRatio).toFixed(2)); 18 | console.log('amountToBuy', amountToBuy); 19 | const bid = await placeBid('THB_BTC', amountToBuy, 0, 'market'); // sym, amt, rat, type 20 | console.log('bid', bid); 21 | LINE(`Buy ${amountToBuy} BTC @ ${obj.close}`); 22 | } 23 | if (obj.side === 'sell') { 24 | const wallet = await getWallet(); 25 | const btc = wallet.result.BTC; 26 | const buyRatio = parseFloat(process.env.SELL_RATIO); 27 | const amountToSell = btc * buyRatio; 28 | console.log('amountToSell', amountToSell); 29 | const ask = await placeAsk('THB_BTC', amountToSell, 0, 'market'); // sym, amt, rat, type => minimum is 0.0001 BTC 30 | console.log('ask', ask); 31 | LINE(`Sell ${amountToSell} BTC @ ${obj.close}`); 32 | } 33 | res.sendStatus(200); 34 | } catch (err) { 35 | console.error(err); 36 | LINE(err.message); 37 | res.sendStatus(200); 38 | } 39 | }); 40 | 41 | router.get('/wallet', async (req, res, next) => { 42 | try { 43 | const response = await getWallet(); 44 | res.json(response); 45 | } catch (err) { 46 | console.error(err); 47 | res.status(500).json({ 48 | err: err.message 49 | }); 50 | } 51 | }); 52 | 53 | router.post('/buy', async (req, res, next) => { 54 | try { 55 | const symbol = 'THB_BTC'; 56 | const amount = 40; 57 | const rate = 2.5; 58 | const type = 'limit'; 59 | const response = await placeBid(symbol, amount, rate, type); 60 | LINE(`Buy ${amount} BTC @ ${rate}`); 61 | res.json(response); 62 | } catch (err) { 63 | console.error(err); 64 | res.status(500).json({ 65 | err: err.message 66 | }); 67 | } 68 | }); 69 | 70 | router.post('/sell', async (req, res, next) => { 71 | try { 72 | const symbol = 'THB_BTC'; 73 | const amount = 0001; 74 | const rate = 600000; 75 | const type = 'limit'; 76 | const response = await placeAsk(symbol, amount, rate, type); 77 | LINE(`Sell ${amount} BTC @ ${rate}`); 78 | res.json(response); 79 | } catch (err) { 80 | console.error(err); 81 | res.status(500).json({ 82 | err: err.message 83 | }); 84 | } 85 | }); 86 | 87 | router.post('/cancel', async (req, res, next) => { 88 | try { 89 | const hash = 'fwQ6dnQYKnqB17qriRcuwRmhYVR'; 90 | await cancelOrder(hash); 91 | LINE(`Cancel order: ${hash}`); 92 | res.json(response); 93 | } catch (err) { 94 | console.error(err); 95 | res.status(500).json({ 96 | err: err.message 97 | }); 98 | } 99 | }); 100 | 101 | router.get('/hello', async (req, res, next) => { 102 | try { 103 | res.json({ 104 | msg: 'Hello!' 105 | }); 106 | } catch (err) { 107 | console.error(err); 108 | res.status(500).json({ 109 | err: err.message 110 | }); 111 | } 112 | }); 113 | 114 | module.exports = router; 115 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bitkub-optimize 2 | ![CodeQL](https://github.com/nanmcpe/bitkub-optimize/workflows/CodeQL/badge.svg) 3 | 4 | Node.JS trading bot for [Bitkub.com](https://bitkub.com) using [TradingView](https://tradingview.com) webhook. 5 | 6 | This application is based on [bitkub-official-api-docs](https://github.com/bitkub/bitkub-official-api-docs) 7 | 8 | ![](https://smartadvicefordumbmillennials.com/wp-content/uploads/2017/10/wolf_of_wall_street_money.gif) 9 | 10 | ## Features 11 | - Buy or Sell Bitcoin when received webhook from TradingView. 12 | - Records transactions to the self-managed database. 13 | - Notify trading transactions with LINE (need to [register](https://notify-bot.line.me/)). 14 | 15 | ## TODOs (PR-Welcomed) 16 | - ✅ Replace moment.js with [date-fns](https://github.com/date-fns/date-fns). 17 | - ⏰ Add other instant messaging (e.g. Slack) for notify action. 18 | - ⏰ Add stop-limit order feature (waiting for Bitkub to release this API, [see more](https://github.com/bitkub/bitkub-official-api-docs/issues/24)). 19 | - ⏰ Add trailing stop order feature (after stop-limit order API is released). 20 | - ⏰ Design a web dashboard to visualize profit and loss. 21 | 22 | ## Strategy 23 | - Buying or selling signals will be triggered from TradingView, we should set up at TradingView side. 24 | #### These are some strategies that configurable 25 | - Buy BTC using 90% off THB on your available balance at market price when the condition is met. 26 | - Sell 100% BTC of your available balance at market price when the condition is met. 27 | 28 | ## Prerequisite 29 | - TradingView [Pro plan](https://www.tradingview.com/gopro/?share_your_love=ThanwaJindarattana) or above for server-side webhook. 30 | - Setup trigger condition and webhook URL. 31 | 32 | Example TradingView payload 33 | ``` 34 | side = sell, tf = 4h, exchange = {{exchange}}, ticker = {{ticker}}, open = {{open}}, close = {{close}}, high = {{high}}, low = {{low}}, volume = {{volume}}, timestamp = {{time}} 35 | ``` 36 | 37 | ## Clone this repository 38 | `git clone https://github.com/nanmcpe/bitkub-optimize` 39 | 40 | ## Install dependencies 41 | `npm install` 42 | 43 | ## Setup firewall (Recommended) 44 | We allow incoming traffic from trusted sources only. 45 | Here is a list of IP addresses that we need to receive POST requests. 46 | For more information, please see [About webhooks](https://www.tradingview.com/chart/?solution=43000529348). 47 | ``` 48 | 52.89.214.238 49 | 34.212.75.30 50 | 54.218.53.128 51 | 52.32.178.7 52 | ``` 53 | TradingView only accepts URLs with port numbers 80 and 443. 54 | 55 | ``` 56 | # ufw allow from 52.89.214.238 to any port 443 57 | # ufw allow from 34.212.75.30 to any port 443 58 | # ufw allow from 54.218.53.128 to any port 443 59 | # ufw allow from 52.32.178.7 to any port 443 60 | ``` 61 | 62 | ## Edit database configuration and other parameters on .env 63 | ``` 64 | NODE_ENV=Development 65 | API_KEY= 66 | API_SECRET= 67 | DB_SQL_HOST= 68 | DB_SQL_PORT=3306 69 | DB_SQL_DB= 70 | DB_SQL_USER= 71 | DB_SQL_PASS= 72 | SERVER_PORT=3000 73 | BITKUB_ROOT_URL=https://api.bitkub.com 74 | LINE_TOKEN= 75 | BUY_RATIO=0.9 76 | SELL_RATIO=1 77 | ``` 78 | 79 | ## Import database 80 | `$ mysql -u -p bitkub < database.sql` 81 | 82 | ## Dockerize (Optional) 83 | #### Build docker image 84 | `docker build -t /bitkub-optimize:` 85 | 86 | #### Push docker image 87 | `docker push /bitkub-optimize:` 88 | 89 | #### Pull docker image 90 | `docker-compose pull` 91 | 92 | ## Run application 93 | `node app.js` or `docker-compose up -d` 94 | 95 | ## Appendix 96 | ### nginx.conf 97 | ``` 98 | location / { 99 | proxy_pass http://172.50.0.2:3000/; # IP of docker container 100 | proxy_http_version 1.1; 101 | proxy_set_header Upgrade $http_upgrade; 102 | proxy_set_header Connection 'upgrade'; 103 | proxy_set_header Host $host; 104 | proxy_set_header X-Forwarded-Proto $scheme; 105 | proxy_set_header X-Real-IP $remote_addr; # Add this header to get real remote IP 106 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 107 | proxy_cache_bypass $http_upgrade; 108 | } 109 | ``` 110 | ### Certbot 111 | If you use [Certbot](https://certbot.eff.org/) to renew the SSL certificate, you need to change from `HTTP challenge` to `DNS challenge` because we only whitelist from TradingView so the Certbot task will fail 112 | 113 | #### If you guys like this project, feel free to give me some coffee ☕️ 114 | - BTC: 3NkbtCeykMvAX32rAd14h3pBstHZ47RaNb 115 | - ETH: 0xc0430624d2e04a2d5e393554904ebefca39b48ca 116 | - 117 | -------------------------------------------------------------------------------- /utils/index.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | const crypto = require("crypto"); 3 | const { TradingLogs } = require('../databases/bitkub'); 4 | const { getTime } = require('date-fns'); 5 | 6 | function parseObject(text) { 7 | let obj = {}; 8 | text.split(',').forEach(item => { 9 | const key = item.trim().split('=')[0].trim(); 10 | const value = item.trim().split('=')[1].trim(); 11 | if (key === 'volume' || key === 'open' || key === 'high' || key === 'low' || key === 'close') { 12 | obj[key] = parseFloat(value); 13 | } else if (key === 'timestamp') { 14 | obj[key] = new Date(value); 15 | } else { 16 | obj[key] = value; 17 | } 18 | }); 19 | return obj; 20 | } 21 | 22 | function signBody(body) { 23 | const digest = crypto.createHmac('sha256', process.env.API_SECRET) 24 | .update(JSON.stringify(body)) 25 | .digest('hex'); 26 | return digest; 27 | } 28 | 29 | function getErrorDescription(errCode) { 30 | switch (errCode) { 31 | case 0: return 'No error'; 32 | case 1: return 'Invalid JSON payload'; 33 | case 2: return 'Missing X-BTK-APIKEY'; 34 | case 3: return 'Invalid API key'; 35 | case 4: return 'API pending for activation'; 36 | case 5: return 'IP not allowed'; 37 | case 6: return 'Missing / invalid signature'; 38 | case 7: return 'Missing timestamp'; 39 | case 8: return 'Invalid timestamp'; 40 | case 9: return 'Invalid user'; 41 | case 10: return 'Invalid parameter'; 42 | case 11: return 'Invalid symbol'; 43 | case 12: return 'Invalid amount'; 44 | case 13: return 'Invalid rate'; 45 | case 14: return 'Improper rate'; 46 | case 15: return 'Amount too low'; 47 | case 16: return 'Failed to get balance'; 48 | case 17: return 'Wallet is empty'; 49 | case 18: return 'Insufficient balance'; 50 | case 19: return 'Failed to insert order into db'; 51 | case 20: return 'Failed to deduct balance'; 52 | case 21: return 'Invalid order for cancellation'; 53 | case 22: return 'Invalid side'; 54 | case 23: return 'Failed to update order status'; 55 | case 24: return 'Invalid order for lookup (or cancelled)'; 56 | case 25: return 'KYC level 1 is required to proceed'; 57 | case 30: return 'Limit exceeds'; 58 | case 40: return 'Pending withdrawal exists'; 59 | case 41: return 'Invalid currency for withdrawal'; 60 | case 42: return 'Address is not in whitelist'; 61 | case 43: return 'Failed to deduct crypto'; 62 | case 44: return 'Failed to create withdrawal record'; 63 | case 45: return 'Nonce has to be numeric'; 64 | case 46: return 'Invalid nonce'; 65 | case 47: return 'Withdrawal limit exceeds'; 66 | case 48: return 'Invalid bank account'; 67 | case 49: return 'Bank limit exceeds'; 68 | case 50: return 'Pending withdrawal exists'; 69 | case 51: return 'Withdrawal is under maintenance'; 70 | case 90: return 'Server error (please contact support)'; 71 | } 72 | } 73 | 74 | function getWallet() { 75 | return new Promise(async (resolve, reject) => { 76 | try { 77 | let body = { 78 | ts: Math.floor(getTime(new Date()) / 1000) 79 | }; 80 | const signedBody = signBody(body); 81 | body.sig = signedBody; 82 | const response = await axios({ 83 | method: 'post', 84 | url: `${process.env.BITKUB_ROOT_URL}/api/market/wallet`, 85 | headers: { 86 | 'Accept': 'application/json', 87 | 'Content-type': 'application/json', 88 | 'X-BTK-APIKEY': `${process.env.API_KEY}` 89 | }, 90 | data: body, 91 | }).then(res => res.data);; 92 | console.log('response', response); 93 | resolve(response); 94 | } catch (err) { 95 | reject(err); 96 | } 97 | }); 98 | } 99 | 100 | function placeBid(symbol, amount, rate, type) { 101 | return new Promise(async (resolve, reject) => { 102 | try { 103 | let body = { 104 | sym: symbol, 105 | amt: amount, // THB no trailing zero 106 | rat: rate, // for market order use 0 107 | typ: type, 108 | ts: Math.floor(getTime(new Date()) / 1000) 109 | }; 110 | const signedBody = signBody(body); 111 | body.sig = signedBody; 112 | const response = await axios({ 113 | method: 'post', 114 | url: `${process.env.BITKUB_ROOT_URL}/api/market/place-bid`, 115 | headers: { 116 | 'Accept': 'application/json', 117 | 'Content-type': 'application/json', 118 | 'X-BTK-APIKEY': `${process.env.API_KEY}` 119 | }, 120 | data: body, 121 | }).then(res => res.data);; 122 | console.log('response', response); 123 | if (response.error !== 0) { 124 | const errMessage = getErrorDescription(response.error); 125 | console.log('message', errMessage); 126 | reject({ 127 | error: response.error, 128 | message: errMessage 129 | }); 130 | } else { 131 | response.result.ts = response.result.ts * 1000; // emit millisecond 132 | response.result.side = 'buy'; 133 | response.result.sym = symbol; 134 | await TradingLogs.create(response.result); 135 | resolve(response.result); 136 | } 137 | } catch (err) { 138 | reject(err); 139 | } 140 | }); 141 | } 142 | 143 | function placeAsk(symbol, amount, rate, type) { 144 | return new Promise(async (resolve, reject) => { 145 | try { 146 | let body = { 147 | sym: symbol, 148 | amt: amount, // BTC no trailing zero 149 | rat: rate, // for market order use 0 150 | typ: type, 151 | ts: Math.floor(getTime(new Date()) / 1000) 152 | }; 153 | const signedBody = signBody(body); 154 | body.sig = signedBody; 155 | const response = await axios({ 156 | method: 'post', 157 | url: `${process.env.BITKUB_ROOT_URL}/api/market/place-ask`, 158 | headers: { 159 | 'Accept': 'application/json', 160 | 'Content-type': 'application/json', 161 | 'X-BTK-APIKEY': `${process.env.API_KEY}` 162 | }, 163 | data: body, 164 | }).then(res => res.data);; 165 | console.log('response', response); 166 | if (response.error !== 0) { 167 | const errMessage = getErrorDescription(response.error); 168 | console.log('message', errMessage); 169 | reject({ 170 | error: response.error, 171 | message: errMessage 172 | }); 173 | } else { 174 | response.result.ts = response.result.ts * 1000; // emit millisecond 175 | response.result.side = 'sell'; 176 | response.result.sym = symbol; 177 | await TradingLogs.create(response.result); 178 | resolve(response.result); 179 | } 180 | } catch (err) { 181 | reject(err); 182 | } 183 | }); 184 | } 185 | 186 | function cancelOrder(hash) { 187 | return new Promise(async (resolve, reject) => { 188 | try { 189 | let body = { 190 | hash: hash, 191 | ts: Math.floor(getTime(new Date()) / 1000), 192 | }; 193 | const signedBody = signBody(body); 194 | body.sig = signedBody; 195 | const response = await axios({ 196 | method: 'post', 197 | url: `${process.env.BITKUB_ROOT_URL}/api/market/cancel-order`, 198 | headers: { 199 | 'Accept': 'application/json', 200 | 'Content-type': 'application/json', 201 | 'X-BTK-APIKEY': `${process.env.API_KEY}` 202 | }, 203 | data: body, 204 | }).then(res => res.data); 205 | console.log('response', response); 206 | if (response.error !== 0) { 207 | const errMessage = getErrorDescription(response.error); 208 | console.log('message', errMessage); 209 | reject({ 210 | error: response.error, 211 | message: errMessage 212 | }); 213 | } else { 214 | resolve({ msg: 'success' }); // no response payload from Bitkub's server 215 | } 216 | } catch (err) { 217 | reject(err); 218 | } 219 | }); 220 | } 221 | 222 | module.exports = { 223 | parseObject, 224 | getErrorDescription, 225 | signBody, 226 | getWallet, 227 | placeBid, 228 | placeAsk, 229 | cancelOrder, 230 | }; --------------------------------------------------------------------------------