package com.goldenchart.service; 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; import com.goldenchart.upbit.UpbitOrderApiClient; import jakarta.annotation.PostConstruct; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; /** * 실거래 포지션 틱 단위 손절/익절 — 계좌 스냅샷(30초) + 틱 비교. */ @Service @RequiredArgsConstructor @Slf4j public class LiveRiskMonitorService { private final GcAppSettingsRepository appSettingsRepo; private final AppSettingsService appSettingsService; private final BacktestSettingsService backtestSettingsService; private final UpbitOrderApiClient upbitApi; private final LiveTradingService liveTradingService; private final TradingExecutionService tradingExecutionService; private final Map exitCooldown = new ConcurrentHashMap<>(); /** deviceId → currency → {balance, avgPrice} */ private final Map> accountCache = new ConcurrentHashMap<>(); private static final long COOLDOWN_MS = 5_000; @PostConstruct void init() { refreshAccounts(); } @Scheduled(fixedDelay = 30_000) public void refreshAccounts() { for (GcAppSettings app : appSettingsRepo.findAll()) { if (!Boolean.TRUE.equals(app.getLiveAutoTradeEnabled())) continue; if (!liveTradingService.hasApiKeys(app)) continue; if (!TradingMode.fromString(app.getTradingMode()).useLive()) 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; JsonNode accounts = upbitApi.getAccounts( creds.accessKey(), creds.secretKey()); Map coins = new ConcurrentHashMap<>(); if (accounts != null && accounts.isArray()) { for (JsonNode a : accounts) { String cur = a.path("currency").asText(); double bal = a.path("balance").asDouble(0); if (bal <= 0 || "KRW".equals(cur)) continue; double avg = a.path("avg_buy_price").asDouble(0); coins.put(cur, new double[]{bal, avg}); } } accountCache.put(cacheKey, coins); } catch (Exception e) { log.debug("[LiveRisk] account sync userId={}: {}", userId, e.getMessage()); } } } public void onTick(String market, double tradePrice) { if (tradePrice <= 0) return; String currency = market.startsWith("KRW-") ? market.substring(4) : market; for (var entry : accountCache.entrySet()) { String cacheKey = entry.getKey(); double[] pos = entry.getValue().get(currency); if (pos == null || pos[0] <= 0 || pos[1] <= 0) continue; String cooldownKey = cacheKey + ":" + market; long now = System.currentTimeMillis(); Long last = exitCooldown.get(cooldownKey); if (last != null && now - last < COOLDOWN_MS) continue; Long userId = parseUserIdFromCacheKey(cacheKey); if (userId == null) continue; GcAppSettings app = appSettingsRepo.findByUserId(userId).orElse(null); if (app == null) continue; 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(cooldownKey, now); tradingExecutionService.executeRiskExit( 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(cooldownKey, now); tradingExecutionService.executeRiskExit( 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; } } }