...
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user