..
This commit is contained in:
@@ -354,7 +354,7 @@ class TechnicalAnalyzer {
|
||||
// 🌟 3. 3개 중 2개 이상 만족 시 반등 임박(Approaching)으로 판단
|
||||
val passedConditions = listOf(isPriceDropped, isPastMinTime, isWithinMaxTime).count { it }
|
||||
//A. 진입 하락률 기준checkReboundApproaching 3개 중 2개 만족 시 통과 가격 하락 조건(isPriceDropped)을 필수 조건(AND)으로 고정
|
||||
return passedConditions >= 2
|
||||
return passedConditions > 2
|
||||
}
|
||||
|
||||
fun calculateMFI(candles: List<CandleData>, period: Int = 14): Double {
|
||||
|
||||
@@ -590,7 +590,7 @@ object AutoTradingManager {
|
||||
qty = holding.availOrderCount,
|
||||
price = targetPrice.toInt().toString(),
|
||||
isBuy = false,
|
||||
orderDivision = if (marketCode.equals("Y")) "07" else "",
|
||||
orderDivision = if (marketCode.equals("Y")) "41" else "",
|
||||
marketCode = if (marketCode.equals("Y")) "KRX" else "NXT"
|
||||
).onSuccess { newOrderNo ->
|
||||
println("✅ [${if (marketCode.equals("Y")) "시간외 단일가" else "대체거래소"} 주문 완료] ${holding.name}: $newOrderNo")
|
||||
@@ -743,7 +743,7 @@ object AutoTradingManager {
|
||||
) {
|
||||
var targetPrice = holding.avgPrice.toDouble()
|
||||
targetPrice = MarketUtil.roundToTickSize(
|
||||
targetPrice + MarketUtil.getTickSize(targetPrice) * KisSession.tradeConfig.autoSellOrderAppend
|
||||
targetPrice + (MarketUtil.getTickSize(targetPrice) * KisSession.tradeConfig.autoSellOrderAppend)
|
||||
)
|
||||
tradeService.postOrder(
|
||||
stockCode = holding.code,
|
||||
@@ -761,7 +761,7 @@ object AutoTradingManager {
|
||||
TradingLogStore.addNotice(
|
||||
"보유주식[${holding.name}]",
|
||||
holding.code,
|
||||
"매수가 기준 (${holding.avgPrice.toDouble()} 3호가 위[${targetPrice}] 매도 주문 ${if (isSuccess) "성공" else "실패[${errMsg}]"}"
|
||||
"매수가 기준 (${holding.avgPrice.toDouble()} ${KisSession.tradeConfig.autoSellOrderAppend}호가 위[${targetPrice}] 매도 주문 ${if (isSuccess) "성공" else "실패[${errMsg}]"}"
|
||||
)
|
||||
} else if (KisSession.config.stop_Loss
|
||||
&& holding != null && holding.quantity.toInt() > 0
|
||||
@@ -1323,7 +1323,7 @@ object AutoTradingManager {
|
||||
true
|
||||
)
|
||||
var list = mutableListOf<String>("X")
|
||||
if (now.hour != 8 && now.hour < 18) {
|
||||
if (now.hour != 8 && now.hour < 20) {
|
||||
list.add("Y")
|
||||
}
|
||||
list.forEach { code ->
|
||||
@@ -1363,69 +1363,98 @@ object AutoTradingManager {
|
||||
val maxBudget = KisSession.config.getValues(ConfigIndex.MAX_BUDGET_INDEX)
|
||||
val maxPrice = KisSession.config.getValues(ConfigIndex.MAX_PRICE_INDEX)
|
||||
val minPrice = KisSession.config.getValues(ConfigIndex.MIN_PRICE_INDEX)
|
||||
|
||||
// 개별 종목 분석은 최대 2분으로 제한
|
||||
withTimeout(ONE_STOCK_ALYSIS_TIME) {
|
||||
// -------------------------------------------------------------
|
||||
// [1차 필터: 가벼운 메타데이터 & 정책 검증 (불필요한 API 호출 방지)]
|
||||
// -------------------------------------------------------------
|
||||
// 1. 종목명 및 기업 고유번호 유효성 검사
|
||||
val corpInfo = DartCodeManager.getCorpCode(stock.code)
|
||||
if (corpInfo?.cName.isNullOrEmpty()) {
|
||||
print("-> 기업명을 못찾아서 제외 | ")
|
||||
return@withTimeout
|
||||
}
|
||||
if (currentBalance?.getHoldings()
|
||||
?.any { it.code.equals(stock.code) && it.quantity.toInt() > 2 } == true
|
||||
) {
|
||||
println("물타기 대상 분석")
|
||||
|
||||
// 2. [보완] 재무 건전성 미달 종목 사전 차단 (최상단으로 이동하여 차트 API 소모 방지)
|
||||
if (isSafetyBeltStockCodes.contains(stock.code)) {
|
||||
print("-> [${stock.name}] 재무 건전성 미달(안전벨트 탈락 종목) 제외 | ")
|
||||
return@withTimeout
|
||||
}
|
||||
|
||||
// 3. 물타기 대상 로깅
|
||||
if (currentBalance?.getHoldings()?.any { it.code == stock.code && it.quantity.toInt() > 2 } == true) {
|
||||
println("물타기 대상 분석: ${stock.name}")
|
||||
}
|
||||
|
||||
// 초기 콜백 알림 (분석 진행 표시용)
|
||||
callback(TradingDecision().apply {
|
||||
this.stockCode = stock.code
|
||||
this.confidence = -1.0
|
||||
this.stockName = stock.name
|
||||
}, false)
|
||||
|
||||
val dailyData =
|
||||
tradeService.fetchPeriodChartData(stock.code, "D", true).getOrNull()
|
||||
// 4. [보완] 배당 전략 사용 시 사전 필터링 (복잡한 기술 분석 전 조기 탈락)
|
||||
if (KisSession.tradeConfig.isUpcomingDividend) {
|
||||
val dividend = KisTradeService.fetchUpcomingDividend(stock.code).getOrNull()
|
||||
if (dividend?.hasDividend == true) {
|
||||
println("[${stock.name}] 배당락일 ${dividend.exDividendDate} : ${dividend.dividendAmount}")
|
||||
} else {
|
||||
print("-> [${stock.name}] 배당 정보 없어 분석 종료 | ")
|
||||
return@withTimeout
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// [2차 필터: 일봉 데이터 수집 및 현재가/호가/리스크 검증]
|
||||
// -------------------------------------------------------------
|
||||
val dailyData = tradeService.fetchPeriodChartData(stock.code, "D", true).getOrNull()
|
||||
?: return@withTimeout
|
||||
val today = dailyData.lastOrNull() ?: null
|
||||
var rate = today?.getFluctuationRate() ?: 0.0
|
||||
val isOk =
|
||||
((rate < KisSession.tradeConfig.plusFilter) && (rate > (abs(KisSession.tradeConfig.minusFilter) * -1)))
|
||||
|
||||
// 5. [보완] 데이터 부족 종목 방어 (신규 상장주, 통계 최소 표본 30봉 미만 차단)
|
||||
if (dailyData.size < 30) {
|
||||
print("-> [${stock.name}] 캔들 데이터 부족(30봉 미만) 제외 | ")
|
||||
return@withTimeout
|
||||
}
|
||||
|
||||
val today = dailyData.lastOrNull()
|
||||
val rate = today?.getFluctuationRate() ?: 0.0
|
||||
val isOk = (rate < KisSession.tradeConfig.plusFilter) && (rate > (abs(KisSession.tradeConfig.minusFilter) * -1))
|
||||
|
||||
delay(50)
|
||||
// 1. var 대신 val을 사용해야 아래에서 스마트 캐스트가 작동하여 !!를 안 써도 됩니다.
|
||||
val currentStock = KisTradeService.fetchCurrentPrice(stock.code).getOrNull()
|
||||
|
||||
if (today == null || currentStock == null) {
|
||||
// failList.add(stock.code)
|
||||
print("-> 금일 금액 조회 실패 | ${isOk}")
|
||||
print("-> 금일 금액 조회 실패 | isOk: ${isOk} | ")
|
||||
return@withTimeout
|
||||
}
|
||||
// 3. 위에서 확실하게 null 체크를 했으므로, 이제 currentStock은 절대 null이 아닙니다.
|
||||
// 안전하게(Safe call ? 없이) 바로 접근 가능합니다.
|
||||
val currentPrice = currentStock.stck_prpr.toDouble()
|
||||
println("${stock.name}[${stock.code}] 현재가 : ${currentPrice} , 변동률 : ${rate} , 거래 기준 : ${isOk}")
|
||||
|
||||
// 4. 위험한 !! 단언 기호 없이 깔끔하게 호출
|
||||
val currentPrice = currentStock.stck_prpr.toDouble()
|
||||
println("${stock.name}[${stock.code}] 현재가: ${currentPrice}, 변동률: ${rate}%, 거래기준(isOk): ${isOk}")
|
||||
|
||||
// 6. 리스크 매니저 검문소 통과 여부 확인 (관리종목, 환기종목 등)
|
||||
val riskResult = RiskManager.evaluateRisk(currentStock)
|
||||
if (!riskResult.isSafe) {
|
||||
print("-> ${stock.name}[${stock.code}] 검문소 탈락: ${riskResult.rejectReason}")
|
||||
print("-> ${stock.name}[${stock.code}] 리스크 검문소 탈락: ${riskResult.rejectReason} | ")
|
||||
return@withTimeout
|
||||
}
|
||||
|
||||
|
||||
// 7. 계좌 예산 및 단가 정책 필터링
|
||||
if (!isOk || (myCash > 10L && currentPrice > myCash) || currentPrice > maxBudget || currentPrice > maxPrice || currentPrice < minPrice) {
|
||||
print("-> [${stock.name}] 가격 정책으로 제외 [1주:${currentPrice}, 자산:${myCash}, 최소 기준:${minPrice}, 최대 기준:${maxPrice}] | ")
|
||||
print("-> [${stock.name}] 가격 정책으로 제외 [1주:${currentPrice}, 자산:${myCash}, 최소:${minPrice}, 최대:${maxPrice}] | ")
|
||||
return@withTimeout
|
||||
}
|
||||
|
||||
|
||||
// 🌟 [추가] 고도화된 사전 필터링 (검문소)
|
||||
// -------------------------------------------------------------
|
||||
// [3차 필터: 기술적 통계 분석 (변동성, 반등 주기, 지하실 회피)]
|
||||
// -------------------------------------------------------------
|
||||
val tempAnalyzer = TechnicalAnalyzer().apply { this.daily = dailyData }
|
||||
|
||||
// 1. 변동성 기반 수익률 검증 (2% 이상 열려있는가?)
|
||||
println("(dailyData.size * 0.8).toInt() ${(dailyData.size * 0.3).toInt()}")
|
||||
// 1. 변동성 기반 현실적 기대수익률 예측
|
||||
val volatility = tempAnalyzer.calculateVolatilityForecast(dailyData, 20)
|
||||
val expectedProfitRate =
|
||||
((volatility.realisticHigh - currentPrice) / currentPrice) * 100.0
|
||||
val expectedProfitRate = ((volatility.realisticHigh - currentPrice) / currentPrice) * 100.0
|
||||
|
||||
// 2. 일봉 기준 반등 주기 통계 추출 (일주일 내 승부 가능한가?)
|
||||
// 2. 동적 반등 통계 산출
|
||||
val dailyStats = tempAnalyzer.calculateDynamicReboundStats(dailyData, 5.0)
|
||||
val isApproaching = tempAnalyzer.checkReboundApproaching(
|
||||
candles = dailyData,
|
||||
@@ -1433,86 +1462,61 @@ object AutoTradingManager {
|
||||
dropThreshold = dailyStats.avgDropRate,
|
||||
timeTolerance = dailyStats.timeTolerance
|
||||
)
|
||||
print("-> [${stock.name}] 필터링 ${dailyStats.avgReboundPeriod} ${dailyStats.avgDropRate} ${dailyStats.timeTolerance}")
|
||||
val isSteadyUptrend = false //tempAnalyzer.checkSteadyUptrend(dailyData)
|
||||
print("-> [${stock.name}] 반등통계(주기:${"%.1f".format(dailyStats.avgReboundPeriod)}일, 평균하락폭:${"%.1f".format(dailyStats.avgDropRate)}%, 오차:${"%.1f".format(dailyStats.timeTolerance)})")
|
||||
|
||||
// 3. 우상향 추세(Trend Following) 판별 - 필요 시 활성화
|
||||
val isSteadyUptrend = tempAnalyzer.checkSteadyUptrend(dailyData)
|
||||
|
||||
// 🌟 [수정] 조건 통합 (OR 조건)
|
||||
val isProfitable =
|
||||
expectedProfitRate >= KisSession.tradeConfig.minExpectedProfitRate || dailyStats.avgReboundAmplitude >= KisSession.tradeConfig.minExpectedProfitRate
|
||||
// 4. 기대수익률 조건 (예측 수익 또는 과거 평균 반등폭이 기준 이상인가?)
|
||||
val isProfitable = expectedProfitRate >= KisSession.tradeConfig.minExpectedProfitRate ||
|
||||
dailyStats.avgReboundAmplitude >= KisSession.tradeConfig.minExpectedProfitRate
|
||||
|
||||
// 반등 주기에 도달했거나(Mean Reversion), 안정적으로 뻗어나가는 우상향 종목(Trend Following)이면 통과
|
||||
val isValidEntryTiming =
|
||||
(dailyStats.isValid && isApproaching && dailyStats.avgReboundPeriod <= KisSession.tradeConfig.maxExpectedReboundDays && dailyStats.avgReboundPeriod >= KisSession.tradeConfig.minExpectedReboundDays) || isSteadyUptrend
|
||||
// 5. 진입 타이밍 조건 (통계적 반등 주기에 도달했거나, 안정적 우상향 추세인가?)
|
||||
val isValidEntryTiming = (dailyStats.isValid && isApproaching &&
|
||||
dailyStats.avgReboundPeriod <= KisSession.tradeConfig.maxExpectedReboundDays &&
|
||||
dailyStats.avgReboundPeriod >= KisSession.tradeConfig.minExpectedReboundDays) || isSteadyUptrend
|
||||
|
||||
val currentAtr = tempAnalyzer.calculateATR(dailyData)
|
||||
if (!isProfitable || !isValidEntryTiming) {
|
||||
print(
|
||||
"-> [${stock.name}] 조건 미달 필터링 (예측수익: ${"%.1f".format(expectedProfitRate)}%, 주기: ${
|
||||
"%.1f".format(
|
||||
dailyStats.avgReboundPeriod
|
||||
)
|
||||
}일, 진입권: $isValidEntryTiming) | "
|
||||
)
|
||||
return@withTimeout // 조건에 맞지 않으면 주봉/월봉 API 호출 및 LLM 분석 없이 즉시 다음 종목으로 넘어감
|
||||
}
|
||||
val dropPrediction =
|
||||
tempAnalyzer.predictDropBottom(dailyData, dailyStats, volatility, currentAtr)
|
||||
|
||||
if (dropPrediction != null) {
|
||||
// 💡 [방어 로직 1] 아직 바닥까지 한참 남았다면 지하실 방지
|
||||
if (dropPrediction.remainingDropRate < -1.0 && !dropPrediction.isBottomZone) {
|
||||
print("-> [${stock.name}] 지하실 주의 (현재 ${"%.1f".format(dropPrediction.currentDropRate)}% 하락, 추가 하락 위험) | ")
|
||||
print("-> [${stock.name}] 조건 미달 필터링 (예측수익:${"%.1f".format(expectedProfitRate)}%, 주기:${"%.1f".format(dailyStats.avgReboundPeriod)}일, 진입권:$isValidEntryTiming) | ")
|
||||
return@withTimeout
|
||||
}
|
||||
|
||||
// 💡 [방어 로직 2 - 신규] 가격은 바닥권에 왔지만, 캔들에 브레이크(지지)가 걸렸는가?
|
||||
// 6. [보완] 바닥 예측 및 지하실(추가 낙폭) 회피 로직
|
||||
val dropPrediction = tempAnalyzer.predictDropBottom(dailyData, dailyStats, volatility, currentAtr)
|
||||
if (dropPrediction != null) {
|
||||
// (1) 부호 혼선 방지: 현재가와 예상 바닥가 간의 실제 추가 하락 여력(%) 직관적 계산
|
||||
val distanceToBottomPct = ((currentPrice - dropPrediction.expectedBottomPrice) / currentPrice) * 100.0
|
||||
|
||||
// 바닥존에 들어오지 않았는데 바닥까지 1.5% 이상 추가 하락 여력이 남아있다면 진입 차단
|
||||
if (distanceToBottomPct > 2.0 && !dropPrediction.isBottomZone) {
|
||||
print("-> [${stock.name}] 지하실 주의 (예상 바닥까지 -${"%.1f".format(distanceToBottomPct)}% 추가 하락 여력) | ")
|
||||
return@withTimeout
|
||||
}
|
||||
|
||||
// (2) 바닥권 도달 시 하락을 멈추는 브레이크(지지 캔들/반등 시그널) 확인
|
||||
if (dropPrediction.isBottomZone) {
|
||||
val hasBrake = tempAnalyzer.checkBrakeAndReversal(dailyData)
|
||||
if (!hasBrake) {
|
||||
print("-> [${stock.name}] 바닥 가격 도달했으나, 브레이크(지지/거래량 진정) 미확인. 떨어지는 칼날 회피 | ")
|
||||
return@withTimeout // 브레이크가 없으면 매수 안 함!
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (KisSession.tradeConfig.isUpcomingDividend) {
|
||||
var dividend = KisTradeService.fetchUpcomingDividend(stock.code).getOrNull()
|
||||
if (dividend?.hasDividend == true) {
|
||||
println("[${stock.name}] 배당락일 ${dividend.exDividendDate} : ${dividend.dividendAmount}")
|
||||
} else {
|
||||
println("[${stock.name}] 배당 정보 없어서 분석 종료")
|
||||
print("-> [${stock.name}] 바닥 가격 도달했으나 지지/브레이크 미확인 (떨어지는 칼날 회피) | ")
|
||||
return@withTimeout
|
||||
}
|
||||
} else {
|
||||
println("[${stock.name}] 배당 정보 무관 함.")
|
||||
}
|
||||
}
|
||||
|
||||
println(
|
||||
"🔍 [분석 진입] ${stock.name} (${LocalTime.now()}) (예측수익: ${
|
||||
"%.1f".format(
|
||||
expectedProfitRate
|
||||
)
|
||||
}%, 주기: ${"%.1f".format(dailyStats.avgReboundPeriod)}일, 진입권: $isValidEntryTiming)"
|
||||
)
|
||||
if (!isSafetyBeltStockCodes.contains(stock.code)) {
|
||||
println("\n🔍 [사전 필터 통과 -> 정밀 분석 진입] ${stock.name} (${LocalTime.now()})")
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// [4차: 멀티 타임프레임(30분/주봉/월봉) 비동기 호출 & AI(RAG) 분석]
|
||||
// -------------------------------------------------------------
|
||||
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 {
|
||||
this.daily = dailyData
|
||||
delay(50)
|
||||
@@ -1523,9 +1527,9 @@ object AutoTradingManager {
|
||||
this.monthly = monthly.await()
|
||||
}
|
||||
}
|
||||
if (analyzer.isValid()) {
|
||||
|
||||
println("✅ [분석 시작] ${stock.name} (${LocalTime.now()} 분석 데이터 정합성 -> ${analyzer.isValid()})")
|
||||
if (analyzer.isValid()) {
|
||||
println("✅ [분석 시작] ${stock.name} (${LocalTime.now()} - 데이터 정합성 통과)")
|
||||
RagService.processStock(
|
||||
currentPrice,
|
||||
analyzer,
|
||||
@@ -1538,12 +1542,9 @@ object AutoTradingManager {
|
||||
)
|
||||
}
|
||||
} else {
|
||||
println("✅ [분석 실패] ${stock.name} (${LocalTime.now()} 분석 데이터 정합성 -> ${analyzer.isValid()})")
|
||||
println("❌ [분석 실패] ${stock.name} (${LocalTime.now()} - 필수 캔들 데이터 누락)")
|
||||
}
|
||||
} else {
|
||||
println("재무 안정성 부족 (캐시)")
|
||||
}
|
||||
println("✅ [분석 종료] ${stock.name} (${LocalTime.now()})")
|
||||
println("🏁 [분석 종료] ${stock.name} (${LocalTime.now()})")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
|
||||
Reference in New Issue
Block a user