Skip to content

Commit 99d48c3

Browse files
authored
Merge pull request #101 from SynergyX-AI-Pattern/refactor/#97_ai_top_20_add_actual_data
Refactor/#97 ai top 20 add actual data
2 parents 90d1aaa + 654e565 commit 99d48c3

4 files changed

Lines changed: 60 additions & 83 deletions

File tree

src/main/java/com/synergyx/trading/model/StockDetail.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,10 @@ public class StockDetail extends BaseEntity {
3737
@Version
3838
@Column(name = "version")
3939
private Long version; // 동시성 제어를 위해 추가
40+
41+
@Column(name = "ai_avg_increase", columnDefinition = "double default 0")
42+
private Double aiAvgIncrease;
43+
44+
@Column(name = "ai_rank")
45+
private Integer aiRank;
4046
}

src/main/java/com/synergyx/trading/repository/StockDetailRepository.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,14 @@
44
import org.springframework.data.jpa.repository.JpaRepository;
55
import org.springframework.stereotype.Repository;
66

7+
import java.util.List;
78
import java.util.Optional;
89

910
@Repository
1011
public interface StockDetailRepository extends JpaRepository<StockDetail, Long> {
1112
Optional<StockDetail> findByStock_Symbol(String symbol);
1213

1314
Optional<StockDetail> findByStock_Id(Long stockId);
15+
16+
List<StockDetail> findTop20ByOrderByAiRankAsc();
1417
}

src/main/java/com/synergyx/trading/repository/StockOhlcvRepository.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,4 +43,14 @@ ORDER BY (o.close * o.volume) DESC
4343
// entryAt (포함) 이전의 가장 최근의 종가 조회
4444
Optional<StockOhlcv> findTop1ByStockIdAndTimestampLessThanEqualOrderByTimestampDesc(Long stockId, LocalDateTime entryAt);
4545

46+
// ai top 20 - 현재가 조회
47+
@Query("""
48+
SELECT o FROM StockOhlcv o
49+
WHERE o.timestamp = :timestamp
50+
AND o.stock.id IN :stockIds
51+
""")
52+
List<StockOhlcv> findByTimestampAndStockIdIn(
53+
@Param("timestamp") LocalDateTime timestamp,
54+
@Param("stockIds") List<Long> stockIds
55+
);
4656
}
Lines changed: 41 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
package com.synergyx.trading.service.stockService.ranking;
22

3-
import com.synergyx.trading.apiPayload.code.status.ErrorStatus;
4-
import com.synergyx.trading.apiPayload.exception.GeneralException;
53
import com.synergyx.trading.dto.stockDetail.RankedStockDTO;
64
import com.synergyx.trading.model.Stock;
75
import com.synergyx.trading.model.StockDetail;
@@ -19,10 +17,8 @@
1917
import java.util.Collections;
2018
import java.util.List;
2119
import java.util.Map;
22-
import java.util.Optional;
2320
import java.util.concurrent.atomic.AtomicInteger;
2421
import java.util.stream.Collectors;
25-
import java.util.stream.IntStream;
2622

2723
import static com.synergyx.trading.util.ParsingUtil.toFormattedNumber;
2824
import static com.synergyx.trading.util.ParsingUtil.toFormattedPercentage;
@@ -100,102 +96,64 @@ public List<RankedStockDTO> getTop20() {
10096
}
10197

