diff --git a/.gitignore b/.gitignore index 16e7ffe9..110ceb61 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,4 @@ __pycache__/ /venv/ /env /.vs +backend/src/main/java/org/sejongisc/backend/stock/TestController.java diff --git a/backend/build.gradle b/backend/build.gradle index 400b8140..d3768009 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -38,7 +38,6 @@ dependencies { runtimeOnly 'org.postgresql:postgresql' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' - runtimeOnly 'com.h2database:h2' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' implementation 'io.jsonwebtoken:jjwt-api:0.11.5' diff --git a/backend/src/main/java/org/sejongisc/backend/backtest/controller/BacktestController.java b/backend/src/main/java/org/sejongisc/backend/backtest/controller/BacktestController.java index db99f09a..da3d7827 100644 --- a/backend/src/main/java/org/sejongisc/backend/backtest/controller/BacktestController.java +++ b/backend/src/main/java/org/sejongisc/backend/backtest/controller/BacktestController.java @@ -1,6 +1,11 @@ package org.sejongisc.backend.backtest.controller; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.ExampleObject; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.sejongisc.backend.backtest.dto.BacktestRequest; import org.sejongisc.backend.backtest.dto.BacktestResponse; @@ -14,12 +19,20 @@ @RestController @RequestMapping("/api/backtest") +@Tag( + name = "백테스팅 API", + description = "백테스팅 관련 API 제공" +) @RequiredArgsConstructor public class BacktestController { private final BacktestService backtestService; // 백테스트 실행 상태 조회 @GetMapping("/runs/{backtestRunId}/status") + @Operation( + summary = "백테스트 실행 상태 조회", + description = "지정된 백테스트 실행 ID에 대한 현재 상태를 조회합니다." + ) public ResponseEntity getBacktestStatus(@PathVariable Long backtestRunId, @AuthenticationPrincipal CustomUserDetails customUserDetails) { return ResponseEntity.ok(backtestService.getBacktestStatus(backtestRunId, customUserDetails.getUserId())); @@ -27,6 +40,10 @@ public ResponseEntity getBacktestStatus(@PathVariable Long bac // 백테스트 기록 상세 조회 @GetMapping("/runs/{backtestRunId}") + @Operation( + summary = "백테스트 실행 기록 상세 조회", + description = "지정된 백테스트 실행 ID에 대한 상세 결과를 조회합니다." + ) public ResponseEntity getBackTestResultDetails(@PathVariable Long backtestRunId, @AuthenticationPrincipal CustomUserDetails customUserDetails) { return ResponseEntity.ok(backtestService.getBackTestDetails(backtestRunId, customUserDetails.getUserId())); @@ -34,6 +51,128 @@ public ResponseEntity getBackTestResultDetails(@PathVariable L // 백테스트 실행 @PostMapping("/runs") + @Operation( + summary = "백테스트 실행", + description = "사용자가 요청한 전략을 기반으로 백테스트를 실행합니다." + ) + @io.swagger.v3.oas.annotations.parameters.RequestBody( + description = "백테스트 실행을 위한 기본 정보 및 전략", + required = true, + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = BacktestRequest.class), + examples = { + @ExampleObject( + summary = "SMA 골든크로스 및 RSI 필터 전략 예시", + value = """ + { + "title": "골든크로스 + RSI 필터 (AAPL)", + "startDate": "2023-01-01", + "endDate": "2024-12-31", + "templateId": null, + "strategy": { + "initialCapital": 100000.00, + "ticker": "AAPL", + "buyConditions": [ + { + "leftOperand": { + "type": "indicator", + "indicatorCode": "RSI", + "output": "value", + "params": { + "length": 14 + } + }, + "operator": "LT", + "rightOperand": { + "type": "const", + "constantValue": 30 + }, + "isAbsolute": true + }, + { + "leftOperand": { + "type": "indicator", + "indicatorCode": "SMA", + "output": "value", + "params": { + "length": 50 + } + }, + "operator": "CROSSES_ABOVE", + "rightOperand": { + "type": "indicator", + "indicatorCode": "SMA", + "output": "value", + "params": { + "length": 200 + } + }, + "isAbsolute": false + }, + { + "leftOperand": { + "type": "price", + "priceField": "Close" + }, + "operator": "GTE", + "rightOperand": { + "type": "indicator", + "indicatorCode": "SMA", + "output": "value", + "params": { + "length": 200 + } + }, + "isAbsolute": false + } + ], + "sellConditions": [ + { + "leftOperand": { + "type": "indicator", + "indicatorCode": "RSI", + "output": "value", + "params": { + "length": 14 + } + }, + "operator": "GT", + "rightOperand": { + "type": "const", + "constantValue": 70 + }, + "isAbsolute": true + }, + { + "leftOperand": { + "type": "indicator", + "indicatorCode": "SMA", + "output": "value", + "params": { + "length": 50 + } + }, + "operator": "CROSSES_BELOW", + "rightOperand": { + "type": "indicator", + "indicatorCode": "SMA", + "output": "value", + "params": { + "length": 200 + } + }, + "isAbsolute": false + } + ], + "note": "간단한 골든크로스 전략 테스트. RSI 과매수/과매도 시 우선 청산/진입." + } + } + """ + ) + } + ) + ) public ResponseEntity runBacktest(@RequestBody BacktestRequest request, @AuthenticationPrincipal CustomUserDetails customUserDetails) { request.setUserId(customUserDetails.getUserId()); @@ -42,6 +181,10 @@ public ResponseEntity runBacktest(@RequestBody BacktestRequest // 백테스트 실행 정보 삭제 @DeleteMapping("/runs/{backtestRunId}") + @Operation( + summary = "백테스트 실행 정보 삭제", + description = "지정된 백테스트 실행 ID에 대한 기록을 삭제합니다." + ) public ResponseEntity deleteBacktest(@PathVariable Long backtestRunId, @AuthenticationPrincipal CustomUserDetails customUserDetails) { backtestService.deleteBacktest(backtestRunId, customUserDetails.getUserId()); @@ -50,6 +193,10 @@ public ResponseEntity deleteBacktest(@PathVariable Long backtestRunId, // 백테스트를 특정 템플릿에 저장 @PatchMapping("/runs/{backtestRunId}") + @Operation( + summary = "템플릿에 백테스트 저장", + description = "지정된 백테스트를 특정 템플릿에 추가합니다." + ) public ResponseEntity postBacktestIntoTemplate(@RequestBody BacktestRequest request, @AuthenticationPrincipal CustomUserDetails customUserDetails) { request.setUserId(customUserDetails.getUserId()); @@ -59,6 +206,10 @@ public ResponseEntity postBacktestIntoTemplate(@RequestBody BacktestReques // 특정 템플릿의 백테스트 리스트 삭제 @DeleteMapping("/templates/{templateId}/runs") + @Operation( + summary = "템플릿의 백테스트 삭제", + description = "지정된 템플릿에서 특정 백테스트를 삭제합니다." + ) public ResponseEntity deleteBacktestFromTemplate(@RequestBody BacktestRequest request, @PathVariable UUID templateId, @AuthenticationPrincipal CustomUserDetails customUserDetails) { diff --git a/backend/src/main/java/org/sejongisc/backend/backtest/dto/BacktestRequest.java b/backend/src/main/java/org/sejongisc/backend/backtest/dto/BacktestRequest.java index 28853d65..68258b9b 100644 --- a/backend/src/main/java/org/sejongisc/backend/backtest/dto/BacktestRequest.java +++ b/backend/src/main/java/org/sejongisc/backend/backtest/dto/BacktestRequest.java @@ -23,7 +23,7 @@ public class BacktestRequest { @Schema(description = "백테스트 ID") private Long backtestRunId; - @Schema(description = "백테스트 제목") + @Schema(description = "백테스트 제목", defaultValue = "골든크로스 + RSI (AAPL)") private String title; @Schema(description = "백테스트 시작일") diff --git a/backend/src/main/java/org/sejongisc/backend/backtest/dto/BacktestRunRequest.java b/backend/src/main/java/org/sejongisc/backend/backtest/dto/BacktestRunRequest.java index ff51e305..709b4e07 100644 --- a/backend/src/main/java/org/sejongisc/backend/backtest/dto/BacktestRunRequest.java +++ b/backend/src/main/java/org/sejongisc/backend/backtest/dto/BacktestRunRequest.java @@ -17,10 +17,11 @@ @NoArgsConstructor @AllArgsConstructor public class BacktestRunRequest { - @Schema(description = "초기 자본금") + + @Schema(description = "초기 자본금", defaultValue = "10000000") private BigDecimal initialCapital; - @Schema(description = "대상 종목 티커") + @Schema(description = "대상 종목 티커", defaultValue = "AAPL") private String ticker; @Schema(description = "매수 조건 그룹") @@ -29,7 +30,7 @@ public class BacktestRunRequest { @Schema(description = "매도 조건 그룹") private List sellConditions; - @Schema(description = "노트") + @Schema(description = "노트", defaultValue = "골든크로스 + RSI 필터 전략 테스트") private String note; /* diff --git a/backend/src/main/java/org/sejongisc/backend/backtest/dto/StrategyCondition.java b/backend/src/main/java/org/sejongisc/backend/backtest/dto/StrategyCondition.java index 5ba624e3..be2ccc6f 100644 --- a/backend/src/main/java/org/sejongisc/backend/backtest/dto/StrategyCondition.java +++ b/backend/src/main/java/org/sejongisc/backend/backtest/dto/StrategyCondition.java @@ -1,5 +1,6 @@ package org.sejongisc.backend.backtest.dto; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; @@ -13,19 +14,15 @@ @AllArgsConstructor public class StrategyCondition { - // 좌항 + @Schema(description = "좌향") private StrategyOperand leftOperand; - // 연산자 (예: "GT", "LT", "CROSSES_ABOVE") + @Schema(description = "연산자 (예: \"GT\", \"LT\", \"CROSSES_ABOVE\")") private String operator; - // 우항 + @Schema(description = "우향") private StrategyOperand rightOperand; - /** - * "무조건 행동" 조건인지 여부 - * true = 이 조건이 맞으면 다른 '일반' 조건 무시 - * false = '일반' 조건 - */ + @Schema(description = "\"무조건 행동\" 조건인지 여부 (true = 이 조건이 맞으면 다른 '일반' 조건 무시, false = 이 조건은 일반 조건)") private boolean isAbsolute; } \ No newline at end of file diff --git a/backend/src/main/java/org/sejongisc/backend/backtest/dto/StrategyOperand.java b/backend/src/main/java/org/sejongisc/backend/backtest/dto/StrategyOperand.java index 73aae8d6..68c31188 100644 --- a/backend/src/main/java/org/sejongisc/backend/backtest/dto/StrategyOperand.java +++ b/backend/src/main/java/org/sejongisc/backend/backtest/dto/StrategyOperand.java @@ -1,5 +1,6 @@ package org.sejongisc.backend.backtest.dto; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; @@ -16,24 +17,21 @@ @AllArgsConstructor public class StrategyOperand { - // 항의 타입: "indicator", "price", "const" + @Schema(description = "항의 타입: \"indicator\", \"price\", \"const\"") private String type; - // type == "indicator" 일 때 - // 지표 코드 (예: "SMA", "RSI", "MACD") + @Schema(description = "type == \"indicator\" 일 때의 지표 코드 (예: \"SMA\", \"RSI\", \"MACD\")") private String indicatorCode; - // type == "price" 일 때 - // 가격 필드 (예: "Close", "Open", "High", "Low", "Volume") + @Schema(description = "type == \"price\" 일 때의 가격 필드 (예: \"Close\", \"Open\", \"High\", \"Low\", \"Volume\")") private String priceField; - // type == "const" 일 때 - // 상수 값 (예: 30, 0.02) + @Schema(description = "type == \"const\" 일 때의 상수 값 (예: 30, 0.02)") private BigDecimal constantValue; - // 지표의 출력값 (예: "value", "macd", "signal", "hist") + @Schema(description = "지표의 출력값 (예: \"value\", \"macd\", \"signal\", \"hist\")") private String output; - // 지표의 파라미터 맵 (예: {"length": 20}) + @Schema(description = "지표의 파라미터 맵 (예: {\"length\": 20})") private Map params; } \ No newline at end of file diff --git a/backend/src/main/java/org/sejongisc/backend/backtest/repository/BacktestRunRepository.java b/backend/src/main/java/org/sejongisc/backend/backtest/repository/BacktestRunRepository.java index e23ab6a0..51276412 100644 --- a/backend/src/main/java/org/sejongisc/backend/backtest/repository/BacktestRunRepository.java +++ b/backend/src/main/java/org/sejongisc/backend/backtest/repository/BacktestRunRepository.java @@ -2,6 +2,18 @@ import org.sejongisc.backend.backtest.entity.BacktestRun; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; +import java.util.List; +import java.util.UUID; + +@Repository public interface BacktestRunRepository extends JpaRepository { + @Query("SELECT br FROM BacktestRun br " + + "JOIN FETCH br.template t " + + "WHERE t.templateId = :templateTemplateId " + + "ORDER BY br.startedAt DESC") + List findByTemplate_TemplateIdWithTemplate(@Param("templateTemplateId") UUID templateTemplateId); } diff --git a/backend/src/main/java/org/sejongisc/backend/common/config/PrimaryDataSourceConfig.java b/backend/src/main/java/org/sejongisc/backend/common/config/PrimaryDataSourceConfig.java index 2405b2f4..40b643af 100644 --- a/backend/src/main/java/org/sejongisc/backend/common/config/PrimaryDataSourceConfig.java +++ b/backend/src/main/java/org/sejongisc/backend/common/config/PrimaryDataSourceConfig.java @@ -16,6 +16,9 @@ import javax.sql.DataSource; import com.zaxxer.hikari.HikariDataSource; +import java.util.HashMap; +import java.util.Map; + @Configuration @EnableTransactionManagement @EnableJpaRepositories( @@ -48,6 +51,11 @@ public DataSource primaryDataSource(@Qualifier("primaryDataSourceProperties") Da public LocalContainerEntityManagerFactoryBean primaryEntityManagerFactory( EntityManagerFactoryBuilder builder, @Qualifier("primaryDataSource") DataSource dataSource) { + + Map jpaProperties = new HashMap<>(); + jpaProperties.put("hibernate.dialect", "org.hibernate.dialect.PostgreSQLDialect"); + jpaProperties.put("hibernate.hbm2ddl.auto", "update"); + return builder .dataSource(dataSource) .packages( @@ -62,6 +70,7 @@ public LocalContainerEntityManagerFactoryBean primaryEntityManagerFactory( "org.sejongisc.backend.user.entity" ) .persistenceUnit("primary") + .properties(jpaProperties) .build(); } diff --git a/backend/src/main/java/org/sejongisc/backend/common/config/StockDataSourceConfig.java b/backend/src/main/java/org/sejongisc/backend/common/config/StockDataSourceConfig.java index 0fe15a78..c234e8f5 100644 --- a/backend/src/main/java/org/sejongisc/backend/common/config/StockDataSourceConfig.java +++ b/backend/src/main/java/org/sejongisc/backend/common/config/StockDataSourceConfig.java @@ -45,10 +45,16 @@ public LocalContainerEntityManagerFactoryBean stockEntityManagerFactory( EntityManagerFactoryBuilder builder, @Qualifier("stockDataSource") DataSource dataSource) { + Map jpaProperties = new HashMap<>(); + jpaProperties.put("hibernate.dialect", "org.hibernate.dialect.PostgreSQLDialect"); + jpaProperties.put("hibernate.default_schema", "public"); + jpaProperties.put("hibernate.hbm2ddl.auto", "none"); + return builder .dataSource(dataSource) .packages("org.sejongisc.backend.stock.entity") // PriceData 엔티티가 있는 패키지 지정 .persistenceUnit("stock") // Persistence Unit 이름 + .properties(jpaProperties) .build(); } diff --git a/backend/src/main/java/org/sejongisc/backend/point/controller/PointHistoryController.java b/backend/src/main/java/org/sejongisc/backend/point/controller/PointHistoryController.java index bd42a436..bdc106a9 100644 --- a/backend/src/main/java/org/sejongisc/backend/point/controller/PointHistoryController.java +++ b/backend/src/main/java/org/sejongisc/backend/point/controller/PointHistoryController.java @@ -1,6 +1,8 @@ package org.sejongisc.backend.point.controller; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.sejongisc.backend.common.auth.springsecurity.CustomUserDetails; import org.sejongisc.backend.point.dto.PointHistoryResponse; @@ -16,25 +18,32 @@ @RestController @RequestMapping("/api/points") @RequiredArgsConstructor +@Tag( + name = "포인트 내역 및 리더보드 API", + description = "포인트 내역 조회 및 리더보드 관련 API 제공" +) public class PointHistoryController { private final PointHistoryService pointHistoryService; @GetMapping("/history") - public ResponseEntity getPointHistory( - @RequestParam int pageNumber, - @RequestParam int pageSize, - @AuthenticationPrincipal CustomUserDetails customUserDetails - ) { + @Operation( + summary = "포인트 내역 조회", + description = "인증된 사용자의 포인트 내역을 페이지네이션하여 조회합니다." + ) + public ResponseEntity getPointHistory(@RequestParam int pageNumber, @RequestParam int pageSize, + @AuthenticationPrincipal CustomUserDetails customUserDetails) { return ResponseEntity.ok(pointHistoryService.getPointHistoryListByUserId( customUserDetails.getUserId(), PageRequest.of(pageNumber, pageSize)) ); } @GetMapping("/leaderboard") - public ResponseEntity getPointLeaderboard( - @RequestParam int period - ) { + @Operation( + summary = "포인트 리더보드 조회", + description = "지정된 기간 동안의 포인트 리더보드를 조회합니다. 기간은 일간, 주간, 월간 단위의 요청이 가능합니다. ex) /leaderboard?period=1 or 7 or 30" + ) + public ResponseEntity getPointLeaderboard(@RequestParam int period) { return ResponseEntity.ok(pointHistoryService.getPointLeaderboard(period)); } } diff --git a/backend/src/main/java/org/sejongisc/backend/stock/repository/PriceDataRepository.java b/backend/src/main/java/org/sejongisc/backend/stock/repository/PriceDataRepository.java index fc8d4519..655022f3 100644 --- a/backend/src/main/java/org/sejongisc/backend/stock/repository/PriceDataRepository.java +++ b/backend/src/main/java/org/sejongisc/backend/stock/repository/PriceDataRepository.java @@ -11,4 +11,5 @@ @Repository public interface PriceDataRepository extends JpaRepository { List findByTickerAndDateBetweenOrderByDateAsc(String ticker, LocalDate startDate, LocalDate endDate); + List findByTicker(String ticker); } \ No newline at end of file diff --git a/backend/src/main/java/org/sejongisc/backend/template/controller/TemplateController.java b/backend/src/main/java/org/sejongisc/backend/template/controller/TemplateController.java index 90f9ab06..371a380b 100644 --- a/backend/src/main/java/org/sejongisc/backend/template/controller/TemplateController.java +++ b/backend/src/main/java/org/sejongisc/backend/template/controller/TemplateController.java @@ -1,5 +1,7 @@ package org.sejongisc.backend.template.controller; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.sejongisc.backend.common.auth.springsecurity.CustomUserDetails; import org.sejongisc.backend.template.dto.TemplateRequest; @@ -16,25 +18,40 @@ @RestController @RequiredArgsConstructor @RequestMapping("/api/backtest/templates") +@Tag( + name = "템플릿 API", + description = "백테스트 템플릿 관련 API 제공" +) public class TemplateController { private final TemplateService templateService; - // 템플릿 목록 조회 @GetMapping + @Operation( + summary = "템플릿 목록 조회", + description = "사용자의 백테스트 템플릿 목록을 조회합니다." + ) public ResponseEntity getTemplateList(@AuthenticationPrincipal CustomUserDetails customUserDetails) { return ResponseEntity.ok(templateService.findAllByUserId(customUserDetails.getUserId())); } // 템플릿 상세 조회 @GetMapping("/{templateId}") + @Operation( + summary = "템플릿 상세 조회", + description = "지정된 템플릿 ID에 대한 상세 정보 및 템플릿에 저장된 백테스트 실행 기록들을 조회합니다." + ) public ResponseEntity getTemplateById(@PathVariable UUID templateId, @AuthenticationPrincipal CustomUserDetails customUserDetails) { - return ResponseEntity.ok(templateService.findById(templateId)); + return ResponseEntity.ok(templateService.findById(templateId, customUserDetails.getUserId())); } // 템플릿 생성 @PostMapping + @Operation( + summary = "템플릿 생성", + description = "새로운 백테스트 템플릿을 생성합니다." + ) public ResponseEntity createTemplate(@RequestBody TemplateRequest request, @AuthenticationPrincipal CustomUserDetails customUserDetail) { request.setUserId(customUserDetail.getUserId()); @@ -43,6 +60,10 @@ public ResponseEntity createTemplate(@RequestBody TemplateRequ // 템플릿 수정 @PatchMapping("/{templateId}") + @Operation( + summary = "템플릿 수정", + description = "기존의 백테스트 템플릿을 수정합니다." + ) public ResponseEntity updateTemplate(@RequestBody TemplateRequest request, @AuthenticationPrincipal CustomUserDetails customUserDetails) { return ResponseEntity.ok(templateService.updateTemplate(request)); @@ -50,6 +71,10 @@ public ResponseEntity updateTemplate(@RequestBody TemplateRequ // 템플릿 삭제 @DeleteMapping("/{templateId}") + @Operation( + summary = "템플릿 삭제", + description = "지정된 템플릿 ID에 대한 템플릿을 삭제합니다." + ) public ResponseEntity deleteTemplate(@PathVariable UUID templateId, @AuthenticationPrincipal CustomUserDetails customUserDetails) { templateService.deleteTemplate(templateId, customUserDetails.getUserId()); diff --git a/backend/src/main/java/org/sejongisc/backend/template/dto/TemplateResponse.java b/backend/src/main/java/org/sejongisc/backend/template/dto/TemplateResponse.java index ca70c63b..0be5bc73 100644 --- a/backend/src/main/java/org/sejongisc/backend/template/dto/TemplateResponse.java +++ b/backend/src/main/java/org/sejongisc/backend/template/dto/TemplateResponse.java @@ -2,6 +2,7 @@ import lombok.Builder; import lombok.Getter; +import org.sejongisc.backend.backtest.entity.BacktestRun; import org.sejongisc.backend.template.entity.Template; import java.util.List; @@ -12,4 +13,5 @@ public class TemplateResponse { private List