상세 컨텐츠

본문 제목

라즈베리파이로 내 주식 모니터링 하기

라즈베리파이

by mingoon.com 2023. 5. 12. 11:04

본문

반응형

모 커뮤니티에서 본 라즈베리파이용 터치 스크린을 몇달전에 구매했었는데, 설치만 해보고 어디에 쓸까.. 고민하다가 묵혀놓던 중이었다.

https://shop.pimoroni.com/products/hyperpixel-4?variant=12569485443155 

 

HyperPixel 4.0 - Hi-Res Display for Raspberry Pi - Touch

 

shop.pimoroni.com

최근 주식에 재미를 붙여 시도때도 없이 토스를 들여다 보던 중에, 놀고 있는 파이와 터치 스크린을 활용해 내가 산 주식들을 모아보자는 생각을 하게 됐다.

 

앱을 만들기 전에 케이스를 뭘 씌울까 찾아봤는데, 영국에서 만든 스크린인데다 많이 팔리는 제품이 아니다보니 케이스를 파는 곳이 별로 없었다. 검색을 며칠 해보니 3D 도면만 수두룩하게 나오던 중, 엣시에서 이 도면을 3D 프린팅해 판매하는 샵을 발견하고 곧 주문.

 

품질이 썩 맘에 들진 않지만 그럭저럭 나쁘지 않았고, 이제 앱 개발에 착수해본다.

 

일단 앱은 이전에 장애인용 키오스크앱 만들때 사용한 electron.js로 개발한다.

웹 기반의 크로스 플랫폼 지원 프레임워크로 나같은 웹개발자에겐 손쉽게 데스크톱 앱을 만들 수 있다.

 

완성된 화면 캡쳐

 

개인 사용 용도이므로, 서버는 따로 두지 않고 github에 다음과 같이 JSON 형태로 파일을 만들고 GitHup Pages로 등록하여 웹에서 접근이 가능하도록 한다. 변경이 필요한 경우 github에 들어가 직접 편집하면 되니 그리 번거롭진 않다.

[
	{
		"name": "나무가",
		"code": "190510",
		"image": "https://www.namuga.co.kr/images/common/logo.png",
		"bought": 16060
	},
	{
		"name": "넥스트칩",
		"code": "396270",
		"image": "https://www.nextchip.com/images/common/footer_logo.png",
		"bought": 11521
	},
	{
		"name": "LG엔솔",
		"code": "373220",
		"image": "https://www.lgensol.com/assets/img/common/logo.svg",
		"bought": 582500
	},
	{
		"name": "삼성전자",
		"code": "005930",
		"image": "https://www.samsung.com/sec/static/_images/common/logo_samsung_black.svg",
		"bought": 65416
	},
	{
		"name": "에코프로비엠",
		"code": "247540",
		"image": "https://www.ecoprobm.co.kr/images/korean/contents/ecoprobm.png",
		"bought": 261250
	}

]

종목 이름(name), 종목 코드(code), 이미지 (image), 내가 구매한 가격 (bought)을 Array로 저장했다.

 

다음은 실시간 주식 정보를 어디서 가져오는지 검색해 본다.

대부분의 증권사들이 제공하는 API는 DLL, OCX 등의 윈도우 기반 모듈을 제공해 라즈베리파이에선 사용이 불가능했고, 한국투자증권이 OAuth 인증 기반 REST API를 제공하고 있어 이를 사용하기로 했다.

 

https://apiportal.koreainvestment.com/apiservice

 

KIS Developers

WEBSOCKET 실시간 (웹소켓) 접속키 발급[실시간-000] 기본정보 Method POST 실전 Domain https://openapi.koreainvestment.com:9443 모의 Domain https://openapivts.koreainvestment.com:29443 URL /oauth2/Approval Format JSON Content-Type  개

apiportal.koreainvestment.com

사용할 API는 '실시간시세 (국내주식) - 국내주식 실시간 체결가'로 웹소켓으로 데이터를 받아와야 한다.

