132 lines
5.5 KiB
Java
132 lines
5.5 KiB
Java
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<String, Long> exitCooldown = new ConcurrentHashMap<>();
|
|
/** deviceId → currency → {balance, avgPrice} */
|
|
private final Map<String, Map<String, double[]>> 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<String, double[]> 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;
|
|
}
|
|
}
|
|
}
|