알림 전체닫기 오류 수정

This commit is contained in:
Macbook
2026-06-04 13:00:59 +09:00
parent 5740a9a59d
commit 128b589369
16 changed files with 513 additions and 126 deletions
@@ -221,7 +221,6 @@ public class PaperTradingService {
throw new IllegalArgumentException("가격은 0보다 커야 합니다.");
}
String orderType = resolveOrderType(req);
String source = req.getSource() != null ? req.getSource() : "MANUAL";
String orderKind = req.getOrderKind() != null ? req.getOrderKind() : "limit";
@@ -235,15 +234,11 @@ public class PaperTradingService {
throw new IllegalArgumentException("주문 수량을 계산할 수 없습니다.");
}
if ("LIMIT".equals(orderType)) {
GcPaperOrder order = createPendingLimitOrder(account, app, req.getMarket(), side, orderKind,
source, req.getStrategyId(), price, qty);
return PaperPlaceOrderResult.builder().order(toOrderDto(order)).build();
}
PaperTradeDto trade = executeTrade(uid, app, req.getMarket(), side, orderKind, source,
req.getStrategyId(), price, qty, null, false);
return PaperPlaceOrderResult.builder().trade(trade).build();
// 시장가(MARKET)를 포함한 모든 주문을 현재가 기준 지정가 미체결로 생성한다.
// 체결은 PaperOrderMatcherService 가 다음 시세 틱에서 처리한다.
GcPaperOrder order = createPendingLimitOrder(account, app, req.getMarket(), side, orderKind,
source, req.getStrategyId(), price, qty, false);
return PaperPlaceOrderResult.builder().order(toOrderDto(order)).build();
}
public PaperOrderDto cancelOrder(Long userId, Long orderId) {
@@ -296,24 +291,30 @@ public class PaperTradingService {
? app.getPaperInitialCapital() : BigDecimal.valueOf(10_000_000));
GcPaperSymbolAllocation alloc = allocationService.getOrDefaultEntity(
account.getId(), market, defaultMax);
boolean staged = PaperTradeStageService.usesStagedTrading(alloc);
boolean useStaged = PaperTradeStageService.usesStagedTrading(alloc);
GcPaperPosition posNow = positionRepo.findByAccountIdAndSymbol(account.getId(), market)
.orElse(null);
double held = posNow != null ? posNow.getQuantity().doubleValue() : 0;
if ("LONG_ONLY".equals(positionMode) && !staged) {
if ("LONG_ONLY".equals(positionMode) && !useStaged) {
if ("BUY".equals(signalType) && held > 0) return;
if ("SELL".equals(signalType) && held <= 0) return;
}
// 비분할 모드에서 이미 동일 방향 미체결 주문이 있으면 중복 생성 방지
if (!useStaged && hasPendingOrderForSide(account.getId(), market, signalType)) {
log.debug("[Paper] 이미 미체결 주문 존재 — 신규 생성 생략 {} {}", market, signalType);
return;
}
double feeRate = pct(app.getPaperFeeRatePct(), 0.05);
double slip = pct(app.getPaperSlippagePct(), 0);
double minOrder = app.getPaperMinOrderKrw() != null
? app.getPaperMinOrderKrw().doubleValue() : 5000;
if ("BUY".equals(signalType)) {
if (staged) {
if (useStaged) {
if (held <= 0) {
allocationService.resetTradeStages(account.getId(), market);
alloc = allocationService.getOrDefaultEntity(account.getId(), market, defaultMax);
@@ -329,10 +330,10 @@ public class PaperTradingService {
if (denom <= 0) return;
double qty = stageKrw / denom;
if (qty * execPrice < minOrder) return;
executeTrade(uid, app, market, "BUY", "market", "STRATEGY",
strategyId, price, qty, null, true);
allocationService.advanceBuyStageAfterTrade(account.getId(), market);
log.info("[Paper] STRATEGY BUY (staged) {} qty≈{} @ {}", market, qty, price);
// 알림 발생 시점 현재가 기준 미체결 주문 생성 (staged=true → 체결 시 단계 진행)
createPendingLimitOrder(account, app, market, "BUY", "market", "STRATEGY",
strategyId, price, qty, true);
log.info("[Paper] STRATEGY BUY (staged) 미체결 생성 {} qty≈{} @ {}", market, qty, price);
} else {
double budgetPct = pct(app.getPaperAutoTradeBudgetPct(), 95);
double cash = account.getCashBalance().doubleValue();
@@ -344,34 +345,36 @@ public class PaperTradingService {
if (denom <= 0 || budget < minOrder) return;
double qty = budget / denom;
if (qty * execPrice < minOrder) return;
executeTrade(uid, app, market, "BUY", "market", "STRATEGY",
strategyId, price, qty, null, true);
log.info("[Paper] STRATEGY BUY {} qty≈{} @ {}", market, qty, price);
// 알림 발생 시점 현재가 기준 미체결 주문 생성
createPendingLimitOrder(account, app, market, "BUY", "market", "STRATEGY",
strategyId, price, qty, false);
log.info("[Paper] STRATEGY BUY 미체결 생성 {} qty≈{} @ {}", market, qty, price);
}
} else {
if (held <= 0) return;
double avail = availableSellQty(account.getId(), market, held);
if (avail <= 0) return;
if (staged) {
if (useStaged) {
double stageKrw = PaperTradeStageService.nextSellStageKrw(alloc);
if (stageKrw <= 0) return;
if (stageKrw < minOrder) return;
double execPrice = price * (1 - slip / 100.0);
double qty = PaperTradeStageService.sellQtyForStageKrw(stageKrw, avail, execPrice);
if (qty * execPrice < minOrder) return;
executeTrade(uid, app, market, "SELL", "market", "STRATEGY",
strategyId, price, qty, null, true);
allocationService.advanceSellStageAfterTrade(account.getId(), market);
log.info("[Paper] STRATEGY SELL (staged) {} qty={} @ {}", market, qty, price);
// 알림 발생 시점 현재가 기준 미체결 주문 생성 (staged=true → 체결 시 단계 진행)
createPendingLimitOrder(account, app, market, "SELL", "market", "STRATEGY",
strategyId, price, qty, true);
log.info("[Paper] STRATEGY SELL (staged) 미체결 생성 {} qty={} @ {}", market, qty, price);
} else {
executeTrade(uid, app, market, "SELL", "market", "STRATEGY",
strategyId, price, avail, null, true);
log.info("[Paper] STRATEGY SELL {} qty={} @ {}", market, avail, price);
// 알림 발생 시점 현재가 기준 미체결 주문 생성
createPendingLimitOrder(account, app, market, "SELL", "market", "STRATEGY",
strategyId, price, avail, false);
log.info("[Paper] STRATEGY SELL 미체결 생성 {} qty={} @ {}", market, avail, price);
}
}
} catch (Exception e) {
log.warn("[Paper] auto trade failed {} {}: {}", market, signalType, e.getMessage());
log.warn("[Paper] auto trade 미체결 생성 실패 {} {}: {}", market, signalType, e.getMessage());
}
}
@@ -379,12 +382,51 @@ public class PaperTradingService {
GcPaperOrder order = orderRepo.findById(orderId).orElse(null);
if (order == null || !"PENDING".equals(order.getStatus())) return;
boolean fill = false;
double limit = order.getLimitPrice() != null ? order.getLimitPrice().doubleValue() : 0;
if ("BUY".equals(order.getSide())) {
fill = markPrice <= limit;
} else if ("SELL".equals(order.getSide())) {
fill = markPrice >= limit;
// ── 체결 모드 조회 ───────────────────────────────────────────────────────
GcPaperAccount acctCheck = accountRepo.findById(order.getAccountId()).orElse(null);
String fillMode = "LIMIT_ONLY";
double autoCancelPct = 1.0;
if (acctCheck != null && acctCheck.getUserId() != null) {
GcAppSettings appCheck = appSettingsService.getEntity(acctCheck.getUserId(), null);
fillMode = appCheck.getPaperOrderFillMode() != null
? appCheck.getPaperOrderFillMode() : "LIMIT_ONLY";
autoCancelPct = appCheck.getPaperOrderAutoCancelPct() != null
? appCheck.getPaperOrderAutoCancelPct().doubleValue() : 1.0;
}
boolean fill = false;
boolean autoCancel = false;
switch (fillMode) {
case "NEXT_TICK" -> fill = true; // B. 다음 틱 무조건 체결
case "AUTO_CANCEL" -> { // C. 허용폭 초과 시 자동 취소
double deviation = limit > 0 ? Math.abs(markPrice - limit) / limit * 100.0 : 0;
if (deviation > autoCancelPct) {
autoCancel = true;
} else {
// 허용폭 내이면 지정가 조건 충족 시 체결
if ("BUY".equals(order.getSide())) fill = markPrice <= limit;
else if ("SELL".equals(order.getSide())) fill = markPrice >= limit;
}
}
default -> { // A. LIMIT_ONLY (기본값)
if ("BUY".equals(order.getSide())) fill = markPrice <= limit;
else if ("SELL".equals(order.getSide())) fill = markPrice >= limit;
}
}
if (autoCancel) {
GcPaperAccount acctForCancel = accountRepo.findById(order.getAccountId()).orElse(null);
if (acctForCancel != null) {
releaseReservation(acctForCancel, order);
order.setStatus("CANCELLED");
orderRepo.save(order);
log.info("[Paper] AUTO_CANCEL 주문 자동취소 orderId={} symbol={} limit={} markPrice={} deviation>{}%",
orderId, order.getSymbol(), limit, markPrice, autoCancelPct);
}
return;
}
if (!fill) return;
@@ -406,6 +448,21 @@ public class PaperTradingService {
order.setReservedCash(BigDecimal.ZERO);
order.setReservedQty(BigDecimal.ZERO);
orderRepo.save(order);
// staged 주문이면 체결 시점에 분할매매 단계를 진행
if (Boolean.TRUE.equals(order.getStaged())) {
try {
if ("BUY".equals(order.getSide())) {
allocationService.advanceBuyStageAfterTrade(order.getAccountId(), order.getSymbol());
log.info("[Paper] staged BUY 단계 진행 완료 {} orderId={}", order.getSymbol(), orderId);
} else if ("SELL".equals(order.getSide())) {
allocationService.advanceSellStageAfterTrade(order.getAccountId(), order.getSymbol());
log.info("[Paper] staged SELL 단계 진행 완료 {} orderId={}", order.getSymbol(), orderId);
}
} catch (Exception e) {
log.warn("[Paper] staged 단계 진행 실패 orderId={}: {}", orderId, e.getMessage());
}
}
}
public double resolveMarkPrice(String symbol) {
@@ -475,7 +532,7 @@ public class PaperTradingService {
private GcPaperOrder createPendingLimitOrder(GcPaperAccount account, GcAppSettings app,
String market, String side, String orderKind,
String source, Long strategyId,
double limitPrice, double qty) {
double limitPrice, double qty, boolean staged) {
double feeRate = pct(app.getPaperFeeRatePct(), 0.05);
double slip = pct(app.getPaperSlippagePct(), 0);
double execPrice = "BUY".equals(side)
@@ -502,6 +559,7 @@ public class PaperTradingService {
.orderKind(orderKind)
.source(source)
.strategyId(strategyId)
.staged(staged)
.build());
}
@@ -523,9 +581,16 @@ public class PaperTradingService {
.orderKind(orderKind)
.source(source)
.strategyId(strategyId)
.staged(staged)
.build());
}
private boolean hasPendingOrderForSide(Long accountId, String symbol, String side) {
return orderRepo.findByAccountIdAndStatusOrderByCreatedAtDesc(accountId, "PENDING")
.stream()
.anyMatch(o -> symbol.equals(o.getSymbol()) && side.equals(o.getSide()));
}
private void releaseReservation(GcPaperAccount account, GcPaperOrder order) {
if (order.getReservedCash() != null && order.getReservedCash().signum() > 0) {
account.setReservedCash(account.getReservedCash().subtract(order.getReservedCash()).max(BigDecimal.ZERO));
@@ -668,15 +733,6 @@ public class PaperTradingService {
return avail * pctVal / 100.0;
}
private static String resolveOrderType(PaperOrderRequest req) {
if (req.getOrderType() != null) {
String t = req.getOrderType().toUpperCase();
if ("MARKET".equals(t) || "LIMIT".equals(t)) return t;
}
String kind = req.getOrderKind() != null ? req.getOrderKind().toLowerCase() : "limit";
return "market".equals(kind) ? "MARKET" : "LIMIT";
}
private void upsertPositionBuy(GcPaperAccount account, String market, String koreanName,
BigDecimal qty, BigDecimal price) {
GcPaperPosition pos = positionRepo.findByAccountIdAndSymbol(account.getId(), market).orElse(null);