Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/#10 실시간 데이터 수집 추가, pingpong 추가 #208

Merged
merged 8 commits into from
Nov 21, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,4 @@ report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json

# vscode setting
.vscode

4 changes: 4 additions & 0 deletions packages/backend/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,7 @@ pids

# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json

# backup file
.backup
.bak
4 changes: 2 additions & 2 deletions packages/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,11 @@
"@nestjs/core": "^10.0.0",
"@nestjs/passport": "^10.0.3",
"@nestjs/platform-express": "^10.0.0",
"@nestjs/platform-socket.io": "^10.4.7",
"@nestjs/platform-socket.io": "^10.4.8",
"@nestjs/schedule": "^4.1.1",
"@nestjs/swagger": "^8.0.5",
"@nestjs/typeorm": "^10.0.2",
"@nestjs/websockets": "^10.4.7",
"@nestjs/websockets": "^10.4.8",
"axios": "^1.7.7",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { UseFilters } from '@nestjs/common';
import { Injectable, UseFilters } from '@nestjs/common';
import { Cron } from '@nestjs/schedule';
import { Between, DataSource } from 'typeorm';
import { openApiConfig } from '../config/openapi.config';
Expand All @@ -18,6 +18,7 @@ import { Stock } from '@/stock/domain/stock.entity';
import { StockDaily } from '@/stock/domain/stockData.entity';
import { StockDetail } from '@/stock/domain/stockDetail.entity';

@Injectable()
export class OpenapiDetailData {
private readonly financialUrl: string =
'/uapi/domestic-stock/v1/finance/financial-ratio';
Expand Down
100 changes: 100 additions & 0 deletions packages/backend/src/scraper/openapi/api/openapiLiveData.api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { Inject } from '@nestjs/common';
import { EntityManager } from 'typeorm';
import { Logger } from 'winston';
import { openApiConfig } from '../config/openapi.config';
import { StockData, parseStockData } from '../type/openapiLiveData.type';
import { decryptAES256 } from '../util/openapiUtil.api';
import { openApiToken } from './openapiToken.api';
import { KospiStock } from '@/stock/domain/kospiStock.entity';
import { StockLiveData } from '@/stock/domain/stockLiveData.entity';

export class OpenapiLiveData {
public readonly TR_ID: string = 'H0STCNT0';
private readonly WEBSOCKET_MAX: number = 40;
constructor(
@Inject('winston') private readonly logger: Logger,
private readonly manager: EntityManager,
) {}

public async getMessage(): Promise<string[]> {
const kospi = await this.getKospiStockId();
const config = openApiToken.configs;
const configLength = config.length;
const ret: string[] = [];

for (let i = 0; i < configLength; i++) {
const stocks = kospi.splice(
i * this.WEBSOCKET_MAX,
(i + 1) * this.WEBSOCKET_MAX,
);
for (const stock of stocks) {
ret.push(this.convertObjectToMessage(config[i], stock.id!));
}
}

return ret;
}

private convertObjectToMessage(
config: typeof openApiConfig,
stockId: string,
): string {
const message = {
header: {
approval_key: config.STOCK_WEBSOCKET_KEY!,
custtype: 'P',
tr_type: '1',
'content-type': 'utf-8',
},
body: {
input: {
tr_id: this.TR_ID,
tr_key: stockId,
},
},
};
return JSON.stringify(message);
}

private async getKospiStockId() {
const kospi = await this.manager.find(KospiStock);
return kospi;
}

private async saveLiveData(data: StockLiveData) {
await this.manager.save(StockLiveData, data);
}

private convertLiveData(message: string[]): StockLiveData {
const stockData: StockData = parseStockData(message);
const stockLiveData = new StockLiveData();
stockLiveData.currentPrice = parseFloat(stockData.STCK_PRPR);
stockLiveData.changeRate = parseFloat(stockData.PRDY_CTRT);
stockLiveData.volume = parseInt(stockData.CNTG_VOL);
stockLiveData.high = parseFloat(stockData.STCK_HGPR);
stockLiveData.low = parseFloat(stockData.STCK_LWPR);
stockLiveData.open = parseFloat(stockData.STCK_OPRC);
stockLiveData.previousClose = parseFloat(stockData.WGHN_AVRG_STCK_PRC);
stockLiveData.updatedAt = new Date();

return stockLiveData;
}

public async output(message: Buffer, iv?: string, key?: string) {
const parsed = message.toString().split('|');
if (parsed.length > 0) {
if (parsed[0] == '1' && iv && key)
parsed[4] = decryptAES256(parsed[4], iv, key);
if (parsed[1] !== this.TR_ID) return;
const stockData = parsed[4].split('^');
const length = stockData.length / parseInt(parsed[3]);
const size = parseInt(parsed[2]);
const i = 0;
while (i < size) {
const data = stockData.splice(i * length, (i + 1) * length);
const liveData = this.convertLiveData(data);
this.saveLiveData(liveData);
}
}
}
}
21 changes: 14 additions & 7 deletions packages/backend/src/scraper/openapi/api/openapiToken.api.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Inject } from '@nestjs/common';
import { Inject, UseFilters } from '@nestjs/common';
import { Cron } from '@nestjs/schedule';
import { Logger } from 'winston';
import { openApiConfig } from '../config/openapi.config';
import { OpenapiExceptionFilter } from '../Decorator/openapiException.filter';
import { OpenapiException } from '../util/openapiCustom.error';
import { postOpenApi } from '../util/openapiUtil.api';
import { logger } from '@/configs/logger.config';
Expand Down Expand Up @@ -34,6 +35,7 @@ class OpenapiTokenApi {
return this.config;
}

