분석레포트 로직 수정
This commit is contained in:
@@ -12,6 +12,9 @@ public class PaperTradeDto {
|
|||||||
private String orderKind;
|
private String orderKind;
|
||||||
private String source;
|
private String source;
|
||||||
private Long strategyId;
|
private Long strategyId;
|
||||||
|
private String candleType;
|
||||||
|
private String strategyName;
|
||||||
|
private String executionType;
|
||||||
private Double price;
|
private Double price;
|
||||||
private Double quantity;
|
private Double quantity;
|
||||||
private Double grossAmount;
|
private Double grossAmount;
|
||||||
|
|||||||
@@ -28,6 +28,9 @@ public class GcBacktestResult {
|
|||||||
@Column(name = "settings_json", columnDefinition = "JSON")
|
@Column(name = "settings_json", columnDefinition = "JSON")
|
||||||
@JdbcTypeCode(SqlTypes.JSON) private String settingsJson;
|
@JdbcTypeCode(SqlTypes.JSON) private String settingsJson;
|
||||||
|
|
||||||
|
@Column(name = "execution_snapshot_json", columnDefinition = "JSON")
|
||||||
|
@JdbcTypeCode(SqlTypes.JSON) private String executionSnapshotJson;
|
||||||
|
|
||||||
@Column(name = "signals_json", columnDefinition = "JSON")
|
@Column(name = "signals_json", columnDefinition = "JSON")
|
||||||
@JdbcTypeCode(SqlTypes.JSON) private String signalsJson;
|
@JdbcTypeCode(SqlTypes.JSON) private String signalsJson;
|
||||||
|
|
||||||
|
|||||||
@@ -61,6 +61,15 @@ public class GcPaperOrder {
|
|||||||
@Column(name = "strategy_id")
|
@Column(name = "strategy_id")
|
||||||
private Long strategyId;
|
private Long strategyId;
|
||||||
|
|
||||||
|
@Column(name = "candle_type", length = 10)
|
||||||
|
private String candleType;
|
||||||
|
|
||||||
|
@Column(name = "strategy_name", length = 200)
|
||||||
|
private String strategyName;
|
||||||
|
|
||||||
|
@Column(name = "execution_type", length = 30)
|
||||||
|
private String executionType;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 분할매매(staged) 주문 여부.
|
* 분할매매(staged) 주문 여부.
|
||||||
* TRUE 이면 체결(FILLED) 시 allocationService 단계를 자동 진행한다.
|
* TRUE 이면 체결(FILLED) 시 allocationService 단계를 자동 진행한다.
|
||||||
|
|||||||
@@ -35,6 +35,18 @@ public class GcPaperTrade {
|
|||||||
@Column(name = "strategy_id")
|
@Column(name = "strategy_id")
|
||||||
private Long strategyId;
|
private Long strategyId;
|
||||||
|
|
||||||
|
/** 체결 시점 전략 평가 시간봉 (1m, 10m, …) */
|
||||||
|
@Column(name = "candle_type", length = 10)
|
||||||
|
private String candleType;
|
||||||
|
|
||||||
|
/** 체결 시점 전략명 스냅샷 */
|
||||||
|
@Column(name = "strategy_name", length = 200)
|
||||||
|
private String strategyName;
|
||||||
|
|
||||||
|
/** CANDLE_CLOSE | REALTIME_TICK */
|
||||||
|
@Column(name = "execution_type", length = 30)
|
||||||
|
private String executionType;
|
||||||
|
|
||||||
@Column(name = "price", precision = 20, scale = 2, nullable = false)
|
@Column(name = "price", precision = 20, scale = 2, nullable = false)
|
||||||
private BigDecimal price;
|
private BigDecimal price;
|
||||||
|
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ public class BacktestingService {
|
|||||||
: new BooleanRule(false);
|
: new BooleanRule(false);
|
||||||
Rule exitRule = buildExitRule(baseExitRule, series, cfg);
|
Rule exitRule = buildExitRule(baseExitRule, series, cfg);
|
||||||
|
|
||||||
return runBacktest(series, entryRule, exitRule, req, cfg, strategyName);
|
return runBacktest(series, entryRule, exitRule, req, cfg, strategyName, buyDsl, sellDsl, params);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 청산 규칙 합성 ────────────────────────────────────────────────────────
|
// ── 청산 규칙 합성 ────────────────────────────────────────────────────────
|
||||||
@@ -124,7 +124,9 @@ public class BacktestingService {
|
|||||||
|
|
||||||
private BacktestResponse runBacktest(BarSeries series, Rule entryRule, Rule exitRule,
|
private BacktestResponse runBacktest(BarSeries series, Rule entryRule, Rule exitRule,
|
||||||
BacktestRequest req, BacktestSettingsDto cfg,
|
BacktestRequest req, BacktestSettingsDto cfg,
|
||||||
String strategyName) {
|
String strategyName,
|
||||||
|
JsonNode execBuyDsl, JsonNode execSellDsl,
|
||||||
|
Map<String, Map<String, Object>> execParams) {
|
||||||
|
|
||||||
BaseTradingRecord record = new BaseTradingRecord();
|
BaseTradingRecord record = new BaseTradingRecord();
|
||||||
|
|
||||||
@@ -295,7 +297,8 @@ public class BacktestingService {
|
|||||||
Stats stats = toStats(analysis, signals);
|
Stats stats = toStats(analysis, signals);
|
||||||
|
|
||||||
// ── DB 저장 ───────────────────────────────────────────────────────────
|
// ── DB 저장 ───────────────────────────────────────────────────────────
|
||||||
Long resultId = saveResult(req, cfg, signals, analysis, series, strategyName);
|
Long resultId = saveResult(req, cfg, signals, analysis, series, strategyName,
|
||||||
|
execBuyDsl, execSellDsl, execParams);
|
||||||
|
|
||||||
return BacktestResponse.builder()
|
return BacktestResponse.builder()
|
||||||
.signals(signals)
|
.signals(signals)
|
||||||
@@ -601,12 +604,24 @@ public class BacktestingService {
|
|||||||
|
|
||||||
private Long saveResult(BacktestRequest req, BacktestSettingsDto cfg,
|
private Long saveResult(BacktestRequest req, BacktestSettingsDto cfg,
|
||||||
List<Signal> signals, BacktestAnalysisDto analysis,
|
List<Signal> signals, BacktestAnalysisDto analysis,
|
||||||
BarSeries series, String strategyName) {
|
BarSeries series, String strategyName,
|
||||||
|
JsonNode execBuyDsl, JsonNode execSellDsl,
|
||||||
|
Map<String, Map<String, Object>> execParams) {
|
||||||
try {
|
try {
|
||||||
List<OhlcvBar> bars = req.getBars();
|
List<OhlcvBar> bars = req.getBars();
|
||||||
long fromTime = bars.isEmpty() ? 0 : bars.get(0).getTime();
|
long fromTime = bars.isEmpty() ? 0 : bars.get(0).getTime();
|
||||||
long toTime = bars.isEmpty() ? 0 : bars.get(bars.size() - 1).getTime();
|
long toTime = bars.isEmpty() ? 0 : bars.get(bars.size() - 1).getTime();
|
||||||
|
|
||||||
|
Map<String, Object> snapshot = new LinkedHashMap<>();
|
||||||
|
snapshot.put("strategyId", req.getStrategyId());
|
||||||
|
snapshot.put("strategyName", strategyName);
|
||||||
|
snapshot.put("symbol", req.getSymbol() != null ? req.getSymbol() : "UNKNOWN");
|
||||||
|
snapshot.put("timeframe", req.getTimeframe());
|
||||||
|
snapshot.put("barCount", bars.size());
|
||||||
|
if (execParams != null && !execParams.isEmpty()) snapshot.put("indicatorParams", execParams);
|
||||||
|
if (execBuyDsl != null && !execBuyDsl.isNull()) snapshot.put("buyCondition", execBuyDsl);
|
||||||
|
if (execSellDsl != null && !execSellDsl.isNull()) snapshot.put("sellCondition", execSellDsl);
|
||||||
|
|
||||||
GcBacktestResult entity = GcBacktestResult.builder()
|
GcBacktestResult entity = GcBacktestResult.builder()
|
||||||
.deviceId(req.getDeviceId())
|
.deviceId(req.getDeviceId())
|
||||||
.strategyId(req.getStrategyId())
|
.strategyId(req.getStrategyId())
|
||||||
@@ -617,6 +632,7 @@ public class BacktestingService {
|
|||||||
.fromTime(fromTime)
|
.fromTime(fromTime)
|
||||||
.toTime(toTime)
|
.toTime(toTime)
|
||||||
.settingsJson(objectMapper.writeValueAsString(cfg))
|
.settingsJson(objectMapper.writeValueAsString(cfg))
|
||||||
|
.executionSnapshotJson(objectMapper.writeValueAsString(snapshot))
|
||||||
.signalsJson(objectMapper.writeValueAsString(signals))
|
.signalsJson(objectMapper.writeValueAsString(signals))
|
||||||
.analysisJson(objectMapper.writeValueAsString(analysis))
|
.analysisJson(objectMapper.writeValueAsString(analysis))
|
||||||
.totalReturn(BigDecimal.valueOf(analysis.getTotalReturnPct()))
|
.totalReturn(BigDecimal.valueOf(analysis.getTotalReturnPct()))
|
||||||
|
|||||||
+4
-1
@@ -87,10 +87,13 @@ public class BarCloseStrategyEvaluationService {
|
|||||||
closeSignal, signalBar.getClosePrice().doubleValue(),
|
closeSignal, signalBar.getClosePrice().doubleValue(),
|
||||||
candleTimeEpoch, ct, signalExecType);
|
candleTimeEpoch, ct, signalExecType);
|
||||||
if (s.getUserId() != null) {
|
if (s.getUserId() != null) {
|
||||||
|
String strategyName = tradeSignalService.resolveStrategyName(
|
||||||
|
s.getStrategyId(), null);
|
||||||
orderExecutionQueue.submitSignal(
|
orderExecutionQueue.submitSignal(
|
||||||
s.getUserId(), market,
|
s.getUserId(), market,
|
||||||
s.getStrategyId(), closeSignal,
|
s.getStrategyId(), closeSignal,
|
||||||
signalBar.getClosePrice().doubleValue());
|
signalBar.getClosePrice().doubleValue(),
|
||||||
|
ct, strategyName, signalExecType);
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("[BarCloseEval] 시그널 처리 실패 ({}/{} strategyId={}): {}",
|
log.warn("[BarCloseEval] 시그널 처리 실패 ({}/{} strategyId={}): {}",
|
||||||
|
|||||||
@@ -237,7 +237,7 @@ public class PaperTradingService {
|
|||||||
// 시장가(MARKET)를 포함한 모든 주문을 현재가 기준 지정가 미체결로 생성한다.
|
// 시장가(MARKET)를 포함한 모든 주문을 현재가 기준 지정가 미체결로 생성한다.
|
||||||
// 체결은 PaperOrderMatcherService 가 다음 시세 틱에서 처리한다.
|
// 체결은 PaperOrderMatcherService 가 다음 시세 틱에서 처리한다.
|
||||||
GcPaperOrder order = createPendingLimitOrder(account, app, req.getMarket(), side, orderKind,
|
GcPaperOrder order = createPendingLimitOrder(account, app, req.getMarket(), side, orderKind,
|
||||||
source, req.getStrategyId(), price, qty, false);
|
source, req.getStrategyId(), price, qty, false, null, null, null);
|
||||||
return PaperPlaceOrderResult.builder().order(toOrderDto(order)).build();
|
return PaperPlaceOrderResult.builder().order(toOrderDto(order)).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -261,6 +261,12 @@ public class PaperTradingService {
|
|||||||
|
|
||||||
public void tryExecuteOnSignal(Long userId, String market,
|
public void tryExecuteOnSignal(Long userId, String market,
|
||||||
Long strategyId, String signalType, double price) {
|
Long strategyId, String signalType, double price) {
|
||||||
|
tryExecuteOnSignal(userId, market, strategyId, signalType, price, null, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void tryExecuteOnSignal(Long userId, String market,
|
||||||
|
Long strategyId, String signalType, double price,
|
||||||
|
String candleType, String strategyName, String executionType) {
|
||||||
long uid;
|
long uid;
|
||||||
try {
|
try {
|
||||||
uid = TradingAccess.requireUserId(userId);
|
uid = TradingAccess.requireUserId(userId);
|
||||||
@@ -332,7 +338,8 @@ public class PaperTradingService {
|
|||||||
if (qty * execPrice < minOrder) return;
|
if (qty * execPrice < minOrder) return;
|
||||||
// 알림 발생 시점 현재가 기준 미체결 주문 생성 (staged=true → 체결 시 단계 진행)
|
// 알림 발생 시점 현재가 기준 미체결 주문 생성 (staged=true → 체결 시 단계 진행)
|
||||||
createPendingLimitOrder(account, app, market, "BUY", "market", "STRATEGY",
|
createPendingLimitOrder(account, app, market, "BUY", "market", "STRATEGY",
|
||||||
strategyId, price, qty, true);
|
strategyId, price, qty, true,
|
||||||
|
candleType, strategyName, executionType);
|
||||||
log.info("[Paper] STRATEGY BUY (staged) 미체결 생성 {} qty≈{} @ {}", market, qty, price);
|
log.info("[Paper] STRATEGY BUY (staged) 미체결 생성 {} qty≈{} @ {}", market, qty, price);
|
||||||
} else {
|
} else {
|
||||||
double budgetPct = pct(app.getPaperAutoTradeBudgetPct(), 95);
|
double budgetPct = pct(app.getPaperAutoTradeBudgetPct(), 95);
|
||||||
@@ -347,7 +354,8 @@ public class PaperTradingService {
|
|||||||
if (qty * execPrice < minOrder) return;
|
if (qty * execPrice < minOrder) return;
|
||||||
// 알림 발생 시점 현재가 기준 미체결 주문 생성
|
// 알림 발생 시점 현재가 기준 미체결 주문 생성
|
||||||
createPendingLimitOrder(account, app, market, "BUY", "market", "STRATEGY",
|
createPendingLimitOrder(account, app, market, "BUY", "market", "STRATEGY",
|
||||||
strategyId, price, qty, false);
|
strategyId, price, qty, false,
|
||||||
|
candleType, strategyName, executionType);
|
||||||
log.info("[Paper] STRATEGY BUY 미체결 생성 {} qty≈{} @ {}", market, qty, price);
|
log.info("[Paper] STRATEGY BUY 미체결 생성 {} qty≈{} @ {}", market, qty, price);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -364,12 +372,14 @@ public class PaperTradingService {
|
|||||||
if (qty * execPrice < minOrder) return;
|
if (qty * execPrice < minOrder) return;
|
||||||
// 알림 발생 시점 현재가 기준 미체결 주문 생성 (staged=true → 체결 시 단계 진행)
|
// 알림 발생 시점 현재가 기준 미체결 주문 생성 (staged=true → 체결 시 단계 진행)
|
||||||
createPendingLimitOrder(account, app, market, "SELL", "market", "STRATEGY",
|
createPendingLimitOrder(account, app, market, "SELL", "market", "STRATEGY",
|
||||||
strategyId, price, qty, true);
|
strategyId, price, qty, true,
|
||||||
|
candleType, strategyName, executionType);
|
||||||
log.info("[Paper] STRATEGY SELL (staged) 미체결 생성 {} qty={} @ {}", market, qty, price);
|
log.info("[Paper] STRATEGY SELL (staged) 미체결 생성 {} qty={} @ {}", market, qty, price);
|
||||||
} else {
|
} else {
|
||||||
// 알림 발생 시점 현재가 기준 미체결 주문 생성
|
// 알림 발생 시점 현재가 기준 미체결 주문 생성
|
||||||
createPendingLimitOrder(account, app, market, "SELL", "market", "STRATEGY",
|
createPendingLimitOrder(account, app, market, "SELL", "market", "STRATEGY",
|
||||||
strategyId, price, avail, false);
|
strategyId, price, avail, false,
|
||||||
|
candleType, strategyName, executionType);
|
||||||
log.info("[Paper] STRATEGY SELL 미체결 생성 {} qty={} @ {}", market, avail, price);
|
log.info("[Paper] STRATEGY SELL 미체결 생성 {} qty={} @ {}", market, avail, price);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -440,7 +450,8 @@ public class PaperTradingService {
|
|||||||
double qty = order.getQuantity().doubleValue();
|
double qty = order.getQuantity().doubleValue();
|
||||||
executeTrade(uid, app, order.getSymbol(),
|
executeTrade(uid, app, order.getSymbol(),
|
||||||
order.getSide(), order.getOrderKind(), order.getSource(), order.getStrategyId(),
|
order.getSide(), order.getOrderKind(), order.getSource(), order.getStrategyId(),
|
||||||
limit, qty, null, "STRATEGY".equals(order.getSource()));
|
limit, qty, null, "STRATEGY".equals(order.getSource()),
|
||||||
|
order.getCandleType(), order.getStrategyName(), order.getExecutionType());
|
||||||
|
|
||||||
order.setStatus("FILLED");
|
order.setStatus("FILLED");
|
||||||
order.setFilledQuantity(order.getQuantity());
|
order.setFilledQuantity(order.getQuantity());
|
||||||
@@ -532,7 +543,9 @@ public class PaperTradingService {
|
|||||||
private GcPaperOrder createPendingLimitOrder(GcPaperAccount account, GcAppSettings app,
|
private GcPaperOrder createPendingLimitOrder(GcPaperAccount account, GcAppSettings app,
|
||||||
String market, String side, String orderKind,
|
String market, String side, String orderKind,
|
||||||
String source, Long strategyId,
|
String source, Long strategyId,
|
||||||
double limitPrice, double qty, boolean staged) {
|
double limitPrice, double qty, boolean staged,
|
||||||
|
String candleType, String strategyName,
|
||||||
|
String executionType) {
|
||||||
double feeRate = pct(app.getPaperFeeRatePct(), 0.05);
|
double feeRate = pct(app.getPaperFeeRatePct(), 0.05);
|
||||||
double slip = pct(app.getPaperSlippagePct(), 0);
|
double slip = pct(app.getPaperSlippagePct(), 0);
|
||||||
double execPrice = "BUY".equals(side)
|
double execPrice = "BUY".equals(side)
|
||||||
@@ -559,6 +572,9 @@ public class PaperTradingService {
|
|||||||
.orderKind(orderKind)
|
.orderKind(orderKind)
|
||||||
.source(source)
|
.source(source)
|
||||||
.strategyId(strategyId)
|
.strategyId(strategyId)
|
||||||
|
.candleType(candleType)
|
||||||
|
.strategyName(strategyName)
|
||||||
|
.executionType(executionType)
|
||||||
.staged(staged)
|
.staged(staged)
|
||||||
.build());
|
.build());
|
||||||
}
|
}
|
||||||
@@ -581,6 +597,9 @@ public class PaperTradingService {
|
|||||||
.orderKind(orderKind)
|
.orderKind(orderKind)
|
||||||
.source(source)
|
.source(source)
|
||||||
.strategyId(strategyId)
|
.strategyId(strategyId)
|
||||||
|
.candleType(candleType)
|
||||||
|
.strategyName(strategyName)
|
||||||
|
.executionType(executionType)
|
||||||
.staged(staged)
|
.staged(staged)
|
||||||
.build());
|
.build());
|
||||||
}
|
}
|
||||||
@@ -611,6 +630,15 @@ public class PaperTradingService {
|
|||||||
String market, String side, String orderKind, String source,
|
String market, String side, String orderKind, String source,
|
||||||
Long strategyId, double inputPrice, double inputQty,
|
Long strategyId, double inputPrice, double inputQty,
|
||||||
String koreanName, boolean autoTrade) {
|
String koreanName, boolean autoTrade) {
|
||||||
|
return executeTrade(userId, app, market, side, orderKind, source, strategyId,
|
||||||
|
inputPrice, inputQty, koreanName, autoTrade, null, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private PaperTradeDto executeTrade(long userId, GcAppSettings app,
|
||||||
|
String market, String side, String orderKind, String source,
|
||||||
|
Long strategyId, double inputPrice, double inputQty,
|
||||||
|
String koreanName, boolean autoTrade,
|
||||||
|
String candleType, String strategyName, String executionType) {
|
||||||
GcPaperAccount account = getOrCreateAccount(userId, app);
|
GcPaperAccount account = getOrCreateAccount(userId, app);
|
||||||
double feeRate = pct(app.getPaperFeeRatePct(), 0.05);
|
double feeRate = pct(app.getPaperFeeRatePct(), 0.05);
|
||||||
double slip = pct(app.getPaperSlippagePct(), 0);
|
double slip = pct(app.getPaperSlippagePct(), 0);
|
||||||
@@ -646,6 +674,9 @@ public class PaperTradingService {
|
|||||||
.orderKind(orderKind)
|
.orderKind(orderKind)
|
||||||
.source(source)
|
.source(source)
|
||||||
.strategyId(strategyId)
|
.strategyId(strategyId)
|
||||||
|
.candleType(candleType)
|
||||||
|
.strategyName(strategyName)
|
||||||
|
.executionType(executionType)
|
||||||
.price(price)
|
.price(price)
|
||||||
.quantity(qty)
|
.quantity(qty)
|
||||||
.grossAmount(gross)
|
.grossAmount(gross)
|
||||||
@@ -690,6 +721,9 @@ public class PaperTradingService {
|
|||||||
.orderKind(orderKind)
|
.orderKind(orderKind)
|
||||||
.source(source)
|
.source(source)
|
||||||
.strategyId(strategyId)
|
.strategyId(strategyId)
|
||||||
|
.candleType(candleType)
|
||||||
|
.strategyName(strategyName)
|
||||||
|
.executionType(executionType)
|
||||||
.price(price)
|
.price(price)
|
||||||
.quantity(qty)
|
.quantity(qty)
|
||||||
.grossAmount(gross)
|
.grossAmount(gross)
|
||||||
@@ -791,6 +825,9 @@ public class PaperTradingService {
|
|||||||
.orderKind(t.getOrderKind())
|
.orderKind(t.getOrderKind())
|
||||||
.source(t.getSource())
|
.source(t.getSource())
|
||||||
.strategyId(t.getStrategyId())
|
.strategyId(t.getStrategyId())
|
||||||
|
.candleType(t.getCandleType())
|
||||||
|
.strategyName(t.getStrategyName())
|
||||||
|
.executionType(t.getExecutionType())
|
||||||
.price(t.getPrice().doubleValue())
|
.price(t.getPrice().doubleValue())
|
||||||
.quantity(t.getQuantity().doubleValue())
|
.quantity(t.getQuantity().doubleValue())
|
||||||
.grossAmount(t.getGrossAmount().doubleValue())
|
.grossAmount(t.getGrossAmount().doubleValue())
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ public class TradeSignalService {
|
|||||||
return deviceId.equals(entity.getDeviceId());
|
return deviceId.equals(entity.getDeviceId());
|
||||||
}
|
}
|
||||||
|
|
||||||
private String resolveStrategyName(Long strategyId, String strategyName) {
|
public String resolveStrategyName(Long strategyId, String strategyName) {
|
||||||
if (strategyName != null && !strategyName.isBlank()) return strategyName;
|
if (strategyName != null && !strategyName.isBlank()) return strategyName;
|
||||||
if (strategyId == null) return null;
|
if (strategyId == null) return null;
|
||||||
return strategyRepo.findById(strategyId)
|
return strategyRepo.findById(strategyId)
|
||||||
|
|||||||
@@ -24,12 +24,20 @@ public class TradingExecutionService {
|
|||||||
|
|
||||||
public void executeSignal(Long userId, String market,
|
public void executeSignal(Long userId, String market,
|
||||||
Long strategyId, String side, double price) {
|
Long strategyId, String side, double price) {
|
||||||
|
executeSignal(userId, market, strategyId, side, price, null, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void executeSignal(Long userId, String market,
|
||||||
|
Long strategyId, String side, double price,
|
||||||
|
String candleType, String strategyName, String executionType) {
|
||||||
if (!TradingAccess.isRegisteredUser(userId)) return;
|
if (!TradingAccess.isRegisteredUser(userId)) return;
|
||||||
GcAppSettings app = appSettingsService.getEntity(userId, null);
|
GcAppSettings app = appSettingsService.getEntity(userId, null);
|
||||||
TradingMode mode = TradingMode.fromString(app.getTradingMode());
|
TradingMode mode = TradingMode.fromString(app.getTradingMode());
|
||||||
|
|
||||||
if (mode.usePaper() && Boolean.TRUE.equals(app.getPaperTradingEnabled())) {
|
if (mode.usePaper() && Boolean.TRUE.equals(app.getPaperTradingEnabled())) {
|
||||||
paperTradingService.tryExecuteOnSignal(userId, market, strategyId, side, price);
|
paperTradingService.tryExecuteOnSignal(
|
||||||
|
userId, market, strategyId, side, price,
|
||||||
|
candleType, strategyName, executionType);
|
||||||
}
|
}
|
||||||
if (mode.useLive()) {
|
if (mode.useLive()) {
|
||||||
liveTradingService.tryExecuteOnSignal(
|
liveTradingService.tryExecuteOnSignal(
|
||||||
|
|||||||
@@ -67,9 +67,16 @@ public class OrderExecutionQueue {
|
|||||||
|
|
||||||
public void submitSignal(Long userId, String market,
|
public void submitSignal(Long userId, String market,
|
||||||
Long strategyId, String signal, double price) {
|
Long strategyId, String signal, double price) {
|
||||||
|
submitSignal(userId, market, strategyId, signal, price, null, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void submitSignal(Long userId, String market,
|
||||||
|
Long strategyId, String signal, double price,
|
||||||
|
String candleType, String strategyName, String executionType) {
|
||||||
if (!TradingAccess.isRegisteredUser(userId)) return;
|
if (!TradingAccess.isRegisteredUser(userId)) return;
|
||||||
submit(OrderRequest.strategySignal(
|
submit(OrderRequest.strategySignal(
|
||||||
TradingAccess.accountDeviceKey(userId), userId, market, strategyId, signal, price));
|
TradingAccess.accountDeviceKey(userId), userId, market, strategyId, signal, price,
|
||||||
|
candleType, strategyName, executionType));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void runLoop() {
|
private void runLoop() {
|
||||||
@@ -97,7 +104,8 @@ public class OrderExecutionQueue {
|
|||||||
if (req.kind() == OrderRequest.OrderKind.STRATEGY_SIGNAL) {
|
if (req.kind() == OrderRequest.OrderKind.STRATEGY_SIGNAL) {
|
||||||
tradingExecutionService.executeSignal(
|
tradingExecutionService.executeSignal(
|
||||||
req.userId(), req.market(),
|
req.userId(), req.market(),
|
||||||
req.strategyId(), req.side(), req.price());
|
req.strategyId(), req.side(), req.price(),
|
||||||
|
req.candleType(), req.strategyName(), req.executionType());
|
||||||
} else {
|
} else {
|
||||||
tradingExecutionService.executeRiskExit(req);
|
tradingExecutionService.executeRiskExit(req);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,10 @@ public record OrderRequest(
|
|||||||
Long strategyId,
|
Long strategyId,
|
||||||
String side,
|
String side,
|
||||||
double price,
|
double price,
|
||||||
String reason
|
String reason,
|
||||||
|
String candleType,
|
||||||
|
String strategyName,
|
||||||
|
String executionType
|
||||||
) {
|
) {
|
||||||
public enum OrderKind {
|
public enum OrderKind {
|
||||||
STRATEGY_SIGNAL,
|
STRATEGY_SIGNAL,
|
||||||
@@ -20,20 +23,25 @@ public record OrderRequest(
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static OrderRequest strategySignal(String deviceId, Long userId, String market,
|
public static OrderRequest strategySignal(String deviceId, Long userId, String market,
|
||||||
Long strategyId, String side, double price) {
|
Long strategyId, String side, double price,
|
||||||
|
String candleType, String strategyName,
|
||||||
|
String executionType) {
|
||||||
return new OrderRequest(OrderKind.STRATEGY_SIGNAL, deviceId, userId, market,
|
return new OrderRequest(OrderKind.STRATEGY_SIGNAL, deviceId, userId, market,
|
||||||
strategyId, side, price, "STRATEGY");
|
strategyId, side, price, "STRATEGY",
|
||||||
|
candleType, strategyName, executionType);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static OrderRequest stopLoss(String deviceId, Long userId, String market,
|
public static OrderRequest stopLoss(String deviceId, Long userId, String market,
|
||||||
double price, double lossPct) {
|
double price, double lossPct) {
|
||||||
return new OrderRequest(OrderKind.STOP_LOSS, deviceId, userId, market,
|
return new OrderRequest(OrderKind.STOP_LOSS, deviceId, userId, market,
|
||||||
null, "SELL", price, "STOP_LOSS_" + lossPct + "%");
|
null, "SELL", price, "STOP_LOSS_" + lossPct + "%",
|
||||||
|
null, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static OrderRequest takeProfit(String deviceId, Long userId, String market,
|
public static OrderRequest takeProfit(String deviceId, Long userId, String market,
|
||||||
double price, double profitPct) {
|
double price, double profitPct) {
|
||||||
return new OrderRequest(OrderKind.TAKE_PROFIT, deviceId, userId, market,
|
return new OrderRequest(OrderKind.TAKE_PROFIT, deviceId, userId, market,
|
||||||
null, "SELL", price, "TAKE_PROFIT_" + profitPct + "%");
|
null, "SELL", price, "TAKE_PROFIT_" + profitPct + "%",
|
||||||
|
null, null, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -122,10 +122,13 @@ public class LiveStrategyScheduler {
|
|||||||
barStartEpoch, candleType, "REALTIME_TICK"
|
barStartEpoch, candleType, "REALTIME_TICK"
|
||||||
);
|
);
|
||||||
if (s.getUserId() != null) {
|
if (s.getUserId() != null) {
|
||||||
|
String strategyName = tradeSignalService.resolveStrategyName(
|
||||||
|
s.getStrategyId(), null);
|
||||||
orderExecutionQueue.submitSignal(
|
orderExecutionQueue.submitSignal(
|
||||||
s.getUserId(), market,
|
s.getUserId(), market,
|
||||||
s.getStrategyId(), signal,
|
s.getStrategyId(), signal,
|
||||||
lastBar.getClosePrice().doubleValue());
|
lastBar.getClosePrice().doubleValue(),
|
||||||
|
candleType, strategyName, "REALTIME_TICK");
|
||||||
}
|
}
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
log.warn("[LiveStrategyScheduler] 시그널 DB 저장 실패: {}", ex.getMessage());
|
log.warn("[LiveStrategyScheduler] 시그널 DB 저장 실패: {}", ex.getMessage());
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
-- 매매 실행 당시 전략·시간봉 스냅샷 (분석레포트 실시간 매매 탭)
|
||||||
|
ALTER TABLE gc_paper_trade
|
||||||
|
ADD COLUMN candle_type VARCHAR(10) NULL COMMENT '체결 시점 평가 시간봉' AFTER strategy_id,
|
||||||
|
ADD COLUMN strategy_name VARCHAR(200) NULL COMMENT '체결 시점 전략명' AFTER candle_type,
|
||||||
|
ADD COLUMN execution_type VARCHAR(30) NULL COMMENT 'CANDLE_CLOSE | REALTIME_TICK' AFTER strategy_name;
|
||||||
|
|
||||||
|
ALTER TABLE gc_paper_order
|
||||||
|
ADD COLUMN candle_type VARCHAR(10) NULL AFTER strategy_id,
|
||||||
|
ADD COLUMN strategy_name VARCHAR(200) NULL AFTER candle_type,
|
||||||
|
ADD COLUMN execution_type VARCHAR(30) NULL AFTER strategy_name;
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
-- 백테스트 실행 당시 전략 DSL·지표 파라미터 스냅샷
|
||||||
|
ALTER TABLE gc_backtest_result
|
||||||
|
ADD COLUMN execution_snapshot_json JSON NULL COMMENT '실행 당시 symbol/timeframe/DSL/indicatorParams 스냅샷' AFTER settings_json;
|
||||||
@@ -251,14 +251,10 @@ export function BacktestHistoryPage({ theme = 'dark' }: Props) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (tab === 'live' && selectedLive) {
|
if (tab === 'live' && selectedLive) {
|
||||||
return buildLiveReportModel(
|
return buildLiveReportModel(selectedLive, activeSignals);
|
||||||
selectedLive,
|
|
||||||
activeSignals,
|
|
||||||
chartProps?.timeframe ?? '1h',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}, [tab, selectedBacktest, backtestDetail, selectedLive, activeSignals, chartProps?.timeframe, strategyNamesById]);
|
}, [tab, selectedBacktest, backtestDetail, selectedLive, activeSignals, strategyNamesById]);
|
||||||
|
|
||||||
const showRightDetail = tab === 'backtest'
|
const showRightDetail = tab === 'backtest'
|
||||||
? backtestDetail?.analysis != null
|
? backtestDetail?.analysis != null
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ export function AnalysisReportPage({ theme = 'dark' }: Props) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (sourceTab === 'live' && selectedLive) {
|
if (sourceTab === 'live' && selectedLive) {
|
||||||
return buildLiveReportModel(selectedLive, ctx?.signals ?? [], '1h');
|
return buildLiveReportModel(selectedLive, ctx?.signals ?? []);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}, [sourceTab, selectedBacktest, selectedLive, ctx, strategyNamesById]);
|
}, [sourceTab, selectedBacktest, selectedLive, ctx, strategyNamesById]);
|
||||||
|
|||||||
@@ -110,10 +110,12 @@ export default function BacktestExecutionList({
|
|||||||
const code = item.symbol.replace(/^KRW-/i, '').toLowerCase();
|
const code = item.symbol.replace(/^KRW-/i, '').toLowerCase();
|
||||||
const strategy = item.strategyLabel.toLowerCase();
|
const strategy = item.strategyLabel.toLowerCase();
|
||||||
const source = item.sourceLabel.toLowerCase();
|
const source = item.sourceLabel.toLowerCase();
|
||||||
|
const tf = item.timeframe?.toLowerCase() ?? '';
|
||||||
return ko.includes(normalizedQuery)
|
return ko.includes(normalizedQuery)
|
||||||
|| code.includes(normalizedQuery)
|
|| code.includes(normalizedQuery)
|
||||||
|| strategy.includes(normalizedQuery)
|
|| strategy.includes(normalizedQuery)
|
||||||
|| source.includes(normalizedQuery);
|
|| source.includes(normalizedQuery)
|
||||||
|
|| tf.includes(normalizedQuery);
|
||||||
});
|
});
|
||||||
}, [liveItems, normalizedQuery]);
|
}, [liveItems, normalizedQuery]);
|
||||||
|
|
||||||
@@ -133,7 +135,7 @@ export default function BacktestExecutionList({
|
|||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className="btd-exec-search-input"
|
className="btd-exec-search-input"
|
||||||
placeholder={tab === 'backtest' ? '종목·전략·타임프레임 검색…' : '종목·전략 검색…'}
|
placeholder={tab === 'backtest' ? '종목·전략·타임프레임 검색…' : '종목·전략·타임프레임 검색…'}
|
||||||
value={query}
|
value={query}
|
||||||
onChange={e => setQuery(e.target.value)}
|
onChange={e => setQuery(e.target.value)}
|
||||||
aria-label="목록 검색"
|
aria-label="목록 검색"
|
||||||
@@ -242,12 +244,16 @@ export default function BacktestExecutionList({
|
|||||||
<div className="btd-history-card-row">
|
<div className="btd-history-card-row">
|
||||||
<div className="btd-history-card-main">
|
<div className="btd-history-card-main">
|
||||||
<span className="btd-history-ko">{ko}</span>
|
<span className="btd-history-ko">{ko}</span>
|
||||||
<span className="btd-history-strategy">{item.strategyLabel} · {item.sourceLabel}</span>
|
<span className="btd-history-strategy">
|
||||||
|
{item.strategyLabel}
|
||||||
|
{item.timeframe !== 'unknown' ? ` · ${formatTimeframeKo(item.timeframe)}` : ''}
|
||||||
|
{item.sourceLabel !== '자동' ? ` · ${item.sourceLabel}` : ''}
|
||||||
|
</span>
|
||||||
<span className="btd-history-date">{fmtShortTimestamp(item.createdAt)}</span>
|
<span className="btd-history-date">{fmtShortTimestamp(item.createdAt)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="btd-history-card-side">
|
<div className="btd-history-card-side">
|
||||||
<BacktestSparkline curve={parseLiveSpark(item)} positive={positive} width={56} height={22} />
|
<BacktestSparkline curve={parseLiveSpark(item)} positive={positive} width={56} height={22} />
|
||||||
<span className="btd-history-win">체결 {item.tradeCount}건</span>
|
<span className="btd-history-win">라운드 {item.roundTripCount} · 체결 {item.tradeCount}건</span>
|
||||||
<span className={`btd-history-roi${positive ? ' up' : ' down'}`}>{pct(item.totalReturnPct)}</span>
|
<span className={`btd-history-roi${positive ? ' up' : ' down'}`}>{pct(item.totalReturnPct)}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import type { LiveExecutionItem } from './liveExecutionGroups';
|
|||||||
import type { EquityPoint, TradeHistoryRow } from './backtestEquity';
|
import type { EquityPoint, TradeHistoryRow } from './backtestEquity';
|
||||||
import { buildEquityFromSignals } from './backtestEquity';
|
import { buildEquityFromSignals } from './backtestEquity';
|
||||||
import { paperTradesToSignals } from './liveExecutionGroups';
|
import { paperTradesToSignals } from './liveExecutionGroups';
|
||||||
|
import { buildAnalysisFromPaperTrades } from './paperMetrics';
|
||||||
import { repairUtf8Mojibake } from './textEncoding';
|
import { repairUtf8Mojibake } from './textEncoding';
|
||||||
|
|
||||||
export type AnalysisSourceTab = 'backtest' | 'live';
|
export type AnalysisSourceTab = 'backtest' | 'live';
|
||||||
@@ -72,37 +73,14 @@ export function buildContextFromLive(item: LiveExecutionItem): AnalysisReportCon
|
|||||||
const signals = paperTradesToSignals(item.trades);
|
const signals = paperTradesToSignals(item.trades);
|
||||||
const cap = 10_000_000;
|
const cap = 10_000_000;
|
||||||
const eq = buildEquityFromSignals(signals, cap, item.symbol);
|
const eq = buildEquityFromSignals(signals, cap, item.symbol);
|
||||||
const finalEquity = cap * (1 + item.totalReturnPct);
|
const analysis = buildAnalysisFromPaperTrades(item.trades, cap);
|
||||||
const analysis: BacktestAnalysis = {
|
const timeframe = item.timeframe !== 'unknown' ? item.timeframe : '—';
|
||||||
initialCapital: cap,
|
|
||||||
finalEquity,
|
|
||||||
totalReturnPct: item.totalReturnPct,
|
|
||||||
totalProfitLoss: finalEquity - cap,
|
|
||||||
grossProfit: 0,
|
|
||||||
grossLoss: 0,
|
|
||||||
avgReturnPct: item.tradeCount > 0 ? item.totalReturnPct / item.tradeCount : 0,
|
|
||||||
profitLossRatio: item.profitFactor,
|
|
||||||
numberOfPositions: item.tradeCount,
|
|
||||||
numberOfWinning: Math.round(item.tradeCount * item.winRatePct),
|
|
||||||
numberOfLosing: Math.max(0, item.tradeCount - Math.round(item.tradeCount * item.winRatePct)),
|
|
||||||
numberOfBreakEven: 0,
|
|
||||||
winRate: item.winRatePct,
|
|
||||||
maxDrawdownPct: item.mddPct,
|
|
||||||
maxRunupPct: 0,
|
|
||||||
sharpeRatio: item.sharpeRatio,
|
|
||||||
sortinoRatio: 0,
|
|
||||||
calmarRatio: 0,
|
|
||||||
valueAtRisk95: 0,
|
|
||||||
expectedShortfall: 0,
|
|
||||||
buyAndHoldReturnPct: 0,
|
|
||||||
vsBuyAndHold: 0,
|
|
||||||
};
|
|
||||||
return {
|
return {
|
||||||
sourceTab: 'live',
|
sourceTab: 'live',
|
||||||
strategyName: repairUtf8Mojibake(item.strategyLabel),
|
strategyName: repairUtf8Mojibake(item.strategyLabel),
|
||||||
symbol: item.symbol,
|
symbol: item.symbol,
|
||||||
timeframe: '1h',
|
timeframe,
|
||||||
barCount: 300,
|
barCount: item.trades.length,
|
||||||
createdAt: item.createdAt,
|
createdAt: item.createdAt,
|
||||||
analysis,
|
analysis,
|
||||||
signals,
|
signals,
|
||||||
|
|||||||
@@ -702,6 +702,9 @@ export interface PaperTradeDto {
|
|||||||
orderKind: string;
|
orderKind: string;
|
||||||
source: string;
|
source: string;
|
||||||
strategyId?: number | null;
|
strategyId?: number | null;
|
||||||
|
candleType?: string | null;
|
||||||
|
strategyName?: string | null;
|
||||||
|
executionType?: string | null;
|
||||||
price: number;
|
price: number;
|
||||||
quantity: number;
|
quantity: number;
|
||||||
grossAmount: number;
|
grossAmount: number;
|
||||||
@@ -1208,6 +1211,7 @@ export interface BacktestResultRecord {
|
|||||||
fromTime: number;
|
fromTime: number;
|
||||||
toTime: number;
|
toTime: number;
|
||||||
settingsJson: string;
|
settingsJson: string;
|
||||||
|
executionSnapshotJson?: string;
|
||||||
signalsJson: string;
|
signalsJson: string;
|
||||||
analysisJson: string;
|
analysisJson: string;
|
||||||
totalReturn: number;
|
totalReturn: number;
|
||||||
|
|||||||
@@ -6,41 +6,17 @@ import type {
|
|||||||
} from './backendApi';
|
} from './backendApi';
|
||||||
import type { BacktestAnalysisReportModel } from '../components/backtest/BacktestAnalysisReportModal';
|
import type { BacktestAnalysisReportModel } from '../components/backtest/BacktestAnalysisReportModal';
|
||||||
import type { LiveExecutionItem } from './liveExecutionGroups';
|
import type { LiveExecutionItem } from './liveExecutionGroups';
|
||||||
|
import { buildAnalysisFromPaperTrades } from './paperMetrics';
|
||||||
import { fmtListTimestamp, toUpbitMarket } from './backtestUiUtils';
|
import { fmtListTimestamp, toUpbitMarket } from './backtestUiUtils';
|
||||||
import { getKoreanName } from './marketNameCache';
|
import { getKoreanName } from './marketNameCache';
|
||||||
import { repairUtf8Mojibake } from './textEncoding';
|
import { repairUtf8Mojibake } from './textEncoding';
|
||||||
|
|
||||||
/** 실시간 매매 탭 — KPI만 있는 경우 최소 분석 객체 */
|
/** 실시간 매매 탭 — 체결 이력 기반 분석 */
|
||||||
export function buildAnalysisFromLive(
|
export function buildAnalysisFromLive(
|
||||||
item: LiveExecutionItem,
|
item: LiveExecutionItem,
|
||||||
initialCapital = 10_000_000,
|
initialCapital = 10_000_000,
|
||||||
): BacktestAnalysis {
|
): BacktestAnalysis {
|
||||||
const finalEquity = initialCapital * (1 + item.totalReturnPct);
|
return buildAnalysisFromPaperTrades(item.trades, initialCapital);
|
||||||
const winCount = Math.round(item.tradeCount * item.winRatePct);
|
|
||||||
return {
|
|
||||||
initialCapital,
|
|
||||||
finalEquity,
|
|
||||||
totalReturnPct: item.totalReturnPct,
|
|
||||||
totalProfitLoss: finalEquity - initialCapital,
|
|
||||||
grossProfit: 0,
|
|
||||||
grossLoss: 0,
|
|
||||||
avgReturnPct: item.tradeCount > 0 ? item.totalReturnPct / item.tradeCount : 0,
|
|
||||||
profitLossRatio: item.profitFactor,
|
|
||||||
numberOfPositions: item.tradeCount,
|
|
||||||
numberOfWinning: winCount,
|
|
||||||
numberOfLosing: Math.max(0, item.tradeCount - winCount),
|
|
||||||
numberOfBreakEven: 0,
|
|
||||||
winRate: item.winRatePct,
|
|
||||||
maxDrawdownPct: item.mddPct,
|
|
||||||
maxRunupPct: 0,
|
|
||||||
sharpeRatio: item.sharpeRatio,
|
|
||||||
sortinoRatio: 0,
|
|
||||||
calmarRatio: 0,
|
|
||||||
valueAtRisk95: 0,
|
|
||||||
expectedShortfall: 0,
|
|
||||||
buyAndHoldReturnPct: 0,
|
|
||||||
vsBuyAndHold: 0,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildAnalysisFromStats(s: BacktestStats): BacktestAnalysis {
|
function buildAnalysisFromStats(s: BacktestStats): BacktestAnalysis {
|
||||||
@@ -101,14 +77,11 @@ export function buildBacktestReportModel(
|
|||||||
record: BacktestResultRecord,
|
record: BacktestResultRecord,
|
||||||
analysis: BacktestAnalysis,
|
analysis: BacktestAnalysis,
|
||||||
signals: BacktestSignal[],
|
signals: BacktestSignal[],
|
||||||
strategyNamesById: Record<number, string> = {},
|
_strategyNamesById: Record<number, string> = {},
|
||||||
): BacktestAnalysisReportModel {
|
): BacktestAnalysisReportModel {
|
||||||
const market = toUpbitMarket(record.symbol);
|
const market = toUpbitMarket(record.symbol);
|
||||||
const freshName = record.strategyId != null
|
|
||||||
? strategyNamesById[record.strategyId]
|
|
||||||
: undefined;
|
|
||||||
const strategyName = repairUtf8Mojibake(
|
const strategyName = repairUtf8Mojibake(
|
||||||
freshName || record.strategyName || '전략 없음',
|
record.strategyName || '전략 없음',
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
analysis,
|
analysis,
|
||||||
@@ -126,10 +99,10 @@ export function buildBacktestReportModel(
|
|||||||
export function buildLiveReportModel(
|
export function buildLiveReportModel(
|
||||||
item: LiveExecutionItem,
|
item: LiveExecutionItem,
|
||||||
signals: BacktestSignal[],
|
signals: BacktestSignal[],
|
||||||
timeframe: string,
|
|
||||||
initialCapital = 10_000_000,
|
initialCapital = 10_000_000,
|
||||||
): BacktestAnalysisReportModel {
|
): BacktestAnalysisReportModel {
|
||||||
const market = toUpbitMarket(item.symbol);
|
const market = toUpbitMarket(item.symbol);
|
||||||
|
const timeframe = item.timeframe !== 'unknown' ? item.timeframe : '—';
|
||||||
return {
|
return {
|
||||||
analysis: buildAnalysisFromLive(item, initialCapital),
|
analysis: buildAnalysisFromLive(item, initialCapital),
|
||||||
signals,
|
signals,
|
||||||
@@ -137,7 +110,7 @@ export function buildLiveReportModel(
|
|||||||
symbol: item.symbol,
|
symbol: item.symbol,
|
||||||
symbolKo: getKoreanName(market),
|
symbolKo: getKoreanName(market),
|
||||||
timeframe,
|
timeframe,
|
||||||
barCount: 300,
|
barCount: item.trades.length,
|
||||||
createdAt: item.createdAt ? fmtListTimestamp(item.createdAt) : undefined,
|
createdAt: item.createdAt ? fmtListTimestamp(item.createdAt) : undefined,
|
||||||
reportKind: 'live',
|
reportKind: 'live',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,16 +1,21 @@
|
|||||||
import type { PaperSummaryDto, PaperTradeDto } from './backendApi';
|
import type { PaperSummaryDto, PaperTradeDto } from './backendApi';
|
||||||
import { computePaperMetrics } from './paperMetrics';
|
import { buildAnalysisFromPaperTrades, computePaperMetrics } from './paperMetrics';
|
||||||
|
|
||||||
export interface LiveExecutionItem {
|
export interface LiveExecutionItem {
|
||||||
id: string;
|
id: string;
|
||||||
symbol: string;
|
symbol: string;
|
||||||
strategyLabel: string;
|
strategyLabel: string;
|
||||||
sourceLabel: string;
|
sourceLabel: string;
|
||||||
|
/** 체결 시점 평가 시간봉 */
|
||||||
|
timeframe: string;
|
||||||
|
executionType?: string;
|
||||||
|
strategyId?: number | null;
|
||||||
trades: PaperTradeDto[];
|
trades: PaperTradeDto[];
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
totalReturnPct: number;
|
totalReturnPct: number;
|
||||||
winRatePct: number;
|
winRatePct: number;
|
||||||
tradeCount: number;
|
tradeCount: number;
|
||||||
|
roundTripCount: number;
|
||||||
mddPct: number;
|
mddPct: number;
|
||||||
sharpeRatio: number;
|
sharpeRatio: number;
|
||||||
profitFactor: number;
|
profitFactor: number;
|
||||||
@@ -26,6 +31,27 @@ function sourceLabel(source: string): string {
|
|||||||
return '수동';
|
return '수동';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveStrategyLabel(
|
||||||
|
t: PaperTradeDto,
|
||||||
|
strategyNames: Record<number, string>,
|
||||||
|
): string {
|
||||||
|
if (t.strategyName) return t.strategyName;
|
||||||
|
if (t.strategyId != null) {
|
||||||
|
return strategyNames[t.strategyId] ?? `전략 #${t.strategyId}`;
|
||||||
|
}
|
||||||
|
return '수동 매매';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 자동매매: 종목·전략·시간봉·실행방식 기준 / 수동: 종목·일자 기준 */
|
||||||
|
function groupKey(t: PaperTradeDto): string {
|
||||||
|
if (t.source === 'STRATEGY') {
|
||||||
|
const tf = t.candleType ?? 'unknown';
|
||||||
|
const exec = t.executionType ?? '';
|
||||||
|
return `${t.symbol}|${t.strategyId ?? 0}|${tf}|${exec}|STRATEGY`;
|
||||||
|
}
|
||||||
|
return `${t.symbol}|manual|${dayKey(t.createdAt)}|MANUAL`;
|
||||||
|
}
|
||||||
|
|
||||||
/** 가상투자·모의투자 체결 이력을 실행 단위 목록으로 그룹 */
|
/** 가상투자·모의투자 체결 이력을 실행 단위 목록으로 그룹 */
|
||||||
export function buildLiveExecutionItems(
|
export function buildLiveExecutionItems(
|
||||||
trades: PaperTradeDto[],
|
trades: PaperTradeDto[],
|
||||||
@@ -36,7 +62,7 @@ export function buildLiveExecutionItems(
|
|||||||
|
|
||||||
const groups = new Map<string, PaperTradeDto[]>();
|
const groups = new Map<string, PaperTradeDto[]>();
|
||||||
for (const t of trades) {
|
for (const t of trades) {
|
||||||
const key = `${t.symbol}|${t.strategyId ?? 0}|${dayKey(t.createdAt)}|${t.source}`;
|
const key = groupKey(t);
|
||||||
const list = groups.get(key) ?? [];
|
const list = groups.get(key) ?? [];
|
||||||
list.push(t);
|
list.push(t);
|
||||||
groups.set(key, list);
|
groups.set(key, list);
|
||||||
@@ -51,26 +77,25 @@ export function buildLiveExecutionItems(
|
|||||||
);
|
);
|
||||||
const first = sorted[0];
|
const first = sorted[0];
|
||||||
const last = sorted[sorted.length - 1];
|
const last = sorted[sorted.length - 1];
|
||||||
|
const analysis = buildAnalysisFromPaperTrades(sorted, initial);
|
||||||
const metrics = computePaperMetrics(sorted, summary);
|
const metrics = computePaperMetrics(sorted, summary);
|
||||||
const buyVol = sorted.filter(t => t.side === 'BUY').reduce((s, t) => s + t.netAmount, 0);
|
|
||||||
const sellVol = sorted.filter(t => t.side === 'SELL').reduce((s, t) => s + t.netAmount, 0);
|
|
||||||
const pnl = sellVol - buyVol;
|
|
||||||
const totalReturnPct = initial > 0 ? pnl / initial : 0;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: key,
|
id: key,
|
||||||
symbol: first.symbol,
|
symbol: first.symbol,
|
||||||
strategyLabel: first.strategyId != null
|
strategyLabel: resolveStrategyLabel(first, strategyNames),
|
||||||
? (strategyNames[first.strategyId] ?? `전략 #${first.strategyId}`)
|
|
||||||
: '수동 매매',
|
|
||||||
sourceLabel: sourceLabel(first.source),
|
sourceLabel: sourceLabel(first.source),
|
||||||
|
timeframe: first.candleType ?? 'unknown',
|
||||||
|
executionType: first.executionType ?? undefined,
|
||||||
|
strategyId: first.strategyId,
|
||||||
trades: sorted,
|
trades: sorted,
|
||||||
createdAt: last.createdAt ?? first.createdAt ?? new Date().toISOString(),
|
createdAt: last.createdAt ?? first.createdAt ?? new Date().toISOString(),
|
||||||
totalReturnPct,
|
totalReturnPct: analysis.totalReturnPct,
|
||||||
winRatePct: metrics.winRatePct / 100,
|
winRatePct: analysis.winRate,
|
||||||
tradeCount: sorted.length,
|
tradeCount: sorted.length,
|
||||||
mddPct: metrics.mddPct / 100,
|
roundTripCount: analysis.numberOfPositions,
|
||||||
sharpeRatio: metrics.sharpeRatio,
|
mddPct: analysis.maxDrawdownPct,
|
||||||
|
sharpeRatio: analysis.sharpeRatio,
|
||||||
profitFactor: metrics.profitFactor,
|
profitFactor: metrics.profitFactor,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
@@ -84,6 +109,7 @@ export function paperTradesToSignals(trades: PaperTradeDto[]) {
|
|||||||
time: Math.floor(new Date(t.createdAt!).getTime() / 1000),
|
time: Math.floor(new Date(t.createdAt!).getTime() / 1000),
|
||||||
type: t.side as 'BUY' | 'SELL',
|
type: t.side as 'BUY' | 'SELL',
|
||||||
price: t.price,
|
price: t.price,
|
||||||
|
quantity: t.quantity,
|
||||||
barIndex: 0,
|
barIndex: 0,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { PaperSummaryDto, PaperTradeDto } from './backendApi';
|
import type { PaperSummaryDto, PaperTradeDto, BacktestAnalysis } from './backendApi';
|
||||||
|
|
||||||
export interface PaperPerformanceMetrics {
|
export interface PaperPerformanceMetrics {
|
||||||
mddPct: number;
|
mddPct: number;
|
||||||
@@ -126,3 +126,43 @@ export function computePaperMetrics(
|
|||||||
profitFactor: Math.min(profitFactor, 99),
|
profitFactor: Math.min(profitFactor, 99),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 실시간 매매 체결 이력 → 백테스트 분석 DTO (ledger·라운드트립 기준) */
|
||||||
|
export function buildAnalysisFromPaperTrades(
|
||||||
|
trades: PaperTradeDto[],
|
||||||
|
initialCapital = 10_000_000,
|
||||||
|
): BacktestAnalysis {
|
||||||
|
const sorted = [...trades].sort((a, b) =>
|
||||||
|
(a.createdAt ?? '').localeCompare(b.createdAt ?? ''),
|
||||||
|
);
|
||||||
|
const metrics = computePaperMetrics(sorted, { initialCapital } as PaperSummaryDto);
|
||||||
|
const curve = buildEquityCurve(sorted, initialCapital);
|
||||||
|
const finalEquity = curve.length > 0 ? curve[curve.length - 1] : initialCapital;
|
||||||
|
const { wins, total, grossProfit, grossLoss } = roundTripStats(sorted);
|
||||||
|
const profitLossRatio = grossLoss > 0 ? grossProfit / grossLoss : grossProfit > 0 ? 99 : 0;
|
||||||
|
|
||||||
|
return {
|
||||||
|
initialCapital,
|
||||||
|
finalEquity,
|
||||||
|
totalReturnPct: initialCapital > 0 ? (finalEquity - initialCapital) / initialCapital : 0,
|
||||||
|
totalProfitLoss: finalEquity - initialCapital,
|
||||||
|
grossProfit,
|
||||||
|
grossLoss,
|
||||||
|
avgReturnPct: total > 0 ? (finalEquity - initialCapital) / initialCapital / total : 0,
|
||||||
|
profitLossRatio: Math.min(profitLossRatio, 99),
|
||||||
|
numberOfPositions: total,
|
||||||
|
numberOfWinning: wins,
|
||||||
|
numberOfLosing: Math.max(0, total - wins),
|
||||||
|
numberOfBreakEven: 0,
|
||||||
|
winRate: total > 0 ? wins / total : 0,
|
||||||
|
maxDrawdownPct: metrics.mddPct / 100,
|
||||||
|
maxRunupPct: 0,
|
||||||
|
sharpeRatio: metrics.sharpeRatio,
|
||||||
|
sortinoRatio: 0,
|
||||||
|
calmarRatio: 0,
|
||||||
|
valueAtRisk95: 0,
|
||||||
|
expectedShortfall: 0,
|
||||||
|
buyAndHoldReturnPct: 0,
|
||||||
|
vsBuyAndHold: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user