[Automatic trading of Bitcoin] A story about operating with Docker on AWS, with 1 TAP ON / OFF & Line notification from Apple Watch on the go

autocoin2

BitCoin's Forex automatic trading program. Using Bitflyer's API, we have automated cryptocurrency trading with node.js. How nice it would be if you could make more money when you were sleeping, in the bathroom, or 24 hours a day. .. : gem: I want to have fun and make money automatically! I tried to make it with such bad human motivation. iOS の画像.jpgスクリーンショット2020-09-2010.05.38.png

** Suddenly the conclusion is ... unfortunately not profitable: scream: Rather, it is decreasing. .. ** **

However, if you tune it, you may make a profit. (* We are not responsible for any damage. *) Please do so at your own risk!

** Code is available on Github **

Characteristic

--Supports both sell and buy positions --Weighted trading judgment by multiple algorithms --Saving trading history with MongoDB --Line notification of transaction start --If you exceed the threshold of the profit and loss amount, you will be notified by Line --A function to automatically release the position after a certain number of days have passed. --A function to suppress the acquisition of new positions 30 minutes before the date change --ON / OFF with one tap from iphone even when you are out with Apple Home cooperation --Regular human trading is possible in parallel while the program is running

System overview

autocoin2.png

Technology used

Directory structure

.
├── autocoin
│  ├── algo.js
│  ├── app.js
│  ├── config.js
│  ├── crypto.js
│  ├── line.js
│  ├── mongo.js
│  └── utils.js
├── container_data
├── homebridge_AWS
│  ├── startAWS.sh
│  └── stopAWS.sh
├── .env
├── Dockerfile
└── docker-compose.yml

Main: app.js

The entry point for this program. Buying and selling is repeated by turning the code in a loop process.


'use strict';
const config = require('./config');
const moment = require('moment');
const ccxt = require('ccxt');
const bitflyer = new ccxt.bitflyer(config);

const Crypto = require('./crypto')
const Mongo = require('./mongo');
const mongo = new Mongo();
const Line = require('./line');
const line = new Line(config.line_token)
const utils = require('./utils');
const Algo = require('./algo');

//Trading interval(Seconds)
const tradeInterval = 180;
//Trading volume
const orderSize = 0.01;
//swap days
const swapDays = 3;
//Price difference threshold for notifications
const infoThreshold = 100;

//PsychAlgo settings;Positive line count
const psychoParam = {
  'range': 10,
  'ratio': 0.7,
};
//crossAlgo setting value:Moving average width
const crossParam = {
  'shortMA': 5,
  'longMA': 30,
};

//Bollinger band settings
const BBOrder = {
  //order
  'period': 10,
  'sigma': 1.7
};
const BBProfit = {
  //Profitability
  'period': 10,
  'sigma': 1
};
const BBLossCut = {
  //Loss cut:Judgment on a daily basis
  'period': 10,
  'sigma': 2.5
};

//Algorithm weighting:Set unused to 0
const algoWeight = {
  // 'psychoAlgo': 0,
  // 'crossAlgo': 0,
  // 'bollingerAlgo': 1,
  'psychoAlgo': 0.1,
  'crossAlgo': 0.2,
  'bollingerAlgo': 0.7,
};
//Threshold for transaction judgment
const algoThreshold = 0.3;
//Loss cut threshold
const lossCutThreshold = 0.5;


