This commit is contained in:
lunaticbum 2026-05-26 11:20:37 +09:00
parent e94869b9e7
commit 1cca11edc4
4 changed files with 138 additions and 71 deletions

View File

@ -624,7 +624,7 @@ object TradingLogStore {
val lastSentTime = noticeFilter[code.uppercase()] val lastSentTime = noticeFilter[code.uppercase()]
// 기록이 없거나(처음 보냄), 마지막 발송 기준 30분이 지났다면 // 기록이 없거나(처음 보냄), 마지막 발송 기준 30분이 지났다면
if (lastSentTime == null || (current - lastSentTime > 1000 * 60 * 30L)) { if (lastSentTime == null || (current - lastSentTime > (1000 * 60) * KisSession.tradeConfig.noticeGapTime)) {
isSendable = true isSendable = true
noticeFilter[code.uppercase()] = current // 즉시 발송 시간 갱신하여 중복 방지 noticeFilter[code.uppercase()] = current // 즉시 발송 시간 갱신하여 중복 방지
} }

View File

@ -255,6 +255,9 @@ class TradeConfig {
var excuteCountOnMin : Int = 2 var excuteCountOnMin : Int = 2
var autoSellOrder : Boolean = false var autoSellOrder : Boolean = false
var excuteMinCheck : Int = 2 var excuteMinCheck : Int = 2
var noticeGapTime : Int = 60
var lowerAveragePrice : Boolean = false
var lowerAverageStockCount : Int = 1
} }

View File

