Compare commits

..

1 Commits
g ... main

Author SHA1 Message Date
ad6d00ac39 ... 2026-05-13 11:37:57 +09:00
8 changed files with 128 additions and 96 deletions

View File

@ -88,8 +88,13 @@ fun getLlamaBinPath(): String {
} }
// Windows NUC // Windows NUC
os.contains("win") -> { os.contains("win") -> {
if (KisSession.tradeConfig.isLowPerformanceMonitoring) {
"$basePath/win-x64/llama-server.exe"
}
else {
"$basePath/win-x64-n/llama-server.exe" "$basePath/win-x64-n/llama-server.exe"
} }
}
else -> "$basePath/llama-server" else -> "$basePath/llama-server"
} }
} }
@ -111,6 +116,7 @@ private var isAppStarted = false
fun main() = application { fun main() = application {
if (!isAppStarted) { if (!isAppStarted) {
initLogger(DETAILLOG) initLogger(DETAILLOG)
KisSession.tradeConfig = KisSession.loadTradeConfig()
try { try {
val (port1, port2) = PortFinder.findAvailablePortPair(18080, false) val (port1, port2) = PortFinder.findAvailablePortPair(18080, false)
if (port1 > 18000 && port2 > port1) { if (port1 > 18000 && port2 > port1) {

View File

@ -5,6 +5,7 @@ import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import model.AppConfig import model.AppConfig
import model.KisSession
import model.TradingDecision import model.TradingDecision
import network.NewsService import network.NewsService
import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.*
@ -490,7 +491,18 @@ object TradingLogStore {
decision = decision.decision ?: "HOLD", decision = decision.decision ?: "HOLD",
confidence = decision.confidence, confidence = decision.confidence,
reason = decision.reason ?: "" reason = decision.reason ?: ""
)) ).apply {
if (KisSession.tradeConfig.useTagsShare.contains(this.decision) && KisSession.tradeConfig.useLogKeywordsShare.any {
reason.contains(
it
)
}) {
CoroutineScope(Dispatchers.Default).launch {
NewsService.sendTelegramMessage("${this@apply.decision}$stockName ${reason}")
}
}
}
)
} }
} }
@ -504,11 +516,25 @@ object TradingLogStore {
decision = decision, decision = decision,
confidence = 100.0, confidence = 100.0,
reason = log reason = log
).apply {
if (KisSession.tradeConfig.useTagsShare.contains(decision) && KisSession.tradeConfig.useLogKeywordsShare.any {
log.contains(
it
) )
}) {
CoroutineScope(Dispatchers.Default).launch {
println("CALLED sendTelegramMessage")
NewsService.sendTelegramMessage("${this@apply.decision}$stockName ${log}")
}
}
}
) )
} }
} }
fun addLog(tradingDecision: TradingDecision, decision: String, log: String) { fun addLog(tradingDecision: TradingDecision, decision: String, log: String) {
synchronized(this) { synchronized(this) {
if (decisionLogs.size > 1000) decisionLogs.removeAt(0) if (decisionLogs.size > 1000) decisionLogs.removeAt(0)
@ -521,11 +547,8 @@ object TradingLogStore {
reason = log reason = log
).apply { ).apply {
CoroutineScope(Dispatchers.Default).launch { CoroutineScope(Dispatchers.Default).launch {
println("CALLED sendTelegramMessage -1") if (((tradingDecision.investmentGrade?.name?.length ?: 0) > 0 && KisSession.tradeConfig.useGradeShare.contains(tradingDecision.investmentGrade?.name))
if (decision.contains("WATCH") || ((tradingDecision.investmentGrade?.ordinal
?: 0) < 2)
) { ) {
println("CALLED sendTelegramMessage OK")
NewsService.sendTelegramMessage("${this@apply.decision} ${tradingDecision.stockName}[${tradingDecision.currentPrice}] ${log}") NewsService.sendTelegramMessage("${this@apply.decision} ${tradingDecision.stockName}[${tradingDecision.currentPrice}] ${log}")
} }
} }
@ -534,6 +557,32 @@ object TradingLogStore {
} }
} }
fun addWatchLog(tradingDecision: TradingDecision, decision: String, log: String) {
synchronized(this) {
if (decisionLogs.size > 1000) decisionLogs.removeAt(0)
decisionLogs.add(
LogEntry(
time = LocalTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss")),
stockName = "${tradingDecision.stockName}[${tradingDecision.currentPrice}]",
decision = decision,
confidence = tradingDecision.confidence,
reason = log
).apply {
CoroutineScope(Dispatchers.Default).launch {
if (KisSession.tradeConfig.useTagsShare.contains(decision) && KisSession.tradeConfig.useLogKeywordsShare.any {
log.contains(
it
)
}) {
NewsService.sendTelegramMessage("${this@apply.decision} ${tradingDecision.stockName}[${tradingDecision.currentPrice}] ${log}")
}
}
}
)
}
}
fun addAnalyzer(name : String, code : String, log: String, positive : Boolean = false) { fun addAnalyzer(name : String, code : String, log: String, positive : Boolean = false) {
synchronized(this) { synchronized(this) {
if (decisionLogs.size > 1000) decisionLogs.removeAt(0) if (decisionLogs.size > 1000) decisionLogs.removeAt(0)
@ -545,7 +594,17 @@ object TradingLogStore {
decision = if (positive) "ANALYZER" else "PASS", decision = if (positive) "ANALYZER" else "PASS",
confidence = 100.0, confidence = 100.0,
reason = log reason = log
).apply {
if (KisSession.tradeConfig.useTagsShare.contains(decision) && KisSession.tradeConfig.useLogKeywordsShare.any {
log.contains(
it
) )
}) {
CoroutineScope(Dispatchers.Default).launch {
NewsService.sendTelegramMessage("${this@apply.decision}$name[$code] ${log}")
}
}
}
) )
} }
} }
@ -562,9 +621,19 @@ object TradingLogStore {
confidence = 100.0, confidence = 100.0,
reason = log reason = log
).apply { ).apply {
if (KisSession.tradeConfig.useTagsShare.contains(decision) && KisSession.tradeConfig.useLogKeywordsShare.any {
log.contains(
it
)
}) {
var current = System.currentTimeMillis()
var sendable = noticeFilter.filter { it.key.equals(code, true) && ((current - it.value) > 1000 * 60 * 30L)}.isNotEmpty()
if (sendable) {
CoroutineScope(Dispatchers.Default).launch { CoroutineScope(Dispatchers.Default).launch {
println("CALLED sendTelegramMessage")
NewsService.sendTelegramMessage("${this@apply.decision}$name[$code] ${log}") NewsService.sendTelegramMessage("${this@apply.decision}$name[$code] ${log}")
}}
noticeFilter[code] = current
} }
} }
) )
@ -572,6 +641,8 @@ object TradingLogStore {
} }
} }
var noticeFilter = hashMapOf<String, Long>()
fun addNotice(name : String, code : String, log: String, qty: Int? = null) { fun addNotice(name : String, code : String, log: String, qty: Int? = null) {
synchronized(this) { synchronized(this) {
if (decisionLogs.size > 1000) decisionLogs.removeAt(0) if (decisionLogs.size > 1000) decisionLogs.removeAt(0)

View File

@ -241,6 +241,17 @@ class TradeConfig {
var end_buy_time : String = "15:10" var end_buy_time : String = "15:10"
var enableOverSea : Boolean = false var enableOverSea : Boolean = false
var tlg_id : String = "" var tlg_id : String = ""
var CYCLE_TIMEOUT = 15 * 60 * 1000L // 한 사이클 최대 10분
var WATCHDOG_CHECK_INTERVAL = 30 * 1000L // 30초마다 생존 확인
var STUCK_THRESHOLD = 7 * 60 * 1000L // 5분간 반응 없으면 'Stuck'으로 판단
var ONE_STOCK_ALYSIS_TIME = 180000L
var isLowPerformanceMonitoring: Boolean = false
var useGradeShare : List<String> = listOf("LEVEL_4","LEVEL_5")
var useTagsShare : List<String> = listOf("NOTICE", "WATCH")
var useLogKeywordsShare : List<String> = listOf("재분석")
var useAutoRepost : Boolean = false
var minusFilter : Double = 15.0
var plusFilter : Double = 15.0
} }

View File

@ -671,9 +671,6 @@ object RagService {
val alignmentBonus = if (s.ultraShort > s.shortTerm && s.shortTerm > s.midTerm) 3.0 else 0.0 val alignmentBonus = if (s.ultraShort > s.shortTerm && s.shortTerm > s.midTerm) 3.0 else 0.0
return (base + alignmentBonus).coerceIn(0.0, 25.0) return (base + alignmentBonus).coerceIn(0.0, 25.0)
} }
} }

