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

View File

@ -255,6 +255,9 @@ class TradeConfig {
var excuteCountOnMin : Int = 2
var autoSellOrder : Boolean = false
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)) {
// 로그를 남기고 싶다면 주석 해제, 아니면 조용히 패스
// logTime(stockName, "재무 미달 (캐시) 조기 종료", 0, System.currentTimeMillis() - totalStartTime)
println("재무 안정성 부족 (캐시)")
result(tradingDecision.apply { decision = "HOLD"; reason = "재무 안정성 부족 (캐시)" }, false)
return@coroutineScope
}

View File

@ -38,6 +38,7 @@ import network.KisAuthService
import network.KisTradeService
import network.KisWebSocketManager
import network.RagService
import network.RagService.isSafetyBeltStockCodes
import network.StockUniverseLoader
import report.SnapshotType
import report.TradingReportManager
@ -108,9 +109,9 @@ object AutoTradingManager {
if (KisSession.isAvailBuyTime(now) && isSuccess && completeTradingDecision != null) {
val decision = completeTradingDecision
println("${decision.stockName} ${decision.decision} ${buysCodes.contains(decision.stockCode) == false}")
// 1. 이미 AI가 결정한 decision과 confidence를 신뢰함
if (decision.decision == "BUY") {
val minScore = KisSession.config.getValues(ConfigIndex.MIN_PURCHASE_SCORE_INDEX)
if (decision.decision == "BUY" && buysCodes.contains(decision.stockCode) == false) {
// AI가 이미 검증한 등급을 사용 (재계산 불필요)
val grade = decision.investmentGrade ?: InvestmentGrade.LEVEL_1_SPECULATIVE
@ -118,8 +119,14 @@ object AutoTradingManager {
// 2. 최종 매수 실행
val gradeRate = KisSession.config.getValues(grade.allocationRate)
val maxBudget = KisSession.config.getValues(ConfigIndex.MAX_BUDGET_INDEX) * gradeRate
val calculatedQty = (maxBudget / decision.currentPrice).toInt().coerceAtLeast(1)
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(
decision = decision,
orderQty = calculatedQty.toString(),
@ -202,7 +209,7 @@ object AutoTradingManager {
rawGrade
}
}
var buysCodes = arrayListOf<String>()
fun excuteTrade(decision: TradingDecision, orderQty: String, profitRate1: Double?, investmentGrade: InvestmentGrade = InvestmentGrade.LEVEL_2_HIGH_RISK) {
scope.launch {
var basePrice = decision.currentPrice
@ -219,33 +226,41 @@ object AutoTradingManager {
AutoTradingManager.addToReanalysis(RankingStock(mksc_shrn_iscd = stockCode,hts_kor_isnm = stockName))
TradingLogStore.addWatchLog(decision,"WATCH","매수 실패 : 최대 보유 종목 도달로 신규 매수 일시 중단 => 재분석 대기열에 추가")
} else if (KisSession.isAvailBuyTime(LocalTime.now())){
println("basePrice : $basePrice, oneTickLowerPrice : $oneTickLowerPrice, finalPrice : $finalPrice")
KisTradeService.postOrder(stockCode, orderQty, finalPrice.toLong().toString(), isBuy = true)
println("basePrice : $basePrice, oneTickLowerPrice : $oneTickLowerPrice, finalPrice : $finalPrice hasStocks : ${stockCode.contains(stockCode)}" )
var realOrderQty = orderQty
KisTradeService.postOrder(stockCode, realOrderQty, finalPrice.toLong().toString(), isBuy = true)
.onSuccess { realOrderNo ->
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
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 inputQty = orderQty.replace(",", "").toIntOrNull() ?: 0
DatabaseFactory.saveAutoTrade(AutoTradeItem(
orderNo = realOrderNo,
code = stockCode,
name = stockName,
quantity = inputQty,
profitRate = effectiveProfitRate,
stopLossRate = sRate,
targetPrice = calculatedTarget,
stopLossPrice = calculatedStop,
status = "PENDING_BUY",
isDomestic = true
))
DatabaseFactory.saveAutoTrade(
AutoTradeItem(
orderNo = realOrderNo,
code = stockCode,
name = stockName,
quantity = inputQty,
profitRate = effectiveProfitRate,
stopLossRate = sRate,
targetPrice = calculatedTarget,
stopLossPrice = calculatedStop,
status = "PENDING_BUY",
isDomestic = true
)
)
TradingReportManager.recordTradeDecision(
orderNo = realOrderNo,
@ -260,16 +275,34 @@ object AutoTradingManager {
syncAndExecute(realOrderNo)
// 💡 [개선 3] 감시 설정 로그에도 등급 정보 노출
TradingLogStore.addLog(decision, "BUY", "[${investmentGrade.displayName}] 매수 및 감시 설정 완료 (목표 수익률: ${String.format("%.4f", effectiveProfitRate)}%): $realOrderNo")
TradingLogStore.addLog(
decision,
"BUY",
"[${investmentGrade.displayName}] 매수 및 감시 설정 완료 (목표 수익률: ${
String.format(
"%.4f",
effectiveProfitRate
)
}%): $realOrderNo"
)
}
.onFailure {
println("매수 실패: ${it.message} ${stockCode} $orderQty $finalPrice")
if (it.message?.contains("주문가능금액을 초과") == true) {
AutoTradingManager.addToReanalysis(RankingStock(mksc_shrn_iscd = stockCode,hts_kor_isnm = stockName))
TradingLogStore.addWatchLog(decision,"WATCH","${it.message ?: " 매수 실패"} => 재분석 대기열에 추가")
AutoTradingManager.addToReanalysis(
RankingStock(
mksc_shrn_iscd = stockCode,
hts_kor_isnm = stockName
)
)
TradingLogStore.addWatchLog(
decision,
"WATCH",
"${it.message ?: " 매수 실패"} => 재분석 대기열에 추가"
)
} else {
TradingLogStore.addLog(decision,"BUY",it.message ?: "매수 실패")
TradingLogStore.addLog(decision, "BUY", it.message ?: "매수 실패")
}
}
} else {
@ -342,7 +375,6 @@ object AutoTradingManager {
// ✅ 2. 매도 완료 시점 (실제 매도 체결가)
val actualSellPrice = execData.price.toDoubleOrNull() ?: 0.0
val actualSellQty = execData.qty.toIntOrNull() ?: dbItem.quantity
// 💡 매도 주문번호에 대해 '진짜 판 가격'을 기록
TradingReportManager.updateExecution(orderNo, actualSellPrice, actualSellQty)
@ -534,7 +566,7 @@ object AutoTradingManager {
} else {
var errMsg = ""
var isSuccess = false
if (KisSession.tradeConfig.autoSellOrder
if (KisSession.tradeConfig.autoSellOrder
&& holding != null && holding.quantity.toInt() > 0
&& holding.availOrderCount.toInt() > 0
&& holding.profitRate.toDouble() <= -15.0
@ -561,34 +593,34 @@ object AutoTradingManager {
"매수가 기준 (${holding.avgPrice.toDouble()} 3호가 위[${targetPrice}] 매도 주문 ${if (isSuccess) "성공" else "실패[${errMsg}]"}"
)
} else if (KisSession.config.stop_Loss
&& holding != null && holding.quantity.toInt() > 0
&& holding.availOrderCount.toInt() > 0
&& holding.profitRate.toDouble() <= KisSession.config.getValues(ConfigIndex.LOSS_MINRATE)
&& holding.profitRate.toDouble() >= KisSession.config.getValues(ConfigIndex.LOSS_MAXRATE)
&& holding.valuationProfitAmount.toDouble() >= KisSession.config.getValues(ConfigIndex.LOSS_MAX_MONEY)) {
println("${holding.name} ${holding.profitRate.toDouble()} ${holding.valuationProfitAmount.toDouble()} ${KisSession.config.getValues(ConfigIndex.LOSS_MAX_MONEY)} , ${KisSession.config.getValues(ConfigIndex.LOSS_MINRATE)} , ${KisSession.config.getValues(ConfigIndex.STOP_LOSS)}")
val profit = holding.profitRate.toDouble()
var targetPrice = holding.currentPrice.toDouble()
targetPrice = MarketUtil.roundToTickSize(targetPrice + MarketUtil.getTickSize(targetPrice) * 3.0)
&& holding != null && holding.quantity.toInt() > 0
&& holding.availOrderCount.toInt() > 0
&& holding.profitRate.toDouble() <= KisSession.config.getValues(ConfigIndex.LOSS_MINRATE)
&& holding.profitRate.toDouble() >= KisSession.config.getValues(ConfigIndex.LOSS_MAXRATE)
&& holding.valuationProfitAmount.toDouble() >= KisSession.config.getValues(ConfigIndex.LOSS_MAX_MONEY)) {
println("${holding.name} ${holding.profitRate.toDouble()} ${holding.valuationProfitAmount.toDouble()} ${KisSession.config.getValues(ConfigIndex.LOSS_MAX_MONEY)} , ${KisSession.config.getValues(ConfigIndex.LOSS_MINRATE)} , ${KisSession.config.getValues(ConfigIndex.STOP_LOSS)}")
val profit = holding.profitRate.toDouble()
var targetPrice = holding.currentPrice.toDouble()
targetPrice = MarketUtil.roundToTickSize(targetPrice + MarketUtil.getTickSize(targetPrice) * 3.0)
tradeService.postOrder(
stockCode = holding.code,
qty = holding.availOrderCount,
price = targetPrice.toInt().toString(),
isBuy = false,
).onSuccess { newOrderNo ->
println("✅ [보유 주식 손절 처리] 수익률($profit%) -> ${holding.valuationProfitAmount} 손해 중이며 현제 손절 가이드에 적합함 시장가 매도.")
}.onFailure { err->
println("✅ [보유 주식 손절 처리] 실패 ${err.message}")
}
tradeService.postOrder(
stockCode = holding.code,
qty = holding.availOrderCount,
price = targetPrice.toInt().toString(),
isBuy = false,
).onSuccess { newOrderNo ->
println("✅ [보유 주식 손절 처리] 수익률($profit%) -> ${holding.valuationProfitAmount} 손해 중이며 현제 손절 가이드에 적합함 시장가 매도.")
}.onFailure { err->
println("✅ [보유 주식 손절 처리] 실패 ${err.message}")
}
TradingLogStore.addNotice(
"보유주식[${holding.name}]",
holding.code,
"수익률($profit%) -> ${holding.valuationProfitAmount} 손해 중이며 현제 손절 가이드에 적합함 시장가 매도."
)
}
TradingLogStore.addNotice(
"보유주식[${holding.name}]",
holding.code,
"수익률($profit%) -> ${holding.valuationProfitAmount} 손해 중이며 현제 손절 가이드에 적합함 시장가 매도."
)
}
analyzeDeepLossHoldingsAfterMarket(holding , true)
}
delay(200) // API 호출 부하 방지
@ -914,7 +946,20 @@ object AutoTradingManager {
candidates.addAll(reanalysisList)
}
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 })
} else {
println("미확인 데이터 ${remainingCandidates.size}")
@ -1001,6 +1046,7 @@ object AutoTradingManager {
}
if (isExecuted) { executionCountMap[timeKey] = currentCount + 1 }
if (now.hour >= 20) {
buysCodes.clear()
executionCountMap.clear()
noticeFilter.clear()
}
@ -1034,7 +1080,8 @@ object AutoTradingManager {
this.stockName = stock.name
}, 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
if (today == null) {
failList.add(stock.code)
@ -1043,29 +1090,46 @@ object AutoTradingManager {
}
val currentPrice = today.stck_prpr.toDouble()
if (currentPrice > myCash || currentPrice > maxBudget || currentPrice > maxPrice || currentPrice < minPrice) {
if (currentPrice > myCash || currentPrice > maxBudget || currentPrice > maxPrice || currentPrice < minPrice) {
print("-> 가격 정책으로 제외 [1주:${currentPrice}, 자산:${myCash}, 최소 기준:${minPrice}, 최대 기준:${maxPrice}] | ")
return@withTimeout
}
println("🔍 [분석 진입] ${stock.name} (${LocalTime.now()})")
if (!isSafetyBeltStockCodes.contains(stock.code)) {
val analyzer = coroutineScope {
val min30 = async { tradeService.fetchChartData(stock.code, true).getOrDefault(emptyList()) }
val weekly = async { tradeService.fetchPeriodChartData(stock.code, "W", true).getOrDefault(emptyList()) }
val monthly = async { tradeService.fetchPeriodChartData(stock.code, "M", true).getOrDefault(emptyList()) }
TechnicalAnalyzer().apply {
this.daily = dailyData
this.min30 = min30.await()
this.weekly = weekly.await()
this.monthly = monthly.await()
val analyzer = coroutineScope {
val min30 = async { tradeService.fetchChartData(stock.code, true).getOrDefault(emptyList()) }
delay(20)
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 {
this.daily = dailyData
delay(50)
this.min30 = min30.await()
delay(50)
this.weekly = weekly.await()
delay(50)
this.monthly = monthly.await()
}
}
}
if (analyzer.isValid()) {
RagService.processStock(currentPrice, analyzer, stock.name, stock.code) { decision, isSuccess ->
callback(decision?.apply { this.currentPrice = currentPrice }, isSuccess)
if (analyzer.isValid()) {
println("✅ [분석 시작] ${stock.name} (${LocalTime.now()} 분석 데이터 정합성 -> ${analyzer.isValid()})")
RagService.processStock(currentPrice, analyzer, stock.name, stock.code) { decision, isSuccess ->
callback(decision?.apply { this.currentPrice = currentPrice }, isSuccess)
}
} else {
println("✅ [분석 실패] ${stock.name} (${LocalTime.now()} 분석 데이터 정합성 -> ${analyzer.isValid()})")
}
} else {
println("재무 안정성 부족 (캐시)")
}
println("✅ [분석 종료] ${stock.name} (${LocalTime.now()})")
}