diff --git a/backend/src/main/java/org/dgu/backend/controller/CandleInfoController.java b/backend/src/main/java/org/dgu/backend/controller/CandleInfoController.java index 8deb250..1091fd0 100644 --- a/backend/src/main/java/org/dgu/backend/controller/CandleInfoController.java +++ b/backend/src/main/java/org/dgu/backend/controller/CandleInfoController.java @@ -35,7 +35,7 @@ public ResponseEntity> collectCandleData( @RequestParam("end_date") @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime endDate, @RequestParam("candle_name") String candleName) { - candleDataCollector.collectCandleData(koreanName, startDate, endDate, candleName); + candleDataCollector.collectCandleData(koreanName, candleName, startDate, endDate); return ApiResponse.onSuccess(SuccessStatus.SUCCESS_CANDLE_INFOS); } diff --git a/backend/src/main/java/org/dgu/backend/repository/CandleInfoRepository.java b/backend/src/main/java/org/dgu/backend/repository/CandleInfoRepository.java index bf4c496..b246990 100644 --- a/backend/src/main/java/org/dgu/backend/repository/CandleInfoRepository.java +++ b/backend/src/main/java/org/dgu/backend/repository/CandleInfoRepository.java @@ -10,9 +10,11 @@ import java.util.List; public interface CandleInfoRepository extends JpaRepository { - @Query("SELECT c FROM CandleInfo c WHERE c.candle = :candle AND c.dateTime BETWEEN :startDate AND :endDate ORDER BY c.dateTime") - List findFilteredCandleInfo(Candle candle, LocalDateTime startDate, LocalDateTime endDate); + @Query("SELECT c FROM CandleInfo c WHERE c.market = :market AND c.candle = :candle AND c.dateTime BETWEEN :startDate AND :endDate ORDER BY c.dateTime") + List findFilteredCandleInfo(Market market, Candle candle, LocalDateTime startDate, LocalDateTime endDate); @Query("SELECT c FROM CandleInfo c WHERE c.market = :market AND c.candle = :candle AND c.dateTime > :startDate ORDER BY c.dateTime") List findByMarketAndCandleAndDateTimeAfter(Market market, Candle candle, LocalDateTime startDate); + + CandleInfo findTopByMarketAndCandleOrderByTimestampDesc(Market market, Candle candle); } \ No newline at end of file diff --git a/backend/src/main/java/org/dgu/backend/service/BackTestingCalculator.java b/backend/src/main/java/org/dgu/backend/service/BackTestingCalculator.java index e2134bc..0123b9e 100644 --- a/backend/src/main/java/org/dgu/backend/service/BackTestingCalculator.java +++ b/backend/src/main/java/org/dgu/backend/service/BackTestingCalculator.java @@ -34,14 +34,6 @@ public class BackTestingCalculator { private static Long lowValueStrategy; private static Long highLossValueStrategy; - // 캔들 차트에서 중복 데이터를 제거하는 메서드 - public List removeDuplicatedCandles(List candles) { - Set uniqueDates = new HashSet<>(); - return candles.stream() - .filter(candle -> uniqueDates.add(candle.getDateTime())) - .collect(Collectors.toList()); - } - // 지수 이동평균선을 계산하는 메서드 public List calculateEMA(List candles, int date) { Double k = 2.0 / (date + 1); diff --git a/backend/src/main/java/org/dgu/backend/service/BackTestingServiceImpl.java b/backend/src/main/java/org/dgu/backend/service/BackTestingServiceImpl.java index f3f96b3..6bdb26b 100644 --- a/backend/src/main/java/org/dgu/backend/service/BackTestingServiceImpl.java +++ b/backend/src/main/java/org/dgu/backend/service/BackTestingServiceImpl.java @@ -7,6 +7,7 @@ import org.dgu.backend.exception.PortfolioErrorResult; import org.dgu.backend.exception.PortfolioException; import org.dgu.backend.repository.*; +import org.dgu.backend.util.CandleUtil; import org.dgu.backend.util.DateUtil; import org.dgu.backend.util.JwtUtil; import org.springframework.stereotype.Service; @@ -15,14 +16,16 @@ import java.util.*; @Service -@Transactional @RequiredArgsConstructor public class BackTestingServiceImpl implements BackTestingService { private final JwtUtil jwtUtil; private final DateUtil dateUtil; + private final CandleUtil candleUtil; private final BackTestingCalculator backTestingCalculator; + private final CandleInfoUpdater candleInfoUpdater; private final CandleInfoRepository candleInfoRepository; private final CandleRepository candleRepository; + private final MarketRepository marketRepository; private final PortfolioRepository portfolioRepository; private final PortfolioOptionRepository portfolioOptionRepository; private final TradingResultRepository tradingResultRepository; @@ -32,12 +35,27 @@ public class BackTestingServiceImpl implements BackTestingService { // 백테스팅 결과를 생성하는 메서드 @Override public BackTestingDto.BackTestingResponse createBackTestingResult(String authorizationHeader, BackTestingDto.StepInfo stepInfo) { + updateCandleInfo("비트코인", stepInfo.getCandleName()); + + return fetchBackTestingResult(authorizationHeader, stepInfo); + } + + // 캔들 정보 최신화 메서드 + @Transactional + protected void updateCandleInfo(String koreanName, String candleName) { + candleInfoUpdater.ensureCandleInfoUpToDate(koreanName, candleName); + } + + // 최신화된 캔들 정보를 사용해 백테스팅 결과를 생성하는 메서드 + @Transactional + protected BackTestingDto.BackTestingResponse fetchBackTestingResult(String authorizationHeader, BackTestingDto.StepInfo stepInfo) { + Market market = marketRepository.findByKoreanName("비트코인"); Candle candle = candleRepository.findByCandleName(stepInfo.getCandleName()); LocalDateTime startDate = dateUtil.convertToLocalDateTime(stepInfo.getStartDate()); LocalDateTime endDate = dateUtil.convertToLocalDateTime(stepInfo.getEndDate()); - List candles = candleInfoRepository.findFilteredCandleInfo(candle, startDate, endDate); - candles = backTestingCalculator.removeDuplicatedCandles(candles); // 중복 데이터 제거 + List candles = candleInfoRepository.findFilteredCandleInfo(market, candle, startDate, endDate); + candles = candleUtil.removeDuplicatedCandles(candles); // 골든 크로스 지점 찾기 List goldenCrossPoints = backTestingCalculator.findGoldenCrossPoints(candles, stepInfo); @@ -58,6 +76,7 @@ public BackTestingDto.BackTestingResponse createBackTestingResult(String authori // 백테스팅 결과를 저장하는 메서드 @Override + @Transactional public void saveBackTestingResult(String authorizationHeader, BackTestingDto.SavingRequest savingRequest) { User user = jwtUtil.getUserFromHeader(authorizationHeader); @@ -76,6 +95,7 @@ public void saveBackTestingResult(String authorizationHeader, BackTestingDto.Sav // 백테스팅 최근 결과를 반환하는 메서드 @Override + @Transactional public BackTestingDto.BackTestingResponse getRecentBackTestingResult(String authorizationHeader) { User user = jwtUtil.getUserFromHeader(authorizationHeader); @@ -90,7 +110,8 @@ public BackTestingDto.BackTestingResponse getRecentBackTestingResult(String auth } // 백테스팅 결과를 임시 저장하는 메서드 - private void saveTempBackTestingResult(String authorizationHeader, BackTestingDto.StepInfo stepInfo, BackTestingDto.BackTestingResponse backTestingResponse) { + @Transactional + protected void saveTempBackTestingResult(String authorizationHeader, BackTestingDto.StepInfo stepInfo, BackTestingDto.BackTestingResponse backTestingResponse) { User user = jwtUtil.getUserFromHeader(authorizationHeader); // 기존에 있던 거래 로그 삭제 diff --git a/backend/src/main/java/org/dgu/backend/service/CandleInfoService.java b/backend/src/main/java/org/dgu/backend/service/CandleInfoService.java index 5ee4f02..bbddfc0 100644 --- a/backend/src/main/java/org/dgu/backend/service/CandleInfoService.java +++ b/backend/src/main/java/org/dgu/backend/service/CandleInfoService.java @@ -3,5 +3,5 @@ import java.time.LocalDateTime; public interface CandleInfoService { - void getCandleInfo(String marketKoreanName, LocalDateTime to, int count, String candleType); + void getCandleInfo(String koreanName, LocalDateTime to, int count, String candleName); } \ No newline at end of file diff --git a/backend/src/main/java/org/dgu/backend/service/CandleInfoServiceImpl.java b/backend/src/main/java/org/dgu/backend/service/CandleInfoServiceImpl.java index 489285c..a880dec 100644 --- a/backend/src/main/java/org/dgu/backend/service/CandleInfoServiceImpl.java +++ b/backend/src/main/java/org/dgu/backend/service/CandleInfoServiceImpl.java @@ -28,6 +28,7 @@ public class CandleInfoServiceImpl implements CandleInfoService { private final CandleRepository candleRepository; private final RestTemplate restTemplate; + // 업비트 API를 통해 캔들 정보를 가져오는 메서드 @Override public void getCandleInfo(String koreanName, LocalDateTime to, int count, String candleName) { Market market = marketRepository.findByKoreanName(koreanName); diff --git a/backend/src/main/java/org/dgu/backend/service/CandleInfoUpdater.java b/backend/src/main/java/org/dgu/backend/service/CandleInfoUpdater.java new file mode 100644 index 0000000..149dfdc --- /dev/null +++ b/backend/src/main/java/org/dgu/backend/service/CandleInfoUpdater.java @@ -0,0 +1,48 @@ +package org.dgu.backend.service; + +import lombok.RequiredArgsConstructor; +import org.dgu.backend.domain.Candle; +import org.dgu.backend.domain.CandleInfo; +import org.dgu.backend.domain.Market; +import org.dgu.backend.repository.CandleInfoRepository; +import org.dgu.backend.repository.CandleRepository; +import org.dgu.backend.repository.MarketRepository; +import org.dgu.backend.util.CandleDataCollector; +import org.dgu.backend.util.CandleUtil; +import org.dgu.backend.util.DateUtil; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; +import java.util.Objects; + +@Component +@RequiredArgsConstructor +public class CandleInfoUpdater { + private final MarketRepository marketRepository; + private final CandleRepository candleRepository; + private final CandleInfoRepository candleInfoRepository; + private final CandleDataCollector candleDataCollector; + private final CandleUtil candleUtil; + private final DateUtil dateUtil; + + // 현재 시각을 기준으로 캔들 정보를 최신화하는 메서드 + public void ensureCandleInfoUpToDate(String koreanName, String candleName) { + Market market = marketRepository.findByKoreanName(koreanName); + Candle candle = candleRepository.findByCandleName(candleName); + + // 가장 최근 캔들 차트 + CandleInfo latestCandleInfo = candleInfoRepository.findTopByMarketAndCandleOrderByTimestampDesc(market, candle); + int candleInterval = candleUtil.calculateCandleInterval(candleName); + LocalDateTime startDate; + if (Objects.isNull(latestCandleInfo)) { + startDate = dateUtil.convertToLocalDateTime("2019-01-01T00:00:00"); + } else { + startDate = latestCandleInfo.getDateTime(); + if (startDate.plusMinutes(candleInterval).isAfter(LocalDateTime.now())) { + return; + } + } + + candleDataCollector.collectCandleData(koreanName, candleName, startDate, LocalDateTime.now()); + } +} \ No newline at end of file diff --git a/backend/src/main/java/org/dgu/backend/service/ChartServiceImpl.java b/backend/src/main/java/org/dgu/backend/service/ChartServiceImpl.java index d8d9f05..fd32db5 100644 --- a/backend/src/main/java/org/dgu/backend/service/ChartServiceImpl.java +++ b/backend/src/main/java/org/dgu/backend/service/ChartServiceImpl.java @@ -11,50 +11,51 @@ import org.dgu.backend.repository.CandleInfoRepository; import org.dgu.backend.repository.CandleRepository; import org.dgu.backend.repository.MarketRepository; +import org.dgu.backend.util.CandleUtil; import org.springframework.stereotype.Service; import java.time.LocalDateTime; -import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; @Service -@Transactional @RequiredArgsConstructor public class ChartServiceImpl implements ChartService { private final MarketRepository marketRepository; private final CandleRepository candleRepository; private final CandleInfoRepository candleInfoRepository; - - private static final List SEVEN_DAY_CANDLES = Arrays.asList("minutes1", "minutes3", "minutes5", "minutes10", "minutes15", "minutes30"); - private static final List SIX_MONTH_CANDLES = Arrays.asList("minutes60", "minutes240"); + private final CandleInfoUpdater candleInfoUpdater; + private final CandleUtil candleUtil; // OHLCV 차트를 반환하는 메서드 @Override public List getOHLCVCharts(String koreanName, String candleName) { + updateCandleInfo(koreanName, candleName); + + return fetchUpdatedCandleInfo(koreanName, candleName); + } + + // 캔들 정보 최신화 메서드 + @Transactional + protected void updateCandleInfo(String koreanName, String candleName) { + candleInfoUpdater.ensureCandleInfoUpToDate(koreanName, candleName); + } + + // 최신화된 캔들 정보를 반환하는 메서드 + @Transactional + protected List fetchUpdatedCandleInfo(String koreanName, String candleName) { Market market = marketRepository.findByKoreanName(koreanName); Candle candle = candleRepository.findByCandleName(candleName); - - LocalDateTime startDate = getStartDateByCandleName(candleName); + LocalDateTime startDate = candleUtil.getStartDateByCandleName(candleName); List candleInfos = candleInfoRepository.findByMarketAndCandleAndDateTimeAfter(market, candle, startDate); if (candleInfos.isEmpty()) { throw new ChartException(ChartErrorResult.NOT_FOUND_CHARTS); } + candleInfos = candleUtil.removeDuplicatedCandles(candleInfos); + return candleInfos.stream() .map(ChartDto.OHLCVResponse::of) .collect(Collectors.toList()); } - - // 캔들 종류에 따라 시작 기간을 계산해 반환하는 메서드 - private LocalDateTime getStartDateByCandleName(String candleName) { - LocalDateTime now = LocalDateTime.now(); - if (SEVEN_DAY_CANDLES.contains(candleName)) { - return now.minusDays(7); - } else if (SIX_MONTH_CANDLES.contains(candleName)) { - return now.minusMonths(6); - } else { - return LocalDateTime.of(2019, 1, 1, 0, 0); - } - } -} \ No newline at end of file +} diff --git a/backend/src/main/java/org/dgu/backend/util/CandleDataCollector.java b/backend/src/main/java/org/dgu/backend/util/CandleDataCollector.java index db953d3..d3ac4fc 100644 --- a/backend/src/main/java/org/dgu/backend/util/CandleDataCollector.java +++ b/backend/src/main/java/org/dgu/backend/util/CandleDataCollector.java @@ -15,13 +15,14 @@ @RequiredArgsConstructor public class CandleDataCollector { private final CandleInfoService candleInfoService; + private final CandleUtil candleUtil; private final int batchSize = 200; private final RateLimiter rateLimiter = RateLimiter.create(10.0); // 초당 요청 허용량 10개로 제한 private final long retryDelayMillis = 100; // 재시도 대기 시간 (0.1초) - public void collectCandleData(String koreanName, LocalDateTime startDate, LocalDateTime endDate, String candleName) { + public void collectCandleData(String koreanName, String candleName, LocalDateTime startDate, LocalDateTime endDate) { // 캔들을 분 기준으로 변환 - int candleInterval = calculateCandleInterval(candleName); + int candleInterval = candleUtil.calculateCandleInterval(candleName); // 시작 시간부터 종료 시간까지의 총 분 수 계산 long totalMinutes = Duration.between(startDate, endDate).toMinutes(); @@ -79,18 +80,4 @@ public void collectCandleData(String koreanName, LocalDateTime startDate, LocalD e.printStackTrace(); } } - - // 캔들을 분 기준으로 변환하는 메서드 - private int calculateCandleInterval(String candleType) { - switch (candleType) { - case "days": - return 1440; // 1일(24시간 * 60분) - case "weeks": - return 10080; // 1주(7일 * 24시간 * 60분) - case "months": - return 43200; // 1개월(30일 * 24시간 * 60분) - default: // minutesN 형식의 캔들 타입 처리 - return Integer.parseInt(candleType.replace("minutes", "")); - } - } } \ No newline at end of file diff --git a/backend/src/main/java/org/dgu/backend/util/CandleUtil.java b/backend/src/main/java/org/dgu/backend/util/CandleUtil.java new file mode 100644 index 0000000..12fc9c3 --- /dev/null +++ b/backend/src/main/java/org/dgu/backend/util/CandleUtil.java @@ -0,0 +1,54 @@ +package org.dgu.backend.util; + +import lombok.RequiredArgsConstructor; +import org.dgu.backend.domain.CandleInfo; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + + +@Component +@RequiredArgsConstructor +public class CandleUtil { + private static final List SEVEN_DAY_CANDLES = Arrays.asList("minutes1", "minutes3", "minutes5", "minutes10", "minutes15", "minutes30"); + private static final List SIX_MONTH_CANDLES = Arrays.asList("minutes60", "minutes240"); + + // 캔들을 분 기준으로 변환하는 메서드 + public int calculateCandleInterval(String candleName) { + switch (candleName) { + case "days": + return 1440; // 1일(24시간 * 60분) + case "weeks": + return 10080; // 1주(7일 * 24시간 * 60분) + case "months": + return 43200; // 1개월(30일 * 24시간 * 60분) + default: // minutesN 형식의 캔들 타입 처리 + return Integer.parseInt(candleName.replace("minutes", "")); + } + } + + // 캔들 종류에 따라 시작 기간을 계산해 반환하는 메서드 + public LocalDateTime getStartDateByCandleName(String candleName) { + LocalDateTime now = LocalDateTime.now(); + if (SEVEN_DAY_CANDLES.contains(candleName)) { + return now.minusDays(7); + } else if (SIX_MONTH_CANDLES.contains(candleName)) { + return now.minusMonths(6); + } else { + return LocalDateTime.of(2019, 1, 1, 0, 0); + } + } + + // 캔들 차트에서 중복 데이터를 제거하는 메서드 + public List removeDuplicatedCandles(List candles) { + Set uniqueDates = new HashSet<>(); + return candles.stream() + .filter(candle -> uniqueDates.add(candle.getDateTime())) + .collect(Collectors.toList()); + } +} \ No newline at end of file