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
3 changes: 3 additions & 0 deletions backend/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ dependencies {
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.3'

implementation 'org.springframework.boot:spring-boot-starter-validation'

// backtesting library
implementation 'org.ta4j:ta4j-core:0.15'
Comment on lines +60 to +62
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

ta4j-core 버전 최신성 확인 및 BOM 연동 고려

백테스팅 라이브러리 추가는 👍. 다만 0.15가 현재 기준 최신 안정판인지 확인해 주세요. Spring BOM에 의해 관리되지 않는 아티팩트라 주기적 점검이 필요합니다. 필요 시 버전 카탈로그(gradle/libs.versions.toml)나 dependencyManagement에 명시적으로 등록을 권장합니다.


🌐 Web query:

What is the latest stable version of org.ta4j:ta4j-core as of today?

💡 Result:

The latest stable version is 0.18. [1][2]

Sources:
[1] GitHub ta4j README (shows dependency version 0.18).
[2] MVNRepository listing for org.ta4j:ta4j-core — 0.18 (May 15, 2025).


org.ta4j:ta4j-core 버전 업그레이드 필요

현재 지정된 0.15 버전은 더 이상 최신 안정판이 아닙니다. 최신 안정 버전은 0.18입니다(2025년 5월 출시). 0.15에서 0.18로 업그레이드하여 호환성 개선과 잠재적 보안 업데이트를 적용해 주세요. 이 아티팩트는 Spring BOM의 관리 대상이 아니므로, 버전 카탈로그(gradle/libs.versions.toml)나 dependencyManagement 블록에 명시적으로 등록하여 장기 유지보수성을 높이는 것을 권장합니다.

🤖 Prompt for AI Agents
In backend/build.gradle around lines 60 to 62, the ta4j dependency is pinned to
0.15 and needs to be upgraded; update the implementation declaration to use
version 0.18 instead of 0.15, and add a corresponding entry in the version
catalog (gradle/libs.versions.toml) or the project's dependencyManagement block
so the artifact version is managed centrally for future maintenance; ensure the
build.gradle uses the catalog alias (or dependencyManagement BOM) rather than a
hardcoded literal version to keep consistency.

}

