Files
goldenChart/backend/src/main/java/com/goldenchart/service/TradeSignalService.java
T
2026-05-29 20:42:35 +09:00

150 lines
6.7 KiB
Java

package com.goldenchart.service;
import com.goldenchart.dto.TradeSignalDto;
import com.goldenchart.entity.GcTradeSignal;
import com.goldenchart.repository.GcStrategyRepository;
import com.goldenchart.repository.GcTradeSignalRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
/**
* 매매 시그널 이력 서비스.
* 실시간 전략 체크(LiveStrategyEvaluator / LiveStrategyScheduler)에서
* BUY/SELL 시그널이 발생하면 이 서비스를 통해 DB에 저장한다.
*/
@Service
@RequiredArgsConstructor
@Slf4j
@Transactional
public class TradeSignalService {
private final GcTradeSignalRepository repo;
private final GcStrategyRepository strategyRepo;
private final FcmPushService fcmPushService;
// ── 저장 ─────────────────────────────────────────────────────────────────
public TradeSignalDto save(String deviceId, Long userId, String market,
Long strategyId, String strategyName,
String signalType, double price,
long candleTime, String candleType,
String executionType) {
String resolvedName = resolveStrategyName(strategyId, strategyName);
GcTradeSignal entity = GcTradeSignal.builder()
.deviceId(deviceId)
.userId(userId)
.market(market)
.strategyId(strategyId)
.strategyName(resolvedName)
.signalType(signalType)
.price(price)
.candleTime(candleTime)
.candleType(candleType)
.executionType(executionType)
.build();
GcTradeSignal saved = repo.save(entity);
log.info("[TradeSignal] saved {} {} @ {} market={} userId={} deviceId={}",
signalType, candleType, price, market, userId, deviceId);
// 로그인 사용자는 live 설정에 userId만 있고 deviceId가 null — FCM은 userId 기준으로 발송
if (userId != null || (deviceId != null && !deviceId.isBlank())) {
fcmPushService.sendTradeSignalIfEnabled(
deviceId, userId, market, signalType, price, resolvedName, saved.getId());
}
return toDto(saved);
}
// ── 조회 ─────────────────────────────────────────────────────────────────
@Transactional(readOnly = true)
public List<TradeSignalDto> list(Long userId, String deviceId) {
List<GcTradeSignal> list = userId != null
? repo.findByUserIdOrderByCreatedAtDesc(userId)
: repo.findByDeviceIdOrderByCreatedAtDesc(deviceId != null ? deviceId : "");
return list.stream().map(this::toDto).toList();
}
@Transactional(readOnly = true)
public List<TradeSignalDto> listByMarket(Long userId, String deviceId, String market) {
List<GcTradeSignal> list = userId != null
? repo.findByUserIdAndMarketOrderByCreatedAtDesc(userId, market)
: repo.findByDeviceIdAndMarketOrderByCreatedAtDesc(
deviceId != null ? deviceId : "", market);
return list.stream().map(this::toDto).toList();
}
// ── 삭제 ─────────────────────────────────────────────────────────────────
public void deleteOne(Long id, Long userId, String deviceId) {
if (id == null) return;
repo.findById(id).ifPresent(entity -> {
if (!owns(entity, userId, deviceId)) return;
repo.delete(entity);
log.info("[TradeSignal] deleted id={} market={}", id, entity.getMarket());
});
}
public int deleteBatch(List<Long> ids, Long userId, String deviceId) {
if (ids == null || ids.isEmpty()) return 0;
List<GcTradeSignal> toDelete = ids.stream()
.distinct()
.map(repo::findById)
.filter(java.util.Optional::isPresent)
.map(java.util.Optional::get)
.filter(e -> owns(e, userId, deviceId))
.toList();
if (toDelete.isEmpty()) return 0;
repo.deleteAll(toDelete);
log.info("[TradeSignal] batch deleted count={}", toDelete.size());
return toDelete.size();
}
public int deleteAll(Long userId, String deviceId) {
if (userId != null) {
long count = repo.findByUserIdOrderByCreatedAtDesc(userId).size();
repo.deleteByUserId(userId);
log.info("[TradeSignal] deleted all for userId={} count≈{}", userId, count);
return (int) count;
}
String dev = deviceId != null ? deviceId : "";
long count = repo.findByDeviceIdOrderByCreatedAtDesc(dev).size();
repo.deleteByDeviceId(dev);
log.info("[TradeSignal] deleted all for deviceId={} count≈{}", dev, count);
return (int) count;
}
private static boolean owns(GcTradeSignal entity, Long userId, String deviceId) {
if (userId != null && userId.equals(entity.getUserId())) return true;
if (deviceId == null || deviceId.isBlank()) return false;
return deviceId.equals(entity.getDeviceId());
}
private String resolveStrategyName(Long strategyId, String strategyName) {
if (strategyName != null && !strategyName.isBlank()) return strategyName;
if (strategyId == null) return null;
return strategyRepo.findById(strategyId)
.map(s -> s.getName())
.orElse(null);
}
// ── 변환 ─────────────────────────────────────────────────────────────────
private TradeSignalDto toDto(GcTradeSignal e) {
return TradeSignalDto.builder()
.id(e.getId())
.market(e.getMarket())
.strategyId(e.getStrategyId())
.strategyName(e.getStrategyName())
.signalType(e.getSignalType())
.price(e.getPrice())
.candleTime(e.getCandleTime())
.candleType(e.getCandleType())
.executionType(e.getExecutionType())
.createdAt(e.getCreatedAt() != null ? e.getCreatedAt().toString() : null)
.build();
}
}