웹/Javascript

카카오톡 챗봇 코인 정보봇 만들기

인생마린 2021. 12. 24. 04:12
반응형

카카오톡 오픈채팅방에 돌아다니다 보면 코인이나 주식의 symbol을 입력하면 해당 정보를 요약해서 보여주는 봇이 종종 보인다.

 

코인에 대하여 정보를 요약해서 보여주는 봇을 만들어보자!

 

먼저 우리는 Javascript로 만들기 때문에 node module같은 편한 라이브러리를 쓸수 없다.

 

따라서 json형태로 던져주는 rest api를 찾아야 된다.

 

코인 같은 경우 주식과 다르게 거래소마다 다른 가격을 띄기도 하고,

어떤 거래소에서는 상장되었는데 어떤 거래소에서는 상장이 안된 경우도 있다.

 

따라서 이번에 총 3곳의 거래소 API를 활용하여 코인 정보봇을 만들어 보려고 한다.

해당 거래소는 gate, binance, upbit 이다.

 

 

GATE API

먼저 gate documentation이라고 구글링 해보고 우리가 원하는 api를 찾아본다

https://www.gate.io/docs/apiv4/en/#get-details-of-a-specifc-order

해당 링크 클릭시 정상적인 이동이 이루어지지 않으므로 ctrl + f를 활용하여 "/spot/tickers"를 검색하여 찾아보자

ticker 정보를 받아올 수 있다.

GET 요청으로 /spot/tickers url로 요청을 하면 티커들의 정보를 얻을 수 있다.

이 때 파라미터로 currency_pair값 즉 티커명을 적으면, 특정 티커에 대한 정보만 받을 수 있다.(안적을 경우 모든 티커에 대한 정보를 던져준다)

 

오른쪽 위에 예시코드는 파이썬으로 우리가 자바스크립트로 코드로 따로 컨버트하여서 작성해주어야 된다.

해당 코드가 정상적으로 작동하면 json형태로 우리에게 데이터를 던져주게 된다.

 

https://api.gateio.ws/api/v4/spot/tickers

https://api.gateio.ws/api/v4/spot/tickers?currency_pair=doge_usdt  //doge에 대한 정보만 가져오고 싶을때

 

이제 자바스크립트로 웹정보를 받아오기만 하면 되는데,

통상적으로 해당앱에서 제공하는 유틸함수가 있는데, 웹에 있는 컨텐츠를 긁어오는 함수이다.

Utils.getWebText(url)

하지만 이 함수로 긁어올 경우 헤더정보를 포함하기도 까다롭고, 파싱하기도 까다롭다.

그래서 또다른 대안이 있었는데, 이 앱은 안드로이드 앱으로 자바로 작성 된 앱이다.

따라서 자바에서 지원하는 Jsoup을 지원해준다.

org.jsoup.Jsoup.connect(url)

Jsoup에서 헤더를 추가하여 json 정보를 가져오는법은 다음과 같다

let json = org.jsoup.Jsoup.connect(url)
            .header('Accept', 'application/json')
            .header('Content-Type', 'application/json')
            .ignoreContentType(true)
            .execute()
            .body();

ignoreContentType(true)를 해줘야만 정상작동하는데, 뭔가 문제가 있다.. 따라서 body부분이 남기 때문에 .body()를 통해 body안에 있는 content들만 읽어올 수 있게 한다.

 

그러면 문자열 형태의 json이 변수에 담기게 된다.

 

여기까지 진행하였다면, 한번 정상적으로 json을 가져오는지 replier로 출력해보자!

 

msg.startsWith('1g ')부분을 해석하자면, 메세지가 "1g "로 시작하는지를 확인하는 것이다. 그후 substr로 뒤에 티커를 분리해서 함수 파라미터로 넘겨주는 것이다.

function response(room, msg, sender, isGroupChat, replier, ImageDB, packageName) {
    if (msg.startsWith('1g ')) {
        cmd = msg.substr(3);
        replier.reply(gate_get_symbol_info(cmd));
    }
}

// gate에 특정 티커 정보를 출력합니다.
function gate_get_symbol_info(symbol) {
    symbol = symbol.toUpperCase();

    if (!symbol.includes('_')) {
        symbol += '_USDT';
    }

    host = 'https://api.gateio.ws/api/v4/spot/tickers';

    let json = org.jsoup.Jsoup.connect(host + '?' + 'currency_pair=' + symbol)
        .header('Accept', 'application/json')
        .header('Content-Type', 'application/json')
        .ignoreContentType(true)
        .execute()
        .body();

    return json;
}