(async function () {
  let sumProfit = 0;
  let beforeProfit = null;
  const nowTime = moment();
  const collateral = await bitflyer.fetch2('getcollateral', 'private', 'GET');

  //(Minutes)Record creation
  const crypto = new Crypto();
  const beforeHour = crossParam.longMA * tradeInterval;
  const timeStamp = nowTime.unix() - beforeHour;
  let records = await crypto.getOhlc(tradeInterval, timeStamp);

  const algo = new Algo(records);

  //Notify Line of automatic trading start
  const strTime = nowTime.format('YYYY/MM/DD HH:mm:ss');
  const message = `\n Automatic trading start\n date: ${strTime}\n collateral: ${collateral.collateral}`;
  line.notify(message);


  while (true) {
    let flag = null;
    let label = "";
    let tradeLog = null;

    const nowTime = moment();
    const strTime = nowTime.format('YYYY/MM/DD HH:mm:ss');

    //Check the operating status of the exchange
    let health = await bitflyer.fetch2('getboardstate');
    if (health.state !== 'RUNNING') {
      //If abnormal, at the beginning of while
      console.log('Operation status of the exchange:', health);
      await utils.sleep(tradeInterval * 1000);
      continue;
    }

    //Get current price
    const ticker = await bitflyer.fetchTicker('FX_BTC_JPY');
    const nowPrice = ticker.close;

    //Update record
    algo.records.push(nowPrice);
    algo.records.shift()

    //Initialize Param for algorithm
    let bbRes = null;
    let totalEva = 0;
    algo.initEva();
    //Common algorithm
    let crossRes = algo.crossAlgo(crossParam.shortMA, crossParam.longMA);
    let psychoRes = algo.psychoAlgo(psychoParam.range, psychoParam.ratio)

    //Examine open interest
    const jsonOpenI = await bitflyer.fetch2('getpositions', 'private', 'GET', {product_code: "FX_BTC_JPY"});
    const openI = utils.chkOpenI(jsonOpenI)

    //Common display
    console.log('================');
    console.log('time:', strTime);
    console.log('nowPrice: ', nowPrice);


    //If there is open interest
    if (openI.side) {
      //Common display of open interest
      console.log('');
      console.log('Open interest content');
      console.log(openI);

      let diffDays = nowTime.diff(openI.open_date, 'days');
      //If the swap days are exceeded
      if (diffDays >= swapDays) {
        //Return open interest to 0
        label = 'Reset open interest because swap days have been exceeded'

        if (openI.side === 'BUY') {
          await bitflyer.createMarketSellOrder('FX_BTC_JPY', openI.size);
          flag = 'SELL';

        } else {
          await bitflyer.createMarketBuyOrder('FX_BTC_JPY', openI.size);
          flag = 'BUY';
        }
        sumProfit += openI.pnl;

      } else {
        //If the number of days is not exceeded
        //If you are making a profit
        if (openI.pnl > 0) {
          label = 'Profitability'
          bbRes = algo.bollingerAlgo(BBProfit.period, BBProfit.sigma, openI.price);
          totalEva = algo.tradeAlgo(algoWeight)

          //There is a downward signal on the open interest
          if (openI.side === 'BUY' && totalEva < -algoThreshold) {
            await bitflyer.createMarketSellOrder('FX_BTC_JPY', openI.size);
            sumProfit += openI.pnl;
            flag = 'SELL';

            //There is a rising signal on open interest
          } else if (openI.side === 'SELL' && totalEva > algoThreshold) {
            await bitflyer.createMarketBuyOrder('FX_BTC_JPY', openI.size);
            sumProfit += openI.pnl;
            flag = 'BUY';

          }
        } else {
          //If you are losing
          label = 'Loss cut';

          //Algorithm judgment on a daily basis
          const dayPeriods = 60 * 60 * 24;
          const lossTimeStamp = nowTime.unix() - dayPeriods * BBLossCut.period;
          let dayRecords = await crypto.getOhlc(dayPeriods, lossTimeStamp);

          crossRes = algo.crossAlgo(crossParam.shortMA, crossParam.longMA, dayRecords);
          psychoRes = algo.psychoAlgo(psychoParam.range, psychoParam.ratio, dayRecords);
          bbRes = algo.bollingerAlgo(BBLossCut.period, BBLossCut.sigma, openI.price, dayRecords);
          totalEva = algo.tradeAlgo(algoWeight)

          //Even though I'm losing, there are signs that a big trend is going down with buying
          if (openI.side === 'BUY' && totalEva < -lossCutThreshold) {
            await bitflyer.createMarketSellOrder('FX_BTC_JPY', openI.size);
            sumProfit += openI.pnl;
            flag = 'SELL';

            //Even though I'm losing, I have a sell and a big trend is rising
          } else if (openI.side === 'SELL' && totalEva > lossCutThreshold) {
            await bitflyer.createMarketBuyOrder('FX_BTC_JPY', openI.size);
            sumProfit += openI.pnl;
            flag = 'BUY';
          }
        }
      }

      //If you settle the open interest
      if (flag) {
        tradeLog = {
          flag: flag,
          label: label,
          sumProfit: sumProfit,
          profit: openI.pnl,
          nowPrice: nowPrice,
          openPrice: openI.price,
          strTime: strTime,
          created_at: nowTime._d,
          openI: openI,
          bollinger: bbRes,
          cross: crossRes,
          psycho: psychoRes,
          totalEva: totalEva,
        };
        mongo.insert(tradeLog);

        console.log('');
        console.log(label);
        console.log(tradeLog);
      }

      //Line notification(If the threshold is exceeded)
      if (beforeProfit !== null) {
        const profit = openI.pnl;
        const diff = Math.abs(sumProfit + profit - beforeProfit);
        if (diff >= infoThreshold) {
          const message = `\n date: ${strTime}\n sumProfit: ${sumProfit}\n profit: ${profit}\n collateral: ${collateral.collateral}`;
          line.notify(message);
          beforeProfit = sumProfit + profit;
        }
      } else {
        //Alert initialization
        beforeProfit = sumProfit;
      }


    } else {
      //If you do not have open interest

      //Swap point support 23:30-0:00 Do not order
      const limitDay = moment().hours(23).minutes(30).seconds(0)
      if (nowTime.isSameOrAfter(limitDay)) {
        console.log(' ');
        console.log('Supporting swap points_23:30-0:00');
        //Move to the beginning while not accepting orders
        await utils.sleep(tradeInterval * 1000);
        continue;
      }

      //Use Bollinger to order
      bbRes = algo.bollingerAlgo(BBOrder.period, BBOrder.sigma);
      totalEva = algo.tradeAlgo(algoWeight)

      if (totalEva > algoThreshold) {
        //Open a position with [Buy]
        await bitflyer.createMarketBuyOrder('FX_BTC_JPY', orderSize);
        flag = 'BUY';

      } else if (totalEva < -algoThreshold) {
        //Open with [Sell]
        await bitflyer.createMarketSellOrder('FX_BTC_JPY', orderSize);
        flag = 'SELL';
      }

      //If you get open interest
      if (flag) {
        label = 'Open interest acquisition';

        tradeLog = {
          flag: flag,
          label: label,
          sumProfit: sumProfit,
          nowPrice: nowPrice,
          bollinger: bbRes,
          cross: crossRes,
          psycho: psychoRes,
          totalEva: totalEva,
          strTime: strTime,
          created_at: nowTime._d,
        };
        mongo.insert(tradeLog);

        console.log('');
        console.log(label);
        console.log(tradeLog);
      }
    }

    console.log('');
    console.log('★sumProfit: ', sumProfit);
    console.log('');
    await utils.sleep(tradeInterval * 1000);
  }
})();