@UseFilters(OpenapiExceptionFilter)
private async initAuthenValue() {
const delay = 60000;
const delayMinute = delay / 1000 / 60;
Expand All @@ -50,9 +52,10 @@ class OpenapiTokenApi {
this.logger.warn(
`Request failed. Retrying in ${delayMinute} minute...`,
);
await new Promise((resolve) => setTimeout(resolve, delay));
await this.initAccessToken();
await this.initWebSocketKey();
setTimeout(async () => {
await this.initAccessToken();
await this.initWebSocketKey();
}, delay);
}
}
}
Expand All @@ -70,9 +73,13 @@ class OpenapiTokenApi {

@Cron('50 0 * * 1-5')
private async initWebSocketKey() {
this.config.forEach(async (val) => {
val.STOCK_WEBSOCKET_KEY = await this.getWebSocketKey(val)!;
});
const updatedConfig = await Promise.all(
this.config.map(async (val) => {
val.STOCK_WEBSOCKET_KEY = await this.getWebSocketKey(val)!;
return val;
}),
);
this.config = updatedConfig;
}

private async getToken(config: typeof openApiConfig): Promise<string> {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { OpenapiDetailData } from './api/openapiDetailData.api';
import { OpenapiLiveData } from './api/openapiLiveData.api';
import { OpenapiMinuteData } from './api/openapiMinuteData.api';
import { OpenapiPeriodData } from './api/openapiPeriodData.api';
import { OpenapiScraperService } from './openapi-scraper.service';
import { WebsocketClient } from './websocketClient.service';
import { Stock } from '@/stock/domain/stock.entity';
import {
StockDaily,
Expand Down Expand Up @@ -34,6 +36,8 @@ import { StockLiveData } from '@/stock/domain/stockLiveData.entity';
OpenapiMinuteData,
OpenapiDetailData,
OpenapiScraperService,
OpenapiLiveData,
WebsocketClient,
],
})
export class OpenapiScraperModule {}
152 changes: 152 additions & 0 deletions packages/backend/src/scraper/openapi/type/openapiLiveData.type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable max-lines-per-function */

export type StockData = {
MKSC_SHRN_ISCD: string; // 유가증권 단축 종목코드
STCK_CNTG_HOUR: string; // 주식 체결 시간
STCK_PRPR: string; // 주식 현재가
PRDY_VRSS_SIGN: string; // 전일 대비 부호
PRDY_VRSS: string; // 전일 대비
PRDY_CTRT: string; // 전일 대비율
WGHN_AVRG_STCK_PRC: string; // 가중 평균 주식 가격
STCK_OPRC: string; // 주식 시가
STCK_HGPR: string; // 주식 최고가
STCK_LWPR: string; // 주식 최저가
ASKP1: string; // 매도호가1
BIDP1: string; // 매수호가1
CNTG_VOL: string; // 체결 거래량
ACML_VOL: string; // 누적 거래량
ACML_TR_PBMN: string; // 누적 거래 대금
SELN_CNTG_CSNU: string; // 매도 체결 건수
SHNU_CNTG_CSNU: string; // 매수 체결 건수
NTBY_CNTG_CSNU: string; // 순매수 체결 건수
CTTR: string; // 체결강도
SELN_CNTG_SMTN: string; // 총 매도 수량
SHNU_CNTG_SMTN: string; // 총 매수 수량
CCLD_DVSN: string; // 체결구분
SHNU_RATE: string; // 매수비율
PRDY_VOL_VRSS_ACML_VOL_RATE: string; // 전일 거래량 대비 등락율
OPRC_HOUR: string; // 시가 시간
OPRC_VRSS_PRPR_SIGN: string; // 시가대비구분
OPRC_VRSS_PRPR: string; // 시가대비
HGPR_HOUR: string; // 최고가 시간
HGPR_VRSS_PRPR_SIGN: string; // 고가대비구분
HGPR_VRSS_PRPR: string; // 고가대비
LWPR_HOUR: string; // 최저가 시간
LWPR_VRSS_PRPR_SIGN: string; // 저가대비구분
LWPR_VRSS_PRPR: string; // 저가대비
BSOP_DATE: string; // 영업 일자
NEW_MKOP_CLS_CODE: string; // 신 장운영 구분 코드
TRHT_YN: string; // 거래정지 여부
ASKP_RSQN1: string; // 매도호가 잔량1
BIDP_RSQN1: string; // 매수호가 잔량1
TOTAL_ASKP_RSQN: string; // 총 매도호가 잔량
TOTAL_BIDP_RSQN: string; // 총 매수호가 잔량
VOL_TNRT: string; // 거래량 회전율
PRDY_SMNS_HOUR_ACML_VOL: string; // 전일 동시간 누적 거래량
PRDY_SMNS_HOUR_ACML_VOL_RATE: string; // 전일 동시간 누적 거래량 비율
HOUR_CLS_CODE: string; // 시간 구분 코드
MRKT_TRTM_CLS_CODE: string; // 임의종료구분코드
VI_STND_PRC: string; // 정적VI발동기준가
};