정상적으로 실행되는 모습

doge에 대한 정보를 검색하니 json형태로 정상적으로 받아오는것을 확인 할 수 있다.

 

이제 우리는 이걸 자바스크립트 변수처럼 사용해야 되는데 방법은 간단하다

json = JSON.parse(json)[0];

JSON.parse()함수를 사용하여 문자열인 json을 자바스크립트 변수형태로 바꾸어주는 것이다.

이때 우리는 특정 코인에 대한 정보가 담긴 0번 인덱스를 값만 가져와준다.

 

추가로 해당 코인의 기간별 수익률을 출력해주려고 한다.

1주, 1달, 3달, 6달, 1년 단위로 수익률을 판별하려 하는데, 그러기 위해서는 365일치의 데이터가 필요하다.

이러한 API를 documentation에서 찾아볼 수 있다.

 

https://www.gate.io/docs/apiv4/en/index.html#market-candlesticks

 

candlesticks api

캔들을 그릴때 사용하는 api로 interval과 limit를 파라미터로 넘겨서 365일치 데이터를 넘겨받을 수 있다.

응답값의 의미

각 응답값은 위와 같은 의미를 지니게 된다.

 

https://api.gateio.ws/api/v4/spot/candlesticks?currency_pair=DOGE_USDT&limit=365&interval=1d // doge코인 365일치 데이터 하루단위

 

UPBIT API

https://docs.upbit.com/reference/%EC%9D%BCday-%EC%BA%94%EB%93%A4-1

업비트에 대한 일일 캔들데이터를 documentation을 통해 얻는법을 쉽게 알 수 있다.

개인적으로 업비트가 가장 documentation이 깔끔했던것 같다

 

일 캔들

오른쪽 부분을 보면 Try It!으로 api를 실제 사용해볼 수 있는데, 밑으로 내려보면 parameter를 수정 할 수 있다.

먼저 parameter를 수정한 후 Try It!을 눌러보자

 

parameter 수정
조회 성공!

https://api.upbit.com/v1/candles/days?market=KRW-DOGE&count=365

사용법을 익혔으니 gate와 마찬가지로 코드를 짜주면 된다. 똑같은 과정이므로 따로 예시는 들지 않겠다

 

BINANCE API

바이낸스가 API부분에서 제일 골때렸다.

https://binance-docs.github.io/apidocs/futures/en/#kline-candlestick-data 

여기에 적혀있는대로 할 경우 candlestick date를 정상적으로 호출하지 못하는데,

stackoverflow를 통해서 따로 찾아보니 다른 url을 사용해야 됐다... (documentation 업데이트좀..)

 

사용법은 동일하다

다행히도 url부분만 바꾸면 사용법은 똑같았다.

https://api.binance.com/api/v3/klines?symbol=DOGEUSDT&interval=1d&limit=365 

마찬가지로 사용법을 익혔으니 gate와 같게 코드를 짜주면 된다.

 

최종 코드

이제 각 API를 활용하여 소스코드를 합치면 다음과 같다

1calc은 계산 명령어인데, 위에서 따로 설명하지 않았다. 사칙연산만 가능하고 지수연산은 지원하지 않는다.

eval함수로 구현하였기 때문에, regex를 통해 표현식을 검사한다.

function response(room, msg, sender, isGroupChat, replier, ImageDB, packageName) {
    if (msg.startsWith('1help')) {
        replier.reply(help());
    }
    if (msg.startsWith('1g ')) {
        cmd = msg.substr(3);
        replier.reply(gate_get_symbol_info(cmd));
    }

    if (msg.startsWith('1u ')) {
        cmd = msg.substr(3);
        replier.reply(upbit_get_symbol_info(cmd));
    }

    if (msg.startsWith('1b ')) {
        cmd = msg.substr(3);
        replier.reply(binance_get_symbol_info(cmd, ''));
    }

    if (msg.startsWith('1calc ')) {
        cmd = msg.substr(6);
        replier.reply(calc(cmd));
    }
}