이를 위해 'OAuth 인증 - 실시간 (웹소켓) 접속키 발급'을 먼저 요청한다.

function getApprovalKeyBeforeSocket() {
  var params = {
    grant_type: "client_credentials",
    appkey: gAppKey,
    secretkey: gAppSecret,
  };
  $.ajax({
    type: "POST",
    url: "https://openapi.koreainvestment.com:9443/oauth2/Approval",
    async: true,
    dataType: "JSON",
    data: JSON.stringify(params),
    contentType: "application/json; utf-8",
    success: function (data) {
      approvalKey = data.approval_key;
      createWebsocket();
    },
    error: function (e) {
      console.log(e);
    },
  });
}

응답은 다음과 같다.

{
    "approval_key": "a2585daf-8c09-4587-9fce-8ab893XXXXX"
}

받은 approval_key로 웹소켓을 생성한다.

var approvalKey;
var pingCount = 0;
var shutdownReserved = false;

function createWebsocket() {
  var url = "ws://ops.koreainvestment.com:21000";
  var w = new WebSocket(url);

  w.onopen = function () {
    console.log("Connection OK");
    // 신규 등록된 종목 코드가 있으면 실시간 체결가 등록 요청
    requestCodeRegist(w);
  };

  w.onclose = function (e) {
    console.log("Connection Closed");
  };

  w.onmessage = function (e) {
    var data = e.data;

    // 신규 등록된 종목 코드가 있으면 실시간 체결가 등록 요청
    requestCodeRegist(w);
    // 삭제된 종목 코드가 있으면 실시간 체결가 해제 요청
    requestCodeRemove(w);

    // 수신 상태를 나타내는 아이콘의 Blink 처리
    blink();
    if (data.startsWith("0") || data.startsWith("1")) {
      // 0, 1로 시작하는 문자열 응답은 실시간 체결가 데이터
      const dataArr = data.split("|");
      const resultCode = parseInt(dataArr[0]);
      const dataType = dataArr[1];
      const dataCount = parseInt(dataArr[2]);
      if (resultCode == 0) {
        // 정상 응답에 대한 결과 화면 반영
        stocksPurchase(dataType, dataCount, dataArr[3]);
      } else {
        console.log("Unknown Response '" + data + "'");
      }
      pingCount = 0;
    } else {
      // 그 외 JSON 응답은 등록/해제 요청에 대한 응답이거나 실시간 데이터가 없는 경우에 대한 PINGPONG 응답
      const response = JSON.parse(data);
      if (response.header.tr_id == "PINGPONG") {
        // PINGPONG 응답
        w.send(data);
        // PINGPONG 횟수 저장
        pingCount++;
      } else if (response.header.tr_id == "H0STCNT0") {
        // 등록/해제 요청에 대한 응답
        // 실시간 체결가 등록 오류 시 재시도
        if (response.body.msg1.includes("ERROR")) {
          var msg =
            '{"header":{"approval_key": "' +
            approvalKey +
            '","custtype":"P","tr_type":"1","content-type":"utf-8"},"body":{"input":{"tr_id":"H0STCNT0","tr_key":"' +
            response.header.tr_key +
            '"}}}';
          w.send(msg);
        }
        pingCount = 0;
      } else {
        console.log("Unknown Response '" + data + "'");
      }
    }

    // PINGPONG 휫수가 5 초과인 경우 장 종료로 보고 앱 종료
    if (pingCount > 5) {
      w.close();
      if (!shutdownReserved) {
        shutdownReserved = true;
        $(".toast-ready-shutdown").show();
        setTimer(".toast-ready-shutdown .time", 60);
        setTimeout(shutdownSystem, 1000 * 60); // 1분후 종료
      }
    }
  };

  w.onerror = function (e) {
    console.log(e);
  };
}

