This commit is contained in:
2026-06-19 14:31:33 +09:00
parent 8e145803d8
commit ce07537eef
5 changed files with 463 additions and 19 deletions
+58 -12
View File
@@ -110,15 +110,33 @@ object AutoTradingManager {
println("${decision.stockName} ${decision.decision}")
// 1. 이미 AI가 결정한 decision과 confidence를 신뢰함
if (decision.decision == "BUY") {
var maxRealisticProfitRate = 0.0
// AI가 이미 검증한 등급을 사용 (재계산 불필요)
val grade = decision.investmentGrade ?: InvestmentGrade.LEVEL_1_SPECULATIVE
decision.analyzer?.let { a ->
val volatility = a?.calculateVolatilityForecast(a.daily, 20)
volatility?.let {
maxRealisticProfitRate = ((volatility.realisticHigh - decision.currentPrice) / decision.currentPrice) * 100.0
}
}
// 1. 통계적으로 도달 가능한 현실적인 최대 수익률 계산 (1표준편차 상단 기준)
// 2. 시스템 기본 설정 수익률과 비교
val baseProfitRate = KisSession.config.getValues(ConfigIndex.PROFIT_INDEX) + KisSession.config.getValues(grade.profitGuide)
// 3. 스마트 익절률 결정: 시스템 설정값이 통계적 한계를 넘어서면, 통계적 한계치로 눈높이를 낮춤
val finalProfitRate = if (maxRealisticProfitRate > 0.0 && baseProfitRate > maxRealisticProfitRate) {
max(maxRealisticProfitRate ,0.05)
} else {
baseProfitRate // 변동성이 충분히 크다면 원래 시스템 설정대로 진행
}
// 2. 최종 매수 실행
val gradeRate = KisSession.config.getValues(grade.allocationRate)
val maxBudget = KisSession.config.getValues(ConfigIndex.MAX_BUDGET_INDEX) * gradeRate
TradingLogStore.addLog(decision,"BUY",decision.summary())
decision.maxRealisticProfitRate = maxRealisticProfitRate
TradingLogStore.addLog(decision,"BUY",decision.summary(KisSession.config.getValues(ConfigIndex.PROFIT_INDEX) * KisSession.config.getValues(grade.profitGuide)))
var hasCodes = KisSession.tradeConfig.lowerAveragePrice && currentBalance?.getHoldings()?.any { it.code.equals(decision.stockCode) && it.quantity.toInt() > 2 && it.availOrderCount.toInt() > 0} ?: false
if (hasCodes == true) {
TradingLogStore.addNotice(decision.stockName,decision.stockCode,"물타기 시도 1주 매수")
@@ -127,7 +145,7 @@ object AutoTradingManager {
excuteTrade(
decision = decision,
orderQty = calculatedQty.toString(),
profitRate1 = KisSession.config.getValues(ConfigIndex.PROFIT_INDEX) * KisSession.config.getValues(grade.profitGuide),
profitRate1 = finalProfitRate,
investmentGrade = grade,
hasCode = hasCodes == true
)
@@ -278,9 +296,9 @@ object AutoTradingManager {
reason = decision.reason ?: "", // AI 이유
decision = decision // AI 객체 통째로 전달
)
syncAndExecute(realOrderNo)
if (!hasCode) {
syncAndExecute(realOrderNo)
}
// 💡 [개선 3] 감시 설정 로그에도 등급 정보 노출
TradingLogStore.addLog(
decision,
@@ -340,6 +358,7 @@ object AutoTradingManager {
if (processingIds.contains(orderNo)) return
processingIds.add(orderNo)
try {
val dbItem = DatabaseFactory.findByOrderNo(orderNo)
val execData = executionCache[orderNo]
@@ -1070,7 +1089,8 @@ object AutoTradingManager {
println("⏰ [강제 스케줄 실행] 오전 9시 ${currentMinute}분 - 보유주식 매도 체크를 시작합니다.")
checkBalance()
isExecuted = true
} else if (((now.hour == 8 && KisSession.tradeConfig.before_nxt && currentMinute < 45) || (now.hour >= 16 && now.hour < 20 && KisSession.tradeConfig.after_nxt)) && (currentMinute % 2 == 1)) {
} else if (((now.hour == 8 && KisSession.tradeConfig.before_nxt && currentMinute < 45) ||
(now.hour >= 16 && now.hour < 20 && KisSession.tradeConfig.after_nxt)) && (currentMinute % 2 == 0)) {
TradingLogStore.addAnalyzer(
" - ",
" - ",
@@ -1141,12 +1161,37 @@ object AutoTradingManager {
return@withTimeout
}
// 🌟 [추가] 고도화된 사전 필터링 (검문소)
val tempAnalyzer = TechnicalAnalyzer().apply { this.daily = dailyData }
// 1. 변동성 기반 수익률 검증 (2% 이상 열려있는가?)
val volatility = tempAnalyzer.calculateVolatilityForecast(dailyData, 20)
val expectedProfitRate = ((volatility.realisticHigh - currentPrice) / currentPrice) * 100.0
// 2. 일봉 기준 반등 주기 통계 추출 (일주일 내 승부 가능한가?)
val dailyStats = tempAnalyzer.calculateDynamicReboundStats(dailyData)
val isApproaching = tempAnalyzer.checkReboundApproaching(
candles = dailyData,
avgReboundTerm = dailyStats.avgReboundPeriod,
dropThreshold = dailyStats.avgDropRate * 0.8,
timeTolerance = dailyStats.timeTolerance
)
val isSteadyUptrend = tempAnalyzer.checkSteadyUptrend(dailyData)
// 🌟 [수정] 조건 통합 (OR 조건)
val isProfitable = expectedProfitRate >= 2.0 || dailyStats.avgReboundAmplitude >= 2.0
// 반등 주기에 도달했거나(Mean Reversion), 안정적으로 뻗어나가는 우상향 종목(Trend Following)이면 통과
val isValidEntryTiming = (dailyStats.isValid && isApproaching && dailyStats.avgReboundPeriod <= 10.0 && dailyStats.avgReboundPeriod >= 1.5) || isSteadyUptrend
if (!isProfitable || !isValidEntryTiming) {
print("-> [${stock.name}] 조건 미달 필터링 (예측수익: ${"%.1f".format(expectedProfitRate)}%, 주기: ${"%.1f".format(dailyStats.avgReboundPeriod)}일, 진입권: $isApproaching) | ")
return@withTimeout // 조건에 맞지 않으면 주봉/월봉 API 호출 및 LLM 분석 없이 즉시 다음 종목으로 넘어감
}
println("🔍 [분석 진입] ${stock.name} (${LocalTime.now()})")
if (!isSafetyBeltStockCodes.contains(stock.code)) {
val analyzer = coroutineScope {
val min30 = async { tradeService.fetchChartData(stock.code, true).getOrDefault(emptyList()) }
delay(20)
@@ -1167,6 +1212,7 @@ object AutoTradingManager {
}
}
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)