This commit is contained in:
2026-09-15 15:35:27 +09:00
parent 0e241205ee
commit 1a2fe8b6e6
3 changed files with 38 additions and 29 deletions
@@ -373,18 +373,15 @@ class TechnicalAnalyzer {
// 🌟 2. 3가지 핵심 조건 분리
val isPriceDropped = currentDropRate <= -dropThreshold
// 조건 A: 가격이 통계적 하락폭만큼 충분히 빠졌는가?
val isPastMinTime = daysSincePeak >= (avgReboundTerm - timeTolerance)
// 조건 B: 반등 '최소' 기간을 채웠는가? (떨어지는 칼날을 너무 일찍 잡는 것 방지)
val isWithinMaxTime = daysSincePeak <= (avgReboundTerm + (timeTolerance * 2))
// 조건 C: 반등 '최대' 기간을 넘기지 않았는가? (죽은 주식처럼 너무 오래 횡보하는 것 방지)
// 최대 기간 조건은 참고용으로 남겨두되 매수 차단 로직에서는 제외합니다.
// val isWithinMaxTime = daysSincePeak <= (avgReboundTerm + (timeTolerance * 2))
// 🌟 3. 3개 중 2개 이상 만족 시 반등 임박(Approaching)으로 판단
val passedConditions = listOf(isPriceDropped, isPastMinTime, isWithinMaxTime).count { it }
//A. 진입 하락률 기준checkReboundApproaching 3개 중 2개 만족 시 통과 가격 하락 조건(isPriceDropped)을 필수 조건(AND)으로 고정
return passedConditions > 2
// 🌟 3. 현실적인 타점 판별 (필수 2가지만 강력하게 요구)
// 필수 1: 가격이 통계적 하락폭만큼 충분히 빠졌는가? (눌림목 대전제)
// 필수 2: 최소한의 반등 준비 기간(평균 기간 - 오차)은 지났는가? (떨어지는 칼날 방지)
return isPriceDropped && isPastMinTime
}
fun calculateMFI(candles: List<CandleData>, period: Int = 14): Double {
+1 -1
View File
@@ -570,7 +570,7 @@ object KisTradeService {
if (response.status.isSuccess()) {
val body = response.body<CurrentPriceResponse>()
if (body.rt_cd == "0") {
println("${body.output}")
// println("${body.output}")
Result.success(body.output)
} else {
println("API 에러: ${body.msg1}")
+31 -19
View File
@@ -116,7 +116,7 @@ object AutoTradingManager {
val globalCallback = { completeTradingDecision: TradingDecision?, isSuccess: Boolean ->
val seoulZone = ZoneId.of("Asia/Seoul")
val now = LocalTime.now(ZoneId.of("Asia/Seoul"))
if (KisSession.isAvailBuyTime(now) && isSuccess && completeTradingDecision != null) {
if (KisSession.isMarketOpenTime(now) && isSuccess && completeTradingDecision != null) {
val decision = completeTradingDecision
println("${decision.stockName} ${decision.decision}")
@@ -409,7 +409,7 @@ object AutoTradingManager {
"WATCH",
"매수 시간 외 분석 => 재분석 대기열에 추가"
)
} else {
} else if (KisSession.isMarketOpenTime(LocalTime.now()) == false){
val unfilledResult = KisTradeService.fetchUnfilledOrders()
unfilledResult.onSuccess { response ->
response.filter { it.sll_buy_dvsn_cd == "02" }.forEach { order ->
@@ -464,7 +464,7 @@ object AutoTradingManager {
if (hasCodes) {
actualBuyPrice = actualBuyPrice * 1.1
}
val absoluteMinRate = KisSession.config.getValues(ConfigIndex.TAX_INDEX) + 0.05
val absoluteMinRate = KisSession.config.getValues(ConfigIndex.TAX_INDEX)
val finalProfitRate = maxOf(dbItem.profitRate, absoluteMinRate)
val finalTargetPrice =
MarketUtil.roundToTickSize(actualBuyPrice * (1 + finalProfitRate / 100.0))
@@ -1201,34 +1201,38 @@ object AutoTradingManager {
candidates.addAll(reanalysisList)
}
reanalysisList.clear()
val wateringCodes = mutableSetOf<String>()
if (KisSession.tradeConfig.lowerAveragePrice) {
currentBalance?.getHoldings()?.map {
// map 대신 forEach를 사용하여 메모리 낭비 방지
currentBalance?.getHoldings()?.forEach {
if (
it.quantity.toInt() > KisSession.tradeConfig.lowerAverageTargetCount &&
it.profitRate.toDouble() < 0.0 &&
it.profitRate.toDouble() < (abs(KisSession.tradeConfig.lowerAverageMaxRate) * -1) &&
it.profitRate.toDouble() > (abs(KisSession.tradeConfig.lowerAverageMinRate) * -1)
) {
// 물타기 전용 Set에 코드 등록
wateringCodes.add(it.code)
candidates.add(
RankingStock(
mksc_shrn_iscd = it.code,
hts_kor_isnm = it.name
)
)
println("물타기 대상 추가 ${it.name}[${it.code}]")
var oldTarget = it
if (oldTarget != null) {
var avgPrive = oldTarget.avgPrice.toDouble()
println("💧 [물타기 우선순위 할당] ${it.name}[${it.code}]")
var qty = oldTarget.quantity.toDouble()
var basePrice =
((avgPrive * qty) + it.currentPrice.toDouble()).div(qty!!.toInt() + 1)
println("물타기 ${avgPrive}, ${qty} ${basePrice}")
val avgPrive = it.avgPrice.toDouble()
val qty = it.quantity.toDouble()
val basePrice = ((avgPrive * qty) + it.currentPrice.toDouble()).div(qty.toInt() + 1)
println(" -> 단가 현황: 평단가 $avgPrive, 보유 ${qty}주, 예상단가 $basePrice")
}
}
}
}
remainingCandidates.addAll(candidates.filter {
// 1. 기존 필터링 및 중복 제거 적용
val filteredCandidates = candidates.filter {
(if (KisSession.tradeConfig.lowerAveragePrice) {
true
} else {
@@ -1238,8 +1242,16 @@ object AutoTradingManager {
it.code !in executionCache.values.map { it.code } &&
it.code !in failList &&
it.code !in isSafetyBeltStockCodes
}.distinctBy { it.code })
remainingCandidates.shuffle()
}.distinctBy { it.code }
// 🌟 2. [핵심] 리스트를 두 그룹으로 파티셔닝 (물타기 대상 vs 신규 발굴)
val (wateringList, newList) = filteredCandidates.partition { it.code in wateringCodes }
remainingCandidates.clear()
// 🌟 3. 분석 대기열 재조립 (물타기 대상 1순위 배치)
remainingCandidates.addAll(wateringList) // 물타기 대상을 무조건 리스트 맨 앞으로 (셔플 안 함)
remainingCandidates.addAll(newList.shuffled())
} else {
println("미확인 데이터 ${remainingCandidates.size}")
}
@@ -1373,7 +1385,7 @@ object AutoTradingManager {
// 🌟 [핵심] 물타기 대상 여부 플래그 식별
val targetHolding = currentBalance?.getHoldings()?.firstOrNull {
it.code == stock.code && it.quantity.toInt() > 2
it.code == stock.code && it.quantity.toInt() > 0
}
val isWatering = targetHolding != null && KisSession.tradeConfig.lowerAveragePrice
@@ -1472,7 +1484,7 @@ object AutoTradingManager {
val currentAtr = tempAnalyzer.calculateATR(dailyData)
if (!isProfitable || !isValidEntryTiming) {
print("-> [${stock.name}] 조건 미달 (물타기:$isWatering, 수익:$isProfitable, 타이밍:$isValidEntryTiming) | ")
print("-> [${stock.name}] 목표 수익 조건 미달 (물타기:$isWatering, 수익:$isProfitable, 타이밍:$isValidEntryTiming) | 기대 수익율 : ${expectedProfitRate}")
return@withTimeout
}
@@ -1497,7 +1509,7 @@ object AutoTradingManager {
}
}
println("\n💧 [검문소 통과 -> ${if (isWatering) "물타기 탈출 분석" else "신규 매수 분석"}] ${stock.name} (물타기:$isWatering, 수익:$isProfitable, 타이밍:$isValidEntryTiming)")
println("\n💧 [검문소 통과 -> ${if (isWatering) "물타기 탈출 분석" else "신규 매수 분석"}] ${stock.name} (물타기:$isWatering, 수익:$isProfitable, 타이밍:$isValidEntryTiming) | 기대 수익율 : ${expectedProfitRate}")
// 멀티 타임프레임 및 AI 분석 진입
val analyzer = coroutineScope {