Description of hyperparameters

--tradeInterval: Trade interval. The shortest is 60 seconds. --orderSize: Number of orders --swapDays: The number of days you want to keep open interest. If you exceed it, let it go. --infoThreshold: Line Amount range for notification. Line will announce that you will lose or gain more than the set amount. --Psychparam: Parameters used for the psychological line algorithm. --Period --Ratio --crossParam: Parameters used for the Golden Cross / Dead Cross algorithm. --Short-term moving average period --Long-term moving average period --BBOrder / BBProfit / BBLossCut: Parameters used for the Bollinger Bands algorithm. It is divided because the judgment material is different for each open interest acquisition / profit settlement / loss cut. --Judgment period

Introductory explanation of branching and flow

It is a rough processing flow.

--Start trading Obtain the transaction details before code execution from cryptowatch to use as a judgment material. Notify that "automatic trading has started" on Line

--Start a trading loop Loop around the set transaction interval.

--Check the operating status of the bitflyer exchange If abnormal, move to the beginning of the loop

--Get the current bitcoin price

--Common algorithm Evaluate cross functions and psychological functions

--Obtain the contents of the open position (position) you are holding.

--If you are holding open interest, check the number of days you are holding open interest. If the retention days are longer than the specified number of days, let go of the open interest (Swap gold and to avoid being salted.)

--When the retention period is short ――If you are profitable, buy and sell based on position and algorithm judgment

--If there is a loss, loss cut by algorithm judgment with daily material The reason why the loss cut is used on a daily basis is that the index is too fluid for the minute bar and it is necessary to make a judgment on a big trend. In fact, I used to use the minute leg in the old days, but it was easy to accumulate small losses as well as losing the gain chance due to being swayed by a fairly small blur. It is possible to change the minute and hourly bars, so it may be a good idea to adjust them.

--If a certain amount of profit or loss occurs, you will be notified by Line.

--If you do not have open interest

--If it is 30 minutes just before the date change, loop without trading. To avoid getting swap money early after getting open interest.

