...
This commit is contained in:
parent
81ce9b1539
commit
7474558136
@ -480,7 +480,7 @@ $standardizedScores
|
|||||||
}
|
}
|
||||||
|
|
||||||
val reboundRate = if (bottomPrice > 0) ((currentClose - bottomPrice) / bottomPrice * 100) else 0.0
|
val reboundRate = if (bottomPrice > 0) ((currentClose - bottomPrice) / bottomPrice * 100) else 0.0
|
||||||
if (reboundRate >= 3.0) { // 3% 이상 반등 시 사이클 종료 및 기록
|
if (reboundRate >= minDropToDetect) { // 3% 이상 반등 시 사이클 종료 및 기록
|
||||||
reboundTerms.add(i - bottomIndex)
|
reboundTerms.add(i - bottomIndex)
|
||||||
dropRates.add(abs((bottomPrice - peakPrice) / peakPrice * 100))
|
dropRates.add(abs((bottomPrice - peakPrice) / peakPrice * 100))
|
||||||
reboundAmplitudes.add(reboundRate) // 🌟 상승폭 기록
|
reboundAmplitudes.add(reboundRate) // 🌟 상승폭 기록
|
||||||
@ -614,7 +614,67 @@ $standardizedScores
|
|||||||
|
|
||||||
return VolatilityForecast(realisticHigh, realisticLow, extremeHigh, extremeLow)
|
return VolatilityForecast(realisticHigh, realisticLow, extremeHigh, extremeLow)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [신규] 종목의 현재 하락 진행 상태와 통계적 예상 바닥(Bottom)을 계산합니다.
|
||||||
|
*/
|
||||||
|
fun predictDropBottom(
|
||||||
|
candles: List<CandleData>,
|
||||||
|
reboundStats: ReboundStats,
|
||||||
|
volatility: VolatilityForecast
|
||||||
|
): DropPrediction? {
|
||||||
|
// 데이터가 부족하거나, 유의미한 과거 반등 패턴이 없으면 예측 불가
|
||||||
|
if (candles.size < 20 || !reboundStats.isValid) return null
|
||||||
|
|
||||||
|
// 1. 최근 20일 내 단기 고점 파악 (현재 진행 중인 하락 파동의 시작점)
|
||||||
|
val recentCandles = candles.takeLast(20)
|
||||||
|
var recentPeakPrice = 0.0
|
||||||
|
|
||||||
|
for (i in recentCandles.indices.reversed()) {
|
||||||
|
val highPrice = recentCandles[i].stck_hgpr.toDouble()
|
||||||
|
if (highPrice > recentPeakPrice) {
|
||||||
|
recentPeakPrice = highPrice
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (recentPeakPrice == 0.0) return null
|
||||||
|
|
||||||
|
val currentPrice = candles.last().stck_prpr.toDouble()
|
||||||
|
|
||||||
|
// 2. 현재까지의 하락률 계산
|
||||||
|
val currentDropRate = (recentPeakPrice - currentPrice) / recentPeakPrice * 100.0 // 양수로 표현 (예: 4.5% 하락)
|
||||||
|
|
||||||
|
// 3. 1차 예상 바닥가 (과거 평균 하락폭 적용)
|
||||||
|
// 예: 고점이 10,000원이고 과거 평균 10% 빠졌다면, 예상 바닥은 9,000원
|
||||||
|
val expectedBottomPrice = recentPeakPrice * (1.0 - (reboundStats.avgDropRate / 100.0))
|
||||||
|
|
||||||
|
// 4. 추가 하락 여력 계산 (얼마나 더 빠질 수 있는가?)
|
||||||
|
val remainingDropRate = reboundStats.avgDropRate - currentDropRate
|
||||||
|
|
||||||
|
// 5. 바닥권 진입 판별 (예상 바닥가의 +2% 이내로 들어왔거나, 통계적 마지노선(extremeLow) 근처일 때)
|
||||||
|
val isBottomZone = currentPrice <= (expectedBottomPrice * 1.02) || currentPrice <= (volatility.extremeLow * 1.02)
|
||||||
|
|
||||||
|
return DropPrediction(
|
||||||
|
recentPeakPrice = recentPeakPrice,
|
||||||
|
expectedBottomPrice = expectedBottomPrice,
|
||||||
|
extremeSupportPrice = volatility.extremeLow,
|
||||||
|
currentDropRate = -currentDropRate, // 음수로 표기 (예: -4.5%)
|
||||||
|
remainingDropRate = -remainingDropRate, // 음수면 더 빠질 공간이 남았다는 뜻
|
||||||
|
isBottomZone = isBottomZone
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class DropPrediction(
|
||||||
|
val recentPeakPrice: Double, // 최근 단기 고점
|
||||||
|
val expectedBottomPrice: Double, // 과거 평균 하락률(avgDropRate)을 적용한 1차 예상 바닥가
|
||||||
|
val extremeSupportPrice: Double, // 2표준편차(extremeLow) 기반의 통계적 2차 마지노선
|
||||||
|
val currentDropRate: Double, // 단기 고점 대비 현재까지 하락한 비율 (%)
|
||||||
|
val remainingDropRate: Double, // 1차 예상 바닥까지 남은 추가 하락 여력 (%) - 양수면 더 빠질 공간이 있다는 뜻
|
||||||
|
val isBottomZone: Boolean // 현재 가격이 바닥권(예상 바닥가의 상하 2% 이내)에 진입했는지 여부
|
||||||
|
)
|
||||||
|
|
||||||
data class VolatilityForecast(
|
data class VolatilityForecast(
|
||||||
val realisticHigh: Double, // 1표준편차 상단 (현실적 목표가, 68% 확률 내)
|
val realisticHigh: Double, // 1표준편차 상단 (현실적 목표가, 68% 확률 내)
|
||||||
val realisticLow: Double, // 1표준편차 하단 (현실적 지지선)
|
val realisticLow: Double, // 1표준편차 하단 (현실적 지지선)
|
||||||
|
|||||||
@ -462,7 +462,7 @@ object AutoTradingManager {
|
|||||||
} else {
|
} else {
|
||||||
val now = LocalTime.now()
|
val now = LocalTime.now()
|
||||||
|
|
||||||
val targetProfitLimit = if (holding.isTodayEntry && now.isBefore(LocalTime.of(18, 0))) {
|
val targetProfitLimit = if (holding.isTodayEntry && now.isBefore(LocalTime.of(16, 0))) {
|
||||||
// 당일 매수 종목: 짧은 익절 (예: 1.0% 이상이면 즉시 매도)
|
// 당일 매수 종목: 짧은 익절 (예: 1.0% 이상이면 즉시 매도)
|
||||||
KisSession.config.getValues(ConfigIndex.PROFIT_INDEX)+ KisSession.config.getValues(ConfigIndex.TAX_INDEX)
|
KisSession.config.getValues(ConfigIndex.PROFIT_INDEX)+ KisSession.config.getValues(ConfigIndex.TAX_INDEX)
|
||||||
} else {
|
} else {
|
||||||
@ -973,10 +973,10 @@ object AutoTradingManager {
|
|||||||
if (corpInfo?.cName.isNullOrEmpty()) {
|
if (corpInfo?.cName.isNullOrEmpty()) {
|
||||||
false
|
false
|
||||||
} else if (it.code !in myHoldings &&
|
} else if (it.code !in myHoldings &&
|
||||||
it.code !in pendingStocks &&
|
it.code !in pendingStocks &&
|
||||||
it.code !in executionCache.values.map { it.code } &&
|
it.code !in executionCache.values.map { it.code } &&
|
||||||
it.code !in failList &&
|
it.code !in failList &&
|
||||||
it.code !in isSafetyBeltStockCodes){
|
it.code !in isSafetyBeltStockCodes){
|
||||||
isOk
|
isOk
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
@ -1060,7 +1060,7 @@ object AutoTradingManager {
|
|||||||
if (now.isBefore(LocalTime.of(8,50)) && now.isAfter(LocalTime.of(8,45))) {
|
if (now.isBefore(LocalTime.of(8,50)) && now.isAfter(LocalTime.of(8,45))) {
|
||||||
cancelAllPendingSellOrders()
|
cancelAllPendingSellOrders()
|
||||||
isExecuted = true
|
isExecuted = true
|
||||||
} else if ( (now.isBefore(LocalTime.of(16,0)) && now.isAfter(KisSession.endBuyTime())) ) {
|
} else if ( (now.isBefore(LocalTime.of(15,40)) && now.isAfter(KisSession.endBuyTime())) ) {
|
||||||
val unfilledResult = KisTradeService.fetchUnfilledOrders()
|
val unfilledResult = KisTradeService.fetchUnfilledOrders()
|
||||||
unfilledResult.onSuccess { response ->
|
unfilledResult.onSuccess { response ->
|
||||||
response.filter { it.sll_buy_dvsn_cd == "02" }.forEach { order ->
|
response.filter { it.sll_buy_dvsn_cd == "02" }.forEach { order ->
|
||||||
@ -1082,8 +1082,11 @@ object AutoTradingManager {
|
|||||||
println("⏰ [강제 스케줄 실행] 오전 9시 ${currentMinute}분 - 보유주식 매도 체크를 시작합니다.")
|
println("⏰ [강제 스케줄 실행] 오전 9시 ${currentMinute}분 - 보유주식 매도 체크를 시작합니다.")
|
||||||
checkBalance()
|
checkBalance()
|
||||||
isExecuted = true
|
isExecuted = true
|
||||||
} else if (((now.hour == 8 && KisSession.tradeConfig.before_nxt && currentMinute < 45) ||
|
} else if (
|
||||||
(now.hour >= 16 && now.hour < 20 && KisSession.tradeConfig.after_nxt)) && (currentMinute % 2 == 0)) {
|
(
|
||||||
|
(now.hour == 8 && KisSession.tradeConfig.before_nxt && currentMinute < 45) ||
|
||||||
|
(now.isAfter(LocalTime.of(15,40)) && now.isBefore(LocalTime.of(20,0)) && KisSession.tradeConfig.after_nxt)
|
||||||
|
) && (currentMinute % 2 == 0)) {
|
||||||
TradingLogStore.addAnalyzer(
|
TradingLogStore.addAnalyzer(
|
||||||
" - ",
|
" - ",
|
||||||
" - ",
|
" - ",
|
||||||
@ -1158,17 +1161,18 @@ object AutoTradingManager {
|
|||||||
val tempAnalyzer = TechnicalAnalyzer().apply { this.daily = dailyData }
|
val tempAnalyzer = TechnicalAnalyzer().apply { this.daily = dailyData }
|
||||||
|
|
||||||
// 1. 변동성 기반 수익률 검증 (2% 이상 열려있는가?)
|
// 1. 변동성 기반 수익률 검증 (2% 이상 열려있는가?)
|
||||||
val volatility = tempAnalyzer.calculateVolatilityForecast(dailyData, 20)
|
val volatility = tempAnalyzer.calculateVolatilityForecast(dailyData, 40)
|
||||||
val expectedProfitRate = ((volatility.realisticHigh - currentPrice) / currentPrice) * 100.0
|
val expectedProfitRate = ((volatility.realisticHigh - currentPrice) / currentPrice) * 100.0
|
||||||
|
|
||||||
// 2. 일봉 기준 반등 주기 통계 추출 (일주일 내 승부 가능한가?)
|
// 2. 일봉 기준 반등 주기 통계 추출 (일주일 내 승부 가능한가?)
|
||||||
val dailyStats = tempAnalyzer.calculateDynamicReboundStats(dailyData)
|
val dailyStats = tempAnalyzer.calculateDynamicReboundStats(dailyData, 5.0)
|
||||||
val isApproaching = tempAnalyzer.checkReboundApproaching(
|
val isApproaching = tempAnalyzer.checkReboundApproaching(
|
||||||
candles = dailyData,
|
candles = dailyData,
|
||||||
avgReboundTerm = dailyStats.avgReboundPeriod,
|
avgReboundTerm = dailyStats.avgReboundPeriod,
|
||||||
dropThreshold = dailyStats.avgDropRate * 0.8,
|
dropThreshold = dailyStats.avgDropRate,
|
||||||
timeTolerance = dailyStats.timeTolerance
|
timeTolerance = dailyStats.timeTolerance
|
||||||
)
|
)
|
||||||
|
print("-> [${stock.name}] 필터링 ${dailyStats.avgReboundPeriod} ${dailyStats.avgDropRate} ${dailyStats.timeTolerance}")
|
||||||
val isSteadyUptrend = tempAnalyzer.checkSteadyUptrend(dailyData)
|
val isSteadyUptrend = tempAnalyzer.checkSteadyUptrend(dailyData)
|
||||||
|
|
||||||
|
|
||||||
@ -1183,7 +1187,18 @@ object AutoTradingManager {
|
|||||||
print("-> [${stock.name}] 조건 미달 필터링 (예측수익: ${"%.1f".format(expectedProfitRate)}%, 주기: ${"%.1f".format(dailyStats.avgReboundPeriod)}일, 진입권: $isValidEntryTiming) | ")
|
print("-> [${stock.name}] 조건 미달 필터링 (예측수익: ${"%.1f".format(expectedProfitRate)}%, 주기: ${"%.1f".format(dailyStats.avgReboundPeriod)}일, 진입권: $isValidEntryTiming) | ")
|
||||||
return@withTimeout // 조건에 맞지 않으면 주봉/월봉 API 호출 및 LLM 분석 없이 즉시 다음 종목으로 넘어감
|
return@withTimeout // 조건에 맞지 않으면 주봉/월봉 API 호출 및 LLM 분석 없이 즉시 다음 종목으로 넘어감
|
||||||
}
|
}
|
||||||
|
val dropPrediction = tempAnalyzer.predictDropBottom(dailyData, dailyStats, volatility)
|
||||||
|
|
||||||
|
if (dropPrediction != null) {
|
||||||
|
// 💡 [방어 로직] 아직 바닥까지 한참 남았는데 섣불리 들어가는 것을 방지!
|
||||||
|
// 과거 평균 10% 빠지는 종목인데, 지금 겨우 -3% 빠진 상태라면 (남은 하락폭 -7%)
|
||||||
|
if (dropPrediction.remainingDropRate < -2.0 && !dropPrediction.isBottomZone) {
|
||||||
|
print("-> [${stock.name}] 지하실 주의 (현재 ${"%.1f".format(dropPrediction.currentDropRate)}% 하락, 바닥까지 ${"%.1f".format(dropPrediction.remainingDropRate)}% 추가 하락 위험) | ")
|
||||||
|
return@withTimeout // 매수 후보에서 과감히 제외!
|
||||||
|
}
|
||||||
|
|
||||||
|
// 반대로 완벽한 바닥권(isBottomZone = true)에 들어왔다면 매수 타점으로 인정하여 다음 단계로 넘김
|
||||||
|
}
|
||||||
println("🔍 [분석 진입] ${stock.name} (${LocalTime.now()}) (예측수익: ${"%.1f".format(expectedProfitRate)}%, 주기: ${"%.1f".format(dailyStats.avgReboundPeriod)}일, 진입권: $isValidEntryTiming)")
|
println("🔍 [분석 진입] ${stock.name} (${LocalTime.now()}) (예측수익: ${"%.1f".format(expectedProfitRate)}%, 주기: ${"%.1f".format(dailyStats.avgReboundPeriod)}일, 진입권: $isValidEntryTiming)")
|
||||||
if (!isSafetyBeltStockCodes.contains(stock.code)) {
|
if (!isSafetyBeltStockCodes.contains(stock.code)) {
|
||||||
val analyzer = coroutineScope {
|
val analyzer = coroutineScope {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user