....
This commit is contained in:
@@ -671,7 +671,7 @@ $standardizedScores
|
|||||||
|
|
||||||
// 바닥권 인정 마진 (ATR 기반)
|
// 바닥권 인정 마진 (ATR 기반)
|
||||||
//B. 바닥권 판정 마진predictDropBottom currentAtr * 0.7 여유 마진 마진을 축소(currentAtr * 0.3)하여 예상 바닥에 더 근접해야 인정
|
//B. 바닥권 판정 마진predictDropBottom currentAtr * 0.7 여유 마진 마진을 축소(currentAtr * 0.3)하여 예상 바닥에 더 근접해야 인정
|
||||||
val bottomMargin = currentAtr * 0.5
|
val bottomMargin = currentAtr * 0.6
|
||||||
|
|
||||||
// 저점 평균(smoothedBottom)도 마지노선 계산에 추가 가중치로 섞어줌으로써 저점에 더 밀착시킴
|
// 저점 평균(smoothedBottom)도 마지노선 계산에 추가 가중치로 섞어줌으로써 저점에 더 밀착시킴
|
||||||
val adjustedExtremeLow = (volatility.extremeLow + smoothedBottom) / 2.0
|
val adjustedExtremeLow = (volatility.extremeLow + smoothedBottom) / 2.0
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ class TradingDecision {
|
|||||||
var signalModel : ScalpingSignalModel? = null
|
var signalModel : ScalpingSignalModel? = null
|
||||||
var maxRealisticProfitRate :Double = 0.0
|
var maxRealisticProfitRate :Double = 0.0
|
||||||
var reboundDaysDaily: Double = 0.0 // 일봉 기준 평균 반등 소요일
|
var reboundDaysDaily: Double = 0.0 // 일봉 기준 평균 반등 소요일
|
||||||
|
var isWatering: Boolean = false
|
||||||
var reboundWeeksWeekly: Double = 0.0 // 주봉 기준 평균 반등 소요주
|
var reboundWeeksWeekly: Double = 0.0 // 주봉 기준 평균 반등 소요주
|
||||||
var isReboundApproaching: Boolean = false // 반등 주기에 근접했는지 여부
|
var isReboundApproaching: Boolean = false // 반등 주기에 근접했는지 여부
|
||||||
var reboundGuideMessage: String = "반등 주기 데이터 없음" // UI나 로그에 노출할 가이드 메시지
|
var reboundGuideMessage: String = "반등 주기 데이터 없음" // UI나 로그에 노출할 가이드 메시지
|
||||||
|
|||||||
@@ -237,7 +237,7 @@ object RagService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun processStock(currentPrice: Double, technicalAnalyzer: TechnicalAnalyzer, stockName: String, stockCode: String, result: TradingDecisionCallback) {
|
suspend fun processStock(currentPrice: Double, technicalAnalyzer: TechnicalAnalyzer, stockName: String, stockCode: String, isWatering : Boolean, result: TradingDecisionCallback) {
|
||||||
val totalStartTime = System.currentTimeMillis()
|
val totalStartTime = System.currentTimeMillis()
|
||||||
|
|
||||||
coroutineScope {
|
coroutineScope {
|
||||||
@@ -246,6 +246,7 @@ object RagService {
|
|||||||
this.stockCode = stockCode
|
this.stockCode = stockCode
|
||||||
this.analyzer = technicalAnalyzer
|
this.analyzer = technicalAnalyzer
|
||||||
this.currentPrice = currentPrice
|
this.currentPrice = currentPrice
|
||||||
|
this.isWatering = isWatering
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isSafetyBeltStockCodes.contains(stockCode)) {
|
if (isSafetyBeltStockCodes.contains(stockCode)) {
|
||||||
@@ -277,8 +278,10 @@ object RagService {
|
|||||||
isSafetyBeltStockCodes.add(stockCode)
|
isSafetyBeltStockCodes.add(stockCode)
|
||||||
return@coroutineScope
|
return@coroutineScope
|
||||||
}
|
}
|
||||||
|
val techScore = tradingDecision.signalModel?.compositeScore ?: 0
|
||||||
if ((tradingDecision.signalModel?.compositeScore ?: 0) < 40) {
|
// 🌟 신규 매수는 40점 컷오프, 물타기는 20점(또는 제한 없음)으로 하향
|
||||||
|
val minTechCutoff = if (tradingDecision.isWatering) 15 else 40
|
||||||
|
if (techScore < minTechCutoff) {
|
||||||
logTime(stockName, "기술 점수 미달 조기 종료 ${tradingDecision.signalModel?.compositeScore} , ${tradingDecision.signalModel?.successProbPct} ", techDuration, System.currentTimeMillis() - totalStartTime)
|
logTime(stockName, "기술 점수 미달 조기 종료 ${tradingDecision.signalModel?.compositeScore} , ${tradingDecision.signalModel?.successProbPct} ", techDuration, System.currentTimeMillis() - totalStartTime)
|
||||||
if (FinancialAnalyzer.isBuyConsiderationMet(financialStmt) && financialScore > 70) {
|
if (FinancialAnalyzer.isBuyConsiderationMet(financialStmt) && financialScore > 70) {
|
||||||
TradingLogStore.addAnalyzer(stockName, stockCode, "매수 타점 미도달 (재무 우량주로 감시 지속)", true)
|
TradingLogStore.addAnalyzer(stockName, stockCode, "매수 타점 미도달 (재무 우량주로 감시 지속)", true)
|
||||||
@@ -487,9 +490,27 @@ object RagService {
|
|||||||
val synthStartTime = System.currentTimeMillis()
|
val synthStartTime = System.currentTimeMillis()
|
||||||
val sysScore100 = calculateSystemPoint(scores) * 4.0
|
val sysScore100 = calculateSystemPoint(scores) * 4.0
|
||||||
|
|
||||||
// 가중치 합성 (Tech 35% : Fin 25% : News 20% : Sys 20%)
|
val hasNews = tempDecision.newsContext != null && tempDecision.newsContext!!.isNotBlank()
|
||||||
var finalConfidence = (finScore100 * 0.25) + (techScore100 * 0.25) + (newsScore100 * 0.30) + (sysScore100 * 0.20)
|
|
||||||
// var finalConfidence = (finScore100 * 0.25) + (techScore100 * 0.35) + (newsScore100 * 0.20) + (sysScore100 * 0.20)
|
var finalConfidence = if (hasNews) {
|
||||||
|
// 뉴스가 있을 때: 원래 로직
|
||||||
|
(finScore100 * 0.25) + (techScore100 * 0.25) + (newsScore100 * 0.30) + (sysScore100 * 0.20)
|
||||||
|
} else {
|
||||||
|
// 뉴스가 없을 때: 기술 45%, 재무 35%, 시스템 20%로 배분 (뉴스 영향력 제거)
|
||||||
|
(finScore100 * 0.35) + (techScore100 * 0.45) + (sysScore100 * 0.20)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tempDecision.isWatering) {
|
||||||
|
// 물타기 대상이 여기까지 왔다는 건 '볼린저 하단 터치'나 '초과매도(RSI<35)' 등 바닥 확인이 끝났다는 뜻임.
|
||||||
|
// 깎인 기술 점수를 보완하기 위해 강한 턴어라운드 기대 가점을 부여
|
||||||
|
finalConfidence += 20.0
|
||||||
|
|
||||||
|
// 물타기 전용 등급 강제 상향 (잡주가 아니라는 전제 하에)
|
||||||
|
if (finalConfidence >= KisSession.config.getValues(ConfigIndex.MIN_PURCHASE_SCORE_INDEX) * 0.8) {
|
||||||
|
// 물타기 전용 등급(예: 신규 매수 로직에 안 잡히게 LEVEL_3 정도로 고정)
|
||||||
|
tempDecision.investmentGrade = InvestmentGrade.LEVEL_3_CAUTIOUS_RECOMMEND
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 보너스 및 패널티 로직
|
// 보너스 및 패널티 로직
|
||||||
if (finScore100 >= 80.0 && techScore100 >= 70.0) finalConfidence += 8.0
|
if (finScore100 >= 80.0 && techScore100 >= 70.0) finalConfidence += 8.0
|
||||||
@@ -569,6 +590,7 @@ object RagService {
|
|||||||
this.stockName = stockName
|
this.stockName = stockName
|
||||||
this.currentPrice = tempDecision.currentPrice
|
this.currentPrice = tempDecision.currentPrice
|
||||||
this.techSummary = tempDecision.techSummary
|
this.techSummary = tempDecision.techSummary
|
||||||
|
this.isWatering = tempDecision.isWatering
|
||||||
this.ultraShortScore = scores.ultraShort.toDouble()
|
this.ultraShortScore = scores.ultraShort.toDouble()
|
||||||
this.shortTermScore = scores.shortTerm.toDouble()
|
this.shortTermScore = scores.shortTerm.toDouble()
|
||||||
this.midTermScore = scores.midTerm.toDouble()
|
this.midTermScore = scores.midTerm.toDouble()
|
||||||
|
|||||||
@@ -47,6 +47,8 @@ import java.time.LocalDate
|
|||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
import java.time.LocalTime
|
import java.time.LocalTime
|
||||||
import java.time.ZoneId
|
import java.time.ZoneId
|
||||||
|
import java.util.Collections
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
import java.util.concurrent.atomic.AtomicLong
|
import java.util.concurrent.atomic.AtomicLong
|
||||||
import kotlin.collections.List
|
import kotlin.collections.List
|
||||||
import kotlin.collections.filter
|
import kotlin.collections.filter
|
||||||
@@ -420,6 +422,7 @@ object AutoTradingManager {
|
|||||||
order.ord_no, // 원주문번호
|
order.ord_no, // 원주문번호
|
||||||
order.pdno
|
order.pdno
|
||||||
)
|
)
|
||||||
|
delay(200)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -436,8 +439,8 @@ object AutoTradingManager {
|
|||||||
syncAndExecute(orderNo)
|
syncAndExecute(orderNo)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val executionCache = mutableMapOf<String, ExecutionData>()
|
val executionCache = ConcurrentHashMap<String, ExecutionData>()
|
||||||
val processingIds = mutableSetOf<String>() // 주문번호 기준 잠금
|
val processingIds = Collections.newSetFromMap(ConcurrentHashMap<String, Boolean>())
|
||||||
suspend fun syncAndExecute(orderNo: String) {
|
suspend fun syncAndExecute(orderNo: String) {
|
||||||
if (processingIds.contains(orderNo)) return
|
if (processingIds.contains(orderNo)) return
|
||||||
processingIds.add(orderNo)
|
processingIds.add(orderNo)
|
||||||
@@ -1345,10 +1348,9 @@ object AutoTradingManager {
|
|||||||
|
|
||||||
fun addToReanalysis(stock: RankingStock) {
|
fun addToReanalysis(stock: RankingStock) {
|
||||||
val count = retryCountMap.getOrDefault(stock.code, 0)
|
val count = retryCountMap.getOrDefault(stock.code, 0)
|
||||||
if (count < 10) { // 최대 2회까지만 재시도하여 무한 루프 방지
|
if (count < 30) { // 최대 2회까지만 재시도하여 무한 루프 방지
|
||||||
retryCountMap[stock.code] = count + 1
|
retryCountMap[stock.code] = count + 1
|
||||||
reanalysisList.add(stock)
|
reanalysisList.add(stock)
|
||||||
// println("📝 [Memory] ${stock.name} 관망 판정 -> 차기 루프 재분석 리스트 등록")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1364,191 +1366,161 @@ object AutoTradingManager {
|
|||||||
val maxPrice = KisSession.config.getValues(ConfigIndex.MAX_PRICE_INDEX)
|
val maxPrice = KisSession.config.getValues(ConfigIndex.MAX_PRICE_INDEX)
|
||||||
val minPrice = KisSession.config.getValues(ConfigIndex.MIN_PRICE_INDEX)
|
val minPrice = KisSession.config.getValues(ConfigIndex.MIN_PRICE_INDEX)
|
||||||
|
|
||||||
// 개별 종목 분석은 최대 2분으로 제한
|
|
||||||
withTimeout(ONE_STOCK_ALYSIS_TIME) {
|
withTimeout(ONE_STOCK_ALYSIS_TIME) {
|
||||||
// -------------------------------------------------------------
|
// 1. 기업 코드 검증
|
||||||
// [1차 필터: 가벼운 메타데이터 & 정책 검증 (불필요한 API 호출 방지)]
|
|
||||||
// -------------------------------------------------------------
|
|
||||||
// 1. 종목명 및 기업 고유번호 유효성 검사
|
|
||||||
val corpInfo = DartCodeManager.getCorpCode(stock.code)
|
val corpInfo = DartCodeManager.getCorpCode(stock.code)
|
||||||
if (corpInfo?.cName.isNullOrEmpty()) {
|
if (corpInfo?.cName.isNullOrEmpty()) return@withTimeout
|
||||||
print("-> 기업명을 못찾아서 제외 | ")
|
|
||||||
return@withTimeout
|
// 🌟 [핵심] 물타기 대상 여부 플래그 식별
|
||||||
|
val targetHolding = currentBalance?.getHoldings()?.firstOrNull {
|
||||||
|
it.code == stock.code && it.quantity.toInt() > 2
|
||||||
|
}
|
||||||
|
val isWatering = targetHolding != null && KisSession.tradeConfig.lowerAveragePrice
|
||||||
|
|
||||||
|
if (isWatering) {
|
||||||
|
println("💧 [물타기 분석 모드 가동] ${stock.name} (현재 수익률: ${targetHolding?.profitRate}%)")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. [보완] 재무 건전성 미달 종목 사전 차단 (최상단으로 이동하여 차트 API 소모 방지)
|
// 2. 재무 안정성 체크 (물타기라도 상폐/자본잠식 위험주는 추가 매수 금지)
|
||||||
if (isSafetyBeltStockCodes.contains(stock.code)) {
|
if (isSafetyBeltStockCodes.contains(stock.code)) {
|
||||||
print("-> [${stock.name}] 재무 건전성 미달(안전벨트 탈락 종목) 제외 | ")
|
print("-> [${stock.name}] 재무 건전성 미달 제외 | ")
|
||||||
return@withTimeout
|
return@withTimeout
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 물타기 대상 로깅
|
|
||||||
if (currentBalance?.getHoldings()?.any { it.code == stock.code && it.quantity.toInt() > 2 } == true) {
|
|
||||||
println("물타기 대상 분석: ${stock.name}")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 초기 콜백 알림 (분석 진행 표시용)
|
|
||||||
callback(TradingDecision().apply {
|
callback(TradingDecision().apply {
|
||||||
this.stockCode = stock.code
|
this.stockCode = stock.code
|
||||||
this.confidence = -1.0
|
this.confidence = -1.0
|
||||||
this.stockName = stock.name
|
this.stockName = stock.name
|
||||||
}, false)
|
}, false)
|
||||||
|
|
||||||
// 4. [보완] 배당 전략 사용 시 사전 필터링 (복잡한 기술 분석 전 조기 탈락)
|
// 3. 배당 필터 (물타기 종목은 배당 여부와 무관하게 탈출이 우선이므로 패스)
|
||||||
if (KisSession.tradeConfig.isUpcomingDividend) {
|
if (!isWatering && KisSession.tradeConfig.isUpcomingDividend) {
|
||||||
val dividend = KisTradeService.fetchUpcomingDividend(stock.code).getOrNull()
|
val dividend = KisTradeService.fetchUpcomingDividend(stock.code).getOrNull()
|
||||||
if (dividend?.hasDividend == true) {
|
if (dividend?.hasDividend != true) return@withTimeout
|
||||||
println("[${stock.name}] 배당락일 ${dividend.exDividendDate} : ${dividend.dividendAmount}")
|
|
||||||
} else {
|
|
||||||
print("-> [${stock.name}] 배당 정보 없어 분석 종료 | ")
|
|
||||||
return@withTimeout
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------
|
|
||||||
// [2차 필터: 일봉 데이터 수집 및 현재가/호가/리스크 검증]
|
|
||||||
// -------------------------------------------------------------
|
|
||||||
val dailyData = tradeService.fetchPeriodChartData(stock.code, "D", true).getOrNull()
|
val dailyData = tradeService.fetchPeriodChartData(stock.code, "D", true).getOrNull()
|
||||||
?: return@withTimeout
|
?: return@withTimeout
|
||||||
|
|
||||||
// 5. [보완] 데이터 부족 종목 방어 (신규 상장주, 통계 최소 표본 30봉 미만 차단)
|
if (dailyData.size < 30) return@withTimeout
|
||||||
if (dailyData.size < 30) {
|
|
||||||
print("-> [${stock.name}] 캔들 데이터 부족(30봉 미만) 제외 | ")
|
|
||||||
return@withTimeout
|
|
||||||
}
|
|
||||||
|
|
||||||
val today = dailyData.lastOrNull()
|
val today = dailyData.lastOrNull() ?: return@withTimeout
|
||||||
val rate = today?.getFluctuationRate() ?: 0.0
|
val rate = today.getFluctuationRate()
|
||||||
val isOk = (rate < KisSession.tradeConfig.plusFilter) && (rate > (abs(KisSession.tradeConfig.minusFilter) * -1))
|
|
||||||
|
// 🌟 [완화 1] 당일 등락률 필터
|
||||||
|
// 신규: 좁은 박스권만 허용 | 물타기: 급락(-8%)이 아니면 대부분 허용
|
||||||
|
val isOk = if (isWatering) {
|
||||||
|
rate > -8.0 && rate < 15.0
|
||||||
|
} else {
|
||||||
|
(rate < KisSession.tradeConfig.plusFilter) && (rate > (abs(KisSession.tradeConfig.minusFilter) * -1))
|
||||||
|
}
|
||||||
|
|
||||||
delay(50)
|
delay(50)
|
||||||
val currentStock = KisTradeService.fetchCurrentPrice(stock.code).getOrNull()
|
val currentStock = KisTradeService.fetchCurrentPrice(stock.code).getOrNull() ?: return@withTimeout
|
||||||
|
|
||||||
if (today == null || currentStock == null) {
|
|
||||||
print("-> 금일 금액 조회 실패 | isOk: ${isOk} | ")
|
|
||||||
return@withTimeout
|
|
||||||
}
|
|
||||||
|
|
||||||
val currentPrice = currentStock.stck_prpr.toDouble()
|
val currentPrice = currentStock.stck_prpr.toDouble()
|
||||||
println("${stock.name}[${stock.code}] 현재가: ${currentPrice}, 변동률: ${rate}%, 거래기준(isOk): ${isOk}")
|
|
||||||
|
|
||||||
// 6. 리스크 매니저 검문소 통과 여부 확인 (관리종목, 환기종목 등)
|
|
||||||
val riskResult = RiskManager.evaluateRisk(currentStock)
|
val riskResult = RiskManager.evaluateRisk(currentStock)
|
||||||
if (!riskResult.isSafe) {
|
if (!riskResult.isSafe) return@withTimeout
|
||||||
print("-> ${stock.name}[${stock.code}] 리스크 검문소 탈락: ${riskResult.rejectReason} | ")
|
|
||||||
|
// 🌟 [완화 2] 예산 필터 (물타기는 기존 설정 수량 매수 예산만 있으면 통과)
|
||||||
|
val budgetCheck = if (isWatering) {
|
||||||
|
val waterBudget = currentPrice * KisSession.tradeConfig.lowerAverageStockCount
|
||||||
|
myCash >= waterBudget
|
||||||
|
} else {
|
||||||
|
currentPrice <= maxBudget && (myCash <= 10L || currentPrice <= myCash)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isOk || !budgetCheck || currentPrice > maxPrice || currentPrice < minPrice) {
|
||||||
|
print("-> [${stock.name}] 조건/가격 제외 (물타기여부:$isWatering) | ")
|
||||||
return@withTimeout
|
return@withTimeout
|
||||||
}
|
}
|
||||||
|
|
||||||
// 7. 계좌 예산 및 단가 정책 필터링
|
|
||||||
if (!isOk || (myCash > 10L && currentPrice > myCash) || currentPrice > maxBudget || currentPrice > maxPrice || currentPrice < minPrice) {
|
|
||||||
print("-> [${stock.name}] 가격 정책으로 제외 [1주:${currentPrice}, 자산:${myCash}, 최소:${minPrice}, 최대:${maxPrice}] | ")
|
|
||||||
return@withTimeout
|
|
||||||
}
|
|
||||||
|
|
||||||
// -------------------------------------------------------------
|
|
||||||
// [3차 필터: 기술적 통계 분석 (변동성, 반등 주기, 지하실 회피)]
|
|
||||||
// -------------------------------------------------------------
|
|
||||||
val tempAnalyzer = TechnicalAnalyzer().apply { this.daily = dailyData }
|
val tempAnalyzer = TechnicalAnalyzer().apply { this.daily = dailyData }
|
||||||
|
|
||||||
// 1. 변동성 기반 현실적 기대수익률 예측
|
|
||||||
val volatility = tempAnalyzer.calculateVolatilityForecast(dailyData, 20)
|
val volatility = tempAnalyzer.calculateVolatilityForecast(dailyData, 20)
|
||||||
val expectedProfitRate = ((volatility.realisticHigh - currentPrice) / currentPrice) * 100.0
|
val expectedProfitRate = ((volatility.realisticHigh - currentPrice) / currentPrice) * 100.0
|
||||||
|
|
||||||
// 2. 동적 반등 통계 산출
|
|
||||||
val dailyStats = tempAnalyzer.calculateDynamicReboundStats(dailyData, 5.0)
|
val dailyStats = tempAnalyzer.calculateDynamicReboundStats(dailyData, 5.0)
|
||||||
val isApproaching = tempAnalyzer.checkReboundApproaching(
|
|
||||||
candles = dailyData,
|
|
||||||
avgReboundTerm = dailyStats.avgReboundPeriod,
|
|
||||||
dropThreshold = dailyStats.avgDropRate,
|
|
||||||
timeTolerance = dailyStats.timeTolerance
|
|
||||||
)
|
|
||||||
print("-> [${stock.name}] 반등통계(주기:${"%.1f".format(dailyStats.avgReboundPeriod)}일, 평균하락폭:${"%.1f".format(dailyStats.avgDropRate)}%, 오차:${"%.1f".format(dailyStats.timeTolerance)})")
|
|
||||||
|
|
||||||
// 3. 우상향 추세(Trend Following) 판별 - 필요 시 활성화
|
// 🌟 [완화 3] 기대수익률 및 진입 타이밍 이원화
|
||||||
val isSteadyUptrend = tempAnalyzer.checkSteadyUptrend(dailyData)
|
val isProfitable: Boolean
|
||||||
|
val isValidEntryTiming: Boolean
|
||||||
|
|
||||||
// 4. 기대수익률 조건 (예측 수익 또는 과거 평균 반등폭이 기준 이상인가?)
|
if (isWatering) {
|
||||||
val isProfitable = expectedProfitRate >= KisSession.tradeConfig.minExpectedProfitRate ||
|
// 물타기: 평단 낮추기 목적이므로 기대수익률 1.0% 이상이면 충분
|
||||||
dailyStats.avgReboundAmplitude >= KisSession.tradeConfig.minExpectedProfitRate
|
isProfitable = expectedProfitRate >= 1.0 || dailyStats.avgReboundAmplitude >= 1.5
|
||||||
|
|
||||||
// 5. 진입 타이밍 조건 (통계적 반등 주기에 도달했거나, 안정적 우상향 추세인가?)
|
// 물타기 진입 타이밍: 반등 주기 도달 OR (일봉 RSI 35 이하 과매도) OR (볼린저 하단 터치)
|
||||||
val isValidEntryTiming = (dailyStats.isValid && isApproaching &&
|
val rsi = tempAnalyzer.calculateRSI(dailyData)
|
||||||
dailyStats.avgReboundPeriod <= KisSession.tradeConfig.maxExpectedReboundDays &&
|
val lowerBand = AdvancedTradeAssistant.calculateBollingerLowerBand(dailyData)
|
||||||
dailyStats.avgReboundPeriod >= KisSession.tradeConfig.minExpectedReboundDays) || isSteadyUptrend
|
val isOversoldZone = rsi <= 38.0 || (lowerBand > 0 && currentPrice <= lowerBand * 1.03)
|
||||||
|
|
||||||
|
isValidEntryTiming = isOversoldZone || (dailyStats.isValid && tempAnalyzer.checkReboundApproaching(
|
||||||
|
dailyData, dailyStats.avgReboundPeriod, dailyStats.avgDropRate, dailyStats.timeTolerance
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
// 신규 진입: 기존 엄격 기준 유지
|
||||||
|
isProfitable = expectedProfitRate >= KisSession.tradeConfig.minExpectedProfitRate ||
|
||||||
|
dailyStats.avgReboundAmplitude >= KisSession.tradeConfig.minExpectedProfitRate
|
||||||
|
|
||||||
|
val isApproaching = tempAnalyzer.checkReboundApproaching(
|
||||||
|
dailyData, dailyStats.avgReboundPeriod, dailyStats.avgDropRate, dailyStats.timeTolerance
|
||||||
|
)
|
||||||
|
isValidEntryTiming = (dailyStats.isValid && isApproaching &&
|
||||||
|
dailyStats.avgReboundPeriod <= KisSession.tradeConfig.maxExpectedReboundDays &&
|
||||||
|
dailyStats.avgReboundPeriod >= KisSession.tradeConfig.minExpectedReboundDays) ||
|
||||||
|
tempAnalyzer.checkSteadyUptrend(dailyData)
|
||||||
|
}
|
||||||
|
|
||||||
val currentAtr = tempAnalyzer.calculateATR(dailyData)
|
val currentAtr = tempAnalyzer.calculateATR(dailyData)
|
||||||
if (!isProfitable || !isValidEntryTiming) {
|
if (!isProfitable || !isValidEntryTiming) {
|
||||||
print("-> [${stock.name}] 조건 미달 필터링 (예측수익:${"%.1f".format(expectedProfitRate)}%, 주기:${"%.1f".format(dailyStats.avgReboundPeriod)}일, 진입권:$isValidEntryTiming) | ")
|
print("-> [${stock.name}] 조건 미달 (물타기:$isWatering, 수익:$isProfitable, 타이밍:$isValidEntryTiming) | ")
|
||||||
return@withTimeout
|
return@withTimeout
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. [보완] 바닥 예측 및 지하실(추가 낙폭) 회피 로직
|
// 🌟 [완화 4] 지하실 방어 및 브레이크 확인
|
||||||
val dropPrediction = tempAnalyzer.predictDropBottom(dailyData, dailyStats, volatility, currentAtr)
|
val dropPrediction = tempAnalyzer.predictDropBottom(dailyData, dailyStats, volatility, currentAtr)
|
||||||
if (dropPrediction != null) {
|
if (dropPrediction != null) {
|
||||||
// (1) 부호 혼선 방지: 현재가와 예상 바닥가 간의 실제 추가 하락 여력(%) 직관적 계산
|
|
||||||
val distanceToBottomPct = ((currentPrice - dropPrediction.expectedBottomPrice) / currentPrice) * 100.0
|
val distanceToBottomPct = ((currentPrice - dropPrediction.expectedBottomPrice) / currentPrice) * 100.0
|
||||||
|
val allowedMargin = if (isWatering) 4.0 else 2.0 // 물타기는 하락 여력 마진을 4%까지 관대하게 인정
|
||||||
|
|
||||||
// 바닥존에 들어오지 않았는데 바닥까지 1.5% 이상 추가 하락 여력이 남아있다면 진입 차단
|
if (distanceToBottomPct > allowedMargin && !dropPrediction.isBottomZone) {
|
||||||
if (distanceToBottomPct > 2.0 && !dropPrediction.isBottomZone) {
|
print("-> [${stock.name}] 지하실 주의 (추가하락여력: ${"%.1f".format(distanceToBottomPct)}%) | ")
|
||||||
print("-> [${stock.name}] 지하실 주의 (예상 바닥까지 -${"%.1f".format(distanceToBottomPct)}% 추가 하락 여력) | ")
|
|
||||||
return@withTimeout
|
return@withTimeout
|
||||||
}
|
}
|
||||||
|
|
||||||
// (2) 바닥권 도달 시 하락을 멈추는 브레이크(지지 캔들/반등 시그널) 확인
|
// 멈춤 브레이크는 물타기에서도 필수 (떨어지는 칼날에 물타면 비중만 커져 위험)
|
||||||
if (dropPrediction.isBottomZone) {
|
if (dropPrediction.isBottomZone || isWatering) {
|
||||||
val hasBrake = tempAnalyzer.checkBrakeAndReversal(dailyData)
|
val hasBrake = tempAnalyzer.checkBrakeAndReversal(dailyData)
|
||||||
if (!hasBrake) {
|
if (!hasBrake) {
|
||||||
print("-> [${stock.name}] 바닥 가격 도달했으나 지지/브레이크 미확인 (떨어지는 칼날 회피) | ")
|
print("-> [${stock.name}] 지지/브레이크 미확인 (칼날 회피) | ")
|
||||||
return@withTimeout
|
return@withTimeout
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
println("\n🔍 [사전 필터 통과 -> 정밀 분석 진입] ${stock.name} (${LocalTime.now()})")
|
println("\n💧 [검문소 통과 -> ${if (isWatering) "물타기 탈출 분석" else "신규 매수 분석"}] ${stock.name} (물타기:$isWatering, 수익:$isProfitable, 타이밍:$isValidEntryTiming)")
|
||||||
|
|
||||||
// -------------------------------------------------------------
|
// 멀티 타임프레임 및 AI 분석 진입
|
||||||
// [4차: 멀티 타임프레임(30분/주봉/월봉) 비동기 호출 & AI(RAG) 분석]
|
|
||||||
// -------------------------------------------------------------
|
|
||||||
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()) }
|
||||||
delay(20)
|
|
||||||
val weekly = async { tradeService.fetchPeriodChartData(stock.code, "W", 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 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()} - 데이터 정합성 통과)")
|
RagService.processStock(currentPrice, analyzer, stock.name, stock.code, isWatering) { decision, isSuccess ->
|
||||||
RagService.processStock(
|
callback(decision?.apply { this.currentPrice = currentPrice }, isSuccess)
|
||||||
currentPrice,
|
|
||||||
analyzer,
|
|
||||||
stock.name,
|
|
||||||
stock.code
|
|
||||||
) { decision, isSuccess ->
|
|
||||||
callback(
|
|
||||||
decision?.apply { this.currentPrice = currentPrice },
|
|
||||||
isSuccess
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
println("❌ [분석 실패] ${stock.name} (${LocalTime.now()} - 필수 캔들 데이터 누락)")
|
|
||||||
}
|
}
|
||||||
println("🏁 [분석 종료] ${stock.name} (${LocalTime.now()})")
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
println("❌ [Stock Error] ${stock.name}: ${e.message}")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user