export function parseStockData(message: string[]): StockData {
return {
MKSC_SHRN_ISCD: message[0],
STCK_CNTG_HOUR: message[1],
STCK_PRPR: message[2],
PRDY_VRSS_SIGN: message[3],
PRDY_VRSS: message[4],
PRDY_CTRT: message[5],
WGHN_AVRG_STCK_PRC: message[6],
STCK_OPRC: message[7],
STCK_HGPR: message[8],
STCK_LWPR: message[9],
ASKP1: message[10],
BIDP1: message[11],
CNTG_VOL: message[12],
ACML_VOL: message[13],
ACML_TR_PBMN: message[14],
SELN_CNTG_CSNU: message[15],
SHNU_CNTG_CSNU: message[16],
NTBY_CNTG_CSNU: message[17],
CTTR: message[18],
SELN_CNTG_SMTN: message[19],
SHNU_CNTG_SMTN: message[20],
CCLD_DVSN: message[21],
SHNU_RATE: message[22],
PRDY_VOL_VRSS_ACML_VOL_RATE: message[23],
OPRC_HOUR: message[24],
OPRC_VRSS_PRPR_SIGN: message[25],
OPRC_VRSS_PRPR: message[26],
HGPR_HOUR: message[27],
HGPR_VRSS_PRPR_SIGN: message[28],
HGPR_VRSS_PRPR: message[29],
LWPR_HOUR: message[30],
LWPR_VRSS_PRPR_SIGN: message[31],
LWPR_VRSS_PRPR: message[32],
BSOP_DATE: message[33],
NEW_MKOP_CLS_CODE: message[34],
TRHT_YN: message[35],
ASKP_RSQN1: message[36],
BIDP_RSQN1: message[37],
TOTAL_ASKP_RSQN: message[38],
TOTAL_BIDP_RSQN: message[39],
VOL_TNRT: message[40],
PRDY_SMNS_HOUR_ACML_VOL: message[41],
PRDY_SMNS_HOUR_ACML_VOL_RATE: message[42],
HOUR_CLS_CODE: message[43],
MRKT_TRTM_CLS_CODE: message[44],
VI_STND_PRC: message[45],
};
}

export type OpenApiMessage = {
header: {
approval_key: string;
custtype: string;
tr_type: string;
'content-type': string;
};
body: {
input: {
tr_id: string;
tr_key: string;
};
};
};

export type MessageResponse = {
header: {
tr_id: string;
tr_key: string;
encrypt: string;
};
body: {
rt_cd: string;
msg_cd: string;
msg1: string;
output?: {
iv: string;
key: string;
};
};
};

export function isMessageResponse(data: any): data is MessageResponse {
return (
typeof data === 'object' &&
data !== null &&
typeof data.header === 'object' &&
data.header !== null &&
typeof data.header.tr_id === 'object' &&
typeof data.header.tr_key === 'object' &&
typeof data.header.encrypt === 'object' &&
typeof data.body === 'object' &&
data.body !== null &&
typeof data.body.rt_cd === 'object' &&
typeof data.body.msg_cd === 'object' &&
typeof data.body.msg1 === 'object' &&
typeof data.body.output === 'object'
);
}
16 changes: 16 additions & 0 deletions packages/backend/src/scraper/openapi/util/openapiUtil.api.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/* eslint-disable @typescript-eslint/no-explicit-any*/
import * as crypto from 'crypto';
import { HttpStatus } from '@nestjs/common';
import axios from 'axios';
import { openApiConfig } from '../config/openapi.config';
Expand Down Expand Up @@ -77,11 +78,26 @@ const getCurrentTime = () => {
const seconds = String(now.getSeconds()).padStart(2, '0');
return `${hours}${minutes}${seconds}`;
};
const decryptAES256 = (
encryptedText: string,
key: string,
iv: string,
): string => {
const decipher = crypto.createDecipheriv(
'aes-256-cbc',
Buffer.from(key, 'hex'),
Buffer.from(iv, 'hex'),
);
let decrypted = decipher.update(encryptedText, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
};

export {
postOpenApi,
getOpenApi,
getTodayDate,
getPreviousDate,
getCurrentTime,
decryptAES256,
};
Loading