Skip to content

Commit

Permalink
Feature/#253 - 가격 상승률 및 하락률 데이터 수집 (#260)
Browse files Browse the repository at this point in the history
  • Loading branch information
xjfcnfw3 authored Nov 27, 2024
2 parents ca95b47 + d3b6d65 commit ec540c4
Show file tree
Hide file tree
Showing 9 changed files with 252 additions and 14 deletions.
2 changes: 1 addition & 1 deletion packages/backend/src/auth/google/googleAuth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export class GoogleAuthController {
@Get('/redirect')
@UseGuards(GoogleAuthGuard)
async handleRedirect(@Res() response: Response) {
response.redirect('/');
response.redirect('http://localhost:5173');
}

@ApiOperation({
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { Inject, Injectable } from '@nestjs/common';
import { Cron } from '@nestjs/schedule';
import { DataSource, EntityManager } from 'typeorm';
import { Logger } from 'winston';
import { OpenapiTokenApi } from '@/scraper/openapi/api/openapiToken.api';
import {
DECREASE_STOCK_QUERY,
INCREASE_STOCK_QUERY,
} from '@/scraper/openapi/constants/query';
import { TR_IDS } from '@/scraper/openapi/type/openapiUtil.type';
import { getOpenApi } from '@/scraper/openapi/util/openapiUtil.api';
import { FluctuationRankStock } from '@/stock/domain/FluctuationRankStock.entity';

@Injectable()
export class OpenapiFluctuationData {
private readonly fluctuationUrl: string =
'/uapi/domestic-stock/v1/ranking/fluctuation';
constructor(
private readonly openApiToken: OpenapiTokenApi,
private readonly datasource: DataSource,
@Inject('winston') private readonly logger: Logger,
) {
setTimeout(() => this.getFluctuationRankStocks(), 1000);
}

@Cron('*/1 9-16 * * 1-5')
async getFluctuationRankStocks() {
await this.getDecreaseRankStocks();
await this.getIncreaseRankStocks();
}

async getDecreaseRankStocks(count = 5) {
try {
if (count === 0) return;
await this.datasource.transaction(async (manager) => {
const result = await this.getFluctuationRankApiStocks(false);
await this.datasource.manager.delete(FluctuationRankStock, {
isRising: false,
});
await this.saveFluctuationRankStocks(result, manager);
this.logger.info('decrease rank stocks updated');
});
} catch (error) {
this.logger.warn(error);
this.getDecreaseRankStocks(--count);
}
}

async getIncreaseRankStocks(count = 5) {
try {
if (count === 0) return;
await this.datasource.transaction(async (manager) => {
const result = await this.getFluctuationRankApiStocks(true);
await this.datasource.manager.delete(FluctuationRankStock, {
isRising: true,
});
await this.saveFluctuationRankStocks(result, manager);
this.logger.info('increase rank stocks updated');
});
} catch (error) {
this.logger.warn(error);
this.getIncreaseRankStocks(--count);
}
}

private async saveFluctuationRankStocks(
result: FluctuationRankStock[],
manager: EntityManager,
) {
await manager
.getRepository(FluctuationRankStock)
.createQueryBuilder()
.insert()
.into(FluctuationRankStock)
.values(result)
.execute();
}

private async getFluctuationRankApiStocks(isRising: boolean) {
const query = isRising ? INCREASE_STOCK_QUERY : DECREASE_STOCK_QUERY;
const result = await getOpenApi(
this.fluctuationUrl,
(await this.openApiToken.configs())[0],
query,
TR_IDS.FLUCTUATION_DATA,
);

return result.output.map((result: Record<string, string>) => ({
rank: result.data_rank,
fluctuationRate: result.prdy_ctrt,
stock: { id: result.stck_shrn_iscd },
isRising,
}));
}
}
26 changes: 26 additions & 0 deletions packages/backend/src/scraper/openapi/constants/query.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
const BASE_QUERY = {
fid_cond_mrkt_div_code: 'J',
fid_cond_scr_div_code: '20170',
fid_input_iscd: '0000',
fid_input_cnt_1: '0',
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: '',
fid_rsfl_rate2: '',
};

export const DECREASE_STOCK_QUERY = {
...BASE_QUERY,
fid_rank_sort_cls_code: '1',
fid_prc_cls_code: '1',
};

export const INCREASE_STOCK_QUERY = {
...BASE_QUERY,
fid_rank_sort_cls_code: '0',
fid_prc_cls_code: '1',
};
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import {
} from '@/stock/domain/stockData.entity';
import { StockDetail } from '@/stock/domain/stockDetail.entity';
import { StockLiveData } from '@/stock/domain/stockLiveData.entity';
import { OpenapiFluctuationData } from '@/scraper/openapi/api/openapiFluctuationData.api';
import { FluctuationRankStock } from '@/stock/domain/FluctuationRankStock.entity';

@Module({
imports: [
Expand All @@ -27,6 +29,7 @@ import { StockLiveData } from '@/stock/domain/stockLiveData.entity';
StockYearly,
StockLiveData,
StockDetail,
FluctuationRankStock,
]),
],
controllers: [],
Expand All @@ -36,7 +39,7 @@ import { StockLiveData } from '@/stock/domain/stockLiveData.entity';
OpenapiMinuteData,
OpenapiDetailData,
OpenapiScraperService,
OpenapiTokenApi,
OpenapiFluctuationData,
],
})
export class OpenapiScraperModule {}
2 changes: 2 additions & 0 deletions packages/backend/src/scraper/openapi/type/openapiUtil.type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export type TR_ID =
| 'FHKST03010200'
| 'FHKST66430300'
| 'HHKDB669107C0'
| 'FHPST01700000'
| 'FHKST01010100'
| 'CTPF1002R';

