모의투자 로직 변경

This commit is contained in:
Macbook
2026-05-31 14:05:46 +09:00
parent ead97dad5d
commit d6eedf19bb
33 changed files with 1456 additions and 330 deletions
@@ -5,6 +5,7 @@ import com.goldenchart.dto.*;
import com.goldenchart.entity.*;
import com.goldenchart.repository.*;
import com.goldenchart.storage.Ta4jStorage;
import com.goldenchart.trading.TradingAccess;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
@@ -51,9 +52,10 @@ public class PaperTradingService {
// ── 조회 ─────────────────────────────────────────────────────────────────
@Transactional(readOnly = true)
public PaperSummaryDto getSummary(Long userId, String deviceId, Map<String, Double> markPrices) {
GcAppSettings app = appSettingsService.getEntity(userId, deviceId);
GcPaperAccount account = getOrCreateAccount(userId, deviceId, app);
public PaperSummaryDto getSummary(Long userId, Map<String, Double> markPrices) {
long uid = TradingAccess.requireUserId(userId);
GcAppSettings app = appSettingsService.getEntity(uid, null);
GcPaperAccount account = getOrCreateAccount(uid, app);
List<GcPaperPosition> positions = positionRepo.findByAccountIdOrderBySymbolAsc(account.getId());
double stockEval = 0;
@@ -121,38 +123,40 @@ public class PaperTradingService {
}
@Transactional(readOnly = true)
public List<PaperAllocationDto> listAllocations(Long userId, String deviceId,
Map<String, Double> markPrices) {
GcAppSettings app = appSettingsService.getEntity(userId, deviceId);
GcPaperAccount account = getOrCreateAccount(userId, deviceId, app);
PaperSummaryDto s = getSummary(userId, deviceId, markPrices);
public List<PaperAllocationDto> listAllocations(Long userId, Map<String, Double> markPrices) {
long uid = TradingAccess.requireUserId(userId);
GcAppSettings app = appSettingsService.getEntity(uid, null);
GcPaperAccount account = getOrCreateAccount(uid, app);
PaperSummaryDto s = getSummary(uid, markPrices);
return allocationService.listWithMetrics(account.getId(), markPrices, s.getTotalAsset());
}
public List<PaperAllocationDto> bulkAllocations(Long userId, String deviceId,
PaperAllocationBulkRequest req) {
GcAppSettings app = appSettingsService.getEntity(userId, deviceId);
GcPaperAccount account = getOrCreateAccount(userId, deviceId, app);
public List<PaperAllocationDto> bulkAllocations(Long userId, PaperAllocationBulkRequest req) {
long uid = TradingAccess.requireUserId(userId);
GcAppSettings app = appSettingsService.getEntity(uid, null);
GcPaperAccount account = getOrCreateAccount(uid, app);
BigDecimal defaultMax = account.getInitialCapitalSnapshot() != null
? account.getInitialCapitalSnapshot()
: app.getPaperInitialCapital();
return allocationService.bulkUpsert(account.getId(), req, defaultMax);
}
public PaperAllocationDto patchAllocation(Long userId, String deviceId, String symbol,
public PaperAllocationDto patchAllocation(Long userId, String symbol,
PaperAllocationPatchRequest req) {
GcAppSettings app = appSettingsService.getEntity(userId, deviceId);
GcPaperAccount account = getOrCreateAccount(userId, deviceId, app);
long uid = TradingAccess.requireUserId(userId);
GcAppSettings app = appSettingsService.getEntity(uid, null);
GcPaperAccount account = getOrCreateAccount(uid, app);
return allocationService.patch(account.getId(), symbol, req);
}
@Transactional(readOnly = true)
public PaperPageDto<PaperTradeDto> listTrades(Long userId, String deviceId,
public PaperPageDto<PaperTradeDto> listTrades(Long userId,
String symbol, String side, String source,
LocalDateTime from, LocalDateTime to,
int page, int size) {
GcAppSettings app = appSettingsService.getEntity(userId, deviceId);
GcPaperAccount account = getOrCreateAccount(userId, deviceId, app);
long uid = TradingAccess.requireUserId(userId);
GcAppSettings app = appSettingsService.getEntity(uid, null);
GcPaperAccount account = getOrCreateAccount(uid, app);
PageRequest pr = PageRequest.of(Math.max(0, page), Math.min(100, Math.max(1, size)));
Page<GcPaperTrade> result = tradeRepo.findFiltered(
account.getId(), symbol, side, source, from, to, pr);
@@ -165,39 +169,43 @@ public class PaperTradingService {
.build();
}
public List<PaperTradeDto> listTradesLegacy(Long userId, String deviceId) {
return listTrades(userId, deviceId, null, null, null, null, null, 0, 100).getContent();
public List<PaperTradeDto> listTradesLegacy(Long userId) {
return listTrades(userId, null, null, null, null, null, 0, 100).getContent();
}
@Transactional(readOnly = true)
public PaperPageDto<PaperLedgerEntryDto> listLedger(Long userId, String deviceId,
public PaperPageDto<PaperLedgerEntryDto> listLedger(Long userId,
LocalDateTime from, LocalDateTime to,
int page, int size) {
GcAppSettings app = appSettingsService.getEntity(userId, deviceId);
GcPaperAccount account = getOrCreateAccount(userId, deviceId, app);
long uid = TradingAccess.requireUserId(userId);
GcAppSettings app = appSettingsService.getEntity(uid, null);
GcPaperAccount account = getOrCreateAccount(uid, app);
return ledgerService.list(account.getId(), from, to, page, size);
}
@Transactional(readOnly = true)
public List<PaperDailySnapshotDto> listDailySnapshots(Long userId, String deviceId,
public List<PaperDailySnapshotDto> listDailySnapshots(Long userId,
LocalDate from, LocalDate to) {
GcAppSettings app = appSettingsService.getEntity(userId, deviceId);
GcPaperAccount account = getOrCreateAccount(userId, deviceId, app);
long uid = TradingAccess.requireUserId(userId);
GcAppSettings app = appSettingsService.getEntity(uid, null);
GcPaperAccount account = getOrCreateAccount(uid, app);
return snapshotService.list(account.getId(), from, to);
}
@Transactional(readOnly = true)
public List<PaperOrderDto> listPendingOrders(Long userId, String deviceId) {
GcPaperAccount account = getOrCreateAccount(userId, deviceId,
appSettingsService.getEntity(userId, deviceId));
public List<PaperOrderDto> listPendingOrders(Long userId) {
long uid = TradingAccess.requireUserId(userId);
GcPaperAccount account = getOrCreateAccount(uid,
appSettingsService.getEntity(uid, null));
return orderRepo.findByAccountIdAndStatusOrderByCreatedAtDesc(account.getId(), "PENDING")
.stream().map(this::toOrderDto).toList();
}
// ── 주문 ─────────────────────────────────────────────────────────────────
public PaperPlaceOrderResult placeOrder(Long userId, String deviceId, PaperOrderRequest req) {
GcAppSettings app = appSettingsService.getEntity(userId, deviceId);
public PaperPlaceOrderResult placeOrder(Long userId, PaperOrderRequest req) {
long uid = TradingAccess.requireUserId(userId);
GcAppSettings app = appSettingsService.getEntity(uid, null);
if (!Boolean.TRUE.equals(app.getPaperTradingEnabled())) {
throw new IllegalStateException("모의투자가 비활성화되어 있습니다.");
}
@@ -217,7 +225,7 @@ public class PaperTradingService {
String source = req.getSource() != null ? req.getSource() : "MANUAL";
String orderKind = req.getOrderKind() != null ? req.getOrderKind() : "limit";
GcPaperAccount account = getOrCreateAccount(userId, deviceId, app);
GcPaperAccount account = getOrCreateAccount(uid, app);
double qty = req.getQuantity() != null ? req.getQuantity() : 0;
if (qty <= 0) {
qty = resolveAutoQuantity(account, app, req.getMarket(), side, price,
@@ -233,14 +241,15 @@ public class PaperTradingService {
return PaperPlaceOrderResult.builder().order(toOrderDto(order)).build();
}
PaperTradeDto trade = executeTrade(userId, deviceId, app, req.getMarket(), side, orderKind, source,
PaperTradeDto trade = executeTrade(uid, app, req.getMarket(), side, orderKind, source,
req.getStrategyId(), price, qty, null, false);
return PaperPlaceOrderResult.builder().trade(trade).build();
}
public PaperOrderDto cancelOrder(Long userId, String deviceId, Long orderId) {
GcPaperAccount account = getOrCreateAccount(userId, deviceId,
appSettingsService.getEntity(userId, deviceId));
public PaperOrderDto cancelOrder(Long userId, Long orderId) {
long uid = TradingAccess.requireUserId(userId);
GcPaperAccount account = getOrCreateAccount(uid,
appSettingsService.getEntity(uid, null));
GcPaperOrder order = orderRepo.findById(orderId)
.orElseThrow(() -> new IllegalArgumentException("주문을 찾을 수 없습니다."));
if (!order.getAccountId().equals(account.getId())) {
@@ -255,12 +264,17 @@ public class PaperTradingService {
return toOrderDto(order);
}
public void tryExecuteOnSignal(String deviceId, Long userId, String market,
public void tryExecuteOnSignal(Long userId, String market,
Long strategyId, String signalType, double price) {
if (deviceId == null || deviceId.isBlank()) return;
long uid;
try {
uid = TradingAccess.requireUserId(userId);
} catch (Exception e) {
return;
}
if (!"BUY".equals(signalType) && !"SELL".equals(signalType)) return;
GcAppSettings app = appSettingsService.getEntity(userId, deviceId);
GcAppSettings app = appSettingsService.getEntity(uid, null);
if (!Boolean.TRUE.equals(app.getPaperTradingEnabled())) return;
if (!Boolean.TRUE.equals(app.getPaperAutoTradeEnabled())) return;
List<GcLiveStrategySettings> liveForMarket = liveSettingsRepo.findActiveByMarket(market).stream()
@@ -270,7 +284,7 @@ public class PaperTradingService {
if (!Boolean.TRUE.equals(app.getLiveStrategyCheck()) && !marketActive) return;
try {
GcPaperAccount account = getOrCreateAccount(userId, deviceId, app);
GcPaperAccount account = getOrCreateAccount(uid, app);
String positionMode = liveForMarket.stream()
.map(GcLiveStrategySettings::getPositionMode)
.filter(m -> m != null && !m.isBlank())
@@ -299,7 +313,7 @@ public class PaperTradingService {
if (denom <= 0 || budget < minOrder) return;
double qty = budget / denom;
if (qty * execPrice < minOrder) return;
executeTrade(userId, deviceId, app, market, "BUY", "market", "STRATEGY",
executeTrade(uid, app, market, "BUY", "market", "STRATEGY",
strategyId, price, qty, null, true);
log.info("[Paper] STRATEGY BUY {} qty≈{} @ {}", market, qty, price);
} else {
@@ -308,7 +322,7 @@ public class PaperTradingService {
if (pos == null || pos.getQuantity().doubleValue() <= 0) return;
double avail = availableSellQty(account.getId(), market, pos.getQuantity().doubleValue());
if (avail <= 0) return;
executeTrade(userId, deviceId, app, market, "SELL", "market", "STRATEGY",
executeTrade(uid, app, market, "SELL", "market", "STRATEGY",
strategyId, price, avail, null, true);
log.info("[Paper] STRATEGY SELL {} qty={} @ {}", market, avail, price);
}
@@ -332,11 +346,13 @@ public class PaperTradingService {
GcPaperAccount account = accountRepo.findById(order.getAccountId()).orElse(null);
if (account == null) return;
GcAppSettings app = appSettingsService.getEntity(account.getUserId(), account.getDeviceId());
if (account.getUserId() == null) return;
long uid = account.getUserId();
GcAppSettings app = appSettingsService.getEntity(uid, null);
releaseReservation(account, order);
double qty = order.getQuantity().doubleValue();
executeTrade(account.getUserId(), account.getDeviceId(), app, order.getSymbol(),
executeTrade(uid, app, order.getSymbol(),
order.getSide(), order.getOrderKind(), order.getSource(), order.getStrategyId(),
limit, qty, null, "STRATEGY".equals(order.getSource()));
@@ -361,9 +377,10 @@ public class PaperTradingService {
return 0;
}
public PaperSummaryDto resetAccount(Long userId, String deviceId) {
GcAppSettings app = appSettingsService.getEntity(userId, deviceId);
GcPaperAccount account = getOrCreateAccount(userId, deviceId, app);
public PaperSummaryDto resetAccount(Long userId) {
long uid = TradingAccess.requireUserId(userId);
GcAppSettings app = appSettingsService.getEntity(uid, null);
GcPaperAccount account = getOrCreateAccount(uid, app);
BigDecimal initial = app.getPaperInitialCapital() != null
? app.getPaperInitialCapital() : BigDecimal.valueOf(10_000_000);
BigDecimal cashBefore = account.getCashBalance();
@@ -405,8 +422,8 @@ public class PaperTradingService {
accountRepo.save(account);
allocationService.seedFromPositions(account.getId(), initial);
log.info("[Paper] account reset device={} capital={}", deviceId, initial);
return getSummary(userId, deviceId, null);
log.info("[Paper] account reset userId={} capital={}", uid, initial);
return getSummary(uid, null);
}
// ── 내부 ─────────────────────────────────────────────────────────────────
@@ -481,11 +498,11 @@ public class PaperTradingService {
return Math.max(0, held - reserved);
}
private PaperTradeDto executeTrade(Long userId, String deviceId, GcAppSettings app,
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) {
GcPaperAccount account = getOrCreateAccount(userId, deviceId, app);
GcPaperAccount account = getOrCreateAccount(userId, app);
double feeRate = pct(app.getPaperFeeRatePct(), 0.05);
double slip = pct(app.getPaperSlippagePct(), 0);
double minOrder = app.getPaperMinOrderKrw() != null
@@ -638,14 +655,14 @@ public class PaperTradingService {
positionRepo.save(pos);
}
private GcPaperAccount getOrCreateAccount(Long userId, String deviceId, GcAppSettings app) {
String dev = deviceId != null && !deviceId.isBlank() ? deviceId : "anonymous";
return accountRepo.findByDeviceId(dev).orElseGet(() -> {
private GcPaperAccount getOrCreateAccount(long userId, GcAppSettings app) {
return accountRepo.findByUserId(userId).orElseGet(() -> {
String devKey = TradingAccess.accountDeviceKey(userId);
BigDecimal initial = app.getPaperInitialCapital() != null
? app.getPaperInitialCapital() : BigDecimal.valueOf(10_000_000);
GcPaperAccount a = GcPaperAccount.builder()
.userId(userId)
.deviceId(dev)
.deviceId(devKey)
.cashBalance(initial)
.realizedPnl(BigDecimal.ZERO)
.reservedCash(BigDecimal.ZERO)