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