모의투자 로직 변경

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
@@ -58,6 +58,7 @@ public class BarCloseStrategyEvaluationService {
long candleTimeEpoch = barEndEpoch - signalBar.getTimePeriod().getSeconds();
for (GcLiveStrategySettings s : activeSettings) {
if (s.getUserId() == null) continue;
if (!conditionTimeframes.usesTimeframe(s.getStrategyId(), ct)) continue;
String closeSignal = "NONE";
@@ -82,9 +83,9 @@ public class BarCloseStrategyEvaluationService {
market, s.getStrategyId(), null,
closeSignal, signalBar.getClosePrice().doubleValue(),
candleTimeEpoch, ct, signalExecType);
if (s.getDeviceId() != null) {
if (s.getUserId() != null) {
orderExecutionQueue.submitSignal(
s.getDeviceId(), s.getUserId(), market,
s.getUserId(), market,
s.getStrategyId(), closeSignal,
signalBar.getClosePrice().doubleValue());
}
@@ -51,22 +51,27 @@ public class DashboardService {
: watchlistRepo.findByDeviceIdOrderByDisplayOrderAsc(deviceId).size();
out.put("watchlistCount", watchlistCount);
int liveCheckMarkets = (int) liveSettingsRepo.findAllByIsLiveCheckTrue().stream()
.filter(s -> deviceId.equals(s.getDeviceId()) || (userId != null && userId.equals(s.getUserId())))
.count();
int liveCheckMarkets = userId != null
? (int) liveSettingsRepo.findAllByIsLiveCheckTrueAndUserIdIsNotNull().stream()
.filter(s -> userId.equals(s.getUserId()))
.count()
: 0;
out.put("liveCheckMarkets", liveCheckMarkets);
List<LiveStrategySettingsDto> active = liveStrategySettingsService.listActive(userId, deviceId);
out.put("monitoredMarkets", active.size());
PaperSummaryDto paper = paperTradingService.getSummary(userId, deviceId, null);
out.put("paper", paper);
if (userId != null) {
out.put("paper", paperTradingService.getSummary(userId, null));
out.put("live", liveTradingService.getSummary(userId));
} else {
out.put("paper", PaperSummaryDto.builder().build());
out.put("live", LiveSummaryDto.builder().build());
}
LiveSummaryDto live = liveTradingService.getSummary(userId, deviceId);
out.put("live", live);
List<TradeSignalDto> recentSignals = signalRepo
.findByDeviceIdOrderByCreatedAtDesc(deviceId)
List<TradeSignalDto> recentSignals = (userId != null
? signalRepo.findByUserIdOrderByCreatedAtDesc(userId)
: signalRepo.findByDeviceIdOrderByCreatedAtDesc(deviceId != null ? deviceId : ""))
.stream().limit(8)
.map(s -> TradeSignalDto.builder()
.id(s.getId())
@@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.JsonNode;
import com.goldenchart.dto.BacktestSettingsDto;
import com.goldenchart.entity.GcAppSettings;
import com.goldenchart.repository.GcAppSettingsRepository;
import com.goldenchart.trading.TradingAccess;
import com.goldenchart.trading.TradingExecutionService;
import com.goldenchart.trading.TradingMode;
import com.goldenchart.trading.pipeline.OrderRequest;
@@ -50,8 +51,9 @@ public class LiveRiskMonitorService {
if (!Boolean.TRUE.equals(app.getLiveAutoTradeEnabled())) continue;
if (!liveTradingService.hasApiKeys(app)) continue;
if (!TradingMode.fromString(app.getTradingMode()).useLive()) continue;
String deviceId = app.getDeviceId();
if (deviceId == null || deviceId.isBlank()) continue;
Long userId = app.getUserId();
if (userId == null || userId <= 0) continue;
String cacheKey = TradingAccess.accountDeviceKey(userId);
try {
var creds = appSettingsService.resolveUpbitCredentials(app);
if (!creds.isComplete()) continue;
@@ -67,9 +69,9 @@ public class LiveRiskMonitorService {
coins.put(cur, new double[]{bal, avg});
}
}
accountCache.put(deviceId, coins);
accountCache.put(cacheKey, coins);
} catch (Exception e) {
log.debug("[LiveRisk] account sync {}: {}", deviceId, e.getMessage());
log.debug("[LiveRisk] account sync userId={}: {}", userId, e.getMessage());
}
}
}
@@ -79,40 +81,51 @@ public class LiveRiskMonitorService {
String currency = market.startsWith("KRW-") ? market.substring(4) : market;
for (var entry : accountCache.entrySet()) {
String deviceId = entry.getKey();
String cacheKey = entry.getKey();
double[] pos = entry.getValue().get(currency);
if (pos == null || pos[0] <= 0 || pos[1] <= 0) continue;
String key = deviceId + ":" + market;
String cooldownKey = cacheKey + ":" + market;
long now = System.currentTimeMillis();
Long last = exitCooldown.get(key);
Long last = exitCooldown.get(cooldownKey);
if (last != null && now - last < COOLDOWN_MS) continue;
GcAppSettings app = appSettingsRepo.findByDeviceId(deviceId).orElse(null);
Long userId = parseUserIdFromCacheKey(cacheKey);
if (userId == null) continue;
GcAppSettings app = appSettingsRepo.findByUserId(userId).orElse(null);
if (app == null) continue;
BacktestSettingsDto risk = backtestSettingsService.get(deviceId);
BacktestSettingsDto risk = backtestSettingsService.get(cacheKey);
double avg = pos[1];
double pnlPct = (tradePrice - avg) / avg * 100.0;
if (Boolean.TRUE.equals(risk.getStopLossEnabled())
&& risk.getStopLossPct() != null
&& pnlPct <= -risk.getStopLossPct().doubleValue()) {
exitCooldown.put(key, now);
exitCooldown.put(cooldownKey, now);
tradingExecutionService.executeRiskExit(
OrderRequest.stopLoss(deviceId, app.getUserId(), market, tradePrice,
OrderRequest.stopLoss(cacheKey, userId, market, tradePrice,
risk.getStopLossPct().doubleValue()));
return;
}
if (Boolean.TRUE.equals(risk.getTakeProfitEnabled())
&& risk.getTakeProfitPct() != null
&& pnlPct >= risk.getTakeProfitPct().doubleValue()) {
exitCooldown.put(key, now);
exitCooldown.put(cooldownKey, now);
tradingExecutionService.executeRiskExit(
OrderRequest.takeProfit(deviceId, app.getUserId(), market, tradePrice,
OrderRequest.takeProfit(cacheKey, userId, market, tradePrice,
risk.getTakeProfitPct().doubleValue()));
return;
}
}
}
private static Long parseUserIdFromCacheKey(String cacheKey) {
if (cacheKey == null || !cacheKey.startsWith("user:")) return null;
try {
return Long.parseLong(cacheKey.substring(5));
} catch (NumberFormatException e) {
return null;
}
}
}
@@ -8,6 +8,7 @@ import com.goldenchart.dto.UpbitApiCredentials;
import com.goldenchart.entity.GcAppSettings;
import com.goldenchart.entity.GcLiveTrade;
import com.goldenchart.repository.GcLiveTradeRepository;
import com.goldenchart.trading.TradingAccess;
import com.goldenchart.trading.TradingMode;
import com.goldenchart.upbit.UpbitOrderApiClient;
import lombok.RequiredArgsConstructor;
@@ -36,8 +37,9 @@ public class LiveTradingService {
private final GcLiveTradeRepository tradeRepo;
@Transactional(readOnly = true)
public LiveSummaryDto getSummary(Long userId, String deviceId) {
GcAppSettings app = appSettingsService.getEntity(userId, deviceId);
public LiveSummaryDto getSummary(Long userId) {
long uid = TradingAccess.requireUserId(userId);
GcAppSettings app = appSettingsService.getEntity(uid, null);
UpbitApiCredentials creds = appSettingsService.resolveUpbitCredentials(app);
boolean configured = creds.isComplete();
double krw = 0;
@@ -76,22 +78,27 @@ public class LiveTradingService {
}
@Transactional(readOnly = true)
public List<LiveTradeDto> listTrades(Long userId, String deviceId) {
String dev = resolveDeviceId(deviceId);
return tradeRepo.findTop100ByDeviceIdOrderByCreatedAtDesc(dev)
public List<LiveTradeDto> listTrades(Long userId) {
long uid = TradingAccess.requireUserId(userId);
return tradeRepo.findTop100ByUserIdOrderByCreatedAtDesc(uid)
.stream().map(this::toDto).toList();
}
/**
* 전략 시그널·SL/TP 에서 호출.
*/
public void tryExecuteOnSignal(String deviceId, Long userId, String market,
public void tryExecuteOnSignal(Long userId, String market,
Long strategyId, String signalType, double price,
String source) {
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);
TradingMode mode = TradingMode.fromString(app.getTradingMode());
if (!mode.useLive()) return;
if (!Boolean.TRUE.equals(app.getLiveAutoTradeEnabled())) return;
@@ -118,7 +125,7 @@ public class LiveTradingService {
return;
}
JsonNode res = upbitApi.placeMarketBuy(access, secret, market, budget);
recordTrade(deviceId, userId, market, "BUY", source, strategyId, res, price, budget);
recordTrade(uid, market, "BUY", source, strategyId, res, price, budget);
log.info("[LiveTrading] BUY {} ~{} KRW", market, (long) budget);
} else {
double vol = upbitApi.getCoinBalance(access, secret, market);
@@ -127,7 +134,7 @@ public class LiveTradingService {
return;
}
JsonNode res = upbitApi.placeMarketSell(access, secret, market, vol);
recordTrade(deviceId, userId, market, "SELL", source, strategyId, res, price, vol);
recordTrade(uid, market, "SELL", source, strategyId, res, price, vol);
log.info("[LiveTrading] SELL {} vol={}", market, vol);
}
} catch (UpbitOrderApiClient.UpbitApiException e) {
@@ -138,11 +145,12 @@ public class LiveTradingService {
/**
* 알림 모달·수동 주문 — 시장가 매수/매도.
*/
public LiveTradeDto placeManualOrder(Long userId, String deviceId, LiveOrderRequest req) {
public LiveTradeDto placeManualOrder(Long userId, LiveOrderRequest req) {
long uid = TradingAccess.requireUserId(userId);
if (req == null || req.getMarket() == null || req.getSide() == null) {
throw new IllegalArgumentException("market, side required");
}
GcAppSettings app = appSettingsService.getEntity(userId, deviceId);
GcAppSettings app = appSettingsService.getEntity(uid, null);
UpbitApiCredentials creds = appSettingsService.resolveUpbitCredentials(app);
if (!creds.isComplete()) {
throw new IllegalStateException("업비트 API 키가 등록되지 않았습니다.");
@@ -151,8 +159,6 @@ public class LiveTradingService {
String secret = creds.secretKey();
String market = req.getMarket();
String side = req.getSide().toUpperCase();
String dev = resolveDeviceId(deviceId);
try {
if ("BUY".equals(side)) {
double budget = req.getKrwAmount() != null && req.getKrwAmount() > 0
@@ -162,8 +168,8 @@ public class LiveTradingService {
throw new IllegalStateException("주문 가능 금액이 부족합니다.");
}
JsonNode res = upbitApi.placeMarketBuy(access, secret, market, budget);
recordTrade(dev, userId, market, "BUY", "MANUAL", req.getStrategyId(), res, 0, budget);
return listTrades(userId, deviceId).get(0);
recordTrade(uid, market, "BUY", "MANUAL", req.getStrategyId(), res, 0, budget);
return listTrades(uid).get(0);
}
if ("SELL".equals(side)) {
double vol = req.getQuantity() != null && req.getQuantity() > 0
@@ -171,8 +177,8 @@ public class LiveTradingService {
: upbitApi.getCoinBalance(access, secret, market);
if (vol <= 0) throw new IllegalStateException("매도 가능 수량이 없습니다.");
JsonNode res = upbitApi.placeMarketSell(access, secret, market, vol);
recordTrade(dev, userId, market, "SELL", "MANUAL", req.getStrategyId(), res, 0, vol);
return listTrades(userId, deviceId).get(0);
recordTrade(uid, market, "SELL", "MANUAL", req.getStrategyId(), res, 0, vol);
return listTrades(uid).get(0);
}
throw new IllegalArgumentException("side must be BUY or SELL");
} catch (UpbitOrderApiClient.UpbitApiException e) {
@@ -198,7 +204,7 @@ public class LiveTradingService {
return appSettingsService.hasUpbitApiKeys(app);
}
private void recordTrade(String deviceId, Long userId, String market, String side,
private void recordTrade(long userId, String market, String side,
String source, Long strategyId, JsonNode res,
double refPrice, double amountOrVol) {
String uuid = res != null ? res.path("uuid").asText(null) : null;
@@ -209,7 +215,7 @@ public class LiveTradingService {
: BigDecimal.valueOf(refPrice * executed).setScale(2, RM);
tradeRepo.save(GcLiveTrade.builder()
.deviceId(resolveDeviceId(deviceId))
.deviceId(TradingAccess.accountDeviceKey(userId))
.userId(userId)
.symbol(market)
.side(side)
@@ -239,7 +245,4 @@ public class LiveTradingService {
.build();
}
private static String resolveDeviceId(String deviceId) {
return deviceId != null && !deviceId.isBlank() ? deviceId : "anonymous";
}
}
@@ -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)