--If it is not just before the date, the open position is acquired by the algorithm judgment.

Algorithm: algo.js

It summarizes the buying and selling algorithms.

const gauss = require('gauss');

module.exports = class Algo {

  constructor(records) {
    this.records = records;

    //Evaluation points of each algorithm
    //Rising signal:+Down signal:-
    this.eva = {
      'psychoAlgo': 0,
      'crossAlgo': 0,
      'bollingerAlgo': 0
    };
  }

  psychoAlgo(range, ratio, list = this.records) {
    //Judging buying and selling by the ratio of positive lines

    let countHigh = 0
    //Count the number of positive rays in any period
    for (let i = range; i > 0; i--) {
      const before = list[list.length - i - 1];
      const after = list[list.length - i];

      if (before <= after) {
        countHigh += 1;
      }
    }

    let psychoRatio = 0;
    psychoRatio = countHigh / range;
    if (psychoRatio >= ratio) {
      this.eva['psychoAlgo'] = 1;
    } else if (psychoRatio <= 1 - ratio) {
      this.eva['psychoAlgo'] = -1;
    }

    return psychoRatio;
  }


  crossAlgo(shortMA, longMA, list = this.records) {
    //Judging buying and selling at the Golden Dead Cross

    //Moving average creation
    const prices = new gauss.Vector(list);
    const shortValue = prices.ema(shortMA).pop();
    const longValue = prices.ema(longMA).pop();

    if (shortValue >= longValue) {
      this.eva['crossAlgo'] = 1;
    } else if (shortValue < longValue) {
      this.eva['crossAlgo'] = -1;
    }

    return {'shortValue': shortValue, 'longValue': longValue};
  }


  bollingerAlgo(period, sigma, price = this.records.slice(-1)[0], list = this.records) {
    //Bollinger band

    const prices = new gauss.Vector(list.slice(-period));
    //Use SMA
    const sma = prices.sma(period).pop();
    const stdev = prices.stdev()

    const upper = Math.round(sma + stdev * sigma);
    const lower = Math.round(sma - stdev * sigma);

    if (price <= lower) {
      this.eva['bollingerAlgo'] = 1;
    } else if (price >= upper) {
      this.eva['bollingerAlgo'] = -1;
    }

    return {'upper': upper, 'lower': lower}
  }


  tradeAlgo(weight) {
    //Weighted and comprehensive transaction decisions

    let totalEva = 0
    //Multiply the evaluation points by their respective weights and add them together
    for (const [key, value] of Object.entries(this.eva)) {
      totalEva += value * weight[key];
    }

    totalEva = Math.round(totalEva * 100) / 100

    return totalEva
  }


  initEva() {
    //Initialize all evaluation points
    Object.keys(this.eva).forEach(key => {
      this.eva[key] = 0;
    });
  }

}

Combined evaluation

tradeAlgo()

A transaction decision is a compound decision based on multiple algorithms. Each algorithm holds an evaluation point. Each algorithm gives an evaluation of either -1 or +1 depending on the material data and setting parameters. Positive number (+1) is an uptrend Negative number (-1) is downtrend Multiply each algorithm by the evaluation points and their weights, and finally add them all together to calculate the total evaluation points.

Execute the transaction if the absolute value of the total evaluation points exceeds the threshold value in app.js. Whether to trade in a buy or sell position is determined by app.js depending on the situation.

About adding an algorithm

If you want to add a new algorithm in the future, refer to the procedure below.

--Algo class --this.eva (adding the same evaluation point as the method name) --Add algorithm as method

Bollinger band

bollingerAlgo()

Bollinger Bands is a judgment algorithm that uses a moving average and standard deviation. Roughly speaking, the larger the absolute value of the standard deviation, the stronger the ability to return to the mean. I won't touch on it in detail, but this explanation is easy to understand. Explanation of Monex, Inc.

Uses 4 variables.

--Judgment period --Standard deviation to threshold --Price you want to judge --Price movement list

Extract the period you want to judge from the price movement list. Next, the upper value and lower value of the specified standard deviation are calculated based on the extracted list. Finally, if the price is out of the calculated upper and lower standard deviation bands, an evaluation point is added.

If the price is lower than the lower value Its price is lower than the trend, so it is easy to start rising. Add +1 as an uptrend

When the price is higher than the upper value Its price is higher than the trend, so it is easy to turn down. Add -1 as a downtrend

Psychological line

psychoAlgo()

