atrade/src/main/kotlin/report/LocalReportGenerator.kt

166 lines
9.1 KiB
Kotlin
Raw Normal View History

2026-04-16 15:48:23 +09:00
package report
import java.awt.Desktop
import java.io.File
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
object LocalReportGenerator {
// 💡 모든 거래 내역의 상세 지표를 담는 DTO
data class TradeDetailData(
val isBuy: Boolean, // 매수/매도 구분
val stockName: String,
val orderTime: String, // 주문 시각
val timeTaken: String, // 처리 완료 총 시간 또는 미완료 상태
val execQty: Int, // 처리량 (체결 수량)
val avgPrice: Long, // 체결 평균 단가
val profitRate: Double, // 수익률 (매도 시에만 유효)
val profitAmount: Long, // 수익금액 (매도 시에만 유효)
val reason: String,
val investmentGrade: String?,
val aiScore: Double?
)
fun generateAndOpen(startAsset: Long, endAsset: Long, tradeLogs: List<TradeDetailData>) {
val today = LocalDate.now().toString()
val profitAmount = endAsset - startAsset
val profitRate = if (startAsset > 0) (profitAmount.toDouble() / startAsset) * 100 else 0.0
val profitColor = if (profitAmount > 0) "#FF3B30" else if (profitAmount < 0) "#007AFF" else "#333333"
val profitSign = if (profitAmount > 0) "+" else ""
val timeFormatter = DateTimeFormatter.ofPattern("HH:mm:ss")
// 1. 매매 상세 내역 HTML 행 생성
val tradeRowsHtml = if (tradeLogs.isEmpty()) {
"<tr><td colspan='8' style='padding: 30px; text-align: center; color: #999;'>금일 발생한 주문 내역이 없습니다.</td></tr>"
} else {
tradeLogs.joinToString("\n") { trade ->
// 매수(빨강), 매도(파랑) 뱃지
val typeBadge = if (trade.isBuy) "<span class='badge type-buy'>매수</span>" else "<span class='badge type-sell'>매도</span>"
// 시간 포맷팅
val orderTimeParsed = LocalDateTime.parse(trade.orderTime).format(timeFormatter)
// 수익률/수익금 처리 (매수일 경우 표시 안 함)
val rateText = if (trade.isBuy) "-" else "${String.format("%.2f", trade.profitRate)}%"
val rateColor = if (trade.isBuy || trade.profitRate == 0.0) "#333" else if (trade.profitRate > 0) "#FF3B30" else "#007AFF"
val amountText = if (trade.isBuy) "-" else "${String.format("%,d", trade.profitAmount)}"
// 상태/시간 표시 처리
val timeStatusHtml = if (trade.timeTaken.contains("미완료")) {
"<span style='color: #FF9500; font-weight: bold;'>${trade.timeTaken}</span>"
} else {
"<span style='color: #888;'>${trade.timeTaken}</span>"
}
val gradeHtml = trade.investmentGrade?.let { "<span class='badge grade-$it'>$it</span>" } ?: ""
"""
<tr>
<td class="center">$typeBadge</td>
<td class="stock-name"><strong>${trade.stockName}</strong> $gradeHtml</td>
<td class="center">$orderTimeParsed</td>
<td class="center">$timeStatusHtml</td>
<td class="right">${String.format("%,d", trade.execQty)}</td>
<td class="right">${String.format("%,d", trade.avgPrice)}</td>
<td class="right rate" style="color: $rateColor;">$rateText</td>
<td class="right rate" style="color: $rateColor;">$amountText</td>
</tr>
<tr class="reason-row">
<td colspan="8">
<div class="reason-box">
<span class="reason-icon">💡</span>
<span class="reason-text"><strong>AI (${String.format("%.1f", trade.aiScore ?: 0.0)}):</strong> ${trade.reason.replace("\n", " ")}</span>
</div>
</td>
</tr>
""".trimIndent()
}
}
// 2. 전체 HTML 템플릿
val htmlTemplate = """
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>ATRADE 일간 리포트 - $today</title>
<style>
:root { --red: #FF3B30; --blue: #007AFF; --bg: #f4f7f6; --text: #333; }
body { font-family: 'Pretendard', 'Apple SD Gothic Neo', sans-serif; background-color: var(--bg); color: var(--text); padding: 40px 20px; margin: 0; }
.container { max-width: 1000px; margin: 0 auto; background-color: #fff; border-radius: 16px; overflow: hidden; box-shadow: 0 10px 30px rgba(0,0,0,0.05); }
.header { background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%); color: #fff; padding: 40px; text-align: center; }
.header h1 { margin: 0; font-size: 32px; font-weight: 800; }
.summary { padding: 40px; text-align: center; border-bottom: 1px solid #eee; }
.summary .profit { font-size: 54px; font-weight: 800; color: $profitColor; margin: 10px 0; }
.details { display: flex; justify-content: center; gap: 50px; margin-top: 25px; background: #f8f9fa; padding: 20px; border-radius: 12px; }
.details div { display: flex; flex-direction: column; }
.details span.label { font-size: 13px; color: #888; margin-bottom: 5px; }
.details span.value { font-size: 20px; font-weight: 700; color: #333; }
.table-container { padding: 40px; overflow-x: auto; }
table { width: 100%; border-collapse: collapse; font-size: 14px; min-width: 800px; }
th { padding: 12px; text-align: left; background-color: #f8f9fa; color: #555; border-bottom: 2px solid #ddd; }
th.right { text-align: right; }
th.center { text-align: center; }
td { padding: 14px 12px; border-bottom: 1px solid #f0f0f0; vertical-align: middle; }
td.right { text-align: right; font-weight: 500; }
td.center { text-align: center; }
.badge { display: inline-block; padding: 4px 8px; border-radius: 4px; font-size: 12px; font-weight: bold; color: #fff; margin-left: 4px; }
.type-buy { background-color: var(--red); }
.type-sell { background-color: var(--blue); }
.grade-S { background-color: #8A2BE2; } .grade-A { background-color: #FF9500; } .grade-B { background-color: #34C759; }
.reason-row td { padding: 0 12px 12px 12px; border-bottom: 2px solid #eee; }
.reason-box { background-color: #f8fcfd; border-left: 4px solid #34b3e4; padding: 10px 14px; border-radius: 0 6px 6px 0; font-size: 13px; color: #555; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>ATRADE 일간 매매 상세 리포트</h1>
</div>
<div class="summary">
<h2>오늘의 실현 손익 (END - START)</h2>
<p class="profit">$profitSign${String.format("%,d", profitAmount)} <span style="font-size: 32px;">($profitSign${String.format("%.2f", profitRate)}%)</span></p>
</div>
<div class="table-container">
<h3>📊 금일 주문 체결 내역 전체</h3>
<table>
<thead>
<tr>
<th class="center">구분</th>
<th>종목명</th>
<th class="center">주문시각</th>
<th class="center">소요시간/상태</th>
<th class="right">체결량</th>
<th class="right">평균단가</th>
<th class="right">수익률</th>
<th class="right">수익금</th>
</tr>
</thead>
<tbody>
$tradeRowsHtml
</tbody>
</table>
</div>
</div>
</body>
</html>
""".trimIndent()
val directory = File("reports")
if (!directory.exists()) directory.mkdirs()
val reportFile = File(directory, "ATRADE_Report_$today.html")
try {
reportFile.writeText(htmlTemplate, Charsets.UTF_8)
if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) {
Desktop.getDesktop().browse(reportFile.toURI())
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}