View File

@ -103,32 +103,6 @@ object LocalReportGenerator {
} }
} }
fun generateAndOpenAsyncDirectly(
summary: RawSummaryData,
rawHoldings: List<RawHoldingData>,
rawTrades: List<RawTradeData>
) {
reportScope.launch {
try {
// 1. [핵심] 대시보드 통계 지표 추출 (Generator가 직접 계산)
val stats = calculateDashboardStats(rawHoldings, rawTrades)
// 2. 탭 2 & 3 HTML 가공
val holdingsHtml = processHoldings(rawHoldings)
val tradesHtml = processTrades(rawTrades)
// 3. 전체 HTML 조립
val htmlContent = buildHtml(summary, stats, holdingsHtml, tradesHtml)
if (summary.type.equals("END", true) || summary.type.equals("MIDDLE", true)) {
saveAndOpen(summary.type, htmlContent)
}
} catch (e: Exception) {
println("❌ [Report] 리포트 비동기 생성 중 오류 발생: ${e.message}")
e.printStackTrace()
}
}
}
// --- [새로운 통계 계산 로직] --- // --- [새로운 통계 계산 로직] ---
private fun calculateDashboardStats(holdings: List<RawHoldingData>, trades: List<RawTradeData>): DashboardStats { private fun calculateDashboardStats(holdings: List<RawHoldingData>, trades: List<RawTradeData>): DashboardStats {
val tradesByStock = trades.groupBy { it.stockCode } val tradesByStock = trades.groupBy { it.stockCode }

View File

@ -59,8 +59,10 @@ object TradingReportManager : TradingReportService {
private val activePositions = mutableMapOf<String, String>() private val activePositions = mutableMapOf<String, String>()
override fun recordAssetSnapshot(type: SnapshotType, balance: UnifiedBalance, remark: String?) { override fun recordAssetSnapshot(type: SnapshotType, balance: UnifiedBalance, remark: String?) {
if (!KisSession.tradeConfig.useAutoRepost) {
return
}
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
println("❌ [Report] 리포트 비동기 생성 중 오류 발생: gggg")
val todayDate = LocalDate.now().toString() val todayDate = LocalDate.now().toString()
// 1. 중복 없는 전체 종목 코드 리스트 추출 // 1. 중복 없는 전체 종목 코드 리스트 추출
@ -230,7 +232,7 @@ object TradingReportManager : TradingReportService {
} }
// 6. 코루틴 기반 제너레이터 호출 // 6. 코루틴 기반 제너레이터 호출
LocalReportGenerator.generateAndOpenAsyncDirectly(summaryData, holdingLogs, tradeLogs) LocalReportGenerator.generateAndOpenAsync(summaryData, holdingLogs, tradeLogs)
} }
} }
} }