Expand All @@ -12,4 +13,5 @@ export const TR_IDS: Record<string, TR_ID> = {
FINANCIAL_DATA: 'FHKST66430300',
PRODUCTION_DETAIL: 'CTPF1002R',
LIVE_DATA: 'FHKST01010100',
FLUCTUATION_DATA : 'FHPST01700000',
};
31 changes: 31 additions & 0 deletions packages/backend/src/stock/domain/FluctuationRankStock.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import {
Column,
CreateDateColumn,
Entity,
JoinColumn,
ManyToOne,
PrimaryGeneratedColumn,
} from 'typeorm';
import { Stock } from '@/stock/domain/stock.entity';

@Entity()
export class FluctuationRankStock {
@PrimaryGeneratedColumn()
id: number;

@ManyToOne(() => Stock, (stock) => stock.id)
@JoinColumn({ name: 'stock_id' })
stock: Stock;

@Column({ name: 'fluctuation_rate', type: 'decimal', precision: 5, scale: 2 })
fluctuationRate: string;

@Column()
isRising: boolean;

@Column()
rank: number;

@CreateDateColumn({ name: 'created_at', type: 'timestamp' })
createdAt: Date;
}
7 changes: 7 additions & 0 deletions packages/backend/src/stock/domain/stock.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
import { StockLiveData } from './stockLiveData.entity';
import { Like } from '@/chat/domain/like.entity';
import { DateEmbedded } from '@/common/dateEmbedded.entity';
import { FluctuationRankStock } from '@/stock/domain/FluctuationRankStock.entity';
import { UserStock } from '@/stock/domain/userStock.entity';

@Entity()
Expand Down Expand Up @@ -58,4 +59,10 @@ export class Stock {

@OneToOne(() => KospiStock, (kospiStock) => kospiStock.stock)
kospiStock?: KospiStock;

@OneToMany(
() => FluctuationRankStock,
(fluctuationRankStock) => fluctuationRankStock.stock,
)
fluctuationRankStocks?: FluctuationRankStock[];
}
63 changes: 63 additions & 0 deletions packages/backend/src/stock/dto/stock.response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,3 +101,66 @@ export class StockSearchResponse {
}));
}
}

