diff --git a/src/main/kotlin/analyzer/RiskManager.kt b/src/main/kotlin/analyzer/RiskManager.kt new file mode 100644 index 0000000..26704cb --- /dev/null +++ b/src/main/kotlin/analyzer/RiskManager.kt @@ -0,0 +1,50 @@ +package analyzer + +import model.CurrentPriceOutput +import model.RiskValidationResult + +object RiskManager { + + /** + * 통합된 시세 데이터를 바탕으로 매수 불가능한 '위험 종목'을 걸러냅니다. + * @param data API에서 받아온 통합 CurrentPriceOutput 객체 + * @return RiskValidationResult (isSafe == true 이면 매수 진행) + */ + fun evaluateRisk(data: CurrentPriceOutput): RiskValidationResult { + + // 1. 거래 정지 및 임시 정지 확인 + if (data.iscd_stat_cls_code == "58" || data.temp_stop_yn.uppercase() == "Y") { + return RiskValidationResult(false, "거래정지 또는 임시정지 종목") + } + + // 2. 상장폐지 직전 단계 (정리매매, 관리종목) + if (data.sltr_yn.uppercase() == "Y") { + return RiskValidationResult(false, "정리매매 진행 중인 종목") + } + if (data.iscd_stat_cls_code == "51" || data.mang_issu_cls_code == "1" || data.mang_issu_cls_code.uppercase() == "Y") { + return RiskValidationResult(false, "상장폐지 위험이 있는 관리종목") + } + + // 3. 거래소 경고 (투자위험, 투자경고, 투자주의, 단기과열종목) + val warningCodes = listOf("52", "53", "54", "59") + if (warningCodes.contains(data.iscd_stat_cls_code)) { + return RiskValidationResult(false, "거래소 경고 상태 (상태코드: ${data.iscd_stat_cls_code})") + } + + if (data.short_over_yn.uppercase() == "Y") { + return RiskValidationResult(false, "단기과열 지정 종목") + } + + if (data.invt_caful_yn.uppercase() == "Y") { + return RiskValidationResult(false, "투자유의 지정 종목") + } + + // 4. 시장경고코드 필터링 (정상 상태인 "00"이나 빈 값이 아닌 경우) + if (data.mrkt_warn_cls_code.isNotEmpty() && data.mrkt_warn_cls_code != "00") { + return RiskValidationResult(false, "시장경고 발동 중 (경고코드: ${data.mrkt_warn_cls_code})") + } + + // 모든 위험 요소를 무사히 통과한 경우 + return RiskValidationResult(true, "안전") + } +} \ No newline at end of file diff --git a/src/main/kotlin/model/StockModels.kt b/src/main/kotlin/model/StockModels.kt index bc7b4fe..6a3a3a2 100644 --- a/src/main/kotlin/model/StockModels.kt +++ b/src/main/kotlin/model/StockModels.kt @@ -247,6 +247,7 @@ data class ExecutionData( ) +@Serializable data class CurrentPriceResponse( val rt_cd: String, // 0: 성공, 0 이외: 실패 val msg_cd: String, @@ -254,19 +255,36 @@ data class CurrentPriceResponse( val output: CurrentPriceOutput ) +@Serializable data class CurrentPriceOutput( - val stck_prpr: String, // 주식 현재가 - val prdy_vrss: String, // 전일 대비 - val prdy_ctrt: String, // 전일 대비율 - val acml_vol: String, // 누적 거래량 - val stck_oprc: String, // 시가 - val stck_hgpr: String, // 고가 - val stck_lwpr: String, // 저가 - val hts_avls: String, // 시가총액 + // --- 📊 기존 시세 및 기본 펀더멘털 필드 --- + val stck_shrn_iscd: String, // 종목 코드 + val stck_prpr: String, // 주식 현재가 + val prdy_vrss: String, // 전일 대비 + val prdy_ctrt: String, // 전일 대비율 + val acml_vol: String, // 누적 거래량 + val stck_oprc: String, // 시가 + val stck_hgpr: String, // 고가 + val stck_lwpr: String, // 저가 + val hts_avls: String, // 시가총액 val per: String, val pbr: String, - val stck_shrn_iscd: String // 종목 코드 - // ... 필요한 필드가 있다면 Python 모델을 참고하여 추가하세요. + + // --- 🚨 신규 추가: 리스크 필터링 및 상태 필드 --- + val rprs_mrkt_kor_name: String, // 대표 시장 한글 명 (KOSPI, KOSDAQ 등) + val iscd_stat_cls_code: String, // 종목 상태 구분 코드 (51:관리, 52:위험, 58:정지 등) + val mrkt_warn_cls_code: String, // 시장경고코드 (보통 "00"이 정상) + val short_over_yn: String, // 단기과열여부 (Y/N) + val sltr_yn: String, // 정리매매여부 (Y/N) + val mang_issu_cls_code: String, // 관리종목여부 (1/0 또는 Y/N) + val temp_stop_yn: String, // 임시 정지 여부 (Y/N) + val invt_caful_yn: String // 투자유의여부 (Y/N) +) + +// 필터링 결과를 담을 객체 +data class RiskValidationResult( + val isSafe: Boolean, + val rejectReason: String = "" ) diff --git a/src/main/kotlin/network/KisTradeService.kt b/src/main/kotlin/network/KisTradeService.kt index c3b5994..0101f69 100644 --- a/src/main/kotlin/network/KisTradeService.kt +++ b/src/main/kotlin/network/KisTradeService.kt @@ -570,14 +570,19 @@ object KisTradeService { if (response.status.isSuccess()) { val body = response.body() if (body.rt_cd == "0") { + println("${body.output}") Result.success(body.output) } else { + println("API 에러: ${body.msg1}") Result.failure(Exception("API 에러: ${body.msg1}")) } } else { + println("HTTP 에러: ${response.status}") Result.failure(Exception("HTTP 에러: ${response.status}")) } } catch (e: Exception) { + e.printStackTrace() + println("HTTP 에러: ${e.message}") Result.failure(e) } } diff --git a/src/main/kotlin/service/AutoTradingManager.kt b/src/main/kotlin/service/AutoTradingManager.kt index d4d1531..91ab9fb 100644 --- a/src/main/kotlin/service/AutoTradingManager.kt +++ b/src/main/kotlin/service/AutoTradingManager.kt @@ -7,6 +7,7 @@ import Defines.LLM_PORT import TradingLogStore import TradingLogStore.noticeFilter import analyzer.AdvancedTradeAssistant +import analyzer.RiskManager import analyzer.TechnicalAnalyzer import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -1147,19 +1148,35 @@ object AutoTradingManager { val today = dailyData.lastOrNull() ?: null var rate = today?.getFluctuationRate() ?: 0.0 val isOk = ((rate < KisSession.tradeConfig.plusFilter) && (rate > (abs(KisSession.tradeConfig.minusFilter) * -1))) - println("${stock.name}[${stock.code}] 변동률 : ${rate} , 거래 기준 : ${isOk}") - if (today == null) { - failList.add(stock.code) + delay(50) +// 1. var 대신 val을 사용해야 아래에서 스마트 캐스트가 작동하여 !!를 안 써도 됩니다. + val currentStock = KisTradeService.fetchCurrentPrice(stock.code).getOrNull() + + if (today == null || currentStock == null) { +// failList.add(stock.code) print("-> 금일 금액 조회 실패 | ${isOk}") return@withTimeout } - val currentPrice = today.stck_prpr.toDouble() + // 3. 위에서 확실하게 null 체크를 했으므로, 이제 currentStock은 절대 null이 아닙니다. +// 안전하게(Safe call ? 없이) 바로 접근 가능합니다. + val currentPrice = currentStock.stck_prpr.toDouble() + println("${stock.name}[${stock.code}] 현재가 : ${currentPrice} , 변동률 : ${rate} , 거래 기준 : ${isOk}") + +// 4. 위험한 !! 단언 기호 없이 깔끔하게 호출 + val riskResult = RiskManager.evaluateRisk(currentStock) + if (!riskResult.isSafe) { + print("-> ${stock.name}[${stock.code}] 검문소 탈락: ${riskResult.rejectReason}") + return@withTimeout + } + if (!isOk || (myCash > 10L && currentPrice > myCash) || currentPrice > maxBudget || currentPrice > maxPrice || currentPrice < minPrice) { print("-> [${stock.name}] 가격 정책으로 제외 [1주:${currentPrice}, 자산:${myCash}, 최소 기준:${minPrice}, 최대 기준:${maxPrice}] | ") return@withTimeout } + + // 🌟 [추가] 고도화된 사전 필터링 (검문소) val tempAnalyzer = TechnicalAnalyzer().apply { this.daily = dailyData } @@ -1258,6 +1275,7 @@ object AutoTradingManager { println("✅ [분석 종료] ${stock.name} (${LocalTime.now()})") } } catch (e: Exception) { + e.printStackTrace() println("❌ [Stock Error] ${stock.name}: ${e.message}") } }