From 3f209dcd4d41433651c5db1227464f6ce70893e7 Mon Sep 17 00:00:00 2001 From: lunaticbum Date: Thu, 5 Feb 2026 14:26:02 +0900 Subject: [PATCH] ... --- src/main/kotlin/model/StockModels.kt | 26 +++ src/main/kotlin/network/KisTradeService.kt | 17 +- src/main/kotlin/network/RagService.kt | 6 +- src/main/kotlin/service/AutoTradingManager.kt | 181 ++++++++++-------- src/main/kotlin/service/DynamicNewsScraper.kt | 13 +- .../kotlin/service/SystemSleepPreventer.kt | 6 + src/main/kotlin/ui/DashboardScreen.kt | 1 - src/main/kotlin/ui/IntegratedOrderSection.kt | 44 +++-- src/main/resources/logback.xml | 7 + 9 files changed, 194 insertions(+), 107 deletions(-) create mode 100644 src/main/resources/logback.xml diff --git a/src/main/kotlin/model/StockModels.kt b/src/main/kotlin/model/StockModels.kt index 7c6e983..9dd0741 100644 --- a/src/main/kotlin/model/StockModels.kt +++ b/src/main/kotlin/model/StockModels.kt @@ -59,6 +59,32 @@ enum class RankingType( VOLUME_POWER("체결강도순", "FHPST01680000", "20168", "/uapi/domestic-stock/v1/ranking/volume-power", "0"), // BEFORE("장전예상", "FHPST01820000", "20182", "/uapi/domestic-stock/v1/ranking/exp-trans-updown", "0"), // AFTER("장후예상", "FHPST01820000", "20182", "/uapi/domestic-stock/v1/ranking/exp-trans-updown", "1") + FOREIGNER_BUY("외인순매수", "FHPST01720000", "20172", "uapidomestic-stockv1quotationsfrgn-buy-rank", "0"), + INSTITUTION_BUY("기관순매수", "FHPST01730000", "20173", "uapidomestic-stockv1quotationsinst-buy-rank", "0"), + + // 신규: 재무/지표 + PER_RANK("PER", "FHPST01760000", "20176", "uapidomestic-stockv1quotationsper-rank", "0"), + PBR_RANK("PBR", "FHPST01770000", "20177", "uapidomestic-stockv1quotationspbr-rank", "0"), + DIVIDEND("배당률", "FHPST01800000", "20180", "uapidomestic-stockv1quotationsdividend-rank", "0"), + + // 신규: 시간외/예상 + AFTER_HOURS_VOLUME("시간외거래량", "FHPST01810000", "20181", "uapidomestic-stockv1rankingafterhours-volume", "0"), + EXPECTED_RISE("예상상승", "FHPST01820000", "20182", "uapidomestic-stockv1rankingexp-trans-updown", "0"), + EXPECTED_FALL("예상하락", "FHPST01820000", "20182", "uapidomestic-stockv1rankingexp-trans-updown", "1"), + + // 신규: 체결/호가/신고가 등 + EXEC_STRENGTH("체결강도", "FHPST01780000", "20178", "uapidomestic-stockv1quotationsexec-strength", "0"), + BID_ASK_VOLUME("호가잔량", "FHPST01790000", "20179", "uapidomestic-stockv1quotationsbid-ask-volume", "0"), + NEW_HIGH("52주신고가", "FHPST01690000", "20169", "uapidomestic-stockv1rankingsh-52w-high", "0"), + + // 신규: 신용/공매도/대량 + MARGIN_BALANCE("신용잔고", "FHPST01830000", "20183", "uapidomestic-stockv1quotationsmargin-balance", "0"), + SHORT_SELL("공매도", "FHPST01840000", "20184", "uapidomestic-stockv1quotationsshort-sell", "0"), + LARGE_DEAL("대량체결", "FHPST01850000", "20185", "uapidomestic-stockv1quotationslarge-deal", "0"), + + // 기타 인기 (KIS HTS 순위분석 기반) + INTEREST_TOP("관심순", "FHPST01860000", "20186", "uapidomestic-stockv1rankinginterest-top", "0"), + COMPANY_TRADE("당사매매", "FHPST01870000", "20187", "uapidomestic-stockv1rankingcompany-trade", "0") } @Serializable diff --git a/src/main/kotlin/network/KisTradeService.kt b/src/main/kotlin/network/KisTradeService.kt index 90c790f..8d22c33 100644 --- a/src/main/kotlin/network/KisTradeService.kt +++ b/src/main/kotlin/network/KisTradeService.kt @@ -147,9 +147,22 @@ object KisTradeService { // RankingType.BEFORE -> { // parameter("FID_MKOP_CLS_CODE", type.sortCode) // } - else -> { - + RankingType.FOREIGNER_BUY, RankingType.INSTITUTION_BUY -> { + parameter("FID_BLNG_CLS_CODE", type.sortCode) // 순매수용 + parameter("FID_INPUT_CNT_1", "5") // 5일 누적 등 조정 가능 } + RankingType.PER_RANK, RankingType.PBR_RANK -> { + parameter("FID_FINCL_CLS_CODE", type.sortCode) // 재무비율용 + } + RankingType.AFTER_HOURS_VOLUME -> { + parameter("FID_TIME_OUT_CLS_CODE", "1") // 시간외 구분 + } + RankingType.EXPECTED_RISE -> parameter("FID_MK_OP_CLS_CODE", type.sortCode) + // 체결강도/호가 등은 FID_EXEC_CLS_CODE 추가 + else -> parameter("FID_BLNG_CLS_CODE", "0") // 기본 +// else -> { +// +// } } parameter("FID_PBMN", "") parameter("FID_APLY_RANG_PRC_1", "") diff --git a/src/main/kotlin/network/RagService.kt b/src/main/kotlin/network/RagService.kt index 3126b3c..510c173 100644 --- a/src/main/kotlin/network/RagService.kt +++ b/src/main/kotlin/network/RagService.kt @@ -139,7 +139,11 @@ object RagService { corpInfo?.stockName = stockName tradingDecision.stockName = stockName tradingDecision.corpName = corpInfo?.cName ?: "" - corpInfo?.let { NewsService.fetchAndIngestNews(it) } + corpInfo?.let { + try { + NewsService.fetchAndIngestNews(it) + } catch (e: Exception) {} + } val financialDataDeferred = async { NewsService.fetchFinancialGrowth(corpInfo?.cCode ?: "") } diff --git a/src/main/kotlin/service/AutoTradingManager.kt b/src/main/kotlin/service/AutoTradingManager.kt index c953973..48ab481 100644 --- a/src/main/kotlin/service/AutoTradingManager.kt +++ b/src/main/kotlin/service/AutoTradingManager.kt @@ -4,9 +4,11 @@ import TradingDecision import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job +import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.async import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import kotlinx.coroutines.withTimeout import model.CandleData import model.RankingType import network.KisTradeService @@ -26,7 +28,8 @@ object AutoTradingManager { private var discoveryJob: Job? = null - + val MIN = 0.1 + val MAX = 15.0 fun startAutoDiscoveryLoop( tradeService: KisTradeService, callback: TradingDecisionCallback @@ -56,100 +59,113 @@ object AutoTradingManager { val riseList = riseRankDeferred.await() val amountList = amountRankDeferred.await() val volumeList = volumePowerDeferred.await() -// (C) 거래대금 상위 종목 필터링 (시장의 주도주) - val amountCandidates = amountList - .filter { stock -> - val rate = stock.prdy_ctrt.toDoubleOrNull() ?: 0.0 - rate in 1.0..15.0 // 너무 과열되지 않은 주도주 - } - - val volCandidates = volList - .filter { stock -> - val rate = stock.prdy_ctrt.toDoubleOrNull() ?: 0.0 - rate in 1.0..18.0 // 0% 초과 20% 이하 - } -// (B) 상승률 상위 종목 중: 너무 급등한(20% 초과) 종목은 제외하고, 적당히 오르고 있는 종목만 필터링 -> 상위 10개 -// 보통 상승률 랭킹은 상한가(30%)부터 내려오므로, 앞쪽의 급등주를 건너뛰어야 함 - val riseCandidates = riseList - .filter { stock -> - val rate = stock.prdy_ctrt.toDoubleOrNull() ?: 0.0 - rate in 2.0..18.0 // 최소 3% 이상은 올라야 의미 있음, 20% 이하는 안전 구간 - } - val volumeCandidates = volumeList .filter { stock -> - val rate = stock.prdy_ctrt.toDoubleOrNull() ?: 0.0 - rate in 1.0..18.0 // 최소 3% 이상은 올라야 의미 있음, 20% 이하는 안전 구간 - } + // 3. 리스트 합치기 (중복 제거) - val candidates = (volCandidates + riseCandidates + amountCandidates + volumeCandidates).distinctBy { it.code } + val candidates = (volList + riseList + amountList + volumeList + async { tradeService.fetchMarketRanking(RankingType.FOREIGNER_BUY, true).getOrDefault(emptyList()) + async { tradeService.fetchMarketRanking(RankingType.INSTITUTION_BUY, true).getOrDefault(emptyList()) }.await()}.await()).filter {stock -> + val rate = stock.prdy_ctrt.toDoubleOrNull() ?: 0.0 + rate in MIN..MAX // 너무 과열되지 않은 주도주 + }.distinctBy { it.code } println("🔎 1차 필터링 후보 ${candidates.size}개 (급등주 제외) 검증 시작...") candidates.forEach { stock -> - // [조건 1] 이미 보유한 종목 제외 - if (myHoldings.contains(stock.code)) return@forEach + try { - val currentPrice = stock.stck_prpr.replace(",", "").toDoubleOrNull() ?: 0.0 - // [조건 2] 최소 1주 매수 가능 여부 - if (currentPrice > myCash || currentPrice > 5000) return@forEach - callback(TradingDecision().apply { - this.stockCode = stock.code - this.confidence = -1.0 - this.stockName = stock.name - }, false) - // 3. 일봉 데이터 조회 (필터링 용도 + TechnicalAnalyzer 입력용) - val dailyResult = tradeService.fetchPeriodChartData(stock.code, "D", true) - val dailyData = dailyResult.getOrNull() - val todayCandle = dailyData?.lastOrNull() + // [조건 1] 이미 보유한 종목 제외 + if (myHoldings.contains(stock.code)) return@forEach - if (dailyData != null && todayCandle != null) { + val currentPrice = stock.stck_prpr.replace(",", "").toDoubleOrNull() ?: 0.0 - val open = todayCandle.stck_oprc.toDoubleOrNull() ?: 0.0 - val current = todayCandle.stck_prpr.toDoubleOrNull() ?: 0.0 + // [조건 2] 최소 1주 매수 가능 여부 + if (currentPrice > myCash || currentPrice > 10000) { + println("⏭️ [제외] ${stock.name}: 주가($currentPrice)가 예산 초과") + return@forEach + } + callback(TradingDecision().apply { + this.stockCode = stock.code + this.confidence = -1.0 + this.stockName = stock.name + }, false) + // 3. 일봉 데이터 조회 (필터링 용도 + TechnicalAnalyzer 입력용) + val dailyResult = tradeService.fetchPeriodChartData(stock.code, "D", true) + val dailyData = dailyResult.getOrNull() + val todayCandle = dailyData?.lastOrNull() - if (open > 0) { - val riseRate = (current - open) / open * 100 + if (dailyData != null && todayCandle != null) { - // [조건 3] 상승 중(양봉)이면서 20% 이하 상승 - if (riseRate > 0 && riseRate <= 20.0) { - println("✨ [발굴] ${stock.name} (+${String.format("%.1f", riseRate)}%) -> 데이터 수집 및 분석") + val open = todayCandle.stck_oprc.toDoubleOrNull() ?: 0.0 + val current = todayCandle.stck_prpr.toDoubleOrNull() ?: 0.0 - // [핵심 수정] AI 분석 전 필요한 차트 데이터(30분, 주봉, 월봉)를 모두 가져와 TechnicalAnalyzer에 주입 - // 비동기로 동시에 요청하여 속도 향상 - val min30Def = async { tradeService.fetchChartData(stock.code, true).getOrDefault(emptyList()) } - val weekDef = async { tradeService.fetchPeriodChartData(stock.code, "W", true).getOrDefault(emptyList()) } - val monthDef = async { tradeService.fetchPeriodChartData(stock.code, "M", true).getOrDefault(emptyList()) } + if (open > 0) { + val riseRate = (current - open) / open * 100 - val min30Data = min30Def.await() - val weeklyData = weekDef.await() - val monthlyData = monthDef.await() + // [조건 3] 상승 중(양봉)이면서 20% 이하 상승 + if (riseRate > 0 && riseRate <= 20.0) { + println( + "✨ [발굴] ${stock.name} (+${ + String.format( + "%.1f", + riseRate + ) + }%) -> 데이터 수집 및 분석" + ) - // TechnicalAnalyzer 상태 업데이트 (싱글톤이므로 순차 처리 필수) - val t = TechnicalAnalyzer() - t.daily = dailyData - t.weekly = weeklyData - t.monthly = monthlyData - t.min30 = min30Data - - // 데이터 준비 완료 후 AI 분석 요청 (suspend 함수이므로 완료될 때까지 대기 -> 데이터 섞임 방지) - RagService.processStock(t,stock.name, stock.code) { decision, isSuccess -> - if (decision != null) { - decision.stockName = stock.name - decision.currentPrice = current // 차트에서 확인한 최신 현재가 주입 + // [핵심 수정] AI 분석 전 필요한 차트 데이터(30분, 주봉, 월봉)를 모두 가져와 TechnicalAnalyzer에 주입 + // 비동기로 동시에 요청하여 속도 향상 + val min30Def = async { + tradeService.fetchChartData(stock.code, true).getOrDefault(emptyList()) + } + val weekDef = async { + tradeService.fetchPeriodChartData(stock.code, "W", true) + .getOrDefault(emptyList()) + } + val monthDef = async { + tradeService.fetchPeriodChartData(stock.code, "M", true) + .getOrDefault(emptyList()) } - callback(decision, isSuccess) // DashboardScreen으로 전달 - } - // 분석 후 잠시 대기 (서버 부하 조절) - delay(2000) + val min30Data = min30Def.await() + val weeklyData = weekDef.await() + val monthlyData = monthDef.await() + + // TechnicalAnalyzer 상태 업데이트 (싱글톤이므로 순차 처리 필수) + val t = TechnicalAnalyzer() + t.daily = dailyData + t.weekly = weeklyData + t.monthly = monthlyData + t.min30 = min30Data + + try { + withTimeout(60000L) { // 60초 타임아웃 설정 + RagService.processStock(t, stock.name, stock.code) { decision, isSuccess -> + if (decision != null) { + decision.stockName = stock.name + decision.currentPrice = current + } + callback(decision, isSuccess) + } + } + } catch (e: TimeoutCancellationException) { + println("⏳ [Timeout] ${stock.name} AI 분석 시간 초과로 스킵합니다.") + } catch (e: Exception) { + println("⚠️ [Error] AI 분석 중 오류: ${e.message}") + } + + // 데이터 준비 완료 후 AI 분석 요청 (suspend 함수이므로 완료될 때까지 대기 -> 데이터 섞임 방지) + // 분석 후 잠시 대기 (서버 부하 조절) + delay(2000) + } } } + delay(300) // 종목 간 API 호출 간격 + } catch (e: Exception) { + println("⚠️ [오류] ${stock.name} 분석 중 예외 발생: ${e.message}") } - delay(300) // 종목 간 API 호출 간격 } @@ -164,9 +180,10 @@ object AutoTradingManager { delay(tickMillis) currentWait += tickMillis val leftSec = (totalWaitMillis - currentWait) / 1000 - // 1분 단위 혹은 10초 단위로 자유롭게 로그 조절 가능 - if (leftSec % 30 == 0L || leftSec <= 30) { - println("📡 [AutoTrading] 시스템 정상 작동 중... (다음 분석 ${leftSec}초 전)") + if (leftSec % 60 == 0L) { + val runtime = Runtime.getRuntime() + val usedMem = (runtime.totalMemory() - runtime.freeMemory()) / 1024 / 1024 + println("📡 [System] 정상 작동 중... (남은 시간: ${leftSec}초 | 메모리 사용: ${usedMem}MB)") } } @@ -199,19 +216,19 @@ object AutoTradingManager { println("🔥 [주문 집행] $code $type 완료") } } - +data class InvestmentScores( + val ultraShort: Int, // 초단기 (분봉/에너지) + val shortTerm: Int, // 단기 (일봉/뉴스) + val midTerm: Int, // 중기 (주봉/재무) + val longTerm: Int // 장기 (월봉/펀더멘털) +) class TechnicalAnalyzer { var monthly: List = emptyList() var weekly: List = emptyList() var daily: List = emptyList() var min30: List = emptyList() - data class InvestmentScores( - val ultraShort: Int, // 초단기 (분봉/에너지) - val shortTerm: Int, // 단기 (일봉/뉴스) - val midTerm: Int, // 중기 (주봉/재무) - val longTerm: Int // 장기 (월봉/펀더멘털) - ) + fun calculateScores( financialScore: Int // 재무제표 점수 (성장률 등 기반) diff --git a/src/main/kotlin/service/DynamicNewsScraper.kt b/src/main/kotlin/service/DynamicNewsScraper.kt index fd6d0aa..3ab9e96 100644 --- a/src/main/kotlin/service/DynamicNewsScraper.kt +++ b/src/main/kotlin/service/DynamicNewsScraper.kt @@ -12,6 +12,7 @@ import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.withPermit import model.NewsItem import network.CorpInfo +import java.net.URL import kotlin.random.Random object DynamicNewsScraper { @@ -91,20 +92,25 @@ object DynamicNewsScraper { return page.evaluate(script) as String } - + var failDomainList = arrayListOf() suspend fun fetchFullContent(url: String): String { // browser.newContext().use { ... } 대신 직접 변수를 선언하고 제어합니다. + val domain = URL(url).host val context = browser.newContext() + if(failDomainList.contains(domain)) { + println("실패한 도메인 스크래핑 종료 $domain ") + return "" + } return try { context.use { ctx -> ctx.newPage().use { page -> - delay(Random.nextInt(1000).toLong()) + delay(Random.nextInt(2000).toLong()) // 1. 리스너 설정 시 예외 처리 강화 blockUnnecessaryResources(page) // 2. 타임아웃을 설정하여 무한 대기 방지 - val options = Page.NavigateOptions().setTimeout(30000.0) + val options = Page.NavigateOptions().setTimeout(8000.0) page.navigate(url, options) // 3. 페이지가 완전히 닫히기 전에 모든 대기 중인 이벤트를 해제하기 위해 LOAD 상태 대기 @@ -119,6 +125,7 @@ object DynamicNewsScraper { } } } catch (e: Exception) { + failDomainList.add(domain) println("❌ [Playwright] 스크래핑 실패 ($url): ${e.message}") "" } finally { diff --git a/src/main/kotlin/service/SystemSleepPreventer.kt b/src/main/kotlin/service/SystemSleepPreventer.kt index 2b6f117..b1ed6dc 100644 --- a/src/main/kotlin/service/SystemSleepPreventer.kt +++ b/src/main/kotlin/service/SystemSleepPreventer.kt @@ -1,6 +1,9 @@ package service import java.util.concurrent.TimeUnit +import org.slf4j.LoggerFactory +import ch.qos.logback.classic.Level +import ch.qos.logback.classic.Logger object SystemSleepPreventer { private var process: Process? = null @@ -9,6 +12,9 @@ object SystemSleepPreventer { * 맥의 절전 모드 및 디스플레이 취침을 방지하는 명령 실행 */ fun start() { + val root = LoggerFactory.getLogger("Exposed") as Logger + root.level = Level.ERROR + if (process?.isAlive == true) return try { diff --git a/src/main/kotlin/ui/DashboardScreen.kt b/src/main/kotlin/ui/DashboardScreen.kt index 2edc48f..49656bd 100644 --- a/src/main/kotlin/ui/DashboardScreen.kt +++ b/src/main/kotlin/ui/DashboardScreen.kt @@ -159,7 +159,6 @@ fun DashboardScreen() { // 3. 실시간 체결 통보 핸들러 (주문번호 중심) wsManager.onExecutionReceived = {code, qty, price,orderNo, isBuy -> scope.launch { - println("$orderNo, $code, $price, $qty, $isBuy") val exec = ExecutionData(orderNo, code, price, qty, isBuy) executionCache[orderNo] = exec syncAndExecute(orderNo) diff --git a/src/main/kotlin/ui/IntegratedOrderSection.kt b/src/main/kotlin/ui/IntegratedOrderSection.kt index 173fb7d..33073d5 100644 --- a/src/main/kotlin/ui/IntegratedOrderSection.kt +++ b/src/main/kotlin/ui/IntegratedOrderSection.kt @@ -82,12 +82,12 @@ fun IntegratedOrderSection( // 계산용 변수 val curPriceNum = currentPrice.replace(",", "").toDoubleOrNull() ?: 0.0 val basePrice = (if (orderPrice.isEmpty()) curPriceNum else orderPrice.toDoubleOrNull() ?: 0.0) - val inputQty = orderQty.replace(",", "").toIntOrNull() ?: 0 - fun excuteTrade(willEnableAutoSell: Boolean, orderQty: String, profitRate1: Double?) { + + fun excuteTrade(willEnableAutoSell: Boolean, orderQty: String, profitRate1: Double?,confidence : Boolean = false) { scope.launch { val tickSize = MarketUtil.getTickSize(basePrice) - val oneTickLowerPrice = basePrice - tickSize + val oneTickLowerPrice = basePrice - (tickSize * if (confidence) { 1} else {2}) // 2. 주문 가격 설정 (직접 입력값이 없으면 한 틱 낮은 가격 사용) val finalPrice = if (orderPrice.isBlank()) { @@ -114,7 +114,7 @@ fun IntegratedOrderSection( // 4. 보정된 수익률을 적용하여 목표가 계산 val calculatedTarget = MarketUtil.roundToTickSize(basePrice * (1 + effectiveProfitRate / 100.0)) val calculatedStop = MarketUtil.roundToTickSize(basePrice * (1 + sRate / 100.0)) - + val inputQty = orderQty.replace(",", "").toIntOrNull() ?: 0 // 5. DB 저장 (effectiveProfitRate를 저장하여 분석 시 실제 목표치를 확인 가능하게 함) DatabaseFactory.saveAutoTrade(AutoTradeItem( orderNo = realOrderNo, @@ -146,13 +146,7 @@ fun IntegratedOrderSection( completeTradingDecision.stockCode.equals(stockCode)) { fun resultCheck(completeTradingDecision :TradingDecision) { - println(""" - corpName : ${completeTradingDecision.corpName} - confidence : ${completeTradingDecision.confidence + append} - shortPossible : ${completeTradingDecision.shortPossible() + append} - profitPossible : ${completeTradingDecision.profitPossible()+ append} - safePossible : ${completeTradingDecision.safePossible()+ append} - """.trimIndent()) + val weights = mapOf( "short" to 0.3, // 초단기 점수가 낮아도 전체에 미치는 영향 감소 @@ -169,8 +163,15 @@ fun IntegratedOrderSection( // 3. 매수 결정 문턱값 (예: 70점 이상이면 매수 가능) val MIN_PURCHASE_SCORE = 68.0 val HIGH_QUALITY_SCORE = 85.0 // 강력 추천 기준 - - if (totalScore >= MIN_PURCHASE_SCORE && completeTradingDecision.confidence > MIN_CONFIDENCE) { + println(""" + corpName : ${completeTradingDecision.corpName} + confidence : ${completeTradingDecision.confidence + append} + shortPossible : ${completeTradingDecision.shortPossible() + append} + profitPossible : ${completeTradingDecision.profitPossible()+ append} + safePossible : ${completeTradingDecision.safePossible()+ append} + totalScore : ${totalScore} + """.trimIndent()) + if (totalScore >= MIN_PURCHASE_SCORE && completeTradingDecision.confidence >= MIN_CONFIDENCE) { // 4. 점수에 따른 가변 마진 적용 // 토탈 스코어가 85점 이상이면 마진을 3.0으로 고정하거나 추가 가산(append) 적용 @@ -182,16 +183,23 @@ fun IntegratedOrderSection( } println("🚀 [매수 진행] 토탈 스코어: ${String.format("%.1f", totalScore)} -> 종목: ${completeTradingDecision.stockCode}") - + val MAX_BUDGET = 25000.0 + // basePrice(현재가 혹은 지정가)를 기준으로 매수 가능 수량 산출 (최소 1주 보장) + val calculatedQty = if (basePrice > 0) { + (MAX_BUDGET / basePrice).toInt().coerceAtLeast(1) + } else { + 1 + } // 5. 매수 실행 (계산된 finalMargin 전달) excuteTrade( willEnableAutoSell = true, - orderQty = "1", - profitRate1 = finalMargin + orderQty = calculatedQty.toString(), + profitRate1 = finalMargin, + confidence = totalScore >= HIGH_QUALITY_SCORE ) } else { - println("✋ [관망] 토탈 스코어(${String.format("%.1f", totalScore)})가 기준치($MIN_PURCHASE_SCORE) 미달") + println("✋ [관망] 토탈 스코어(${String.format("%.1f", totalScore)})가 기준치($MIN_PURCHASE_SCORE) 미달 또는 신뢰도 ${completeTradingDecision.confidence}가 기준치 ${MIN_CONFIDENCE} 미달") } } when (completeTradingDecision?.decision) { @@ -228,7 +236,7 @@ fun IntegratedOrderSection( modifier = Modifier.weight(1f) ) } - + val inputQty = orderQty.replace(",", "").toIntOrNull() ?: 0 // 수익률 시뮬레이션 표 if (basePrice > 0 && inputQty > 0) { SimulationCard(basePrice, inputQty.toDouble()) diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml new file mode 100644 index 0000000..cd3b0c6 --- /dev/null +++ b/src/main/resources/logback.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file