function requestCodeRegist(ws) {
  if (companyCodesRegist.length > 0) {
    console.log("### companyCodesRegist", companyCodesRegist);
    for (i = 0; i < companyCodesRegist.length; i++) {
      var msg =
        '{"header":{"approval_key": "' +
        approvalKey +
        '","custtype":"P","tr_type":"1","content-type":"utf-8"},"body":{"input":{"tr_id":"H0STCNT0","tr_key":"' +
        companyCodesRegist[i] +
        '"}}}';
      ws.send(msg);
    }
    companyCodesRegist = [];
  }
}

function requestCodeRemove(ws) {
  if (companyCodesRemove.length > 0) {
    console.log("### companyCodesRemove", companyCodesRemove);
    for (i = 0; i < companyCodesRemove.length; i++) {
      var msg =
        '{"header":{"approval_key": "' +
        approvalKey +
        '","custtype":"P","tr_type":"2","content-type":"utf-8"},"body":{"input":{"tr_id":"H0STCNT0","tr_key":"' +
        companyCodesRemove[i] +
        '"}}}';
      ws.send(msg);
    }
    companyCodesRemove = [];
  }
}

기능 구현을 마치고, 라즈베리파이에서 실행이 가능하도록 package.json에 아래 내용을 추가해 준다.

{
  ...
  "build": {
    "linux": {
      "target": {
        "target": "deb",
        "arch": "armv7l"
      },
      "category": "Utility",
      "executableName": "minimon",
      "artifactName": "${productName}-${version}.${ext}"
    },
    "deb": {
      "fpm": [
        "--architecture",
        "armhf"
      ]
    }
  }
}

그리고 빌드!

➜  minimon git:(vertical) ✗ npm run build:linux64

> minimon@1.0.0 build:linux64
> electron-builder --linux --x64

  • electron-builder  version=23.6.0 os=22.4.0
  • loaded configuration  file=package.json ("build" field)
  • description is missed in the package.json  appPackageFile=/Users/jeongminkim/git/minimon/package.json
  • writing effective config  file=dist/builder-effective-config.yaml
  • rebuilding native dependencies  dependencies=bufferutil@4.0.7, utf-8-validate@5.0.10 platform=linux arch=x64
  • packaging       platform=linux arch=x64 electron=18.3.15 appOutDir=dist/linux-unpacked
  • downloading     url=https://github.com/electron/electron/releases/download/v18.3.15/electron-v18.3.15-linux-x64.zip size=83 MB parts=8
  • downloaded      url=https://github.com/electron/electron/releases/download/v18.3.15/electron-v18.3.15-linux-x64.zip duration=10.838s
  • building        target=snap arch=x64 file=dist/minimon-1.0.0.snap
  • building        target=AppImage arch=x64 file=dist/minimon-1.0.0.AppImage
  • default Electron icon is used  reason=application icon is not set
  • rebuilding native dependencies  dependencies=bufferutil@4.0.7, utf-8-validate@5.0.10 platform=linux arch=armv7l
  • packaging       platform=linux arch=armv7l electron=18.3.15 appOutDir=dist/linux-armv7l-unpacked
  • downloading     url=https://github.com/electron-userland/electron-builder-binaries/releases/download/appimage-12.0.1/appimage-12.0.1.7z size=1.6 MB parts=1
  • downloading     url=https://github.com/electron-userland/electron-builder-binaries/releases/download/snap-template-4.0-2/snap-template-electron-4.0-2-amd64.tar.7z size=1.5 MB parts=1
  • downloading     url=https://github.com/electron/electron/releases/download/v18.3.15/electron-v18.3.15-linux-armv7l.zip size=73 MB parts=8
  • downloaded      url=https://github.com/electron-userland/electron-builder-binaries/releases/download/appimage-12.0.1/appimage-12.0.1.7z duration=1.366s
  • downloaded      url=https://github.com/electron-userland/electron-builder-binaries/releases/download/snap-template-4.0-2/snap-template-electron-4.0-2-amd64.tar.7z duration=2.329s
  • downloaded      url=https://github.com/electron/electron/releases/download/v18.3.15/electron-v18.3.15-linux-armv7l.zip duration=9.99s
  • building        target=deb arch=armv7l file=dist/minimon-1.0.0.deb
  • default Electron icon is used  reason=application icon is not set
  • downloading     url=https://github.com/electron-userland/electron-builder-binaries/releases/download/linux-tools-mac-10.12.3/linux-tools-mac-10.12.3.7z size=520 kB parts=1
  • downloaded      url=https://github.com/electron-userland/electron-builder-binaries/releases/download/linux-tools-mac-10.12.3/linux-tools-mac-10.12.3.7z duration=1.408s
  • downloading     url=https://github.com/electron-userland/electron-builder-binaries/releases/download/fpm-1.9.3-20150715-2.2.2-mac/fpm-1.9.3-20150715-2.2.2-mac.7z size=5.4 MB parts=1
  • downloaded      url=https://github.com/electron-userland/electron-builder-binaries/releases/download/fpm-1.9.3-20150715-2.2.2-mac/fpm-1.9.3-20150715-2.2.2-mac.7z duration=1.939s

