Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,4 @@ __pycache__/
/venv/
/env
/.vs
backend/src/main/java/org/sejongisc/backend/stock/TestController.java
1 change: 0 additions & 1 deletion backend/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -14,26 +19,160 @@

@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<BacktestResponse> getBacktestStatus(@PathVariable Long backtestRunId,
@AuthenticationPrincipal CustomUserDetails customUserDetails) {
return ResponseEntity.ok(backtestService.getBacktestStatus(backtestRunId, customUserDetails.getUserId()));
}

// 백테스트 기록 상세 조회
@GetMapping("/runs/{backtestRunId}")
@Operation(
summary = "백테스트 실행 기록 상세 조회",
description = "지정된 백테스트 실행 ID에 대한 상세 결과를 조회합니다."
)
public ResponseEntity<BacktestResponse> getBackTestResultDetails(@PathVariable Long backtestRunId,
@AuthenticationPrincipal CustomUserDetails customUserDetails) {
return ResponseEntity.ok(backtestService.getBackTestDetails(backtestRunId, customUserDetails.getUserId()));
}

// 백테스트 실행
@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<BacktestResponse> runBacktest(@RequestBody BacktestRequest request,
@AuthenticationPrincipal CustomUserDetails customUserDetails) {
request.setUserId(customUserDetails.getUserId());
Expand All @@ -42,6 +181,10 @@ public ResponseEntity<BacktestResponse> runBacktest(@RequestBody BacktestRequest

// 백테스트 실행 정보 삭제
@DeleteMapping("/runs/{backtestRunId}")
@Operation(
summary = "백테스트 실행 정보 삭제",
description = "지정된 백테스트 실행 ID에 대한 기록을 삭제합니다."
)
public ResponseEntity<Void> deleteBacktest(@PathVariable Long backtestRunId,
@AuthenticationPrincipal CustomUserDetails customUserDetails) {
backtestService.deleteBacktest(backtestRunId, customUserDetails.getUserId());
Expand All @@ -50,6 +193,10 @@ public ResponseEntity<Void> deleteBacktest(@PathVariable Long backtestRunId,

// 백테스트를 특정 템플릿에 저장
@PatchMapping("/runs/{backtestRunId}")
@Operation(
summary = "템플릿에 백테스트 저장",
description = "지정된 백테스트를 특정 템플릿에 추가합니다."
)
public ResponseEntity<Void> postBacktestIntoTemplate(@RequestBody BacktestRequest request,
@AuthenticationPrincipal CustomUserDetails customUserDetails) {
request.setUserId(customUserDetails.getUserId());
Expand All @@ -59,6 +206,10 @@ public ResponseEntity<Void> postBacktestIntoTemplate(@RequestBody BacktestReques

// 특정 템플릿의 백테스트 리스트 삭제
@DeleteMapping("/templates/{templateId}/runs")
@Operation(
summary = "템플릿의 백테스트 삭제",
description = "지정된 템플릿에서 특정 백테스트를 삭제합니다."
)
public ResponseEntity<Void> deleteBacktestFromTemplate(@RequestBody BacktestRequest request,
@PathVariable UUID templateId,
@AuthenticationPrincipal CustomUserDetails customUserDetails) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "백테스트 시작일")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "매수 조건 그룹")
Expand All @@ -29,7 +30,7 @@ public class BacktestRunRequest {
@Schema(description = "매도 조건 그룹")
private List<StrategyCondition> sellConditions;

@Schema(description = "노트")
@Schema(description = "노트", defaultValue = "골든크로스 + RSI 필터 전략 테스트")
private String note;

/*
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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<String, Object> params;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<BacktestRun, Long> {
@Query("SELECT br FROM BacktestRun br " +
"JOIN FETCH br.template t " +
"WHERE t.templateId = :templateTemplateId " +
"ORDER BY br.startedAt DESC")
List<BacktestRun> findByTemplate_TemplateIdWithTemplate(@Param("templateTemplateId") UUID templateTemplateId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
import javax.sql.DataSource;
import com.zaxxer.hikari.HikariDataSource;

import java.util.HashMap;
import java.util.Map;

@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(
Expand Down Expand Up @@ -48,6 +51,11 @@ public DataSource primaryDataSource(@Qualifier("primaryDataSourceProperties") Da
public LocalContainerEntityManagerFactoryBean primaryEntityManagerFactory(
EntityManagerFactoryBuilder builder,
@Qualifier("primaryDataSource") DataSource dataSource) {

Map<String, String> jpaProperties = new HashMap<>();
jpaProperties.put("hibernate.dialect", "org.hibernate.dialect.PostgreSQLDialect");
jpaProperties.put("hibernate.hbm2ddl.auto", "update");

return builder
.dataSource(dataSource)
.packages(
Expand All @@ -62,6 +70,7 @@ public LocalContainerEntityManagerFactoryBean primaryEntityManagerFactory(
"org.sejongisc.backend.user.entity"
)
.persistenceUnit("primary")
.properties(jpaProperties)
.build();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,16 @@ public LocalContainerEntityManagerFactoryBean stockEntityManagerFactory(
EntityManagerFactoryBuilder builder,
@Qualifier("stockDataSource") DataSource dataSource) {

Map<String, String> 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();
}

Expand Down
Loading