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 all 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
169 changes: 133 additions & 36 deletions packages/backend/src/scraper/openapi/api/openapiDetailData.api.ts
Original file line number Diff line number Diff line change
@@ -1,53 +1,92 @@
import { UseFilters } from '@nestjs/common';
import { Inject, Injectable, UseFilters } from '@nestjs/common';
import { Cron } from '@nestjs/schedule';
import { Between, DataSource } from 'typeorm';
import { Logger } from 'winston';
import { openApiConfig } from '../config/openapi.config';
import { OpenapiExceptionFilter } from '../Decorator/openapiException.filter';
import {
DetailDataQuery,
FinancialData,
isFinancialData,
FinancialRatio,
isFinancialRatioData,
isProductDetail,
ProductDetail,
StockDetailQuery,
} from '../type/openapiDetailData.type';
import { TR_IDS } from '../type/openapiUtil.type';
import { getOpenApi } from '../util/openapiUtil.api';
import { openApiToken } from './openapiToken.api';
import { KospiStock } from '@/stock/domain/kospiStock.entity';
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';
private readonly defaultUrl: string =
private readonly productUrl: string =
'/uapi/domestic-stock/v1/quotations/search-stock-info';
private readonly incomeUrl: string =
'/uapi/domestic-stock/v1/finance/income-statement';
private readonly intervals = 100;
private readonly config: (typeof openApiConfig)[] = openApiToken.configs;
constructor(private readonly datasource: DataSource) {}
private readonly intervals = 1000;
constructor(
private readonly datasource: DataSource,
@Inject('winston') private readonly logger: Logger,
) {
setTimeout(() => this.getDetailData(), 5000);
}

@Cron('0 8 * * 1-5')
@UseFilters(OpenapiExceptionFilter)
public async getDetailData() {
if (process.env.NODE_ENV !== 'production') return;
//if (process.env.NODE_ENV !== 'production') return;
const entityManager = this.datasource.manager;
const stocks = await entityManager.find(Stock);
const configCount = this.config.length;
const configCount = openApiToken.configs.length;
const chunkSize = Math.ceil(stocks.length / configCount);

for (let i = 0; i < configCount; i++) {
this.logger.info(openApiToken.configs[i]);
const chunk = stocks.slice(i * chunkSize, (i + 1) * chunkSize);
this.getDetailDataChunk(chunk, this.config[i]);
this.getDetailDataChunk(chunk, openApiToken.configs[i]);
}
}

private async saveDetailData(stockDetail: StockDetail) {
const manager = this.datasource.manager;
const entity = StockDetail;
manager.save(entity, stockDetail);
const existingStockDetail = await manager.findOne(entity, {
where: {
stock: { id: stockDetail.stock.id },
},
});
if (existingStockDetail) {
manager.update(
entity,
{ stock: { id: stockDetail.stock.id } },
stockDetail,
);
} else {
manager.save(entity, stockDetail);
}
}

private async saveKospiData(stockDetail: KospiStock) {
const manager = this.datasource.manager;
const entity = KospiStock;
const existingStockDetail = await manager.findOne(entity, {
where: {
stock: { id: stockDetail.stock.id },
},
});

if (existingStockDetail) {
manager.update(
entity,
{ stock: { id: stockDetail.stock.id } },
stockDetail,
);
} else {
manager.save(entity, stockDetail);
}
}

private async calPer(eps: number): Promise<number> {
Expand All @@ -58,10 +97,16 @@ export class OpenapiDetailData {
take: 1,
order: { createdAt: 'desc' },
});
const currentPrice = latestResult[0].close;
const per = currentPrice / eps;
// TODO : price가 없는 경우 0으로 리턴, 나중에 NaN과 대응되게 리턴
if (latestResult && latestResult[0] && latestResult[0].close) {
const currentPrice = latestResult[0].close;
const per = currentPrice / eps;

return per;
if (isNaN(per)) return 0;
else return per;
} else {
return 0;
}
}

private async calMarketCap(lstg: number) {
Expand All @@ -71,9 +116,17 @@ export class OpenapiDetailData {
take: 1,
order: { createdAt: 'desc' },
});
const currentPrice = latestResult[0].close;
const marketCap = lstg * currentPrice;
return marketCap;

// TODO : price가 없는 경우 0으로 리턴, 나중에 NaN과 대응되게 리턴
if (latestResult && latestResult[0] && latestResult[0].close) {
const currentPrice = latestResult[0].close;
const marketCap = lstg * currentPrice;

if (isNaN(marketCap)) return 0;
else return marketCap;
} else {
return 0;
}
}

