Skip to content

Commit

Permalink
Merge pull request #58 from CSID-DGU/feature/#53/chart
Browse files Browse the repository at this point in the history
[feat] : 차트 관련 요청 시, 최신화 후 반환하는 기능 추가
  • Loading branch information
bbbang105 authored Jun 9, 2024
2 parents 91726f9 + 3196ffa commit cf79b1f
Show file tree
Hide file tree
Showing 10 changed files with 158 additions and 52 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public ResponseEntity<ApiResponse<Object>> 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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@
import java.util.List;

public interface CandleInfoRepository extends JpaRepository<CandleInfo,Long> {
@Query("SELECT c FROM CandleInfo c WHERE c.candle = :candle AND c.dateTime BETWEEN :startDate AND :endDate ORDER BY c.dateTime")
List<CandleInfo> 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<CandleInfo> 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<CandleInfo> findByMarketAndCandleAndDateTimeAfter(Market market, Candle candle, LocalDateTime startDate);

CandleInfo findTopByMarketAndCandleOrderByTimestampDesc(Market market, Candle candle);
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,6 @@ public class BackTestingCalculator {
private static Long lowValueStrategy;
private static Long highLossValueStrategy;

// 캔들 차트에서 중복 데이터를 제거하는 메서드
public List<CandleInfo> removeDuplicatedCandles(List<CandleInfo> candles) {
Set<LocalDateTime> uniqueDates = new HashSet<>();
return candles.stream()
.filter(candle -> uniqueDates.add(candle.getDateTime()))
.collect(Collectors.toList());
}

// 지수 이동평균선을 계산하는 메서드
public List<BackTestingDto.EMAInfo> calculateEMA(List<CandleInfo> candles, int date) {
Double k = 2.0 / (date + 1);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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<CandleInfo> candles = candleInfoRepository.findFilteredCandleInfo(candle, startDate, endDate);
candles = backTestingCalculator.removeDuplicatedCandles(candles); // 중복 데이터 제거
List<CandleInfo> candles = candleInfoRepository.findFilteredCandleInfo(market, candle, startDate, endDate);
candles = candleUtil.removeDuplicatedCandles(candles);

// 골든 크로스 지점 찾기
List<LocalDateTime> goldenCrossPoints = backTestingCalculator.findGoldenCrossPoints(candles, stepInfo);
Expand All @@ -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);

Expand All @@ -76,6 +95,7 @@ public void saveBackTestingResult(String authorizationHeader, BackTestingDto.Sav

// 백테스팅 최근 결과를 반환하는 메서드
@Override
@Transactional
public BackTestingDto.BackTestingResponse getRecentBackTestingResult(String authorizationHeader) {
User user = jwtUtil.getUserFromHeader(authorizationHeader);

Expand All @@ -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);

// 기존에 있던 거래 로그 삭제
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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());
}
}
41 changes: 21 additions & 20 deletions backend/src/main/java/org/dgu/backend/service/ChartServiceImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> SEVEN_DAY_CANDLES = Arrays.asList("minutes1", "minutes3", "minutes5", "minutes10", "minutes15", "minutes30");
private static final List<String> SIX_MONTH_CANDLES = Arrays.asList("minutes60", "minutes240");
private final CandleInfoUpdater candleInfoUpdater;
private final CandleUtil candleUtil;

// OHLCV 차트를 반환하는 메서드
@Override
public List<ChartDto.OHLCVResponse> 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<ChartDto.OHLCVResponse> 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<CandleInfo> 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);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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", ""));
}
}
}
54 changes: 54 additions & 0 deletions backend/src/main/java/org/dgu/backend/util/CandleUtil.java
Original file line number Diff line number Diff line change
@@ -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<String> SEVEN_DAY_CANDLES = Arrays.asList("minutes1", "minutes3", "minutes5", "minutes10", "minutes15", "minutes30");
private static final List<String> 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<CandleInfo> removeDuplicatedCandles(List<CandleInfo> candles) {
Set<LocalDateTime> uniqueDates = new HashSet<>();
return candles.stream()
.filter(candle -> uniqueDates.add(candle.getDateTime()))
.collect(Collectors.toList());
}
}

0 comments on commit cf79b1f

Please sign in to comment.