This commit is contained in:
2026-08-03 11:53:29 +09:00
parent b134dea1e1
commit acd26abe1e
7 changed files with 138 additions and 25 deletions
@@ -627,7 +627,7 @@ $standardizedScores
if (candles.size < 20 || !reboundStats.isValid) return null
// 1. 최근 20일 내 단기 고점 파악 (현재 진행 중인 하락 파동의 시작점)
val recentCandles = candles.takeLast(20)
val recentCandles = candles.takeLast((candles.size.times(0.8).toInt()))
var recentPeakPrice = 0.0
for (i in recentCandles.indices.reversed()) {
@@ -652,7 +652,7 @@ $standardizedScores
val remainingDropRate = reboundStats.avgDropRate - currentDropRate
// 5. 바닥권 진입 판별 (예상 바닥가의 +2% 이내로 들어왔거나, 통계적 마지노선(extremeLow) 근처일 때)
val isBottomZone = currentPrice <= (expectedBottomPrice * 1.02) || currentPrice <= (volatility.extremeLow * 1.02)
val isBottomZone = currentPrice <= (expectedBottomPrice * 1.015) || currentPrice <= (volatility.extremeLow * 1.015)
return DropPrediction(
recentPeakPrice = recentPeakPrice,
+1
View File
@@ -267,6 +267,7 @@ class TradeConfig {
var minExpectedProfitRate: Double = 2.0 // 필터링 기준 최소 기대 수익률 (%)
var maxExpectedReboundDays: Double = 10.0 // 필터링 기준 최대 허용 반등 주기 (일)
var minExpectedReboundDays: Double = 1.5
var isUpcomingDividend : Boolean = false
}
+10
View File
@@ -3,6 +3,8 @@ package model
import AutoTradeItem
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import java.math.BigDecimal
@Serializable
data class StockBalanceResponse(
val rt_cd: String = "",
@@ -106,6 +108,14 @@ enum class RankingType(
HTS_TOP20("HTS조회상위", "HHMCM000100C0", "20175", "/uapi/domestic-stock/v1/ranking/hts-top-view", emptyMap())
}
data class UpcomingDividend(
val hasDividend: Boolean,
val stockCode: String,
val stockName: String,
val exDividendDate: String?,
val dividendAmount: BigDecimal?
)
@Serializable
data class RankingStock(
val hts_kor_isnm: String = "", // 종목명
@@ -227,6 +227,78 @@ object KisTradeService {
}
}
suspend fun fetchUpcomingDividend(
stockCode: String
): Result<UpcomingDividend> {
val config = KisSession.config
val formatter = DateTimeFormatter.ofPattern("yyyyMMdd")
val fromDate = LocalDate.now().format(formatter)
val toDate = LocalDate.now().plusMonths(6).format(formatter)
return try {
val response = client.get("$prodUrl/uapi/domestic-stock/v1/ksdinfo/dividend") {
header("authorization", "Bearer ${config.marketToken}")
header("appkey", config.realAppKey)
header("appsecret", config.realSecretKey)
header("tr_id", "HHKDB669102C0")
header("custtype", "P")
// 실제 API 문서의 파라미터명으로 변경
parameter("CTS", "")
parameter("GB1", "0")
parameter("SHT_CD", stockCode)
parameter("HIGH_GB", "")
parameter("F_DT", fromDate)
parameter("T_DT", toDate)
}
val body = response.body<JsonObject>()
if (body["rt_cd"]?.jsonPrimitive?.content != "0") {
return Result.failure(
Exception(body["msg1"]?.jsonPrimitive?.content ?: "배당 조회 실패")
)
}
val output = body["output1"]?.jsonArray.orEmpty()
val item = output
.map { it.jsonObject }
.firstOrNull {
it["sht_cd"]?.jsonPrimitive?.content == stockCode
}
println("fetchUpcomingDividend 배당 body >>> ${item}")
if (item == null) {
return Result.success(
UpcomingDividend(
hasDividend = false,
stockCode = stockCode,
stockName = "",
exDividendDate = null,
dividendAmount = null
)
)
}
Result.success(
UpcomingDividend(
hasDividend = true,
stockCode = item["sht_cd"]?.jsonPrimitive?.content.orEmpty(),
stockName = item["isin_name"]?.jsonPrimitive?.content.orEmpty(),
exDividendDate = item["record_date"]?.jsonPrimitive?.content,
dividendAmount = item["per_sto_divi_amt"]?.jsonPrimitive?.content?.toBigDecimalOrNull()
)
)
} catch (e: Exception) {
Result.failure(e)
}
}
/**
* [추가] 기간별(일/주/월) 차트 데이터 조회
* @param periodCode "D"(일), "W"(주), "M"(월)
+2 -2
View File
@@ -278,8 +278,8 @@ object RagService {
return@coroutineScope
}
if ((tradingDecision.signalModel?.compositeScore ?: 0) < 50) {
logTime(stockName, "기술 점수 미달 조기 종료", techDuration, System.currentTimeMillis() - totalStartTime)
if ((tradingDecision.signalModel?.compositeScore ?: 0) < 40) {
logTime(stockName, "기술 점수 미달 조기 종료 ${tradingDecision.signalModel?.compositeScore} , ${tradingDecision.signalModel?.successProbPct} ", techDuration, System.currentTimeMillis() - totalStartTime)
if (FinancialAnalyzer.isBuyConsiderationMet(financialStmt) && financialScore > 70) {
TradingLogStore.addAnalyzer(stockName, stockCode, "매수 타점 미도달 (재무 우량주로 감시 지속)", true)
result(tradingDecision.apply {
+18 -3
View File
@@ -39,6 +39,7 @@ import network.KisWebSocketManager
import network.RagService
import network.RagService.isSafetyBeltStockCodes
import network.StockUniverseLoader
import okhttp3.internal.wait
import report.TradingReportManager
import util.MarketUtil
import java.time.LocalDate
@@ -1161,7 +1162,8 @@ object AutoTradingManager {
val tempAnalyzer = TechnicalAnalyzer().apply { this.daily = dailyData }
// 1. 변동성 기반 수익률 검증 (2% 이상 열려있는가?)
val volatility = tempAnalyzer.calculateVolatilityForecast(dailyData, 40)
println("(dailyData.size * 0.8).toInt() ${(dailyData.size * 0.8).toInt()}")
val volatility = tempAnalyzer.calculateVolatilityForecast(dailyData, (dailyData.size * 0.8).toInt())
val expectedProfitRate = ((volatility.realisticHigh - currentPrice) / currentPrice) * 100.0
// 2. 일봉 기준 반등 주기 통계 추출 (일주일 내 승부 가능한가?)
@@ -1173,7 +1175,7 @@ object AutoTradingManager {
timeTolerance = dailyStats.timeTolerance
)
print("-> [${stock.name}] 필터링 ${dailyStats.avgReboundPeriod} ${dailyStats.avgDropRate} ${dailyStats.timeTolerance}")
val isSteadyUptrend = tempAnalyzer.checkSteadyUptrend(dailyData)
val isSteadyUptrend = false //tempAnalyzer.checkSteadyUptrend(dailyData)
// 🌟 [수정] 조건 통합 (OR 조건)
@@ -1192,13 +1194,26 @@ object AutoTradingManager {
if (dropPrediction != null) {
// 💡 [방어 로직] 아직 바닥까지 한참 남았는데 섣불리 들어가는 것을 방지!
// 과거 평균 10% 빠지는 종목인데, 지금 겨우 -3% 빠진 상태라면 (남은 하락폭 -7%)
if (dropPrediction.remainingDropRate < -2.0 && !dropPrediction.isBottomZone) {
if (dropPrediction.remainingDropRate < -1.5 && !dropPrediction.isBottomZone) {
print("-> [${stock.name}] 지하실 주의 (현재 ${"%.1f".format(dropPrediction.currentDropRate)}% 하락, 바닥까지 ${"%.1f".format(dropPrediction.remainingDropRate)}% 추가 하락 위험) | ")
return@withTimeout // 매수 후보에서 과감히 제외!
}
// 반대로 완벽한 바닥권(isBottomZone = true)에 들어왔다면 매수 타점으로 인정하여 다음 단계로 넘김
}
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}] 배당 정보 없어서 분석 종료")
return@withTimeout
}
} else {
println("[${stock.name}] 배당 정보 무관 함.")
}
println("🔍 [분석 진입] ${stock.name} (${LocalTime.now()}) (예측수익: ${"%.1f".format(expectedProfitRate)}%, 주기: ${"%.1f".format(dailyStats.avgReboundPeriod)}일, 진입권: $isValidEntryTiming)")
if (!isSafetyBeltStockCodes.contains(stock.code)) {
val analyzer = coroutineScope {
+27 -12
View File
@@ -564,23 +564,37 @@ fun TradingDecisionLog() {
helperText = "현재: ${tradeConfig.auto_cancel_pending_time / 1000}초 후 취소"
)
SettingSwitchField(
Row(horizontalArrangement = Arrangement.SpaceEvenly) {
SettingSwitchField (
modifier = Modifier.weight(1.0f, true),
label = "장 전 대체마켓 매도",
initialChecked = tradeConfig.before_nxt,
onCheckedChange = { tradeConfig.before_nxt = it
KisSession.saveTradeConfig() }
onCheckedChange = {
tradeConfig.before_nxt = it
KisSession.saveTradeConfig()
}
)
SettingSwitchField(
modifier = Modifier.weight(1.0f, true),
label = "장 후 대체 마켓 매도",
initialChecked = tradeConfig.after_nxt,
onCheckedChange = { tradeConfig.after_nxt = it
KisSession.saveTradeConfig() }
onCheckedChange = {
tradeConfig.after_nxt = it
KisSession.saveTradeConfig()
}
)
}
// SettingSwitchField(
// label = "해외 주식",
// initialChecked = tradeConfig.enableOverSea,
// onCheckedChange = { tradeConfig.enableOverSea = it
// KisSession.saveTradeConfig() }
// )
SettingSwitchField(
label = "해외 주식",
initialChecked = tradeConfig.enableOverSea,
onCheckedChange = { tradeConfig.enableOverSea = it
label = "배당 주만 거래",
initialChecked = tradeConfig.isUpcomingDividend,
onCheckedChange = { tradeConfig.isUpcomingDividend = it
KisSession.saveTradeConfig() }
)
@@ -965,6 +979,9 @@ fun SettingInputField(
@Composable
fun SettingSwitchField(
modifier :Modifier = Modifier
.fillMaxWidth()
.padding(vertical = 4.dp, horizontal = 4.dp),
label: String,
initialChecked: Boolean,
helperText: String = "",
@@ -974,9 +991,7 @@ fun SettingSwitchField(
var localChecked by remember { mutableStateOf(initialChecked) }
Column(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 4.dp, horizontal = 4.dp)
modifier = modifier
) {
Row(
modifier = Modifier.fillMaxWidth(),