Skip to content

Commit

Permalink
Merge pull request #30 from boostcampwm-2024/feature/api/topfive-#12
Browse files Browse the repository at this point in the history
[#12] 4.04 오늘의 상/하위 종목 API 구현
  • Loading branch information
uuuo3o authored Nov 7, 2024
2 parents a5d34f5 + 4375e5f commit 83d246a
Show file tree
Hide file tree
Showing 7 changed files with 295 additions and 0 deletions.
2 changes: 2 additions & 0 deletions BE/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule } from '@nestjs/config';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { TopfiveModule } from './stocks/topfive/topfive.module';

@Module({
imports: [
Expand All @@ -17,6 +18,7 @@ import { AppService } from './app.service';
entities: [],
synchronize: true,
}),
TopfiveModule,
],
controllers: [AppController],
providers: [AppService],
Expand Down
21 changes: 21 additions & 0 deletions BE/src/stocks/topfive/dto/stock-ranking-data.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { ApiProperty } from '@nestjs/swagger';

/**
* 등락률 API 요청 후 받은 응답값 정제용 DTO
*/
export class StockRankingDataDto {
@ApiProperty({ description: 'HTS 한글 종목명' })
hts_kor_isnm: string;

@ApiProperty({ description: '주식 현재가' })
stck_prpr: string;

@ApiProperty({ description: '전일 대비' })
prdy_vrss: string;

@ApiProperty({ description: '전일 대비 부호' })
prdy_vrss_sign: string;

@ApiProperty({ description: '전일 대비율' })
prdy_ctrt: string;
}
23 changes: 23 additions & 0 deletions BE/src/stocks/topfive/dto/stock-ranking-request.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* 등락률 API를 사용할 때 쿼리 파라미터로 사용할 요청값 DTO
*/
export class StockRankigRequestDto {
/**
* 조건 시장 분류 코드
* 'J' 주식
*/
fid_cond_mrkt_div_code: string;

/**
* 입력 종목 코드
* '0000' 전체 / '0001' 코스피
* '1001' 코스닥 / '2001' 코스피200
*/
fid_input_iscd: string;

/**
* 순위 정렬 구분 코드
* '0' 상승률 / '1' 하락률
*/
fid_rank_sort_cls_code: string;
}
13 changes: 13 additions & 0 deletions BE/src/stocks/topfive/dto/stock-ranking-response.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { ApiProperty } from '@nestjs/swagger';
import { StockRankingDataDto } from './stock-ranking-data.dto';

/**
* 순위 정렬 후 FE에 보낼 DTO
*/
export class StockRankingResponseDto {
@ApiProperty({ type: [StockRankingDataDto], description: '상승률 순위' })
high: StockRankingDataDto[];

@ApiProperty({ type: [StockRankingDataDto], description: '하락률 순위' })
low: StockRankingDataDto[];
}
28 changes: 28 additions & 0 deletions BE/src/stocks/topfive/topfive.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { ApiOperation, ApiQuery, ApiResponse } from '@nestjs/swagger';
import { Controller, Get, Query } from '@nestjs/common';
import { TopFiveService, MarketType } from './topfive.service';
import { StockRankingResponseDto } from './dto/stock-ranking-response.dto';

@Controller('/api/stocks')
export class TopfiveController {
constructor(private readonly topFiveService: TopFiveService) {}

@Get('topfive')
@ApiOperation({ summary: '오늘의 상/하위 종목 조회 API' })
@ApiQuery({
name: 'market',
enum: MarketType,
required: true,
description:
'주식 시장 구분\n' +
'ALL: 전체, KOSPI: 코스피, KOSDAQ: 코스닥, KOSPI200: 코스피200',
})
@ApiResponse({
status: 200,
description: '주식 시장별 순위 조회 성공',
type: StockRankingResponseDto,
})
async getTopFive(@Query('market') market: MarketType) {
return this.topFiveService.getMarketRanking(market);
}
}
11 changes: 11 additions & 0 deletions BE/src/stocks/topfive/topfive.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { TopfiveController } from './topfive.controller';
import { TopFiveService } from './topfive.service';

@Module({
imports: [ConfigModule],
controllers: [TopfiveController],
providers: [TopFiveService],
})
export class TopfiveModule {}
197 changes: 197 additions & 0 deletions BE/src/stocks/topfive/topfive.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
import axios from 'axios';
import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { StockRankigRequestDto } from './dto/stock-ranking-request.dto';
import { StockRankingResponseDto } from './dto/stock-ranking-response.dto';
import { StockRankingDataDto } from './dto/stock-ranking-data.dto';

export enum MarketType {
ALL = 'ALL',
KOSPI = 'KOSPI',
KOSDAQ = 'KOSDAQ',
KOSPI200 = 'KOSPI200',
}