private async get52WeeksLowHigh() {
Expand All @@ -92,48 +145,92 @@ export class OpenapiDetailData {
if (prev.high < cur.high) prev.high = cur.high;
return cur;
}, new StockDaily());
return { low: result.low, high: result.high };
let low = 0;
let high = 0;
if (result.low && !isNaN(result.low)) low = result.low;
if (result.high && !isNaN(result.high)) high = result.high;
return { low, high };
}

private async makeStockDetailObject(
output1: FinancialData,
output1: FinancialRatio,
output2: ProductDetail,
stockId: string,
): Promise<StockDetail> {
const result = new StockDetail();
result.stock = { id: stockId } as Stock;
result.marketCap =
(await this.calMarketCap(parseInt(output2.lstg_stqt))) + '';
result.eps = parseInt(output1.eps);
const { low, high } = await this.get52WeeksLowHigh();
result.low52w = low;
result.high52w = high;
result.eps = parseInt(output1.eps);
result.per = await this.calPer(parseInt(output1.eps));
const eps = parseInt(output1.eps);
if (isNaN(eps)) result.eps = 0;
else result.eps = eps;
const per = await this.calPer(eps);
if (isNaN(per)) result.per = 0;
else result.per = per;
result.updatedAt = new Date();
return result;
}

private async getDetailDataDelay(stock: Stock, conf: typeof openApiConfig) {
const dataQuery = this.getDetailDataQuery(stock.id!);
const defaultQuery = this.getDefaultDataQuery(stock.id!);
private async makeKospiStockObject(output: ProductDetail, stockId: string) {
const ret = new KospiStock();
ret.isKospi = output.kospi200_item_yn === 'Y' ? true : false;
ret.stock = { id: stockId } as Stock;
return ret;
}

private async getFinancialRatio(stock: Stock, conf: typeof openApiConfig) {
const dataQuery = this.getDetailDataQuery(stock.id!);
// 여기서 가져올 건 eps -> eps와 per 계산하자.
const output1 = await getOpenApi(
this.incomeUrl,
const response = await getOpenApi(
this.financialUrl,
conf,
dataQuery,
TR_IDS.FINANCIAL_DATA,
);
if (response.output) {
const output1 = response.output;
return output1[0];
}
}

private async getProductData(stock: Stock, conf: typeof openApiConfig) {
const defaultQuery = this.getFinancialDataQuery(stock.id!);

// 여기서 가져올 건 lstg-stqt - 상장주수를 바탕으로 시가총액 계산, kospi200_item_yn 코스피200종목여부 업데이트
const output2 = await getOpenApi(
this.defaultUrl,
const response = await getOpenApi(
this.productUrl,
conf,
defaultQuery,
TR_IDS.PRODUCTION_DETAIL,
);
if (response.output) {
const output2 = response.output;
return output2;
//return bufferToObject(output2);
}
}

private async getDetailDataDelay(stock: Stock, conf: typeof openApiConfig) {
const output1 = await this.getFinancialRatio(stock, conf);
const output2 = await this.getProductData(stock, conf);

if (isFinancialData(output1) && isProductDetail(output2)) {
const stockDetail = await this.makeStockDetailObject(output1, output2);
this.logger.info(JSON.stringify(output1));
this.logger.info(JSON.stringify(output2));
if (isFinancialRatioData(output1) && isProductDetail(output2)) {
const stockDetail = await this.makeStockDetailObject(
output1,
output2,
stock.id!,
);
this.saveDetailData(stockDetail);
const kospiStock = await this.makeKospiStockObject(output2, stock.id!);
this.saveKospiData(kospiStock);

this.logger.info(`${stock.id!} is saved`);
}
}

Expand All @@ -145,25 +242,25 @@ export class OpenapiDetailData {
}
}

private getDefaultDataQuery(
private getFinancialDataQuery(
stockId: string,
code: '300' | '301' | '302' | '306' = '300',
): StockDetailQuery {
return {
pdno: stockId,
code: code,
prdt_type_cd: code,
};
}

private getDetailDataQuery(
stockId: string,
divCode: 'J' = 'J',
classify: '0' | '1' = '1',
classify: '0' | '1' = '0',
): DetailDataQuery {
return {
fid_div_cls_code: classify,
fid_cond_mrkt_div_code: divCode,
fid_input_iscd: stockId,
fid_div_cls_code: classify,
};
}

Expand Down
Loading
Loading