From 5e91c6e9301b93c2fb33db6baa11744365d92418 Mon Sep 17 00:00:00 2001 From: JiHwan Date: Sun, 12 Jan 2025 15:46:28 +0900 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20=EC=A0=84=EB=9E=B5=EB=B3=84=20?= =?UTF-8?q?=EB=A7=A4=EB=A7=A4=EB=82=B4=EC=97=AD=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EA=B0=9C=EB=B0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/config/RedisConfiguration.java | 5 +- .../module/history/controller/HistoryApi.java | 21 ++ .../history/controller/HistoryController.java | 18 +- .../dto/request/BackTestRequestDto.java | 35 +-- .../dto/response/BackTestResponseDto.java | 15 +- .../domain/repository/dao/HistoryDao.java | 32 +- .../implement/HistoryCacheProcessor.java | 15 + .../history/implement/HistoryCacheReader.java | 39 +++ .../history/implement/HistoryProcessor.java | 13 + .../history/implement/HistoryReader.java | 92 ++++++ .../history/service/HistoryService.java | 288 ++++++++---------- .../history/service/dto/StrategyInfoDto.java | 38 --- .../repository/StrategyQueryRepository.java | 8 +- .../impl/StrategyQueryRepositoryImpl.java | 123 +++++--- .../strategy/implement/StrategyReader.java | 60 ++-- .../strategy/service/StrategyService.java | 170 +++++------ 16 files changed, 535 insertions(+), 437 deletions(-) create mode 100644 src/main/java/com/tradin/module/history/controller/HistoryApi.java create mode 100644 src/main/java/com/tradin/module/history/implement/HistoryCacheProcessor.java create mode 100644 src/main/java/com/tradin/module/history/implement/HistoryCacheReader.java create mode 100644 src/main/java/com/tradin/module/history/implement/HistoryProcessor.java create mode 100644 src/main/java/com/tradin/module/history/implement/HistoryReader.java delete mode 100644 src/main/java/com/tradin/module/history/service/dto/StrategyInfoDto.java diff --git a/src/main/java/com/tradin/common/config/RedisConfiguration.java b/src/main/java/com/tradin/common/config/RedisConfiguration.java index b007b74..53647fb 100644 --- a/src/main/java/com/tradin/common/config/RedisConfiguration.java +++ b/src/main/java/com/tradin/common/config/RedisConfiguration.java @@ -10,13 +10,13 @@ import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.repository.configuration.EnableRedisRepositories; import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; -import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; @Configuration @RequiredArgsConstructor @EnableRedisRepositories public class RedisConfiguration { + private final RedisProperties redisProperties; @Bean @@ -37,7 +37,8 @@ public RedisTemplate redisTemplate() { public RedisTemplate historyRedisTemplate() { RedisTemplate redisTemplate = new RedisTemplate<>(); redisTemplate.setConnectionFactory(redisConnectionFactory()); - redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<>(HistoryDao.class)); + redisTemplate.setKeySerializer(new StringRedisSerializer()); + redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer()); return redisTemplate; } } diff --git a/src/main/java/com/tradin/module/history/controller/HistoryApi.java b/src/main/java/com/tradin/module/history/controller/HistoryApi.java new file mode 100644 index 0000000..229823e --- /dev/null +++ b/src/main/java/com/tradin/module/history/controller/HistoryApi.java @@ -0,0 +1,21 @@ +package com.tradin.module.history.controller; + +import com.tradin.common.response.TradinResponse; +import com.tradin.module.history.controller.dto.request.BackTestRequestDto; +import com.tradin.module.history.controller.dto.response.BackTestResponseDto; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.data.domain.Pageable; + +@Tag(name = "히스토리", description = "히스토리 관련 API") +public interface HistoryApi { + + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "성공") + }) + @Operation(summary = "백테스트 실행") + TradinResponse backTest(BackTestRequestDto request, Pageable pageable); + +} diff --git a/src/main/java/com/tradin/module/history/controller/HistoryController.java b/src/main/java/com/tradin/module/history/controller/HistoryController.java index 4afc8c1..2a43681 100644 --- a/src/main/java/com/tradin/module/history/controller/HistoryController.java +++ b/src/main/java/com/tradin/module/history/controller/HistoryController.java @@ -1,28 +1,28 @@ package com.tradin.module.history.controller; -import com.tradin.common.annotation.DisableAuthInSwagger; +import com.tradin.common.response.TradinResponse; import com.tradin.module.history.controller.dto.request.BackTestRequestDto; import com.tradin.module.history.controller.dto.response.BackTestResponseDto; import com.tradin.module.history.service.HistoryService; -import io.swagger.v3.oas.annotations.Operation; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import jakarta.validation.Valid; - @RestController @RequiredArgsConstructor @RequestMapping("/v1/histories") -public class HistoryController { +public class HistoryController implements HistoryApi { + private final HistoryService historyService; - @Operation(summary = "백테스트") - @DisableAuthInSwagger @GetMapping("") - public BackTestResponseDto backTest(@Valid @ModelAttribute BackTestRequestDto request) { - return historyService.backTest(request.toServiceDto()); + public TradinResponse backTest( + //TODO - TIMEZONE UTC로 통일하기 + @Valid @ModelAttribute BackTestRequestDto request, Pageable pageable) { + return TradinResponse.success(historyService.backTest(request.toServiceDto(), pageable)); } } diff --git a/src/main/java/com/tradin/module/history/controller/dto/request/BackTestRequestDto.java b/src/main/java/com/tradin/module/history/controller/dto/request/BackTestRequestDto.java index 16e537a..a278b3e 100644 --- a/src/main/java/com/tradin/module/history/controller/dto/request/BackTestRequestDto.java +++ b/src/main/java/com/tradin/module/history/controller/dto/request/BackTestRequestDto.java @@ -1,46 +1,31 @@ package com.tradin.module.history.controller.dto.request; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer; -import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer; import com.tradin.module.history.service.dto.BackTestDto; import com.tradin.module.strategy.domain.TradingType; import io.swagger.v3.oas.annotations.media.Schema; -import lombok.AllArgsConstructor; -import lombok.Getter; -import org.springframework.format.annotation.DateTimeFormat; - import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import java.time.LocalDate; +import org.springframework.format.annotation.DateTimeFormat; -@AllArgsConstructor -@Getter -public class BackTestRequestDto { - @NotNull(message = "StrategyId must not be null") - private long id; +@Schema(description = "백테스트 실행 DTO") +public record BackTestRequestDto( + @NotNull(message = "StrategyId must not be null") long id, - @NotBlank(message = "StrategyName must not be blank") - private String name; + @NotBlank(message = "StrategyName must not be blank") String name, @NotNull(message = "StartDate must not be null") - @JsonSerialize(using = LocalDateSerializer.class) - @JsonDeserialize(using = LocalDateDeserializer.class) @DateTimeFormat(pattern = "yyyy-MM-dd") - @Schema(description = "시작 연,월,일", example = "2021-01-01") - private LocalDate startDate; + @Schema(description = "시작 연,월,일", example = "2021-01-01") LocalDate startDate, @NotNull(message = "EndDate must not be null") - @JsonSerialize(using = LocalDateSerializer.class) - @JsonDeserialize(using = LocalDateDeserializer.class) @DateTimeFormat(pattern = "yyyy-MM-dd") - @Schema(description = "종료 연,월,일", example = "2021-01-01") - private LocalDate endDate; - @Schema(description = "매매 타입", example = "LONG") + @Schema(description = "종료 연,월,일", example = "2021-01-01") LocalDate endDate, @NotNull(message = "TradingType must not be null") - private TradingType tradingType; + + @Schema(description = "매매 타입", example = "LONG") TradingType tradingType +) { public BackTestDto toServiceDto() { return BackTestDto.of(id, name, startDate, endDate, tradingType); diff --git a/src/main/java/com/tradin/module/history/controller/dto/response/BackTestResponseDto.java b/src/main/java/com/tradin/module/history/controller/dto/response/BackTestResponseDto.java index db2f7b8..98f2a2d 100644 --- a/src/main/java/com/tradin/module/history/controller/dto/response/BackTestResponseDto.java +++ b/src/main/java/com/tradin/module/history/controller/dto/response/BackTestResponseDto.java @@ -1,19 +1,12 @@ package com.tradin.module.history.controller.dto.response; import com.tradin.module.history.domain.repository.dao.HistoryDao; -import com.tradin.module.history.service.dto.StrategyInfoDto; -import lombok.AllArgsConstructor; -import lombok.Getter; - +import com.tradin.module.strategy.domain.repository.dao.StrategyInfoDao; import java.util.List; -@AllArgsConstructor -@Getter -public class BackTestResponseDto { - private final StrategyInfoDto strategyInfoDto; - private final List historyDaos; +public record BackTestResponseDto(StrategyInfoDao strategyInfoDao, List historyDaos) { - public static BackTestResponseDto of(StrategyInfoDto strategyInfoDto, List historyDaos) { - return new BackTestResponseDto(strategyInfoDto, historyDaos); + public static BackTestResponseDto of(StrategyInfoDao strategyInfoDao, List historyDaos) { + return new BackTestResponseDto(strategyInfoDao, historyDaos); } } diff --git a/src/main/java/com/tradin/module/history/domain/repository/dao/HistoryDao.java b/src/main/java/com/tradin/module/history/domain/repository/dao/HistoryDao.java index bd3658a..795e384 100644 --- a/src/main/java/com/tradin/module/history/domain/repository/dao/HistoryDao.java +++ b/src/main/java/com/tradin/module/history/domain/repository/dao/HistoryDao.java @@ -4,28 +4,22 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.querydsl.core.annotations.QueryProjection; import com.tradin.module.strategy.domain.Position; -import lombok.Getter; -import lombok.Setter; +import io.swagger.v3.oas.annotations.media.Schema; -@Getter -@Setter -public class HistoryDao { - private final Long id; - private final Position entryPosition; - private final Position exitPosition; - private final double profitRate; - private double compoundProfitRate; +@Schema(description = "히스토리 정보") +public record HistoryDao( + @Schema(description = "히스토리 ID", example = "1") Long id, + @Schema(description = "진입 포지션") Position entryPosition, + @Schema(description = "종료 포지션") Position exitPosition, + @Schema(description = "수익률") double profitRate, + @Schema(description = "복리 수익률") double compoundProfitRate) { @JsonCreator @QueryProjection public HistoryDao(@JsonProperty("id") Long id, - @JsonProperty("entryPosition") Position entryPosition, - @JsonProperty("exitPosition") Position exitPosition, - @JsonProperty("profitRate") double profitRate) { - this.id = id; - this.entryPosition = entryPosition; - this.exitPosition = exitPosition; - this.profitRate = profitRate; - this.compoundProfitRate = 0; + @JsonProperty("entryPosition") Position entryPosition, + @JsonProperty("exitPosition") Position exitPosition, + @JsonProperty("profitRate") double profitRate) { + this(id, entryPosition, exitPosition, profitRate, 0); } -} +} \ No newline at end of file diff --git a/src/main/java/com/tradin/module/history/implement/HistoryCacheProcessor.java b/src/main/java/com/tradin/module/history/implement/HistoryCacheProcessor.java new file mode 100644 index 0000000..2470eca --- /dev/null +++ b/src/main/java/com/tradin/module/history/implement/HistoryCacheProcessor.java @@ -0,0 +1,15 @@ +package com.tradin.module.history.implement; + +import com.tradin.module.history.domain.repository.dao.HistoryDao; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class HistoryCacheProcessor { + + public void addHistoryCache(String cacheKey, List historyDaos) { + //TODO - 캐시 추가 로직 event + } +} diff --git a/src/main/java/com/tradin/module/history/implement/HistoryCacheReader.java b/src/main/java/com/tradin/module/history/implement/HistoryCacheReader.java new file mode 100644 index 0000000..e48fbd3 --- /dev/null +++ b/src/main/java/com/tradin/module/history/implement/HistoryCacheReader.java @@ -0,0 +1,39 @@ +package com.tradin.module.history.implement; + +import com.tradin.module.history.domain.repository.dao.HistoryDao; +import java.time.LocalDate; +import java.time.ZoneOffset; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.ZSetOperations; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class HistoryCacheReader { + + private final RedisTemplate historyRedisTemplate; + + public List readHistoryByIdAndPeriod(String cacheKey, LocalDate startDate, LocalDate endDate, Pageable pageable) { + + ZSetOperations historyCaches = historyRedisTemplate.opsForZSet(); + + Set historySet = historyCaches.rangeByScore( + cacheKey, + convertLocalDateToEpochSecond(startDate), + convertLocalDateToEpochSecond(endDate), + pageable.getOffset(), + pageable.getPageSize() + ); + + return new ArrayList<>(historySet); + } + + private long convertLocalDateToEpochSecond(LocalDate date) { + return date.atStartOfDay().toEpochSecond(ZoneOffset.UTC); + } +} diff --git a/src/main/java/com/tradin/module/history/implement/HistoryProcessor.java b/src/main/java/com/tradin/module/history/implement/HistoryProcessor.java new file mode 100644 index 0000000..6644113 --- /dev/null +++ b/src/main/java/com/tradin/module/history/implement/HistoryProcessor.java @@ -0,0 +1,13 @@ +package com.tradin.module.history.implement; + +import com.tradin.module.history.domain.repository.HistoryRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class HistoryProcessor { + + private final HistoryRepository historyRepository; + +} diff --git a/src/main/java/com/tradin/module/history/implement/HistoryReader.java b/src/main/java/com/tradin/module/history/implement/HistoryReader.java new file mode 100644 index 0000000..2a12cd0 --- /dev/null +++ b/src/main/java/com/tradin/module/history/implement/HistoryReader.java @@ -0,0 +1,92 @@ +package com.tradin.module.history.implement; + +import com.tradin.module.history.domain.repository.HistoryRepository; +import com.tradin.module.history.domain.repository.dao.HistoryDao; +import com.tradin.module.strategy.domain.TradingType; +import java.time.LocalDate; +import java.util.List; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class HistoryReader { + + private final HistoryCacheReader HistoryCacheReader; + private final HistoryCacheProcessor HistoryCacheProcessor; + private final HistoryRepository historyRepository; + + public List readHistoryByIdAndPeriodAndTradingType(Long strategyId, LocalDate startDate, LocalDate endDate, TradingType tradingType, Pageable pageable) { + String cacheKey = "strategyId:" + strategyId; + + List cachedHistories = HistoryCacheReader.readHistoryByIdAndPeriod(cacheKey, startDate, endDate, pageable); + + if (isCachedHistoriesEmpty(cachedHistories)) { + return findHistoryByIdAndPeriodAndTradingType(strategyId, startDate, endDate, tradingType, cacheKey); + } + + return filterByPeriodAndTradingType(cachedHistories, startDate, endDate, tradingType); + } + + private List findHistoryByIdAndPeriodAndTradingType(Long strategyId, LocalDate startDate, LocalDate endDate, TradingType tradingType, String cacheKey) { + List historyDaos = historyRepository.findHistoryDaoByStrategyId(strategyId); + addHistoryCache(cacheKey, historyDaos); + + return filterByPeriodAndTradingType(historyDaos, startDate, endDate, tradingType); + } + + private boolean isCachedHistoriesEmpty(List historyDaos) { + return historyDaos.isEmpty(); + } + + private void addHistoryCache(String cacheKey, List historyDaos) { + HistoryCacheProcessor.addHistoryCache(cacheKey, historyDaos); + } + + private List filterByPeriodAndTradingType(List historyDaos, LocalDate startDate, LocalDate endDate, TradingType tradingType) { + List filteredHistoryDaos = historyDaos.stream() + .filter(historyDao -> isInPeriod(historyDao, startDate, endDate)) + .filter(historyDao -> isCorrespondTradingType(historyDao, tradingType)) + .toList(); + + return calculateCompoundProfitRate(filteredHistoryDaos, startDate, endDate); + } + + private boolean isInPeriod(HistoryDao historyDao, LocalDate startDate, LocalDate endDate) { + return historyDao.entryPosition().getTime().toLocalDate().isAfter(startDate) && + historyDao.exitPosition().getTime().toLocalDate().isBefore(endDate); + } + + private boolean isCorrespondTradingType(HistoryDao historyDao, TradingType tradingType) { + return switch (tradingType) { + case LONG -> historyDao.entryPosition().getTradingType().equals(TradingType.LONG); + case SHORT -> historyDao.entryPosition().getTradingType().equals(TradingType.SHORT); + case BOTH -> historyDao.entryPosition().getTradingType().equals(TradingType.LONG) || + historyDao.entryPosition().getTradingType().equals(TradingType.SHORT); + default -> false; + }; + } + + private List calculateCompoundProfitRate(List filteredHistoryDaos, LocalDate startDate, LocalDate endDate) { + double[] cumulativeProfitRate = {0.0}; + + return filteredHistoryDaos.stream() + .map(history -> { + cumulativeProfitRate[0] = calculateCompound(cumulativeProfitRate[0], history.profitRate()); + return new HistoryDao( + history.id(), + history.entryPosition(), + history.exitPosition(), + history.profitRate(), + cumulativeProfitRate[0] + ); + }) + .collect(Collectors.toList()); + } + + private double calculateCompound(double cumulativeProfitRate, double newProfitRate) { + return (1 + cumulativeProfitRate / 100) * (1 + newProfitRate / 100) - 1; + } +} diff --git a/src/main/java/com/tradin/module/history/service/HistoryService.java b/src/main/java/com/tradin/module/history/service/HistoryService.java index b90b888..f74b440 100644 --- a/src/main/java/com/tradin/module/history/service/HistoryService.java +++ b/src/main/java/com/tradin/module/history/service/HistoryService.java @@ -1,178 +1,138 @@ package com.tradin.module.history.service; -import com.tradin.common.exception.TradinException; import com.tradin.module.history.controller.dto.response.BackTestResponseDto; -import com.tradin.module.history.domain.History; -import com.tradin.module.history.domain.repository.HistoryRepository; import com.tradin.module.history.domain.repository.dao.HistoryDao; +import com.tradin.module.history.implement.HistoryReader; import com.tradin.module.history.service.dto.BackTestDto; -import com.tradin.module.history.service.dto.StrategyInfoDto; -import com.tradin.module.strategy.domain.Position; -import com.tradin.module.strategy.domain.Strategy; -import com.tradin.module.strategy.domain.TradingType; +import com.tradin.module.strategy.domain.repository.dao.StrategyInfoDao; +import com.tradin.module.strategy.implement.StrategyReader; +import java.util.List; import lombok.RequiredArgsConstructor; -import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.data.redis.core.ZSetOperations; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.time.ZoneOffset; -import java.util.*; - -import static com.tradin.common.exception.ExceptionType.NOT_FOUND_OPEN_POSITION_EXCEPTION; -import static com.tradin.common.exception.ExceptionType.NOT_FOUND_STRATEGY_EXCEPTION; -import static com.tradin.module.strategy.domain.TradingType.BOTH; - @Service -@Transactional +@Transactional(readOnly = true) @RequiredArgsConstructor public class HistoryService { - private final HistoryRepository historyRepository; - private final RedisTemplate historyRedisTemplate; - - public void closeOngoingHistory(Strategy strategy, Position exitPosition) { - History ongoingHistory = findLastHistoryByStrategyId(strategy.getId()); - closeOpenPosition(ongoingHistory, exitPosition); - calculateProfitRate(ongoingHistory); - } - - public void createNewHistory(Strategy strategy, Position position) { - History newHistory = History.of(position, strategy); - historyRepository.save(newHistory); - } - - public void evictHistoryCache(Long strategyId) { - String cacheKey = "strategyId:" + strategyId; - historyRedisTemplate.delete(cacheKey); - } - - public List findHistoryDaoByStrategyId(Long id) { - return historyRepository.findHistoryDaoByStrategyId(id); - } - - public BackTestResponseDto backTest(BackTestDto request) { - List historyDaos = getHistories(request.getId(), request.getStartDate(), request.getEndDate()); - - return calculateHistoryDaos(historyDaos, request); - } - - public List getHistories(Long strategyId, LocalDate startDate, LocalDate endDate) { - String cacheKey = "strategyId:" + strategyId; - - ZSetOperations ops = historyRedisTemplate.opsForZSet(); - - LocalDateTime startTime = startDate.atStartOfDay().minusHours(9); - LocalDateTime endTime = endDate.atStartOfDay().minusHours(9).minusDays(1); - - long startScore = startTime.toEpochSecond(ZoneOffset.UTC); - long endScore = endTime.toEpochSecond(ZoneOffset.UTC); - - Set historySet = ops.rangeByScore(cacheKey, startScore, endScore); - - if (historySet != null && historySet.isEmpty()) { - addHistory(cacheKey, strategyId); - historySet = ops.rangeByScore(cacheKey, startScore, endScore); - } - - if (historySet != null && historySet.isEmpty()) { - throw new TradinException(NOT_FOUND_STRATEGY_EXCEPTION); - } - - return new ArrayList<>(Objects.requireNonNull(historySet)); - } - - - public void addHistory(String cacheKey, Long strategyId) { - List historyDaos = findHistoryDaoByStrategyId(strategyId); - ZSetOperations ops = historyRedisTemplate.opsForZSet(); - - for (HistoryDao historyDao : historyDaos) { - ops.add(cacheKey, historyDao, historyDao.getEntryPosition().getTime().toEpochSecond(ZoneOffset.UTC)); - } - } - - - private BackTestResponseDto calculateHistoryDaos(List historyDaos, BackTestDto request) { - List histories = new ArrayList<>(); - double compoundProfitRate = 0; - int winCount = 0; - int totalTradeCount = 0; - double simpleTotalProfitRate = 0; - double simpleWinProfitRate = 0; - double simpleLoseProfitRate = 0; - for (HistoryDao history : historyDaos) { - if (isInPeriod(history, request.getStartDate(), request.getEndDate()) && - isCorrespondTradingType(history, request.getTradingType())) { - - totalTradeCount++; - compoundProfitRate = (((1 + compoundProfitRate / 100.0) * (1 + history.getProfitRate() / 100.0)) - 1) * 100; - history.setCompoundProfitRate(compoundProfitRate); - histories.add(history); - - - double profitRate = history.getProfitRate(); - if (profitRate > 0) { - simpleWinProfitRate += profitRate; - winCount++; - } else if (profitRate < 0) { - simpleLoseProfitRate += Math.abs(profitRate); - } - - simpleTotalProfitRate += profitRate; - } - } - - Collections.reverse(histories); - - double winRate = calculateWinRate(winCount, totalTradeCount); - double averageProfitRate = calculateAverageProfitRate(simpleTotalProfitRate, totalTradeCount); - double profitFactor = calculateProfitFactor(simpleWinProfitRate, simpleLoseProfitRate); - - StrategyInfoDto strategyInfoDto = StrategyInfoDto.of(request.getId(), request.getName(), compoundProfitRate, winRate, profitFactor, totalTradeCount, averageProfitRate); - - return BackTestResponseDto.of(strategyInfoDto, histories); - } - - - private double calculateWinRate(double winCount, int totalTradeCount) { - return totalTradeCount == 0 ? 0 : winCount / totalTradeCount * 100; - } - - private double calculateAverageProfitRate(double simpleProfitRate, int totalTradeCount) { - return totalTradeCount == 0 ? 0 : simpleProfitRate / totalTradeCount; - } - - private double calculateProfitFactor(double simpleWinProfitRate, double simpleLoseProfitRate) { - return simpleLoseProfitRate == 0 ? 0 : simpleWinProfitRate / simpleLoseProfitRate; - } - - - private boolean isInPeriod(HistoryDao history, LocalDate startDate, LocalDate endDate) { - return history.getEntryPosition().getTime().isAfter(startDate.atStartOfDay()) && - history.getExitPosition().getTime().isBefore(endDate.atStartOfDay()); - } - - private boolean isCorrespondTradingType(HistoryDao historyDao, TradingType tradingType) { - return tradingType == historyDao.getEntryPosition().getTradingType() || tradingType == BOTH; - } - - private static void calculateProfitRate(History history) { - history.calculateProfitRate(); - } - - private static void closeOpenPosition(History ongoingHistory, Position exitPosition) { - if (ongoingHistory.getExitPosition() != null) { - throw new TradinException(NOT_FOUND_OPEN_POSITION_EXCEPTION); - - } - ongoingHistory.closeOpenPosition(exitPosition); - } - - private History findLastHistoryByStrategyId(Long id) { - return historyRepository.findLastHistoryByStrategyId(id) - .orElseThrow(() -> new TradinException(NOT_FOUND_OPEN_POSITION_EXCEPTION)); - } + private final StrategyReader strategyReader; + private final HistoryReader historyReader; + + public BackTestResponseDto backTest(BackTestDto request, Pageable pageable) { + StrategyInfoDao strategyInfoDao = strategyReader.readStrategyInfoDaoById(request.getId()); + List historyDaos = historyReader.readHistoryByIdAndPeriodAndTradingType( + request.getId(), + request.getStartDate(), + request.getEndDate(), + request.getTradingType(), + pageable + ); + + return BackTestResponseDto.of(strategyInfoDao, historyDaos); + } + +// private BackTestResponseDto calculateHistoryDaos(List historyDaos, +// BackTestDto request) { +// List histories = new ArrayList<>(); +// double compoundProfitRate = 0; +// int winCount = 0; +// int totalTradeCount = 0; +// double simpleTotalProfitRate = 0; +// double simpleWinProfitRate = 0; +// double simpleLoseProfitRate = 0; +// +// for (HistoryDao history : historyDaos) { +// if (isInPeriod(history, request.getStartDate(), request.getEndDate()) && +// isCorrespondTradingType(history, request.getTradingType())) { +// +// totalTradeCount++; +// compoundProfitRate = +// (((1 + compoundProfitRate / 100.0) * (1 + history.getProfitRate() / 100.0)) - 1) +// * 100; +// history.setCompoundProfitRate(compoundProfitRate); +// histories.add(history); +// +// double profitRate = history.getProfitRate(); +// if (profitRate > 0) { +// simpleWinProfitRate += profitRate; +// winCount++; +// } else if (profitRate < 0) { +// simpleLoseProfitRate += Math.abs(profitRate); +// } +// +// simpleTotalProfitRate += profitRate; +// } +// } +// +// Collections.reverse(histories); +// +// double winRate = calculateWinRate(winCount, totalTradeCount); +// double averageProfitRate = calculateAverageProfitRate(simpleTotalProfitRate, +// totalTradeCount); +// double profitFactor = calculateProfitFactor(simpleWinProfitRate, simpleLoseProfitRate); +// +// StrategyInfoDto strategyInfoDto = StrategyInfoDto.of(request.getId(), request.getName(), +// compoundProfitRate, winRate, profitFactor, totalTradeCount, averageProfitRate); +// +// return BackTestResponseDto.of(strategyInfoDto, histories); +// } +// +// +// private double calculateWinRate(double winCount, int totalTradeCount) { +// return totalTradeCount == 0 ? 0 : winCount / totalTradeCount * 100; +// } +// +// private double calculateAverageProfitRate(double simpleProfitRate, int totalTradeCount) { +// return totalTradeCount == 0 ? 0 : simpleProfitRate / totalTradeCount; +// } +// +// private double calculateProfitFactor(double simpleWinProfitRate, double simpleLoseProfitRate) { +// return simpleLoseProfitRate == 0 ? 0 : simpleWinProfitRate / simpleLoseProfitRate; +// } +// +// +// private boolean isInPeriod(HistoryDao history, LocalDate startDate, LocalDate endDate) { +// return history.getEntryPosition().getTime().isAfter(startDate.atStartOfDay()) && +// history.getExitPosition().getTime().isBefore(endDate.atStartOfDay()); +// } +// +// private boolean isCorrespondTradingType(HistoryDao historyDao, TradingType tradingType) { +// return tradingType == historyDao.getEntryPosition().getTradingType() || tradingType == BOTH; +// } +// +// private static void calculateProfitRate(History history) { +// history.calculateProfitRate(); +// } +// +// private static void closeOpenPosition(History ongoingHistory, Position exitPosition) { +// if (ongoingHistory.getExitPosition() != null) { +// throw new TradinException(NOT_FOUND_OPEN_POSITION_EXCEPTION); +// +// } +// ongoingHistory.closeOpenPosition(exitPosition); +// } +// +// private History findLastHistoryByStrategyId(Long id) { +// return historyRepository.findLastHistoryByStrategyId(id) +// .orElseThrow(() -> new TradinException(NOT_FOUND_OPEN_POSITION_EXCEPTION)); +// } +// +// +// public void closeOngoingHistory(Strategy strategy, Position exitPosition) { +// History ongoingHistory = findLastHistoryByStrategyId(strategy.getId()); +// closeOpenPosition(ongoingHistory, exitPosition); +// calculateProfitRate(ongoingHistory); +// } +// +// public void createNewHistory(Strategy strategy, Position position) { +// History newHistory = History.of(position, strategy); +// historyRepository.save(newHistory); +// } +// +// public void evictHistoryCache(Long strategyId) { +// String cacheKey = "strategyId:" + strategyId; +// historyRedisTemplate.delete(cacheKey); +// } } diff --git a/src/main/java/com/tradin/module/history/service/dto/StrategyInfoDto.java b/src/main/java/com/tradin/module/history/service/dto/StrategyInfoDto.java deleted file mode 100644 index 9b7a368..0000000 --- a/src/main/java/com/tradin/module/history/service/dto/StrategyInfoDto.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.tradin.module.history.service.dto; - -import lombok.Builder; -import lombok.Getter; - -@Getter -public class StrategyInfoDto { - private final Long id; - private final String name; - private final double compoundProfitRate; - private final double winRate; - private final double profitFactor; - private final int totalTradeCount; - private final double averageProfitRate; - - @Builder - public StrategyInfoDto(Long id, String name, double compoundProfitRate, double winRate, double profitFactor, int totalTradeCount, double averageProfitRate) { - this.id = id; - this.name = name; - this.compoundProfitRate = compoundProfitRate; - this.winRate = winRate; - this.profitFactor = profitFactor; - this.totalTradeCount = totalTradeCount; - this.averageProfitRate = averageProfitRate; - } - - public static StrategyInfoDto of(Long id, String name, double compoundProfitRate, double winRate, double profitFactor, int totalTradeCount, double averageProfitRate) { - return StrategyInfoDto.builder() - .id(id) - .name(name) - .compoundProfitRate(compoundProfitRate) - .winRate(winRate) - .profitFactor(profitFactor) - .totalTradeCount(totalTradeCount) - .averageProfitRate(averageProfitRate) - .build(); - } -} diff --git a/src/main/java/com/tradin/module/strategy/domain/repository/StrategyQueryRepository.java b/src/main/java/com/tradin/module/strategy/domain/repository/StrategyQueryRepository.java index 9d4857d..74d3c5e 100644 --- a/src/main/java/com/tradin/module/strategy/domain/repository/StrategyQueryRepository.java +++ b/src/main/java/com/tradin/module/strategy/domain/repository/StrategyQueryRepository.java @@ -2,14 +2,16 @@ import com.tradin.module.strategy.domain.repository.dao.StrategyInfoDao; import com.tradin.module.strategy.domain.repository.dao.SubscriptionStrategyInfoDao; - import java.util.List; import java.util.Optional; public interface StrategyQueryRepository { - List findFutureStrategiesInfoDao(); + + List findAllFutureStrategiesInfoDao(); Optional> findSubscriptionStrategiesInfoDao(); - List findSpotStrategiesInfoDao(); + List findAllSpotStrategiesInfoDao(); + + Optional findStrategyInfoDaoById(Long id); } diff --git a/src/main/java/com/tradin/module/strategy/domain/repository/impl/StrategyQueryRepositoryImpl.java b/src/main/java/com/tradin/module/strategy/domain/repository/impl/StrategyQueryRepositoryImpl.java index b7c8db4..2e05828 100644 --- a/src/main/java/com/tradin/module/strategy/domain/repository/impl/StrategyQueryRepositoryImpl.java +++ b/src/main/java/com/tradin/module/strategy/domain/repository/impl/StrategyQueryRepositoryImpl.java @@ -1,5 +1,9 @@ package com.tradin.module.strategy.domain.repository.impl; +import static com.tradin.module.strategy.domain.QStrategy.strategy; +import static com.tradin.module.strategy.domain.StrategyType.FUTURE; +import static com.tradin.module.strategy.domain.StrategyType.SPOT; + import com.querydsl.jpa.impl.JPAQueryFactory; import com.tradin.module.strategy.domain.StrategyType; import com.tradin.module.strategy.domain.repository.StrategyQueryRepository; @@ -7,75 +11,100 @@ import com.tradin.module.strategy.domain.repository.dao.QSubscriptionStrategyInfoDao; import com.tradin.module.strategy.domain.repository.dao.StrategyInfoDao; import com.tradin.module.strategy.domain.repository.dao.SubscriptionStrategyInfoDao; -import lombok.RequiredArgsConstructor; - import java.util.List; import java.util.Optional; - -import static com.tradin.module.strategy.domain.QStrategy.strategy; -import static com.tradin.module.strategy.domain.StrategyType.FUTURE; -import static com.tradin.module.strategy.domain.StrategyType.SPOT; +import lombok.RequiredArgsConstructor; @RequiredArgsConstructor public class StrategyQueryRepositoryImpl implements StrategyQueryRepository { + private final JPAQueryFactory jpaQueryFactory; @Override - public List findFutureStrategiesInfoDao() { + public List findAllFutureStrategiesInfoDao() { return getStrategyInfoDaos(FUTURE); } @Override - public List findSpotStrategiesInfoDao() { + public List findAllSpotStrategiesInfoDao() { return getStrategyInfoDaos(SPOT); } + @Override + public Optional findStrategyInfoDaoById(Long id) { + return Optional.ofNullable(jpaQueryFactory + .select( + new QStrategyInfoDao( + strategy.id, + strategy.name, + strategy.type.coinType, + strategy.profitFactor, + strategy.rate.winningRate, + strategy.rate.simpleProfitRate, + strategy.rate.compoundProfitRate, + strategy.rate.totalProfitRate, + strategy.rate.totalLossRate, + strategy.rate.averageProfitRate, + strategy.count.totalTradeCount, + strategy.count.winCount, + strategy.count.lossCount, + strategy.currentPosition.tradingType, + strategy.currentPosition.time, + strategy.currentPosition.price, + strategy.averageHoldingPeriod + )) + .from(strategy) + .where(strategy.id.eq(id)) + .fetchOne()); + } + private List getStrategyInfoDaos(StrategyType strategyType) { return jpaQueryFactory - .select( - new QStrategyInfoDao( - strategy.id, - strategy.name, - strategy.type.coinType, - strategy.profitFactor, - strategy.rate.winningRate, - strategy.rate.simpleProfitRate, - strategy.rate.compoundProfitRate, - strategy.rate.totalProfitRate, - strategy.rate.totalLossRate, - strategy.rate.averageProfitRate, - strategy.count.totalTradeCount, - strategy.count.winCount, - strategy.count.lossCount, - strategy.currentPosition.tradingType, - strategy.currentPosition.time, - strategy.currentPosition.price, - strategy.averageHoldingPeriod - )) - .from(strategy) - .where(strategy.type.strategyType.eq(strategyType)) - .orderBy(strategy.id.asc()) - .fetch(); + .select( + new QStrategyInfoDao( + strategy.id, + strategy.name, + strategy.type.coinType, + strategy.profitFactor, + strategy.rate.winningRate, + strategy.rate.simpleProfitRate, + strategy.rate.compoundProfitRate, + strategy.rate.totalProfitRate, + strategy.rate.totalLossRate, + strategy.rate.averageProfitRate, + strategy.count.totalTradeCount, + strategy.count.winCount, + strategy.count.lossCount, + strategy.currentPosition.tradingType, + strategy.currentPosition.time, + strategy.currentPosition.price, + strategy.averageHoldingPeriod + )) + .from(strategy) + .where(strategy.type.strategyType.eq(strategyType)) + .orderBy(strategy.id.asc()) + .fetch(); } @Override public Optional> findSubscriptionStrategiesInfoDao() { List subscriptionStrategyInfoDaos = jpaQueryFactory - .select( - new QSubscriptionStrategyInfoDao( - strategy.id, - strategy.name, - strategy.type.coinType, - strategy.profitFactor, - strategy.rate.winningRate, - strategy.rate.compoundProfitRate, - strategy.rate.averageProfitRate - )) - .from(strategy) - .where(strategy.type.strategyType.eq(FUTURE)) - .orderBy(strategy.id.asc()) - .fetch(); + .select( + new QSubscriptionStrategyInfoDao( + strategy.id, + strategy.name, + strategy.type.coinType, + strategy.profitFactor, + strategy.rate.winningRate, + strategy.rate.compoundProfitRate, + strategy.rate.averageProfitRate + )) + .from(strategy) + .where(strategy.type.strategyType.eq(FUTURE)) + .orderBy(strategy.id.asc()) + .fetch(); - return subscriptionStrategyInfoDaos.isEmpty() ? Optional.empty() : Optional.of(subscriptionStrategyInfoDaos); + return subscriptionStrategyInfoDaos.isEmpty() ? Optional.empty() + : Optional.of(subscriptionStrategyInfoDaos); } } diff --git a/src/main/java/com/tradin/module/strategy/implement/StrategyReader.java b/src/main/java/com/tradin/module/strategy/implement/StrategyReader.java index 9193254..e268549 100644 --- a/src/main/java/com/tradin/module/strategy/implement/StrategyReader.java +++ b/src/main/java/com/tradin/module/strategy/implement/StrategyReader.java @@ -2,11 +2,8 @@ import com.tradin.common.exception.ExceptionType; import com.tradin.common.exception.TradinException; -import com.tradin.module.strategy.domain.Strategy; import com.tradin.module.strategy.domain.repository.StrategyRepository; import com.tradin.module.strategy.domain.repository.dao.StrategyInfoDao; -import com.tradin.module.strategy.domain.repository.dao.SubscriptionStrategyInfoDao; -import java.util.Collections; import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; @@ -14,29 +11,38 @@ @Component @RequiredArgsConstructor public class StrategyReader { - private final StrategyRepository strategyRepository; - public List findFutureStrategyInfoDaos() { - return strategyRepository.findFutureStrategiesInfoDao(); - } - - public List findSpotStrategyInfoDaos() { - return strategyRepository.findSpotStrategiesInfoDao(); - } - - public List findSubscriptionStrategyInfoDaos() { - return strategyRepository.findSubscriptionStrategiesInfoDao() - .orElse(Collections.emptyList()); - } - - public Strategy findByName(String name) { - return strategyRepository.findByName(name) - .orElseThrow(() -> new TradinException(ExceptionType.NOT_FOUND_STRATEGY_EXCEPTION)); - } - - - public Strategy findById(Long id) { - return strategyRepository.findById(id) - .orElseThrow(() -> new TradinException(ExceptionType.NOT_FOUND_STRATEGY_EXCEPTION)); - } + private final StrategyRepository strategyRepository; + + public List readFutureStrategyInfoDaos() { + return strategyRepository.findAllFutureStrategiesInfoDao(); + } + + public List readSpotStrategyInfoDaos() { + return strategyRepository.findAllSpotStrategiesInfoDao(); + } + + public StrategyInfoDao readStrategyInfoDaoById(Long id) { + return findStrategyInfoDaoById(id); + } + + private StrategyInfoDao findStrategyInfoDaoById(Long id) { + return strategyRepository.findStrategyInfoDaoById(id) + .orElseThrow(() -> new TradinException(ExceptionType.NOT_FOUND_STRATEGY_EXCEPTION)); + } + +// public List findSubscriptionStrategyInfoDaos() { +// return strategyRepository.findSubscriptionStrategiesInfoDao() +// .orElse(Collections.emptyList()); +// } +// +// public Strategy findByName(String name) { +// return strategyRepository.findByName(name) +// .orElseThrow(() -> new TradinException(ExceptionType.NOT_FOUND_STRATEGY_EXCEPTION)); +// } +// +// public Strategy findById(Long id) { +// return strategyRepository.findById(id) +// .orElseThrow(() -> new TradinException(ExceptionType.NOT_FOUND_STRATEGY_EXCEPTION)); +// } } diff --git a/src/main/java/com/tradin/module/strategy/service/StrategyService.java b/src/main/java/com/tradin/module/strategy/service/StrategyService.java index 65347fa..76bf213 100644 --- a/src/main/java/com/tradin/module/strategy/service/StrategyService.java +++ b/src/main/java/com/tradin/module/strategy/service/StrategyService.java @@ -1,26 +1,14 @@ package com.tradin.module.strategy.service; -import static com.tradin.common.exception.ExceptionType.NOT_SUBSCRIBED_STRATEGY_EXCEPTION; -import static com.tradin.module.strategy.domain.TradingType.LONG; - -import com.tradin.common.exception.TradinException; import com.tradin.common.utils.AESUtils; import com.tradin.module.feign.service.BinanceFeignService; import com.tradin.module.history.service.HistoryService; import com.tradin.module.strategy.controller.dto.response.FindStrategiesInfoResponseDto; -import com.tradin.module.strategy.controller.dto.response.FindSubscriptionStrategiesInfoResponseDto; -import com.tradin.module.strategy.domain.Position; -import com.tradin.module.strategy.domain.Strategy; import com.tradin.module.strategy.domain.TradingType; import com.tradin.module.strategy.domain.repository.dao.StrategyInfoDao; -import com.tradin.module.strategy.domain.repository.dao.SubscriptionStrategyInfoDao; import com.tradin.module.strategy.implement.StrategyReader; -import com.tradin.module.strategy.service.dto.UnSubscribeStrategyDto; -import com.tradin.module.strategy.service.dto.WebHookDto; import com.tradin.module.trade.service.TradeService; -import com.tradin.module.users.domain.Users; import com.tradin.module.users.service.UsersService; -import com.tradin.module.users.service.dto.SubscribeStrategyDto; import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -32,6 +20,7 @@ @Slf4j @RequiredArgsConstructor public class StrategyService { + private final HistoryService historyService; private final BinanceFeignService binanceFeignService; private final UsersService userService; @@ -40,92 +29,90 @@ public class StrategyService { private final StrategyReader strategyReader; public FindStrategiesInfoResponseDto findFutureStrategiesInfo() { - List strategiesInfo = strategyReader.findFutureStrategyInfoDaos(); + List strategiesInfo = strategyReader.readFutureStrategyInfoDaos(); return FindStrategiesInfoResponseDto.of(strategiesInfo); } public FindStrategiesInfoResponseDto findSpotStrategiesInfo() { - List strategiesInfo = strategyReader.findSpotStrategyInfoDaos(); + List strategiesInfo = strategyReader.readSpotStrategyInfoDaos(); return FindStrategiesInfoResponseDto.of(strategiesInfo); } - private static TradingType webHookTradingType(WebHookDto request) { - return request.getPosition().getTradingType(); - } - - public FindSubscriptionStrategiesInfoResponseDto findSubscriptionStrategiesInfo() { - List subscriptionStrategyInfo = strategyReader.findSubscriptionStrategyInfoDaos(); - return new FindSubscriptionStrategiesInfoResponseDto(subscriptionStrategyInfo); - } - - - public void subscribeStrategy(SubscribeStrategyDto request) { - Users savedUser = getUserFromSecurityContext(); - Strategy strategy = strategyReader.findById(request.getId()); - String encryptedApiKey = getEncryptedKey(request.getBinanceApiKey()); - String encryptedSecretKey = getEncryptedKey(request.getBinanceSecretKey()); - - savedUser.subscribeStrategy(strategy, encryptedApiKey, encryptedSecretKey, request.getLeverage(), request.getQuantityRate(), request.getTradingType()); - } - - public void unsubscribeStrategy(UnSubscribeStrategyDto request) { - Users savedUser = getUserFromSecurityContext(); - Strategy strategy = strategyReader.findById(request.getId()); - - isUserSubscribedStrategy(savedUser, strategy); - - if (request.isPositionClose() && isUserPositionExist(savedUser.getCurrentPositionType())) { - String side = getSideFromUserCurrentPosition(savedUser); - closePosition(savedUser.getBinanceApiKey(), savedUser.getBinanceSecretKey(), side); - } - - savedUser.unsubscribeStrategy(); - } - - private static void isUserSubscribedStrategy(Users users, Strategy strategy) { - if (!users.getStrategy().getId().equals(strategy.getId())) { - throw new TradinException(NOT_SUBSCRIBED_STRATEGY_EXCEPTION); - } - } - - private void autoTrading(String name, TradingType tradingType) { - tradeService.autoTrading(name, tradingType); - } - - private String getSideFromUserCurrentPosition(Users savedUser) { - return savedUser.getCurrentPositionType().equals(LONG) ? "SELL" : "BUY"; - } - - private void closePosition(String apiKey, String secretKey, String side) { - binanceFeignService.closePosition(apiKey, secretKey, side); - } - - private String getEncryptedKey(String key) { - return aesUtils.encrypt(key); - } - - private Users getUserFromSecurityContext() { - return userService.getUserFromSecurityContext(); - } - - - private void closeOngoingHistory(Strategy strategy, Position exitPosition) { - historyService.closeOngoingHistory(strategy, exitPosition); - } - - private void createNewHistory(Strategy strategy, Position position) { - historyService.createNewHistory(strategy, position); - } - - private void evictHistoryCache(Long strategyId) { - historyService.evictHistoryCache(strategyId); - } - - private void updateStrategyMetaData(Strategy strategy, Position position) { - strategy.updateMetaData(position); - } - - +// private static TradingType webHookTradingType(WebHookDto request) { +// return request.getPosition().getTradingType(); +// } +// +// public FindSubscriptionStrategiesInfoResponseDto findSubscriptionStrategiesInfo() { +// List subscriptionStrategyInfo = strategyReader.findSubscriptionStrategyInfoDaos(); +// return new FindSubscriptionStrategiesInfoResponseDto(subscriptionStrategyInfo); +// } +// +// +// public void subscribeStrategy(SubscribeStrategyDto request) { +// Users savedUser = getUserFromSecurityContext(); +// Strategy strategy = strategyReader.findById(request.getId()); +// String encryptedApiKey = getEncryptedKey(request.getBinanceApiKey()); +// String encryptedSecretKey = getEncryptedKey(request.getBinanceSecretKey()); +// +// savedUser.subscribeStrategy(strategy, encryptedApiKey, encryptedSecretKey, request.getLeverage(), request.getQuantityRate(), request.getTradingType()); +// } +// +// public void unsubscribeStrategy(UnSubscribeStrategyDto request) { +// Users savedUser = getUserFromSecurityContext(); +// Strategy strategy = strategyReader.findById(request.getId()); +// +// isUserSubscribedStrategy(savedUser, strategy); +// +// if (request.isPositionClose() && isUserPositionExist(savedUser.getCurrentPositionType())) { +// String side = getSideFromUserCurrentPosition(savedUser); +// closePosition(savedUser.getBinanceApiKey(), savedUser.getBinanceSecretKey(), side); +// } +// +// savedUser.unsubscribeStrategy(); +// } +// +// private static void isUserSubscribedStrategy(Users users, Strategy strategy) { +// if (!users.getStrategy().getId().equals(strategy.getId())) { +// throw new TradinException(NOT_SUBSCRIBED_STRATEGY_EXCEPTION); +// } +// } +// +// private void autoTrading(String name, TradingType tradingType) { +// tradeService.autoTrading(name, tradingType); +// } +// +// private String getSideFromUserCurrentPosition(Users savedUser) { +// return savedUser.getCurrentPositionType().equals(LONG) ? "SELL" : "BUY"; +// } +// +// private void closePosition(String apiKey, String secretKey, String side) { +// binanceFeignService.closePosition(apiKey, secretKey, side); +// } +// +// private String getEncryptedKey(String key) { +// return aesUtils.encrypt(key); +// } +// +// private Users getUserFromSecurityContext() { +// return userService.getUserFromSecurityContext(); +// } +// +// +// private void closeOngoingHistory(Strategy strategy, Position exitPosition) { +// historyService.closeOngoingHistory(strategy, exitPosition); +// } +// +// private void createNewHistory(Strategy strategy, Position position) { +// historyService.createNewHistory(strategy, position); +// } +// +// private void evictHistoryCache(Long strategyId) { +// historyService.evictHistoryCache(strategyId); +// } +// +// private void updateStrategyMetaData(Strategy strategy, Position position) { +// strategy.updateMetaData(position); +// } // private List findSubscriptionStrategyInfoDaos() { // return strategyRepository.findSubscriptionStrategiesInfoDao() @@ -133,7 +120,6 @@ private void updateStrategyMetaData(Strategy strategy, Position position) { // } - private boolean isUserPositionExist(TradingType tradingType) { return tradingType != TradingType.NONE; } From 328abaff6a980ef6ed528fd9c2711bbb8237825c Mon Sep 17 00:00:00 2001 From: JiHwan Date: Sun, 12 Jan 2025 16:16:11 +0900 Subject: [PATCH 2/3] =?UTF-8?q?fix:=20=EB=B6=88=ED=95=84=EC=9A=94=ED=95=9C?= =?UTF-8?q?=20=EC=A0=95=EB=A0=AC=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../impl/HistoryQueryRepositoryImpl.java | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/main/java/com/tradin/module/history/domain/repository/impl/HistoryQueryRepositoryImpl.java b/src/main/java/com/tradin/module/history/domain/repository/impl/HistoryQueryRepositoryImpl.java index 4928e4c..d772be0 100644 --- a/src/main/java/com/tradin/module/history/domain/repository/impl/HistoryQueryRepositoryImpl.java +++ b/src/main/java/com/tradin/module/history/domain/repository/impl/HistoryQueryRepositoryImpl.java @@ -1,39 +1,39 @@ package com.tradin.module.history.domain.repository.impl; +import static com.tradin.module.history.domain.QHistory.history; + import com.querydsl.jpa.impl.JPAQueryFactory; import com.tradin.module.history.domain.History; import com.tradin.module.history.domain.repository.HistoryQueryRepository; import com.tradin.module.history.domain.repository.dao.HistoryDao; import com.tradin.module.history.domain.repository.dao.QHistoryDao; -import lombok.RequiredArgsConstructor; - import java.util.List; import java.util.Optional; - -import static com.tradin.module.history.domain.QHistory.history; +import lombok.RequiredArgsConstructor; @RequiredArgsConstructor public class HistoryQueryRepositoryImpl implements HistoryQueryRepository { + private final JPAQueryFactory jpaQueryFactory; @Override public Optional findLastHistoryByStrategyId(Long id) { return Optional.ofNullable( - jpaQueryFactory - .selectFrom(history) - .where(history.strategy.id.eq(id)) - .orderBy(history.entryPosition.time.desc()) - .fetchFirst() + jpaQueryFactory + .selectFrom(history) + .where(history.strategy.id.eq(id)) + .orderBy(history.entryPosition.time.desc()) + .fetchFirst() ); } @Override public List findHistoryDaoByStrategyId(Long id) { return jpaQueryFactory.select(new QHistoryDao(history.id, history.entryPosition, history.exitPosition, - history.profitRate)) - .from(history) - .where(history.strategy.id.eq(id)) - .orderBy(history.id.asc()) - .fetch(); + history.profitRate + )) + .from(history) + .where(history.strategy.id.eq(id)) + .fetch(); } } From c6b85b1e6f3d8ca51d4bf696a144cfc7494e13a3 Mon Sep 17 00:00:00 2001 From: JiHwan Date: Mon, 13 Jan 2025 23:10:28 +0900 Subject: [PATCH 3/3] =?UTF-8?q?feat:=20=EB=B9=84=EB=8F=99=EA=B8=B0?= =?UTF-8?q?=EC=A0=81=EC=9C=BC=EB=A1=9C=20=EC=BA=90=EC=8B=9C=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=EB=B0=8F=20=EB=84=A4=EC=9D=B4=EB=B0=8D=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/HistoryQueryRepository.java | 4 +- .../impl/HistoryQueryRepositoryImpl.java | 2 +- .../implement/HistoryCacheProcessor.java | 22 +++++++- .../history/implement/HistoryReader.java | 54 ++++++++++--------- .../history/service/HistoryService.java | 15 ++++-- 5 files changed, 64 insertions(+), 33 deletions(-) diff --git a/src/main/java/com/tradin/module/history/domain/repository/HistoryQueryRepository.java b/src/main/java/com/tradin/module/history/domain/repository/HistoryQueryRepository.java index a4b1e89..9a60701 100644 --- a/src/main/java/com/tradin/module/history/domain/repository/HistoryQueryRepository.java +++ b/src/main/java/com/tradin/module/history/domain/repository/HistoryQueryRepository.java @@ -2,12 +2,12 @@ import com.tradin.module.history.domain.History; import com.tradin.module.history.domain.repository.dao.HistoryDao; - import java.util.List; import java.util.Optional; public interface HistoryQueryRepository { + Optional findLastHistoryByStrategyId(Long id); - List findHistoryDaoByStrategyId(Long id); + List findHistoryByStrategyId(Long id); } diff --git a/src/main/java/com/tradin/module/history/domain/repository/impl/HistoryQueryRepositoryImpl.java b/src/main/java/com/tradin/module/history/domain/repository/impl/HistoryQueryRepositoryImpl.java index d772be0..d8df29a 100644 --- a/src/main/java/com/tradin/module/history/domain/repository/impl/HistoryQueryRepositoryImpl.java +++ b/src/main/java/com/tradin/module/history/domain/repository/impl/HistoryQueryRepositoryImpl.java @@ -28,7 +28,7 @@ public Optional findLastHistoryByStrategyId(Long id) { } @Override - public List findHistoryDaoByStrategyId(Long id) { + public List findHistoryByStrategyId(Long id) { return jpaQueryFactory.select(new QHistoryDao(history.id, history.entryPosition, history.exitPosition, history.profitRate )) diff --git a/src/main/java/com/tradin/module/history/implement/HistoryCacheProcessor.java b/src/main/java/com/tradin/module/history/implement/HistoryCacheProcessor.java index 2470eca..b9faa78 100644 --- a/src/main/java/com/tradin/module/history/implement/HistoryCacheProcessor.java +++ b/src/main/java/com/tradin/module/history/implement/HistoryCacheProcessor.java @@ -1,15 +1,33 @@ package com.tradin.module.history.implement; import com.tradin.module.history.domain.repository.dao.HistoryDao; +import java.time.ZoneOffset; import java.util.List; import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisOperations; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.SessionCallback; +import org.springframework.data.redis.core.ZSetOperations; +import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Component; @Component @RequiredArgsConstructor public class HistoryCacheProcessor { - public void addHistoryCache(String cacheKey, List historyDaos) { - //TODO - 캐시 추가 로직 event + private final RedisTemplate historyRedisTemplate; + + @Async + public void addHistoryCache(String cacheKey, List histories) { + historyRedisTemplate.executePipelined(new SessionCallback() { + @Override + public Object execute(RedisOperations operations) { + ZSetOperations ops = operations.opsForZSet(); + for (HistoryDao history : histories) { + ops.add((K) cacheKey, (V) history, history.entryPosition().getTime().toEpochSecond(ZoneOffset.UTC)); + } + return null; + } + }); } } diff --git a/src/main/java/com/tradin/module/history/implement/HistoryReader.java b/src/main/java/com/tradin/module/history/implement/HistoryReader.java index 2a12cd0..3602c33 100644 --- a/src/main/java/com/tradin/module/history/implement/HistoryReader.java +++ b/src/main/java/com/tradin/module/history/implement/HistoryReader.java @@ -21,58 +21,62 @@ public class HistoryReader { public List readHistoryByIdAndPeriodAndTradingType(Long strategyId, LocalDate startDate, LocalDate endDate, TradingType tradingType, Pageable pageable) { String cacheKey = "strategyId:" + strategyId; - List cachedHistories = HistoryCacheReader.readHistoryByIdAndPeriod(cacheKey, startDate, endDate, pageable); + List cachedHistories = readCachedHistories(startDate, endDate, pageable, cacheKey); if (isCachedHistoriesEmpty(cachedHistories)) { - return findHistoryByIdAndPeriodAndTradingType(strategyId, startDate, endDate, tradingType, cacheKey); + return findAndFilterHistoriesByIdAndPeriodAndTradingType(strategyId, startDate, endDate, tradingType, cacheKey); } return filterByPeriodAndTradingType(cachedHistories, startDate, endDate, tradingType); } - private List findHistoryByIdAndPeriodAndTradingType(Long strategyId, LocalDate startDate, LocalDate endDate, TradingType tradingType, String cacheKey) { - List historyDaos = historyRepository.findHistoryDaoByStrategyId(strategyId); - addHistoryCache(cacheKey, historyDaos); + private List readCachedHistories(LocalDate startDate, LocalDate endDate, Pageable pageable, String cacheKey) { + return HistoryCacheReader.readHistoryByIdAndPeriod(cacheKey, startDate, endDate, pageable); + } + + private List findAndFilterHistoriesByIdAndPeriodAndTradingType(Long strategyId, LocalDate startDate, LocalDate endDate, TradingType tradingType, String cacheKey) { + List histories = historyRepository.findHistoryByStrategyId(strategyId); + addHistoryCache(cacheKey, histories); - return filterByPeriodAndTradingType(historyDaos, startDate, endDate, tradingType); + return filterByPeriodAndTradingType(histories, startDate, endDate, tradingType); } - private boolean isCachedHistoriesEmpty(List historyDaos) { - return historyDaos.isEmpty(); + private boolean isCachedHistoriesEmpty(List histories) { + return histories.isEmpty(); } - private void addHistoryCache(String cacheKey, List historyDaos) { - HistoryCacheProcessor.addHistoryCache(cacheKey, historyDaos); + private void addHistoryCache(String cacheKey, List histories) { + HistoryCacheProcessor.addHistoryCache(cacheKey, histories); } - private List filterByPeriodAndTradingType(List historyDaos, LocalDate startDate, LocalDate endDate, TradingType tradingType) { - List filteredHistoryDaos = historyDaos.stream() - .filter(historyDao -> isInPeriod(historyDao, startDate, endDate)) - .filter(historyDao -> isCorrespondTradingType(historyDao, tradingType)) + private List filterByPeriodAndTradingType(List histories, LocalDate startDate, LocalDate endDate, TradingType tradingType) { + List filteredHistories = histories.stream() + .filter(history -> isInPeriod(history, startDate, endDate)) + .filter(history -> isCorrespondTradingType(history, tradingType)) .toList(); - return calculateCompoundProfitRate(filteredHistoryDaos, startDate, endDate); + return calculateCompoundProfitRate(filteredHistories, startDate, endDate); } - private boolean isInPeriod(HistoryDao historyDao, LocalDate startDate, LocalDate endDate) { - return historyDao.entryPosition().getTime().toLocalDate().isAfter(startDate) && - historyDao.exitPosition().getTime().toLocalDate().isBefore(endDate); + private boolean isInPeriod(HistoryDao history, LocalDate startDate, LocalDate endDate) { + return history.entryPosition().getTime().toLocalDate().isAfter(startDate) && + history.exitPosition().getTime().toLocalDate().isBefore(endDate); } - private boolean isCorrespondTradingType(HistoryDao historyDao, TradingType tradingType) { + private boolean isCorrespondTradingType(HistoryDao history, TradingType tradingType) { return switch (tradingType) { - case LONG -> historyDao.entryPosition().getTradingType().equals(TradingType.LONG); - case SHORT -> historyDao.entryPosition().getTradingType().equals(TradingType.SHORT); - case BOTH -> historyDao.entryPosition().getTradingType().equals(TradingType.LONG) || - historyDao.entryPosition().getTradingType().equals(TradingType.SHORT); + case LONG -> history.entryPosition().getTradingType().equals(TradingType.LONG); + case SHORT -> history.entryPosition().getTradingType().equals(TradingType.SHORT); + case BOTH -> history.entryPosition().getTradingType().equals(TradingType.LONG) || + history.entryPosition().getTradingType().equals(TradingType.SHORT); default -> false; }; } - private List calculateCompoundProfitRate(List filteredHistoryDaos, LocalDate startDate, LocalDate endDate) { + private List calculateCompoundProfitRate(List filteredHistories, LocalDate startDate, LocalDate endDate) { double[] cumulativeProfitRate = {0.0}; - return filteredHistoryDaos.stream() + return filteredHistories.stream() .map(history -> { cumulativeProfitRate[0] = calculateCompound(cumulativeProfitRate[0], history.profitRate()); return new HistoryDao( diff --git a/src/main/java/com/tradin/module/history/service/HistoryService.java b/src/main/java/com/tradin/module/history/service/HistoryService.java index f74b440..37dda2c 100644 --- a/src/main/java/com/tradin/module/history/service/HistoryService.java +++ b/src/main/java/com/tradin/module/history/service/HistoryService.java @@ -20,17 +20,26 @@ public class HistoryService { private final StrategyReader strategyReader; private final HistoryReader historyReader; + @Transactional public BackTestResponseDto backTest(BackTestDto request, Pageable pageable) { - StrategyInfoDao strategyInfoDao = strategyReader.readStrategyInfoDaoById(request.getId()); - List historyDaos = historyReader.readHistoryByIdAndPeriodAndTradingType( + StrategyInfoDao strategyInfoDao = readStrategyInfoDaoById(request); + List historyDaos = readHistoryByIdAndPeriodAndTradingType(request, pageable); + + return BackTestResponseDto.of(strategyInfoDao, historyDaos); + } + + private List readHistoryByIdAndPeriodAndTradingType(BackTestDto request, Pageable pageable) { + return historyReader.readHistoryByIdAndPeriodAndTradingType( request.getId(), request.getStartDate(), request.getEndDate(), request.getTradingType(), pageable ); + } - return BackTestResponseDto.of(strategyInfoDao, historyDaos); + private StrategyInfoDao readStrategyInfoDaoById(BackTestDto request) { + return strategyReader.readStrategyInfoDaoById(request.getId()); } // private BackTestResponseDto calculateHistoryDaos(List historyDaos,