function help() {
    let msg = '(⊙_⊙)?봇 사용법\n' + '\u200b'.repeat(500);
    const help_msg = [
        '1help: 도움말을 출력합니다', // 완성
        '1b [ticker]: 바이낸스에 코인가격을 출력합니다', // 완성
        '1g [ticker]: 게이트에 코인가격을 출력합니다', // 완성
        '1u [ticker]: 업비트에 코인가격을 출력합니다', // 완성
        '1calc [계산식]: 계산을 해줍니다', // 완성
        '',
        '❄ 예시',
        '⎼⎼⎼⎼⎼⎼⎼⎼⎼⎼⎼⎼⎼⎼⎼⎼⎼⎼⎼⎼⎼⎼⎼⎼⎼⎼⎼⎼⎼⎼⎼⎼⎼⎼⎼⎼⎼⎼⎼⎼⎼⎼⎼⎼⎼⎼⎼⎼⎼⎼⎼⎼⎼⎼⎼⎼⎼⎼⎼⎼⎼⎼⎼⎼⎼⎼⎼⎼⎼⎼⎼⎼⎼⎼⎼⎼⎼⎼⎼⎼',
        '바이낸스 Ticker ex) DOGE -> DOGEUSDT, DOGEBTC -> DOGEBTC',
        '게이트 Ticker ex) DOGE -> DOGE_USDT, DOGE_BTC -> DOGE_BTC',
        '업비트 Ticker ex) DOGE -> KRW-DOGE, USDT-DOGE -> USDT-DOGE',
    ];

    msg += help_msg.join('\n');

    return msg;
}

function calc(expression) {
    try {
        let fa = expression.search(/\*{2}/g);
        if (fa != -1) {
            return '지수 연산자는 지원하지 않습니다. ' + expression;
        }
        let m = expression.match(/[0-9*/%+\- .()]+/);
        if (m == null) {
            return '숫자나 사칙연산을 제외한 기호는 지원하지 않습니다. ' + expression;
        }

        return eval(m[0]);
    } catch (e) {
        return '잘못된 수식이 입력되었습니다 ' + e;
    }
}

function num_to_unit(num) {
    num = parseFloat(num);
    const unit = { 0: '', 1: 'K', 2: 'M', 3: 'B', 4: 'T' };
    let index = 0;
    for (let i = 0; i < 4; i++) {
        if (num >= 1000) {
            num /= 1000;
        } else break;
        index += 1;
    }

    return num.toFixed(2) + unit[index];
}

function thousands_comma(num) {
    return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}

// gate에 특정 티커 정보를 출력합니다.
function gate_get_symbol_info(symbol) {
    try {
        symbol = symbol.toUpperCase();

        if (!symbol.includes('_')) {
            symbol += '_USDT';
        }

        host = 'https://api.gateio.ws/api/v4/spot/tickers';

        let json = org.jsoup.Jsoup.connect(host + '?' + 'currency_pair=' + symbol)
            .header('Accept', 'application/json')
            .header('Content-Type', 'application/json')
            .ignoreContentType(true)
            .execute()
            .body();

        json = JSON.parse(json)[0];
        let output = '😎GATE ' + json.currency_pair + '😎\n';
        output += '💰 : ' + json.last + ' (' + json.change_percentage + '%)\n';
        output += '📣 : ' + num_to_unit(json.quote_volume) + '\n';
        output += '📈 : ' + json.high_24h + '\n';
        output += '📉 : ' + json.low_24h;

        let rate = gate_get_symbol_rate(symbol);
        if (rate !== '') {
            output = output + '\n\n' + rate;
        }
        return output;
    } catch (e) {
        return '잘못된 티커 정보입니다. ' + e;
    }
}

function gate_get_symbol_rate(symbol) {
    try {
        host = 'https://api.gateio.ws/api/v4/spot/candlesticks';

        let json = org.jsoup.Jsoup.connect(host + '?' + 'currency_pair=' + symbol + '&limit=365' + '&interval=1d')
            .header('Accept', 'application/json')
            .header('Content-Type', 'application/json')
            .ignoreContentType(true)
            .execute()
            .body();

        json = JSON.parse(json);

        let answer = '🍕 수익률\n';

        const term = { '1W': 7, '1M': 30, '3M': 90, '6M': 180, '1Y': 365 };
        let cnt = 0;

        for (let key in term) {
            let days = term[key];
            if (cnt % 2 == 0) {
                end = ' ';
            } else {
                end = '\n';
            }

            if (json.length >= days) {
                let rate = ((json[json.length - 1][2] / json[json.length - days][2] - 1.0) * 100.0).toFixed(2);
                answer += key + ': ' + rate + '%' + end;
            } else {
                answer += key + ': -' + end;
            }

            cnt += 1;
        }

        return answer;
    } catch (e) {
        return '';
    }
}

