package com.goldenchart.service; import com.goldenchart.dto.PaperDailySnapshotDto; import com.goldenchart.entity.GcPaperAccount; import com.goldenchart.entity.GcPaperDailySnapshot; import com.goldenchart.entity.GcPaperPosition; import com.goldenchart.repository.GcPaperAccountRepository; import com.goldenchart.repository.GcPaperDailySnapshotRepository; import com.goldenchart.repository.GcPaperPositionRepository; import com.goldenchart.storage.Ta4jStorage; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.ta4j.core.Bar; import org.ta4j.core.BarSeries; import java.math.BigDecimal; import java.math.RoundingMode; import java.time.LocalDate; import java.time.ZoneId; import java.util.HashMap; import java.util.List; import java.util.Map; @Service @RequiredArgsConstructor @Slf4j @Transactional public class PaperSnapshotService { private static final ZoneId KST = ZoneId.of("Asia/Seoul"); private final GcPaperDailySnapshotRepository snapshotRepo; private final GcPaperAccountRepository accountRepo; private final GcPaperPositionRepository positionRepo; private final Ta4jStorage ta4jStorage; @Transactional(readOnly = true) public List list(Long accountId, LocalDate from, LocalDate to) { LocalDate f = from != null ? from : LocalDate.now(KST).minusDays(90); LocalDate t = to != null ? to : LocalDate.now(KST); return snapshotRepo.findByAccountIdAndSnapshotDateBetweenOrderBySnapshotDateAsc(accountId, f, t) .stream().map(this::toDto).toList(); } public void captureEodForAllAccounts() { LocalDate today = LocalDate.now(KST); for (GcPaperAccount account : accountRepo.findAll()) { try { captureEod(account, today); } catch (Exception e) { log.warn("[PaperSnapshot] account {} failed: {}", account.getId(), e.getMessage()); } } } public void captureEod(GcPaperAccount account, LocalDate date) { List positions = positionRepo.findByAccountIdOrderBySymbolAsc(account.getId()); Map marks = new HashMap<>(); double stockEval = 0; for (GcPaperPosition p : positions) { double qty = p.getQuantity().doubleValue(); if (qty <= 0) continue; double mark = resolveMark(p.getSymbol(), p.getAvgPrice().doubleValue()); marks.put(p.getSymbol(), mark); stockEval += mark * qty; } double cash = account.getCashBalance().doubleValue(); double total = cash + stockEval; double initial = account.getInitialCapitalSnapshot() != null ? account.getInitialCapitalSnapshot().doubleValue() : cash; double cumRet = initial > 0 ? (total - initial) / initial * 100 : 0; GcPaperDailySnapshot prev = snapshotRepo .findByAccountIdAndSnapshotDate(account.getId(), date.minusDays(1)) .orElse(null); double prevTotal = prev != null ? prev.getTotalAsset().doubleValue() : initial; double dailyPnl = total - prevTotal; double dailyRet = prevTotal > 0 ? dailyPnl / prevTotal * 100 : 0; GcPaperDailySnapshot snap = snapshotRepo.findByAccountIdAndSnapshotDate(account.getId(), date) .orElse(GcPaperDailySnapshot.builder() .accountId(account.getId()) .snapshotDate(date) .build()); snap.setCashBalance(BigDecimal.valueOf(cash).setScale(2, RoundingMode.HALF_UP)); snap.setStockEvalAmount(BigDecimal.valueOf(stockEval).setScale(2, RoundingMode.HALF_UP)); snap.setTotalAsset(BigDecimal.valueOf(total).setScale(2, RoundingMode.HALF_UP)); snap.setDailyPnl(BigDecimal.valueOf(dailyPnl).setScale(2, RoundingMode.HALF_UP)); snap.setDailyReturnPct(BigDecimal.valueOf(dailyRet).setScale(4, RoundingMode.HALF_UP)); snap.setCumulativeReturnPct(BigDecimal.valueOf(cumRet).setScale(4, RoundingMode.HALF_UP)); snapshotRepo.save(snap); } private double resolveMark(String symbol, double fallback) { try { if (!ta4jStorage.exists(symbol, "1m")) return fallback; BarSeries series = ta4jStorage.getOrCreate(symbol, "1m"); if (!series.isEmpty()) { Bar last = series.getLastBar(); return last.getClosePrice().doubleValue(); } } catch (Exception ignored) { } return fallback; } private PaperDailySnapshotDto toDto(GcPaperDailySnapshot s) { return PaperDailySnapshotDto.builder() .snapshotDate(s.getSnapshotDate().toString()) .cashBalance(s.getCashBalance().doubleValue()) .stockEvalAmount(s.getStockEvalAmount().doubleValue()) .totalAsset(s.getTotalAsset().doubleValue()) .dailyPnl(s.getDailyPnl().doubleValue()) .dailyReturnPct(s.getDailyReturnPct().doubleValue()) .cumulativeReturnPct(s.getCumulativeReturnPct().doubleValue()) .build(); } }