299 lines
14 KiB
Java
299 lines
14 KiB
Java
package com.goldenchart.service;
|
|
|
|
import com.goldenchart.dto.LiveStrategyBulkRequest;
|
|
import com.goldenchart.dto.LiveStrategySettingsDto;
|
|
import com.goldenchart.entity.GcAppSettings;
|
|
import com.goldenchart.entity.GcLiveStrategySettings;
|
|
import com.goldenchart.entity.GcWatchlist;
|
|
import com.goldenchart.repository.GcLiveStrategySettingsRepository;
|
|
import com.goldenchart.repository.GcWatchlistRepository;
|
|
import com.goldenchart.websocket.DynamicSubscriptionManager;
|
|
import lombok.RequiredArgsConstructor;
|
|
import lombok.extern.slf4j.Slf4j;
|
|
import org.springframework.stereotype.Service;
|
|
import org.springframework.transaction.annotation.Transactional;
|
|
|
|
import java.util.ArrayList;
|
|
import java.util.HashMap;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.Optional;
|
|
|
|
@Service
|
|
@RequiredArgsConstructor
|
|
@Slf4j
|
|
@Transactional
|
|
public class LiveStrategySettingsService {
|
|
|
|
private final GcLiveStrategySettingsRepository repo;
|
|
private final GcWatchlistRepository watchlistRepo;
|
|
private final AppSettingsService appSettingsService;
|
|
private final DynamicSubscriptionManager subscriptionManager;
|
|
private final LiveStrategyEvaluator liveStrategyEvaluator;
|
|
private final StrategyConditionTimeframeService conditionTimeframes;
|
|
|
|
// ── 조회 ──────────────────────────────────────────────────────────────────
|
|
|
|
@Transactional(readOnly = true)
|
|
public LiveStrategySettingsDto get(Long userId, String deviceId, String market) {
|
|
GcAppSettings app = appSettingsService.getEntity(userId, deviceId);
|
|
Optional<GcLiveStrategySettings> entity = findEntity(userId, deviceId, market);
|
|
LiveStrategySettingsDto dto = entity.isPresent()
|
|
? toDto(entity.get())
|
|
: defaultDtoFromApp(market, app);
|
|
// 실시간 체크 마스터 스위치·전략 ID는 앱 전역 설정(gc_app_settings) 기준
|
|
dto = mergeGlobalTemplate(dto, app, market);
|
|
enrichStrategyTimeframes(dto);
|
|
return dto;
|
|
}
|
|
|
|
@Transactional(readOnly = true)
|
|
public List<LiveStrategySettingsDto> listActive(Long userId, String deviceId) {
|
|
GcAppSettings app = appSettingsService.getEntity(userId, deviceId);
|
|
if (!Boolean.TRUE.equals(app.getLiveStrategyCheck()) || app.getLiveStrategyId() == null) {
|
|
return List.of();
|
|
}
|
|
List<String> watchlist = listWatchlistSymbols(userId, deviceId);
|
|
Long strategyId = app.getLiveStrategyId();
|
|
List<String> strategyCandleTypes = conditionTimeframes.collectForStrategyList(strategyId);
|
|
return watchlist.stream()
|
|
.map(m -> LiveStrategySettingsDto.builder()
|
|
.market(m)
|
|
.strategyId(strategyId)
|
|
.liveCheck(true)
|
|
.executionType(app.getLiveExecutionType())
|
|
.positionMode(app.getLivePositionMode())
|
|
.strategyCandleTypes(strategyCandleTypes)
|
|
.build())
|
|
.toList();
|
|
}
|
|
|
|
// ── 관심종목 연동 (WatchlistService 에서 호출) ─────────────────────────────
|
|
|
|
/** 관심종목 등록 시 — DB 관심종목 = 전략 체크 대상 행 생성/갱신 */
|
|
public void enableForWatchlistMarket(Long userId, String deviceId, String market) {
|
|
GcAppSettings app = appSettingsService.getEntity(userId, deviceId);
|
|
LiveStrategySettingsDto template = findEntity(userId, deviceId, market)
|
|
.map(e -> mergeGlobalTemplate(toDto(e), app, market))
|
|
.orElseGet(() -> mergeGlobalTemplate(defaultDtoFromApp(market, app), app, market));
|
|
template.setLiveCheck(true);
|
|
enrichStrategyTimeframes(template);
|
|
upsertEntity(userId, deviceId, template);
|
|
pinIfReady(market, template);
|
|
log.debug("[LiveStrategy] watchlist add → enabled {}", market);
|
|
}
|
|
|
|
/** 관심종목 해제 시 — 해당 종목 전략 체크 비활성화 */
|
|
public void disableForWatchlistMarket(Long userId, String deviceId, String market) {
|
|
findEntity(userId, deviceId, market).ifPresent(entity -> {
|
|
entity.setIsLiveCheck(false);
|
|
repo.save(entity);
|
|
liveStrategyEvaluator.invalidateCache(market);
|
|
});
|
|
log.debug("[LiveStrategy] watchlist remove → disabled {}", market);
|
|
}
|
|
|
|
// ── 일괄 저장 (레거시 API) ─────────────────────────────────────────────────
|
|
|
|
public List<LiveStrategySettingsDto> saveBulk(Long userId, String deviceId,
|
|
LiveStrategyBulkRequest req) {
|
|
if (req.getMarkets() == null || req.getMarkets().isEmpty()) {
|
|
return List.of();
|
|
}
|
|
LiveStrategySettingsDto template = req.getTemplate() != null
|
|
? req.getTemplate()
|
|
: defaultDtoFromApp("KRW-BTC", appSettingsService.getEntity(userId, deviceId));
|
|
persistGlobalTemplate(userId, deviceId, template);
|
|
List<LiveStrategySettingsDto> results = new ArrayList<>();
|
|
for (String market : req.getMarkets()) {
|
|
if (market == null || market.isBlank()) continue;
|
|
LiveStrategySettingsDto one = LiveStrategySettingsDto.builder()
|
|
.market(market)
|
|
.strategyId(template.getStrategyId())
|
|
.liveCheck(true)
|
|
.executionType(template.getExecutionType())
|
|
.positionMode(template.getPositionMode())
|
|
.build();
|
|
results.add(upsertEntity(userId, deviceId, one));
|
|
}
|
|
log.info("[LiveStrategySettings] bulk save: {} markets", results.size());
|
|
return results;
|
|
}
|
|
|
|
// ── 저장/갱신 — 전역 템플릿 + 관심종목 전체 동기화 ─────────────────────────
|
|
|
|
public LiveStrategySettingsDto save(Long userId, String deviceId,
|
|
LiveStrategySettingsDto dto) {
|
|
persistGlobalTemplate(userId, deviceId, dto);
|
|
syncAllWatchlistMarkets(userId, deviceId, dto);
|
|
String market = dto.getMarket() != null ? dto.getMarket() : "KRW-BTC";
|
|
log.info("[LiveStrategySettings] global saved + watchlist sync, template market={}", market);
|
|
GcAppSettings app = appSettingsService.getEntity(userId, deviceId);
|
|
LiveStrategySettingsDto out = findEntity(userId, deviceId, market)
|
|
.map(this::toDto)
|
|
.orElseGet(() -> defaultDtoFromApp(market, app));
|
|
out = mergeGlobalTemplate(out, app, market);
|
|
if (dto.getExecutionType() != null) {
|
|
out.setExecutionType(dto.getExecutionType());
|
|
}
|
|
if (dto.getPositionMode() != null) {
|
|
out.setPositionMode(dto.getPositionMode());
|
|
}
|
|
enrichStrategyTimeframes(out);
|
|
return out;
|
|
}
|
|
|
|
// ── Private ───────────────────────────────────────────────────────────────
|
|
|
|
private void persistGlobalTemplate(Long userId, String deviceId, LiveStrategySettingsDto dto) {
|
|
Map<String, Object> patch = new HashMap<>();
|
|
patch.put("liveStrategyCheck", dto.isLiveCheck());
|
|
patch.put("liveStrategyId", dto.getStrategyId());
|
|
patch.put("liveExecutionType", dto.getExecutionType());
|
|
patch.put("livePositionMode", dto.getPositionMode());
|
|
appSettingsService.save(userId, deviceId, patch);
|
|
}
|
|
|
|
private void syncAllWatchlistMarkets(Long userId, String deviceId,
|
|
LiveStrategySettingsDto template) {
|
|
GcAppSettings app = appSettingsService.getEntity(userId, deviceId);
|
|
boolean active = Boolean.TRUE.equals(app.getLiveStrategyCheck())
|
|
&& app.getLiveStrategyId() != null;
|
|
for (String symbol : listWatchlistSymbols(userId, deviceId)) {
|
|
LiveStrategySettingsDto row = findEntity(userId, deviceId, symbol)
|
|
.map(e -> mergeGlobalTemplate(toDto(e), app, symbol))
|
|
.orElseGet(() -> defaultDtoFromApp(symbol, app));
|
|
row.setLiveCheck(active);
|
|
row.setStrategyId(app.getLiveStrategyId());
|
|
row.setExecutionType(app.getLiveExecutionType() != null
|
|
? app.getLiveExecutionType() : row.getExecutionType());
|
|
row.setPositionMode(app.getLivePositionMode() != null
|
|
? app.getLivePositionMode() : row.getPositionMode());
|
|
enrichStrategyTimeframes(row);
|
|
LiveStrategySettingsDto saved = upsertEntity(userId, deviceId, row);
|
|
if (active) {
|
|
pinIfReady(symbol, saved);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void enrichStrategyTimeframes(LiveStrategySettingsDto dto) {
|
|
if (dto.getStrategyId() != null) {
|
|
dto.setStrategyCandleTypes(
|
|
conditionTimeframes.collectForStrategyList(dto.getStrategyId()));
|
|
}
|
|
}
|
|
|
|
private void pinAllStrategyTimeframes(String market, Long strategyId) {
|
|
if (strategyId == null) return;
|
|
subscriptionManager.ensureMarketPinned(market, "1m");
|
|
for (String ct : conditionTimeframes.collectForStrategy(strategyId)) {
|
|
if (!"1m".equals(ct)) {
|
|
subscriptionManager.ensureMarketPinned(market, ct);
|
|
}
|
|
}
|
|
}
|
|
|
|
private LiveStrategySettingsDto upsertEntity(Long userId, String deviceId,
|
|
LiveStrategySettingsDto dto) {
|
|
String market = dto.getMarket() != null ? dto.getMarket() : "KRW-BTC";
|
|
GcLiveStrategySettings entity = findEntity(userId, deviceId, market)
|
|
.orElseGet(() -> GcLiveStrategySettings.builder()
|
|
.userId(userId)
|
|
.deviceId(userId == null ? deviceId : null)
|
|
.market(market)
|
|
.build());
|
|
|
|
applyOwnerKeys(entity, userId, deviceId);
|
|
|
|
entity.setStrategyId(dto.getStrategyId());
|
|
entity.setIsLiveCheck(dto.isLiveCheck());
|
|
entity.setExecutionType(
|
|
"REALTIME_TICK".equals(dto.getExecutionType()) ? "REALTIME_TICK" : "CANDLE_CLOSE");
|
|
entity.setPositionMode(
|
|
"SIGNAL_ONLY".equals(dto.getPositionMode()) ? "SIGNAL_ONLY" : "LONG_ONLY");
|
|
|
|
repo.save(entity);
|
|
liveStrategyEvaluator.invalidateCache(market);
|
|
return toDto(entity);
|
|
}
|
|
|
|
private void pinIfReady(String market, LiveStrategySettingsDto dto) {
|
|
if (Boolean.TRUE.equals(dto.isLiveCheck()) && dto.getStrategyId() != null) {
|
|
pinAllStrategyTimeframes(market, dto.getStrategyId());
|
|
}
|
|
}
|
|
|
|
private List<String> listWatchlistSymbols(Long userId, String deviceId) {
|
|
List<GcWatchlist> list = userId != null
|
|
? watchlistRepo.findByUserIdOrderByDisplayOrderAsc(userId)
|
|
: watchlistRepo.findByDeviceIdOrderByDisplayOrderAsc(
|
|
deviceId != null ? deviceId : "anonymous");
|
|
return list.stream().map(GcWatchlist::getSymbol).toList();
|
|
}
|
|
|
|
/**
|
|
* user_id·device_id 어느 쪽으로 저장됐든 동일 마켓 행을 찾는다.
|
|
* (게스트 → 로그인 전환 시 device_id 행만 있어도 upsert 가 INSERT 로 중복 나지 않게)
|
|
*/
|
|
private Optional<GcLiveStrategySettings> findEntity(Long userId, String deviceId,
|
|
String market) {
|
|
if (userId != null) {
|
|
Optional<GcLiveStrategySettings> byUser = repo.findByUserIdAndMarket(userId, market);
|
|
if (byUser.isPresent()) return byUser;
|
|
}
|
|
if (deviceId != null && !deviceId.isBlank()) {
|
|
return repo.findByDeviceIdAndMarket(deviceId, market);
|
|
}
|
|
return Optional.empty();
|
|
}
|
|
|
|
/** 회원: user_id 전용 행 / 비회원: device_id 전용 행 (UK 충돌 방지) */
|
|
private void applyOwnerKeys(GcLiveStrategySettings entity, Long userId, String deviceId) {
|
|
if (userId != null) {
|
|
entity.setUserId(userId);
|
|
entity.setDeviceId(null);
|
|
} else {
|
|
entity.setUserId(null);
|
|
entity.setDeviceId(deviceId != null && !deviceId.isBlank() ? deviceId : null);
|
|
}
|
|
}
|
|
|
|
private LiveStrategySettingsDto toDto(GcLiveStrategySettings e) {
|
|
LiveStrategySettingsDto dto = LiveStrategySettingsDto.builder()
|
|
.market(e.getMarket())
|
|
.strategyId(e.getStrategyId())
|
|
.liveCheck(Boolean.TRUE.equals(e.getIsLiveCheck()))
|
|
.executionType(e.getExecutionType())
|
|
.positionMode(e.getPositionMode() != null ? e.getPositionMode() : "LONG_ONLY")
|
|
.build();
|
|
enrichStrategyTimeframes(dto);
|
|
return dto;
|
|
}
|
|
|
|
/** 앱 전역 실시간 체크 ON/OFF·전략 ID를 DTO에 반영 (종목별 행과 분리) */
|
|
private LiveStrategySettingsDto mergeGlobalTemplate(
|
|
LiveStrategySettingsDto dto, GcAppSettings app, String market) {
|
|
dto.setMarket(market);
|
|
dto.setLiveCheck(Boolean.TRUE.equals(app.getLiveStrategyCheck()));
|
|
dto.setStrategyId(app.getLiveStrategyId());
|
|
if (app.getLiveExecutionType() != null) {
|
|
dto.setExecutionType(app.getLiveExecutionType());
|
|
}
|
|
if (app.getLivePositionMode() != null) {
|
|
dto.setPositionMode(app.getLivePositionMode());
|
|
}
|
|
return dto;
|
|
}
|
|
|
|
private LiveStrategySettingsDto defaultDtoFromApp(String market, GcAppSettings app) {
|
|
return LiveStrategySettingsDto.builder()
|
|
.market(market)
|
|
.strategyId(app.getLiveStrategyId())
|
|
.liveCheck(Boolean.TRUE.equals(app.getLiveStrategyCheck()))
|
|
.executionType(app.getLiveExecutionType() != null ? app.getLiveExecutionType() : "CANDLE_CLOSE")
|
|
.positionMode(app.getLivePositionMode() != null ? app.getLivePositionMode() : "LONG_ONLY")
|
|
.build();
|
|
}
|
|
}
|