View File

@ -61,10 +61,10 @@ object AutoTradingManager {
private val lastTickTime = AtomicLong(System.currentTimeMillis()) private val lastTickTime = AtomicLong(System.currentTimeMillis())
private var watchdogJob: Job? = null private var watchdogJob: Job? = null
private const val CYCLE_TIMEOUT = 15 * 60 * 1000L // 한 사이클 최대 10분 var CYCLE_TIMEOUT = KisSession.tradeConfig.CYCLE_TIMEOUT
private const val WATCHDOG_CHECK_INTERVAL = 30 * 1000L // 30초마다 생존 확인 var WATCHDOG_CHECK_INTERVAL = KisSession.tradeConfig.WATCHDOG_CHECK_INTERVAL
private const val STUCK_THRESHOLD = 7 * 60 * 1000L // 5분간 반응 없으면 'Stuck'으로 판단 var STUCK_THRESHOLD = KisSession.tradeConfig.STUCK_THRESHOLD
private const val ONE_STOCK_ALYSIS_TIME = 180000L var ONE_STOCK_ALYSIS_TIME = KisSession.tradeConfig.ONE_STOCK_ALYSIS_TIME
fun isRunning(): Boolean = discoveryJob?.isActive == true fun isRunning(): Boolean = discoveryJob?.isActive == true
private var remainingCandidates = mutableListOf<RankingStock>() private var remainingCandidates = mutableListOf<RankingStock>()
// private val processedCodes = mutableSetOf<String>() // 중복 처리 방지용 (선택 사항) // private val processedCodes = mutableSetOf<String>() // 중복 처리 방지용 (선택 사항)
@ -216,7 +216,7 @@ object AutoTradingManager {
println("🚫 [안전 장치 작동] 현재 포지션이 가득 찼습니다. (최대 ${myOredsAndBalanceCodes.size}/${maxStocks}종목). 신규 매수를 일시 중단하고 매도에 집중합니다.") println("🚫 [안전 장치 작동] 현재 포지션이 가득 찼습니다. (최대 ${myOredsAndBalanceCodes.size}/${maxStocks}종목). 신규 매수를 일시 중단하고 매도에 집중합니다.")
TradingLogStore.addNotice("SYSTEM", "LIMIT", "최대 보유 종목 도달로 신규 매수 일시 중단") TradingLogStore.addNotice("SYSTEM", "LIMIT", "최대 보유 종목 도달로 신규 매수 일시 중단")
AutoTradingManager.addToReanalysis(RankingStock(mksc_shrn_iscd = stockCode,hts_kor_isnm = stockName)) AutoTradingManager.addToReanalysis(RankingStock(mksc_shrn_iscd = stockCode,hts_kor_isnm = stockName))
TradingLogStore.addLog(decision,"WATCH","매수 실패 : 최대 보유 종목 도달로 신규 매수 일시 중단 => 재분석 대기열에 추가") TradingLogStore.addWatchLog(decision,"WATCH","매수 실패 : 최대 보유 종목 도달로 신규 매수 일시 중단 => 재분석 대기열에 추가")
} else { } else {
println("basePrice : $basePrice, oneTickLowerPrice : $oneTickLowerPrice, finalPrice : $finalPrice") println("basePrice : $basePrice, oneTickLowerPrice : $oneTickLowerPrice, finalPrice : $finalPrice")
@ -266,7 +266,7 @@ object AutoTradingManager {
if (it.message?.contains("주문가능금액을 초과") == true) { if (it.message?.contains("주문가능금액을 초과") == true) {
AutoTradingManager.addToReanalysis(RankingStock(mksc_shrn_iscd = stockCode,hts_kor_isnm = stockName)) AutoTradingManager.addToReanalysis(RankingStock(mksc_shrn_iscd = stockCode,hts_kor_isnm = stockName))
TradingLogStore.addLog(decision,"WATCH","${it.message ?: " 매수 실패"} => 재분석 대기열에 추가") TradingLogStore.addWatchLog(decision,"WATCH","${it.message ?: " 매수 실패"} => 재분석 대기열에 추가")
} else { } else {
TradingLogStore.addLog(decision,"BUY",it.message ?: "매수 실패") TradingLogStore.addLog(decision,"BUY",it.message ?: "매수 실패")
} }
@ -538,21 +538,15 @@ object AutoTradingManager {
isBuy = false, isBuy = false,
).onSuccess { newOrderNo -> ).onSuccess { newOrderNo ->
println("✅ [보유 주식 손절 처리] 수익률($profit%) -> ${holding.valuationProfitAmount} 손해 중이며 현제 손절 가이드에 적합함 시장가 매도.") println("✅ [보유 주식 손절 처리] 수익률($profit%) -> ${holding.valuationProfitAmount} 손해 중이며 현제 손절 가이드에 적합함 시장가 매도.")
TradingLogStore.addSellLog(
holding.code,
targetPrice.toString(),
"SELL",
"☠️ 보유 주식 손절 처리 [수익률 : ${profit}%] ${holding.valuationProfitAmount} 손해 중이며 현시세{${holding.currentPrice}}로 기준 호가 위 매도[$targetPrice] 주문 완료"
)
}.onFailure { err-> }.onFailure { err->
println("✅ [보유 주식 손절 처리] 실패 ${err.message}") println("✅ [보유 주식 손절 처리] 실패 ${err.message}")
} }
// TradingLogStore.addNotice( TradingLogStore.addNotice(
// "보유주식[${holding.name}]", "보유주식[${holding.name}]",
// holding.code, holding.code,
// "수익률($profit%) -> ${holding.valuationProfitAmount} 손해 중이며 현제 손절 가이드에 적합함 시장가 매도." "수익률($profit%) -> ${holding.valuationProfitAmount} 손해 중이며 현제 손절 가이드에 적합함 시장가 매도."
// ) )
} }
analyzeDeepLossHoldingsAfterMarket(holding , true) analyzeDeepLossHoldingsAfterMarket(holding , true)
} }
@ -565,7 +559,9 @@ object AutoTradingManager {
private suspend fun analyzeDeepLossHoldingsAfterMarket(holding: UnifiedStockHolding, isForce : Boolean = false) { // 💡 [신규 추가] 수익률이 크게 마이너스인 종목(-5.0% 이하) 심층 가이드 분석 private suspend fun analyzeDeepLossHoldingsAfterMarket(holding: UnifiedStockHolding, isForce : Boolean = false) { // 💡 [신규 추가] 수익률이 크게 마이너스인 종목(-5.0% 이하) 심층 가이드 분석
val now = LocalTime.now() val now = LocalTime.now()
val currentMinute = now.minute val currentMinute = now.minute
if ((!isForce && (now.hour == 8 || now.hour == 16 || now.hour == 17)) || (isForce && (currentMinute % 5 == 0))) { if ((holding.availOrderCount.toInt()
?: 0) > 0 && ((!isForce && (now.hour == 8 || now.hour == 16 || now.hour == 17)) || (isForce && (currentMinute % 5 == 0)))
) {
val profit = holding.profitRate.toDouble() val profit = holding.profitRate.toDouble()
val lossThreshold = -5.0 // 가이드를 작동시킬 손실 기준선 (필요시 ConfigIndex 로 빼셔도 좋습니다) val lossThreshold = -5.0 // 가이드를 작동시킬 손실 기준선 (필요시 ConfigIndex 로 빼셔도 좋습니다)
if (profit <= lossThreshold) { if (profit <= lossThreshold) {
@ -807,7 +803,7 @@ object AutoTradingManager {
val priceGap = Math.abs(currentPrice - orderedPrice) / orderedPrice val priceGap = Math.abs(currentPrice - orderedPrice) / orderedPrice
println("checkAndCancelPendingBuyOrders order $order ${elapsedMillis / 1000L}${priceGap}% 차이") println("checkAndCancelPendingBuyOrders order $order ${elapsedMillis / 1000L}${priceGap}% 차이")
if (priceGap >= KisSession.tradeConfig.auto_cancel_pending_rate) { if (priceGap >= KisSession.tradeConfig.auto_cancel_pending_rate) {
TradingLogStore.addNotice(order.prdt_name,order.pdno,"[주문 취소] ${order.prdt_name} (${order.pdno}) - 시간경과 및 가격괴리(${String.format("%.2f", priceGap * 100)}%)로 취소 시도") TradingLogStore.addNotice(order.prdt_name,order.pdno,"[주문 취소] ${order.prdt_name} (${order.pdno}) - 시간경과 및 가격괴리(${String.format("%.2f", priceGap)}%)로 취소 시도")
KisTradeService.cancelOrder( KisTradeService.cancelOrder(
order.ord_no, // 원주문번호 order.ord_no, // 원주문번호
order.pdno order.pdno
@ -859,7 +855,7 @@ object AutoTradingManager {
}.filter { }.filter {
val rate = it.prdy_ctrt.toDouble() val rate = it.prdy_ctrt.toDouble()
val corpInfo = DartCodeManager.getCorpCode(it.code) val corpInfo = DartCodeManager.getCorpCode(it.code)
val isOk = (rate > 0 && rate < 15) || (rate < 0 && rate > -15) val isOk = (rate > 0 && rate < KisSession.tradeConfig.plusFilter) || (rate < 0 && rate > (KisSession.tradeConfig.minusFilter * -1))
if (corpInfo?.cName.isNullOrEmpty()) { if (corpInfo?.cName.isNullOrEmpty()) {
false false
} else { } else {

View File

@ -50,13 +50,9 @@ import model.KisSession
import network.KisTradeService import network.KisTradeService
import network.NewsService import network.NewsService
import network.StockUniverseLoader import network.StockUniverseLoader
import report.SnapshotType
import report.TradingReportManager
import service.AutoTradingManager import service.AutoTradingManager
import service.AutoTradingManager.currentBalance
import java.io.File import java.io.File
import java.net.URI import java.net.URI
import java.time.LocalTime
@OptIn(ExperimentalMaterialApi::class) @OptIn(ExperimentalMaterialApi::class)
@Composable @Composable
@ -68,7 +64,6 @@ fun TradingDecisionLog() {
val filterOptions = listOf("전체", "BUY", "SELL", "SETTING","ANALYZER","WATCH","AFTER","NOTICE")//"PASS",,"RETRY""HOLD", val filterOptions = listOf("전체", "BUY", "SELL", "SETTING","ANALYZER","WATCH","AFTER","NOTICE")//"PASS",,"RETRY""HOLD",
var llmAnalyser by remember { mutableStateOf(AutoTradingManager.llmAnalyser) } var llmAnalyser by remember { mutableStateOf(AutoTradingManager.llmAnalyser) }
val tradeConfig by remember { val tradeConfig by remember {
KisSession.tradeConfig = KisSession.loadTradeConfig()
CoroutineScope(Dispatchers.Default).launch { CoroutineScope(Dispatchers.Default).launch {
println("CALLED sendTelegramMessage -1") println("CALLED sendTelegramMessage -1")
val now = java.time.LocalTime.now(java.time.ZoneId.of("Asia/Seoul")) val now = java.time.LocalTime.now(java.time.ZoneId.of("Asia/Seoul"))
@ -109,35 +104,15 @@ fun TradingDecisionLog() {
Row(modifier = Modifier.fillMaxSize().background(Color(0xFFF2F2F2))) { Row(modifier = Modifier.fillMaxSize().background(Color(0xFFF2F2F2))) {
Column(modifier = Modifier.weight(1f).padding(8.dp).fillMaxHeight().background(Color.White)) { Column(modifier = Modifier.weight(1f).padding(8.dp).fillMaxHeight().background(Color.White)) {
Row(modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
Button( Button(
onClick = { onClick = {
coroutineScope.launch { coroutineScope.launch {
// index 0으로 부드럽게 스크롤 (즉시 이동은 scrollToItem(0)) // index 0으로 부드럽게 스크롤 (즉시 이동은 scrollToItem(0))
listState.animateScrollToItem(if (filteredLogs.size - 1 >= 0) filteredLogs.size - 1 else 0) listState.animateScrollToItem(filteredLogs.size - 1)
} }
} }
) { Text("AI 자동매매 실시간 로그", style = MaterialTheme.typography.h6) } ) { Text("AI 자동매매 실시간 로그", style = MaterialTheme.typography.h6) }
Button(
onClick = {
coroutineScope.launch {
currentBalance = KisTradeService.fetchIntegratedBalance().getOrNull()
currentBalance?.let { currentBalance ->
if (LocalTime.now().isBefore(LocalTime.of(18,1))) {
TradingReportManager.recordAssetSnapshot(
if (LocalTime.now().isAfter(LocalTime.of(18, 0))
) SnapshotType.END else SnapshotType.MIDDLE, currentBalance, ""
)
}
}
}
}
) { Text("Open the report", style = MaterialTheme.typography.body2) }
}
Row( Row(
modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp), modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp),
horizontalArrangement = Arrangement.Start horizontalArrangement = Arrangement.Start
@ -248,9 +223,9 @@ fun TradingDecisionLog() {
Text( Text(
text = log.decision, text = log.decision,
color = when (log.decision) { color = when (log.decision) {
"BUY" -> Color(0xFF800080) "BUY" -> Color.Red
"SETTING" -> Color(0xFFFFA500) "SETTING" -> Color(0xFFFFA500)
"SELL" -> if (log.reason.contains("손절 처리")) Color.Blue else Color.Red "SELL" -> Color(0xFF800080)
"HOLD" -> Color.DarkGray "HOLD" -> Color.DarkGray
"ANALYZER" -> Color.Green "ANALYZER" -> Color.Green
"PASS" -> Color.Yellow "PASS" -> Color.Yellow