This commit is contained in:
2026-08-10 17:23:40 +09:00
parent b83eb11cb7
commit 302802a53d
4 changed files with 105 additions and 14 deletions
+50
View File
@@ -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, "안전")
}
}
+28 -10
View File
@@ -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 = ""
)
@@ -570,14 +570,19 @@ object KisTradeService {
if (response.status.isSuccess()) {
val body = response.body<CurrentPriceResponse>()
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)
}
}
+22 -4
View File
@@ -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}")
}
}