@ -250,7 +250,7 @@ object RagService {
if (isSafetyBeltStockCodes.contains(stockCode)) { if (isSafetyBeltStockCodes.contains(stockCode)) {
// 로그를 남기고 싶다면 주석 해제, 아니면 조용히 패스 // 로그를 남기고 싶다면 주석 해제, 아니면 조용히 패스
// logTime(stockName, "재무 미달 (캐시) 조기 종료", 0, System.currentTimeMillis() - totalStartTime) println("재무 안정성 부족 (캐시)")
result(tradingDecision.apply { decision = "HOLD"; reason = "재무 안정성 부족 (캐시)" }, false) result(tradingDecision.apply { decision = "HOLD"; reason = "재무 안정성 부족 (캐시)" }, false)
return@coroutineScope return@coroutineScope
} }

View File

@ -38,6 +38,7 @@ import network.KisAuthService
import network.KisTradeService import network.KisTradeService
import network.KisWebSocketManager import network.KisWebSocketManager
import network.RagService import network.RagService
import network.RagService.isSafetyBeltStockCodes
import network.StockUniverseLoader import network.StockUniverseLoader
import report.SnapshotType import report.SnapshotType
import report.TradingReportManager import report.TradingReportManager
@ -108,9 +109,9 @@ object AutoTradingManager {
if (KisSession.isAvailBuyTime(now) && isSuccess && completeTradingDecision != null) { if (KisSession.isAvailBuyTime(now) && isSuccess && completeTradingDecision != null) {
val decision = completeTradingDecision val decision = completeTradingDecision
println("${decision.stockName} ${decision.decision} ${buysCodes.contains(decision.stockCode) == false}")
// 1. 이미 AI가 결정한 decision과 confidence를 신뢰함 // 1. 이미 AI가 결정한 decision과 confidence를 신뢰함
if (decision.decision == "BUY") { if (decision.decision == "BUY" && buysCodes.contains(decision.stockCode) == false) {
val minScore = KisSession.config.getValues(ConfigIndex.MIN_PURCHASE_SCORE_INDEX)
// AI가 이미 검증한 등급을 사용 (재계산 불필요) // AI가 이미 검증한 등급을 사용 (재계산 불필요)
val grade = decision.investmentGrade ?: InvestmentGrade.LEVEL_1_SPECULATIVE val grade = decision.investmentGrade ?: InvestmentGrade.LEVEL_1_SPECULATIVE
@ -118,8 +119,14 @@ object AutoTradingManager {
// 2. 최종 매수 실행 // 2. 최종 매수 실행
val gradeRate = KisSession.config.getValues(grade.allocationRate) val gradeRate = KisSession.config.getValues(grade.allocationRate)
val maxBudget = KisSession.config.getValues(ConfigIndex.MAX_BUDGET_INDEX) * gradeRate val maxBudget = KisSession.config.getValues(ConfigIndex.MAX_BUDGET_INDEX) * gradeRate
val calculatedQty = (maxBudget / decision.currentPrice).toInt().coerceAtLeast(1)
TradingLogStore.addLog(decision,"BUY",decision.summary()) TradingLogStore.addLog(decision,"BUY",decision.summary())
var hasCodes = currentBalance?.getHoldings()?.any { it.code.equals(decision.stockCode) && it.quantity.toInt() > 2 && it.isTodayEntry == false}
if (hasCodes == true) {
buysCodes.add(decision.stockCode)
TradingLogStore.addNotice(decision.stockName,decision.stockCode,"물타기 시도 1주 매수")
}
val calculatedQty = if(hasCodes == true) KisSession.tradeConfig.lowerAverageStockCount else (maxBudget / decision.currentPrice).toInt().coerceAtLeast(1)
excuteTrade( excuteTrade(
decision = decision, decision = decision,
orderQty = calculatedQty.toString(), orderQty = calculatedQty.toString(),
@ -202,7 +209,7 @@ object AutoTradingManager {
rawGrade rawGrade
} }
} }
var buysCodes = arrayListOf<String>()
fun excuteTrade(decision: TradingDecision, orderQty: String, profitRate1: Double?, investmentGrade: InvestmentGrade = InvestmentGrade.LEVEL_2_HIGH_RISK) { fun excuteTrade(decision: TradingDecision, orderQty: String, profitRate1: Double?, investmentGrade: InvestmentGrade = InvestmentGrade.LEVEL_2_HIGH_RISK) {
scope.launch { scope.launch {
var basePrice = decision.currentPrice var basePrice = decision.currentPrice
@ -219,22 +226,29 @@ object AutoTradingManager {
AutoTradingManager.addToReanalysis(RankingStock(mksc_shrn_iscd = stockCode,hts_kor_isnm = stockName)) AutoTradingManager.addToReanalysis(RankingStock(mksc_shrn_iscd = stockCode,hts_kor_isnm = stockName))
TradingLogStore.addWatchLog(decision,"WATCH","매수 실패 : 최대 보유 종목 도달로 신규 매수 일시 중단 => 재분석 대기열에 추가") TradingLogStore.addWatchLog(decision,"WATCH","매수 실패 : 최대 보유 종목 도달로 신규 매수 일시 중단 => 재분석 대기열에 추가")
} else if (KisSession.isAvailBuyTime(LocalTime.now())){ } else if (KisSession.isAvailBuyTime(LocalTime.now())){
println("basePrice : $basePrice, oneTickLowerPrice : $oneTickLowerPrice, finalPrice : $finalPrice") println("basePrice : $basePrice, oneTickLowerPrice : $oneTickLowerPrice, finalPrice : $finalPrice hasStocks : ${stockCode.contains(stockCode)}" )
var realOrderQty = orderQty
KisTradeService.postOrder(stockCode, orderQty, finalPrice.toLong().toString(), isBuy = true) KisTradeService.postOrder(stockCode, realOrderQty, finalPrice.toLong().toString(), isBuy = true)
.onSuccess { realOrderNo -> .onSuccess { realOrderNo ->
println("[${investmentGrade.displayName}] 주문 성공: $realOrderNo $stockCode $orderQty $finalPrice") println("[${investmentGrade.displayName}] 주문 성공: $realOrderNo $stockCode $orderQty $finalPrice")
TradingLogStore.addLog(decision, "BUY", "[${investmentGrade.displayName}] 주문 성공: $realOrderNo") TradingLogStore.addLog(
decision,
"BUY",
"[${investmentGrade.displayName}] 주문 성공: $realOrderNo"
)
val sRate = -1.5 val sRate = -1.5
var tax = KisSession.config.getValues(ConfigIndex.TAX_INDEX) var tax = KisSession.config.getValues(ConfigIndex.TAX_INDEX)
val effectiveProfitRate = (profitRate1 ?: KisSession.config.getValues(ConfigIndex.PROFIT_INDEX)) + tax val effectiveProfitRate =
(profitRate1 ?: KisSession.config.getValues(ConfigIndex.PROFIT_INDEX)) + tax
val calculatedTarget = MarketUtil.roundToTickSize(basePrice * (1 + effectiveProfitRate / 100.0)) val calculatedTarget =
MarketUtil.roundToTickSize(basePrice * (1 + effectiveProfitRate / 100.0))
val calculatedStop = MarketUtil.roundToTickSize(basePrice * (1 + sRate / 100.0)) val calculatedStop = MarketUtil.roundToTickSize(basePrice * (1 + sRate / 100.0))
val inputQty = orderQty.replace(",", "").toIntOrNull() ?: 0 val inputQty = orderQty.replace(",", "").toIntOrNull() ?: 0
DatabaseFactory.saveAutoTrade(AutoTradeItem( DatabaseFactory.saveAutoTrade(
AutoTradeItem(
orderNo = realOrderNo, orderNo = realOrderNo,
code = stockCode, code = stockCode,
name = stockName, name = stockName,
@ -245,7 +259,8 @@ object AutoTradingManager {
stopLossPrice = calculatedStop, stopLossPrice = calculatedStop,
status = "PENDING_BUY", status = "PENDING_BUY",
isDomestic = true isDomestic = true
)) )
)
TradingReportManager.recordTradeDecision( TradingReportManager.recordTradeDecision(
orderNo = realOrderNo, orderNo = realOrderNo,
@ -260,16 +275,34 @@ object AutoTradingManager {
syncAndExecute(realOrderNo) syncAndExecute(realOrderNo)
// 💡 [개선 3] 감시 설정 로그에도 등급 정보 노출 // 💡 [개선 3] 감시 설정 로그에도 등급 정보 노출
TradingLogStore.addLog(decision, "BUY", "[${investmentGrade.displayName}] 매수 및 감시 설정 완료 (목표 수익률: ${String.format("%.4f", effectiveProfitRate)}%): $realOrderNo") TradingLogStore.addLog(
decision,
"BUY",
"[${investmentGrade.displayName}] 매수 및 감시 설정 완료 (목표 수익률: ${
String.format(
"%.4f",
effectiveProfitRate
)
}%): $realOrderNo"
)
} }
.onFailure { .onFailure {
println("매수 실패: ${it.message} ${stockCode} $orderQty $finalPrice") println("매수 실패: ${it.message} ${stockCode} $orderQty $finalPrice")
if (it.message?.contains("주문가능금액을 초과") == true) { if (it.message?.contains("주문가능금액을 초과") == true) {
AutoTradingManager.addToReanalysis(RankingStock(mksc_shrn_iscd = stockCode,hts_kor_isnm = stockName)) AutoTradingManager.addToReanalysis(
TradingLogStore.addWatchLog(decision,"WATCH","${it.message ?: " 매수 실패"} => 재분석 대기열에 추가") RankingStock(
mksc_shrn_iscd = stockCode,
hts_kor_isnm = stockName
)
)
TradingLogStore.addWatchLog(
decision,
"WATCH",
"${it.message ?: " 매수 실패"} => 재분석 대기열에 추가"
)
} else { } else {
TradingLogStore.addLog(decision,"BUY",it.message ?: "매수 실패") TradingLogStore.addLog(decision, "BUY", it.message ?: "매수 실패")
} }
} }
} else { } else {
@ -342,7 +375,6 @@ object AutoTradingManager {
// ✅ 2. 매도 완료 시점 (실제 매도 체결가) // ✅ 2. 매도 완료 시점 (실제 매도 체결가)
val actualSellPrice = execData.price.toDoubleOrNull() ?: 0.0 val actualSellPrice = execData.price.toDoubleOrNull() ?: 0.0
val actualSellQty = execData.qty.toIntOrNull() ?: dbItem.quantity val actualSellQty = execData.qty.toIntOrNull() ?: dbItem.quantity
// 💡 매도 주문번호에 대해 '진짜 판 가격'을 기록 // 💡 매도 주문번호에 대해 '진짜 판 가격'을 기록
TradingReportManager.updateExecution(orderNo, actualSellPrice, actualSellQty) TradingReportManager.updateExecution(orderNo, actualSellPrice, actualSellQty)
@ -914,7 +946,20 @@ object AutoTradingManager {
candidates.addAll(reanalysisList) candidates.addAll(reanalysisList)
} }
reanalysisList.clear() reanalysisList.clear()
remainingCandidates.addAll(candidates.filter { it.code !in myHoldings && it.code !in pendingStocks && it.code !in executionCache.values.map { it.code } && it.code !in failList} if (KisSession.tradeConfig.lowerAveragePrice) {
currentBalance?.getHoldings()?.map {
if(!it.isTodayEntry && it.quantity.toInt() > 2) {
remainingCandidates.add(RankingStock(mksc_shrn_iscd = it.code, hts_kor_isnm = it.name))
}
}
}
remainingCandidates.addAll(candidates.filter {
if (KisSession.tradeConfig.lowerAveragePrice) { true } else {it.code !in myHoldings} &&
it.code !in pendingStocks &&
it.code !in executionCache.values.map { it.code } &&
it.code !in failList &&
it.code !in isSafetyBeltStockCodes
}
.distinctBy { it.code }) .distinctBy { it.code })
} else { } else {
println("미확인 데이터 ${remainingCandidates.size}") println("미확인 데이터 ${remainingCandidates.size}")
@ -1001,6 +1046,7 @@ object AutoTradingManager {
} }
if (isExecuted) { executionCountMap[timeKey] = currentCount + 1 } if (isExecuted) { executionCountMap[timeKey] = currentCount + 1 }
if (now.hour >= 20) { if (now.hour >= 20) {
buysCodes.clear()
executionCountMap.clear() executionCountMap.clear()
noticeFilter.clear() noticeFilter.clear()
} }
@ -1034,7 +1080,8 @@ object AutoTradingManager {
this.stockName = stock.name this.stockName = stock.name
}, false) }, false)
val dailyData = tradeService.fetchPeriodChartData(stock.code, "D", true).getOrNull() ?: return@withTimeout val dailyData =
tradeService.fetchPeriodChartData(stock.code, "D", true).getOrNull() ?: return@withTimeout
val today = dailyData.lastOrNull() ?: null val today = dailyData.lastOrNull() ?: null
if (today == null) { if (today == null) {
failList.add(stock.code) failList.add(stock.code)
@ -1049,23 +1096,40 @@ object AutoTradingManager {
} }
println("🔍 [분석 진입] ${stock.name} (${LocalTime.now()})") println("🔍 [분석 진입] ${stock.name} (${LocalTime.now()})")
if (!isSafetyBeltStockCodes.contains(stock.code)) {
val analyzer = coroutineScope { val analyzer = coroutineScope {
val min30 = async { tradeService.fetchChartData(stock.code, true).getOrDefault(emptyList()) } val min30 = async { tradeService.fetchChartData(stock.code, true).getOrDefault(emptyList()) }
val weekly = async { tradeService.fetchPeriodChartData(stock.code, "W", true).getOrDefault(emptyList()) } delay(20)
val monthly = async { tradeService.fetchPeriodChartData(stock.code, "M", true).getOrDefault(emptyList()) } val weekly =
async { tradeService.fetchPeriodChartData(stock.code, "W", true).getOrDefault(emptyList()) }
delay(20)
val monthly =
async { tradeService.fetchPeriodChartData(stock.code, "M", true).getOrDefault(emptyList()) }
delay(20)
TechnicalAnalyzer().apply { TechnicalAnalyzer().apply {
this.daily = dailyData this.daily = dailyData
delay(50)
this.min30 = min30.await() this.min30 = min30.await()
delay(50)
this.weekly = weekly.await() this.weekly = weekly.await()
delay(50)
this.monthly = monthly.await() this.monthly = monthly.await()
} }
} }
if (analyzer.isValid()) { if (analyzer.isValid()) {
println("✅ [분석 시작] ${stock.name} (${LocalTime.now()} 분석 데이터 정합성 -> ${analyzer.isValid()})")
RagService.processStock(currentPrice, analyzer, stock.name, stock.code) { decision, isSuccess -> RagService.processStock(currentPrice, analyzer, stock.name, stock.code) { decision, isSuccess ->
callback(decision?.apply { this.currentPrice = currentPrice }, isSuccess) callback(decision?.apply { this.currentPrice = currentPrice }, isSuccess)
} }
} else {
println("✅ [분석 실패] ${stock.name} (${LocalTime.now()} 분석 데이터 정합성 -> ${analyzer.isValid()})")
}
} else {
println("재무 안정성 부족 (캐시)")
} }
println("✅ [분석 종료] ${stock.name} (${LocalTime.now()})") println("✅ [분석 종료] ${stock.name} (${LocalTime.now()})")
} }