dist 디렉토리에 다음과 같이 deb 파일이 생겼다.

➜  dist git:(vertical) ll
total 468360
-rw-r--r--   1 jeongminkim  staff   1.3K  5 12 12:00 builder-debug.yml
-rw-r--r--   1 jeongminkim  staff   279B  5 12 11:59 builder-effective-config.yaml
-rw-r--r--   1 jeongminkim  staff   364B  5 12 12:00 latest-linux.yml
drwxr-xr-x  22 jeongminkim  staff   704B  5 12 11:59 linux-armv7l-unpacked
drwxr-xr-x  22 jeongminkim  staff   704B  5 12 11:59 linux-unpacked
-rwxr-xr-x   1 jeongminkim  staff    82M  5 12 11:59 minimon-1.0.0.AppImage
-rw-r--r--   1 jeongminkim  staff    52M  5 12 12:00 minimon-1.0.0.deb
-rw-r--r--   1 jeongminkim  staff    70M  5 12 11:59 minimon-1.0.0.snap

이제 라즈베리파이에 .deb 파일을 올려주고 설치한다.

pi@raspberrypi:~ $ sudo dpkg -i minimon-1.0.0.deb
(데이터베이스 읽는중 ...현재 107244개의 파일과 디렉터리가 설치되어 있습니다.)
Preparing to unpack minimon-1.0.0.deb ...
Unpacking minimon (1.0.0) over (1.0.0) ...
minimon (1.0.0) 설정하는 중입니다 ...
Processing triggers for hicolor-icon-theme (0.17-2) ...
Processing triggers for gnome-menus (3.36.0-1) ...
Processing triggers for mailcap (3.69) ...
Processing triggers for desktop-file-utils (0.26-1) ...

실행해보면 다음과 같이 화면이 뜬다. 종목별 화면은 slick.js로 10초마다 슬라이드되도록 설정했다.

그리고 다음과 같이 3개의 쉘 스크립트를 작성했다.

 

on.sh : 스크린 백라이트 켬

#!/bin/sh
echo 1 > /sys/class/backlight/rpi_backlight/brightness

off.sh : 스크린 백라이트 끔

#!/bin/sh
echo 0 > /sys/class/backlight/rpi_backlight/brightness
pkill -ef -9 unclutter

turnOnMinimon.sh : 앱 실행

#!/bin/sh
export XAUTHORITY=/home/pi/.Xauthority
export DISPLAY=:0.0
# 마우스 포인터 비활성화
unclutter -idle 0 &
# 앱 실행
minimon

사무실에 두고 보려고, 출퇴근 및 점심시간에 맞춰 crontab에 설정한다.

pi@raspberrypi:~/scripts $ crontab -l
0 9 * * 1-6 /home/pi/scripts/turnOnMinimon.sh
15 12 * * 1-6 sudo /home/pi/scripts/off.sh
30 15 * * 1-6 sudo /home/pi/scripts/off.sh
10 9 * * 1-6 sudo /home/pi/scripts/on.sh
0 13 * * 1-6 sudo /home/pi/scripts/on.sh

끝!

댓글 영역