모의투자 로직 변경
This commit is contained in:
+3
-2
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user