10298
/**
103-
* AI 예측값의 상승폭을 기준으로 상위 20개 종목을 조회합니다.
99+
* AI 예측값의 상승률을 기준으로 상위 20개 종목을 조회합니다.
104100
*
105-
* @return AI 예측값의 상승폭 기준 TOP 20 종목 목록
101+
* @return AI 예측값의 15일간 평균 상승률 기준 TOP 20 종목 목록
106102
*/
107103
@Override
108104
@Transactional(readOnly = true)
109105
public List<RankedStockDTO> getAiTop20() {
110106
log.info("[StockRanking] AI 예측 기반 TOP20 랭킹 조회 시작");
111107

112-
// todo: 임시 데이터
113-
List<Long> stockIds = IntStream.rangeClosed(1, 20)
114-
.mapToObj(Long::valueOf)
108+
// AI 예측 랭킹 상위 20개 (평균 상승률 기준)
109+
List<StockDetail> topDetails = stockDetailRepository.findTop20ByOrderByAiRankAsc();
110+
if (topDetails.isEmpty()) {
111+
log.warn("[StockRanking] AI 예측 랭킹 데이터가 없습니다.");
112+
return Collections.emptyList();
113+
}
114+
115+
// 필요한 종목 ID 수집 및 매핑
116+
List<Long> stockIds = topDetails.stream()
117+
.map(detail -> detail.getStock().getId())
115118
.toList();
116119

117-
// stock 조회
118120
Map<Long, Stock> stockMap = stockRepository.findAllById(stockIds).stream()
119121
.collect(Collectors.toMap(Stock::getId, s -> s));
120122

121-
List<RankedStockDTO> result = IntStream.rangeClosed(1, 20)
122-
.mapToObj(rank -> {
123-
long stockId = rank;
124-
Stock stock = Optional.ofNullable(stockMap.get(stockId))
125-
.orElseThrow(() -> new GeneralException(ErrorStatus.STOCK_NOT_FOUND));
123+
// Ohlcv 종가 데이터 가져오기
124+
LocalDateTime latestTimestamp = stockOhlcvRepository.findLatestTimestamp();
125+
List<StockOhlcv> latestOhlcvs = stockOhlcvRepository.findByTimestampAndStockIdIn(latestTimestamp, stockIds);
126+
Map<Long, StockOhlcv> ohlcvMap = latestOhlcvs.stream()
127+
.collect(Collectors.toMap(ohlcv -> ohlcv.getStock().getId(), ohlcv -> ohlcv));
128+
129+
// DTO 변환
130+
AtomicInteger rankCounter = new AtomicInteger(1);
131+
132+
List<RankedStockDTO> rankedList = topDetails.stream()
133+
.map(detail -> {
134+
Stock stock = stockMap.get(detail.getStock().getId());
135+
StockOhlcv ohlcv = ohlcvMap.get(detail.getStock().getId());
136+
137+
// 현재 종가
138+
String formattedPrice = (ohlcv != null)
139+
? toFormattedNumber(ohlcv.getClose())
140+
: "N/A";
141+
142+
// 예측 평균 상승률
143+
String formattedPredictedIncrease = toFormattedPercentage(detail.getAiAvgIncrease(), 2);
126144

127145
return RankedStockDTO.builder()
128-
.rank(rank)
129-
.stockId(stockId)
146+
.rank(rankCounter.getAndIncrement())
147+
.stockId(stock.getId())
130148
.stockName(stock.getName())
131-
.price(getMockPrice(rank)) // todo: 임시 예측 종가
132-
.changeRate(getMockChangeRate(rank)) // todo: 임시 상승률
149+
.price(formattedPrice)
150+
.changeRate(formattedPredictedIncrease)
133151
.imageUrl(stock.getImageUrl())
134152
.build();
135-
}).toList();
136-
137-
// log.info("[StockRanking] AI 랭킹 조회 완료 - {}개 반환", result.size());
138-
return result;
139-
}
140-
141-
/**
142-
* 임시 목데이터 - 예측 종가
143-
* todo: delete
144-
*/
145-
private String getMockPrice(int rank) {
146-
return switch (rank) {
147-
case 1 -> "59,100";
148-
case 2 -> "112,000";
149-
case 3 -> "74,300";
150-
case 4 -> "48,900";
151-
case 5 -> "126,000";
152-
case 6 -> "191,000";
153-
case 7 -> "91,400";
154-
case 8 -> "474,000";
155-
case 9 -> "513,000";
156-
case 10 -> "169,000";
157-
case 11 -> "28,500";
158-
case 12 -> "41,600";
159-
case 13 -> "495,000";
160-
case 14 -> "19,600";
161-
case 15 -> "144,000";
162-
case 16 -> "23,900";
163-
case 17 -> "345,000";
164-
case 18 -> "151,000";
165-
case 19 -> "85,200";
166-
case 20 -> "92,300";
167-
default -> "0";
168-
};
169-
}
153+
})
154+
.toList();
170155

171-
/**
172-
* 임시 목데이터 - 예측 상승률
173-
* todo: delete
174-
*/
175-
private String getMockChangeRate(int rank) {
176-
return switch (rank) {
177-
case 1 -> "2.25%";
178-
case 2 -> "-0.82%";
179-
case 3 -> "1.12%";
180-
case 4 -> "-1.20%";
181-
case 5 -> "0.95%";
182-
case 6 -> "3.11%";
183-
case 7 -> "2.78%";
184-
case 8 -> "-0.65%";
185-
case 9 -> "0.85%";
186-
case 10 -> "1.02%";
187-
case 11 -> "2.00%";
188-
case 12 -> "-0.45%";
189-
case 13 -> "1.57%";
190-
case 14 -> "-1.90%";
191-
case 15 -> "0.65%";
192-
case 16 -> "1.33%";
193-
case 17 -> "0.48%";
194-
case 18 -> "-0.95%";
195-
case 19 -> "0.75%";
196-
case 20 -> "1.18%";
197-
default -> "0.00%";
198-
};
156+
// log.info("[StockRanking] AI 예측 기반 TOP20 조회 완료 - {}개 반환", rankedList.size());
157+
return rankedList;
199158
}
200-
201159
}

0 commit comments

Comments
 (0)