interface StockApiOutputData {
stck_shrn_iscd: string;
data_rank: string;
hts_kor_isnm: string;
stck_prpr: string;
prdy_vrss: string;
prdy_vrss_sign: string;
prdy_ctrt: string;
acml_vol: string;
stck_hgpr: string;
hgpr_hour: string;
acml_hgpr_data: string;
stck_lwpr: string;
lwpr_hour: string;
acml_lwpr_date: string;
lwpr_vrss_prpr_rate: string;
dsgt_date_clpr_vrss_prpr_rate: string;
cnnt_ascn_dynu: string;
hgpr_vrss_prpr_rate: string;
cnnt_down_dynu: string;
oprc_vrss_prpr_sign: string;
oprc_vrss_prpr: string;
oprc_vrss_prpr_rate: string;
prd_rsfl: string;
prd_rsfl_rate: string;
}

interface StockApiResponse {
output: StockApiOutputData[];
rt_cd: string;
msg_cd: string;
msg1: string;
}

@Injectable()
export class TopFiveService {
private accessToken: string;
private tokenExpireTime: Date;
private readonly koreaInvestmentConfig: {
appKey: string;
appSecret: string;
baseUrl: string;
};

private readonly logger = new Logger();

constructor(private readonly config: ConfigService) {
this.koreaInvestmentConfig = {
appKey: this.config.get<string>('KOREA_INVESTMENT_APP_KEY'),
appSecret: this.config.get<string>('KOREA_INVESTMENT_APP_SECRET'),
baseUrl: this.config.get<string>('KOREA_INVESTMENT_BASE_URL'),
};
}

private async getAccessToken() {
// accessToken이 유효한 경우
if (this.accessToken && this.tokenExpireTime > new Date()) {
return this.accessToken;
}

const response = await axios.post(
`${this.koreaInvestmentConfig.baseUrl}/oauth2/tokenP`,
{
grant_type: 'client_credentials',
appkey: this.koreaInvestmentConfig.appKey,
appsecret: this.koreaInvestmentConfig.appSecret,
},
);

this.accessToken = response.data.access_token;
this.tokenExpireTime = new Date(Date.now() + +response.data.expires_in);

return this.accessToken;
}

private async requestApi(params: StockRankigRequestDto) {
try {
const token = await this.getAccessToken();

const response = await axios.get<StockApiResponse>(
`${this.koreaInvestmentConfig.baseUrl}/uapi/domestic-stock/v1/ranking/fluctuation`,
{
headers: {
'content-type': 'application/json; charset=utf-8',
authorization: `Bearer ${token}`,
appkey: this.koreaInvestmentConfig.appKey,
appsecret: this.koreaInvestmentConfig.appSecret,
tr_id: 'FHPST01700000',
custtype: 'P',
},
params: {
fid_rsfl_rate2: '',
fid_cond_mrkt_div_code: params.fid_cond_mrkt_div_code,
fid_cond_scr_div_code: '20170',
fid_input_iscd: params.fid_input_iscd,
fid_rank_sort_cls_code: params.fid_rank_sort_cls_code,
fid_input_cnt_1: '0',
fid_prc_cls_code: '1',
fid_input_price_1: '',
fid_input_price_2: '',
fid_vol_cnt: '',
fid_trgt_cls_code: '0',
fid_trgt_exls_cls_code: '0',
fid_div_cls_code: '0',
fid_rsfl_rate1: '',
},
},
);
return response.data;
} catch (error) {
this.logger.error('API Error Details:', {
status: error.response?.status,
statusText: error.response?.statusText,
data: error.response?.data,
headers: error.response?.config?.headers,
message: error.message,
});
throw error;
}
}

async getMarketRanking(marketType: MarketType) {
try {
const params = new StockRankigRequestDto();
params.fid_cond_mrkt_div_code = 'J';

switch (marketType) {
case MarketType.ALL:
params.fid_input_iscd = '0000';
break;
case MarketType.KOSPI:
params.fid_input_iscd = '0001';
break;
case MarketType.KOSDAQ:
params.fid_input_iscd = '1001';
break;
case MarketType.KOSPI200:
params.fid_input_iscd = '2001';
break;
default:
break;
}

const highResponse = await this.requestApi({
...params,
fid_rank_sort_cls_code: '0',
});

const lowResponse = await this.requestApi({
...params,
fid_rank_sort_cls_code: '1',
});

const response = new StockRankingResponseDto();
response.high = this.formatStockData(highResponse.output);
response.low = this.formatStockData(lowResponse.output);

return response;
} catch (error) {
this.logger.error('API Error Details:', {
status: error.response?.status,
statusText: error.response?.statusText,
data: error.response?.data,
headers: error.response?.config?.headers, // 실제 요청 헤더
message: error.message,
});
throw error;
}
}

private formatStockData(stocks: StockApiOutputData[]) {
return stocks.slice(0, 5).map((stock) => {
const stockData = new StockRankingDataDto();
stockData.hts_kor_isnm = stock.hts_kor_isnm;
stockData.stck_prpr = stock.stck_prpr;
stockData.prdy_vrss = stock.prdy_vrss;
stockData.prdy_vrss_sign = stock.prdy_vrss_sign;
stockData.prdy_ctrt = stock.prdy_ctrt;

return stockData;
});
}
}

0 comments on commit 83d246a

Please sign in to comment.