알림 전체닫기 오류 수정
This commit is contained in:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user