....
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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"(월)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -564,23 +564,37 @@ fun TradingDecisionLog() {
|
||||
helperText = "현재: ${tradeConfig.auto_cancel_pending_time / 1000}초 후 취소"
|
||||
)
|
||||
|
||||
Row(horizontalArrangement = Arrangement.SpaceEvenly) {
|
||||
SettingSwitchField (
|
||||
modifier = Modifier.weight(1.0f, true),
|
||||
label = "장 전 대체마켓 매도",
|
||||
initialChecked = tradeConfig.before_nxt,
|
||||
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()
|
||||
}
|
||||
)
|
||||
}
|
||||
// SettingSwitchField(
|
||||
// label = "해외 주식",
|
||||
// initialChecked = tradeConfig.enableOverSea,
|
||||
// onCheckedChange = { tradeConfig.enableOverSea = it
|
||||
// KisSession.saveTradeConfig() }
|
||||
// )
|
||||
SettingSwitchField(
|
||||
label = "장 전 대체마켓 매도",
|
||||
initialChecked = tradeConfig.before_nxt,
|
||||
onCheckedChange = { tradeConfig.before_nxt = it
|
||||
KisSession.saveTradeConfig() }
|
||||
)
|
||||
SettingSwitchField(
|
||||
label = "장 후 대체 마켓 매도",
|
||||
initialChecked = tradeConfig.after_nxt,
|
||||
onCheckedChange = { tradeConfig.after_nxt = 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(),
|
||||
|
||||
Reference in New Issue
Block a user