Algorithm judgment using investor psychology. An algorithm that predicts price fluctuations by determining that if either buying or selling is biased in succession, the tendency will continue, or the opposite will soon occur, and many trades will be made. This page is easy to understand. Explanation of Monex, Inc.

Uses 3 variables.

--Judgment range --Judgment ratio --Price list

Narrow down to the price list for the set period, Find out the number of prices that are higher than the previous price and the ratio of the total price. Compare using the ratio judged as the ratio value, and add evaluation points as an uptrend and a downtrend.

Golden cross, dead cross

crossAlgo()

Golden cross that the long-term moving average penetrates the short-term moving average from bottom to top. The opposite is dead cross. Golden is a sign of an uptrend and dead cross is a sign of a downtrend.

Uses 3 variables.

--Short period --Long term --Price list

As explained above, if the short-term moving average is higher than the long-term moving average, an evaluation point of +1 is given as an uptrend. If the opposite is true, add -1 to the evaluation point as a dead cross. Since I wanted to add more trend momentum, I used the Exponential Moving Average, which emphasizes the latest price movements.

Get OHLC: crypto.js

Use cryptowatch API to get OHLC immediately after program execution. OHLC stands for open / high / low / close and candlestick data.

Line notification: line.js

Line will notify you every time a transaction starts and a certain amount of profit or loss occurs.

This page is easy to understand about Line Nnotify. Try using LINE notify

Save transaction details: mongo.js

Record sales transactions with MongoDB. Since the transaction content is json acquisition from bitflyer API and not standard data, I chose MongoDB of NoSQL.

It is operated by Docker container, but the data is persisted by volumes. The first Docker container launch creates a volume directory: container_data.

Other: utils.js

It summarizes other utility functions.

Linked with Apple Home

The program on AWS can be turned ON / OFF with one tap from iphone and Apple Watch.

  1. Deploy Docker on your AWS EC2 instance
  2. Expand homebridge to your home Raspberry Pi and link the execution shell file

Please refer to the following for details. Official homebridge

[Philips_Hue is linked with API! ~ Make Raspberry Pi HomeKit](https://qiita.com/akinko/items/58c650f99f25fc7e3cb5#%E3%83%A9%E3%82%BA%E3%83%91%E3%82%A4%E3%82 % 92homekit% E5% 8C% 96% E3% 81% 99% E3% 82% 8B)

View transaction DB in IntelliJ

Although it is the transaction content saved in MongoDB, we recommend using the IDE to view it. Browsing directly from MongoDB is quite painful because of the json format. If it is an IDE such as IntelliJ, it will be nice and easy to see. スクリーンショット 2020-09-20 10.18.45.png スクリーンショット 2020-09-20 10.11.57.png Please refer to the past article for the setting method of IntelliJ. [Summary of setting method to operate AWS from IntelliJ](https://qiita.com/akinko/items/d7001a8fe3ac87e1790c#db%E3%81%A8%E3%81%AE%E7%B0%A1%E5%8D% 98% E6% 8E% A5% E7% B6% 9A)

important point

First Docker launch

It takes time for MongoDB to become writable. This is to create container_data for the volume directory. Start it for the first time with a margin, and if it is not written after a while, restart Docker again. In my case, it works fine from the second time onwards.

Docker Volume container_data has root privileges

The container_data of the created volume directory is created with root privileges. If you want to delete it, add sudo. In my inadvertent experience, when rebuilding Docker, I didn't notice this directory permission and got an error and got a little addicted.

At the end

Enjoy and never realize your unearned income dream: skull :: skull: It's persistent, but it's not always profitable, so please take your own risk. : point_up_tone3:

Possibly My algorithm, or the only parameter I don't like, may be profitable with an algorithm added by someone. .. ..

In such a case, please tell me secretly: musical_note: Then, if you like, please enjoy it as a hobby.

Recommended Posts

[Automatic trading of Bitcoin] A story about operating with Docker on AWS, with 1 TAP ON / OFF & Line notification from Apple Watch on the go
A story about hitting the League Of Legends API with JAVA
A story that struggled with the introduction of Web Apple Pay
A story that I was addicted to twice with the automatic startup setting of Tomcat 8 on CentOS 8
The story of making a game launcher with automatic loading function [Java]
A quick note on using jshell with the official Docker image of the JDK
[Jackson] A story about converting the return value of BigDecimal type with a custom serializer.