export class StockRankResponse {
@ApiProperty({
description: '주식 종목 코드',
example: 'A005930',
})
id: string;

@ApiProperty({
description: '주식 종목 이름',
example: '삼성전자',
})
name: string;

@ApiProperty({
description: '주식 현재가',
example: 100000.0,
})
@Transform(({ value }) => parseFloat(value))
currentPrice: number;

@ApiProperty({
description: '주식 변동률',
example: 2.5,
})
@Transform(({ value }) => parseFloat(value))
changeRate: number;

@ApiProperty({
description: '주식 거래량',
example: 500000,
})
@Transform(({ value }) => parseInt(value))
volume: number;

@ApiProperty({
description: '주식 시가 총액',
example: '500000000000.00',
})
marketCap: string;

@ApiProperty({
description: '랭킹',
example: 1,
})
rank: number;
}

export class StockRankResponses {
result: StockRankResponse[];

constructor(stocks: Record<string, string>[]) {
this.result = stocks.map((stock) => ({
id: stock.id,
name: stock.name,
currentPrice: parseFloat(stock.currentPrice),
volume: parseInt(stock.volume),
marketCap: stock.marketCap,
changeRate: parseFloat(stock.fluctuationRate),
rank: parseInt(stock.stockRank),
}));
}
}
35 changes: 23 additions & 12 deletions packages/backend/src/stock/stock.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ import { plainToInstance } from 'class-transformer';
import { DataSource, EntityManager } from 'typeorm';
import { Logger } from 'winston';
import { Stock } from './domain/stock.entity';
import { StockSearchResponse, StocksResponse } from './dto/stock.response';
import {
StockRankResponses,
StockSearchResponse,
StocksResponse,
} from './dto/stock.response';
import { UserStock } from '@/stock/domain/userStock.entity';

@Injectable()
Expand Down Expand Up @@ -94,7 +98,7 @@ export class StockService {
}

async getTopStocksByViews(limit: number) {
const rawData = await this.StocksQuery()
const rawData = await this.getStocksQuery()
.orderBy('stock.views', 'DESC')
.limit(limit)
.getRawMany();
Expand All @@ -103,21 +107,17 @@ export class StockService {
}

async getTopStocksByGainers(limit: number) {
const rawData = await this.StocksQuery()
.orderBy('stockLiveData.changeRate', 'DESC')
.limit(limit)
.getRawMany();
const rawData = await this.getStockRankQuery(true).take(limit).getRawMany();

return plainToInstance(StocksResponse, rawData);
return new StockRankResponses(rawData);
}

async getTopStocksByLosers(limit: number) {
const rawData = await this.StocksQuery()
.orderBy('stockLiveData.changeRate', 'ASC')
.limit(limit)
const rawData = await this.getStockRankQuery(false)
.take(limit)
.getRawMany();

return plainToInstance(StocksResponse, rawData);
return new StockRankResponses(rawData);
}

private async validateStockExists(stockId: string, manager: EntityManager) {
Expand Down Expand Up @@ -153,7 +153,7 @@ export class StockService {
return await manager.exists(Stock, { where: { id: stockId } });
}

private StocksQuery() {
private getStocksQuery() {
return this.datasource
.getRepository(Stock)
.createQueryBuilder('stock')
Expand All @@ -176,4 +176,15 @@ export class StockService {
'stockDetail.marketCap AS marketCap',
]);
}

private getStockRankQuery(isRising: boolean) {
return this.getStocksQuery()
.innerJoinAndSelect('stock.fluctuationRankStocks', 'FluctuationRankStock')
.addSelect([
'fluctuationRankStock.rank AS stockRank',
'fluctuationRankStock.isRising AS isRising',
'fluctuationRankStock.fluctuation_rate AS fluctuationRate',
])
.where('FluctuationRankStock.isRising = :isRising', { isRising });
}
}

0 comments on commit ec540c4

Please sign in to comment.