설정 db 저장

This commit is contained in:
Macbook
2026-06-23 01:09:42 +09:00
parent 32be015b49
commit 2d465cfc26
14 changed files with 192 additions and 37 deletions
@@ -16,14 +16,25 @@ public class BacktestSettingsController {
@GetMapping
public ResponseEntity<BacktestSettingsDto> get(
@RequestHeader(value = "X-User-Id", required = false) String userIdHeader,
@RequestHeader(value = "X-Device-Id", defaultValue = "default") String deviceId) {
return ResponseEntity.ok(service.get(deviceId));
return ResponseEntity.ok(service.get(parseUserId(userIdHeader), deviceId));
}
@PostMapping
public ResponseEntity<BacktestSettingsDto> save(
@RequestHeader(value = "X-User-Id", required = false) String userIdHeader,
@RequestHeader(value = "X-Device-Id", defaultValue = "default") String deviceId,
@RequestBody BacktestSettingsDto dto) {
return ResponseEntity.ok(service.save(deviceId, dto));
return ResponseEntity.ok(service.save(parseUserId(userIdHeader), deviceId, dto));
}
private Long parseUserId(String header) {
if (header == null || header.isBlank()) return null;
try {
return Long.parseLong(header);
} catch (NumberFormatException e) {
return null;
}
}
}
@@ -22,6 +22,9 @@ public class GcBacktestSettings {
@Column(name = "device_id", length = 100)
private String deviceId;
@Column(name = "user_id")
private Long userId;
// ── 자본 설정 ──────────────────────────────────────────────────────────────
@Column(name = "initial_capital", nullable = false, precision = 20, scale = 2)
@Builder.Default
@@ -7,4 +7,6 @@ import java.util.Optional;
public interface GcBacktestSettingsRepository extends JpaRepository<GcBacktestSettings, Long> {
Optional<GcBacktestSettings> findFirstByDeviceIdOrderByUpdatedAtDesc(String deviceId);
Optional<GcBacktestSettings> findFirstByUserIdOrderByUpdatedAtDesc(Long userId);
}
@@ -3,32 +3,99 @@ package com.goldenchart.service;
import com.goldenchart.dto.BacktestSettingsDto;
import com.goldenchart.entity.GcBacktestSettings;
import com.goldenchart.repository.GcBacktestSettingsRepository;
import com.goldenchart.trading.TradingAccess;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Optional;
@Service
@RequiredArgsConstructor
public class BacktestSettingsService {
private final GcBacktestSettingsRepository repo;
/** device_id 기준 설정 조회 — 없으면 기본값 반환 */
/** userId 우선, 없으면 deviceId — 레거시 user:{id}·기기별 행 자동 승격 */
@Transactional(readOnly = true)
public BacktestSettingsDto get(String deviceId) {
return repo.findFirstByDeviceIdOrderByUpdatedAtDesc(deviceId)
public BacktestSettingsDto get(Long userId, String deviceId) {
return findEntity(userId, deviceId)
.map(this::toDto)
.orElseGet(BacktestSettingsDto::new);
}
/** upsert — 기존 설정이 있으면 덮어쓰기, 없으면 새 행 삽입 */
@Transactional
public BacktestSettingsDto save(String deviceId, BacktestSettingsDto dto) {
GcBacktestSettings entity = repo
.findFirstByDeviceIdOrderByUpdatedAtDesc(deviceId)
.orElseGet(GcBacktestSettings::new);
/**
* 레거시 호출 — scopeKey가 {@code user:{userId}} 이면 userId 조회로 위임.
*/
@Transactional(readOnly = true)
public BacktestSettingsDto get(String scopeKey) {
if (scopeKey != null && scopeKey.startsWith("user:")) {
try {
long uid = Long.parseLong(scopeKey.substring(5));
return get(uid, null);
} catch (NumberFormatException ignored) {
/* fall through */
}
}
return get(null, scopeKey);
}
entity.setDeviceId(deviceId);
@Transactional
public BacktestSettingsDto save(Long userId, String deviceId, BacktestSettingsDto dto) {
GcBacktestSettings entity = findOrCreate(userId, deviceId);
applyFields(entity, dto, userId, deviceId);
return toDto(repo.save(entity));
}
// ── 조회·생성 ─────────────────────────────────────────────────────────────
private Optional<GcBacktestSettings> findEntity(Long userId, String deviceId) {
if (userId != null && userId > 0) {
Optional<GcBacktestSettings> byUser = repo.findFirstByUserIdOrderByUpdatedAtDesc(userId);
if (byUser.isPresent()) return byUser;
if (deviceId != null && !deviceId.isBlank()) {
Optional<GcBacktestSettings> byDevice = repo.findFirstByDeviceIdOrderByUpdatedAtDesc(deviceId);
if (byDevice.isPresent()) {
return Optional.of(promoteToUser(byDevice.get(), userId));
}
}
Optional<GcBacktestSettings> legacy = repo.findFirstByDeviceIdOrderByUpdatedAtDesc(
TradingAccess.accountDeviceKey(userId));
if (legacy.isPresent()) {
return Optional.of(promoteToUser(legacy.get(), userId));
}
return Optional.empty();
}
if (deviceId != null && !deviceId.isBlank()) {
return repo.findFirstByDeviceIdOrderByUpdatedAtDesc(deviceId);
}
return Optional.empty();
}
private GcBacktestSettings findOrCreate(Long userId, String deviceId) {
return findEntity(userId, deviceId)
.orElseGet(() -> GcBacktestSettings.builder()
.userId(userId != null && userId > 0 ? userId : null)
.deviceId(deviceId)
.build());
}
/** 기기별 행을 로그인 사용자 소유로 1회 승격 (다른 PC와 공유) */
private GcBacktestSettings promoteToUser(GcBacktestSettings entity, Long userId) {
entity.setUserId(userId);
return repo.save(entity);
}
private void applyFields(GcBacktestSettings entity, BacktestSettingsDto dto,
Long userId, String deviceId) {
if (userId != null && userId > 0) {
entity.setUserId(userId);
}
if (deviceId != null && !deviceId.isBlank()) {
entity.setDeviceId(deviceId);
}
entity.setInitialCapital(dto.getInitialCapital());
entity.setCommissionType(dto.getCommissionType());
entity.setCommissionRate(dto.getCommissionRate());
@@ -58,8 +125,6 @@ public class BacktestSettingsService {
"SCAN_SIGNALS".equals(dto.getTradeExecutionMode())
? "SCAN_SIGNALS"
: "BACKTEST_ENGINE");
return toDto(repo.save(entity));
}
// ── 변환 헬퍼 ─────────────────────────────────────────────────────────────
@@ -95,7 +95,7 @@ public class LiveRiskMonitorService {
GcAppSettings app = appSettingsRepo.findByUserId(userId).orElse(null);
if (app == null) continue;
BacktestSettingsDto risk = backtestSettingsService.get(cacheKey);
BacktestSettingsDto risk = backtestSettingsService.get(userId, null);
double avg = pos[1];
double pnlPct = (tradePrice - avg) / avg * 100.0;
@@ -389,11 +389,8 @@ public class LiveStrategyEvaluator {
if (!globalLive && Boolean.TRUE.equals(s.getIsLiveCheck())) {
return s.getPositionMode() != null ? s.getPositionMode() : "LONG_ONLY";
}
String deviceKey = s.getUserId() != null
? TradingAccess.accountDeviceKey(s.getUserId())
: s.getDeviceId();
if (deviceKey != null && !deviceKey.isBlank()) {
BacktestSettingsDto bt = backtestSettingsService.get(deviceKey);
if (s.getUserId() != null || (s.getDeviceId() != null && !s.getDeviceId().isBlank())) {
BacktestSettingsDto bt = backtestSettingsService.get(s.getUserId(), s.getDeviceId());
if (bt.getPositionMode() != null) {
return "SIGNAL_ONLY".equals(bt.getPositionMode()) ? "SIGNAL_ONLY" : "LONG_ONLY";
}
@@ -75,7 +75,7 @@ public class RealtimePositionMonitor {
double avg = pos.getAvgPrice().doubleValue();
if (avg <= 0) continue;
BacktestSettingsDto risk = backtestSettingsService.get(TradingAccess.accountDeviceKey(userId));
BacktestSettingsDto risk = backtestSettingsService.get(userId, null);
double pnlPct = (tradePrice - avg) / avg * 100.0;
if (Boolean.TRUE.equals(risk.getStopLossEnabled())