diff --git a/backend/src/main/java/org/sejongisc/backend/backtest/controller/BacktestController.java b/backend/src/main/java/org/sejongisc/backend/backtest/controller/BacktestController.java index da3d7827..7e356285 100644 --- a/backend/src/main/java/org/sejongisc/backend/backtest/controller/BacktestController.java +++ b/backend/src/main/java/org/sejongisc/backend/backtest/controller/BacktestController.java @@ -27,6 +27,15 @@ public class BacktestController { private final BacktestService backtestService; + @GetMapping("/stocks/info") + @Operation( + summary = "백테스트용 주식 정보 조회", + description = "백테스트에 사용되는 주식의 기본 정보를 조회합니다." + ) + public ResponseEntity getBacktestStockInfo() { + return ResponseEntity.ok(backtestService.getBacktestStockInfo()); + } + // 백테스트 실행 상태 조회 @GetMapping("/runs/{backtestRunId}/status") @Operation( @@ -73,6 +82,7 @@ public ResponseEntity getBackTestResultDetails(@PathVariable L "strategy": { "initialCapital": 100000.00, "ticker": "AAPL", + "defaultExitDays" : 30, "buyConditions": [ { "leftOperand": { diff --git a/backend/src/main/java/org/sejongisc/backend/backtest/dto/BacktestRequest.java b/backend/src/main/java/org/sejongisc/backend/backtest/dto/BacktestRequest.java index b35ab8ad..68258b9b 100644 --- a/backend/src/main/java/org/sejongisc/backend/backtest/dto/BacktestRequest.java +++ b/backend/src/main/java/org/sejongisc/backend/backtest/dto/BacktestRequest.java @@ -35,10 +35,6 @@ public class BacktestRequest { @Schema(description = "백테스트 실행 요청 JSON") private BacktestRunRequest strategy; - @Schema(description = "기본 청산 기간") - private int defaultExitDays; - - // 백테스트 리스트 삭제 @Schema(description = "삭제할 백테스트 실행 리스트") private List backtestRunIds; diff --git a/backend/src/main/java/org/sejongisc/backend/backtest/dto/BacktestResponse.java b/backend/src/main/java/org/sejongisc/backend/backtest/dto/BacktestResponse.java index 702ba18a..54da487b 100644 --- a/backend/src/main/java/org/sejongisc/backend/backtest/dto/BacktestResponse.java +++ b/backend/src/main/java/org/sejongisc/backend/backtest/dto/BacktestResponse.java @@ -9,6 +9,7 @@ import org.sejongisc.backend.template.entity.Template; import java.time.LocalDate; +import java.util.List; @Getter @@ -18,4 +19,5 @@ public class BacktestResponse { private BacktestRun backtestRun; private BacktestRunMetricsResponse backtestRunMetricsResponse; + private List availableTickers; } \ No newline at end of file diff --git a/backend/src/main/java/org/sejongisc/backend/backtest/dto/BacktestRunRequest.java b/backend/src/main/java/org/sejongisc/backend/backtest/dto/BacktestRunRequest.java index 709b4e07..059b9128 100644 --- a/backend/src/main/java/org/sejongisc/backend/backtest/dto/BacktestRunRequest.java +++ b/backend/src/main/java/org/sejongisc/backend/backtest/dto/BacktestRunRequest.java @@ -24,6 +24,9 @@ public class BacktestRunRequest { @Schema(description = "대상 종목 티커", defaultValue = "AAPL") private String ticker; + @Schema(description = "기본 청산 기간") + private int defaultExitDays; + @Schema(description = "매수 조건 그룹") private List buyConditions; @@ -33,6 +36,10 @@ public class BacktestRunRequest { @Schema(description = "노트", defaultValue = "골든크로스 + RSI 필터 전략 테스트") private String note; + //@Schema(description = "거래 시 매수 비중", defaultValue = "10") + //private int buyRatio; + //@Schema(description = "거래 시 매도 비중", defaultValue = "10") + //private int sellRatio; /* * 타임 프레임 (예: "D", "W", "M") private String timeFrame; diff --git a/backend/src/main/java/org/sejongisc/backend/backtest/dto/TradeLog.java b/backend/src/main/java/org/sejongisc/backend/backtest/dto/TradeLog.java index aadfebd1..556697c9 100644 --- a/backend/src/main/java/org/sejongisc/backend/backtest/dto/TradeLog.java +++ b/backend/src/main/java/org/sejongisc/backend/backtest/dto/TradeLog.java @@ -4,7 +4,11 @@ import java.time.LocalDateTime; public class TradeLog { - public enum Type { BUY, SELL } + public enum Type { + BUY, + SELL, + SELL_FORCED // 기본 청산 기간에 의한 강제 매도 구분용 추가 + } public final Type type; public final LocalDateTime time; public final BigDecimal price; diff --git a/backend/src/main/java/org/sejongisc/backend/backtest/entity/BacktestRunMetrics.java b/backend/src/main/java/org/sejongisc/backend/backtest/entity/BacktestRunMetrics.java index 72831aed..c532c09b 100644 --- a/backend/src/main/java/org/sejongisc/backend/backtest/entity/BacktestRunMetrics.java +++ b/backend/src/main/java/org/sejongisc/backend/backtest/entity/BacktestRunMetrics.java @@ -7,7 +7,6 @@ import lombok.NoArgsConstructor; import java.math.BigDecimal; -import java.util.UUID; @Entity @Getter @@ -37,4 +36,20 @@ public class BacktestRunMetrics { @Column(nullable = false) private int tradesCount; // 총 거래 횟수 + + public static BacktestRunMetrics fromDto(BacktestRun backtestRun, + BigDecimal totalReturn, + BigDecimal maxDrawdown, + BigDecimal sharpeRatio, + BigDecimal avgHoldDays, + int tradesCount) { + return BacktestRunMetrics.builder() + .backtestRun(backtestRun) + .totalReturn(totalReturn) + .maxDrawdown(maxDrawdown) + .sharpeRatio(sharpeRatio) + .avgHoldDays(avgHoldDays) + .tradesCount(tradesCount) + .build(); + } } diff --git a/backend/src/main/java/org/sejongisc/backend/backtest/service/BacktestService.java b/backend/src/main/java/org/sejongisc/backend/backtest/service/BacktestService.java index 3d04c898..968b2d61 100644 --- a/backend/src/main/java/org/sejongisc/backend/backtest/service/BacktestService.java +++ b/backend/src/main/java/org/sejongisc/backend/backtest/service/BacktestService.java @@ -14,6 +14,7 @@ import org.sejongisc.backend.backtest.repository.BacktestRunRepository; import org.sejongisc.backend.common.exception.CustomException; import org.sejongisc.backend.common.exception.ErrorCode; +import org.sejongisc.backend.stock.repository.PriceDataRepository; import org.sejongisc.backend.template.entity.Template; import org.sejongisc.backend.template.repository.TemplateRepository; import org.sejongisc.backend.user.dao.UserRepository; @@ -34,7 +35,17 @@ public class BacktestService { private final BacktestingEngine backtestingEngine; private final ObjectMapper objectMapper; private final UserRepository userRepository; + private final PriceDataRepository priceDataRepository; + // 백테스트용 주식 정보 조회 + @Transactional + public BacktestResponse getBacktestStockInfo() { + return BacktestResponse.builder() + .availableTickers(priceDataRepository.findDistinctTickers()) + .build(); + } + + @Transactional public BacktestResponse getBacktestStatus(Long backtestRunId, UUID userId) { log.info("백테스팅 실행 상태 조회를 시작합니다."); BacktestRun backtestRun = findBacktestRunByIdAndVerifyUser(backtestRunId, userId); @@ -105,7 +116,7 @@ public BacktestResponse runBacktest(BacktestRequest request) { log.info("백테스팅 실행 요청이 성공적으로 처리되었습니다. ID: {}", savedRun.getId()); // 비동기로 백테스팅 실행 시작 - backtestingEngine.execute(savedRun.getId()); + backtestingEngine.execute(savedRun); // 사용자에게 실행 중 응답 반환 return BacktestResponse.builder() diff --git a/backend/src/main/java/org/sejongisc/backend/backtest/service/BacktestingEngine.java b/backend/src/main/java/org/sejongisc/backend/backtest/service/BacktestingEngine.java index a5adcb11..088f0619 100644 --- a/backend/src/main/java/org/sejongisc/backend/backtest/service/BacktestingEngine.java +++ b/backend/src/main/java/org/sejongisc/backend/backtest/service/BacktestingEngine.java @@ -43,83 +43,101 @@ public class BacktestingEngine { @Async //@Transactional(propagation = Propagation.REQUIRES_NEW) @Async는 새로운 쓰레드에서 실행되므로 DB 작업 수행 시 주석 제거 필요 - public void execute(Long backtestRunId) { + public void execute(BacktestRun backtestRun) { + Long backtestRunId = backtestRun.getId(); log.info("백테스팅 실행이 시작됩니다. 실행 ID : {}", backtestRunId); - BacktestRun backtestRun = backtestRunRepository.findById(backtestRunId) - .orElseThrow(() -> new CustomException(ErrorCode.BACKTEST_NOT_FOUND)); // 거래 로그 리스트 초기화 List tradeLogs = new ArrayList<>(); - try { + // 백테스팅 상태 RUNNING 으로 변경 backtestRun.setStatus(BacktestStatus.RUNNING); backtestRun.setStartedAt(LocalDateTime.now()); backtestRunRepository.save(backtestRun); log.debug("백테스팅 상태 RUNNING 으로 변경됨. ID : {}", backtestRunId); - log.debug("paramsJson: {}", backtestRun.getParamsJson()); + // 백테스팅 파라미터 로드 BacktestRunRequest strategyDto = objectMapper.readValue(backtestRun.getParamsJson(), BacktestRunRequest.class); String ticker = strategyDto.getTicker(); - log.info("백테스팅 대상 티커: {}", ticker); + log.debug("백테스팅 대상 티커: {}", ticker); + // 가격 데이터 로드 List priceDataList = priceDataRepository.findByTickerAndDateBetweenOrderByDateAsc( ticker, backtestRun.getStartDate(), backtestRun.getEndDate()); - log.info("가격 데이터 로드 완료. 데이터 개수: {}", priceDataList.size()); + log.debug("가격 데이터 로드 완료. 데이터 개수: {}", priceDataList.size()); if (priceDataList.isEmpty()) { throw new CustomException(ErrorCode.PRICE_DATA_NOT_FOUND); } + // Ta4j BarSeries 생성 BarSeries series = ta4jHelper.createBarSeries(priceDataList); Map> indicatorCache = new HashMap<>(); log.debug("BarSeries 생성 완료. 바 개수: {}", series.getBarCount()); + // 매수/매도 룰 생성 Rule buyRule = ta4jHelper.buildCombinedRule(strategyDto.getBuyConditions(), series, indicatorCache); Rule sellRule = ta4jHelper.buildCombinedRule(strategyDto.getSellConditions(), series, indicatorCache); - BigDecimal initialCapital = strategyDto.getInitialCapital(); - BigDecimal cash = initialCapital; - BigDecimal shares = BigDecimal.ZERO; - int tradesCount = 0; - - // MDD 및 수익률 추적용 리스트 - List dailyPortfolioValue = new ArrayList<>(); - // 일일 수익률 리스트 (샤프 비율 계산에 사용) - List dailyReturns = new ArrayList<>(); - - BigDecimal peakValue = initialCapital; - BigDecimal maxDrawdown = BigDecimal.ZERO; - BigDecimal previousValue = initialCapital; // 전날 포트폴리오 가치 - + // 백테스팅 시뮬레이션 변수 초기화 + BigDecimal initialCapital = strategyDto.getInitialCapital(); // 초기 자본금 + BigDecimal cash = initialCapital; // 잔고 = 초기 자본금 + BigDecimal shares = BigDecimal.ZERO; // 보유 주식 수 + int tradesCount = 0; // 총 거래 횟수 + List dailyPortfolioValue = new ArrayList<>(); // MDD 및 수익률 추적용 리스트 + List dailyReturns = new ArrayList<>(); // 일일 수익률 리스트 (샤프 비율 계산에 사용) + BigDecimal peakValue = initialCapital; // 최고 포트폴리오 가치 + BigDecimal maxDrawdown = BigDecimal.ZERO; // 최대 낙폭 + BigDecimal previousValue = initialCapital; // 전날 포트폴리오 가치 + BigDecimal buyRatio = convertPercentToRatio(10, BigDecimal.ONE); // TODO : DTO 매수 비중 설정 + BigDecimal sellRatio = convertPercentToRatio(100, BigDecimal.ONE); // TODO : DTO 매도 비중 설정 + Integer buyBarIndex = null; // 현재 보유 주식의 매수 시점 바(Bar) 인덱스 + int defaultExitDays = strategyDto.getDefaultExitDays(); // 기본 청산 기간 + // 백테스팅 메인 반복문 for (int i = 0; i < series.getBarCount(); i++) { - LocalDateTime currentTime = series.getBar(i).getEndTime().toLocalDateTime(); - Num numClosePrice = series.getBar(i).getClosePrice(); - BigDecimal currentClosePrice = new BigDecimal(numClosePrice.toString()); - + LocalDateTime currentTime = series.getBar(i).getEndTime().toLocalDateTime(); // 장 종료 시간 + BigDecimal currentClosePrice = new BigDecimal(series.getBar(i).getClosePrice().toString()); // 현재 종가 + // 매수/매도 신호 평가 boolean shouldBuy = buyRule.isSatisfied(i); boolean shouldSell = sellRule.isSatisfied(i); - - // "매수" - if (shares.compareTo(BigDecimal.ZERO) == 0 && shouldBuy) { - BigDecimal buyShares = cash.divide(currentClosePrice, 8, RoundingMode.HALF_UP); - + // 기본 청산 기간(Default Exit Days) 조건 + boolean shouldExitByDays = false; + // 주식을 보유하고 있고, 매수 시점 기록이 있으며, 청산 기간이 0보다 큰 경우에만 체크 + if (shares.compareTo(BigDecimal.ZERO) > 0 && buyBarIndex != null && defaultExitDays > 0) { + if (i - buyBarIndex >= defaultExitDays) { + shouldExitByDays = true; // 매수 후 기본 청산 기간 도달 + log.debug("[{}] DEFAULT EXIT by {} days", currentTime.toLocalDate(), defaultExitDays); + } + } + // 매수 + if (shouldBuy) { + BigDecimal cashToUse = cash.multiply(buyRatio); // 매수 비중 적용 + // 거래 가능한 최대 주식 개수 + BigDecimal buyShares = cashToUse.divide(currentClosePrice, 8, RoundingMode.DOWN); // 거래 로그 기록 - tradeLogs.add(new TradeLog(TradeLog.Type.BUY, currentTime, currentClosePrice, buyShares)); - - shares = buyShares; - cash = BigDecimal.ZERO; - tradesCount++; - log.info("[{}] BUY at {}", currentTime.toLocalDate(), currentClosePrice); + if (buyShares.compareTo(BigDecimal.ZERO) > 0) { + BigDecimal transactionCost = buyShares.multiply(currentClosePrice); + tradeLogs.add(new TradeLog(TradeLog.Type.BUY, currentTime, currentClosePrice, buyShares)); + shares = shares.add(buyShares); // 매수 주식 수 + cash = cash.subtract(transactionCost); // 잔고에서 매수 대금 차감 + tradesCount++; // 거래 횟수 증가 + if (buyBarIndex == null) { + buyBarIndex = i; // 첫 매수 시점에만 인덱스 기록 + } + log.debug("[{}] BUY at {}", currentTime.toLocalDate(), currentClosePrice); + } } - // "매도" - else if (shares.compareTo(BigDecimal.ZERO) > 0 && shouldSell) { - BigDecimal tradeShares = shares; // 매도 주식 수 - BigDecimal tradeValue = shares.multiply(currentClosePrice); - + // 매도 + else if (shares.compareTo(BigDecimal.ZERO) > 0 && (shouldSell || shouldExitByDays)) { + // 매도 대금 계산 - 주식 수 * 현재가 + BigDecimal sharesToSell = shares.multiply(sellRatio).setScale(8, RoundingMode.DOWN); // 매도 비중 적용 + BigDecimal tradeValue = sharesToSell.multiply(currentClosePrice); // 거래 로그 기록 - tradeLogs.add(new TradeLog(TradeLog.Type.SELL, currentTime, currentClosePrice, tradeShares)); - - cash = tradeValue; - shares = BigDecimal.ZERO; - log.info("[{}] SELL at {}", currentTime.toLocalDate(), currentClosePrice); + TradeLog.Type logType = shouldExitByDays ? TradeLog.Type.SELL_FORCED : TradeLog.Type.SELL; // 강제 청산 여부에 따른 로그 타입 설정 + tradeLogs.add(new TradeLog(logType, currentTime, currentClosePrice, sharesToSell)); + shares = shares.subtract(sharesToSell); // 매도 주식 수 차감 + cash = cash.add(tradeValue); // 잔고에서 매도 대금 추가 + tradesCount++; // 거래 횟수 증가 + buyBarIndex = null; // 매도 시점 인덱스 초기화 + log.debug("[{}] SELL at {}", currentTime.toLocalDate(), currentClosePrice); } // 일일 포트폴리오 가치 계산 @@ -133,16 +151,17 @@ else if (shares.compareTo(BigDecimal.ZERO) > 0 && shouldSell) { dailyReturns.add(dailyReturn); } previousValue = currentTotalValue; - + // 최대 낙폭 계산 if (currentTotalValue.compareTo(peakValue) > 0) peakValue = currentTotalValue; BigDecimal drawdown = peakValue.subtract(currentTotalValue).divide(peakValue, 8, RoundingMode.HALF_UP); if (drawdown.compareTo(maxDrawdown) > 0) maxDrawdown = drawdown; } + // 백테스팅 메인 반복문 종료 // 최종 지표 계산 및 저장 - BacktestRunMetrics metrics = calculateMetrics(backtestRun, initialCapital, tradeLogs, dailyPortfolioValue, dailyReturns, maxDrawdown, tradesCount); - - backtestRunMetricsRepository.save(metrics); + backtestRunMetricsRepository.save( + calculateMetrics(backtestRun, initialCapital, tradeLogs, dailyPortfolioValue, dailyReturns, maxDrawdown, tradesCount) + ); backtestRun.setStatus(BacktestStatus.COMPLETED); } catch (Exception e) { @@ -161,37 +180,33 @@ else if (shares.compareTo(BigDecimal.ZERO) > 0 && shouldSell) { private BacktestRunMetrics calculateMetrics(BacktestRun backtestRun, BigDecimal initialCapital, List tradeLogs, List dailyPortfolioValue, List dailyReturns, BigDecimal maxDrawdown, int tradesCount) { - - BigDecimal finalPortfolioValue = dailyPortfolioValue.getLast(); - BigDecimal totalReturnPct = finalPortfolioValue.divide(initialCapital, 4, RoundingMode.HALF_UP) - .subtract(BigDecimal.ONE); - + // 총 수익률 계산 - 백분율로 변환 + BigDecimal totalReturnPct = dailyPortfolioValue.getLast() // 최종 포트폴리오 가치 + .divide(initialCapital, 8, RoundingMode.HALF_UP) // 초기 자본 대비 비율, 소수점 8자리 반올림 + .subtract(BigDecimal.ONE) // 비율 (0.10) + .multiply(BigDecimal.valueOf(100)) // 백분율 (10.00) + .setScale(4, RoundingMode.HALF_UP); // 소수점 4자리 반올림 + // 최대 낙폭 백분율 변환 - -100 곱한 후 소수점 4자리 반올림 BigDecimal maxDrawdownPct = maxDrawdown.multiply(BigDecimal.valueOf(-100)).setScale(4, RoundingMode.HALF_UP); - + // 샤프 비율 계산 BigDecimal sharpeRatio = calculateSharpeRatio(dailyReturns); + // 평균 보유 기간 계산 BigDecimal avgHoldDays = calculateAvgHoldDays(tradeLogs); - return BacktestRunMetrics.builder() - .backtestRun(backtestRun) - .totalReturn(totalReturnPct) - .maxDrawdown(maxDrawdownPct) - .sharpeRatio(sharpeRatio) - .avgHoldDays(avgHoldDays) - .tradesCount(tradesCount) - .build(); + return BacktestRunMetrics.fromDto(backtestRun, totalReturnPct, maxDrawdownPct, sharpeRatio, avgHoldDays, tradesCount); } private BigDecimal calculateSharpeRatio(List dailyReturns) { if (dailyReturns.isEmpty()) return BigDecimal.ZERO; - + // 일일 수익률의 합계와 평균 계산 BigDecimal sum = dailyReturns.stream().reduce(BigDecimal.ZERO, BigDecimal::add); BigDecimal mean = sum.divide(BigDecimal.valueOf(dailyReturns.size()), 8, RoundingMode.HALF_UP); + // 분산, 표준편차 계산 BigDecimal varianceSum = dailyReturns.stream() .map(r -> r.subtract(mean)) .map(d -> d.multiply(d)) .reduce(BigDecimal.ZERO, BigDecimal::add); - BigDecimal variance = varianceSum.divide(BigDecimal.valueOf(dailyReturns.size()), 8, RoundingMode.HALF_UP); BigDecimal standardDeviation = BigDecimal.valueOf(Math.sqrt(variance.doubleValue())); @@ -208,6 +223,7 @@ private BigDecimal calculateAvgHoldDays(List tradeLogs) { List holdDurations = new ArrayList<>(); LocalDateTime currentBuyTime = null; + // 매수-매도 쌍을 찾아 보유 기간 계산 for (TradeLog log : tradeLogs) { if (log.type == TradeLog.Type.BUY) { currentBuyTime = log.time; @@ -217,13 +233,25 @@ private BigDecimal calculateAvgHoldDays(List tradeLogs) { currentBuyTime = null; } } - if (holdDurations.isEmpty()) return BigDecimal.ZERO; - + // 총 기간 합산 long totalDays = holdDurations.stream().reduce(0L, Long::sum); - BigDecimal avgHoldDays = BigDecimal.valueOf(totalDays) - .divide(BigDecimal.valueOf(holdDurations.size()), 2, RoundingMode.HALF_UP); + // 평균 보유 일수 계산 후 소수점 2자리 반올림 + return BigDecimal.valueOf(totalDays) + .divide(BigDecimal.valueOf(holdDurations.size()), 2, RoundingMode.HALF_UP); + } - return avgHoldDays; + // ---------------------------------------------------------------------- + // 퍼센티지(int)를 소수점 비율(BigDecimal)로 변환하는 헬퍼 함수 + // ---------------------------------------------------------------------- + private BigDecimal convertPercentToRatio(Integer percent, BigDecimal defaultValue) { + if (percent == null || percent < 0 || percent > 100) { + // 유효하지 않은 값이거나 null일 경우 기본값(1.00 또는 정의된 값) 반환 + return defaultValue; + } + // 정수 %를 BigDecimal로 변환 후 100으로 나누어 비율을 만듦 + // (예: 10 -> 0.10) + return BigDecimal.valueOf(percent) + .divide(BigDecimal.valueOf(100), 4, RoundingMode.HALF_UP); } } \ No newline at end of file diff --git a/backend/src/main/java/org/sejongisc/backend/backtest/service/Ta4jHelperService.java b/backend/src/main/java/org/sejongisc/backend/backtest/service/Ta4jHelperService.java index fb5495c7..5bfb7b1c 100644 --- a/backend/src/main/java/org/sejongisc/backend/backtest/service/Ta4jHelperService.java +++ b/backend/src/main/java/org/sejongisc/backend/backtest/service/Ta4jHelperService.java @@ -3,6 +3,7 @@ import lombok.RequiredArgsConstructor; import org.sejongisc.backend.backtest.dto.StrategyCondition; import org.sejongisc.backend.backtest.dto.StrategyOperand; +import org.sejongisc.backend.common.exception.CustomException; import org.sejongisc.backend.stock.entity.PriceData; import org.springframework.stereotype.Service; import org.ta4j.core.BarSeries; @@ -32,10 +33,10 @@ public class Ta4jHelperService { */ public BarSeries createBarSeries(List priceDataList) { // BarSeries 이름에 Ticker 추가 - BarSeries series = new BaseBarSeries(priceDataList.get(0).getTicker()); + BarSeries series = new BaseBarSeries(priceDataList.getFirst().getTicker()); for (PriceData p : priceDataList) { series.addBar( - p.getDate().atStartOfDay(ZoneId.systemDefault()), + p.getDate().atStartOfDay(ZoneId.of("Asia/Seoul")), // 시작 시간을 한국 시간대로 설정 p.getOpen(), p.getHigh(), p.getLow(), p.getClosePrice(), p.getVolume() ); } @@ -47,9 +48,7 @@ public BarSeries createBarSeries(List priceDataList) { * "isAbsolute" 로직을 포함합니다. */ public Rule buildCombinedRule(List conditions, BarSeries series, Map> indicatorCache) { - if (series.isEmpty()) { - throw new IllegalArgumentException("Cannot build rules on an empty series."); - } + // 기본 참/거짓 Rule 생성 Num sampleNum = series.getBar(0).getClosePrice(); Num one = sampleNum.numOf(1); Num zero = sampleNum.numOf(0); diff --git a/backend/src/main/java/org/sejongisc/backend/stock/repository/PriceDataRepository.java b/backend/src/main/java/org/sejongisc/backend/stock/repository/PriceDataRepository.java index d2ad97a8..9f4d2714 100644 --- a/backend/src/main/java/org/sejongisc/backend/stock/repository/PriceDataRepository.java +++ b/backend/src/main/java/org/sejongisc/backend/stock/repository/PriceDataRepository.java @@ -3,6 +3,7 @@ import org.sejongisc.backend.stock.entity.PriceData; import org.sejongisc.backend.stock.entity.PriceDataId; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; import java.time.LocalDate; @@ -14,4 +15,9 @@ public interface PriceDataRepository extends JpaRepository findByTickerAndDateBetweenOrderByDateAsc(String ticker, LocalDate startDate, LocalDate endDate); List findByTicker(String ticker); Optional findTopByTickerOrderByDateDesc(String ticker); + /** + * PriceData 테이블에 존재하는 모든 유니크한 티커(ticker) 목록을 조회합니다. + */ + @Query("SELECT DISTINCT p.ticker FROM PriceData p") + List findDistinctTickers(); } \ No newline at end of file