투자관리 화면 수정
This commit is contained in:
@@ -61,6 +61,8 @@ public class PaperAllocationService {
|
||||
if (item.getIsActive() != null) row.setIsActive(item.getIsActive());
|
||||
if (item.getPinned() != null) row.setPinned(item.getPinned());
|
||||
row.setSortOrder(item.getSortOrder() != null ? item.getSortOrder() : order++);
|
||||
applyStageAmounts(row, item.getBuyStage1Krw(), item.getBuyStage2Krw(), item.getBuyStage3Krw(),
|
||||
item.getSellStage1Krw(), item.getSellStage2Krw(), item.getSellStage3Krw());
|
||||
allocRepo.save(row);
|
||||
}
|
||||
return listWithMetrics(accountId, null, 0);
|
||||
@@ -78,10 +80,37 @@ public class PaperAllocationService {
|
||||
if (req.getPinned() != null) row.setPinned(req.getPinned());
|
||||
if (req.getSortOrder() != null) row.setSortOrder(req.getSortOrder());
|
||||
if (req.getKoreanName() != null) row.setKoreanName(req.getKoreanName());
|
||||
applyStageAmounts(row, req.getBuyStage1Krw(), req.getBuyStage2Krw(), req.getBuyStage3Krw(),
|
||||
req.getSellStage1Krw(), req.getSellStage2Krw(), req.getSellStage3Krw());
|
||||
allocRepo.save(row);
|
||||
return toDto(row, accountId, null, 0);
|
||||
}
|
||||
|
||||
public void resetTradeStages(Long accountId, String symbol) {
|
||||
allocRepo.findByAccountIdAndSymbol(accountId, symbol).ifPresent(row -> {
|
||||
PaperTradeStageService.resetTradeStages(row);
|
||||
allocRepo.save(row);
|
||||
});
|
||||
}
|
||||
|
||||
public void advanceBuyStageAfterTrade(Long accountId, String symbol) {
|
||||
allocRepo.findByAccountIdAndSymbol(accountId, symbol).ifPresent(row -> {
|
||||
PaperTradeStageService.advanceBuyStage(row);
|
||||
allocRepo.save(row);
|
||||
});
|
||||
}
|
||||
|
||||
public void advanceSellStageAfterTrade(Long accountId, String symbol) {
|
||||
allocRepo.findByAccountIdAndSymbol(accountId, symbol).ifPresent(row -> {
|
||||
PaperTradeStageService.advanceSellStage(row);
|
||||
allocRepo.save(row);
|
||||
});
|
||||
}
|
||||
|
||||
public GcPaperSymbolAllocation getOrDefaultEntity(Long accountId, String symbol, BigDecimal defaultMax) {
|
||||
return getOrDefault(accountId, symbol, defaultMax);
|
||||
}
|
||||
|
||||
public GcPaperSymbolAllocation getOrDefault(Long accountId, String symbol, BigDecimal defaultMax) {
|
||||
return allocRepo.findByAccountIdAndSymbol(accountId, symbol)
|
||||
.orElseGet(() -> {
|
||||
@@ -191,6 +220,29 @@ public class PaperAllocationService {
|
||||
.evalAmount(eval)
|
||||
.symbolReturnPct(retPct)
|
||||
.weightPct(weight)
|
||||
.buyStage1Krw(stageKrw(a.getBuyStage1Krw()))
|
||||
.buyStage2Krw(stageKrw(a.getBuyStage2Krw()))
|
||||
.buyStage3Krw(stageKrw(a.getBuyStage3Krw()))
|
||||
.sellStage1Krw(stageKrw(a.getSellStage1Krw()))
|
||||
.sellStage2Krw(stageKrw(a.getSellStage2Krw()))
|
||||
.sellStage3Krw(stageKrw(a.getSellStage3Krw()))
|
||||
.buyStageDone(a.getBuyStageDone() != null ? a.getBuyStageDone() : 0)
|
||||
.sellStageDone(a.getSellStageDone() != null ? a.getSellStageDone() : 0)
|
||||
.build();
|
||||
}
|
||||
|
||||
private static void applyStageAmounts(GcPaperSymbolAllocation row,
|
||||
Double b1, Double b2, Double b3,
|
||||
Double s1, Double s2, Double s3) {
|
||||
if (b1 != null) row.setBuyStage1Krw(BigDecimal.valueOf(Math.max(0, b1)).setScale(2, RM));
|
||||
if (b2 != null) row.setBuyStage2Krw(BigDecimal.valueOf(Math.max(0, b2)).setScale(2, RM));
|
||||
if (b3 != null) row.setBuyStage3Krw(BigDecimal.valueOf(Math.max(0, b3)).setScale(2, RM));
|
||||
if (s1 != null) row.setSellStage1Krw(BigDecimal.valueOf(Math.max(0, s1)).setScale(2, RM));
|
||||
if (s2 != null) row.setSellStage2Krw(BigDecimal.valueOf(Math.max(0, s2)).setScale(2, RM));
|
||||
if (s3 != null) row.setSellStage3Krw(BigDecimal.valueOf(Math.max(0, s3)).setScale(2, RM));
|
||||
}
|
||||
|
||||
private static double stageKrw(BigDecimal v) {
|
||||
return v != null ? v.doubleValue() : 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
package com.goldenchart.service;
|
||||
|
||||
import com.goldenchart.entity.GcPaperSymbolAllocation;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* 모의투자 자동매매 — 종목별 1~3차 분할 매수·매도 금액·진행 단계.
|
||||
*/
|
||||
public final class PaperTradeStageService {
|
||||
|
||||
private static final int MAX_STAGES = 3;
|
||||
|
||||
private PaperTradeStageService() {}
|
||||
|
||||
public static boolean usesStagedTrading(GcPaperSymbolAllocation alloc) {
|
||||
return sumBuyStages(alloc) > 0 || sumSellStages(alloc) > 0;
|
||||
}
|
||||
|
||||
public static double sumBuyStages(GcPaperSymbolAllocation alloc) {
|
||||
return stageKrw(alloc.getBuyStage1Krw())
|
||||
+ stageKrw(alloc.getBuyStage2Krw())
|
||||
+ stageKrw(alloc.getBuyStage3Krw());
|
||||
}
|
||||
|
||||
public static double sumSellStages(GcPaperSymbolAllocation alloc) {
|
||||
return stageKrw(alloc.getSellStage1Krw())
|
||||
+ stageKrw(alloc.getSellStage2Krw())
|
||||
+ stageKrw(alloc.getSellStage3Krw());
|
||||
}
|
||||
|
||||
/** 다음 매수 단계(1~3) 금액. 없으면 0 */
|
||||
public static double nextBuyStageKrw(GcPaperSymbolAllocation alloc) {
|
||||
int done = stageDone(alloc.getBuyStageDone());
|
||||
int next = done + 1;
|
||||
if (next > MAX_STAGES) return 0;
|
||||
return stageAmount(alloc, next, true);
|
||||
}
|
||||
|
||||
/** 다음 매도 단계(1~3) 금액. 없으면 0 */
|
||||
public static double nextSellStageKrw(GcPaperSymbolAllocation alloc) {
|
||||
int done = stageDone(alloc.getSellStageDone());
|
||||
int next = done + 1;
|
||||
if (next > MAX_STAGES) return 0;
|
||||
return stageAmount(alloc, next, false);
|
||||
}
|
||||
|
||||
public static void advanceBuyStage(GcPaperSymbolAllocation alloc) {
|
||||
int done = stageDone(alloc.getBuyStageDone());
|
||||
if (done < MAX_STAGES) {
|
||||
alloc.setBuyStageDone(done + 1);
|
||||
}
|
||||
}
|
||||
|
||||
public static void advanceSellStage(GcPaperSymbolAllocation alloc) {
|
||||
int done = stageDone(alloc.getSellStageDone());
|
||||
if (done < MAX_STAGES) {
|
||||
alloc.setSellStageDone(done + 1);
|
||||
}
|
||||
}
|
||||
|
||||
public static void resetTradeStages(GcPaperSymbolAllocation alloc) {
|
||||
alloc.setBuyStageDone(0);
|
||||
alloc.setSellStageDone(0);
|
||||
}
|
||||
|
||||
/** 매도 수량: 단계 금액 기준, 보유 가용 수량 상한 */
|
||||
public static double sellQtyForStageKrw(double stageKrw, double availQty, double execPrice) {
|
||||
if (stageKrw <= 0 || availQty <= 0 || execPrice <= 0) return 0;
|
||||
double byKrw = stageKrw / execPrice;
|
||||
return Math.min(availQty, byKrw);
|
||||
}
|
||||
|
||||
private static int stageDone(Integer done) {
|
||||
if (done == null || done < 0) return 0;
|
||||
return Math.min(MAX_STAGES, done);
|
||||
}
|
||||
|
||||
private static double stageAmount(GcPaperSymbolAllocation alloc, int stage1Based, boolean buy) {
|
||||
return switch (stage1Based) {
|
||||
case 1 -> buy ? stageKrw(alloc.getBuyStage1Krw()) : stageKrw(alloc.getSellStage1Krw());
|
||||
case 2 -> buy ? stageKrw(alloc.getBuyStage2Krw()) : stageKrw(alloc.getSellStage2Krw());
|
||||
case 3 -> buy ? stageKrw(alloc.getBuyStage3Krw()) : stageKrw(alloc.getSellStage3Krw());
|
||||
default -> 0;
|
||||
};
|
||||
}
|
||||
|
||||
private static double stageKrw(BigDecimal v) {
|
||||
return v != null ? Math.max(0, v.doubleValue()) : 0;
|
||||
}
|
||||
}
|
||||
@@ -290,41 +290,85 @@ public class PaperTradingService {
|
||||
.filter(m -> m != null && !m.isBlank())
|
||||
.findFirst()
|
||||
.orElse("LONG_ONLY");
|
||||
if ("LONG_ONLY".equals(positionMode)) {
|
||||
GcPaperPosition posNow = positionRepo.findByAccountIdAndSymbol(account.getId(), market)
|
||||
.orElse(null);
|
||||
double held = posNow != null ? posNow.getQuantity().doubleValue() : 0;
|
||||
BigDecimal defaultMax = account.getInitialCapitalSnapshot() != null
|
||||
? account.getInitialCapitalSnapshot()
|
||||
: (app.getPaperInitialCapital() != null
|
||||
? app.getPaperInitialCapital() : BigDecimal.valueOf(10_000_000));
|
||||
GcPaperSymbolAllocation alloc = allocationService.getOrDefaultEntity(
|
||||
account.getId(), market, defaultMax);
|
||||
boolean staged = 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 ("BUY".equals(signalType) && held > 0) return;
|
||||
if ("SELL".equals(signalType) && held <= 0) 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)) {
|
||||
double budgetPct = pct(app.getPaperAutoTradeBudgetPct(), 95);
|
||||
double cash = account.getCashBalance().doubleValue();
|
||||
double reserved = account.getReservedCash() != null
|
||||
? account.getReservedCash().doubleValue() : 0;
|
||||
double budget = (cash - reserved) * budgetPct / 100.0;
|
||||
double execPrice = price * (1 + slip / 100.0);
|
||||
double denom = execPrice * (1 + feeRate / 100.0);
|
||||
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);
|
||||
if (staged) {
|
||||
if (held <= 0) {
|
||||
allocationService.resetTradeStages(account.getId(), market);
|
||||
alloc = allocationService.getOrDefaultEntity(account.getId(), market, defaultMax);
|
||||
} else if ("LONG_ONLY".equals(positionMode)
|
||||
&& alloc.getBuyStageDone() != null && alloc.getBuyStageDone() >= 3) {
|
||||
return;
|
||||
}
|
||||
double stageKrw = PaperTradeStageService.nextBuyStageKrw(alloc);
|
||||
if (stageKrw <= 0) return;
|
||||
if (stageKrw < minOrder) return;
|
||||
double execPrice = price * (1 + slip / 100.0);
|
||||
double denom = execPrice * (1 + feeRate / 100.0);
|
||||
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);
|
||||
} else {
|
||||
double budgetPct = pct(app.getPaperAutoTradeBudgetPct(), 95);
|
||||
double cash = account.getCashBalance().doubleValue();
|
||||
double reserved = account.getReservedCash() != null
|
||||
? account.getReservedCash().doubleValue() : 0;
|
||||
double budget = (cash - reserved) * budgetPct / 100.0;
|
||||
double execPrice = price * (1 + slip / 100.0);
|
||||
double denom = execPrice * (1 + feeRate / 100.0);
|
||||
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);
|
||||
}
|
||||
} else {
|
||||
GcPaperPosition pos = positionRepo.findByAccountIdAndSymbol(account.getId(), market)
|
||||
.orElse(null);
|
||||
if (pos == null || pos.getQuantity().doubleValue() <= 0) return;
|
||||
double avail = availableSellQty(account.getId(), market, pos.getQuantity().doubleValue());
|
||||
if (held <= 0) return;
|
||||
double avail = availableSellQty(account.getId(), market, held);
|
||||
if (avail <= 0) return;
|
||||
executeTrade(uid, app, market, "SELL", "market", "STRATEGY",
|
||||
strategyId, price, avail, null, true);
|
||||
log.info("[Paper] STRATEGY SELL {} qty={} @ {}", market, avail, price);
|
||||
|
||||
if (staged) {
|
||||
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);
|
||||
} else {
|
||||
executeTrade(uid, app, market, "SELL", "market", "STRATEGY",
|
||||
strategyId, price, avail, null, true);
|
||||
log.info("[Paper] STRATEGY SELL {} qty={} @ {}", market, avail, price);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("[Paper] auto trade failed {} {}: {}", market, signalType, e.getMessage());
|
||||
@@ -591,6 +635,9 @@ public class PaperTradingService {
|
||||
.positionQtyAfter(qtyAfter)
|
||||
.build());
|
||||
ledgerService.recordSellSettle(account, trade, netProceeds);
|
||||
if ("STRATEGY".equals(source) && qtyAfter.compareTo(BigDecimal.valueOf(0.00000001)) <= 0) {
|
||||
allocationService.resetTradeStages(account.getId(), market);
|
||||
}
|
||||
return toTradeDto(trade);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user