Skip to content

Commit 10e5837

Browse files
authored
Merge pull request #96 from SynergyX-AI-Pattern/feat/#93_backtest_ranking
Feat/#93 backtest ranking
2 parents e8f0a04 + 7e66a07 commit 10e5837

6 files changed

Lines changed: 175 additions & 0 deletions

File tree

src/main/java/com/synergyx/trading/apiPayload/code/status/SuccessStatus.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ public enum SuccessStatus implements BaseCode {
3333

3434
// 백테스팅
3535
SUCCESS_BACKTEST_EXECUTE(HttpStatus.OK, "BACKTEST200", "백테스트가 실행되었습니다."),
36+
SUCCESS_RANKING(HttpStatus.OK, "RANK200", "백테스팅 랭킹 조회 성공"),
3637

3738
// 종목 상세
3839
SUCCESS_CHART_DATA(HttpStatus.OK, "CANDLE200", "차트 데이터 조회 성공."),

src/main/java/com/synergyx/trading/controller/BacktestController.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,15 @@
88
import com.synergyx.trading.dto.backtest.BacktestRequestDTO;
99
import com.synergyx.trading.dto.backtest.BacktestResponseDTO;
1010
import com.synergyx.trading.service.backtestService.BacktestService;
11+
import com.synergyx.trading.service.backtestService.ranking.BacktestRankingService;
1112
import io.swagger.v3.oas.annotations.Operation;
1213
import io.swagger.v3.oas.annotations.Parameter;
1314
import io.swagger.v3.oas.annotations.tags.Tag;
1415
import lombok.RequiredArgsConstructor;
1516
import org.springframework.data.domain.Page;
1617
import org.springframework.http.ResponseEntity;
1718
import org.springframework.web.bind.annotation.*;
19+
import java.util.List;
1820

1921
@RestController
2022
@RequiredArgsConstructor
@@ -24,6 +26,7 @@ public class BacktestController {
2426

2527
private final UserContext userContext;
2628
private final BacktestService backtestService;
29+
private final BacktestRankingService backtestRankingService;
2730

2831
// 백테스트 실행
2932
@Operation(summary = "백테스팅 실행", description = "백테스팅을 실행합니다.")
@@ -99,4 +102,18 @@ public ResponseEntity<?> getBacktestCandles(
99102
SuccessStatus.SUCCESS_CHART_DATA.getMessage()
100103
));
101104
}
105+
106+
// 백테스팅 랭킹 조회
107+
@Operation(summary = "백테스팅 랭킹 조회", description = "최대 수익률을 기준으로 백테스팅 랭킹을 조회합니다. (조건: 점 3개 이상의 패턴으로 실행된 백테스팅)")
108+
@GetMapping("/rankings")
109+
public ResponseEntity<?> getRankings(
110+
@RequestParam(required = false, defaultValue = "100") int limit) {
111+
List<?> rankings = backtestRankingService.getRanking(limit);
112+
113+
return ResponseEntity.ok(ApiResponse.onSuccess(
114+
rankings,
115+
SuccessStatus.SUCCESS_RANKING.getCode(),
116+
SuccessStatus.SUCCESS_RANKING.getMessage()
117+
));
118+
}
102119
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package com.synergyx.trading.dto.backtest;
2+
3+
import lombok.*;
4+
5+
import java.time.LocalDate;
6+
7+
// 백테스팅 랭킹 조회
8+
@Getter
9+
@Setter
10+
@Builder
11+
@NoArgsConstructor
12+
@AllArgsConstructor
13+
public class BacktestRankingDTO implements Rankable {
14+
private Integer rank; // 랭킹 순위
15+
private Long userId; // 사용자 아이디
16+
private String username; // 사용자 이름
17+
private String image; // 사용자 이미지
18+
private Double winRate; // 승률
19+
private Double averageReturn; // 평균 수익률
20+
private Double maxReturn; // 최대 수익률
21+
private LocalDate maxReturnDate; // 최대 수익률 발생일
22+
private Long backtestId; // 백테스팅 아이디
23+
24+
public BacktestRankingDTO(Long userId, String username, String image, Double winRate, Double averageReturn, Double maxReturn, LocalDate maxReturnDate, Long backtestId) {
25+
this.userId = userId;
26+
this.username = username;
27+
this.image = image;
28+
this.winRate = winRate;
29+
this.averageReturn = averageReturn;
30+
this.maxReturn = maxReturn;
31+
this.maxReturnDate = maxReturnDate;
32+
this.backtestId = backtestId;
33+
}
34+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package com.synergyx.trading.dto.backtest;
2+
3+
// 백테스팅 랭킹에 쓰이는 순위 부여용 인터페이스
4+
public interface Rankable {
5+
void setRank(Integer rank);
6+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package com.synergyx.trading.repository;
2+
3+
import com.synergyx.trading.dto.backtest.BacktestRankingDTO;
4+
import com.synergyx.trading.model.Backtest;
5+
import org.springframework.data.jpa.repository.JpaRepository;
6+
import org.springframework.data.jpa.repository.Query;
7+
import org.springframework.data.repository.query.Param;
8+
import org.springframework.stereotype.Repository;
9+
10+
import org.springframework.data.domain.Pageable;
11+
12+
import java.time.LocalDate;
13+
import java.util.List;
14+
15+
@Repository
16+
public interface BacktestRankingRepository extends JpaRepository<Backtest, Long> {
17+
18+
// 최대 수익률 월간 랭킹 조회
19+
@Query("""
20+
21+
SELECT new com.synergyx.trading.dto.backtest.BacktestRankingDTO(
22+
b.user.id,
23+
b.user.username,
24+
b.user.image,
25+
b.winRate,
26+
b.averageReturn,
27+
b.maxReturn,
28+
b.maxReturnDate,
29+
b.id
30+
)
31+
FROM Backtest b
32+
WHERE b.executedAt BETWEEN :startOfMonth AND :endOfMonth
33+
AND b.user.username <> '탈퇴회원'
34+
AND b.maxReturn = (
35+
SELECT MAX(b2.maxReturn)
36+
FROM Backtest b2
37+
WHERE b2.user.id = b.user.id
38+
AND b2.executedAt BETWEEN :startOfMonth AND :endOfMonth
39+
)
40+
ORDER BY b.maxReturn DESC, b.winRate DESC
41+
""")
42+
List<BacktestRankingDTO> findMonthlyUserMaxReturnRankings(
43+
@Param("startOfMonth") LocalDate startOfMonth,
44+
@Param("endOfMonth") LocalDate endOfMonth,
45+
Pageable pageable
46+
);
47+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package com.synergyx.trading.service.backtestService.ranking;
2+
3+
import com.synergyx.trading.apiPayload.code.status.ErrorStatus;
4+
import com.synergyx.trading.apiPayload.exception.GeneralException;
5+
import com.synergyx.trading.dto.backtest.BacktestRankingDTO;
6+
import com.synergyx.trading.dto.backtest.Rankable;
7+
import com.synergyx.trading.model.Backtest;
8+
import com.synergyx.trading.repository.BacktestRankingRepository;
9+
import com.synergyx.trading.repository.BacktestRepository;
10+
import lombok.RequiredArgsConstructor;
11+
import org.springframework.data.domain.PageRequest;
12+
import org.springframework.stereotype.Service;
13+
import java.time.LocalDate;
14+
import java.util.List;
15+
import java.util.Map;
16+
import java.util.concurrent.atomic.AtomicInteger;
17+
import java.util.function.Function;
18+
import java.util.stream.Collectors;
19+
20+
@Service
21+
@RequiredArgsConstructor
22+
public class BacktestRankingService {
23+
24+
private final BacktestRepository backtestRepository;
25+
private final BacktestRankingRepository backtestRankingRepository;
26+
27+
// 랭킹 순위 부여용 메서드
28+
private <T extends Rankable> void assignRanks(List<T> results) {
29+
AtomicInteger rank = new AtomicInteger(1);
30+
results.forEach(r -> r.setRank(rank.getAndIncrement()));
31+
}
32+
33+
// 랭킹 조회 (최대 수익률, 월간 기준)
34+
public List<BacktestRankingDTO> getRanking(int limit) {
35+
36+
// 월간 기간 계산 (1일 ~ 31일)
37+
LocalDate now = LocalDate.now();
38+
LocalDate startOfMonth = now.withDayOfMonth(1);
39+
LocalDate endOfMonth = now.withDayOfMonth(now.lengthOfMonth());
40+
41+
// Repository 호출
42+
List<BacktestRankingDTO> rankings = backtestRankingRepository.findMonthlyUserMaxReturnRankings(startOfMonth, endOfMonth, PageRequest.of(0, limit));
43+
44+
// 백테스트 ID 목록 추출
45+
List<Long> backtestIds = rankings.stream()
46+
.map(BacktestRankingDTO::getBacktestId)
47+
.toList();
48+
49+
// 일괄 조회
50+
Map<Long, Backtest> backtestMap = backtestRepository.findAllById(backtestIds)
51+
.stream()
52+
.collect(Collectors.toMap(Backtest::getId, Function.identity()));
53+
54+
// 점 개수 3개 이상 조건 필터링
55+
rankings = rankings.stream()
56+
.filter(r -> {
57+
Backtest backtest = backtestMap.get(r.getBacktestId());
58+
if (backtest == null) {
59+
throw new GeneralException(ErrorStatus.BACKTEST_NOT_FOUND);
60+
}
61+
return backtest.getPattern().getPoints().size() >= 3;
62+
})
63+
.toList();
64+
65+
// 순위 부여
66+
assignRanks(rankings);
67+
68+
return rankings;
69+
}
70+
}

0 commit comments

Comments
 (0)