├── .gitignore ├── .dockerignore ├── Dockerfile.mariadb ├── Dockerfile.mysql ├── config ├── config_global.yml ├── config_global_template.yml ├── config.yml ├── config_default.yml ├── config_template.yml ├── config_100-200-500-0009.yml ├── config_200-100-300-0007.yml ├── config_200-200-200-0005.yml ├── config_200-200-200-0006.yml └── config_200-200-400-0006.yml ├── .github └── workflows │ ├── go.yml │ ├── devskim-analysis.yml │ ├── docker-image.yml │ ├── codacy-analysis.yml │ └── codeql-analysis.yml ├── Dockerfile.cryptopump ├── .vscode └── launch.json ├── documentation ├── HowToCompile.md ├── docker-compose-rpi-example.yml ├── HowToInstall.md └── HowToUse.md ├── go.mod ├── LICENSE ├── docker-compose.yml ├── static └── stylesheets │ └── cryptopump.css ├── nodes ├── nodes_test.go └── nodes.go ├── threads ├── threads_test.go └── threads.go ├── functions ├── functions_test.go └── functions.go ├── logger ├── logger_test.go └── logger.go ├── loader ├── loader_test.go └── loader.go ├── markets ├── markets_test.go └── markets.go ├── README.md ├── plotter ├── plotter_test.go └── plotter.go ├── templates └── admin.html ├── telegram └── telegram.go ├── exchange ├── exchange_test.go ├── binance.go └── exchange.go ├── types └── types.go └── main.go /.gitignore: -------------------------------------------------------------------------------- 1 | cryptopump_debug.log 2 | cryptopump.log 3 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .github 3 | .DS_Store 4 | .vscode 5 | .dccache 6 | /config/config.yml 7 | /config/config_global.yml -------------------------------------------------------------------------------- /Dockerfile.mariadb: -------------------------------------------------------------------------------- 1 | FROM yobasystems/alpine-mariadb:10.4.17-armhf 2 | COPY /mysql/cryptopump-mariadb.sql /docker-entrypoint-initdb.d/init.sql -------------------------------------------------------------------------------- /Dockerfile.mysql: -------------------------------------------------------------------------------- 1 | FROM mysql:8.0 2 | RUN apt-get update -qq && apt-get install -y -qq 3 | COPY /mysql/cryptopump.sql /docker-entrypoint-initdb.d/init.sql -------------------------------------------------------------------------------- /config/config_global.yml: -------------------------------------------------------------------------------- 1 | config_global: 2 | apikey: "" 3 | apikeytestnet: "" 4 | secretkey: "" 5 | secretkeytestnet: "" 6 | tgbotapikey: "" 7 | -------------------------------------------------------------------------------- /config/config_global_template.yml: -------------------------------------------------------------------------------- 1 | config_global: 2 | apikey: "" 3 | apikeytestnet: "" 4 | secretkey: "" 5 | secretkeytestnet: "" 6 | tgbotapikey: "" -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ main, beta ] 6 | 7 | jobs: 8 | 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | 14 | - name: Set up Go 15 | uses: actions/setup-go@v2 16 | with: 17 | go-version: 1.17 18 | 19 | - name: Build 20 | run: go build -v ./... 21 | 22 | - name: Test 23 | run: go test -v ./... -------------------------------------------------------------------------------- /Dockerfile.cryptopump: -------------------------------------------------------------------------------- 1 | FROM golang:1.16 2 | 3 | RUN apt-get update -qq && apt-get install -y -qq \ 4 | && apt-get -qq clean 5 | 6 | WORKDIR /go/src/cryptopump 7 | 8 | COPY . . 9 | 10 | # do not copy configuration files, but rather configuration templates 11 | COPY config/config_template.yml /go/src/cryptopump/config/config.yml 12 | COPY config/config_global_template.yml /go/src/cryptopump/config/config_global.yml 13 | 14 | # install dependencies 15 | RUN go install -v ./... 16 | 17 | # forward request and error logs to docker log collector 18 | RUN ln -sf /dev/stdout cryptopump.log \ 19 | && ln -sf /dev/stderr cryptopump_debug.log 20 | 21 | RUN go build -o /cryptopump 22 | 23 | ENTRYPOINT [ "cryptopump" ] -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Launch file", 9 | "type": "go", 10 | "request": "launch", 11 | "mode": "debug", 12 | "program": "${workspaceFolder}/main.go", 13 | "env": { 14 | "DB_USER": "root", 15 | "DB_PASS": "", 16 | "DB_TCP_HOST": "127.0.0.1", 17 | "DB_PORT": "3306", 18 | "DB_NAME": "cryptopump" 19 | } 20 | } 21 | ] 22 | } -------------------------------------------------------------------------------- /documentation/HowToCompile.md: -------------------------------------------------------------------------------- 1 | ## HOW TO COMPILE 2 | 3 | To compile cryptopump you need Go environment set. 4 | 5 | On Ubuntu 18.04 you can install Go as follow: 6 | 7 | Download the Go language binary archive: 8 | ``` 9 | $ wget https://dl.google.com/go/go1.16.4.linux-amd64.tar.gz 10 | ``` 11 | 12 | Extract it: 13 | ``` 14 | $ sudo tar -xvf go1.16.4.linux-amd64.tar.gz 15 | ``` 16 | 17 | and copy it: 18 | ``` 19 | $ sudo mv go /usr/local 20 | ``` 21 | 22 | Setup Go Environment: 23 | ``` 24 | $ export GOROOT=/usr/local/go 25 | $ export PATH=$GOPATH/bin:$GOROOT/bin:$PATH 26 | ``` 27 | Verify Go is running with 28 | ``` 29 | $ go version 30 | ``` 31 | and check the output for 32 | ``` 33 | go version go1.16.4 linux/amd64 34 | ``` 35 | 36 | Now go to cryptopump directory and compile it with 37 | ``` 38 | $ go build . 39 | ``` 40 | 41 | An executable should be present in cryptopump directory. 42 | -------------------------------------------------------------------------------- /.github/workflows/devskim-analysis.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | 6 | name: DevSkim 7 | 8 | on: 9 | push: 10 | branches: [ main ] 11 | pull_request: 12 | branches: [ main ] 13 | schedule: 14 | - cron: '42 13 * * 4' 15 | 16 | jobs: 17 | lint: 18 | name: DevSkim 19 | runs-on: ubuntu-20.04 20 | permissions: 21 | actions: read 22 | contents: read 23 | security-events: write 24 | steps: 25 | - name: Checkout code 26 | uses: actions/checkout@v2 27 | 28 | - name: Run DevSkim scanner 29 | uses: microsoft/DevSkim-Action@v1 30 | 31 | - name: Upload DevSkim scan results to GitHub Security tab 32 | uses: github/codeql-action/upload-sarif@v1 33 | with: 34 | sarif_file: devskim-results.sarif 35 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/aleibovici/cryptopump 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/DATA-DOG/go-sqlmock v1.5.0 7 | github.com/adshao/go-binance/v2 v2.3.1 8 | github.com/fsnotify/fsnotify v1.5.1 // indirect 9 | github.com/go-echarts/go-echarts/v2 v2.2.4 10 | github.com/go-sql-driver/mysql v1.6.0 11 | github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible 12 | github.com/gorilla/websocket v1.4.2 // indirect 13 | github.com/jtaczanowski/go-scheduler v0.1.0 14 | github.com/paulbellamy/ratecounter v0.2.0 15 | github.com/rs/xid v1.3.0 16 | github.com/sdcoffey/big v0.7.0 17 | github.com/sdcoffey/techan v0.12.1 18 | github.com/sirupsen/logrus v1.8.1 19 | github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 20 | github.com/spf13/cast v1.4.1 // indirect 21 | github.com/spf13/viper v1.8.1 22 | github.com/tcnksm/go-httpstat v0.2.0 23 | github.com/technoweenie/multipartstreamer v1.0.1 // indirect 24 | golang.org/x/sys v0.0.0-20211019181941-9d821ace8654 // indirect 25 | golang.org/x/text v0.3.7 // indirect 26 | ) 27 | -------------------------------------------------------------------------------- /config/config.yml: -------------------------------------------------------------------------------- 1 | config: 2 | buy_24hs_highprice_entry: "0.0005" 3 | buy_24hs_highprice_entry_macd: "20" 4 | buy_direction_down: "20" 5 | buy_direction_up: "10" 6 | buy_macd_entry: "-30" 7 | buy_macd_upmarket: "10" 8 | buy_quantity_fiat_down: "50" 9 | buy_quantity_fiat_init: "50" 10 | buy_quantity_fiat_up: "50" 11 | buy_repeat_threshold_down: "0.002" 12 | buy_repeat_threshold_down_second: "0.002" 13 | buy_repeat_threshold_down_second_start_count: "2" 14 | buy_repeat_threshold_up: "0.0001" 15 | buy_rsi7_entry: "40" 16 | buy_wait: "60" 17 | debug: "false" 18 | dryrun: "false" 19 | exchange_comission: "0.00075" 20 | exchangename: BINANCE 21 | exit: "false" 22 | newsession: "false" 23 | profit_min: "0.001" 24 | sellholdonrsi3: "70" 25 | selltocover: "false" 26 | sellwaitaftercancel: "10" 27 | sellwaitbeforecancel: "20" 28 | stoploss: "0" 29 | symbol: BTCUSDT 30 | symbol_fiat: USDT 31 | symbol_fiat_stash: "100" 32 | testnet: "true" 33 | time_enforce: "false" 34 | time_start: 04:00AM 35 | time_stop: 07:00PM 36 | -------------------------------------------------------------------------------- /config/config_default.yml: -------------------------------------------------------------------------------- 1 | config: 2 | buy_24hs_highprice_entry: "0.0005" 3 | buy_24hs_highprice_entry_macd: "20" 4 | buy_direction_down: "20" 5 | buy_direction_up: "10" 6 | buy_macd_entry: "-30" 7 | buy_macd_upmarket: "10" 8 | buy_quantity_fiat_down: "50" 9 | buy_quantity_fiat_init: "50" 10 | buy_quantity_fiat_up: "50" 11 | buy_repeat_threshold_down: "0.002" 12 | buy_repeat_threshold_down_second: "0.002" 13 | buy_repeat_threshold_down_second_start_count: "2" 14 | buy_repeat_threshold_up: "0.0001" 15 | buy_rsi7_entry: "40" 16 | buy_wait: "60" 17 | debug: "false" 18 | dryrun: "false" 19 | exchange_comission: "0.00075" 20 | exchangename: BINANCE 21 | exit: "false" 22 | newsession: "false" 23 | profit_min: "0.001" 24 | sellholdonrsi3: "70" 25 | selltocover: "false" 26 | sellwaitaftercancel: "10" 27 | sellwaitbeforecancel: "20" 28 | stoploss: "0" 29 | symbol: BTCUSDT 30 | symbol_fiat: USDT 31 | symbol_fiat_stash: "100" 32 | testnet: "true" 33 | time_enforce: "false" 34 | time_start: 04:00AM 35 | time_stop: 07:00PM 36 | -------------------------------------------------------------------------------- /config/config_template.yml: -------------------------------------------------------------------------------- 1 | config: 2 | buy_24hs_highprice_entry: "0.0005" 3 | buy_24hs_highprice_entry_macd: "20" 4 | buy_direction_down: "20" 5 | buy_direction_up: "10" 6 | buy_macd_entry: "-30" 7 | buy_macd_upmarket: "10" 8 | buy_quantity_fiat_down: "50" 9 | buy_quantity_fiat_init: "50" 10 | buy_quantity_fiat_up: "50" 11 | buy_repeat_threshold_down: "0.002" 12 | buy_repeat_threshold_down_second: "0.002" 13 | buy_repeat_threshold_down_second_start_count: "2" 14 | buy_repeat_threshold_up: "0.0001" 15 | buy_rsi7_entry: "40" 16 | buy_wait: "60" 17 | debug: "false" 18 | dryrun: "false" 19 | exchange_comission: "0.00075" 20 | exchangename: BINANCE 21 | exit: "false" 22 | newsession: "false" 23 | profit_min: "0.001" 24 | sellholdonrsi3: "70" 25 | selltocover: "false" 26 | sellwaitaftercancel: "10" 27 | sellwaitbeforecancel: "20" 28 | stoploss: "0" 29 | symbol: BTCUSDT 30 | symbol_fiat: USDT 31 | symbol_fiat_stash: "100" 32 | testnet: "true" 33 | time_enforce: "false" 34 | time_start: 04:00AM 35 | time_stop: 07:00PM 36 | -------------------------------------------------------------------------------- /config/config_100-200-500-0009.yml: -------------------------------------------------------------------------------- 1 | config: 2 | apikey: 3 | apikeytestnet: 4 | buy_24hs_highprice_entry: "0.0005" 5 | buy_direction_down: "20" 6 | buy_direction_up: "10" 7 | buy_quantity_fiat_down: "50.00" 8 | buy_quantity_fiat_init: "50.00" 9 | buy_quantity_fiat_up: "50.00" 10 | buy_repeat_threshold_down: "0.002" 11 | buy_repeat_threshold_down_second: "0.002" 12 | buy_repeat_threshold_down_second_start_count: "2" 13 | buy_repeat_threshold_up: "0.00001" 14 | buy_rsi7_entry: "40" 15 | buy_wait: "60" 16 | debug: "false" 17 | debug_forcebuy: "false" 18 | debug_forcesell: "false" 19 | dryrun: "false" 20 | exchange_comission: "0.00075" 21 | exchangename: BINANCE 22 | exit: "false" 23 | newsession: "false" 24 | profit_min: "0.001" 25 | secretkey: 26 | secretkeytestnet: 27 | sellholdonrsi3: "70" 28 | selltocover: "false" 29 | sellwaitaftercancel: "10" 30 | sellwaitbeforecancel: "20" 31 | symbol: BTCUSDT 32 | symbol_fiat: USDT 33 | symbol_fiat_stash: "500.00" 34 | tgbotapikey: 35 | time_enforce: "false" 36 | time_start: 04:00AM 37 | time_stop: 08:00PM 38 | -------------------------------------------------------------------------------- /config/config_200-100-300-0007.yml: -------------------------------------------------------------------------------- 1 | config: 2 | apikey: 3 | apikeytestnet: 4 | buy_24hs_highprice_entry: "0.0005" 5 | buy_direction_down: "20" 6 | buy_direction_up: "10" 7 | buy_quantity_fiat_down: "50.00" 8 | buy_quantity_fiat_init: "50.00" 9 | buy_quantity_fiat_up: "50.00" 10 | buy_repeat_threshold_down: "0.002" 11 | buy_repeat_threshold_down_second: "0.002" 12 | buy_repeat_threshold_down_second_start_count: "2" 13 | buy_repeat_threshold_up: "0.00001" 14 | buy_rsi7_entry: "40" 15 | buy_wait: "60" 16 | debug: "false" 17 | debug_forcebuy: "false" 18 | debug_forcesell: "false" 19 | dryrun: "false" 20 | exchange_comission: "0.00075" 21 | exchangename: BINANCE 22 | exit: "false" 23 | newsession: "false" 24 | profit_min: "0.001" 25 | secretkey: 26 | secretkeytestnet: 27 | sellholdonrsi3: "70" 28 | selltocover: "false" 29 | sellwaitaftercancel: "10" 30 | sellwaitbeforecancel: "20" 31 | symbol: BTCUSDT 32 | symbol_fiat: USDT 33 | symbol_fiat_stash: "500.00" 34 | tgbotapikey: 35 | time_enforce: "false" 36 | time_start: 04:00AM 37 | time_stop: 08:00PM 38 | -------------------------------------------------------------------------------- /config/config_200-200-200-0005.yml: -------------------------------------------------------------------------------- 1 | config: 2 | apikey: 3 | apikeytestnet: 4 | buy_24hs_highprice_entry: "0.0005" 5 | buy_direction_down: "20" 6 | buy_direction_up: "10" 7 | buy_quantity_fiat_down: "50.00" 8 | buy_quantity_fiat_init: "50.00" 9 | buy_quantity_fiat_up: "50.00" 10 | buy_repeat_threshold_down: "0.002" 11 | buy_repeat_threshold_down_second: "0.002" 12 | buy_repeat_threshold_down_second_start_count: "2" 13 | buy_repeat_threshold_up: "0.00001" 14 | buy_rsi7_entry: "40" 15 | buy_wait: "60" 16 | debug: "false" 17 | debug_forcebuy: "false" 18 | debug_forcesell: "false" 19 | dryrun: "false" 20 | exchange_comission: "0.00075" 21 | exchangename: BINANCE 22 | exit: "false" 23 | newsession: "false" 24 | profit_min: "0.001" 25 | secretkey: 26 | secretkeytestnet: 27 | sellholdonrsi3: "70" 28 | selltocover: "false" 29 | sellwaitaftercancel: "10" 30 | sellwaitbeforecancel: "20" 31 | symbol: BTCUSDT 32 | symbol_fiat: USDT 33 | symbol_fiat_stash: "500.00" 34 | tgbotapikey: 35 | time_enforce: "false" 36 | time_start: 04:00AM 37 | time_stop: 08:00PM 38 | -------------------------------------------------------------------------------- /config/config_200-200-200-0006.yml: -------------------------------------------------------------------------------- 1 | config: 2 | apikey: 3 | apikeytestnet: 4 | buy_24hs_highprice_entry: "0.0005" 5 | buy_direction_down: "20" 6 | buy_direction_up: "10" 7 | buy_quantity_fiat_down: "50.00" 8 | buy_quantity_fiat_init: "50.00" 9 | buy_quantity_fiat_up: "50.00" 10 | buy_repeat_threshold_down: "0.002" 11 | buy_repeat_threshold_down_second: "0.002" 12 | buy_repeat_threshold_down_second_start_count: "2" 13 | buy_repeat_threshold_up: "0.00001" 14 | buy_rsi7_entry: "40" 15 | buy_wait: "60" 16 | debug: "false" 17 | debug_forcebuy: "false" 18 | debug_forcesell: "false" 19 | dryrun: "false" 20 | exchange_comission: "0.00075" 21 | exchangename: BINANCE 22 | exit: "false" 23 | newsession: "false" 24 | profit_min: "0.001" 25 | secretkey: 26 | secretkeytestnet: 27 | sellholdonrsi3: "70" 28 | selltocover: "false" 29 | sellwaitaftercancel: "10" 30 | sellwaitbeforecancel: "20" 31 | symbol: BTCUSDT 32 | symbol_fiat: USDT 33 | symbol_fiat_stash: "500.00" 34 | tgbotapikey: 35 | time_enforce: "false" 36 | time_start: 04:00AM 37 | time_stop: 08:00PM 38 | -------------------------------------------------------------------------------- /config/config_200-200-400-0006.yml: -------------------------------------------------------------------------------- 1 | config: 2 | apikey: 3 | apikeytestnet: 4 | buy_24hs_highprice_entry: "0.0005" 5 | buy_direction_down: "20" 6 | buy_direction_up: "10" 7 | buy_quantity_fiat_down: "50.00" 8 | buy_quantity_fiat_init: "50.00" 9 | buy_quantity_fiat_up: "50.00" 10 | buy_repeat_threshold_down: "0.002" 11 | buy_repeat_threshold_down_second: "0.002" 12 | buy_repeat_threshold_down_second_start_count: "2" 13 | buy_repeat_threshold_up: "0.00001" 14 | buy_rsi7_entry: "40" 15 | buy_wait: "60" 16 | debug: "false" 17 | debug_forcebuy: "false" 18 | debug_forcesell: "false" 19 | dryrun: "false" 20 | exchange_comission: "0.00075" 21 | exchangename: BINANCE 22 | exit: "false" 23 | newsession: "false" 24 | profit_min: "0.001" 25 | secretkey: 26 | secretkeytestnet: 27 | sellholdonrsi3: "70" 28 | selltocover: "false" 29 | sellwaitaftercancel: "10" 30 | sellwaitbeforecancel: "20" 31 | symbol: BTCUSDT 32 | symbol_fiat: USDT 33 | symbol_fiat_stash: "500.00" 34 | tgbotapikey: 35 | time_enforce: "false" 36 | time_start: 04:00AM 37 | time_stop: 08:00PM 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 aleibovici 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 | -------------------------------------------------------------------------------- /documentation/docker-compose-rpi-example.yml: -------------------------------------------------------------------------------- 1 | version: '3.6' 2 | 3 | services: 4 | 5 | cryptopump_db: 6 | image: andreleibovici/cryptopump_mariadb:latest 7 | container_name: cryptopump_db 8 | environment: 9 | MYSQL_ROOT_PASSWORD: password # change this 10 | ports: 11 | - "3306:3306" 12 | tty: true 13 | cap_add: 14 | - SYS_NICE # CAP_SYS_NICE 15 | volumes: 16 | - db_data:/var/lib/mysql # database files 17 | - /etc/localtime:/etc/localtime:ro # timezone 18 | networks: 19 | - backend 20 | 21 | cryptopump_app: 22 | image: andreleibovici/cryptopump:latest 23 | container_name: cryptopump_app 24 | restart: always 25 | environment: 26 | - DB_USER=root 27 | - DB_PASS=password # change this 28 | - DB_TCP_HOST=cryptopump_db 29 | - DB_PORT=3306 30 | - DB_NAME=cryptopump 31 | ports: 32 | - "8080-8090:8080-8090" 33 | tty: true 34 | volumes: 35 | - config_data:/go/src/cryptopump/config # config files 36 | - /etc/localtime:/etc/localtime:ro # timezone 37 | networks: 38 | - backend 39 | depends_on: 40 | - cryptopump_db 41 | 42 | networks: 43 | backend: 44 | 45 | volumes: 46 | db_data: 47 | config_data: -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.6' 2 | 3 | services: 4 | 5 | cryptopump_db: 6 | image: andreleibovici/cryptopump_db:latest 7 | container_name: cryptopump_db 8 | command: --default-authentication-plugin=mysql_native_password 9 | environment: 10 | MYSQL_ROOT_PASSWORD: password # change this 11 | ports: 12 | - "3306:3306" 13 | tty: true 14 | cap_add: 15 | - SYS_NICE # CAP_SYS_NICE 16 | volumes: 17 | - db_data:/var/lib/mysql # database files 18 | - /etc/localtime:/etc/localtime:ro # timezone 19 | networks: 20 | - backend 21 | 22 | cryptopump_app: 23 | image: andreleibovici/cryptopump:latest 24 | container_name: cryptopump_app 25 | restart: always 26 | environment: 27 | - DB_USER=root 28 | - DB_PASS=password # change this 29 | - DB_TCP_HOST=cryptopump_db 30 | - DB_PORT=3306 31 | - DB_NAME=cryptopump 32 | ports: 33 | - "8080-8090:8080-8090" 34 | tty: true 35 | volumes: 36 | - config_data:/go/src/cryptopump/config # config files 37 | - /etc/localtime:/etc/localtime:ro # timezone 38 | networks: 39 | - backend 40 | depends_on: 41 | - cryptopump_db 42 | 43 | networks: 44 | backend: 45 | 46 | volumes: 47 | db_data: 48 | config_data: -------------------------------------------------------------------------------- /static/stylesheets/cryptopump.css: -------------------------------------------------------------------------------- 1 | .container-input { 2 | background-color: #eee; 3 | border-color: darkgrey; 4 | border-style: solid; 5 | border-width: 1px; 6 | padding: auto; 7 | } 8 | 9 | .div-results { 10 | text-align: right; 11 | } 12 | 13 | .top-buffer { 14 | margin-top: 3px; 15 | } 16 | 17 | .bottom-buffer { 18 | margin-bottom: 3px; 19 | } 20 | 21 | .html { 22 | font-size: 14px; 23 | } 24 | 25 | .badge-danger-addon { 26 | color: white; 27 | } 28 | 29 | .dropdown-addon { 30 | height: 30px; 31 | width: fit-content; 32 | } 33 | 34 | .btn-primary-addon { 35 | background: linear-gradient(to bottom, #007dc1 5%, #0061a7 100%); 36 | background-color: #007dc1; 37 | border: 1px solid #124d77; 38 | border-radius: 3px; 39 | box-shadow: none; 40 | color: #fff; 41 | cursor: pointer; 42 | display: inline-block; 43 | font-size: 14px; 44 | margin-right: 5px; 45 | padding: 6px 24px; 46 | text-decoration: none; 47 | text-shadow: 1px #154682; 48 | } 49 | 50 | .btn-primary-addon:hover { 51 | background: linear-gradient(to bottom, #0061a7 5%, #007dc1 100%); 52 | background-color: #0061a7; 53 | } 54 | 55 | .btn-primary-addon:active { 56 | position: relative; 57 | top: 1px; 58 | } 59 | 60 | .table-wrapper { 61 | display: inline-block; 62 | border: none none 1px; 63 | margin: 0px; 64 | max-height: 400px; 65 | min-width: 300px; 66 | overflow-x: hidden; 67 | overflow-y: auto; 68 | } 69 | 70 | .table-wrapper thead th { 71 | background-color: white; 72 | position: static; 73 | text-align: center; 74 | top: 0; 75 | z-index: 1; 76 | } 77 | -------------------------------------------------------------------------------- /.github/workflows/docker-image.yml: -------------------------------------------------------------------------------- 1 | name: Docker Image CI 2 | 3 | on: 4 | push: 5 | branches: [ main, beta ] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v2 14 | 15 | - name: Set up QEMU 16 | uses: docker/setup-qemu-action@v1 17 | 18 | - name: Login to Docker Hub 19 | uses: docker/login-action@v1 20 | with: 21 | username: ${{ secrets.DOCKER_HUB_USERNAME }} 22 | password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} 23 | 24 | - name: Set up Docker Buildx 25 | uses: docker/setup-buildx-action@v1 26 | id: buildx 27 | with: 28 | install: true 29 | 30 | - name: Build and push 31 | uses: docker/build-push-action@v2 32 | with: 33 | context: . 34 | platforms: linux/amd64,linux/arm/v7 35 | file: ./Dockerfile.cryptopump 36 | push: true 37 | tags: ${{ secrets.DOCKER_HUB_USERNAME }}/cryptopump:latest 38 | 39 | - name: Build and push 40 | uses: docker/build-push-action@v2 41 | with: 42 | context: . 43 | platforms: linux/amd64 44 | file: ./Dockerfile.mysql 45 | push: true 46 | tags: ${{ secrets.DOCKER_HUB_USERNAME }}/cryptopump_db:latest 47 | 48 | - name: Build and push 49 | uses: docker/build-push-action@v2 50 | with: 51 | context: . 52 | platforms: linux/arm/v7 53 | file: ./Dockerfile.mariadb 54 | push: true 55 | tags: ${{ secrets.DOCKER_HUB_USERNAME }}/cryptopump_mariadb:latest 56 | 57 | - name: Image digest 58 | run: echo ${{ steps.docker_build.outputs.digest }} -------------------------------------------------------------------------------- /nodes/nodes_test.go: -------------------------------------------------------------------------------- 1 | package nodes 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/aleibovici/cryptopump/types" 7 | ) 8 | 9 | func TestNode_GetRole(t *testing.T) { 10 | type args struct { 11 | configData *types.Config 12 | sessionData *types.Session 13 | } 14 | tests := []struct { 15 | name string 16 | n Node 17 | args args 18 | }{ 19 | { 20 | name: "success", 21 | n: Node{}, 22 | args: args{ 23 | configData: &types.Config{ 24 | TestNet: true, 25 | }, 26 | sessionData: &types.Session{ 27 | MasterNode: true, 28 | }, 29 | }, 30 | }, 31 | { 32 | name: "success", 33 | n: Node{}, 34 | args: args{ 35 | configData: &types.Config{ 36 | TestNet: false, 37 | }, 38 | sessionData: &types.Session{ 39 | MasterNode: true, 40 | }, 41 | }, 42 | }, 43 | { 44 | name: "success", 45 | n: Node{}, 46 | args: args{ 47 | configData: &types.Config{}, 48 | sessionData: &types.Session{ 49 | MasterNode: false, 50 | }, 51 | }, 52 | }, 53 | } 54 | for _, tt := range tests { 55 | t.Run(tt.name, func(t *testing.T) { 56 | n := Node{} 57 | n.GetRole(tt.args.configData, tt.args.sessionData) 58 | }) 59 | } 60 | } 61 | 62 | func TestNode_ReleaseMasterRole(t *testing.T) { 63 | type args struct { 64 | sessionData *types.Session 65 | } 66 | tests := []struct { 67 | name string 68 | n Node 69 | args args 70 | }{ 71 | { 72 | name: "success", 73 | n: Node{}, 74 | args: args{ 75 | sessionData: &types.Session{ 76 | MasterNode: true, 77 | }, 78 | }, 79 | }, 80 | } 81 | for _, tt := range tests { 82 | t.Run(tt.name, func(t *testing.T) { 83 | n := Node{} 84 | n.ReleaseMasterRole(tt.args.sessionData) 85 | }) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /threads/threads_test.go: -------------------------------------------------------------------------------- 1 | package threads 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/aleibovici/cryptopump/types" 7 | ) 8 | 9 | var sessionData = &types.Session{ 10 | ThreadID: "c2q3mt84a8024t1f6590", 11 | Symbol: "BTCUSDT", 12 | SymbolFiat: "USDT", 13 | MasterNode: false, 14 | } 15 | 16 | func TestThread_Lock(t *testing.T) { 17 | type args struct { 18 | sessionData *types.Session 19 | } 20 | tests := []struct { 21 | name string 22 | tr Thread 23 | args args 24 | want bool 25 | }{ 26 | { 27 | name: "success", 28 | tr: Thread{}, 29 | args: args{ 30 | sessionData: sessionData, 31 | }, 32 | want: true, 33 | }, 34 | { 35 | name: "success", 36 | tr: Thread{}, 37 | args: args{ 38 | sessionData: &types.Session{ 39 | ThreadID: "", 40 | }, 41 | }, 42 | want: false, 43 | }, 44 | } 45 | for _, tt := range tests { 46 | t.Run(tt.name, func(t *testing.T) { 47 | tr := Thread{} 48 | if got := tr.Lock(tt.args.sessionData); got != tt.want { 49 | t.Errorf("Thread.Lock() = %v, want %v", got, tt.want) 50 | } 51 | }) 52 | } 53 | } 54 | 55 | func TestThread_Unlock(t *testing.T) { 56 | type args struct { 57 | sessionData *types.Session 58 | } 59 | tests := []struct { 60 | name string 61 | tr Thread 62 | args args 63 | }{ 64 | { 65 | name: "success", 66 | tr: Thread{}, 67 | args: args{ 68 | sessionData: sessionData, 69 | }, 70 | }, 71 | { 72 | name: "success", 73 | tr: Thread{}, 74 | args: args{ 75 | sessionData: &types.Session{ 76 | ThreadID: "", 77 | }, 78 | }, 79 | }, 80 | } 81 | for _, tt := range tests { 82 | t.Run(tt.name, func(t *testing.T) { 83 | tr := Thread{} 84 | tr.Unlock(tt.args.sessionData) 85 | }) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /.github/workflows/codacy-analysis.yml: -------------------------------------------------------------------------------- 1 | # This workflow checks out code, performs a Codacy security scan 2 | # and integrates the results with the 3 | # GitHub Advanced Security code scanning feature. For more information on 4 | # the Codacy security scan action usage and parameters, see 5 | # https://github.com/codacy/codacy-analysis-cli-action. 6 | # For more information on Codacy Analysis CLI in general, see 7 | # https://github.com/codacy/codacy-analysis-cli. 8 | 9 | name: Codacy Security Scan 10 | 11 | on: 12 | push: 13 | branches: [ main, beta ] 14 | pull_request: 15 | # The branches below must be a subset of the branches above 16 | branches: [ main ] 17 | schedule: 18 | - cron: '21 4 * * 1' 19 | 20 | jobs: 21 | codacy-security-scan: 22 | name: Codacy Security Scan 23 | runs-on: ubuntu-latest 24 | steps: 25 | # Checkout the repository to the GitHub Actions runner 26 | - name: Checkout code 27 | uses: actions/checkout@v2 28 | 29 | # Execute Codacy Analysis CLI and generate a SARIF output with the security issues identified during the analysis 30 | - name: Run Codacy Analysis CLI 31 | uses: codacy/codacy-analysis-cli-action@1.1.0 32 | with: 33 | # Check https://github.com/codacy/codacy-analysis-cli#project-token to get your project token from your Codacy repository 34 | # You can also omit the token and run the tools that support default configurations 35 | project-token: ${{ secrets.CODACY_PROJECT_TOKEN }} 36 | verbose: true 37 | output: results.sarif 38 | format: sarif 39 | # Adjust severity of non-security issues 40 | gh-code-scanning-compat: true 41 | # Force 0 exit code to allow SARIF file generation 42 | # This will handover control about PR rejection to the GitHub side 43 | max-allowed-issues: 2147483647 44 | 45 | # Upload the SARIF file generated in the previous step 46 | - name: Upload SARIF results file 47 | uses: github/codeql-action/upload-sarif@v1 48 | with: 49 | sarif_file: results.sarif 50 | -------------------------------------------------------------------------------- /documentation/HowToInstall.md: -------------------------------------------------------------------------------- 1 | ## HOW TO INSTALL 2 | 3 | Cryptopump can be used on Windows or Linux (with a MySQL or MariaDB database) or in a self-contained Docker environment. 4 | 5 | ### DOCKER: 6 | 7 | #### - CryptoPump is now available as a self-contained Docker container set for linux/amd64 and linux/arm/v7 (Raspberry Pi). Check it out at https://hub.docker.com/repository/docker/andreleibovici/cryptopump 8 | 9 | This is the easiest way to get CryptoPump up and running. The Docker Compose file provides all the necessary components to run the system. 10 | 11 | 1 - Create a directory for the project. 12 | 13 | 2 - Copy the file docker-compose.yml to cryptopump directory, and edit the file, replacing for "MYSQL_ROOT_PASSWORD" and "DB_PASS" with a password of your choice. Save the file. 14 | 15 | 3 - Execute docker-compose up -d 16 | 17 | Docker Compose will download all the necessary images, including the database, and start Cryptopump. A persistent volume named "cryptopump_db_data" will be created and host the database files. 18 | 19 | Cryptopump can be accessed by pointing your browser to http://localhost:8080. 20 | 21 | ### WINDOWS: 22 | 23 | On Windows MYSQL can be used with Docker Desktop, use port 3306 and cryptopump as the database name. 24 | User should be root and you can set a password. 25 | 26 | Start the command prompt and do: 27 | ``` 28 | $ docker run -d --name cryptopump -p 3306:3306 -e MYSQL_ROOT_PASSWORD=password mysql:latest 29 | ``` 30 | 31 | Now with your MYSQL server running on docker find the docker container ID 32 | ``` 33 | $ docker ps 34 | ``` 35 | 36 | and now add the cryptopump .sql file that's inside mysql folder of cryptopump to the database 37 | ``` 38 | $ docker exec -i mysql -uroot -p cryptopump < c:\path\to\cryptopump\mysql\cryptopump.sql 39 | ``` 40 | 41 | Now export the environment so the cryptopump executable is able to connect to the MYSQL server. 42 | 43 | Using windows powershell 44 | ``` 45 | $env:DB_TCP_HOST="127.0.0.1" 46 | $env:DB_PORT="3306" 47 | $env:DB_USER="root" 48 | $env:DB_PASS="" 49 | $env:DB_NAME="cryptopump" 50 | $env:PORT="8090" 51 | ``` 52 | 53 | Now set your BINANCE API key under cryptopump configuration file cryptopump\config\config_default.yaml 54 | Set it with apikey: and the secret under secretkey: 55 | 56 | Withing the same shell start cryptopump and if everything worked ok you should see a browser window with cryptopump running. -------------------------------------------------------------------------------- /.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: '22 9 * * 5' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'go' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 37 | # Learn more: 38 | # 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 39 | 40 | steps: 41 | - name: Checkout repository 42 | uses: actions/checkout@v2 43 | 44 | # Initializes the CodeQL tools for scanning. 45 | - name: Initialize CodeQL 46 | uses: github/codeql-action/init@v1 47 | with: 48 | languages: ${{ matrix.language }} 49 | # If you wish to specify custom queries, you can do so here or in a config file. 50 | # By default, queries listed here will override any specified in a config file. 51 | # Prefix the list here with "+" to use these queries and those in the config file. 52 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 53 | 54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 55 | # If this step fails, then you should remove it and run the build manually (see below) 56 | - name: Autobuild 57 | uses: github/codeql-action/autobuild@v1 58 | 59 | # ℹ️ Command-line programs to run using the OS shell. 60 | # 📚 https://git.io/JvXDl 61 | 62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 63 | # and modify them (or add more) to build your code if your project 64 | # uses a compiled language 65 | 66 | #- run: | 67 | # make bootstrap 68 | # make release 69 | 70 | - name: Perform CodeQL Analysis 71 | uses: github/codeql-action/analyze@v1 72 | -------------------------------------------------------------------------------- /functions/functions_test.go: -------------------------------------------------------------------------------- 1 | package functions 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestFloat64ToStr(t *testing.T) { 8 | type args struct { 9 | value float64 10 | prec int 11 | } 12 | tests := []struct { 13 | name string 14 | args args 15 | want string 16 | }{ 17 | { 18 | name: "success", 19 | args: args{ 20 | value: 1.11111111, 21 | prec: 8, 22 | }, 23 | want: "1.11111111", 24 | }, 25 | } 26 | for _, tt := range tests { 27 | t.Run(tt.name, func(t *testing.T) { 28 | if got := Float64ToStr(tt.args.value, tt.args.prec); got != tt.want { 29 | t.Errorf("Float64ToStr() = %v, want %v", got, tt.want) 30 | } 31 | }) 32 | } 33 | } 34 | 35 | func TestIntToFloat64(t *testing.T) { 36 | type args struct { 37 | value int 38 | } 39 | tests := []struct { 40 | name string 41 | args args 42 | want float64 43 | }{ 44 | { 45 | name: "success", 46 | args: args{ 47 | value: 1, 48 | }, 49 | want: 1.00, 50 | }, 51 | } 52 | for _, tt := range tests { 53 | t.Run(tt.name, func(t *testing.T) { 54 | if got := IntToFloat64(tt.args.value); got != tt.want { 55 | t.Errorf("IntToFloat64() = %v, want %v", got, tt.want) 56 | } 57 | }) 58 | } 59 | } 60 | 61 | func TestStrToInt(t *testing.T) { 62 | type args struct { 63 | value string 64 | } 65 | tests := []struct { 66 | name string 67 | args args 68 | wantR int 69 | }{ 70 | { 71 | name: "success", 72 | args: args{ 73 | value: "1", 74 | }, 75 | wantR: 1, 76 | }, 77 | } 78 | for _, tt := range tests { 79 | t.Run(tt.name, func(t *testing.T) { 80 | if gotR := StrToInt(tt.args.value); gotR != tt.wantR { 81 | t.Errorf("StrToInt() = %v, want %v", gotR, tt.wantR) 82 | } 83 | }) 84 | } 85 | } 86 | 87 | func TestStrToFloat64(t *testing.T) { 88 | type args struct { 89 | value string 90 | } 91 | tests := []struct { 92 | name string 93 | args args 94 | wantR float64 95 | }{ 96 | { 97 | name: "success", 98 | args: args{ 99 | value: "11.11", 100 | }, 101 | wantR: 11.11, 102 | }, 103 | } 104 | for _, tt := range tests { 105 | t.Run(tt.name, func(t *testing.T) { 106 | if gotR := StrToFloat64(tt.args.value); gotR != tt.wantR { 107 | t.Errorf("StrToFloat64() = %v, want %v", gotR, tt.wantR) 108 | } 109 | }) 110 | } 111 | } 112 | 113 | func TestGetThreadID(t *testing.T) { 114 | tests := []struct { 115 | name string 116 | want string 117 | }{ 118 | { 119 | name: "success", 120 | want: "", 121 | }, 122 | } 123 | for _, tt := range tests { 124 | t.Run(tt.name, func(t *testing.T) { 125 | if got := GetThreadID(); got == "" { 126 | t.Errorf("GetThreadID() = %v, want %v", got, tt.want) 127 | } 128 | }) 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /logger/logger_test.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/aleibovici/cryptopump/types" 7 | ) 8 | 9 | func TestLogEntry_Do(t *testing.T) { 10 | type fields struct { 11 | Config *types.Config 12 | Market *types.Market 13 | Session *types.Session 14 | Order *types.Order 15 | Message string 16 | LogLevel string 17 | } 18 | tests := []struct { 19 | name string 20 | fields fields 21 | }{ 22 | { 23 | name: "success", 24 | fields: fields{ 25 | Config: &types.Config{}, 26 | Market: &types.Market{}, 27 | Session: &types.Session{}, 28 | Order: &types.Order{}, 29 | Message: "UP", 30 | LogLevel: "infoLevel", 31 | }, 32 | }, 33 | { 34 | name: "success", 35 | fields: fields{ 36 | Config: &types.Config{}, 37 | Market: &types.Market{}, 38 | Session: &types.Session{}, 39 | Order: &types.Order{}, 40 | Message: "BUY", 41 | LogLevel: "infoLevel", 42 | }, 43 | }, 44 | { 45 | name: "success", 46 | fields: fields{ 47 | Config: &types.Config{}, 48 | Market: &types.Market{}, 49 | Session: &types.Session{}, 50 | Order: &types.Order{}, 51 | Message: "SELL", 52 | LogLevel: "infoLevel", 53 | }, 54 | }, 55 | { 56 | name: "success", 57 | fields: fields{ 58 | Config: &types.Config{ 59 | Debug: true, 60 | }, 61 | Market: &types.Market{}, 62 | Session: &types.Session{}, 63 | Order: &types.Order{}, 64 | Message: "CANCELED", 65 | LogLevel: "infoLevel", 66 | }, 67 | }, 68 | { 69 | name: "success", 70 | fields: fields{ 71 | Config: &types.Config{ 72 | Debug: true, 73 | }, 74 | Market: &types.Market{}, 75 | Session: &types.Session{}, 76 | Order: &types.Order{}, 77 | Message: "", 78 | LogLevel: "infoLevel", 79 | }, 80 | }, 81 | { 82 | name: "success", 83 | fields: fields{ 84 | Config: &types.Config{}, 85 | Market: &types.Market{}, 86 | Session: &types.Session{}, 87 | Order: &types.Order{}, 88 | Message: "", 89 | LogLevel: "debugLevel", 90 | }, 91 | }, 92 | { 93 | name: "success", 94 | fields: fields{ 95 | Config: &types.Config{}, 96 | Market: &types.Market{}, 97 | Session: &types.Session{}, 98 | Order: &types.Order{}, 99 | Message: "", 100 | LogLevel: "", 101 | }, 102 | }, 103 | } 104 | for _, tt := range tests { 105 | t.Run(tt.name, func(t *testing.T) { 106 | logEntry := LogEntry{ 107 | Config: tt.fields.Config, 108 | Market: tt.fields.Market, 109 | Session: tt.fields.Session, 110 | Order: tt.fields.Order, 111 | Message: tt.fields.Message, 112 | LogLevel: tt.fields.LogLevel, 113 | } 114 | logEntry.Do() 115 | }) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /threads/threads.go: -------------------------------------------------------------------------------- 1 | package threads 2 | 3 | import ( 4 | "os" 5 | "time" 6 | 7 | "github.com/aleibovici/cryptopump/functions" 8 | "github.com/aleibovici/cryptopump/logger" 9 | "github.com/aleibovici/cryptopump/mysql" 10 | "github.com/aleibovici/cryptopump/nodes" 11 | "github.com/aleibovici/cryptopump/types" 12 | ) 13 | 14 | // Thread locking control 15 | type Thread struct{} 16 | 17 | // Terminate thread 18 | func (Thread) Terminate(sessionData *types.Session, message string) { 19 | 20 | if message != "" { 21 | 22 | logger.LogEntry{ /* Log Entry */ 23 | Config: nil, 24 | Market: nil, 25 | Session: sessionData, 26 | Order: &types.Order{}, 27 | Message: message, 28 | LogLevel: "DebugLevel", 29 | }.Do() 30 | 31 | } 32 | 33 | /* Verify wether buying/selling to allow graceful session exit */ 34 | for sessionData.Busy { 35 | 36 | time.Sleep(time.Millisecond * 200) 37 | 38 | } 39 | 40 | /* Release node role if Master */ 41 | if sessionData.MasterNode { 42 | 43 | nodes.Node{}.ReleaseMasterRole(sessionData) 44 | 45 | } 46 | 47 | // Unlock existing thread 48 | Thread{}.Unlock(sessionData) 49 | 50 | /* Delete session from Session table */ 51 | if err := mysql.DeleteSession(sessionData); err != nil { 52 | 53 | logger.LogEntry{ /* Log Entry */ 54 | Config: nil, 55 | Market: nil, 56 | Session: sessionData, 57 | Order: &types.Order{}, 58 | Message: "Clean Shutdown Failed", 59 | LogLevel: "DebugLevel", 60 | }.Do() 61 | 62 | } else { 63 | 64 | logger.LogEntry{ /* Log Entry */ 65 | Config: nil, 66 | Market: nil, 67 | Session: sessionData, 68 | Order: &types.Order{}, 69 | Message: "Clean Shutdown", 70 | LogLevel: "InfoLevel", 71 | }.Do() 72 | 73 | } 74 | 75 | os.Exit(1) 76 | 77 | } 78 | 79 | // Lock existing thread 80 | func (Thread) Lock(sessionData *types.Session) bool { 81 | 82 | if sessionData.ThreadID == "" { 83 | 84 | return false 85 | 86 | } 87 | 88 | filename := sessionData.ThreadID + ".lock" 89 | 90 | if _, err := os.Stat(filename); err == nil { 91 | 92 | return false 93 | 94 | } else if os.IsNotExist(err) { 95 | 96 | var file, err = os.Create(filename) 97 | 98 | if err != nil { 99 | 100 | return false 101 | 102 | } 103 | 104 | file.Close() 105 | 106 | return true 107 | 108 | } 109 | 110 | return false 111 | 112 | } // //// // ExitThreadID Cleanly exit a Thread 113 | 114 | // Unlock existing thread 115 | func (Thread) Unlock(sessionData *types.Session) { 116 | 117 | if err := os.Remove(sessionData.ThreadID + ".lock"); err != nil { 118 | 119 | logger.LogEntry{ /* Log Entry */ 120 | Config: nil, 121 | Market: nil, 122 | Session: sessionData, 123 | Order: &types.Order{}, 124 | Message: functions.GetFunctionName() + " - " + err.Error(), 125 | LogLevel: "DebugLevel", 126 | }.Do() 127 | 128 | } 129 | 130 | } 131 | -------------------------------------------------------------------------------- /loader/loader_test.go: -------------------------------------------------------------------------------- 1 | package loader 2 | 3 | import ( 4 | "database/sql" 5 | "log" 6 | "regexp" 7 | "testing" 8 | "time" 9 | 10 | "github.com/DATA-DOG/go-sqlmock" 11 | "github.com/aleibovici/cryptopump/types" 12 | "github.com/paulbellamy/ratecounter" 13 | ) 14 | 15 | // NewMock returns a new mock database and sqlmock.Sqlmock 16 | func NewMock() (*sql.DB, sqlmock.Sqlmock) { 17 | 18 | db, mock, err := sqlmock.New() 19 | if err != nil { 20 | log.Fatalf("an error '%s' was not expected when opening a stub database connection", err) 21 | } 22 | 23 | return db, mock 24 | } 25 | 26 | func TestLoadSessionDataAdditionalComponents(t *testing.T) { 27 | type args struct { 28 | sessionData *types.Session 29 | marketData *types.Market 30 | configData *types.Config 31 | } 32 | 33 | db, mock := NewMock() 34 | defer db.Close() 35 | 36 | tests := []struct { 37 | name string 38 | args args 39 | want []byte 40 | wantErr bool 41 | }{ 42 | { 43 | name: "success", 44 | args: args{ 45 | sessionData: &types.Session{ 46 | ThreadID: "c683ok5mk1u1120gnmmg", 47 | Symbol: "BTCUSD", 48 | Db: db, 49 | RateCounter: ratecounter.NewRateCounter(5 * time.Second), 50 | Global: &types.Global{Profit: 0}, 51 | }, 52 | marketData: &types.Market{}, 53 | configData: &types.Config{}, 54 | }, 55 | want: []byte{}, 56 | wantErr: false, 57 | }, 58 | } 59 | 60 | columns := []string{"orderID", "cumulativeQuoteQty", "price", "executedQuantity"} 61 | mock.ExpectBegin() /* begin transaction */ 62 | mock.ExpectQuery(regexp.QuoteMeta("call cryptopump.GetThreadTransactionByThreadID(?)")). /* call procedure */ 63 | WithArgs(tests[0].args.sessionData.ThreadID). /* with args */ 64 | WillReturnRows(sqlmock.NewRows(columns)) /* return 1 row */ 65 | 66 | for _, tt := range tests { 67 | t.Run(tt.name, func(t *testing.T) { 68 | _, err := LoadSessionDataAdditionalComponents(tt.args.sessionData, tt.args.marketData, tt.args.configData) 69 | if (err != nil) != tt.wantErr { 70 | t.Errorf("LoadSessionDataAdditionalComponents() error = %v, wantErr %v", err, tt.wantErr) 71 | return 72 | } 73 | }) 74 | } 75 | } 76 | 77 | func TestLoadSessionDataAdditionalComponentsAsync(t *testing.T) { 78 | type args struct { 79 | sessionData *types.Session 80 | } 81 | 82 | db, mock := NewMock() 83 | defer db.Close() 84 | 85 | tests := []struct { 86 | name string 87 | args args 88 | }{ 89 | { 90 | name: "success", 91 | args: args{ 92 | sessionData: &types.Session{ 93 | Db: db, 94 | Global: &types.Global{}, 95 | }, 96 | }, 97 | }, 98 | } 99 | 100 | columns := []string{"profit", "profitNet", "profitPct", "transactTime"} 101 | mock.ExpectBegin() /* begin transaction */ 102 | mock.ExpectQuery(regexp.QuoteMeta("call cryptopump.GetGlobal()")). /* call procedure */ 103 | WillReturnRows(sqlmock.NewRows(columns)) /* return 1 row */ 104 | 105 | for _, tt := range tests { 106 | t.Run(tt.name, func(t *testing.T) { 107 | LoadSessionDataAdditionalComponentsAsync(tt.args.sessionData) 108 | }) 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /markets/markets_test.go: -------------------------------------------------------------------------------- 1 | package markets 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/aleibovici/cryptopump/exchange" 7 | "github.com/aleibovici/cryptopump/functions" 8 | "github.com/aleibovici/cryptopump/logger" 9 | "github.com/aleibovici/cryptopump/types" 10 | "github.com/sdcoffey/techan" 11 | "github.com/spf13/viper" 12 | ) 13 | 14 | var configData = &types.Config{} 15 | 16 | var sessionData = &types.Session{} 17 | 18 | var marketData = &types.Market{ 19 | Series: &techan.TimeSeries{}, 20 | } 21 | 22 | func init() { 23 | 24 | viperData := &types.ViperData{ /* Viper Configuration */ 25 | V1: viper.New(), /* Session configurations file */ 26 | V2: viper.New(), /* Global configurations file */ 27 | } 28 | 29 | viperData.V1.SetConfigType("yml") /* Set the type of the configurations file */ 30 | viperData.V1.AddConfigPath("../config") /* Set the path to look for the configurations file */ 31 | viperData.V1.SetConfigName("config") /* Set the file name of the configurations file */ 32 | if err := viperData.V1.ReadInConfig(); err != nil { 33 | 34 | logger.LogEntry{ /* Log Entry */ 35 | Config: nil, 36 | Market: nil, 37 | Session: nil, 38 | Order: &types.Order{}, 39 | Message: functions.GetFunctionName() + " - " + err.Error(), 40 | LogLevel: "DebugLevel", 41 | }.Do() 42 | 43 | } 44 | viperData.V1.WatchConfig() 45 | 46 | configData = functions.GetConfigData(viperData, sessionData) 47 | configData.TestNet = true 48 | 49 | exchange.GetClient(configData, sessionData) 50 | 51 | } 52 | 53 | func TestData_LoadKlinePast(t *testing.T) { 54 | type fields struct { 55 | Kline types.WsKline 56 | } 57 | type args struct { 58 | configData *types.Config 59 | marketData *types.Market 60 | sessionData *types.Session 61 | } 62 | tests := []struct { 63 | name string 64 | fields fields 65 | args args 66 | }{ 67 | { 68 | name: "success", 69 | fields: fields{ 70 | Kline: types.WsKline{}, 71 | }, 72 | args: args{ 73 | configData: configData, 74 | marketData: marketData, 75 | sessionData: sessionData, 76 | }, 77 | }, 78 | } 79 | for _, tt := range tests { 80 | t.Run(tt.name, func(t *testing.T) { 81 | d := Data{ 82 | Kline: tt.fields.Kline, 83 | } 84 | d.LoadKlinePast(tt.args.configData, tt.args.marketData, tt.args.sessionData) 85 | }) 86 | } 87 | } 88 | 89 | func TestData_LoadKline(t *testing.T) { 90 | type fields struct { 91 | Kline types.WsKline 92 | } 93 | type args struct { 94 | configData *types.Config 95 | sessionData *types.Session 96 | marketData *types.Market 97 | } 98 | tests := []struct { 99 | name string 100 | fields fields 101 | args args 102 | }{ 103 | { 104 | name: "success", 105 | fields: fields{ 106 | Kline: types.WsKline{}, 107 | }, 108 | args: args{ 109 | configData: configData, 110 | marketData: marketData, 111 | sessionData: sessionData, 112 | }, 113 | }, 114 | } 115 | 116 | sessionData.Symbol = configData.Symbol 117 | 118 | for _, tt := range tests { 119 | t.Run(tt.name, func(t *testing.T) { 120 | d := Data{ 121 | Kline: tt.fields.Kline, 122 | } 123 | d.LoadKline(tt.args.configData, tt.args.sessionData, tt.args.marketData) 124 | }) 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /nodes/nodes.go: -------------------------------------------------------------------------------- 1 | package nodes 2 | 3 | import ( 4 | "os" 5 | "time" 6 | 7 | "github.com/aleibovici/cryptopump/functions" 8 | "github.com/aleibovici/cryptopump/logger" 9 | "github.com/aleibovici/cryptopump/mysql" 10 | "github.com/aleibovici/cryptopump/types" 11 | ) 12 | 13 | // Node functions 14 | type Node struct{} 15 | 16 | // GetRole retrieve correct role for node 17 | func (Node) GetRole( 18 | configData *types.Config, 19 | sessionData *types.Session) { 20 | 21 | var filename = "master.lock" 22 | var err error 23 | 24 | /* Conditional defer logging when there is an error retriving data */ 25 | defer func() { 26 | if err != nil { 27 | logger.LogEntry{ /* Log Entry */ 28 | Config: nil, 29 | Market: nil, 30 | Session: sessionData, 31 | Order: &types.Order{}, 32 | Message: functions.GetFunctionName() + " - " + err.Error(), 33 | LogLevel: "DebugLevel", 34 | }.Do() 35 | } 36 | }() 37 | 38 | /* If TestNet is enabled will not check for "master.lock" to not affect production systems */ 39 | if configData.TestNet { 40 | 41 | sessionData.MasterNode = false 42 | return 43 | 44 | } 45 | 46 | /* If Master Node already set to True */ 47 | if sessionData.MasterNode { 48 | 49 | /* Set access time and modified time of the file to the current time */ 50 | if err = os.Chtimes(filename, time.Now().Local(), time.Now().Local()); err != nil { 51 | 52 | return 53 | 54 | } 55 | 56 | return 57 | 58 | } 59 | 60 | /* If Master Node set to False */ 61 | if file, err := os.Stat(filename); err == nil { /* Check if "master.lock" is created and modified time */ 62 | 63 | sessionData.MasterNode = false 64 | 65 | if time.Duration(time.Since(file.ModTime()).Seconds()) > 100 { /* Remove "master.lock" if old modified time */ 66 | 67 | if err := os.Remove(filename); err != nil { 68 | 69 | return 70 | 71 | } 72 | 73 | } 74 | 75 | } else if os.IsNotExist(err) { /* Check if "master.lock" is created and modified time */ 76 | 77 | var file *os.File 78 | file, err = os.Create(filename) 79 | 80 | file.Close() 81 | 82 | sessionData.MasterNode = true 83 | 84 | } 85 | 86 | } 87 | 88 | // ReleaseMasterRole Release node role if Master 89 | func (Node) ReleaseMasterRole(sessionData *types.Session) { 90 | 91 | /* Release node role if Master */ 92 | if sessionData.MasterNode { 93 | 94 | var filename = "master.lock" 95 | 96 | if err := os.Remove(filename); err != nil { 97 | 98 | logger.LogEntry{ /* Log Entry */ 99 | Config: nil, 100 | Market: nil, 101 | Session: sessionData, 102 | Order: &types.Order{}, 103 | Message: functions.GetFunctionName() + " - " + err.Error(), 104 | LogLevel: "DebugLevel", 105 | }.Do() 106 | 107 | } 108 | 109 | } 110 | 111 | } 112 | 113 | // CheckStatus check for errors on node 114 | func (Node) CheckStatus(configData *types.Config, 115 | sessionData *types.Session) { 116 | 117 | /* Check last WsBookTicker */ 118 | if time.Duration(time.Since(sessionData.LastWsBookTickerTime).Seconds()) > time.Duration(30) { 119 | 120 | sessionData.Status = true 121 | 122 | } 123 | 124 | /* Check last WsKline */ 125 | if time.Duration(time.Since(sessionData.LastWsKlineTime).Seconds()) > time.Duration(100) { 126 | 127 | sessionData.Status = true 128 | 129 | } 130 | 131 | /* Update Session table */ 132 | if err := mysql.UpdateSession( 133 | configData, 134 | sessionData); err != nil { 135 | 136 | logger.LogEntry{ /* Log Entry */ 137 | Config: configData, 138 | Market: nil, 139 | Session: sessionData, 140 | Order: &types.Order{}, 141 | Message: functions.GetFunctionName() + " - " + err.Error(), 142 | LogLevel: "DebugLevel", 143 | }.Do() 144 | 145 | } 146 | 147 | sessionData.Status = false 148 | } 149 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CryptoPump 2 | 3 | CryptoPump is a cryptocurrency trading tool that focuses on extremely high speed and flexibility. 4 | 5 | [![Go](https://github.com/aleibovici/cryptopump/actions/workflows/go.yml/badge.svg?branch=main)](https://github.com/aleibovici/cryptopump/actions/workflows/go.yml) 6 | [![Go Report Card](https://goreportcard.com/badge/github.com/aleibovici/cryptopump)](https://goreportcard.com/report/github.com/aleibovici/cryptopump) 7 | [![Coverage Status](https://coveralls.io/repos/github/aleibovici/cryptopump/badge.svg?branch=main)](https://coveralls.io/github/aleibovici/cryptopump?branch=main) 8 | [![Codacy Security Scan](https://github.com/aleibovici/cryptopump/actions/workflows/codacy-analysis.yml/badge.svg?branch=main)](https://github.com/aleibovici/cryptopump/actions/workflows/codacy-analysis.yml) 9 | [![Maintainability](https://api.codeclimate.com/v1/badges/62f86b5a3d94b1e2e355/maintainability)](https://codeclimate.com/github/aleibovici/cryptopump/maintainability) 10 | 11 | ![](https://github.com/aleibovici/img/blob/main/cryptopump_screen.png?raw=true) 12 | 13 | Do not risk money which you are afraid to lose. USE THE SOFTWARE AT YOUR OWN RISK. THE AUTHORS AND ALL AFFILIATES ASSUME NO RESPONSIBILITY FOR YOUR TRADING RESULTS. 14 | 15 | Always start by running a this trading tool in Dry-run or TestNet and do not engage money before you understand how it works and what profit/loss you should expect. 16 | #### - CryptoPump is now available as a self-contained Docker container set for linux/amd64 and linux/arm/v7 (Raspberry Pi). Check it out at https://hub.docker.com/repository/docker/andreleibovici/cryptopump 17 | 18 | - CryptoPump is a cryptocurrency trading tool that focuses on extremely high speed and flexibility. The algorithms utilize Go Language and the exchange WebSockets to react in real-time to market movements based on Bollinger statistical analysis and pre-defined profit margins. 19 | 20 | - CryptoPump is easy to deploy and with Docker it can be up and running in minutes. 21 | 22 | - CryptoPump calculates the Relative Strength Index (3,7,14), MACD index, and Market Volume Direction, allowing you to configure buying, selling, and holding thresholds. 23 | 24 | - CryptoPump also provides different configuration settings for operating in downmarket, such as specifying the amount to buy in the downmarket when to change purchase behavior and thresholds. 25 | 26 | - CryptoPump supports all cryptocurrency pairs and provides the ability to define the exchange commission when calculating profit and when to sell. 27 | 28 | - CryptoPump also provides DryRun mode, the ability to use Binance TestNet for testing, Telegram bot integration, Time enforcement, Sell-to-cover, and more. () 29 | 30 | - CryptoPump currently only support Binance API but it was developed to allow easy implementation of additional exchanges. 31 | 32 | - CryptoPump has a native Telegram bot that accepts commands /stop /sell /buy and /report. Telegram will also alert you if any issues happen. 33 | 34 | ![](https://github.com/aleibovici/img/blob/b2c9390494906b8e83635a5f320dd48f67a48fbd/telegram_screenshot.jpg?raw=true) 35 | 36 | - CryptoPump requires MySQL to persist data and transactions, and the .sql file to create the structure can be found in the MySQL folder (cryptopump.sql). I use MySQL with Docker in the same machine Cryptopump is running, and it performs well. Cloud-based MySQL instances are also supported. The environment variables are in launch.json if Visual Studio Code is in use; optionally, the following environment variables set DB_USER, DB_PASS, DB_TCP_HOST, DB_PORT, DB_NAME. For using MySQL with docker go here (). (refer to HOW TO INSTALL file) 37 | 38 | - For each instance of the code, a new HTTP port is opened, starting with 8080, 8081, 8082 (or starting with the port defined by environment variable PORT). Just point your browser to the address, and you should get the session configuration page and the Bollinger and Exchange data. 39 | -------------------------------------------------------------------------------- /plotter/plotter_test.go: -------------------------------------------------------------------------------- 1 | package plotter 2 | 3 | import ( 4 | "html/template" 5 | "testing" 6 | 7 | "github.com/aleibovici/cryptopump/types" 8 | "github.com/go-echarts/go-echarts/v2/charts" 9 | "github.com/go-echarts/go-echarts/v2/opts" 10 | ) 11 | 12 | func Test_klineBase(t *testing.T) { 13 | type args struct { 14 | name string 15 | XAxis []string 16 | klineData []opts.KlineData 17 | } 18 | tests := []struct { 19 | name string 20 | args args 21 | want *charts.Kline 22 | }{ 23 | { 24 | name: "success", 25 | args: args{ 26 | name: "", 27 | XAxis: []string{}, 28 | klineData: []opts.KlineData{}, 29 | }, 30 | want: &charts.Kline{}, 31 | }, 32 | } 33 | for _, tt := range tests { 34 | t.Run(tt.name, func(t *testing.T) { 35 | if got := klineBase(tt.args.name, tt.args.XAxis, tt.args.klineData); got != nil { 36 | return 37 | } 38 | }) 39 | } 40 | } 41 | 42 | func Test_lineBase(t *testing.T) { 43 | type args struct { 44 | name string 45 | XAxis []string 46 | lineData []opts.LineData 47 | color string 48 | } 49 | tests := []struct { 50 | name string 51 | args args 52 | want *charts.Line 53 | }{ 54 | { 55 | name: "success", 56 | args: args{ 57 | name: "MA7", 58 | XAxis: make([]string, 0), 59 | lineData: make([]opts.LineData, 0), 60 | color: "blue", 61 | }, 62 | want: &charts.Line{}, 63 | }, 64 | } 65 | for _, tt := range tests { 66 | t.Run(tt.name, func(t *testing.T) { 67 | if got := lineBase(tt.args.name, tt.args.XAxis, tt.args.lineData, tt.args.color); got != nil { 68 | return 69 | } 70 | }) 71 | } 72 | } 73 | 74 | func TestData_Plot(t *testing.T) { 75 | type fields struct { 76 | Kline types.WsKline 77 | } 78 | type args struct { 79 | sessionData *types.Session 80 | } 81 | tests := []struct { 82 | name string 83 | fields fields 84 | args args 85 | wantHTMLSnippet template.HTML 86 | }{ 87 | { 88 | name: "success", 89 | fields: fields{ 90 | Kline: types.WsKline{}, 91 | }, 92 | args: args{ 93 | sessionData: &types.Session{ 94 | KlineData: []types.KlineData{}, 95 | }, 96 | }, 97 | wantHTMLSnippet: "", 98 | }, 99 | } 100 | for _, tt := range tests { 101 | t.Run(tt.name, func(t *testing.T) { 102 | d := Data{ 103 | Kline: tt.fields.Kline, 104 | } 105 | if gotHTMLSnippet := d.Plot(tt.args.sessionData); gotHTMLSnippet != "" { 106 | return 107 | } 108 | }) 109 | } 110 | } 111 | 112 | func TestData_LoadKline(t *testing.T) { 113 | type fields struct { 114 | Kline types.WsKline 115 | } 116 | type args struct { 117 | sessionData *types.Session 118 | marketData *types.Market 119 | } 120 | tests := []struct { 121 | name string 122 | fields fields 123 | args args 124 | }{ 125 | { 126 | name: "success", 127 | fields: fields{ 128 | Kline: types.WsKline{ 129 | StartTime: 1630474980000, 130 | EndTime: 1630475039999, 131 | Symbol: "BTCUSDT", 132 | Interval: "1m", 133 | FirstTradeID: 1291659, 134 | LastTradeID: 1291679, 135 | Open: "47397.49000000", 136 | Close: "47372.73000000", 137 | High: "47397.49000000", 138 | Low: "47354.42000000", 139 | Volume: "0.20152400", 140 | TradeNum: 21, 141 | IsFinal: true, 142 | QuoteVolume: "9547.12114436", 143 | ActiveBuyVolume: "9499.76666436", 144 | ActiveBuyQuoteVolume: "9499.76666436", 145 | }, 146 | }, 147 | args: args{ 148 | sessionData: &types.Session{}, 149 | marketData: &types.Market{}, 150 | }, 151 | }, 152 | } 153 | for _, tt := range tests { 154 | t.Run(tt.name, func(t *testing.T) { 155 | d := Data{ 156 | Kline: tt.fields.Kline, 157 | } 158 | d.LoadKline(tt.args.sessionData, tt.args.marketData) 159 | }) 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /logger/logger.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | 8 | "github.com/aleibovici/cryptopump/types" 9 | 10 | log "github.com/sirupsen/logrus" 11 | ) 12 | 13 | // LogEntry struct 14 | type LogEntry struct { 15 | Config *types.Config /* Config struct */ 16 | Market *types.Market /* Market struct */ 17 | Session *types.Session /* Session struct */ 18 | Order *types.Order /* Order struct */ 19 | Message string /* Error message */ 20 | LogLevel string /* Logrus log level */ 21 | } 22 | 23 | /* Level define log filename and the log level for the log entry */ 24 | func (logEntry LogEntry) level() (filename string) { 25 | 26 | /* Define the log level for the entry */ 27 | switch strings.ToLower(logEntry.LogLevel) { 28 | case "infolevel": 29 | log.SetLevel(log.InfoLevel) 30 | filename = "cryptopump.log" 31 | case "debuglevel": 32 | log.SetLevel(log.DebugLevel) 33 | filename = "cryptopump_debug.log" 34 | default: 35 | log.SetLevel(log.DebugLevel) 36 | filename = "cryptopump_debug.log" 37 | } 38 | 39 | return filename 40 | 41 | } 42 | 43 | /* Set the log formatter */ 44 | func (logEntry LogEntry) formatter() { 45 | 46 | /* Log as JSON instead of the default ASCII formatter */ 47 | log.SetFormatter(&log.TextFormatter{ 48 | DisableColors: false, 49 | TimestampFormat: "2006-01-02 15:04:05", 50 | FullTimestamp: true, 51 | DisableSorting: false, 52 | }) 53 | 54 | } 55 | 56 | // Do is LogEntry method to run system logging 57 | func (logEntry LogEntry) Do() { 58 | 59 | var err error 60 | var file *os.File 61 | 62 | logEntry.formatter() /* Set the log formatter */ 63 | filename := logEntry.level() /* Define the log level for the entry */ 64 | 65 | /* io.Writer output set for file */ 66 | if file, err = os.OpenFile(filename, os.O_CREATE|os.O_RDWR|os.O_APPEND, 0666); err != nil { /* Open the file */ 67 | 68 | log.Fatal(err) /* Log the error */ 69 | 70 | } 71 | 72 | log.SetOutput(file) /* Set the output for the logger */ 73 | 74 | switch { 75 | case log.StandardLogger().GetLevel() == log.InfoLevel: 76 | 77 | switch logEntry.Message { 78 | case "UP", "DOWN", "INIT": 79 | 80 | log.WithFields(log.Fields{ 81 | "threadID": logEntry.Session.ThreadID, 82 | "rsi3": fmt.Sprintf("%.2f", logEntry.Market.Rsi3), 83 | "rsi7": fmt.Sprintf("%.2f", logEntry.Market.Rsi7), 84 | "rsi14": fmt.Sprintf("%.2f", logEntry.Market.Rsi14), 85 | "MACD": fmt.Sprintf("%.2f", logEntry.Market.MACD), 86 | "high": logEntry.Market.PriceChangeStatsHighPrice, 87 | "direction": logEntry.Market.Direction, 88 | }).Info(logEntry.Message) 89 | 90 | case "BUY": 91 | 92 | log.WithFields(log.Fields{ 93 | "threadID": logEntry.Session.ThreadID, 94 | "orderID": logEntry.Order.OrderID, 95 | "orderPrice": fmt.Sprintf("%.4f", logEntry.Order.Price), 96 | }).Info(logEntry.Message) 97 | 98 | case "BUYDRYRUN": 99 | 100 | log.WithFields(log.Fields{ 101 | "threadID": logEntry.Session.ThreadID, 102 | "orderPrice": fmt.Sprintf("%.4f", logEntry.Order.Price), 103 | }).Info(logEntry.Message) 104 | 105 | case "SELL": 106 | 107 | log.WithFields(log.Fields{ 108 | "threadID": logEntry.Session.ThreadID, 109 | "OrderIDSource": logEntry.Order.OrderIDSource, 110 | "orderID": logEntry.Order.OrderID, 111 | "orderPrice": fmt.Sprintf("%.4f", logEntry.Order.Price), 112 | }).Info(logEntry.Message) 113 | 114 | case "SELLDRYRUN": 115 | 116 | log.WithFields(log.Fields{ 117 | "threadID": logEntry.Session.ThreadID, 118 | "orderPrice": fmt.Sprintf("%.4f", logEntry.Order.Price), 119 | }).Info(logEntry.Message) 120 | 121 | case "CANCELED": 122 | 123 | if logEntry.Config.Debug { 124 | 125 | log.WithFields(log.Fields{ 126 | "threadID": logEntry.Session.ThreadID, 127 | "OrderIDSource": logEntry.Order.OrderIDSource, 128 | "orderID": logEntry.Order.OrderID, 129 | }).Info(logEntry.Message) 130 | 131 | } 132 | 133 | case "STOPLOSS": 134 | 135 | log.WithFields(log.Fields{ 136 | "threadID": logEntry.Session.ThreadID, 137 | "orderID": logEntry.Order.OrderID, 138 | }).Info(logEntry.Message) 139 | 140 | default: 141 | 142 | if logEntry.Session == nil || logEntry.Session.ThreadID == "" { 143 | 144 | log.WithFields(log.Fields{}).Info(logEntry.Message) 145 | 146 | } else { 147 | 148 | log.WithFields(log.Fields{ 149 | "threadID": logEntry.Session.ThreadID, 150 | }).Info(logEntry.Message) 151 | 152 | } 153 | 154 | } 155 | 156 | case log.StandardLogger().GetLevel() == log.DebugLevel: 157 | 158 | if logEntry.Session == nil || logEntry.Session.ThreadID == "" { 159 | 160 | log.WithFields(log.Fields{}).Info(logEntry.Message) 161 | 162 | } else { 163 | 164 | log.WithFields(log.Fields{ 165 | "threadID": logEntry.Session.ThreadID, 166 | "orderID": logEntry.Order.OrderID, 167 | }).Debug(logEntry.Message) 168 | 169 | } 170 | 171 | } 172 | 173 | } 174 | -------------------------------------------------------------------------------- /templates/admin.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 | 22 |
23 | 24 |
25 | 26 | 27 | 28 |
29 | 30 |
31 | 32 |
33 |
34 | 35 |
36 |
37 | 41 |
42 |
43 | 44 |
45 |
46 | 47 |
48 |
49 | 52 |
53 |
54 | 55 |
56 |
57 | 58 |
59 |
60 | 63 |
64 |
65 | 66 |
67 |
68 | 69 |
70 |
71 | 74 |
75 |
76 | 77 |
78 |
79 | 80 |
81 |
82 | 85 |
86 |
87 | 88 |
89 | 90 |
91 | 92 |
93 | 94 |
95 | 96 | 100 | 101 |
102 | 103 |
104 | 105 |
106 | 107 |
108 | 109 |
110 | 111 | 112 | 113 | -------------------------------------------------------------------------------- /telegram/telegram.go: -------------------------------------------------------------------------------- 1 | package telegram 2 | 3 | import ( 4 | "math" 5 | "strconv" 6 | "sync" 7 | "time" 8 | 9 | "github.com/aleibovici/cryptopump/functions" 10 | "github.com/aleibovici/cryptopump/logger" 11 | "github.com/aleibovici/cryptopump/mysql" 12 | "github.com/aleibovici/cryptopump/types" 13 | 14 | tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api" 15 | ) 16 | 17 | // Message defines the message structure to send via Telegram 18 | type Message struct { 19 | Text string 20 | ReplyToMessageID int 21 | } 22 | 23 | // Connect to connect to Telegram 24 | type Connect struct{} 25 | 26 | // Send a message via Telegram 27 | func (message Message) Send(sessionData *types.Session) { 28 | 29 | msg := tgbotapi.NewMessage(sessionData.TgBotAPIChatID, message.Text) 30 | 31 | if _, err := sessionData.TgBotAPI.Send(msg); err != nil { 32 | 33 | logger.LogEntry{ /* Log Entry */ 34 | Config: nil, 35 | Market: nil, 36 | Session: sessionData, 37 | Order: &types.Order{}, 38 | Message: functions.GetFunctionName() + " - " + err.Error(), 39 | LogLevel: "DebugLevel", 40 | }.Do() 41 | 42 | } 43 | 44 | } 45 | 46 | // Do establish connectivity to Telegram 47 | func (Connect) Do( 48 | configData *types.Config, 49 | sessionData *types.Session) { 50 | 51 | var err error 52 | 53 | if sessionData.TgBotAPI, err = tgbotapi.NewBotAPI(configData.ConfigGlobal.TgBotApikey); err != nil { 54 | 55 | logger.LogEntry{ /* Log Entry */ 56 | Config: nil, 57 | Market: nil, 58 | Session: sessionData, 59 | Order: &types.Order{}, 60 | Message: functions.GetFunctionName() + " - " + err.Error(), 61 | LogLevel: "DebugLevel", 62 | }.Do() 63 | 64 | } 65 | 66 | sessionData.TgBotAPI.Debug = false 67 | 68 | } 69 | 70 | // CheckUpdates Check for Telegram bot updates 71 | func CheckUpdates( 72 | configData *types.Config, 73 | sessionData *types.Session, 74 | wg *sync.WaitGroup) { 75 | 76 | var err error 77 | var updates tgbotapi.UpdatesChannel 78 | 79 | /* Exit if no API key found */ 80 | if configData.ConfigGlobal.TgBotApikey == "" { 81 | 82 | return 83 | 84 | } 85 | 86 | /* Sleep until Master Node is True */ 87 | for !sessionData.MasterNode { 88 | 89 | time.Sleep(30000 * time.Millisecond) 90 | 91 | } 92 | 93 | /* Establish connectivity to Telegram server */ 94 | Connect{}.Do(configData, sessionData) 95 | 96 | u := tgbotapi.NewUpdate(0) 97 | u.Timeout = 60 98 | 99 | if updates, err = sessionData.TgBotAPI.GetUpdatesChan(u); err != nil { 100 | 101 | logger.LogEntry{ /* Log Entry */ 102 | Config: configData, 103 | Market: nil, 104 | Session: sessionData, 105 | Order: &types.Order{}, 106 | Message: functions.GetFunctionName() + " - " + err.Error(), 107 | LogLevel: "DebugLevel", 108 | }.Do() 109 | 110 | } 111 | 112 | for update := range updates { 113 | 114 | /* ignore any non-Message Updates */ 115 | if update.Message == nil { 116 | 117 | continue 118 | 119 | } 120 | 121 | /* Store Telegram ChatID to allow the system to send direct messages to Telegram server */ 122 | sessionData.TgBotAPIChatID = update.Message.Chat.ID 123 | 124 | switch update.Message.Text { 125 | case "/sell": 126 | 127 | Message{ 128 | Text: "\f" + "Selling @ " + sessionData.ThreadID, 129 | ReplyToMessageID: update.Message.MessageID, 130 | }.Send(sessionData) 131 | 132 | sessionData.ForceSell = true 133 | 134 | case "/buy": 135 | 136 | Message{ 137 | Text: "\f" + "Buying @ " + sessionData.ThreadID, 138 | ReplyToMessageID: update.Message.MessageID, 139 | }.Send(sessionData) 140 | 141 | sessionData.ForceBuy = true 142 | 143 | case "/report": 144 | 145 | var profit float64 146 | var profitNet float64 147 | var profitPct float64 148 | var threadCount int 149 | var status string 150 | var err error 151 | 152 | if profit, profitNet, profitPct, err = mysql.GetProfit(sessionData); err != nil { 153 | return 154 | } 155 | 156 | if threadCount, err = mysql.GetThreadCount(sessionData); err != nil { 157 | return 158 | } 159 | 160 | if threadID, err := mysql.GetSessionStatus(sessionData); err == nil { 161 | 162 | if threadID != "" { 163 | status = "\f" + "System Fault @ " + threadID 164 | } else { 165 | status = "\f" + "System nominal" 166 | } 167 | 168 | } 169 | 170 | Message{ 171 | Text: "\f" + "Available Funds: " + sessionData.SymbolFiat + " " + functions.Float64ToStr(sessionData.SymbolFiatFunds, 2) + "\n" + 172 | "Deployed Funds: " + sessionData.SymbolFiat + " " + functions.Float64ToStr((math.Round(sessionData.Global.ThreadAmount*100)/100), 2) + "\n" + 173 | "Profit: $" + functions.Float64ToStr(profit, 2) + "\n" + 174 | "ROI: " + functions.Float64ToStr(getROI(profit, sessionData), 2) + "%\n" + 175 | "Net Profit: $" + functions.Float64ToStr(profitNet, 2) + "\n" + 176 | "Net ROI: " + functions.Float64ToStr(getROI(profitNet, sessionData), 2) + "%" + "\n" + 177 | "Avg. Transaction: " + functions.Float64ToStr(profitPct, 2) + "%" + "\n" + 178 | "Thread Count: " + strconv.Itoa(threadCount) + "\n" + 179 | "Status: " + status + "\n" + 180 | "Master: " + sessionData.ThreadID, 181 | ReplyToMessageID: update.Message.MessageID, 182 | }.Send(sessionData) 183 | 184 | } 185 | 186 | } 187 | 188 | } 189 | 190 | // getROI returns the ROI of a given profit 191 | func getROI(profit float64, 192 | sessionData *types.Session) (ROI float64) { 193 | 194 | return (profit) / (math.Round(sessionData.Global.ThreadAmount*100) / 100) * 100 195 | 196 | } 197 | -------------------------------------------------------------------------------- /markets/markets.go: -------------------------------------------------------------------------------- 1 | package markets 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "time" 7 | 8 | "github.com/aleibovici/cryptopump/exchange" 9 | "github.com/aleibovici/cryptopump/functions" 10 | "github.com/aleibovici/cryptopump/logger" 11 | "github.com/aleibovici/cryptopump/types" 12 | 13 | "github.com/sdcoffey/big" 14 | "github.com/sdcoffey/techan" 15 | ) 16 | 17 | // Data struct host temporal market data 18 | type Data struct { 19 | Kline types.WsKline 20 | } 21 | 22 | /* Technical analysis Calculations */ 23 | func calculate( 24 | closePrices techan.Indicator, 25 | priceChangeStats []*types.PriceChangeStats, 26 | sessionData *types.Session, 27 | marketData *types.Market) { 28 | 29 | marketData.Rsi3 = calculateRSI(closePrices, marketData.Series, 3) 30 | marketData.Rsi7 = calculateRSI(closePrices, marketData.Series, 7) 31 | marketData.Rsi14 = calculateRSI(closePrices, marketData.Series, 14) 32 | marketData.MACD = calculateMACD(closePrices, marketData.Series, 12, 26) 33 | marketData.Ma7 = calculateMA(closePrices, marketData.Series, 7) 34 | marketData.Ma14 = calculateMA(closePrices, marketData.Series, 14) 35 | if priceChangeStats != nil { 36 | marketData.PriceChangeStatsHighPrice = calculatePriceChangeStatsHighPrice(priceChangeStats) 37 | marketData.PriceChangeStatsLowPrice = calculatePriceChangeStatsLowPrice(priceChangeStats) 38 | } 39 | marketData.TimeStamp = time.Now() /* Time of last retrieved market Data */ 40 | 41 | } 42 | 43 | // LoadKline process realtime KLine data via websocket API 44 | func (d Data) LoadKline( 45 | configData *types.Config, 46 | sessionData *types.Session, 47 | marketData *types.Market) { 48 | 49 | var start int64 50 | var err error 51 | var priceChangeStats []*types.PriceChangeStats 52 | 53 | /* Conditional defer logging when there is an error retriving data */ 54 | defer func() { 55 | if err != nil { 56 | logger.LogEntry{ /* Log Entry */ 57 | Config: configData, 58 | Market: marketData, 59 | Session: sessionData, 60 | Order: &types.Order{}, 61 | Message: functions.GetFunctionName() + " - " + err.Error(), 62 | LogLevel: "DebugLevel", 63 | }.Do() 64 | } 65 | }() 66 | 67 | if start, err = strconv.ParseInt(fmt.Sprint(d.Kline.StartTime), 10, 64); err != nil { 68 | 69 | return 70 | 71 | } 72 | 73 | period := techan.NewTimePeriod(time.Unix((start/1000), 0).UTC(), time.Minute*1) 74 | 75 | candle := techan.NewCandle(period) 76 | candle.OpenPrice = big.NewFromString(d.Kline.Open) 77 | candle.ClosePrice = big.NewFromString(d.Kline.Close) 78 | candle.MaxPrice = big.NewFromString(d.Kline.High) 79 | candle.MinPrice = big.NewFromString(d.Kline.Low) 80 | candle.Volume = big.NewFromString(d.Kline.Volume) 81 | 82 | if !marketData.Series.AddCandle(candle) { /* AddCandle adds the given candle to TimeSeries */ 83 | 84 | return 85 | 86 | } 87 | 88 | if priceChangeStats, err = exchange.GetPriceChangeStats(configData, sessionData, marketData); err != nil { 89 | 90 | return 91 | 92 | } 93 | 94 | calculate( 95 | techan.NewClosePriceIndicator(marketData.Series), 96 | priceChangeStats, 97 | sessionData, 98 | marketData) 99 | 100 | } 101 | 102 | // LoadKlinePast process past KLine data via REST API 103 | func (d Data) LoadKlinePast( 104 | configData *types.Config, 105 | marketData *types.Market, 106 | sessionData *types.Session) { 107 | 108 | var err error 109 | var klines []*types.Kline 110 | var priceChangeStats []*types.PriceChangeStats 111 | 112 | /* Conditional defer logging when there is an error retriving data */ 113 | defer func() { 114 | if err != nil { 115 | logger.LogEntry{ /* Log Entry */ 116 | Config: configData, 117 | Market: marketData, 118 | Session: sessionData, 119 | Order: &types.Order{}, 120 | Message: functions.GetFunctionName() + " - " + err.Error(), 121 | LogLevel: "DebugLevel", 122 | }.Do() 123 | } 124 | }() 125 | 126 | if klines, err = exchange.GetKlines(configData, sessionData); err != nil { 127 | 128 | return 129 | 130 | } 131 | 132 | for _, datum := range klines { 133 | 134 | var start int64 135 | 136 | if start, err = strconv.ParseInt(fmt.Sprint(datum.OpenTime), 10, 64); err != nil { 137 | 138 | return 139 | 140 | } 141 | 142 | period := techan.NewTimePeriod(time.Unix((start/1000), 0).UTC(), time.Minute*1) 143 | 144 | candle := techan.NewCandle(period) 145 | candle.OpenPrice = big.NewFromString(datum.Open) 146 | candle.ClosePrice = big.NewFromString(datum.Close) 147 | candle.MaxPrice = big.NewFromString(datum.High) 148 | candle.MinPrice = big.NewFromString(datum.Low) 149 | candle.Volume = big.NewFromString(datum.Volume) 150 | 151 | if !marketData.Series.AddCandle(candle) { 152 | return 153 | } 154 | 155 | } 156 | 157 | if priceChangeStats, err = exchange.GetPriceChangeStats(configData, sessionData, marketData); err != nil { 158 | 159 | return 160 | 161 | } 162 | 163 | calculate( 164 | techan.NewClosePriceIndicator(marketData.Series), 165 | priceChangeStats, 166 | sessionData, 167 | marketData) 168 | 169 | } 170 | 171 | /* Calculate Relative Strength Index */ 172 | func calculateRSI( 173 | closePrices techan.Indicator, 174 | series *techan.TimeSeries, 175 | timeframe int) float64 { 176 | 177 | return techan.NewRelativeStrengthIndexIndicator(closePrices, timeframe).Calculate(series.LastIndex() - 1).Float() 178 | } 179 | 180 | func calculateMACD( 181 | closePrices techan.Indicator, 182 | series *techan.TimeSeries, 183 | shortwindow int, 184 | longwindow int) float64 { 185 | 186 | return techan.NewMACDIndicator(closePrices, shortwindow, longwindow).Calculate(series.LastIndex() - 1).Float() 187 | } 188 | 189 | func calculateMA( 190 | closePrices techan.Indicator, 191 | series *techan.TimeSeries, 192 | window int) float64 { 193 | 194 | return techan.NewSimpleMovingAverage(closePrices, window).Calculate(series.LastIndex() - 1).Float() 195 | } 196 | 197 | /* Calculate High price for 1 period */ 198 | func calculatePriceChangeStatsHighPrice( 199 | priceChangeStats []*types.PriceChangeStats) float64 { 200 | 201 | return functions.StrToFloat64(priceChangeStats[0].HighPrice) 202 | } 203 | 204 | /* Calculate Low price for 1 period */ 205 | func calculatePriceChangeStatsLowPrice( 206 | priceChangeStats []*types.PriceChangeStats) float64 { 207 | 208 | return functions.StrToFloat64(priceChangeStats[0].LowPrice) 209 | } 210 | -------------------------------------------------------------------------------- /plotter/plotter.go: -------------------------------------------------------------------------------- 1 | package plotter 2 | 3 | import ( 4 | "bytes" 5 | "html/template" 6 | "math" 7 | "time" 8 | 9 | "github.com/aleibovici/cryptopump/functions" 10 | "github.com/aleibovici/cryptopump/logger" 11 | "github.com/aleibovici/cryptopump/types" 12 | 13 | "github.com/go-echarts/go-echarts/v2/charts" 14 | "github.com/go-echarts/go-echarts/v2/opts" 15 | 16 | chartrender "github.com/go-echarts/go-echarts/v2/render" 17 | ) 18 | 19 | // Data struct host temporal plotter data 20 | type Data struct { 21 | Kline types.WsKline 22 | } 23 | 24 | // LoadKline load Kline into long term retention for plotter 25 | func (d Data) LoadKline( 26 | sessionData *types.Session, 27 | marketData *types.Market) { 28 | 29 | kd := []types.KlineData{ 30 | { 31 | Date: d.Kline.EndTime, 32 | Data: [4]float64{functions.StrToFloat64(d.Kline.Open), functions.StrToFloat64(d.Kline.Close), functions.StrToFloat64(d.Kline.Low), functions.StrToFloat64(d.Kline.High)}, 33 | Volumes: functions.StrToFloat64(d.Kline.Volume), 34 | Ma7: math.Round(marketData.Ma7*10000) / 10000, 35 | Ma14: math.Round(marketData.Ma14*10000) / 10000, 36 | }, 37 | } 38 | 39 | /* Maintain klinedata to a maximum of 1440 minutes (24hs) eliminating first item on slice */ 40 | if len(sessionData.KlineData) == 1439 { 41 | 42 | sessionData.KlineData = sessionData.KlineData[1:] 43 | 44 | } 45 | 46 | sessionData.KlineData = append(sessionData.KlineData, kd...) 47 | 48 | } 49 | 50 | // Plot is responsible for rending e-chart 51 | func (d Data) Plot(sessionData *types.Session) (htmlSnippet template.HTML) { 52 | 53 | x := make([]string, 0) 54 | y := make([]opts.KlineData, 0) 55 | v := make([]opts.BarData, 0) 56 | ma7 := make([]opts.LineData, 0) /* Simple Moving Average for 7 periods */ 57 | ma14 := make([]opts.LineData, 0) /* Simple Moving Average for 14 periods */ 58 | 59 | for i := 0; i < len(sessionData.KlineData); i++ { 60 | x = append(x, time.Unix((sessionData.KlineData[i].Date/1000), 0).UTC().Local().Format("15:04")) 61 | y = append(y, opts.KlineData{Value: sessionData.KlineData[i].Data}) 62 | v = append(v, opts.BarData{Value: sessionData.KlineData[i].Volumes}) 63 | ma7 = append(ma7, opts.LineData{Value: sessionData.KlineData[i].Ma7}) /* Simple Moving Average for 7 periods */ 64 | ma14 = append(ma14, opts.LineData{Value: sessionData.KlineData[i].Ma14}) /* Simple Moving Average for 14 periods */ 65 | } 66 | 67 | kline := klineBase("KLINE", x, y) /* Create base kline chart */ 68 | kline.Overlap(lineBase("MA7", x, ma7, "blue"), lineBase("MA14", x, ma14, "orange")) /* Create overlapping line charts */ 69 | 70 | return renderToHTML(kline) 71 | 72 | } 73 | 74 | func renderToHTML(c interface{}) template.HTML { 75 | 76 | var buf bytes.Buffer 77 | 78 | r := c.(chartrender.Renderer) /* interface{} to Renderer */ 79 | 80 | if err := r.Render(&buf); err != nil { /* Render to buffer */ 81 | 82 | logger.LogEntry{ /* Log Entry */ 83 | Config: nil, 84 | Market: nil, 85 | Session: nil, 86 | Order: &types.Order{}, 87 | Message: functions.GetFunctionName() + " - " + err.Error(), 88 | LogLevel: "DebugLevel", 89 | }.Do() 90 | 91 | return "" /* Return empty string */ 92 | 93 | } 94 | 95 | return template.HTML(buf.String()) /* Return rendered HTML */ 96 | } 97 | 98 | /* Create a base KLine Chart */ 99 | func klineBase(name string, XAxis []string, klineData []opts.KlineData) *charts.Kline { 100 | 101 | kline := charts.NewKLine() 102 | 103 | kline.SetGlobalOptions( 104 | charts.WithTitleOpts(opts.Title{ 105 | Title: "", 106 | }), 107 | charts.WithXAxisOpts(opts.XAxis{ /* Dates */ 108 | Type: "category", 109 | Show: true, 110 | SplitNumber: 20, 111 | SplitArea: &opts.SplitArea{}, 112 | SplitLine: &opts.SplitLine{}, 113 | AxisLabel: &opts.AxisLabel{ 114 | Show: true, 115 | }, 116 | }), 117 | charts.WithYAxisOpts(opts.YAxis{ /* Candles */ 118 | Type: "value", 119 | SplitNumber: 2, 120 | Scale: true, 121 | SplitArea: &opts.SplitArea{ 122 | Show: true, 123 | AreaStyle: &opts.AreaStyle{}, 124 | }, 125 | SplitLine: &opts.SplitLine{ 126 | Show: true, 127 | LineStyle: &opts.LineStyle{ 128 | Color: "#777", 129 | }, 130 | }, 131 | AxisLabel: &opts.AxisLabel{ 132 | Show: true, 133 | Formatter: "{value}\n", 134 | }, 135 | }), 136 | charts.WithDataZoomOpts(opts.DataZoom{ 137 | Type: "inside", 138 | Start: 80, 139 | End: 100, 140 | XAxisIndex: []int{0}, 141 | }), 142 | charts.WithDataZoomOpts(opts.DataZoom{ 143 | Type: "slider", 144 | Start: 80, 145 | End: 100, 146 | XAxisIndex: []int{0}, 147 | }), 148 | charts.WithInitializationOpts(opts.Initialization{ 149 | PageTitle: "CryptoPump", 150 | Width: "1900px", 151 | Height: "400px", 152 | }), 153 | charts.WithTooltipOpts(opts.Tooltip{ 154 | Show: true, 155 | Trigger: "axis", 156 | AxisPointer: &opts.AxisPointer{ 157 | Type: "cross", 158 | }, 159 | }), 160 | ) 161 | 162 | kline.SetXAxis(XAxis).AddSeries(name, klineData). 163 | SetSeriesOptions( 164 | charts.WithMarkPointNameTypeItemOpts(opts.MarkPointNameTypeItem{ 165 | Name: "highest value", 166 | Type: "max", 167 | ValueDim: "highest", 168 | }), 169 | charts.WithMarkPointNameTypeItemOpts(opts.MarkPointNameTypeItem{ 170 | Name: "lowest value", 171 | Type: "min", 172 | ValueDim: "lowest", 173 | }), 174 | charts.WithMarkPointStyleOpts(opts.MarkPointStyle{ 175 | Label: &opts.Label{ 176 | Show: true, 177 | Color: "black", 178 | }, 179 | }), 180 | charts.WithItemStyleOpts(opts.ItemStyle{ 181 | Color: "#00da3c", 182 | Color0: "#ec0000", 183 | }), 184 | ) 185 | 186 | return kline 187 | 188 | } 189 | 190 | /* Create a base Line Chart */ 191 | func lineBase(name string, XAxis []string, lineData []opts.LineData, color string) *charts.Line { 192 | 193 | line := charts.NewLine() 194 | line.SetXAxis(XAxis). 195 | AddSeries(name, lineData). 196 | SetSeriesOptions( 197 | charts.WithLineChartOpts(opts.LineChart{ 198 | Smooth: false, 199 | }), 200 | charts.WithLineStyleOpts(opts.LineStyle{ 201 | Color: color, 202 | Width: 2, 203 | Opacity: 0.5, 204 | }), 205 | charts.WithItemStyleOpts(opts.ItemStyle{ 206 | Color: color, 207 | Color0: color, 208 | BorderColor: color, 209 | BorderColor0: color, 210 | Opacity: 0.5, 211 | }), 212 | ) 213 | 214 | return line 215 | } 216 | -------------------------------------------------------------------------------- /exchange/exchange_test.go: -------------------------------------------------------------------------------- 1 | package exchange 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/aleibovici/cryptopump/functions" 8 | "github.com/aleibovici/cryptopump/logger" 9 | "github.com/aleibovici/cryptopump/types" 10 | "github.com/spf13/viper" 11 | ) 12 | 13 | var configData = &types.Config{} 14 | 15 | var sessionData = &types.Session{ 16 | Symbol: "BTCUSDT", 17 | SymbolFiat: "USDT", 18 | MasterNode: false, 19 | } 20 | 21 | var marketData = &types.Market{ 22 | Price: 40000, 23 | } 24 | 25 | func init() { 26 | 27 | viperData := &types.ViperData{ /* Viper Configuration */ 28 | V1: viper.New(), /* Session configurations file */ 29 | V2: viper.New(), /* Global configurations file */ 30 | } 31 | 32 | viperData.V1.SetConfigType("yml") /* Set the type of the configurations file */ 33 | viperData.V1.AddConfigPath("../config") /* Set the path to look for the configurations file */ 34 | viperData.V1.SetConfigName("config") /* Set the file name of the configurations file */ 35 | if err := viperData.V1.ReadInConfig(); err != nil { 36 | 37 | logger.LogEntry{ /* Log Entry */ 38 | Config: nil, 39 | Market: nil, 40 | Session: nil, 41 | Order: &types.Order{}, 42 | Message: functions.GetFunctionName() + " - " + err.Error(), 43 | LogLevel: "DebugLevel", 44 | }.Do() 45 | 46 | } 47 | viperData.V1.WatchConfig() 48 | 49 | configData = functions.GetConfigData(viperData, sessionData) 50 | configData.TestNet = true 51 | 52 | GetClient(configData, sessionData) 53 | GetLotSize(configData, sessionData) 54 | 55 | } 56 | 57 | func TestGetClient(t *testing.T) { 58 | type args struct { 59 | configData *types.Config 60 | sessionData *types.Session 61 | } 62 | tests := []struct { 63 | name string 64 | args args 65 | wantErr bool 66 | }{ 67 | { 68 | name: "success", 69 | args: args{ 70 | configData: configData, 71 | sessionData: sessionData, 72 | }, 73 | wantErr: false, 74 | }, 75 | } 76 | for _, tt := range tests { 77 | t.Run(tt.name, func(t *testing.T) { 78 | if err := GetClient(tt.args.configData, tt.args.sessionData); (err != nil) != tt.wantErr { 79 | t.Errorf("GetClient() error = %v, wantErr %v", err, tt.wantErr) 80 | } 81 | }) 82 | } 83 | } 84 | 85 | func TestGetInfo(t *testing.T) { 86 | type args struct { 87 | configData *types.Config 88 | sessionData *types.Session 89 | } 90 | 91 | tests := []struct { 92 | name string 93 | args args 94 | wantInfo *types.ExchangeInfo 95 | wantErr bool 96 | }{ 97 | { 98 | name: "success", 99 | args: args{ 100 | configData: configData, 101 | sessionData: sessionData}, 102 | wantInfo: &types.ExchangeInfo{}, 103 | wantErr: false, 104 | }, 105 | } 106 | 107 | for _, tt := range tests { 108 | t.Run(tt.name, func(t *testing.T) { 109 | gotInfo, err := GetInfo(tt.args.configData, tt.args.sessionData) 110 | if err == nil { 111 | return 112 | } 113 | if (err != nil) != tt.wantErr { 114 | t.Errorf("GetInfo() error = %v, wantErr %v", err, tt.wantErr) 115 | return 116 | } 117 | if !reflect.DeepEqual(gotInfo, tt.wantInfo) { 118 | t.Errorf("GetInfo() = %v, want %v", gotInfo, tt.wantInfo) 119 | } 120 | }) 121 | } 122 | } 123 | 124 | func TestGetLotSize(t *testing.T) { 125 | type args struct { 126 | configData *types.Config 127 | sessionData *types.Session 128 | } 129 | 130 | tests := []struct { 131 | name string 132 | args args 133 | }{ 134 | { 135 | name: "success", 136 | args: args{ 137 | configData: configData, 138 | sessionData: sessionData, 139 | }, 140 | }, 141 | } 142 | for _, tt := range tests { 143 | t.Run(tt.name, func(t *testing.T) { 144 | GetLotSize(tt.args.configData, tt.args.sessionData) 145 | }) 146 | } 147 | } 148 | 149 | func TestGetKlines(t *testing.T) { 150 | type args struct { 151 | configData *types.Config 152 | sessionData *types.Session 153 | } 154 | 155 | tests := []struct { 156 | name string 157 | args args 158 | wantKlines []*types.Kline 159 | wantErr bool 160 | }{ 161 | { 162 | name: "success", 163 | args: args{ 164 | configData: configData, 165 | sessionData: sessionData, 166 | }, 167 | wantKlines: []*types.Kline{}, 168 | wantErr: false, 169 | }, 170 | } 171 | for _, tt := range tests { 172 | t.Run(tt.name, func(t *testing.T) { 173 | gotKlines, err := GetKlines(tt.args.configData, tt.args.sessionData) 174 | if err == nil { 175 | return 176 | } 177 | if (err != nil) != tt.wantErr { 178 | t.Errorf("GetKlines() error = %v, wantErr %v", err, tt.wantErr) 179 | return 180 | } 181 | if !reflect.DeepEqual(gotKlines, tt.wantKlines) { 182 | t.Errorf("GetKlines() = %v, want %v", gotKlines, tt.wantKlines) 183 | } 184 | }) 185 | } 186 | } 187 | 188 | func TestGetPriceChangeStats(t *testing.T) { 189 | type args struct { 190 | configData *types.Config 191 | sessionData *types.Session 192 | marketData *types.Market 193 | } 194 | 195 | tests := []struct { 196 | name string 197 | args args 198 | wantPriceChangeStats []*types.PriceChangeStats 199 | wantErr bool 200 | }{ 201 | { 202 | name: "success", 203 | args: args{ 204 | configData: configData, 205 | sessionData: sessionData, 206 | marketData: marketData, 207 | }, 208 | wantPriceChangeStats: []*types.PriceChangeStats{}, 209 | wantErr: false, 210 | }, 211 | } 212 | for _, tt := range tests { 213 | t.Run(tt.name, func(t *testing.T) { 214 | gotPriceChangeStats, err := GetPriceChangeStats(tt.args.configData, tt.args.sessionData, tt.args.marketData) 215 | if err == nil { 216 | return 217 | } 218 | if (err != nil) != tt.wantErr { 219 | t.Errorf("GetPriceChangeStats() error = %v, wantErr %v", err, tt.wantErr) 220 | return 221 | } 222 | if !reflect.DeepEqual(gotPriceChangeStats, tt.wantPriceChangeStats) { 223 | t.Errorf("GetPriceChangeStats() = %v, want %v", gotPriceChangeStats, tt.wantPriceChangeStats) 224 | } 225 | }) 226 | } 227 | } 228 | 229 | func TestGetUserStreamServiceListenKey(t *testing.T) { 230 | type args struct { 231 | configData *types.Config 232 | sessionData *types.Session 233 | } 234 | 235 | tests := []struct { 236 | name string 237 | args args 238 | wantListenKey string 239 | wantErr bool 240 | }{ 241 | { 242 | name: "success", 243 | args: args{ 244 | configData: configData, 245 | sessionData: sessionData, 246 | }, 247 | wantListenKey: "", 248 | wantErr: false, 249 | }, 250 | } 251 | for _, tt := range tests { 252 | t.Run(tt.name, func(t *testing.T) { 253 | gotListenKey, err := GetUserStreamServiceListenKey(tt.args.configData, tt.args.sessionData) 254 | if (err != nil) && (gotListenKey != tt.wantListenKey) { 255 | return 256 | } 257 | }) 258 | } 259 | } 260 | 261 | func TestKeepAliveUserStreamServiceListenKey(t *testing.T) { 262 | type args struct { 263 | configData *types.Config 264 | sessionData *types.Session 265 | } 266 | tests := []struct { 267 | name string 268 | args args 269 | wantErr bool 270 | }{ 271 | { 272 | name: "success", 273 | args: args{ 274 | configData: configData, 275 | sessionData: sessionData, 276 | }, 277 | wantErr: true, 278 | }, 279 | } 280 | for _, tt := range tests { 281 | t.Run(tt.name, func(t *testing.T) { 282 | if err := KeepAliveUserStreamServiceListenKey(tt.args.configData, tt.args.sessionData); (err != nil) != tt.wantErr { 283 | t.Errorf("KeepAliveUserStreamServiceListenKey() error = %v, wantErr %v", err, tt.wantErr) 284 | } 285 | }) 286 | } 287 | } 288 | 289 | func TestNewSetServerTimeService(t *testing.T) { 290 | type args struct { 291 | configData *types.Config 292 | sessionData *types.Session 293 | } 294 | tests := []struct { 295 | name string 296 | args args 297 | wantErr bool 298 | }{ 299 | { 300 | name: "success", 301 | args: args{ 302 | configData: configData, 303 | sessionData: sessionData, 304 | }, 305 | wantErr: false, 306 | }, 307 | } 308 | for _, tt := range tests { 309 | t.Run(tt.name, func(t *testing.T) { 310 | if err := NewSetServerTimeService(tt.args.configData, tt.args.sessionData); (err != nil) != tt.wantErr { 311 | t.Errorf("NewSetServerTimeService() error = %v, wantErr %v", err, tt.wantErr) 312 | } 313 | }) 314 | } 315 | } 316 | 317 | func Test_getSellQuantity(t *testing.T) { 318 | type args struct { 319 | order types.Order 320 | sessionData *types.Session 321 | } 322 | tests := []struct { 323 | name string 324 | args args 325 | wantQuantity float64 326 | }{ 327 | // TODO: Add test cases. 328 | } 329 | for _, tt := range tests { 330 | t.Run(tt.name, func(t *testing.T) { 331 | if gotQuantity := getSellQuantity(tt.args.order, tt.args.sessionData); gotQuantity != tt.wantQuantity { 332 | t.Errorf("getSellQuantity() = %v, want %v", gotQuantity, tt.wantQuantity) 333 | } 334 | }) 335 | } 336 | } 337 | 338 | func Test_getBuyQuantity(t *testing.T) { 339 | type args struct { 340 | marketData *types.Market 341 | sessionData *types.Session 342 | fiatQuantity float64 343 | } 344 | tests := []struct { 345 | name string 346 | args args 347 | wantQuantity float64 348 | }{ 349 | { 350 | name: "success", 351 | args: args{ 352 | marketData: marketData, 353 | sessionData: sessionData, 354 | fiatQuantity: 0, 355 | }, 356 | wantQuantity: 0, 357 | }, 358 | } 359 | for _, tt := range tests { 360 | t.Run(tt.name, func(t *testing.T) { 361 | if gotQuantity := getBuyQuantity(tt.args.marketData, tt.args.sessionData, tt.args.fiatQuantity); gotQuantity != tt.wantQuantity { 362 | t.Errorf("getBuyQuantity() = %v, want %v", gotQuantity, tt.wantQuantity) 363 | } 364 | }) 365 | } 366 | } 367 | -------------------------------------------------------------------------------- /loader/loader.go: -------------------------------------------------------------------------------- 1 | package loader 2 | 3 | /* This package contains the functions responsible for loading frequent data 4 | that is made available to the javascript autoloader for html output. This data 5 | is commonly loaded via the webserver using GET/sessiondata */ 6 | 7 | import ( 8 | "encoding/json" 9 | "math" 10 | "strconv" 11 | "time" 12 | 13 | "github.com/aleibovici/cryptopump/functions" 14 | "github.com/aleibovici/cryptopump/logger" 15 | "github.com/aleibovici/cryptopump/mysql" 16 | "github.com/aleibovici/cryptopump/types" 17 | ) 18 | 19 | // LoadSessionDataAdditionalComponents Load dynamic components for javascript autoloader for html output 20 | func LoadSessionDataAdditionalComponents( 21 | sessionData *types.Session, 22 | marketData *types.Market, 23 | configData *types.Config) ([]byte, error) { 24 | 25 | type Market struct { 26 | Rsi3 float64 /* Relative Strength Index for 3 periods */ 27 | Rsi7 float64 /* Relative Strength Index for 7 periods */ 28 | Rsi14 float64 /* Relative Strength Index for 14 periods */ 29 | MACD float64 /* Moving average convergence divergence */ 30 | Price float64 /* Market Price */ 31 | Direction int /* Market Direction */ 32 | } 33 | 34 | type Order struct { 35 | OrderID string /* Order ID */ 36 | Quantity float64 /* Order Quantity */ 37 | Quote float64 /* Quote price */ 38 | Price float64 /* Acquisition Price */ 39 | Target float64 /* Target Price */ 40 | Diff float64 /* Difference between target and market price */ 41 | } 42 | 43 | type Session struct { 44 | ThreadID string /* Unique session ID for the thread */ 45 | SellTransactionCount float64 /* Number of SELL transactions in the last 60 minutes*/ 46 | Symbol string /* Symbol */ 47 | SymbolFunds float64 /* Available crypto funds in exchange */ 48 | SymbolFiat string /* Fiat currency funds */ 49 | SymbolFiatFunds float64 /* Fiat currency funds */ 50 | ProfitThreadID float64 /* ThreadID profit */ 51 | ProfitThreadIDPct float64 /* ThreadID profit percentage */ 52 | Profit float64 /* Total profit */ 53 | ProfitNet float64 /* Total net profit */ 54 | ProfitPct float64 /* Total profit percentage */ 55 | ThreadCount int /* Thread count */ 56 | ThreadAmount float64 /* Thread cost amount */ 57 | Latency int64 /* Latency between the exchange and client */ 58 | RateCounter int64 /* Average Number of transactions per second proccessed by WsBookTicker */ 59 | BuyDecisionTreeResult string /* Hold BuyDecisionTree result */ 60 | SellDecisionTreeResult string /* Hold SellDecisionTree result */ 61 | QuantityOffset float64 /* Quantity offset */ 62 | DiffTotal float64 /* Total difference between target and market price */ 63 | Orders []Order 64 | } 65 | 66 | type Update struct { 67 | Market Market 68 | Session Session 69 | } 70 | 71 | sessiondata := Update{} 72 | 73 | sessiondata.Market.Rsi3 = math.Round(marketData.Rsi3*100) / 100 74 | sessiondata.Market.Rsi7 = math.Round(marketData.Rsi7*100) / 100 75 | sessiondata.Market.Rsi14 = math.Round(marketData.Rsi14*100) / 100 76 | sessiondata.Market.MACD = math.Round(marketData.MACD*10000) / 10000 77 | sessiondata.Market.Price = math.Round(marketData.Price*1000) / 1000 78 | sessiondata.Market.Direction = marketData.Direction 79 | 80 | sessiondata.Session.Latency = sessionData.Latency /* Latency between the exchange and client */ 81 | sessiondata.Session.ThreadID = sessionData.ThreadID 82 | sessiondata.Session.SellTransactionCount = sessionData.SellTransactionCount 83 | sessiondata.Session.Symbol = sessionData.Symbol[0:3] 84 | sessiondata.Session.SymbolFunds = math.Round((sessionData.SymbolFunds)*10000) / 10000 /* Available crypto funds in exchange */ 85 | sessiondata.Session.SymbolFiat = sessionData.SymbolFiat 86 | sessiondata.Session.SymbolFiatFunds = math.Round(sessionData.SymbolFiatFunds*100) / 100 87 | sessiondata.Session.RateCounter = sessionData.RateCounter.Rate() / 5 /* Average Number of transactions per second proccessed by WsBookTicker */ 88 | sessiondata.Session.BuyDecisionTreeResult = sessionData.BuyDecisionTreeResult /* Hold BuyDecisionTree result*/ 89 | sessiondata.Session.SellDecisionTreeResult = sessionData.SellDecisionTreeResult /* Hold SellDecisionTree result */ 90 | sessiondata.Session.QuantityOffset = sessiondata.Session.SymbolFunds /* Quantity offset */ 91 | 92 | sessiondata.Session.Profit = math.Round(sessionData.Global.Profit*100) / 100 /* Sessions.Global loaded from mySQL via loadSessionDataAdditionalComponentsAsync */ 93 | sessiondata.Session.ProfitNet = math.Round(sessionData.Global.ProfitNet*100) / 100 /* Sessions.Global loaded from mySQL via loadSessionDataAdditionalComponentsAsync */ 94 | sessiondata.Session.ProfitPct = math.Round(sessionData.Global.ProfitPct*100) / 100 /* Sessions.Global loaded from mySQL via loadSessionDataAdditionalComponentsAsync */ 95 | sessiondata.Session.ProfitThreadID = math.Round(sessionData.Global.ProfitThreadID*100) / 100 /* Sessions.Global loaded from mySQL via loadSessionDataAdditionalComponentsAsync */ 96 | sessiondata.Session.ProfitThreadIDPct = math.Round(sessionData.Global.ProfitThreadIDPct*100) / 100 /* Sessions.Global loaded from mySQL via loadSessionDataAdditionalComponentsAsync */ 97 | sessiondata.Session.ThreadCount = sessionData.Global.ThreadCount /* Sessions.Global loaded from mySQL via loadSessionDataAdditionalComponentsAsync */ 98 | sessiondata.Session.ThreadAmount = math.Round(sessionData.Global.ThreadAmount*100) / 100 /* Sessions.Global loaded from mySQL via loadSessionDataAdditionalComponentsAsync */ 99 | 100 | if orders, err := mysql.GetThreadTransactionByThreadID(sessionData); err == nil { 101 | 102 | for _, key := range orders { 103 | 104 | tmp := Order{} 105 | tmp.OrderID = strconv.Itoa(key.OrderID) /* Order ID */ 106 | tmp.Quantity = key.ExecutedQuantity /* Order Quantity */ 107 | tmp.Quote = math.Round(key.CumulativeQuoteQuantity*100) / 100 /* Quote price */ 108 | tmp.Price = math.Round(key.Price*10000) / 10000 /* Acquisition Price */ 109 | tmp.Target = math.Round((tmp.Price*(1+configData.ProfitMin))*1000) / 1000 /* Target price */ 110 | tmp.Diff = math.Round((((key.ExecutedQuantity*sessiondata.Market.Price)*(1+configData.ExchangeComission))-key.CumulativeQuoteQuantity)*10) / 10 /* Difference between target and market price */ 111 | 112 | sessiondata.Session.Orders = append(sessiondata.Session.Orders, tmp) 113 | sessiondata.Session.QuantityOffset -= tmp.Quantity /* Quantity offset */ 114 | 115 | sessiondata.Session.DiffTotal += (tmp.Diff * (1 - configData.ExchangeComission)) /* Total difference between target and market price (minus ExchangeComission used for visual aid in UI) */ 116 | } 117 | 118 | sessiondata.Session.DiffTotal = math.Round(sessiondata.Session.DiffTotal*1) / 1 /* Total difference between target and market price round up for session (local function variable)*/ 119 | sessionData.DiffTotal = sessiondata.Session.DiffTotal /* Total difference between target and market price for session (tranfer value to sessionData struct)*/ 120 | 121 | if sessiondata.Session.QuantityOffset >= 0 { /* Only display Quantity offset if negative */ 122 | 123 | sessiondata.Session.QuantityOffset = 0 124 | sessionData.QuantityOffsetFlag = false 125 | 126 | } else if sessiondata.Session.QuantityOffset < 0 { /* Only display Quantity offset if negative */ 127 | 128 | sessiondata.Session.QuantityOffset = math.Round(sessiondata.Session.QuantityOffset*100) / 100 /* Quantity offset */ 129 | 130 | if !sessionData.QuantityOffsetFlag { /* Only log Quantity offset error if first time */ 131 | 132 | logger.LogEntry{ /* Log Entry */ 133 | Config: configData, 134 | Market: nil, 135 | Session: sessionData, 136 | Order: &types.Order{}, 137 | Message: "Quantity offset: " + strconv.FormatFloat(sessiondata.Session.QuantityOffset, 'f', 2, 64), 138 | LogLevel: "DebugLevel", 139 | }.Do() 140 | 141 | } 142 | 143 | sessionData.QuantityOffsetFlag = true /* Quantity offset flag */ 144 | 145 | } 146 | 147 | } 148 | 149 | return json.Marshal(sessiondata) 150 | 151 | } 152 | 153 | // LoadSessionDataAdditionalComponentsAsync Load mySQL dynamic components for javascript autoloader for html output. 154 | // This is a separate function because it is reload with scheduler.RunTaskAtInterval via asyncFunctions 155 | func LoadSessionDataAdditionalComponentsAsync(sessionData *types.Session) { 156 | 157 | var err error 158 | 159 | /* Conditional defer logging when there is an error retriving data */ 160 | defer func() { 161 | if err != nil { 162 | logger.LogEntry{ /* Log Entry */ 163 | Config: nil, 164 | Market: nil, 165 | Session: sessionData, 166 | Order: &types.Order{}, 167 | Message: functions.GetFunctionName() + " - " + err.Error(), 168 | LogLevel: "DebugLevel", 169 | }.Do() 170 | } 171 | }() 172 | 173 | /* Get global data and execute GetProfit if more than 10 seconds since last update. 174 | This function is used to prevent multiple threads from running mysql.GetProfit and 175 | overloading mySQL server since this is a high cost SQL statement. */ 176 | if profit, profitnet, profitPct, transactTime, err := mysql.GetGlobal(sessionData); err == nil { 177 | 178 | sessionData.Global.Profit = profit /* Load global profit from db */ 179 | sessionData.Global.ProfitNet = profitnet /* Load global net profit from db */ 180 | sessionData.Global.ProfitPct = profitPct /* Load global profit from db */ 181 | 182 | if transactTime == 0 { /* If transactTime is 0 then this is the first time this function is called and insert record into db */ 183 | 184 | if err := mysql.SaveGlobal(sessionData); err != nil { 185 | 186 | return /* Return if error */ 187 | 188 | } 189 | 190 | } 191 | 192 | if time.Since(time.Unix(transactTime, 0)).Seconds() > 10 { /* Only execute GetProfit if more than 10 seconds since last update */ 193 | 194 | if sessionData.Global.Profit, sessionData.Global.ProfitNet, sessionData.Global.ProfitPct, err = mysql.GetProfit(sessionData); err != nil { /* Recalculate total profit and total profit percentage */ 195 | 196 | return /* Return if error */ 197 | 198 | } 199 | 200 | if err = mysql.UpdateGlobal(sessionData); err != nil { /* Update global data */ 201 | 202 | return /* Return if error */ 203 | 204 | } 205 | 206 | } 207 | 208 | } 209 | 210 | /* Load total thread profit and total thread profit percentage */ 211 | if sessionData.Global.ProfitThreadID, sessionData.Global.ProfitThreadIDPct, err = mysql.GetProfitByThreadID(sessionData); err != nil { 212 | 213 | return 214 | 215 | } 216 | 217 | /* Load running thread count */ 218 | if sessionData.Global.ThreadCount, err = mysql.GetThreadCount(sessionData); err != nil { 219 | 220 | return 221 | 222 | } 223 | 224 | /* Load total thread dollar amount */ 225 | if sessionData.Global.ThreadAmount, err = mysql.GetThreadAmount(sessionData); err != nil { 226 | 227 | return 228 | 229 | } 230 | 231 | } 232 | -------------------------------------------------------------------------------- /documentation/HowToUse.md: -------------------------------------------------------------------------------- 1 | ## HOW TO USE 2 | 3 | Cryptopump opens in your browse it's first instance. 4 | 5 | ### METRICS 6 | 7 | - On the top left you it shows the thread name and how many instances are in execution. 8 | 9 | - Profit: Shows the Total Profit (Total Profit = Sales - Buys); the Net Profit (Net Profit = Total Profit - Order Differences) where Order Difference is the total difference between each order price and the current pair price for all threads. Another way to understand Net Profit is to look at is as the total profit if all orders were to be closed at that moment in time. Net profit is important because CryptoPump will use Profits to buy orders if the crypto pair goes down in price; finally, the average transaction percentage profit across all present and past running threads. 10 | 11 | - Thread Profit: Shows the ToNet Profit (Net Thread Profit = Total Thread Profit - Order Thread Differences) where Order Difference is the total difference between each order price and the current pair price for the current threads.; finally, the average transaction percentage profit across the running thread. 12 | 13 | - Diff: Shows the sum of Order Differences for the current thread. Order Difference is the total difference between each order price and the current pair price for the current threads. 14 | 15 | - Deployed: Shows how much fiat currency is in use across all threads. 16 | 17 | - Funds: Shows the total amount of crypto pairs acquired by the current thread and the amount of FIAT currency available for additional purchases. 18 | 19 | - Offset : In rare circumstances the database may become out-of-sync with the amount of crypto invested due to the Exchange or Connectivity error. This field represent the disparity between the system and the exchange quantities. (0 means no difference and all is good) 20 | 21 | - Transact./h: Number of Sale transactions per hour. 22 | 23 | - MACD is the Moving Average Convergence Divergence it is a trend-following momentum indicator that shows the relationship between two moving averages. 24 | 25 | - RSI 14/7/3 is the Relative Strength Index it's an indicator based on closing prices over a duration of specific time. 26 | 27 | - Direction: Updated every second from the exchange and is increased at each movement in the same direction, i.e. if the price moves up 10 consecutive times then the direction will be 10. 28 | 29 | - Price$: Current price of the selected crypto currency. 30 | 31 | 32 | ## SETTING UP 33 | 34 | You can use any template to start your personal configuration and modify the values to suit the trading style you want the bot to have. 35 | 36 | ### BUY 37 | 38 | - Buy Quantity FIAT Upmarket: this is the quantity in fiat currency that the bot will attempt to buy when the direction of the market is up (according to Buy Direction Upmarket), when the pair market price is above the lowest existing transaction waiting to be sold, i.e. 30 for BTC USDT will use $30 USDT every time it buys according to the value of buy direction upmarket. 39 | 40 | - Buy Quantity FIAT Downmarket: this is the quantity in fiat currency that the bot will attempt to buy when the direction of the market is up (according to Buy Direction Downmarket). i.e. 30 for BTC USDT will use $30 every time it buys according to the value of buy direction downmarket. 41 | 42 | - Buy Quantity FIAT Initial: The initial amount of fiat currency that should be used by the bot, i.e. if BTC USDT and set to $30 the first buy order will be of $30 USDT. 43 | 44 | - Buy Direction Upmarket: This value is the number of consecutive movements the market does increasing the price of the asset before putting a buy order, i.e. if set to 10 the market needs to move up 10 times consecutive before executing a buy order. The higher the value, the more bullish the market needs to be in order to execute a buy order. 45 | 46 | - Buy Direction Downmarket: This value is the number of consecutive movement the market does decreasing the price of the asset before putting a buy order, i.e. if set to 10 the market needs to move down 10 times consecutive before executing a buy order. 47 | The higher the value, the more bullish the market needs to be in order to execute a buy order. 48 | 49 | - Buy on RSI7: This value indicates the value of RSI (Relative Strength Index) that should be lower so a buy order can be executed, i.e. if set to 45 RSI7 needs to be bellow 45, meaning over sold, so the bot execute the order. 50 | 51 | - Buy 24hs HighPrice Entry: This value indicates the maximum amount that bot can buy in relation to the 24 hours highest price, i.e. if set to 0,0003 the bot will buy at a maximum of 0,3% of the highest price. 52 | 53 | - Buy Repeat Threshold up: This value indicates the percentage that needs to be increased in relation to the last buy transaction so another buy order is executed, i.e. if set to 0,0001 it needs to be 0,1% different of the previews price. 54 | 55 | - Buy Repeat Threshold down: This value indicates the percentage that needs to be decreased in relation to the last buy transaction price so another buy order is executed, i.e. if set to 0,002 it needs to be 2% different of the previews price. 56 | 57 | - Buy repeat threshold 2nd down: This value indicates the percentage that needs to be decreased in relation to the first decreased value on a down market so another buy order is executed, i.e. if set to 0,005 it needs to be 5% lower in relation to the previews price. 58 | 59 | - Buy Repeat threshold 2nd Down Start Count: The number os buys in downmarket before "Buy repeat threshold 2nd down" enters into effect. 60 | 61 | - Buy Wait: Minimum wait time in seconds before executing buy orders, i.e. if set to 10 it will take 10 seconds between buy orders. 62 | 63 | ### SELL 64 | 65 | - Minimum Profit: this value indicates the minimum profit so the bot executes a sell order, i.e. if set to 0,005 it will sell an order for 0,5% + exchange commission price. 66 | 67 | - Wait After Cancel: this value indicates the number of seconds the bot waits after canceling an order and performing another, i.e. if set to 10 it will wait 10 seconds to execute another order. 68 | 69 | - Wait Before Cancel: this value indicates the number of seconds the bot waits before canceling an order, i.e. if set to 10 it will wait 10 seconds before cancelling an order. 70 | 71 | - Sell-to-Cover Low Funds: True or False. This option allows the bot to sell your highest buy orders so it can buy at lower values in a down market. (this feature enabled the bot to keep operating, but effectively sell at a loss). 72 | 73 | - Hold Sale on RSI3: This value sets the value of RSI3 (Relative Strength Index) to avoid selling an order, i.e. if set to 70 and RSI3 above 70, meaning the market is over bought, it will not executed a sell order. 74 | 75 | - Stoploss: This option allows the bot to sell your order if the ratio greater than the value, i.e. if the current price is too low compared to the moment it was bought it will sell to avoid increased loss. 76 | 77 | - Exchange Name: the name of the exchange used. Only BINANCE is supported at the moment. 78 | 79 | - Exchange commission: The commission taken by the exchange that the bot needs to add when selling an order, i.e. if set to 0,00075 the commission is 0,75% per order when using BNB or set to 0,001 when paying with other currencies for 0,1% commission per order. 80 | 81 | - Symbol FIAT: The symbol used for the FIAT currency, i.e. USDT, BUSD. 82 | 83 | - Symbol FIAT Stash: the amount of FIAT that should be preserved by the bot, i.e. if set to 10 and FIAT set to USDT it will always maintain $10 USDT in your trading wallet. 84 | 85 | - Symbol: The pair that the bot will trade in this particular instance, i.e. BTCUSDT. 86 | 87 | - Enforce Time: True or False, enables the bot to operate during a set period of time set on Start Time and Stop Time. 88 | 89 | - Start Time: If enforce time is set to true this value is used as a start time for the bot operation. 90 | 91 | - Stop Time: If enforce time is set to true this value is used to stop the bot operation. 92 | 93 | ### ORDERS GRID 94 | 95 | - OrderID: this value is provided by the exchange when a buy order takes place. 96 | 97 | - Quantity: this value indicates the transaction quantity in crypto-currency. 98 | 99 | - Quote: this value indicates the transaction total amount in FIAT. 100 | 101 | - Price: this value indicates the transaction crypto-currency execution price. 102 | 103 | - Target: this value indicates the target crypto-currency price before selling (includes profit margin and exchange fees). 104 | 105 | - Diff: this value indicates the difference between current transaction sale price and zero margin sale (includes exchange fee). 106 | 107 | - Action [Sell]: The Sell button allows you to execute a sale of an existing order. The 'Diff' lets you know if the order is likely to have a profitable sale or if it is underwater. The sale will occur on the spot market at current market prices. 108 | 109 | ## STATUS 110 | 111 | In the bottom right corner the system status is displayed: 112 | 113 | - Buy: Reason in the decision tree on why a given Buy order is not being executed. This field is important and provide information on what configuration tunning might be required. 114 | 115 | - Sell: Reason in the decision tree on why a given Sell order is not being executed. This field is important and provide information on what configuration tunning might be required. 116 | 117 | - Ops/dec: Number of operation per second. This number is dictated by the crypto-pair volume. Cryptopump analyses every Exchange kline block. 118 | 119 | - Signal: Average latency between Cryptopump and the exchange measured every five seconds (best kept below 200ms). 120 | 121 | ### OTHERS: 122 | 123 | - Debug: True or False, enable debug mode output on logs. 124 | 125 | - Exit: True or false, when set to true the bot will stop buying, and when there are no more transactions to be done, meaning all previews buy orders are sold, it will close the instance the bot is running on. 126 | 127 | - DryRun: True or False, when enabled run the bot in DryRun mode without executing a buy or sell order. 128 | 129 | - New Session: True or False, when enabled forces a new session with the bot. Use it if you want to change the Symbol FIAT and Symbol of the trading pair. 130 | 131 | - TestNet: True or False, when enabled starts the bot on Binance TestNet without using real money (require Binance TestNet API keys). 132 | 133 | - Template: select which template the bot will use to avoid writing the same settings multiple times. 134 | 135 | 136 | ### BUTTONS: 137 | 138 | - Admin: Global configuration page where the exchange API Key, API Secret, API Key TestNet, API Secret TestNet and the Telegram Bot API can be configures. This configuration applies too all CryptoPump sessions and threads. 139 | 140 | - New: When a session is already in progress it will start a new session on a different HTTP port, i.e. if running the first session on 8080 it will start the next one on 8081. 141 | 142 | - Start: Start the bot on the trading pair previously set. 143 | 144 | - Stop: Stop the bot without selling your active orders. 145 | 146 | - Update: write the changes made within the webui into the configuration file. 147 | 148 | - Buy market: Buy order. The purchase will occur on the spot market at current market prices. 149 | 150 | - Sell market: Sell the top order in the orders table. The sale will occur on the spot market at current market prices. 151 | 152 | 153 | ### TELEGRAM: 154 | 155 | Telegram allows you to remote monitor that status of your running cryptopump instances, and BUY/SELL orders. The currently available command are: 156 | 157 | ![](https://github.com/aleibovici/img/blob/b2c9390494906b8e83635a5f320dd48f67a48fbd/telegram_screenshot.jpg?raw=true) 158 | 159 | - /report: Provides Available Funds, Deployed Funds, Profit, Return on Investment, Net Profit, Net Return on Investment, Avg. Transaction Percentage gain, Thread Count, System Status, and Master Node. 160 | - /buy: Buy at the current Master Node thread 161 | - /sell: Sell at the current Master Node thread 162 | 163 | ## RESUMING AND TROUBLESHOOTING: 164 | 165 | If you want to stop buy don't want to sell your orders, press stop at each instance. 166 | To resume start the bot, access the first WebUI, i.e. port 8080, press start. To access the other trading pairs, press new, start the new webui, i.e. port 8081 and press start. Repeat until all instances are resumed. 167 | 168 | If resuming a thread/instance does not work, go into the cryptopump folder and delete the .lock files. Those files are present while the bot is running, if it crashes those won't be deleted so those need to be manually removed before starting the resume process. 169 | -------------------------------------------------------------------------------- /exchange/binance.go: -------------------------------------------------------------------------------- 1 | package exchange 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "time" 7 | 8 | "github.com/aleibovici/cryptopump/functions" 9 | "github.com/aleibovici/cryptopump/logger" 10 | "github.com/aleibovici/cryptopump/threads" 11 | "github.com/aleibovici/cryptopump/types" 12 | 13 | "github.com/adshao/go-binance/v2" 14 | ) 15 | 16 | /* Map binance.Order types to Order type */ 17 | func binanceMapOrder(from *binance.Order) (to *types.Order) { 18 | 19 | to = &types.Order{} 20 | to.ClientOrderID = from.ClientOrderID 21 | to.OrderID = int(from.OrderID) 22 | to.CumulativeQuoteQuantity = functions.StrToFloat64(from.CummulativeQuoteQuantity) 23 | to.ExecutedQuantity = functions.StrToFloat64(from.ExecutedQuantity) 24 | to.Price = functions.StrToFloat64(from.Price) 25 | to.Side = string(from.Side) 26 | to.Status = string(from.Status) 27 | to.Symbol = from.Symbol 28 | 29 | return to 30 | 31 | } 32 | 33 | /* Map binance.CreateOrderResponse types to Order type */ 34 | func binanceMapCreateOrderResponse(from *binance.CreateOrderResponse) (to *types.Order) { 35 | 36 | to = &types.Order{} 37 | to.ClientOrderID = from.ClientOrderID 38 | to.OrderID = int(from.OrderID) 39 | to.CumulativeQuoteQuantity = functions.StrToFloat64(from.CummulativeQuoteQuantity) 40 | to.ExecutedQuantity = functions.StrToFloat64(from.ExecutedQuantity) 41 | to.Price = functions.StrToFloat64(from.Price) 42 | to.Side = string(from.Side) 43 | to.Status = string(from.Status) 44 | to.Symbol = from.Symbol 45 | to.TransactTime = from.TransactTime 46 | 47 | return to 48 | 49 | } 50 | 51 | /* Map binance.CancelOrderResponse types to Order type */ 52 | func binanceMapCancelOrderResponse(from *binance.CancelOrderResponse) (to *types.Order) { 53 | 54 | to = &types.Order{} 55 | to.ClientOrderID = from.ClientOrderID 56 | to.OrderID = int(from.OrderID) 57 | to.CumulativeQuoteQuantity = functions.StrToFloat64(from.CummulativeQuoteQuantity) 58 | to.ExecutedQuantity = functions.StrToFloat64(from.ExecutedQuantity) 59 | to.Price = functions.StrToFloat64(from.Price) 60 | to.Side = string(from.Side) 61 | to.Status = string(from.Status) 62 | to.Symbol = from.Symbol 63 | to.TransactTime = from.TransactTime 64 | 65 | return to 66 | 67 | } 68 | 69 | /* Map binance.Kline types to Kline type */ 70 | func binanceMapKline(from []*binance.Kline) (to []*types.Kline) { 71 | 72 | to = []*types.Kline{} 73 | 74 | for key := range from { 75 | 76 | tmp := &types.Kline{} 77 | tmp.Close = from[key].Close 78 | tmp.High = from[key].High 79 | tmp.Low = from[key].Low 80 | tmp.Open = from[key].Open 81 | tmp.OpenTime = from[key].OpenTime 82 | tmp.Volume = from[key].Volume 83 | 84 | to = append(to, tmp) 85 | 86 | } 87 | 88 | return to 89 | 90 | } 91 | 92 | // BinanceMapWsKline Map binance.WsKline types to WsKline type 93 | func BinanceMapWsKline(from binance.WsKline) (to types.WsKline) { 94 | 95 | to = types.WsKline{} 96 | to.ActiveBuyQuoteVolume = from.ActiveBuyQuoteVolume 97 | to.ActiveBuyVolume = from.ActiveBuyQuoteVolume 98 | to.Close = from.Close 99 | to.EndTime = from.EndTime 100 | to.FirstTradeID = from.FirstTradeID 101 | to.High = from.High 102 | to.Interval = from.Interval 103 | to.IsFinal = from.IsFinal 104 | to.LastTradeID = from.LastTradeID 105 | to.Low = from.Low 106 | to.Open = from.Open 107 | to.QuoteVolume = from.QuoteVolume 108 | to.StartTime = from.StartTime 109 | to.Symbol = from.Symbol 110 | to.TradeNum = from.TradeNum 111 | to.Volume = from.Volume 112 | 113 | return to 114 | 115 | } 116 | 117 | /* Map binance.PriceChangeStats types to Kline type */ 118 | func binanceMapPriceChangeStats(from []*binance.PriceChangeStats) (to []*types.PriceChangeStats) { 119 | 120 | to = []*types.PriceChangeStats{} 121 | 122 | for key := range from { 123 | 124 | tmp := &types.PriceChangeStats{} 125 | tmp.HighPrice = from[key].HighPrice 126 | tmp.LowPrice = from[key].LowPrice 127 | 128 | to = append(to, tmp) 129 | 130 | } 131 | 132 | return to 133 | 134 | } 135 | 136 | /* Map binance.ExchangeInfo types to Order type */ 137 | func binanceMapExchangeInfo(sessionData *types.Session, from *binance.ExchangeInfo) (to *types.ExchangeInfo) { 138 | 139 | to = &types.ExchangeInfo{} 140 | 141 | for key := range from.Symbols { 142 | 143 | if from.Symbols[key].Symbol == sessionData.Symbol { 144 | 145 | to.MaxQuantity = from.Symbols[key].LotSizeFilter().MaxQuantity 146 | to.MinQuantity = from.Symbols[key].LotSizeFilter().MinQuantity 147 | to.StepSize = from.Symbols[key].LotSizeFilter().StepSize 148 | 149 | } 150 | 151 | } 152 | 153 | return to 154 | 155 | } 156 | 157 | /* Get Binance client */ 158 | func binanceGetClient( 159 | configData *types.Config) *binance.Client { 160 | 161 | binance.WebsocketKeepalive = false /* Disable websocket keepalive */ 162 | binance.WebsocketTimeout = time.Second * 100 /* Set websocket timeout */ 163 | 164 | /* If the -test.v flag is set, the testnet API is used */ 165 | if flag.Lookup("test.v") != nil { 166 | 167 | binance.UseTestnet = true 168 | return binance.NewClient(configData.ConfigGlobal.ApikeyTestNet, configData.ConfigGlobal.SecretkeyTestNet) 169 | 170 | } 171 | 172 | /* Exchange test network, used with launch.json */ 173 | if configData.TestNet { 174 | 175 | binance.UseTestnet = true 176 | return binance.NewClient(configData.ConfigGlobal.ApikeyTestNet, configData.ConfigGlobal.SecretkeyTestNet) 177 | 178 | } 179 | 180 | return binance.NewClient(configData.ConfigGlobal.Apikey, configData.ConfigGlobal.Secretkey) 181 | 182 | } 183 | 184 | /* Retrieve exchange information */ 185 | func binanceGetInfo( 186 | sessionData *types.Session) (info *types.ExchangeInfo, err error) { 187 | 188 | var tmp *binance.ExchangeInfo 189 | 190 | if tmp, err = sessionData.Clients.Binance.NewExchangeInfoService().Do(context.Background()); err != nil { 191 | 192 | return nil, err 193 | 194 | } 195 | 196 | return binanceMapExchangeInfo(sessionData, tmp), err 197 | 198 | } 199 | 200 | /* Retrieve listen key for user stream service */ 201 | func binanceGetUserStreamServiceListenKey( 202 | sessionData *types.Session) (listenKey string, err error) { 203 | 204 | if listenKey, err = sessionData.Clients.Binance.NewStartUserStreamService().Do(context.Background()); err != nil { 205 | 206 | logger.LogEntry{ /* Log Entry */ 207 | Config: nil, 208 | Market: nil, 209 | Session: sessionData, 210 | Order: &types.Order{}, 211 | Message: functions.GetFunctionName() + " - " + err.Error(), 212 | LogLevel: "DebugLevel", 213 | }.Do() 214 | 215 | return "", err 216 | 217 | } 218 | 219 | return listenKey, err 220 | 221 | } 222 | 223 | /* Keep user stream service alive */ 224 | func binanceKeepAliveUserStreamServiceListenKey( 225 | sessionData *types.Session) (err error) { 226 | 227 | if err = sessionData.Clients.Binance.NewKeepaliveUserStreamService().ListenKey(sessionData.ListenKey).Do(context.Background()); err != nil { 228 | 229 | return err 230 | 231 | } 232 | 233 | return 234 | 235 | } 236 | 237 | /* Synchronize time */ 238 | func binanceNewSetServerTimeService( 239 | sessionData *types.Session) (err error) { 240 | 241 | if _, err = sessionData.Clients.Binance.NewSetServerTimeService().Do(context.Background()); err != nil { 242 | 243 | return err 244 | 245 | } 246 | 247 | return 248 | 249 | } 250 | 251 | /* Get account */ 252 | func binanceGetAccount(sessionData *types.Session) (account *binance.Account, err error) { 253 | 254 | if account, err = sessionData.Clients.Binance.NewGetAccountService().Do(context.Background()); err != nil { 255 | 256 | logger.LogEntry{ /* Log Entry */ 257 | Config: nil, 258 | Market: nil, 259 | Session: sessionData, 260 | Order: &types.Order{}, 261 | Message: functions.GetFunctionName() + " - " + err.Error(), 262 | LogLevel: "DebugLevel", 263 | }.Do() 264 | 265 | } 266 | 267 | return account, err 268 | 269 | } 270 | 271 | /* Retrieve symbol fiat funds available */ 272 | func binanceGetSymbolFiatFunds( 273 | sessionData *types.Session) (balance float64, err error) { 274 | 275 | var account *binance.Account 276 | 277 | if account, err = binanceGetAccount(sessionData); err != nil { 278 | 279 | return 0, err 280 | 281 | } 282 | 283 | for key := range account.Balances { /* Loop through balances */ 284 | 285 | if account.Balances[key].Asset == sessionData.SymbolFiat { /* If the balance is the fiat balance */ 286 | 287 | return functions.StrToFloat64(account.Balances[key].Free), err /* Return balance */ 288 | 289 | } 290 | 291 | } 292 | 293 | return 0, err 294 | 295 | } 296 | 297 | /* Retrieve symbol funds available */ 298 | func binanceGetSymbolFunds( 299 | sessionData *types.Session) (balance float64, err error) { 300 | 301 | var account *binance.Account 302 | 303 | if account, err = binanceGetAccount(sessionData); err != nil { 304 | 305 | logger.LogEntry{ /* Log Entry */ 306 | Config: nil, 307 | Market: nil, 308 | Session: sessionData, 309 | Order: &types.Order{}, 310 | Message: functions.GetFunctionName() + " - " + err.Error(), 311 | LogLevel: "DebugLevel", 312 | }.Do() 313 | 314 | return 0, err 315 | 316 | } 317 | 318 | for key := range account.Balances { /* Loop through balances */ 319 | 320 | if account.Balances[key].Asset == sessionData.Symbol[0:3] { /* Check if asset is correct */ 321 | 322 | return functions.StrToFloat64(account.Balances[key].Free), err /* Return balance */ 323 | 324 | } 325 | 326 | } 327 | 328 | /* Cleanly exit ThreadID */ 329 | threads.Thread{}.Terminate(sessionData, "Balance or Pair not found for symbol "+sessionData.Symbol[0:3]) 330 | 331 | return 0, err 332 | 333 | } 334 | 335 | /* Minutely crypto currency open/close prices, high/low, trades and others */ 336 | func binanceGetKlines( 337 | sessionData *types.Session) (klines []*binance.Kline, err error) { 338 | 339 | if klines, err = sessionData.Clients.Binance.NewKlinesService().Symbol(sessionData.Symbol). 340 | Interval("1m").Limit(14).Do(context.Background()); err != nil { 341 | 342 | return nil, err 343 | 344 | } 345 | 346 | return klines, err 347 | 348 | } 349 | 350 | /* 24hr ticker price change statistics */ 351 | func binanceGetPriceChangeStats( 352 | sessionData *types.Session) (PriceChangeStats []*types.PriceChangeStats, err error) { 353 | 354 | var tmp []*binance.PriceChangeStats 355 | 356 | if tmp, err = sessionData.Clients.Binance.NewListPriceChangeStatsService().Symbol(sessionData.Symbol).Do(context.Background()); err != nil { 357 | 358 | return nil, err 359 | 360 | } 361 | 362 | return binanceMapPriceChangeStats(tmp), err 363 | 364 | } 365 | 366 | /* Retrieve Order Status */ 367 | func binanceGetOrder( 368 | sessionData *types.Session, 369 | orderID int64) (order *types.Order, err error) { 370 | 371 | var tmp *binance.Order 372 | 373 | if tmp, err = sessionData.Clients.Binance.NewGetOrderService().Symbol(sessionData.Symbol).OrderID(orderID).Do(context.Background()); err != nil { 374 | 375 | return nil, err 376 | 377 | } 378 | 379 | return binanceMapOrder(tmp), err 380 | 381 | } 382 | 383 | /* CANCEL an order */ 384 | func binanceCancelOrder( 385 | sessionData *types.Session, 386 | orderID int64) (cancelOrderResponse *types.Order, err error) { 387 | 388 | var tmp *binance.CancelOrderResponse 389 | 390 | if tmp, err = sessionData.Clients.Binance.NewCancelOrderService().Symbol(sessionData.Symbol).OrderID(orderID).Do(context.Background()); err != nil { 391 | 392 | return nil, err 393 | 394 | } 395 | 396 | return binanceMapCancelOrderResponse(tmp), err 397 | 398 | } 399 | 400 | /* Create order to BUY */ 401 | func binanceBuyOrder( 402 | sessionData *types.Session, 403 | quantity string) (order *types.Order, err error) { 404 | 405 | var tmp *binance.CreateOrderResponse 406 | 407 | /* Execute OrderTypeMarket */ 408 | if tmp, err = sessionData.Clients.Binance.NewCreateOrderService().Symbol(sessionData.Symbol). 409 | Side(binance.SideTypeBuy).Type(binance.OrderTypeMarket). 410 | Quantity(quantity).Do(context.Background()); err != nil { 411 | 412 | logger.LogEntry{ /* Log Entry */ 413 | Config: nil, 414 | Market: nil, 415 | Session: sessionData, 416 | Order: &types.Order{}, 417 | Message: functions.GetFunctionName() + " - " + err.Error(), 418 | LogLevel: "InfoLevel", 419 | }.Do() 420 | 421 | return nil, err 422 | 423 | } 424 | 425 | return binanceMapCreateOrderResponse(tmp), err 426 | 427 | } 428 | 429 | /* WsBookTickerServe serve websocket that pushes updates to the best bid or ask price or quantity in real-time for a specified symbol. */ 430 | func binanceWsBookTickerServe( 431 | sessionData *types.Session, 432 | wsHandler *types.WsHandler, 433 | errHandler func(err error)) (doneC chan struct{}, stopC chan struct{}, err error) { 434 | 435 | doneC, stopC, err = binance.WsBookTickerServe(sessionData.Symbol, wsHandler.BinanceWsBookTicker, errHandler) 436 | 437 | return doneC, stopC, err 438 | 439 | } 440 | 441 | /* WsKlineServe serve websocket kline handler */ 442 | func binanceWsKlineServe( 443 | sessionData *types.Session, 444 | wsHandler *types.WsHandler, 445 | errHandler func(err error)) (doneC chan struct{}, stopC chan struct{}, err error) { 446 | 447 | doneC, stopC, err = binance.WsKlineServe(sessionData.Symbol, "1m", wsHandler.BinanceWsKline, errHandler) 448 | 449 | return doneC, stopC, err 450 | 451 | } 452 | 453 | /* WsUserDataServe serve user data handler with listen key */ 454 | func binanceWsUserDataServe( 455 | sessionData *types.Session, 456 | wsHandler *types.WsHandler, 457 | errHandler func(err error)) (doneC chan struct{}, stopC chan struct{}, err error) { 458 | 459 | doneC, stopC, err = binance.WsUserDataServe(sessionData.ListenKey, wsHandler.BinanceWsUserDataServe, errHandler) 460 | 461 | return doneC, stopC, err 462 | } 463 | 464 | /* Create order to SELL */ 465 | func binanceSellOrder( 466 | marketData *types.Market, 467 | sessionData *types.Session, 468 | quantity string) (order *types.Order, err error) { 469 | 470 | var tmp *binance.CreateOrderResponse 471 | 472 | if !sessionData.ForceSell { 473 | 474 | /* Execute OrderTypeLimit */ 475 | if tmp, err = sessionData.Clients.Binance.NewCreateOrderService().Symbol(sessionData.Symbol).Side(binance.SideTypeSell).Type(binance.OrderTypeLimit).Quantity(quantity).Price(functions.Float64ToStr(marketData.Price, 2)).TimeInForce(binance.TimeInForceTypeGTC).Do(context.Background()); err != nil { 476 | 477 | return nil, err 478 | 479 | } 480 | 481 | } 482 | 483 | if sessionData.ForceSell { 484 | 485 | sessionData.ForceSell = false 486 | 487 | /* Execute OrderTypeMarket */ 488 | if tmp, err = sessionData.Clients.Binance.NewCreateOrderService().Symbol(sessionData.Symbol).Side(binance.SideTypeSell).Type(binance.OrderTypeMarket).Quantity(quantity).Do(context.Background()); err != nil { 489 | 490 | return nil, err 491 | 492 | } 493 | 494 | } 495 | 496 | return binanceMapCreateOrderResponse(tmp), err 497 | } 498 | -------------------------------------------------------------------------------- /types/types.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "database/sql" 5 | "time" 6 | 7 | "github.com/adshao/go-binance/v2" 8 | tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api" 9 | "github.com/paulbellamy/ratecounter" 10 | "github.com/sdcoffey/techan" 11 | "github.com/spf13/viper" 12 | ) 13 | 14 | // Order struct define an exchange order 15 | type Order struct { 16 | ClientOrderID string `json:"clientOrderId"` 17 | CumulativeQuoteQuantity float64 `json:"cumulativeQuoteQty"` 18 | ExecutedQuantity float64 `json:"executedQty"` 19 | OrderID int `json:"orderId"` 20 | Price float64 `json:"price"` 21 | Side string `json:"side"` 22 | Status string `json:"status"` 23 | Symbol string `json:"symbol"` 24 | TransactTime int64 `json:"transactTime"` 25 | ThreadID int 26 | ThreadIDSession int 27 | OrderIDSource int /* Used for logging purposes to define source OrderID for a sale */ 28 | } 29 | 30 | // Kline struct define a kline 31 | type Kline struct { 32 | OpenTime int64 `json:"openTime"` 33 | Open string `json:"open"` 34 | High string `json:"high"` 35 | Low string `json:"low"` 36 | Close string `json:"close"` 37 | Volume string `json:"volume"` 38 | } 39 | 40 | // WsKline struct define websocket kline 41 | type WsKline struct { 42 | StartTime int64 `json:"t"` 43 | EndTime int64 `json:"T"` /* Currently not in use */ 44 | Symbol string `json:"s"` /* Currently not in use */ 45 | Interval string `json:"i"` /* Currently not in use */ 46 | FirstTradeID int64 `json:"f"` /* Currently not in use */ 47 | LastTradeID int64 `json:"L"` /* Currently not in use */ 48 | Open string `json:"o"` 49 | Close string `json:"c"` 50 | High string `json:"h"` 51 | Low string `json:"l"` 52 | Volume string `json:"v"` 53 | TradeNum int64 `json:"n"` /* Currently not in use */ 54 | IsFinal bool `json:"x"` 55 | QuoteVolume string `json:"q"` /* Currently not in use */ 56 | ActiveBuyVolume string `json:"V"` /* Currently not in use */ 57 | ActiveBuyQuoteVolume string `json:"Q"` /* Currently not in use */ 58 | } 59 | 60 | // PriceChangeStats define price change stats 61 | type PriceChangeStats struct { 62 | HighPrice string `json:"highPrice"` 63 | LowPrice string `json:"lowPrice"` 64 | } 65 | 66 | // ExchangeInfo define exchange order size 67 | type ExchangeInfo struct { 68 | MaxQuantity string `json:"maxQty"` 69 | MinQuantity string `json:"minQty"` 70 | StepSize string `json:"stepSize"` 71 | } 72 | 73 | // Session struct define session elements 74 | type Session struct { 75 | ThreadID string /* Unique session ID for the thread */ 76 | ThreadIDSession string 77 | ThreadCount int 78 | SellTransactionCount float64 /* Number of SELL transactions in the last 60 minutes */ 79 | Symbol string /* Symbol */ 80 | SymbolFunds float64 /* Available crypto funds in exchange */ 81 | SymbolFiat string /* Fiat symbol */ 82 | SymbolFiatFunds float64 /* Available fiat funds in exchange */ 83 | LastBuyTransactTime time.Time /* This session variable stores the time of the last buy */ 84 | LastSellCanceledTime time.Time /* This session variable stores the time of the cancelled sell */ 85 | LastWsKlineTime time.Time /* This session variable stores the time of the last WsKline used for status check */ 86 | LastWsBookTickerTime time.Time /* This session variable stores the time of the last WsBookTicker used for status check */ 87 | LastWsUserDataServeTime time.Time /* This session variable stores the time of the last WsUserDataServe used for status check */ 88 | ConfigTemplate int 89 | ForceBuy bool /* This boolean when True force BUY transaction */ 90 | ForceSell bool /* This boolean when True force SELL transaction */ 91 | ForceSellOrderID int /* This variable stores the OrderID of ForceSell */ 92 | ListenKey string /* Listen key for user stream service */ 93 | MasterNode bool /* This boolean is true when Master Node is elected */ 94 | TgBotAPI *tgbotapi.BotAPI /* This variable holds Telegram session bot */ 95 | TgBotAPIChatID int64 /* This variable holds Telegram chat ID */ 96 | Db *sql.DB /* mySQL database connection */ 97 | Clients Client /* Binance client connection */ 98 | KlineData []KlineData /* kline data format for go-echart plotter */ 99 | StopWs bool /* Control when to stop Ws Channels */ 100 | Busy bool /* Control wether buy/selling to allow graceful session exit */ 101 | MinQuantity float64 /* Defines the minimum quantity allowed by exchange */ 102 | MaxQuantity float64 /* Defines the maximum quantity allowed by exchange */ 103 | StepSize float64 /* Defines the intervals that a quantity can be increased/decreased by exchange */ 104 | Latency int64 /* Latency between the exchange and client */ 105 | Status bool /* System status Good (false) or Bad (true) */ 106 | RateCounter *ratecounter.RateCounter /* Average Number of transactions per second proccessed by WsBookTicker */ 107 | BuyDecisionTreeResult string /* Hold BuyDecisionTree result for web UI */ 108 | SellDecisionTreeResult string /* Hold SellDecisionTree result for web UI */ 109 | QuantityOffsetFlag bool /* This flag is true when the quantity is offset */ 110 | DiffTotal float64 /* This variable holds the difference between the total funds and the total funds in the last session */ 111 | Global *Global 112 | Admin bool /* This flag is true when the admin page is selected */ 113 | Port string /* This variable holds the port number for the web server */ 114 | } 115 | 116 | // Global (Session.Global) struct store semi-persistent values to help offload mySQL queries load 117 | type Global struct { 118 | Profit float64 /* Total profit */ 119 | ProfitNet float64 /* Net profit */ 120 | ProfitPct float64 /* Total profit percentage */ 121 | ProfitThreadID float64 /* ThreadID profit */ 122 | ProfitThreadIDPct float64 /* ThreadID profit percentage */ 123 | ThreadCount int /* Thread count */ 124 | ThreadAmount float64 /* Thread cost amount */ 125 | DiffTotal float64 /* /* This variable holds the difference between purchase price and current value across all sessions */ 126 | } 127 | 128 | // Client struct for client libraries 129 | type Client struct { 130 | Binance *binance.Client 131 | } 132 | 133 | // WsHandler struct for websocket handlers for exchanges 134 | type WsHandler struct { 135 | BinanceWsKline func(event *binance.WsKlineEvent) /* WsKlineServe serve websocket kline handler */ 136 | BinanceWsBookTicker func(event *binance.WsBookTickerEvent) /* WsBookTicker serve websocket kline handler */ 137 | BinanceWsUserDataServe func(message []byte) /* WsUserDataServe serve user data handler with listen key */ 138 | } 139 | 140 | // KlineData struct define kline retention for e-charts plotting 141 | type KlineData struct { 142 | Date int64 143 | Data [4]float64 144 | Volumes float64 145 | Ma7 float64 /* Simple Moving Average for 7 periods */ 146 | Ma14 float64 /* Simple Moving Average for 14 periods */ 147 | } 148 | 149 | // Market struct define realtime market data 150 | type Market struct { 151 | Rsi3 float64 /* Relative Strength Index for 3 periods */ 152 | Rsi7 float64 /* Relative Strength Index for 7 periods */ 153 | Rsi14 float64 /* Relative Strength Index for 14 periods */ 154 | MACD float64 /* Moving average convergence divergence */ 155 | Price float64 /* Market Price */ 156 | PriceChangeStatsHighPrice float64 /* High price for 1 period */ 157 | PriceChangeStatsLowPrice float64 /* Low price for 1 period */ 158 | Direction int /* Market Direction */ 159 | TimeStamp time.Time /* Time of last retrieved market Data */ 160 | Series *techan.TimeSeries /* kline data format for technical analysis */ 161 | Ma7 float64 /* Simple Moving Average for 7 periods */ 162 | Ma14 float64 /* Simple Moving Average for 14 periods */ 163 | } 164 | 165 | // Config struct for configuration 166 | type Config struct { 167 | ThreadID string /* For index.html population */ 168 | Buy24hsHighpriceEntry float64 169 | BuyDirectionDown int 170 | BuyDirectionUp int 171 | BuyQuantityFiatUp float64 172 | BuyQuantityFiatDown float64 173 | BuyQuantityFiatInit float64 174 | BuyRepeatThresholdDown float64 175 | BuyRepeatThresholdDownSecond float64 176 | BuyRepeatThresholdDownSecondStartCount int 177 | BuyRepeatThresholdUp float64 178 | BuyRsi7Entry float64 179 | BuyWait int /* Wait time between BUY transactions in seconds */ 180 | ExchangeComission float64 181 | ProfitMin float64 182 | SellWaitBeforeCancel int /* Wait time before cancelling a sale in seconds */ 183 | SellWaitAfterCancel int /* Wait time before selling after a cancel in seconds */ 184 | SellToCover bool /* Define if will sell to cover low funds */ 185 | SellHoldOnRSI3 float64 /* Hold sale if RSI3 above defined threshold */ 186 | Stoploss float64 /* Loss as ratio that should trigger a sale */ 187 | SymbolFiat string 188 | SymbolFiatStash float64 189 | Symbol string 190 | TimeEnforce bool 191 | TimeStart string 192 | TimeStop string 193 | Debug bool 194 | Exit bool 195 | DryRun bool /* Dry Run mode */ 196 | NewSession bool /* Force a new session instead of resume */ 197 | ConfigTemplateList interface{} /* List of configuration templates available in ./config folder */ 198 | ExchangeName string /* Exchange name */ 199 | TestNet bool /* Use Exchange TestNet */ 200 | HTMLSnippet interface{} /* Store kline plotter graph for html output */ 201 | ConfigGlobal *ConfigGlobal 202 | } 203 | 204 | // ConfigGlobal struct for global configuration 205 | type ConfigGlobal struct { 206 | Apikey string /* Exchange API Key */ 207 | Secretkey string /* Exchange Secret Key */ 208 | ApikeyTestNet string /* API key for exchange test network, used with launch.json */ 209 | SecretkeyTestNet string /* Secret key for exchange test network, used with launch.json */ 210 | TgBotApikey string /* Telegram bot API key */ 211 | } 212 | 213 | // OutboundAccountPosition Struct for User Data Streams for Binance 214 | type OutboundAccountPosition struct { 215 | EventType string `json:"e"` /* Event type */ 216 | EventTime int64 `json:"E"` /* Event Time */ 217 | LastUpdate int64 `json:"u"` /* Time of last account update */ 218 | Balances []Balances `json:"B"` /* Balances Array */ 219 | } 220 | 221 | // Balances Struct for User Data Streams for Binance 222 | type Balances struct { 223 | Asset string `json:"a"` /* Asset */ 224 | Free string `json:"f"` /* Free */ 225 | Locked string `json:"l"` /* Locked */ 226 | } 227 | 228 | // ExecutionReport struct define exchange websocket transactions 229 | type ExecutionReport struct { 230 | EventType string `json:"e"` //Event type 231 | EventTime int64 `json:"E"` //Event Time 232 | Symbol string `json:"s"` //Symbol 233 | ClientOrderID string `json:"c"` //Client order ID 234 | Side string `json:"S"` //Side 235 | OrderType string `json:"o"` //Order type 236 | TimeInForce string `json:"f"` //Time in force 237 | Quantity string `json:"q"` //Order quantity 238 | Price string `json:"p"` //Order price 239 | StopPrice string `json:"P"` //Stop price 240 | IcebergQuantity string `json:"F"` //Iceberg quantity 241 | OrderListID int64 `json:"g"` //OrderListId 242 | OriginalClientOrderID string `json:"C"` //Original client order ID; This is the ID of the order being canceled 243 | ExecutionType string `json:"x"` //Current execution type 244 | Status string `json:"X"` //Current order status 245 | OrderRejectReason string `json:"r"` //Order reject reason; will be an error code. 246 | OrderID int `json:"i"` //Order ID 247 | LastExecutedQuantity string `json:"l"` //Last executed quantity 248 | CumulativeQty string `json:"z"` //Cumulative filled quantity 249 | LastExecutedPrice string `json:"L"` //Last executed price 250 | ComissionAmount string `json:"n"` //Commission amount 251 | ComissionAsset string `json:"N"` //Commission asset 252 | TransactTime int64 `json:"T"` //Transaction time 253 | TradeID int `json:"t"` //Trade ID 254 | Ignore0 int `json:"I"` //Ignore 255 | IsOrderOnTheBook bool `json:"w"` //Is the order on the book? 256 | IsTradeMakerSide bool `json:"m"` //Is this trade the maker side? 257 | Ignore1 bool `json:"M"` //Ignore 258 | OrderCreationTime int64 `json:"O"` //Order creation time 259 | CumulativeQuoteQty string `json:"Z"` //Cumulative quote asset transacted quantity 260 | LastQuoteQty string `json:"Y"` //Last quote asset transacted quantity (i.e. lastPrice * lastQty) 261 | QuoteOrderQty string `json:"Q"` //Quote Order Qty 262 | } 263 | 264 | // ViperData struct for Viper configuration files 265 | type ViperData struct { 266 | V1 *viper.Viper `json:"v1"` /* Session configurations file */ 267 | V2 *viper.Viper `json:"v2"` /* Global configurations file */ 268 | } 269 | -------------------------------------------------------------------------------- /exchange/exchange.go: -------------------------------------------------------------------------------- 1 | package exchange 2 | 3 | import ( 4 | "errors" 5 | "math" 6 | "strings" 7 | "time" 8 | 9 | "github.com/aleibovici/cryptopump/functions" 10 | "github.com/aleibovici/cryptopump/logger" 11 | "github.com/aleibovici/cryptopump/mysql" 12 | "github.com/aleibovici/cryptopump/threads" 13 | "github.com/aleibovici/cryptopump/types" 14 | ) 15 | 16 | // GetClient Define the exchange to be used 17 | func GetClient( 18 | configData *types.Config, 19 | sessionData *types.Session) (err error) { 20 | 21 | switch strings.ToLower(configData.ExchangeName) { 22 | case "binance": 23 | 24 | sessionData.Clients.Binance = binanceGetClient(configData) 25 | return nil 26 | 27 | } 28 | 29 | return errors.New("Invalid Exchange Name") 30 | 31 | } 32 | 33 | // GetOrder Retrieve Order Status 34 | func GetOrder( 35 | configData *types.Config, 36 | sessionData *types.Session, 37 | orderID int64) (order *types.Order, err error) { 38 | 39 | switch strings.ToLower(configData.ExchangeName) { 40 | case "binance": 41 | 42 | return binanceGetOrder(sessionData, orderID) 43 | 44 | } 45 | 46 | return 47 | 48 | } 49 | 50 | // BuyOrder Create order to BUY 51 | func BuyOrder( 52 | configData *types.Config, 53 | sessionData *types.Session, 54 | quantity string) (order *types.Order, err error) { 55 | 56 | switch strings.ToLower(configData.ExchangeName) { 57 | case "binance": 58 | 59 | return binanceBuyOrder(sessionData, quantity) 60 | 61 | } 62 | 63 | return 64 | 65 | } 66 | 67 | // SellOrder Create order to SELL 68 | func SellOrder( 69 | configData *types.Config, 70 | marketData *types.Market, 71 | sessionData *types.Session, 72 | quantity string) (order *types.Order, err error) { 73 | 74 | switch strings.ToLower(configData.ExchangeName) { 75 | case "binance": 76 | 77 | return binanceSellOrder(marketData, sessionData, quantity) 78 | 79 | } 80 | 81 | return 82 | 83 | } 84 | 85 | // CancelOrder CANCEL an order 86 | func CancelOrder( 87 | configData *types.Config, 88 | sessionData *types.Session, 89 | orderID int64) (order *types.Order, err error) { 90 | 91 | switch strings.ToLower(configData.ExchangeName) { 92 | case "binance": 93 | 94 | return binanceCancelOrder(sessionData, orderID) 95 | 96 | } 97 | 98 | return 99 | 100 | } 101 | 102 | // GetInfo Retrieve exchange information 103 | func GetInfo( 104 | configData *types.Config, 105 | sessionData *types.Session) (info *types.ExchangeInfo, err error) { 106 | 107 | switch strings.ToLower(configData.ExchangeName) { 108 | case "binance": 109 | 110 | return binanceGetInfo(sessionData) 111 | 112 | } 113 | 114 | return 115 | 116 | } 117 | 118 | // GetLotSize Retrieve Lot Size specs 119 | func GetLotSize( 120 | configData *types.Config, 121 | sessionData *types.Session) { 122 | 123 | if info, err := GetInfo(configData, sessionData); err == nil { 124 | 125 | sessionData.MaxQuantity = functions.StrToFloat64(info.MaxQuantity) 126 | sessionData.MinQuantity = functions.StrToFloat64(info.MinQuantity) 127 | sessionData.StepSize = functions.StrToFloat64(info.StepSize) 128 | 129 | return 130 | 131 | } 132 | 133 | } 134 | 135 | // GetSymbolFiatFunds Retrieve symbol fiat funds available 136 | func GetSymbolFiatFunds( 137 | configData *types.Config, 138 | sessionData *types.Session) (balance float64, err error) { 139 | 140 | switch strings.ToLower(configData.ExchangeName) { 141 | case "binance": 142 | 143 | return binanceGetSymbolFiatFunds(sessionData) 144 | 145 | } 146 | 147 | return 148 | 149 | } 150 | 151 | // GetSymbolFunds Retrieve symbol funds available 152 | func GetSymbolFunds( 153 | configData *types.Config, 154 | sessionData *types.Session) (balance float64, err error) { 155 | 156 | switch strings.ToLower(configData.ExchangeName) { 157 | case "binance": 158 | 159 | return binanceGetSymbolFunds(sessionData) 160 | 161 | } 162 | 163 | return 164 | 165 | } 166 | 167 | // GetKlines Retrieve KLines via REST API 168 | func GetKlines( 169 | configData *types.Config, 170 | sessionData *types.Session) (klines []*types.Kline, err error) { 171 | 172 | switch strings.ToLower(configData.ExchangeName) { 173 | case "binance": 174 | 175 | tmp, err := binanceGetKlines(sessionData) 176 | 177 | if err == nil { 178 | return binanceMapKline(tmp), err 179 | } 180 | 181 | return nil, err 182 | 183 | } 184 | 185 | return 186 | 187 | } 188 | 189 | // GetPriceChangeStats Retrieve 24hs Rolling Price Statistics 190 | func GetPriceChangeStats( 191 | configData *types.Config, 192 | sessionData *types.Session, 193 | marketData *types.Market) (priceChangeStats []*types.PriceChangeStats, err error) { 194 | 195 | switch strings.ToLower(configData.ExchangeName) { 196 | case "binance": 197 | 198 | return binanceGetPriceChangeStats(sessionData) 199 | 200 | } 201 | 202 | return 203 | 204 | } 205 | 206 | /* Calculate the correct quantity to SELL according to the exchange lotSizeStep */ 207 | func getSellQuantity( 208 | order types.Order, 209 | sessionData *types.Session) (quantity float64) { 210 | 211 | return math.Round(order.ExecutedQuantity/sessionData.StepSize) * sessionData.StepSize 212 | 213 | } 214 | 215 | /* Calculate the correct quantity to BUY according to the exchange lotSizeStep */ 216 | func getBuyQuantity( 217 | marketData *types.Market, 218 | sessionData *types.Session, 219 | fiatQuantity float64) (quantity float64) { 220 | 221 | return math.Round((fiatQuantity/marketData.Price)/sessionData.StepSize) * sessionData.StepSize 222 | 223 | } 224 | 225 | // GetUserStreamServiceListenKey Retrieve listen key for user stream service 226 | func GetUserStreamServiceListenKey( 227 | configData *types.Config, 228 | sessionData *types.Session) (listenKey string, err error) { 229 | 230 | switch strings.ToLower(configData.ExchangeName) { 231 | case "binance": 232 | 233 | return binanceGetUserStreamServiceListenKey(sessionData) 234 | 235 | } 236 | 237 | return 238 | 239 | } 240 | 241 | // KeepAliveUserStreamServiceListenKey Keep user stream service alive 242 | func KeepAliveUserStreamServiceListenKey( 243 | configData *types.Config, 244 | sessionData *types.Session) (err error) { 245 | 246 | switch strings.ToLower(configData.ExchangeName) { 247 | case "binance": 248 | 249 | return binanceKeepAliveUserStreamServiceListenKey(sessionData) 250 | 251 | } 252 | 253 | return 254 | 255 | } 256 | 257 | // NewSetServerTimeService Synchronize time 258 | func NewSetServerTimeService( 259 | configData *types.Config, 260 | sessionData *types.Session) (err error) { 261 | 262 | switch strings.ToLower(configData.ExchangeName) { 263 | case "binance": 264 | 265 | return binanceNewSetServerTimeService(sessionData) 266 | 267 | } 268 | 269 | return 270 | 271 | } 272 | 273 | // WsBookTickerServe serve websocket that pushes updates to the best bid or ask price or quantity in real-time for a specified symbol. 274 | func WsBookTickerServe( 275 | configData *types.Config, 276 | sessionData *types.Session, 277 | wsHandler *types.WsHandler, 278 | errHandler func(err error)) (doneC chan struct{}, stopC chan struct{}, err error) { 279 | 280 | switch strings.ToLower(configData.ExchangeName) { 281 | case "binance": 282 | 283 | return binanceWsBookTickerServe(sessionData, wsHandler, errHandler) 284 | 285 | } 286 | 287 | return 288 | 289 | } 290 | 291 | // WsKlineServe serve websocket kline handler 292 | func WsKlineServe( 293 | configData *types.Config, 294 | sessionData *types.Session, 295 | wsHandler *types.WsHandler, 296 | errHandler func(err error)) (doneC chan struct{}, stopC chan struct{}, err error) { 297 | 298 | switch strings.ToLower(configData.ExchangeName) { 299 | case "binance": 300 | 301 | return binanceWsKlineServe(sessionData, wsHandler, errHandler) 302 | 303 | } 304 | 305 | return 306 | 307 | } 308 | 309 | // WsUserDataServe serve user data handler with listen key 310 | func WsUserDataServe( 311 | configData *types.Config, 312 | sessionData *types.Session, 313 | wsHandler *types.WsHandler, 314 | errHandler func(err error)) (doneC chan struct{}, stopC chan struct{}, err error) { 315 | 316 | switch strings.ToLower(configData.ExchangeName) { 317 | case "binance": 318 | 319 | return binanceWsUserDataServe(sessionData, wsHandler, errHandler) 320 | 321 | } 322 | 323 | return 324 | 325 | } 326 | 327 | // BuyTicker Buy Ticker 328 | func BuyTicker( 329 | quantity float64, 330 | configData *types.Config, 331 | marketData *types.Market, 332 | sessionData *types.Session) { 333 | 334 | var orderStatus *types.Order 335 | var orderPrice float64 336 | var orderExecutedQuantity float64 337 | var isCanceled bool 338 | 339 | /* Enter and defer exiting busy mode */ 340 | sessionData.Busy = true 341 | defer func() { 342 | sessionData.Busy = false 343 | }() 344 | 345 | /* Exit if DryRun mode set to true */ 346 | if configData.DryRun { 347 | 348 | logger.LogEntry{ /* Log Entry */ 349 | Config: configData, 350 | Market: marketData, 351 | Session: sessionData, 352 | Order: &types.Order{ 353 | Price: marketData.Price, 354 | }, 355 | Message: "BUYDRYRUN", 356 | LogLevel: "InfoLevel", 357 | }.Do() 358 | 359 | return 360 | 361 | } 362 | 363 | orderResponse, err := BuyOrder( 364 | configData, 365 | sessionData, 366 | functions.Float64ToStr(getBuyQuantity(marketData, sessionData, quantity), 4)) /* Get the correct quantity according to lotSizeMin and lotSizeStep */ 367 | 368 | /* Test orderResponse for errors */ 369 | if (orderResponse == nil && err != nil) || 370 | (orderResponse == nil && err == nil) { 371 | 372 | switch { 373 | case strings.Contains(err.Error(), "1013"): 374 | /* code=-1013, msg=Filter failure: LOT_SIZE */ 375 | 376 | /* Retrieve exchange lot size for ticker and store in sessionData */ 377 | GetLotSize(configData, sessionData) 378 | 379 | return 380 | 381 | } 382 | 383 | return 384 | 385 | } 386 | 387 | /* Check if result is nil and set as zero */ 388 | if orderPrice = orderResponse.CumulativeQuoteQuantity / orderResponse.ExecutedQuantity; math.IsNaN(orderPrice) { 389 | orderPrice = 0 390 | } 391 | 392 | orderExecutedQuantity = orderResponse.ExecutedQuantity 393 | 394 | /* Save order to database */ 395 | if err := mysql.SaveOrder( 396 | sessionData, 397 | orderResponse, 398 | 0, /* OrderIDSource */ 399 | orderPrice /* OrderPrice */); err != nil { 400 | 401 | /* Cleanly exit ThreadID */ 402 | threads.Thread{}.Terminate(sessionData, functions.GetFunctionName()+" - "+err.Error()) 403 | 404 | } 405 | 406 | /* This session variable stores the time of the last buy */ 407 | sessionData.LastBuyTransactTime = time.Now() 408 | 409 | S: 410 | switch orderResponse.Status { 411 | case "FILLED", "PARTIALLY_FILLED": 412 | case "CANCELED": 413 | 414 | isCanceled = true 415 | 416 | case "NEW": 417 | 418 | for orderStatus, err = GetOrder( 419 | configData, 420 | sessionData, 421 | int64(orderResponse.OrderID)); orderStatus == nil || orderStatus.Status == "NEW"; { 422 | 423 | if err != nil { 424 | 425 | break S 426 | 427 | } 428 | 429 | time.Sleep(3000 * time.Millisecond) 430 | 431 | } 432 | 433 | switch orderStatus.Status { 434 | case "FILLED", "PARTIALLY_FILLED": 435 | 436 | orderPrice = orderStatus.CumulativeQuoteQuantity / orderStatus.ExecutedQuantity 437 | 438 | orderExecutedQuantity = orderStatus.ExecutedQuantity 439 | 440 | /* Update order status and price & Save Thread Transaction */ 441 | if err := mysql.UpdateOrder( 442 | sessionData, 443 | int64(orderResponse.OrderID), 444 | orderResponse.CumulativeQuoteQuantity, 445 | orderResponse.ExecutedQuantity, 446 | orderPrice, 447 | string(orderStatus.Status)); err != nil { 448 | 449 | /* Cleanly exit ThreadID */ 450 | threads.Thread{}.Terminate(sessionData, functions.GetFunctionName()+" - "+err.Error()) 451 | 452 | } 453 | 454 | case "CANCELED": 455 | 456 | isCanceled = true 457 | 458 | break S 459 | 460 | } 461 | 462 | } 463 | 464 | if !isCanceled { 465 | 466 | /* Save Thread Transaction */ 467 | if err := mysql.SaveThreadTransaction( 468 | sessionData, 469 | int64(orderResponse.OrderID), 470 | orderResponse.CumulativeQuoteQuantity, 471 | orderPrice, 472 | orderExecutedQuantity); err != nil { 473 | 474 | /* Cleanly exit ThreadID */ 475 | threads.Thread{}.Terminate(sessionData, functions.GetFunctionName()+" - "+err.Error()) 476 | 477 | } 478 | 479 | logger.LogEntry{ /* Log Entry */ 480 | Config: configData, 481 | Market: marketData, 482 | Session: sessionData, 483 | Order: &types.Order{ 484 | OrderID: int(orderResponse.OrderID), 485 | Price: orderPrice, 486 | }, 487 | Message: "BUY", 488 | LogLevel: "InfoLevel", 489 | }.Do() 490 | 491 | } else if isCanceled { 492 | 493 | logger.LogEntry{ /* Log Entry */ 494 | Config: configData, 495 | Market: marketData, 496 | Session: sessionData, 497 | Order: &types.Order{ 498 | OrderID: int(orderResponse.OrderID), 499 | Price: orderPrice, 500 | }, 501 | Message: "CANCELED", 502 | LogLevel: "InfoLevel", 503 | }.Do() 504 | 505 | } 506 | 507 | } 508 | 509 | // SellTicker Sell Ticker 510 | func SellTicker( 511 | order types.Order, 512 | configData *types.Config, 513 | marketData *types.Market, 514 | sessionData *types.Session) { 515 | 516 | var orderResponse *types.Order 517 | var orderStatus *types.Order 518 | 519 | var cancelOrderResponse *types.Order 520 | var isCanceled bool 521 | var err error 522 | var i int 523 | 524 | /* Enter and defer exiting busy mode */ 525 | sessionData.Busy = true 526 | defer func() { 527 | sessionData.Busy = false 528 | }() 529 | 530 | /* Exit if DryRun mode set to true */ 531 | if configData.DryRun { 532 | 533 | logger.LogEntry{ /* Log Entry */ 534 | Config: configData, 535 | Market: marketData, 536 | Session: sessionData, 537 | Order: &types.Order{ 538 | Price: marketData.Price, 539 | }, 540 | Message: "SELLDRYRUN", 541 | LogLevel: "InfoLevel", 542 | }.Do() 543 | 544 | return 545 | 546 | } 547 | 548 | orderResponse, err = SellOrder( 549 | configData, 550 | marketData, 551 | sessionData, 552 | functions.Float64ToStr(getSellQuantity(order, sessionData), 6) /* Get correct quantity to sell according to the lotSizeStep */) 553 | 554 | /* Test orderResponse for errors */ 555 | if (orderResponse == nil && err != nil) || 556 | (orderResponse == nil && err == nil) { 557 | 558 | logger.LogEntry{ /* Log Entry */ 559 | Config: configData, 560 | Market: marketData, 561 | Session: sessionData, 562 | Order: &types.Order{}, 563 | Message: functions.GetFunctionName() + " - " + err.Error(), 564 | LogLevel: "DebugLevel", 565 | }.Do() 566 | 567 | return 568 | 569 | } 570 | 571 | /* Save order to database */ 572 | if err := mysql.SaveOrder( 573 | sessionData, 574 | orderResponse, 575 | int64(order.OrderID), /* OrderIDSource */ 576 | marketData.Price /* OrderPrice */); err != nil { 577 | 578 | /* Cleanly exit ThreadID */ 579 | threads.Thread{}.Terminate(sessionData, functions.GetFunctionName()+" - "+err.Error()) 580 | 581 | } 582 | 583 | S: 584 | switch orderResponse.Status { 585 | case "FILLED": 586 | 587 | case "CANCELED": 588 | 589 | isCanceled = true 590 | 591 | case "PARTIALLY_FILLED", "NEW": 592 | 593 | time.Sleep(2000 * time.Millisecond) 594 | 595 | F: 596 | for orderStatus, err = GetOrder( 597 | configData, 598 | sessionData, 599 | int64(orderResponse.OrderID)); orderStatus == nil || 600 | orderStatus.Status == "NEW" || 601 | orderStatus.Status == "PARTIALLY_FILLED"; { 602 | 603 | if err != nil { 604 | 605 | /* Cleanly exit ThreadID */ 606 | threads.Thread{}.Terminate(sessionData, functions.GetFunctionName()+" - "+err.Error()) 607 | 608 | } 609 | 610 | switch orderStatus.Status { 611 | case "FILLED": 612 | 613 | break F 614 | 615 | case "CANCELED": 616 | 617 | isCanceled = true 618 | 619 | break F 620 | 621 | } 622 | 623 | i++ /* increment iterations before order cancel */ 624 | 625 | /* Initiate order cancel after 10 iterations */ 626 | if i == 9 { 627 | 628 | if cancelOrderResponse, err = CancelOrder( 629 | configData, 630 | sessionData, 631 | int64(orderResponse.OrderID)); err != nil { 632 | 633 | switch { 634 | case strings.Contains(err.Error(), "-2010"), strings.Contains(err.Error(), "-2011"), strings.Contains(err.Error(), "-1021"): 635 | /* -2011 Order filled in full before cancelling */ 636 | /* -2010 Account has insufficient balance for requested action */ 637 | /* -1021 Timestamp for this request was 1000ms ahead of the server's time */ 638 | 639 | if orderStatus, err = GetOrder( 640 | configData, 641 | sessionData, 642 | int64(orderResponse.OrderID)); err != nil { 643 | 644 | /* Cleanly exit ThreadID */ 645 | threads.Thread{}.Terminate(sessionData, functions.GetFunctionName()+" - "+err.Error()) 646 | 647 | } 648 | 649 | break F 650 | 651 | case strings.Contains(err.Error(), "connection reset by peer"): 652 | /* read tcp 192.168.110.110:54914->65.9.137.130:443: read: connection reset by peer */ 653 | 654 | if orderStatus, err = GetOrder( 655 | configData, 656 | sessionData, 657 | int64(orderResponse.OrderID)); err != nil { 658 | 659 | /* Cleanly exit ThreadID */ 660 | threads.Thread{}.Terminate(sessionData, functions.GetFunctionName()+" - "+err.Error()) 661 | 662 | } 663 | 664 | break S 665 | 666 | default: 667 | 668 | logger.LogEntry{ /* Log Entry */ 669 | Config: configData, 670 | Market: marketData, 671 | Session: sessionData, 672 | Order: &types.Order{ 673 | OrderID: int(orderResponse.OrderID), 674 | }, 675 | Message: functions.GetFunctionName() + " - " + err.Error(), 676 | LogLevel: "DebugLevel", 677 | }.Do() 678 | 679 | break S 680 | } 681 | 682 | } 683 | 684 | switch cancelOrderResponse.Status { 685 | case "CANCELED": 686 | 687 | isCanceled = true 688 | 689 | /* This session variable stores the time of the cancelled sell */ 690 | sessionData.LastSellCanceledTime = time.Now() 691 | 692 | if orderStatus, err = GetOrder( 693 | configData, 694 | sessionData, 695 | int64(orderResponse.OrderID)); err != nil { 696 | 697 | /* Cleanly exit ThreadID */ 698 | threads.Thread{}.Terminate(sessionData, functions.GetFunctionName()+" - "+err.Error()) 699 | 700 | } 701 | 702 | break F 703 | 704 | default: 705 | 706 | logger.LogEntry{ /* Log Entry */ 707 | Config: configData, 708 | Market: marketData, 709 | Session: sessionData, 710 | Order: &types.Order{ 711 | OrderID: int(orderResponse.OrderID), 712 | Price: marketData.Price, 713 | }, 714 | Message: "FAILED TO CANCEL ORDER", 715 | LogLevel: "InfoLevel", 716 | }.Do() 717 | 718 | break F 719 | 720 | } 721 | 722 | } 723 | 724 | /* Wait time between iterations (i++). There are ten iterations and the total waiting time define the amount od time before an order is canceled. configData.SellWaitBeforeCancel is divided by then converted into seconds. */ 725 | time.Sleep( 726 | time.Duration( 727 | configData.SellWaitBeforeCancel/10) * time.Second) 728 | 729 | } 730 | 731 | /* Update order status and price */ 732 | if err := mysql.UpdateOrder( 733 | sessionData, 734 | int64(orderResponse.OrderID), 735 | orderStatus.CumulativeQuoteQuantity, 736 | orderStatus.ExecutedQuantity, 737 | marketData.Price, 738 | string(orderStatus.Status)); err != nil { 739 | 740 | /* Cleanly exit ThreadID */ 741 | threads.Thread{}.Terminate(sessionData, functions.GetFunctionName()+" - "+err.Error()) 742 | 743 | } 744 | 745 | } 746 | 747 | if !isCanceled { 748 | 749 | /* Remove Thread transaction from database */ 750 | if err := mysql.DeleteThreadTransactionByOrderID( 751 | sessionData, 752 | order.OrderID); err != nil { 753 | 754 | /* Cleanly exit ThreadID */ 755 | threads.Thread{}.Terminate(sessionData, functions.GetFunctionName()+" - "+err.Error()) 756 | 757 | } 758 | 759 | logger.LogEntry{ /* Log Entry */ 760 | Config: configData, 761 | Market: marketData, 762 | Session: sessionData, 763 | Order: &types.Order{ 764 | OrderID: int(orderResponse.OrderID), 765 | Price: marketData.Price, 766 | OrderIDSource: order.OrderID, 767 | }, 768 | Message: "SELL", 769 | LogLevel: "InfoLevel", 770 | }.Do() 771 | 772 | } else if isCanceled { 773 | 774 | logger.LogEntry{ /* Log Entry */ 775 | Config: configData, 776 | Market: marketData, 777 | Session: sessionData, 778 | Order: &types.Order{ 779 | OrderID: int(orderResponse.OrderID), 780 | Price: marketData.Price, 781 | OrderIDSource: order.OrderID, 782 | }, 783 | Message: "CANCELED", 784 | LogLevel: "InfoLevel", 785 | }.Do() 786 | 787 | } 788 | 789 | } 790 | -------------------------------------------------------------------------------- /functions/functions.go: -------------------------------------------------------------------------------- 1 | package functions 2 | 3 | import ( 4 | "html/template" 5 | "io" 6 | "io/ioutil" 7 | "log" 8 | "net" 9 | "net/http" 10 | "path/filepath" 11 | "runtime" 12 | "strings" 13 | "time" 14 | 15 | "os" 16 | "strconv" 17 | 18 | "github.com/aleibovici/cryptopump/logger" 19 | "github.com/aleibovici/cryptopump/types" 20 | "github.com/tcnksm/go-httpstat" 21 | 22 | "github.com/rs/xid" 23 | ) 24 | 25 | // StrToFloat64 function 26 | /* This public function convert string to float64 */ 27 | func StrToFloat64(value string) (r float64) { 28 | 29 | var err error 30 | 31 | if r, err = strconv.ParseFloat(value, 8); err != nil { 32 | 33 | logger.LogEntry{ /* Log Entry */ 34 | Config: nil, 35 | Market: nil, 36 | Session: nil, 37 | Order: &types.Order{}, 38 | Message: GetFunctionName() + " - " + err.Error(), 39 | LogLevel: "DebugLevel", 40 | }.Do() 41 | 42 | return 0 43 | 44 | } 45 | 46 | return r 47 | } 48 | 49 | // Float64ToStr function 50 | /* This public function convert float64 to string with variable precision */ 51 | func Float64ToStr(value float64, prec int) string { 52 | 53 | return strconv.FormatFloat(value, 'f', prec, 64) 54 | 55 | } 56 | 57 | // IntToFloat64 convert Int to Float64 58 | func IntToFloat64(value int) float64 { 59 | 60 | return float64(value) 61 | 62 | } 63 | 64 | // StrToInt convert string to int 65 | func StrToInt(value string) (r int) { 66 | 67 | var err error 68 | 69 | if r, err = strconv.Atoi(value); err != nil { 70 | 71 | logger.LogEntry{ /* Log Entry */ 72 | Config: nil, 73 | Market: nil, 74 | Session: nil, 75 | Order: &types.Order{}, 76 | Message: GetFunctionName(), 77 | LogLevel: "DebugLevel", 78 | }.Do() 79 | 80 | } 81 | 82 | return r 83 | 84 | } 85 | 86 | // MustGetenv is a helper function for getting environment variables. 87 | // Displays a warning if the environment variable is not set. 88 | func MustGetenv(k string) string { 89 | 90 | v := os.Getenv(k) 91 | if v == "" { 92 | 93 | logger.LogEntry{ /* Log Entry */ 94 | Config: nil, 95 | Market: nil, 96 | Session: nil, 97 | Order: &types.Order{}, 98 | Message: GetFunctionName() + " - " + "Environment variable not set", 99 | LogLevel: "DebugLevel", 100 | }.Do() 101 | 102 | } 103 | 104 | return strings.ToLower(v) 105 | 106 | } 107 | 108 | // GetIP gets a requests IP address by reading off the forwarded-for 109 | // header (for proxies) and falls back to use the remote address. 110 | func GetIP(r *http.Request) string { 111 | 112 | forwarded := r.Header.Get("X-FORWARDED-FOR") 113 | if forwarded != "" { 114 | return forwarded 115 | } 116 | 117 | return r.RemoteAddr 118 | } 119 | 120 | // GetThreadID Return random thread ID 121 | func GetThreadID() string { 122 | 123 | return xid.New().String() 124 | 125 | } 126 | 127 | /* Convert Strign to Time */ 128 | func stringToTime(str string) (r time.Time) { 129 | 130 | var err error 131 | 132 | if r, err = time.Parse(time.Kitchen, str); err != nil { 133 | 134 | logger.LogEntry{ /* Log Entry */ 135 | Config: nil, 136 | Market: nil, 137 | Session: nil, 138 | Order: &types.Order{}, 139 | Message: GetFunctionName() + " - " + err.Error(), 140 | LogLevel: "DebugLevel", 141 | }.Do() 142 | 143 | } 144 | 145 | return r 146 | 147 | } 148 | 149 | // IsInTimeRange Check if time is in a specific range 150 | func IsInTimeRange(startTimeString string, endTimeString string) bool { 151 | 152 | t := time.Now() 153 | timeNowString := t.Format(time.Kitchen) 154 | timeNow := stringToTime(timeNowString) 155 | start := stringToTime(startTimeString) 156 | end := stringToTime(endTimeString) 157 | 158 | if timeNow.Before(start) { 159 | 160 | return false 161 | 162 | } 163 | 164 | if timeNow.After(end) { 165 | 166 | return false 167 | 168 | } 169 | 170 | return true 171 | 172 | } 173 | 174 | // IsFundsAvailable Validate available funds to buy 175 | func IsFundsAvailable( 176 | configData *types.Config, 177 | sessionData *types.Session) bool { 178 | 179 | return (sessionData.SymbolFiatFunds - configData.SymbolFiatStash) >= configData.BuyQuantityFiatDown 180 | 181 | } 182 | 183 | /* Select the correct html template based on sessionData */ 184 | func selectTemplate( 185 | sessionData *types.Session) (template string) { 186 | 187 | if sessionData.Admin { 188 | 189 | template = "admin.html" /* Admin template */ 190 | 191 | } else if sessionData.ThreadID == "" { 192 | 193 | template = "index.html" 194 | 195 | } else { 196 | 197 | template = "index_nostart.html" 198 | 199 | } 200 | 201 | return template 202 | 203 | } 204 | 205 | // ExecuteTemplate is responsible for executing any templates 206 | func ExecuteTemplate( 207 | wr io.Writer, 208 | data interface{}, 209 | sessionData *types.Session) { 210 | 211 | var tlp *template.Template 212 | var err error 213 | 214 | if tlp, err = template.ParseGlob("./templates/*"); err != nil { 215 | 216 | defer os.Exit(1) 217 | 218 | } 219 | 220 | if err = tlp.ExecuteTemplate(wr, selectTemplate(sessionData), data); err != nil { 221 | 222 | defer os.Exit(1) 223 | 224 | } 225 | 226 | /* Conditional defer logging when there is an error retriving data */ 227 | defer func() { 228 | if err != nil { 229 | logger.LogEntry{ /* Log Entry */ 230 | Config: nil, 231 | Market: nil, 232 | Session: nil, 233 | Order: &types.Order{}, 234 | Message: GetFunctionName() + " - " + err.Error(), 235 | LogLevel: "DebugLevel", 236 | }.Do() 237 | } 238 | }() 239 | 240 | } 241 | 242 | // GetFunctionName Retrieve current function name 243 | func GetFunctionName() string { 244 | 245 | pc := make([]uintptr, 15) 246 | n := runtime.Callers(2, pc) 247 | frames := runtime.CallersFrames(pc[:n]) 248 | frame, _ := frames.Next() 249 | 250 | return frame.Function 251 | 252 | } 253 | 254 | // GetPort Determine port for HTTP service. 255 | func GetPort() (port string) { 256 | 257 | port = os.Getenv("PORT") 258 | 259 | if port == "" { 260 | 261 | port = "8080" 262 | 263 | } 264 | 265 | for { 266 | 267 | if l, err := net.Listen("tcp", ":"+port); err != nil { 268 | 269 | port = Float64ToStr((StrToFloat64(port) + 1), 0) 270 | 271 | } else { 272 | 273 | l.Close() 274 | break 275 | 276 | } 277 | 278 | } 279 | 280 | return port 281 | 282 | } 283 | 284 | // DeleteConfigFile Delete configuration file for ThreadID 285 | func DeleteConfigFile(sessionData *types.Session) { 286 | 287 | filename := sessionData.ThreadID + ".yml" 288 | path := "./config/" 289 | 290 | if err := os.Remove(path + filename); err != nil { 291 | 292 | logger.LogEntry{ /* Log Entry */ 293 | Config: nil, 294 | Market: nil, 295 | Session: nil, 296 | Order: &types.Order{}, 297 | Message: GetFunctionName() + " - " + err.Error(), 298 | LogLevel: "DebugLevel", 299 | }.Do() 300 | 301 | return 302 | 303 | } 304 | 305 | } 306 | 307 | // SaveConfigGlobalData save viper configuration from html 308 | func SaveConfigGlobalData( 309 | viperData *types.ViperData, 310 | r *http.Request, 311 | sessionData *types.Session) { 312 | 313 | viperData.V2.Set("config_global.apiKey", r.FormValue("Apikey")) /* Api Key */ 314 | viperData.V2.Set("config_global.secretKey", r.FormValue("Secretkey")) /* Secret Key */ 315 | viperData.V2.Set("config_global.apiKeyTestNet", r.FormValue("ApikeyTestNet")) /* Api Key TestNet */ 316 | viperData.V2.Set("config_global.secretKeyTestNet", r.FormValue("SecretkeyTestNet")) /* Secret Key TestNet */ 317 | viperData.V2.Set("config_global.tgbotapikey", r.FormValue("TgBotApikey")) /* Tg Bot Api Key */ 318 | 319 | if err := viperData.V2.WriteConfig(); err != nil { /* Write configuration file */ 320 | 321 | logger.LogEntry{ /* Log Entry */ 322 | Config: nil, 323 | Market: nil, 324 | Session: nil, 325 | Order: &types.Order{}, 326 | Message: GetFunctionName() + " - " + err.Error(), 327 | LogLevel: "DebugLevel", 328 | }.Do() 329 | 330 | } 331 | 332 | logger.LogEntry{ /* Log Entry */ 333 | Config: nil, 334 | Market: nil, 335 | Session: sessionData, 336 | Order: &types.Order{}, 337 | Message: "Global configuration saved", 338 | LogLevel: "InfoLevel", 339 | }.Do() 340 | 341 | } 342 | 343 | // GetConfigData Retrieve or create config file based on ThreadID 344 | func GetConfigData( 345 | viperData *types.ViperData, 346 | sessionData *types.Session) *types.Config { 347 | 348 | configData := loadConfigData(viperData, sessionData) 349 | 350 | if sessionData.ThreadID != "" { 351 | 352 | filename := sessionData.ThreadID + ".yml" 353 | writePath := "./config/" 354 | 355 | if _, err := os.Stat(writePath + filename); err == nil { 356 | 357 | /* Test for existing ThreadID config file and load configuration */ 358 | viperData.V1.SetConfigFile(writePath + filename) 359 | 360 | if err := viperData.V1.ReadInConfig(); err != nil { 361 | 362 | logger.LogEntry{ /* Log Entry */ 363 | Config: nil, 364 | Market: nil, 365 | Session: nil, 366 | Order: &types.Order{}, 367 | Message: GetFunctionName() + " - " + err.Error(), 368 | LogLevel: "DebugLevel", 369 | }.Do() 370 | 371 | } 372 | 373 | configData = loadConfigData(viperData, sessionData) 374 | 375 | } else if os.IsNotExist(err) { 376 | 377 | /* Create new ThreadID config file and load configuration */ 378 | if err := viperData.V1.WriteConfigAs(writePath + filename); err != nil { 379 | 380 | logger.LogEntry{ /* Log Entry */ 381 | Config: nil, 382 | Market: nil, 383 | Session: nil, 384 | Order: &types.Order{}, 385 | Message: GetFunctionName() + " - " + err.Error(), 386 | LogLevel: "DebugLevel", 387 | }.Do() 388 | 389 | } 390 | 391 | viperData.V1.SetConfigFile(writePath + filename) 392 | 393 | if err := viperData.V1.ReadInConfig(); err != nil { 394 | 395 | logger.LogEntry{ /* Log Entry */ 396 | Config: nil, 397 | Market: nil, 398 | Session: nil, 399 | Order: &types.Order{}, 400 | Message: GetFunctionName() + " - " + err.Error(), 401 | LogLevel: "DebugLevel", 402 | }.Do() 403 | 404 | } 405 | 406 | configData = loadConfigData(viperData, sessionData) 407 | 408 | } 409 | 410 | } 411 | 412 | return configData 413 | 414 | } 415 | 416 | /* This function retrieve the list of configuration files under the root config folder. 417 | .yaml files are considered configuration files. */ 418 | func getConfigTemplateList(sessionData *types.Session) []string { 419 | 420 | var files []string 421 | files = append(files, "-") 422 | 423 | root := "./config" 424 | err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error { 425 | 426 | if filepath.Ext(path) == ".yml" { 427 | files = append(files, info.Name()) 428 | } 429 | 430 | return nil 431 | }) 432 | 433 | if err != nil { 434 | 435 | logger.LogEntry{ /* Log Entry */ 436 | Config: nil, 437 | Market: nil, 438 | Session: nil, 439 | Order: &types.Order{}, 440 | Message: GetFunctionName() + " - " + err.Error(), 441 | LogLevel: "DebugLevel", 442 | }.Do() 443 | 444 | os.Exit(1) 445 | 446 | } 447 | 448 | return files 449 | 450 | } 451 | 452 | // LoadConfigTemplate Load the selected configuration template 453 | // Three's a BUG where it only works before the first UPDATE 454 | func LoadConfigTemplate( 455 | viperData *types.ViperData, 456 | sessionData *types.Session) *types.Config { 457 | 458 | var filename string 459 | 460 | /* Retrieve the list of configuration templates */ 461 | files := getConfigTemplateList(sessionData) 462 | 463 | /* Iterate configuration templates to match the selection */ 464 | for key, file := range files { 465 | if key == sessionData.ConfigTemplate { 466 | filename = file 467 | } 468 | } 469 | 470 | filenameOld := viperData.V1.ConfigFileUsed() 471 | 472 | /* Set selected template as current config and load settings and return configData*/ 473 | viperData.V1.SetConfigFile("./config/" + filename) 474 | if err := viperData.V1.ReadInConfig(); err != nil { 475 | 476 | logger.LogEntry{ /* Log Entry */ 477 | Config: nil, 478 | Market: nil, 479 | Session: nil, 480 | Order: &types.Order{}, 481 | Message: GetFunctionName() + " - " + err.Error(), 482 | LogLevel: "DebugLevel", 483 | }.Do() 484 | } 485 | 486 | configData := loadConfigData(viperData, sessionData) 487 | 488 | /* Set origina template as current config */ 489 | viperData.V1.SetConfigFile(filenameOld) 490 | if err := viperData.V1.ReadInConfig(); err != nil { 491 | 492 | logger.LogEntry{ /* Log Entry */ 493 | Config: nil, 494 | Market: nil, 495 | Session: nil, 496 | Order: &types.Order{}, 497 | Message: GetFunctionName() + " - " + err.Error(), 498 | LogLevel: "DebugLevel", 499 | }.Do() 500 | 501 | } 502 | 503 | return configData 504 | 505 | } 506 | 507 | /* This routine load viper configuration data into map[string]interface{} */ 508 | func loadConfigData( 509 | viperData *types.ViperData, 510 | sessionData *types.Session) *types.Config { 511 | 512 | configData := &types.Config{ 513 | ThreadID: sessionData.ThreadID, 514 | Buy24hsHighpriceEntry: viperData.V1.GetFloat64("config.buy_24hs_highprice_entry"), 515 | BuyDirectionDown: viperData.V1.GetInt("config.buy_direction_down"), 516 | BuyDirectionUp: viperData.V1.GetInt("config.buy_direction_up"), 517 | BuyQuantityFiatUp: viperData.V1.GetFloat64("config.buy_quantity_fiat_up"), 518 | BuyQuantityFiatDown: viperData.V1.GetFloat64("config.buy_quantity_fiat_down"), 519 | BuyQuantityFiatInit: viperData.V1.GetFloat64("config.buy_quantity_fiat_init"), 520 | BuyRepeatThresholdDown: viperData.V1.GetFloat64("config.buy_repeat_threshold_down"), 521 | BuyRepeatThresholdDownSecond: viperData.V1.GetFloat64("config.buy_repeat_threshold_down_second"), 522 | BuyRepeatThresholdDownSecondStartCount: viperData.V1.GetInt("config.buy_repeat_threshold_down_second_start_count"), 523 | BuyRepeatThresholdUp: viperData.V1.GetFloat64("config.buy_repeat_threshold_up"), 524 | BuyRsi7Entry: viperData.V1.GetFloat64("config.buy_rsi7_entry"), 525 | BuyWait: viperData.V1.GetInt("config.buy_wait"), 526 | ExchangeComission: viperData.V1.GetFloat64("config.exchange_comission"), 527 | ProfitMin: viperData.V1.GetFloat64("config.profit_min"), 528 | SellWaitBeforeCancel: viperData.V1.GetInt("config.sellwaitbeforecancel"), 529 | SellWaitAfterCancel: viperData.V1.GetInt("config.sellwaitaftercancel"), 530 | SellToCover: viperData.V1.GetBool("config.selltocover"), 531 | SellHoldOnRSI3: viperData.V1.GetFloat64("config.sellholdonrsi3"), 532 | Stoploss: viperData.V1.GetFloat64("config.stoploss"), 533 | SymbolFiat: viperData.V1.GetString("config.symbol_fiat"), 534 | SymbolFiatStash: viperData.V1.GetFloat64("config.symbol_fiat_stash"), 535 | Symbol: viperData.V1.GetString("config.symbol"), 536 | TimeEnforce: viperData.V1.GetBool("config.time_enforce"), 537 | TimeStart: viperData.V1.GetString("config.time_start"), 538 | TimeStop: viperData.V1.GetString("config.time_stop"), 539 | Debug: viperData.V1.GetBool("config.debug"), 540 | Exit: viperData.V1.GetBool("config.exit"), 541 | DryRun: viperData.V1.GetBool("config.dryrun"), 542 | NewSession: viperData.V1.GetBool("config.newsession"), 543 | ConfigTemplateList: getConfigTemplateList(sessionData), 544 | ExchangeName: viperData.V1.GetString("config.exchangename"), 545 | TestNet: viperData.V1.GetBool("config.testnet"), 546 | HTMLSnippet: nil, 547 | ConfigGlobal: &types.ConfigGlobal{ 548 | Apikey: viperData.V2.GetString("config_global.apiKey"), 549 | Secretkey: viperData.V2.GetString("config_global.secretKey"), 550 | ApikeyTestNet: viperData.V2.GetString("config_global.apiKeyTestNet"), 551 | SecretkeyTestNet: viperData.V2.GetString("config_global.secretKeyTestNet"), 552 | TgBotApikey: viperData.V2.GetString("config_global.tgbotapikey")}, 553 | } 554 | 555 | return configData 556 | 557 | } 558 | 559 | // SaveConfigData save viper configuration from html 560 | func SaveConfigData( 561 | viperData *types.ViperData, 562 | r *http.Request, 563 | sessionData *types.Session) { 564 | 565 | viperData.V1.Set("config.buy_24hs_highprice_entry", r.PostFormValue("buy24hsHighpriceEntry")) 566 | viperData.V1.Set("config.buy_direction_down", r.PostFormValue("buyDirectionDown")) 567 | viperData.V1.Set("config.buy_direction_up", r.PostFormValue("buyDirectionUp")) 568 | viperData.V1.Set("config.buy_quantity_fiat_up", r.PostFormValue("buyQuantityFiatUp")) 569 | viperData.V1.Set("config.buy_quantity_fiat_down", r.PostFormValue("buyQuantityFiatDown")) 570 | viperData.V1.Set("config.buy_quantity_fiat_init", r.PostFormValue("buyQuantityFiatInit")) 571 | viperData.V1.Set("config.buy_rsi7_entry", r.PostFormValue("buyRsi7Entry")) 572 | viperData.V1.Set("config.buy_wait", r.PostFormValue("buyWait")) 573 | viperData.V1.Set("config.buy_repeat_threshold_down", r.PostFormValue("buyRepeatThresholdDown")) 574 | viperData.V1.Set("config.buy_repeat_threshold_down_second", r.PostFormValue("buyRepeatThresholdDownSecond")) 575 | viperData.V1.Set("config.buy_repeat_threshold_down_second_start_count", r.PostFormValue("buyRepeatThresholdDownSecondStartCount")) 576 | viperData.V1.Set("config.buy_repeat_threshold_up", r.PostFormValue("buyRepeatThresholdUp")) 577 | viperData.V1.Set("config.exchange_comission", r.PostFormValue("exchangeComission")) 578 | if r.PostFormValue("exchangename") != "" { /* Test for disabled input in index_nostart.html where return is nil */ 579 | viperData.V1.Set("config.exchangename", r.PostFormValue("exchangename")) 580 | } 581 | viperData.V1.Set("config.profit_min", r.PostFormValue("profitMin")) 582 | viperData.V1.Set("config.sellwaitbeforecancel", r.PostFormValue("sellwaitbeforecancel")) 583 | viperData.V1.Set("config.sellwaitaftercancel", r.PostFormValue("sellwaitaftercancel")) 584 | viperData.V1.Set("config.selltocover", r.PostFormValue("selltocover")) 585 | viperData.V1.Set("config.sellholdonrsi3", r.PostFormValue("sellholdonrsi3")) 586 | viperData.V1.Set("config.Stoploss", r.PostFormValue("stoploss")) 587 | if r.PostFormValue("exchangename") != "" { /* Test for disabled input in index_nostart.html where return is nil */ 588 | viperData.V1.Set("config.symbol", r.PostFormValue("symbol")) 589 | } 590 | if r.PostFormValue("exchangename") != "" { /* Test for disabled input in index_nostart.html where return is nil */ 591 | viperData.V1.Set("config.symbol_fiat", r.PostFormValue("symbol_fiat")) 592 | } 593 | viperData.V1.Set("config.symbol_fiat_stash", r.PostFormValue("symbolFiatStash")) 594 | viperData.V1.Set("config.time_enforce", r.PostFormValue("timeEnforce")) 595 | viperData.V1.Set("config.time_start", r.PostFormValue("timeStart")) 596 | viperData.V1.Set("config.time_stop", r.PostFormValue("timeStop")) 597 | if r.PostFormValue("exchangename") != "" { /* Test for disabled input in index_nostart.html where return is nil */ 598 | viperData.V1.Set("config.testnet", r.PostFormValue("testnet")) 599 | } 600 | viperData.V1.Set("config.debug", r.PostFormValue("debug")) 601 | viperData.V1.Set("config.exit", r.PostFormValue("exit")) 602 | viperData.V1.Set("config.dryrun", r.PostFormValue("dryrun")) 603 | if r.PostFormValue("exchangename") != "" { /* Test for disabled input in index_nostart.html where return is nil */ 604 | viperData.V1.Set("config.newsession", r.PostFormValue(("newsession"))) 605 | } 606 | 607 | if err := viperData.V1.WriteConfig(); err != nil { 608 | 609 | logger.LogEntry{ /* Log Entry */ 610 | Config: nil, 611 | Market: nil, 612 | Session: nil, 613 | Order: &types.Order{}, 614 | Message: GetFunctionName() + " - " + err.Error(), 615 | LogLevel: "DebugLevel", 616 | }.Do() 617 | 618 | os.Exit(1) 619 | } 620 | 621 | } 622 | 623 | // GetExchangeLatency retrieve the latency between the exchange and client 624 | func GetExchangeLatency(sessionData *types.Session) (latency int64, err error) { 625 | 626 | /* Package httpstat traces HTTP latency infomation 627 | (DNSLookup, TCP Connection and so on) on any golang HTTP request. */ 628 | 629 | var req *http.Request 630 | var res *http.Response 631 | 632 | if req, err = http.NewRequest("GET", sessionData.Clients.Binance.BaseURL, nil); err != nil { 633 | 634 | return 0, err 635 | 636 | } 637 | 638 | /* Create go-httpstat powered context and pass it to http.Request */ 639 | var result httpstat.Result 640 | ctx := httpstat.WithHTTPStat(req.Context(), &result) 641 | req = req.WithContext(ctx) 642 | 643 | client := http.DefaultClient 644 | if res, err = client.Do(req); err != nil { 645 | 646 | return 0, err 647 | 648 | } 649 | 650 | if _, err := io.Copy(ioutil.Discard, res.Body); err != nil { 651 | log.Fatal(err) 652 | } 653 | res.Body.Close() 654 | result.End(time.Now()) 655 | 656 | return result.ServerProcessing.Milliseconds(), err 657 | 658 | } 659 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "math/rand" 7 | "net/http" 8 | "os" 9 | "os/exec" 10 | "sync" 11 | "time" 12 | 13 | "github.com/aleibovici/cryptopump/algorithms" 14 | "github.com/aleibovici/cryptopump/exchange" 15 | "github.com/aleibovici/cryptopump/functions" 16 | "github.com/aleibovici/cryptopump/loader" 17 | "github.com/aleibovici/cryptopump/logger" 18 | "github.com/aleibovici/cryptopump/markets" 19 | "github.com/aleibovici/cryptopump/mysql" 20 | "github.com/aleibovici/cryptopump/nodes" 21 | "github.com/aleibovici/cryptopump/plotter" 22 | "github.com/aleibovici/cryptopump/telegram" 23 | "github.com/aleibovici/cryptopump/threads" 24 | "github.com/aleibovici/cryptopump/types" 25 | "github.com/jtaczanowski/go-scheduler" 26 | "github.com/paulbellamy/ratecounter" 27 | "github.com/sdcoffey/techan" 28 | "github.com/skratchdot/open-golang/open" 29 | "github.com/spf13/viper" 30 | 31 | tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api" 32 | ) 33 | 34 | type myHandler struct { 35 | sessionData *types.Session 36 | marketData *types.Market 37 | configData *types.Config 38 | viperData *types.ViperData 39 | } 40 | 41 | func main() { 42 | 43 | viperData := &types.ViperData{ /* Viper Configuration */ 44 | V1: viper.New(), /* Session configurations file */ 45 | V2: viper.New(), /* Global configurations file */ 46 | } 47 | 48 | viperData.V1.SetConfigType("yml") /* Set the type of the configurations file */ 49 | viperData.V1.AddConfigPath("./config") /* Set the path to look for the configurations file */ 50 | viperData.V1.SetConfigName("config") /* Set the file name of the configurations file */ 51 | if err := viperData.V1.ReadInConfig(); err != nil { 52 | 53 | logger.LogEntry{ /* Log Entry */ 54 | Config: nil, 55 | Market: nil, 56 | Session: nil, 57 | Order: &types.Order{}, 58 | Message: functions.GetFunctionName() + " - " + err.Error(), 59 | LogLevel: "DebugLevel", 60 | }.Do() 61 | 62 | } 63 | viperData.V1.WatchConfig() 64 | 65 | viperData.V2.SetConfigType("yml") /* Set the type of the configurations file */ 66 | viperData.V2.AddConfigPath("./config") /* Set the path to look for the configurations file */ 67 | viperData.V2.SetConfigName("config_global") /* Set the file name of the configurations file */ 68 | if err := viperData.V2.ReadInConfig(); err != nil { 69 | 70 | logger.LogEntry{ /* Log Entry */ 71 | Config: nil, 72 | Market: nil, 73 | Session: nil, 74 | Order: &types.Order{}, 75 | Message: functions.GetFunctionName() + " - " + err.Error(), 76 | LogLevel: "DebugLevel", 77 | }.Do() 78 | 79 | } 80 | viperData.V2.WatchConfig() 81 | 82 | sessionData := &types.Session{ 83 | ThreadID: "", 84 | ThreadIDSession: "", 85 | ThreadCount: 0, 86 | SellTransactionCount: 0, 87 | Symbol: "", 88 | SymbolFunds: 0, 89 | SymbolFiat: "", 90 | SymbolFiatFunds: 0, 91 | LastBuyTransactTime: time.Time{}, 92 | LastSellCanceledTime: time.Time{}, 93 | LastWsKlineTime: time.Time{}, 94 | LastWsBookTickerTime: time.Time{}, 95 | LastWsUserDataServeTime: time.Time{}, 96 | ConfigTemplate: 0, 97 | ForceBuy: false, 98 | ForceSell: false, 99 | ForceSellOrderID: 0, 100 | ListenKey: "", 101 | MasterNode: false, 102 | TgBotAPI: &tgbotapi.BotAPI{}, 103 | TgBotAPIChatID: 0, 104 | Db: &sql.DB{}, 105 | Clients: types.Client{}, 106 | KlineData: []types.KlineData{}, 107 | StopWs: false, 108 | Busy: false, 109 | MinQuantity: 0, 110 | MaxQuantity: 0, 111 | StepSize: 0, 112 | Latency: 0, 113 | Status: false, 114 | RateCounter: ratecounter.NewRateCounter(5 * time.Second), 115 | BuyDecisionTreeResult: "", 116 | SellDecisionTreeResult: "", 117 | QuantityOffsetFlag: false, 118 | DiffTotal: 0, 119 | Global: &types.Global{}, 120 | Admin: false, 121 | Port: "", 122 | } 123 | 124 | marketData := &types.Market{ 125 | Rsi3: 0, 126 | Rsi7: 0, 127 | Rsi14: 0, 128 | MACD: 0, 129 | Price: 0, 130 | PriceChangeStatsHighPrice: 0, 131 | PriceChangeStatsLowPrice: 0, 132 | Direction: 0, 133 | TimeStamp: time.Time{}, 134 | Series: &techan.TimeSeries{}, 135 | Ma7: 0, 136 | Ma14: 0, 137 | } 138 | 139 | configData := &types.Config{} 140 | 141 | sessionData.Db = mysql.DBInit() /* Initialize DB connection */ 142 | 143 | myHandler := &myHandler{ 144 | sessionData: sessionData, 145 | marketData: marketData, 146 | configData: configData, 147 | viperData: viperData, 148 | } 149 | 150 | sessionData.Port = functions.GetPort() /* Determine port for HTTP service. */ 151 | 152 | logger.LogEntry{ /* Log Entry */ 153 | Config: configData, 154 | Market: marketData, 155 | Session: sessionData, 156 | Order: &types.Order{}, 157 | Message: "Listening on port " + sessionData.Port, 158 | LogLevel: "InfoLevel", 159 | }.Do() 160 | 161 | http.HandleFunc("/", myHandler.handler) 162 | http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static")))) 163 | 164 | open.Run("http://localhost:" + sessionData.Port) /* Open URI using the OS's default browser */ 165 | 166 | http.ListenAndServe(fmt.Sprintf(":%s", sessionData.Port), nil) /* Start HTTP service. */ 167 | 168 | } 169 | 170 | func (fh *myHandler) handler(w http.ResponseWriter, r *http.Request) { 171 | 172 | w.Header().Set("Content-Type", "text/html") /* Set the Content-Type header */ 173 | w.Header().Set("X-Content-Type-Options", "nosniff") /* Add X-Content-Type-Options header */ 174 | w.Header().Add("Strict-Transport-Security", "max-age=63072000; includeSubDomains") /* Add Strict-Transport-Security header */ 175 | w.Header().Add("X-Frame-Options", "DENY") /* Add X-Frame-Options header */ 176 | 177 | fh.configData = functions.GetConfigData(fh.viperData, fh.sessionData) /* Get configuration data */ 178 | 179 | switch r.Method { 180 | case "GET": 181 | 182 | /* Determine the URI path to de taken */ 183 | switch r.URL.Path { 184 | case "/": 185 | 186 | fh.configData.HTMLSnippet = plotter.Data{}.Plot(fh.sessionData) /* Load dynamic components in configData */ 187 | functions.ExecuteTemplate(w, fh.configData, fh.sessionData) /* This is the template execution for 'index' */ 188 | 189 | case "/sessiondata": 190 | 191 | var tmp []byte 192 | var err error 193 | 194 | w.Header().Set("Content-Type", "application/json") /* Set the Content-Type header */ 195 | 196 | if tmp, err = loader.LoadSessionDataAdditionalComponents(fh.sessionData, fh.marketData, fh.configData); err != nil { /* Load dynamic components for javascript autoloader for html output */ 197 | 198 | logger.LogEntry{ /* Log Entry */ 199 | Config: fh.configData, 200 | Market: fh.marketData, 201 | Session: fh.sessionData, 202 | Order: &types.Order{}, 203 | Message: functions.GetFunctionName() + " - " + err.Error(), 204 | LogLevel: "DebugLevel", 205 | }.Do() 206 | 207 | return 208 | 209 | } 210 | 211 | if _, err := w.Write(tmp); err != nil { /* Write writes the data to the connection as part of an HTTP reply. */ 212 | 213 | logger.LogEntry{ /* Log Entry */ 214 | Config: fh.configData, 215 | Market: fh.marketData, 216 | Session: fh.sessionData, 217 | Order: &types.Order{}, 218 | Message: functions.GetFunctionName() + " - " + err.Error(), 219 | LogLevel: "DebugLevel", 220 | }.Do() 221 | 222 | return 223 | 224 | } 225 | 226 | } 227 | 228 | case "POST": 229 | 230 | switch r.URL.Path { /* Determine the URI path to de taken */ 231 | case "/": 232 | 233 | /* This function reads and parse the html form */ 234 | if err := r.ParseForm(); err != nil { 235 | 236 | logger.LogEntry{ /* Log Entry */ 237 | Config: fh.configData, 238 | Market: nil, 239 | Session: fh.sessionData, 240 | Order: &types.Order{}, 241 | Message: functions.GetFunctionName() + " - " + err.Error(), 242 | LogLevel: "DebugLevel", 243 | }.Do() 244 | 245 | return 246 | 247 | } 248 | 249 | /* This function uses a hidden field 'submitselect' in each HTML template to detect the actions triggered by users. 250 | HTML action must include 'document.getElementById('submitselect').value='about';this.form.submit()' */ 251 | switch r.PostFormValue("submitselect") { 252 | case "adminEnter": 253 | 254 | fh.sessionData.Admin = true /* Set admin flag */ 255 | functions.ExecuteTemplate(w, fh.configData, fh.sessionData) /* This is the template execution for 'admin' */ 256 | 257 | case "adminExit": 258 | 259 | fh.sessionData.Admin = false /* Unset admin flag */ 260 | functions.SaveConfigGlobalData(fh.viperData, r, fh.sessionData) /* Save global data */ 261 | functions.GetConfigData(fh.viperData, fh.sessionData) /* Get Config Data */ 262 | functions.ExecuteTemplate(w, fh.configData, fh.sessionData) /* This is the template execution for 'index' */ 263 | 264 | case "new": 265 | 266 | var path string /* Path to the executable */ 267 | var err error 268 | 269 | /* Spawn a new process */ 270 | if path, err = os.Executable(); err != nil { /* Get the path of the executable */ 271 | 272 | logger.LogEntry{ /* Log Entry */ 273 | Config: fh.configData, 274 | Market: nil, 275 | Session: fh.sessionData, 276 | Order: &types.Order{}, 277 | Message: functions.GetFunctionName() + " - " + err.Error(), 278 | LogLevel: "DebugLevel", 279 | }.Do() 280 | 281 | } 282 | 283 | cmd := exec.Command(path) /* Spawn a new process */ 284 | cmd.Stdout = os.Stdout /* Redirect stdout to os.Stdout */ 285 | cmd.Stderr = os.Stderr /* Redirect stderr to os.Stderr */ 286 | 287 | if err = cmd.Start(); err != nil { /* Start the new process */ 288 | 289 | logger.LogEntry{ /* Log error */ 290 | Config: fh.configData, 291 | Market: nil, 292 | Session: fh.sessionData, 293 | Order: &types.Order{}, 294 | Message: functions.GetFunctionName() + " - " + err.Error(), 295 | LogLevel: "DebugLevel", 296 | }.Do() 297 | 298 | } 299 | 300 | functions.ExecuteTemplate(w, fh.configData, fh.sessionData) /* This is the template execution for 'index' */ 301 | 302 | case "start": 303 | 304 | go execution(fh.viperData, fh.configData, fh.sessionData, fh.marketData) /* Start the execution process */ 305 | time.Sleep(2 * time.Second) /* Sleep time to wait for ThreadID to start */ 306 | http.Redirect(w, r, fmt.Sprintf(r.URL.Path), 301) /* Redirect to root 'index' */ 307 | 308 | case "stop": 309 | 310 | threads.Thread{}.Terminate(fh.sessionData, "") /* Terminate ThreadID */ 311 | 312 | case "update": 313 | 314 | functions.SaveConfigData(fh.viperData, r, fh.sessionData) /* Save the configuration data */ 315 | http.Redirect(w, r, fmt.Sprintf(r.URL.Path), 301) /* Redirect to root 'index' */ 316 | 317 | case "buy": 318 | 319 | fh.sessionData.ForceBuy = true /* Force buy */ 320 | http.Redirect(w, r, fmt.Sprintf(r.URL.Path), 301) /* Redirect to root 'index' */ 321 | 322 | case "sell": 323 | 324 | if r.PostFormValue("orderID") == "" { /* Check if the orderID is empty */ 325 | 326 | fh.sessionData.ForceSellOrderID = 0 /* Force sell most recent order */ 327 | fh.sessionData.ForceSell = true /* Force sell */ 328 | 329 | } else { 330 | 331 | fh.sessionData.ForceSellOrderID = functions.StrToInt(r.PostFormValue("orderID")) /* Force sell a specific orderID */ 332 | fh.sessionData.ForceSell = true /* Force sell */ 333 | 334 | } 335 | 336 | http.Redirect(w, r, fmt.Sprintf(r.URL.Path), 301) /* Redirect to root 'index' */ 337 | 338 | case "configTemplate": 339 | 340 | fh.sessionData.ConfigTemplate = functions.StrToInt(r.PostFormValue("configTemplateList")) /* Retrieve Configuration Template Key selection */ 341 | configData := functions.LoadConfigTemplate(fh.viperData, fh.sessionData) /* Load the configuration data */ 342 | functions.ExecuteTemplate(w, configData, fh.sessionData) /* This is the template execution for 'index' */ 343 | 344 | } 345 | } 346 | 347 | } 348 | 349 | } 350 | 351 | func execution( 352 | viperData *types.ViperData, 353 | configData *types.Config, 354 | sessionData *types.Session, 355 | marketData *types.Market) { 356 | 357 | var err error /* Error handling */ 358 | 359 | /* Connect to Exchange */ 360 | if err = exchange.GetClient(configData, sessionData); err != nil { /* GetClient returns an error if the connection to the exchange is not successful */ 361 | 362 | threads.Thread{}.Terminate(sessionData, functions.GetFunctionName()+" - "+err.Error()) /* Terminate ThreadID */ 363 | 364 | } 365 | 366 | /* Routine to resume operations */ 367 | var threadIDSessionDB string 368 | 369 | if sessionData.ThreadID, threadIDSessionDB, err = mysql.GetThreadTransactionDistinct(sessionData); err != nil { /* GetThreadTransactionDistinct returns an error if the connection to the database is not successful */ 370 | 371 | threads.Thread{}.Terminate(sessionData, functions.GetFunctionName()+" - "+err.Error()) /* Terminate ThreadID */ 372 | 373 | } 374 | 375 | if sessionData.ThreadID != "" && !configData.NewSession { /* If ThreadID is not empty and NewSession is false */ 376 | 377 | threads.Thread{}.Lock(sessionData) /* Lock thread file */ 378 | 379 | configData = functions.GetConfigData(viperData, sessionData) /* Get Config Data */ 380 | 381 | if sessionData.Symbol, err = mysql.GetOrderSymbol(sessionData); err != nil { /* GetOrderSymbol returns an error if the connection to the database is not successful */ 382 | 383 | threads.Thread{}.Terminate(sessionData, functions.GetFunctionName()+" - "+err.Error()) /* Terminate ThreadID */ 384 | 385 | } 386 | 387 | /* Select the symbol coin to be used from sessionData.Symbol */ 388 | if sessionData.SymbolFiat, err = algorithms.ParseSymbolFiat(sessionData); err != nil { /* ParseSymbolFiat returns an error if the symbol is not valid */ 389 | 390 | threads.Thread{}.Terminate(sessionData, functions.GetFunctionName()+" - "+err.Error()) /* Terminate ThreadID */ 391 | 392 | } 393 | 394 | logger.LogEntry{ /* Log Entry */ 395 | Config: configData, 396 | Market: marketData, 397 | Session: sessionData, 398 | Order: &types.Order{}, 399 | Message: "Resuming on port " + sessionData.Port, 400 | LogLevel: "InfoLevel", 401 | }.Do() 402 | 403 | } else { /* If ThreadID is empty or NewSession is true */ 404 | 405 | sessionData.ThreadID = functions.GetThreadID() /* Get ThreadID */ 406 | 407 | if !(threads.Thread{}.Lock(sessionData)) { /* Lock thread file */ 408 | 409 | os.Exit(1) 410 | 411 | } 412 | 413 | /* Select the symbol coin to be used from Config option */ 414 | sessionData.Symbol = configData.Symbol 415 | sessionData.SymbolFiat = configData.SymbolFiat 416 | 417 | logger.LogEntry{ /* Log Entry */ 418 | Config: configData, 419 | Market: marketData, 420 | Session: sessionData, 421 | Order: &types.Order{}, 422 | Message: "Initializing on port " + sessionData.Port, 423 | LogLevel: "InfoLevel", 424 | }.Do() 425 | 426 | } 427 | 428 | asyncFunctions(viperData, configData, sessionData) /* Starts async functions that are executed at specific intervals */ 429 | 430 | /* Retrieve available fiat funds and update database 431 | This is only used for retrieving balances for the first time, and is then followed by 432 | the Websocket routine to retrieve realtime user data */ 433 | if sessionData.SymbolFiatFunds, err = exchange.GetSymbolFiatFunds( /* GetSymbolFiatFunds returns an error if the connection to the exchange is not successful */ 434 | configData, 435 | sessionData); err == nil { /* If the connection to the exchange is successful */ 436 | _ = mysql.UpdateSession( /* Update database with available fiat funds */ 437 | configData, 438 | sessionData) 439 | } 440 | 441 | /* Retrieve available symbol funds 442 | This is only used for retrieving balances for the first time, ans is then followed by 443 | the Websocket routine to retrieve realtime user data */ 444 | sessionData.SymbolFunds, err = exchange.GetSymbolFunds(configData, sessionData) 445 | 446 | /* Retrieve exchange lot size for ticker and store in sessionData */ 447 | exchange.GetLotSize(configData, sessionData) 448 | 449 | sum := 0 450 | for { 451 | 452 | /* Check start/stop times of operation */ 453 | if configData.TimeEnforce { /* If TimeEnforce is true */ 454 | 455 | for !functions.IsInTimeRange(configData.TimeStart, configData.TimeStop) { /* If current time is not in the time range */ 456 | 457 | logger.LogEntry{ /* Log Entry */ 458 | Config: configData, 459 | Market: marketData, 460 | Session: sessionData, 461 | Order: &types.Order{}, 462 | Message: "Sleeping", 463 | LogLevel: "InfoLevel", 464 | }.Do() 465 | 466 | time.Sleep(300000 * time.Millisecond) /* Sleep for 5 minutes */ 467 | 468 | } 469 | 470 | } 471 | 472 | /* Update ThreadCount */ 473 | sessionData.ThreadCount, err = mysql.GetThreadTransactionCount(sessionData) 474 | 475 | /* Update Number of Sale Transactions per hour */ 476 | sessionData.SellTransactionCount, err = mysql.GetOrderTransactionCount(sessionData, "SELL") 477 | 478 | /* This routine is executed when no transaction cycle has initiated (ThreadCount = 0) */ 479 | if sessionData.ThreadCount == 0 { /* If ThreadCount is 0 */ 480 | 481 | sessionData.ThreadIDSession = functions.GetThreadID() /* Get ThreadID */ 482 | 483 | /* Save new session to Session table. */ 484 | if err := mysql.SaveSession( 485 | configData, 486 | sessionData); err != nil { 487 | 488 | /* Update existing session on Session table */ 489 | if err := mysql.UpdateSession( 490 | configData, 491 | sessionData); err != nil { 492 | 493 | threads.Thread{}.Terminate(sessionData, functions.GetFunctionName()+" - "+err.Error()) /* Terminate ThreadID */ 494 | 495 | } 496 | 497 | } 498 | 499 | } else { 500 | 501 | /* Retrieve existing Thread ID Session if first time */ 502 | if threadIDSessionDB != "" { 503 | 504 | sessionData.ThreadIDSession = threadIDSessionDB 505 | threadIDSessionDB = "" 506 | 507 | /* Save new session to Session table then update if fail */ 508 | if err := mysql.SaveSession( 509 | configData, 510 | sessionData); err != nil { 511 | 512 | /* Update existing session on Session table */ 513 | if err := mysql.UpdateSession( 514 | configData, 515 | sessionData); err != nil { 516 | 517 | /* Cleanly exit ThreadID */ 518 | threads.Thread{}.Terminate(sessionData, functions.GetFunctionName()+" - "+err.Error()) 519 | 520 | } 521 | 522 | } 523 | 524 | } 525 | 526 | } 527 | 528 | /* Conditional used in case this is the first run in the cycle go get past market data */ 529 | if marketData.PriceChangeStatsHighPrice == 0 { /* If PriceChangeStatsHighPrice is 0 */ 530 | 531 | markets.Data{}.LoadKlinePast(configData, marketData, sessionData) /* Load Kline Past */ 532 | 533 | } 534 | 535 | wg := &sync.WaitGroup{} /* WaitGroup to stop inside Channels */ 536 | wg.Add(3) /* WaitGroup to stop inside Channels */ 537 | 538 | go telegram.CheckUpdates( /* Check for Telegram updates */ 539 | configData, 540 | sessionData, 541 | wg) 542 | 543 | go algorithms.WsKline( /* Websocket routine to retrieve realtime candle data */ 544 | configData, 545 | marketData, 546 | sessionData, 547 | wg) 548 | 549 | go algorithms.WsUserDataServe( /* Websocket routine to retrieve realtime user data */ 550 | configData, 551 | sessionData, 552 | wg) 553 | 554 | go algorithms.WsBookTicker( /* Websocket routine to retrieve realtime ticker prices */ 555 | viperData, 556 | configData, 557 | marketData, 558 | sessionData, 559 | wg) 560 | 561 | wg.Wait() /* Wait for the goroutines to finish */ 562 | 563 | logger.LogEntry{ /* Log Entry */ 564 | Config: configData, 565 | Market: nil, 566 | Session: sessionData, 567 | Order: &types.Order{}, 568 | Message: "All websocket channels stopped", 569 | LogLevel: "DebugLevel", 570 | }.Do() 571 | 572 | sessionData.StopWs = false /* Reset goroutine channels */ 573 | 574 | /* Reload configuration in case of WsBookTicker broken connection */ 575 | configData = functions.GetConfigData(viperData, sessionData) /* Get Config Data */ 576 | 577 | time.Sleep(3000 * time.Millisecond) /* Sleep for 3 seconds */ 578 | 579 | logger.LogEntry{ /* Log Entry */ 580 | Config: configData, 581 | Market: nil, 582 | Session: sessionData, 583 | Order: &types.Order{}, 584 | Message: "Restarting", 585 | LogLevel: "DebugLevel", 586 | }.Do() 587 | 588 | /* repeated forever */ 589 | sum++ 590 | 591 | } 592 | 593 | } 594 | 595 | // asyncFunctions starts async functions that are executed at specific intervals 596 | func asyncFunctions( 597 | viperData *types.ViperData, 598 | configData *types.Config, 599 | sessionData *types.Session) { 600 | 601 | /* Synchronize time with Binance every 5 minutes */ 602 | _ = exchange.NewSetServerTimeService(configData, sessionData) 603 | scheduler.RunTaskAtInterval( 604 | func() { _ = exchange.NewSetServerTimeService(configData, sessionData) }, 605 | time.Second*300, 606 | time.Second*0) 607 | 608 | /* Retrieve config data every 10 seconds. */ 609 | scheduler.RunTaskAtInterval( 610 | func() { configData = functions.GetConfigData(viperData, sessionData) }, 611 | time.Second*10, 612 | time.Second*0) 613 | 614 | /* run function UpdatePendingOrders() every 180 seconds */ 615 | rand.Seed(time.Now().UnixNano()) 616 | scheduler.RunTaskAtInterval( 617 | func() { algorithms.UpdatePendingOrders(configData, sessionData) }, 618 | time.Second*180, 619 | time.Second*time.Duration(rand.Intn(180-1+1)+1), 620 | ) 621 | 622 | /* Retrieve initial node role and then every 60 seconds */ 623 | nodes.Node{}.GetRole(configData, sessionData) 624 | scheduler.RunTaskAtInterval( 625 | func() { 626 | nodes.Node{}.GetRole(configData, sessionData) 627 | }, 628 | time.Second*60, 629 | time.Second*0) 630 | 631 | /* Keep user stream service alive every 60 seconds */ 632 | scheduler.RunTaskAtInterval( 633 | func() { _ = exchange.KeepAliveUserStreamServiceListenKey(configData, sessionData) }, 634 | time.Second*60, 635 | time.Second*0) 636 | 637 | /* Update Number of Sale Transactions per hour every 3 minutes. 638 | The same function is executed after each sale, and when initiating cycle. */ 639 | scheduler.RunTaskAtInterval( 640 | func() { 641 | sessionData.SellTransactionCount, _ = mysql.GetOrderTransactionCount(sessionData, "SELL") 642 | }, 643 | time.Second*180, 644 | time.Second*0) 645 | 646 | /* Update exchange latency every 5 seconds. */ 647 | scheduler.RunTaskAtInterval( 648 | func() { 649 | sessionData.Latency, _ = functions.GetExchangeLatency(sessionData) 650 | }, 651 | time.Second*5, 652 | time.Second*0) 653 | 654 | /* Check system status every 10 seconds. */ 655 | scheduler.RunTaskAtInterval( 656 | func() { 657 | nodes.Node{}.CheckStatus(configData, sessionData) 658 | }, 659 | time.Second*10, 660 | time.Second*0) 661 | 662 | /* Send Telegram message with system error (only Master Node) every 60 seconds. */ 663 | scheduler.RunTaskAtInterval( 664 | func() { 665 | if sessionData.MasterNode && sessionData.TgBotAPIChatID != 0 { 666 | if threadID, err := mysql.GetSessionStatus(sessionData); err == nil { 667 | if threadID != "" { 668 | telegram.Message{ 669 | Text: "\f" + "System Fault @ " + threadID, 670 | }.Send(sessionData) 671 | } 672 | } 673 | } 674 | }, time.Second*60, 675 | time.Second*0) 676 | 677 | /* Load mySQL dynamic components for javascript autoloader every 10 seconds. */ 678 | scheduler.RunTaskAtInterval( 679 | func() { 680 | loader.LoadSessionDataAdditionalComponentsAsync(sessionData) 681 | }, 682 | time.Second*10, 683 | time.Second*0) 684 | 685 | } 686 | --------------------------------------------------------------------------------