// upbit에 특정 티커 정보를 출력합니다.
function upbit_get_symbol_info(symbol) {
    try {
        symbol = symbol.toUpperCase();

        if (!symbol.includes('-')) {
            symbol = 'KRW-' + symbol;
        }

        host = 'https://api.upbit.com/v1/candles/days';

        let json = org.jsoup.Jsoup.connect(host + '?' + 'market=' + symbol + '&count=365')
            .header('Accept', 'application/json')
            .header('Content-Type', 'application/json')
            .ignoreContentType(true)
            .execute()
            .body();

        json = JSON.parse(json);

        let output = '🤔UPBIT ' + json[0].market + '🤔\n';
        output += '💰 : ' + thousands_comma(json[0].trade_price) + '₩ (' + ((json[0].trade_price / json[0].opening_price - 1.0) * 100.0).toFixed(2) + '%)\n';
        output += '📣 : ' + num_to_unit(json[0].candle_acc_trade_volume) + '\n';
        output += '📈 : ' + thousands_comma(json[0].high_price) + '\n';
        output += '📉 : ' + thousands_comma(json[0].low_price) + '\n\n';

        output += '🍕 수익률\n';
        const term = { '1W': 7, '1M': 30, '3M': 90, '6M': 180, '1Y': 365 };
        let cnt = 0;

        for (let key in term) {
            let days = term[key];
            if (cnt % 2 == 0) {
                end = ' ';
            } else {
                end = '\n';
            }

            if (json.length > days) {
                let rate = ((json[0].trade_price / json[days].trade_price - 1.0) * 100.0).toFixed(2);
                output += key + ': ' + rate + '%' + end;
            } else {
                output += key + ': -' + end;
            }

            cnt += 1;
        }

        return output;
    } catch (e) {
        return '잘못된 티커 정보입니다. ' + e;
    }
}

function binance_get_symbol_info(symbol, affix) {
    try {
        symbol = symbol.toUpperCase();
        symbol += affix;

        if (!symbol.includes('USDT') && affix == '') {
            symbol += 'USDT';
        }

        host = 'https://api.binance.com/api/v3/klines';

        let json = org.jsoup.Jsoup.connect(host + '?' + 'symbol=' + symbol + '&interval=1d&limit=365')
            .header('Accept', 'application/json')
            .header('Content-Type', 'application/json')
            .ignoreContentType(true)
            .execute()
            .body();

        json = JSON.parse(json);

        output = '😎BINANCE ' + symbol + '😎\n';
        output += '💰 : ' + json[json.length - 1][4] + ' (' + ((json[json.length - 1][4] / json[json.length - 1][1] - 1.0) * 100.0).toFixed(2) + '%)\n';
        output += '📣 : ' + num_to_unit(json[json.length - 1][5]) + '\n';
        output += '📈 : ' + json[json.length - 1][2] + '\n';
        output += '📉 : ' + json[json.length - 1][3] + '\n\n';

        output += '🍕 수익률\n';
        const term = { '1W': 7, '1M': 30, '3M': 90, '6M': 180, '1Y': 365 };
        let cnt = 0;

        for (let key in term) {
            let days = term[key];
            if (cnt % 2 == 0) {
                end = ' ';
            } else {
                end = '\n';
            }

            if (json.length >= days) {
                let rate = ((json[json.length - 1][4] / json[json.length - days][4] - 1.0) * 100.0).toFixed(2);
                output += key + ': ' + rate + '%' + end;
            } else {
                output += key + ': -' + end;
            }

            cnt += 1;
        }

        return output;
    } catch (e) {
        return '잘못된 티커 정보입니다. ' + e;
    }
}

 

실행 결과는 다음과 같다

긴 메세지 도배를 막기 위해 작게 표시함
binance 명령어
gate 명령어
upbit 명령어
계산기 명령어
잘못된 티커 입력시

반응형