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
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
import org.sejongisc.backend.user.entity.User;

import java.time.LocalDate;
import java.util.List;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,6 @@
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.sejongisc.backend.backtest.entity.BacktestRun;
import org.sejongisc.backend.backtest.entity.BacktestStatus;
import org.sejongisc.backend.template.entity.Template;

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


Expand Down
Original file line number Diff line number Diff line change
@@ -1,37 +1,28 @@
package org.sejongisc.backend.backtest.dto;


import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.sejongisc.backend.backtest.entity.BacktestRunMetrics;

import java.math.BigDecimal;


@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class BacktestRunMetricsResponse {
private Long id;
private BigDecimal totalReturn; // 총 수익률
private BigDecimal maxDrawdown; // 최대 낙폭
private BigDecimal sharpeRatio; // 샤프 지수
private BigDecimal avgHoldDays; // 평균 보유 기간
private int tradesCount; // 총 거래 횟수
private String assetCurveJson;

public record BacktestRunMetricsResponse(
Long id,
BigDecimal totalReturn, // 총 수익률
BigDecimal maxDrawdown, // 최대 낙폭
BigDecimal sharpeRatio, // 샤프 지수
BigDecimal avgHoldDays, // 평균 보유 기간
int tradesCount, // 총 거래 횟수
String assetCurveJson
) {
public static BacktestRunMetricsResponse fromEntity(BacktestRunMetrics backtestRunMetrics) {
return BacktestRunMetricsResponse.builder()
.id(backtestRunMetrics.getId())
.totalReturn(backtestRunMetrics.getTotalReturn())
.maxDrawdown(backtestRunMetrics.getMaxDrawdown())
.sharpeRatio(backtestRunMetrics.getSharpeRatio())
.avgHoldDays(backtestRunMetrics.getAvgHoldDays())
.tradesCount(backtestRunMetrics.getTradesCount())
.assetCurveJson(backtestRunMetrics.getAssetCurveJson())
.build();
return new BacktestRunMetricsResponse(
backtestRunMetrics.getId(),
backtestRunMetrics.getTotalReturn(),
backtestRunMetrics.getMaxDrawdown(),
backtestRunMetrics.getSharpeRatio(),
backtestRunMetrics.getAvgHoldDays(),
backtestRunMetrics.getTradesCount(),
backtestRunMetrics.getAssetCurveJson()
);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import lombok.NoArgsConstructor;

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

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,28 +1,22 @@
package org.sejongisc.backend.backtest.dto;

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

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

@Schema(description = "좌향")
private StrategyOperand leftOperand;
import io.swagger.v3.oas.annotations.media.Schema;

public record StrategyCondition(

@Schema(description = "좌향 (Operand)")
StrategyOperand leftOperand,

@Schema(description = "연산자 (예: \"GT\", \"LT\", \"CROSSES_ABOVE\")")
private String operator;
@Schema(description = "연산자 (예: \"GT\", \"LT\", \"CROSSES_ABOVE\")")
String operator,

@Schema(description = "우향")
private StrategyOperand rightOperand;
@Schema(description = "우향 (Operand)")
StrategyOperand rightOperand,

@Schema(description = "\"무조건 행동\" 조건인지 여부 (true = 이 조건이 맞으면 다른 '일반' 조건 무시, false = 이 조건은 일반 조건)")
private boolean isAbsolute;
}
@Schema(description = "\"무조건 행동\" 조건인지 여부 (true = OR 조건, false = AND 조건)")
boolean isAbsolute
) {}
Original file line number Diff line number Diff line change
@@ -1,41 +1,40 @@
package org.sejongisc.backend.backtest.dto;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import java.util.Map;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class StrategyOperand {
public record StrategyOperand(

@Schema(description = "유형 (price: 가격데이터, const: 상수, indicator: 보조지표)", example = "indicator")
private String type; // 'price', 'const', 'indicator'
@Schema(description = "유형 (price: 가격데이터, const: 상수, indicator: 보조지표)", example = "indicator")
@NotNull(message = "피연산자 유형은 필수입니다.")
String type, // 'price', 'const', 'indicator'

@Schema(description = "지표 코드 (SMA, EMA, RSI, MACD, BB, STOCH, CCI, ATR, ADX)", example = "BB")
private String indicatorCode;
@Schema(description = "지표 코드 (SMA, EMA, RSI, MACD, BB, STOCH, CCI, ATR, ADX)", example = "BB")
String indicatorCode,

@Schema(description = "가격 기준 (Close, Open, High, Low, Volume)", example = "Close")
private String priceField;
@Schema(description = "가격 기준 (Close, Open, High, Low, Volume)", example = "Close")
String priceField,

@Schema(description = "상수 값 (type이 const일 때 사용)", example = "30")
private Double constantValue;
@Schema(description = "상수 값 (type이 const일 때 사용)", example = "30")
Double constantValue,

@Schema(description = """
@Schema(description = """
지표별 결과값 선택 (다중 출력 지표용):
- BB (볼린저밴드): upper, middle, lower
- MACD: macd, signal, hist
- STOCH (스토캐스틱): k, d
- 나머지 단일 지표는 null 혹은 생략 가능
""", example = "lower")
private String output;
String output,

@Schema(description = """
@Schema(description = """
지표별 필수 파라미터 (Map 형식):
- SMA, EMA, RSI, CCI, ATR, ADX: { "length": 14 }
- MACD: { "fast": 12, "slow": 26, "signal": 9 }
- BB (볼린저밴드): { "length": 20, "k": 2.0 }
- STOCH (스토캐스틱): { "kLength": 14, "dLength": 3 }
""", example = "{\"length\": 20, \"k\": 2.0}")
private Map<String, Object> params;
}
@NotNull(message = "파라미터 맵은 필수입니다. (비어있더라도 {} 전달)")
Map<String, Object> params
) {}
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,12 @@
import java.math.BigDecimal;
import java.time.LocalDateTime;

public class TradeLog {
public enum Type {
BUY,
SELL,
SELL_FORCED // 기본 청산 기간에 의한 강제 매도 구분용 추가
}
public final Type type;
public final LocalDateTime time;
public final BigDecimal price;
public final BigDecimal shares;

public TradeLog(Type type, LocalDateTime time, BigDecimal price, BigDecimal shares) {
this.type = type;
this.time = time;
this.price = price;
this.shares = shares;
}
}
/**
* 백테스팅 거래 기록 (불변 객체)
*/
public record TradeLog(
TradeType type, // Enum 변경 적용
LocalDateTime time,
BigDecimal price,
BigDecimal shares
) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package org.sejongisc.backend.backtest.dto;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public enum TradeType {
BUY("매수"),
SELL("매도"),
SELL_FORCED("기간 만료 강제 청산");

private final String description;
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import lombok.extern.slf4j.Slf4j;
import org.sejongisc.backend.backtest.dto.BacktestRunRequest;
import org.sejongisc.backend.backtest.dto.TradeLog;
import org.sejongisc.backend.backtest.dto.TradeType;
import org.sejongisc.backend.backtest.entity.BacktestRun;
import org.sejongisc.backend.backtest.entity.BacktestRunMetrics;
import org.sejongisc.backend.backtest.entity.BacktestStatus;
Expand All @@ -30,6 +31,8 @@
import java.util.List;
import java.util.Map;

import static org.sejongisc.backend.backtest.dto.TradeType.*;

@Service
@RequiredArgsConstructor
@Slf4j
Expand Down Expand Up @@ -115,7 +118,7 @@ public void execute(BacktestRun backtestRun) {
// 거래 로그 기록
if (buyShares.compareTo(BigDecimal.ZERO) > 0) {
BigDecimal transactionCost = buyShares.multiply(currentClosePrice);
tradeLogs.add(new TradeLog(TradeLog.Type.BUY, currentTime, currentClosePrice, buyShares));
tradeLogs.add(new TradeLog(BUY, currentTime, currentClosePrice, buyShares));
shares = shares.add(buyShares); // 매수 주식 수
cash = cash.subtract(transactionCost); // 잔고에서 매수 대금 차감
tradesCount++; // 거래 횟수 증가
Expand All @@ -131,7 +134,7 @@ else if (shares.compareTo(BigDecimal.ZERO) > 0 && (shouldSell || shouldExitByDay
BigDecimal sharesToSell = shares.multiply(sellRatio).setScale(8, RoundingMode.DOWN); // 매도 비중 적용
BigDecimal tradeValue = sharesToSell.multiply(currentClosePrice);
// 거래 로그 기록
TradeLog.Type logType = shouldExitByDays ? TradeLog.Type.SELL_FORCED : TradeLog.Type.SELL; // 강제 청산 여부에 따른 로그 타입 설정
TradeType logType = shouldExitByDays ? SELL_FORCED : SELL;// 강제 청산 여부에 따른 로그 타입 설정
tradeLogs.add(new TradeLog(logType, currentTime, currentClosePrice, sharesToSell));
shares = shares.subtract(sharesToSell); // 매도 주식 수 차감
cash = cash.add(tradeValue); // 잔고에서 매도 대금 추가
Expand Down Expand Up @@ -227,10 +230,10 @@ private BigDecimal calculateAvgHoldDays(List<TradeLog> tradeLogs) {

// 매수-매도 쌍을 찾아 보유 기간 계산
for (TradeLog log : tradeLogs) {
if (log.type == TradeLog.Type.BUY) {
currentBuyTime = log.time;
} else if ((log.type == TradeLog.Type.SELL || log.type == TradeLog.Type.SELL_FORCED) && currentBuyTime != null) {
long days = java.time.temporal.ChronoUnit.DAYS.between(currentBuyTime.toLocalDate(), log.time.toLocalDate());
if (log.type() == BUY) {
currentBuyTime = log.time();
} else if ((log.type() == SELL || log.type() == SELL_FORCED) && currentBuyTime != null) {
long days = java.time.temporal.ChronoUnit.DAYS.between(currentBuyTime.toLocalDate(), log.time().toLocalDate());
holdDurations.add(days);
currentBuyTime = null;
}
Expand Down
Loading