jacoco {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package org.sejongisc.backend.backtest.dto;

import com.fasterxml.jackson.annotation.JsonIgnore;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
import org.sejongisc.backend.user.entity.User;
Expand All @@ -11,16 +13,29 @@
@Getter
@Setter
public class BacktestRequest {
// hidden 설정하기
@Schema(hidden = true, description = "회원")
@JsonIgnore
private UUID userId;

@Schema(description = "템플릿 ID")
private UUID templateId;

@Schema(description = "백테스트 ID")
private Long backtestRunId;

@Schema(description = "백테스트 제목")
private String title;
private String paramsJson;

@Schema(description = "백테스트 시작일")
private LocalDate startDate;

@Schema(description = "백테스트 종료일")
private LocalDate endDate;

@Schema(description = "백테스트 실행 요청 JSON")
private BacktestRunRequest strategy;

// 백테스트 리스트 삭제
@Schema(description = "삭제할 백테스트 실행 리스트")
private List<Long> backtestRunIds;
}
Original file line number Diff line number Diff line change
@@ -1,19 +1,33 @@
package org.sejongisc.backend.backtest.dto;

import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.JdbcTypeCode;
import org.hibernate.type.SqlTypes;
import org.sejongisc.backend.backtest.entity.BacktestRun;
import org.sejongisc.backend.backtest.entity.BacktestRunMetrics;
import org.sejongisc.backend.backtest.entity.BacktestStatus;
import org.sejongisc.backend.template.entity.Template;
import org.sejongisc.backend.user.entity.User;

import java.time.LocalDate;


@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class BacktestResponse {
private BacktestRun backtestRun;
private BacktestRunMetrics backtestRunMetrics;
private Long id;
private Template template;
private String title;
private BacktestStatus status;
private String paramsJson;
private LocalDate startDate;
private LocalDate endDate;

private BacktestRunMetrics backtestRunMetrics;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package org.sejongisc.backend.backtest.dto;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.List;

/**
* 백테스트 실행 요청 시 Body에 담길 메인 DTO
* (이 객체가 BacktestRun.paramsJson에 직렬화되어 저장됨)
*/
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class BacktestRunRequest {
@Schema(description = "초기 자본금")
private BigDecimal initialCapital;

@Schema(description = "대상 종목 티커")
private String ticker;

@Schema(description = "매수 조건 그룹")
private List<StrategyCondition> buyConditions;

@Schema(description = "매도 조건 그룹")
private List<StrategyCondition> sellConditions;

@Schema(description = "노트")
private String note;

/*
* 타임 프레임 (예: "D", "W", "M")
private String timeFrame;
*/
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package org.sejongisc.backend.backtest.dto;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

/**
* 전략 조건 한 줄 (Operand + Operator + Operand)
* 예: [SMA(20)] [GT] [Close]
*/
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class StrategyCondition {

// 좌항
private StrategyOperand leftOperand;

// 연산자 (예: "GT", "LT", "CROSSES_ABOVE")
private String operator;

// 우항
private StrategyOperand rightOperand;

/**
* "무조건 행동" 조건인지 여부
* true = 이 조건이 맞으면 다른 '일반' 조건 무시
* false = '일반' 조건
*/
private boolean isAbsolute;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package org.sejongisc.backend.backtest.dto;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.math.BigDecimal;
import java.util.Map;

/**
* 전략 조건의 개별 항 (Operand)
* 예: SMA(20), 종가(Close), 30(상수)
*/
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class StrategyOperand {

// 항의 타입: "indicator", "price", "const"
private String type;

// type == "indicator" 일 때
// 지표 코드 (예: "SMA", "RSI", "MACD")
private String indicatorCode;

// type == "price" 일 때
// 가격 필드 (예: "Close", "Open", "High", "Low", "Volume")
private String priceField;

// type == "const" 일 때
// 상수 값 (예: 30, 0.02)
private BigDecimal constantValue;

// 지표의 출력값 (예: "value", "macd", "signal", "hist")
private String output;

// 지표의 파라미터 맵 (예: {"length": 20})
private Map<String, Object> params;
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@


import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.*;
import org.hibernate.annotations.JdbcTypeCode;
import org.hibernate.type.SqlTypes;
import org.sejongisc.backend.template.entity.Template;
import org.sejongisc.backend.user.entity.User;

Expand All @@ -15,6 +14,7 @@

@Entity
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
Expand All @@ -33,7 +33,12 @@ public class BacktestRun {

private String title;

@Enumerated(EnumType.STRING)
private BacktestStatus status;

// 조건/종목 등 파라미터(JSONB). 가장 단순하게 String으로 보관
// 기록 (불변성 목적) : 생성된 순간의 상태 박제 목적
@JdbcTypeCode(SqlTypes.JSON)
@Column(name = "params", columnDefinition = "jsonb")
private String paramsJson;

Expand All @@ -48,6 +53,10 @@ public class BacktestRun {
private LocalDateTime startedAt;
private LocalDateTime finishedAt;

// 오류 발생 시 기록
@Column(name = "error_message", columnDefinition = "TEXT")
private String errorMessage;

public void updateTemplate(Template template) {
this.template = template;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package org.sejongisc.backend.backtest.entity;

public enum BacktestStatus {
PENDING,
RUNNING,
COMPLETED,
FAILED
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
package org.sejongisc.backend.backtest.service;

import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.persistence.EntityManager;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.sejongisc.backend.backtest.dto.BacktestRequest;
import org.sejongisc.backend.backtest.dto.BacktestResponse;
import org.sejongisc.backend.backtest.entity.BacktestRun;
import org.sejongisc.backend.backtest.entity.BacktestRunMetrics;
import org.sejongisc.backend.backtest.entity.BacktestStatus;
import org.sejongisc.backend.backtest.repository.BacktestRunMetricsRepository;
import org.sejongisc.backend.backtest.repository.BacktestRunRepository;
import org.sejongisc.backend.common.exception.CustomException;
import org.sejongisc.backend.common.exception.ErrorCode;
import org.sejongisc.backend.template.entity.Template;
import org.sejongisc.backend.template.repository.TemplateRepository;
import org.sejongisc.backend.user.entity.User;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
Expand All @@ -27,22 +31,37 @@ public class BacktestService {
private final BacktestRunRepository backtestRunRepository;
private final BacktestRunMetricsRepository backtestRunMetricsRepository;
private final TemplateRepository templateRepository;
private final BacktestingEngine backtestingEngine;
private final ObjectMapper objectMapper;
private final EntityManager em;

public BacktestResponse getBacktestStatus(Long backtestRunId, UUID userId) {
// TODO : 백테스트 상태 조회 로직 구현 (진행 중, 완료, 실패 등)
BacktestRun backtestRun = findBacktestRunByIdAndVerifyUser(backtestRunId, userId);
return BacktestResponse.builder()
.backtestRun(backtestRun)
.id(backtestRun.getId())
.paramsJson(backtestRun.getParamsJson())
.title(backtestRun.getTitle())
.status(backtestRun.getStatus())
.startDate(backtestRun.getStartDate())
.endDate(backtestRun.getEndDate())
.template(backtestRun.getTemplate())
.build();
}
@Transactional
public BacktestResponse getBackTestDetails(Long backtestRunId, UUID userId) {
BacktestRun backtestRun = findBacktestRunByIdAndVerifyUser(backtestRunId, userId);
BacktestRunMetrics backtestRunMetrics = backtestRunMetricsRepository.findByBacktestRunId(backtestRunId)
.orElseThrow(() -> new CustomException(ErrorCode.BACKTEST_METRICS_NOT_FOUND));
.orElse(null);
BacktestRun backtestRun = findBacktestRunByIdAndVerifyUser(backtestRunId, userId);

return BacktestResponse.builder()
.backtestRun(backtestRun)
.id(backtestRun.getId())
.paramsJson(backtestRun.getParamsJson())
.title(backtestRun.getTitle())
.status(backtestRun.getStatus())
.startDate(backtestRun.getStartDate())
.endDate(backtestRun.getEndDate())
.template(backtestRun.getTemplate())
.backtestRunMetrics(backtestRunMetrics)
.build();
}
Expand All @@ -62,8 +81,47 @@ public void addBacktestTemplate(BacktestRequest request) {
}

public BacktestResponse runBacktest(BacktestRequest request) {
// TODO : 백테스트 실행 로직 구현 (비동기 처리)
return null;
User userRef = em.getReference(User.class, request.getUserId());

Template templateRef = null;
if (request.getTemplateId() != null)
templateRef = em.getReference(Template.class, request.getTemplateId());

String paramsJson;
try {
paramsJson = objectMapper.writeValueAsString(request.getStrategy());
} catch (Exception e) {
log.error("paramsJson 변환 중 오류 발생", e);
throw new CustomException(ErrorCode.INVALID_BACKTEST_JSON_PARAMS);
}

// BacktestRun 엔티티를 "PENDING" 상태로 생성
BacktestRun backtestRun = BacktestRun.builder()
.user(userRef)
.template(templateRef)
.title(request.getTitle())
.paramsJson(paramsJson)
.startDate(request.getStartDate())
.endDate(request.getEndDate())
.status(BacktestStatus.PENDING)
.build();

BacktestRun savedRun = backtestRunRepository.save(backtestRun);
log.info("백테스팅 실행 요청이 성공적으로 처리되었습니다. ID: {}", savedRun.getId());

// 비동기로 백테스팅 실행 시작
backtestingEngine.execute(savedRun.getId());

// 사용자에게 실행 중 응답 반환
return BacktestResponse.builder()
.id(savedRun.getId())
.paramsJson(savedRun.getParamsJson())
.title(savedRun.getTitle())
.status(savedRun.getStatus())
.startDate(savedRun.getStartDate())
.endDate(savedRun.getEndDate())
.template(templateRef)
.build();
}

@Transactional
Expand Down
Loading
Loading