분석레포트 로직 수정
This commit is contained in:
@@ -12,6 +12,9 @@ public class PaperTradeDto {
|
||||
private String orderKind;
|
||||
private String source;
|
||||
private Long strategyId;
|
||||
private String candleType;
|
||||
private String strategyName;
|
||||
private String executionType;
|
||||
private Double price;
|
||||
private Double quantity;
|
||||
private Double grossAmount;
|
||||
|
||||
@@ -28,6 +28,9 @@ public class GcBacktestResult {
|
||||
@Column(name = "settings_json", columnDefinition = "JSON")
|
||||
@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")
|
||||
@JdbcTypeCode(SqlTypes.JSON) private String signalsJson;
|
||||
|
||||
|
||||
@@ -61,6 +61,15 @@ public class GcPaperOrder {
|
||||
@Column(name = "strategy_id")
|
||||
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) 주문 여부.
|
||||
* TRUE 이면 체결(FILLED) 시 allocationService 단계를 자동 진행한다.
|
||||
|
||||
@@ -35,6 +35,18 @@ public class GcPaperTrade {
|
||||
@Column(name = "strategy_id")
|
||||
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)
|
||||
private BigDecimal price;
|
||||
|
||||
|
||||
@@ -96,7 +96,7 @@ public class BacktestingService {
|
||||
: new BooleanRule(false);
|
||||
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,
|
||||
BacktestRequest req, BacktestSettingsDto cfg,
|
||||
String strategyName) {
|
||||
String strategyName,
|
||||
JsonNode execBuyDsl, JsonNode execSellDsl,
|
||||
Map<String, Map<String, Object>> execParams) {
|
||||
|
||||
BaseTradingRecord record = new BaseTradingRecord();
|
||||
|
||||
@@ -295,7 +297,8 @@ public class BacktestingService {
|
||||
Stats stats = toStats(analysis, signals);
|
||||
|
||||
// ── 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()
|
||||
.signals(signals)
|
||||
@@ -601,12 +604,24 @@ public class BacktestingService {
|
||||
|
||||
private Long saveResult(BacktestRequest req, BacktestSettingsDto cfg,
|
||||
List<Signal> signals, BacktestAnalysisDto analysis,
|
||||
BarSeries series, String strategyName) {
|
||||
BarSeries series, String strategyName,
|
||||
JsonNode execBuyDsl, JsonNode execSellDsl,
|
||||
Map<String, Map<String, Object>> execParams) {
|
||||
try {
|
||||
List<OhlcvBar> bars = req.getBars();
|
||||
long fromTime = bars.isEmpty() ? 0 : bars.get(0).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()
|
||||
.deviceId(req.getDeviceId())
|
||||
.strategyId(req.getStrategyId())
|
||||
@@ -617,6 +632,7 @@ public class BacktestingService {
|
||||
.fromTime(fromTime)
|
||||
.toTime(toTime)
|
||||
.settingsJson(objectMapper.writeValueAsString(cfg))
|
||||
.executionSnapshotJson(objectMapper.writeValueAsString(snapshot))
|
||||
.signalsJson(objectMapper.writeValueAsString(signals))
|
||||
.analysisJson(objectMapper.writeValueAsString(analysis))
|
||||
.totalReturn(BigDecimal.valueOf(analysis.getTotalReturnPct()))
|
||||
|
||||
+4
-1
@@ -87,10 +87,13 @@ public class BarCloseStrategyEvaluationService {
|
||||
closeSignal, signalBar.getClosePrice().doubleValue(),
|
||||
candleTimeEpoch, ct, signalExecType);
|
||||
if (s.getUserId() != null) {
|
||||
String strategyName = tradeSignalService.resolveStrategyName(
|
||||
s.getStrategyId(), null);
|
||||
orderExecutionQueue.submitSignal(
|
||||
s.getUserId(), market,
|
||||
s.getStrategyId(), closeSignal,
|
||||
signalBar.getClosePrice().doubleValue());
|
||||
signalBar.getClosePrice().doubleValue(),
|
||||
ct, strategyName, signalExecType);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("[BarCloseEval] 시그널 처리 실패 ({}/{} strategyId={}): {}",
|
||||
|
||||
@@ -237,7 +237,7 @@ public class PaperTradingService {
|
||||
// 시장가(MARKET)를 포함한 모든 주문을 현재가 기준 지정가 미체결로 생성한다.
|
||||
// 체결은 PaperOrderMatcherService 가 다음 시세 틱에서 처리한다.
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -261,6 +261,12 @@ public class PaperTradingService {
|
||||
|
||||
public void tryExecuteOnSignal(Long userId, String market,
|
||||
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;
|
||||
try {
|
||||
uid = TradingAccess.requireUserId(userId);
|
||||
@@ -332,7 +338,8 @@ public class PaperTradingService {
|
||||
if (qty * execPrice < minOrder) return;
|
||||
// 알림 발생 시점 현재가 기준 미체결 주문 생성 (staged=true → 체결 시 단계 진행)
|
||||
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);
|
||||
} else {
|
||||
double budgetPct = pct(app.getPaperAutoTradeBudgetPct(), 95);
|
||||
@@ -347,7 +354,8 @@ public class PaperTradingService {
|
||||
if (qty * execPrice < minOrder) return;
|
||||
// 알림 발생 시점 현재가 기준 미체결 주문 생성
|
||||
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);
|
||||
}
|
||||
} else {
|
||||
@@ -364,12 +372,14 @@ public class PaperTradingService {
|
||||
if (qty * execPrice < minOrder) return;
|
||||
// 알림 발생 시점 현재가 기준 미체결 주문 생성 (staged=true → 체결 시 단계 진행)
|
||||
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);
|
||||
} else {
|
||||
// 알림 발생 시점 현재가 기준 미체결 주문 생성
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -440,7 +450,8 @@ public class PaperTradingService {
|
||||
double qty = order.getQuantity().doubleValue();
|
||||
executeTrade(uid, app, order.getSymbol(),
|
||||
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.setFilledQuantity(order.getQuantity());
|
||||
@@ -532,7 +543,9 @@ public class PaperTradingService {
|
||||
private GcPaperOrder createPendingLimitOrder(GcPaperAccount account, GcAppSettings app,
|
||||
String market, String side, String orderKind,
|
||||
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 slip = pct(app.getPaperSlippagePct(), 0);
|
||||
double execPrice = "BUY".equals(side)
|
||||
@@ -559,6 +572,9 @@ public class PaperTradingService {
|
||||
.orderKind(orderKind)
|
||||
.source(source)
|
||||
.strategyId(strategyId)
|
||||
.candleType(candleType)
|
||||
.strategyName(strategyName)
|
||||
.executionType(executionType)
|
||||
.staged(staged)
|
||||
.build());
|
||||
}
|
||||
@@ -581,6 +597,9 @@ public class PaperTradingService {
|
||||
.orderKind(orderKind)
|
||||
.source(source)
|
||||
.strategyId(strategyId)
|
||||
.candleType(candleType)
|
||||
.strategyName(strategyName)
|
||||
.executionType(executionType)
|
||||
.staged(staged)
|
||||
.build());
|
||||
}
|
||||
@@ -611,6 +630,15 @@ public class PaperTradingService {
|
||||
String market, String side, String orderKind, String source,
|
||||
Long strategyId, double inputPrice, double inputQty,
|
||||
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);
|
||||
double feeRate = pct(app.getPaperFeeRatePct(), 0.05);
|
||||
double slip = pct(app.getPaperSlippagePct(), 0);
|
||||
@@ -646,6 +674,9 @@ public class PaperTradingService {
|
||||
.orderKind(orderKind)
|
||||
.source(source)
|
||||
.strategyId(strategyId)
|
||||
.candleType(candleType)
|
||||
.strategyName(strategyName)
|
||||
.executionType(executionType)
|
||||
.price(price)
|
||||
.quantity(qty)
|
||||
.grossAmount(gross)
|
||||
@@ -690,6 +721,9 @@ public class PaperTradingService {
|
||||
.orderKind(orderKind)
|
||||
.source(source)
|
||||
.strategyId(strategyId)
|
||||
.candleType(candleType)
|
||||
.strategyName(strategyName)
|
||||
.executionType(executionType)
|
||||
.price(price)
|
||||
.quantity(qty)
|
||||
.grossAmount(gross)
|
||||
@@ -791,6 +825,9 @@ public class PaperTradingService {
|
||||
.orderKind(t.getOrderKind())
|
||||
.source(t.getSource())
|
||||
.strategyId(t.getStrategyId())
|
||||
.candleType(t.getCandleType())
|
||||
.strategyName(t.getStrategyName())
|
||||
.executionType(t.getExecutionType())
|
||||
.price(t.getPrice().doubleValue())
|
||||
.quantity(t.getQuantity().doubleValue())
|
||||
.grossAmount(t.getGrossAmount().doubleValue())
|
||||
|
||||
@@ -122,7 +122,7 @@ public class TradeSignalService {
|
||||
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 (strategyId == null) return null;
|
||||
return strategyRepo.findById(strategyId)
|
||||
|
||||
@@ -24,12 +24,20 @@ public class TradingExecutionService {
|
||||
|
||||
public void executeSignal(Long userId, String market,
|
||||
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;
|
||||
GcAppSettings app = appSettingsService.getEntity(userId, null);
|
||||
TradingMode mode = TradingMode.fromString(app.getTradingMode());
|
||||
|
||||
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()) {
|
||||
liveTradingService.tryExecuteOnSignal(
|
||||
|
||||
@@ -67,9 +67,16 @@ public class OrderExecutionQueue {
|
||||
|
||||
public void submitSignal(Long userId, String market,
|
||||
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;
|
||||
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() {
|
||||
@@ -97,7 +104,8 @@ public class OrderExecutionQueue {
|
||||
if (req.kind() == OrderRequest.OrderKind.STRATEGY_SIGNAL) {
|
||||
tradingExecutionService.executeSignal(
|
||||
req.userId(), req.market(),
|
||||
req.strategyId(), req.side(), req.price());
|
||||
req.strategyId(), req.side(), req.price(),
|
||||
req.candleType(), req.strategyName(), req.executionType());
|
||||
} else {
|
||||
tradingExecutionService.executeRiskExit(req);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,10 @@ public record OrderRequest(
|
||||
Long strategyId,
|
||||
String side,
|
||||
double price,
|
||||
String reason
|
||||
String reason,
|
||||
String candleType,
|
||||
String strategyName,
|
||||
String executionType
|
||||
) {
|
||||
public enum OrderKind {
|
||||
STRATEGY_SIGNAL,
|
||||
@@ -20,20 +23,25 @@ public record OrderRequest(
|
||||
}
|
||||
|
||||
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,
|
||||
strategyId, side, price, "STRATEGY");
|
||||
strategyId, side, price, "STRATEGY",
|
||||
candleType, strategyName, executionType);
|
||||
}
|
||||
|
||||
public static OrderRequest stopLoss(String deviceId, Long userId, String market,
|
||||
double price, double lossPct) {
|
||||
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,
|
||||
double price, double profitPct) {
|
||||
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"
|
||||
);
|
||||
if (s.getUserId() != null) {
|
||||
String strategyName = tradeSignalService.resolveStrategyName(
|
||||
s.getStrategyId(), null);
|
||||
orderExecutionQueue.submitSignal(
|
||||
s.getUserId(), market,
|
||||
s.getStrategyId(), signal,
|
||||
lastBar.getClosePrice().doubleValue());
|
||||
lastBar.getClosePrice().doubleValue(),
|
||||
candleType, strategyName, "REALTIME_TICK");
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
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;
|
||||
Reference in New Issue
Block a user