This commit is contained in:
lunaticbum 2026-04-02 14:05:14 +09:00
parent 62feed6078
commit ef1847f115
8 changed files with 215 additions and 96 deletions

View File

@ -1,11 +1,16 @@
import ConfigTable.grade_1_allocationrate
import ConfigTable.grade_1_buy import ConfigTable.grade_1_buy
import ConfigTable.grade_1_profit import ConfigTable.grade_1_profit
import ConfigTable.grade_2_allocationrate
import ConfigTable.grade_2_buy import ConfigTable.grade_2_buy
import ConfigTable.grade_2_profit import ConfigTable.grade_2_profit
import ConfigTable.grade_3_allocationrate
import ConfigTable.grade_3_buy import ConfigTable.grade_3_buy
import ConfigTable.grade_3_profit import ConfigTable.grade_3_profit
import ConfigTable.grade_4_allocationrate
import ConfigTable.grade_4_buy import ConfigTable.grade_4_buy
import ConfigTable.grade_4_profit import ConfigTable.grade_4_profit
import ConfigTable.grade_5_allocationrate
import ConfigTable.grade_5_buy import ConfigTable.grade_5_buy
import ConfigTable.grade_5_profit import ConfigTable.grade_5_profit
import ConfigTable.max_count import ConfigTable.max_count
@ -195,6 +200,11 @@ fun main() = application {
GRADE_2_PROFIT = it[grade_2_profit], GRADE_2_PROFIT = it[grade_2_profit],
GRADE_1_BUY = it[grade_1_buy], GRADE_1_BUY = it[grade_1_buy],
GRADE_1_PROFIT = it[grade_1_profit], GRADE_1_PROFIT = it[grade_1_profit],
GRADE_1_ALLOCATIONRATE = it[grade_1_allocationrate],
GRADE_2_ALLOCATIONRATE = it[grade_2_allocationrate],
GRADE_3_ALLOCATIONRATE = it[grade_3_allocationrate],
GRADE_4_ALLOCATIONRATE = it[grade_4_allocationrate],
GRADE_5_ALLOCATIONRATE = it[grade_5_allocationrate],
MAX_COUNT = it[max_count], MAX_COUNT = it[max_count],
) )
} }

View File

@ -47,15 +47,34 @@ object ConfigTable : Table("app_config") {
val min_purchase_score = double("min_purchase_score").default( 65.0) val min_purchase_score = double("min_purchase_score").default( 65.0)
val sell_profit = double("sell_profit").default( 1.0) val sell_profit = double("sell_profit").default( 1.0)
val grade_5_buy = integer("grade_5_buy").default(0) val grade_5_buy = integer("grade_5_buy").default(0)
val grade_5_profit = double("grade_5_profit").default(1.8)
val grade_4_buy = integer("grade_4_buy").default(1) val grade_4_buy = integer("grade_4_buy").default(1)
val grade_4_profit = double("grade_4_profit").default(1.3)
val grade_3_buy = integer("grade_3_buy").default(1) val grade_3_buy = integer("grade_3_buy").default(1)
val grade_3_profit = double("grade_3_profit").default(0.9)
val grade_2_buy = integer("grade_2_buy").default(2) val grade_2_buy = integer("grade_2_buy").default(2)
val grade_2_profit = double("grade_2_profit").default(0.7)
val grade_1_buy = integer("grade_1_buy").default(3) val grade_1_buy = integer("grade_1_buy").default(3)
val grade_5_profit = double("grade_5_profit").default(1.8)
val grade_4_profit = double("grade_4_profit").default(1.3)
val grade_3_profit = double("grade_3_profit").default(0.9)
val grade_2_profit = double("grade_2_profit").default(0.7)
val grade_1_profit = double("grade_1_profit").default(0.5) val grade_1_profit = double("grade_1_profit").default(0.5)
val grade_5_allocationrate = double("grade_5_allocationrate").default(1.0)
val grade_4_allocationrate = double("grade_4_allocationrate").default(0.8)
val grade_3_allocationrate = double("grade_3_allocationrate").default(0.6)
val grade_2_allocationrate = double("grade_2_allocationrate").default(0.4)
val grade_1_allocationrate = double("grade_1_allocationrate").default(0.3)
val max_count = integer("max_count").default(20) val max_count = integer("max_count").default(20)
override val primaryKey = PrimaryKey(id) override val primaryKey = PrimaryKey(id)
} }
@ -260,6 +279,11 @@ object DatabaseFactory {
GRADE_2_PROFIT = it[ConfigTable.grade_2_profit], GRADE_2_PROFIT = it[ConfigTable.grade_2_profit],
GRADE_1_BUY = it[ConfigTable.grade_1_buy], GRADE_1_BUY = it[ConfigTable.grade_1_buy],
GRADE_1_PROFIT = it[ConfigTable.grade_1_profit], GRADE_1_PROFIT = it[ConfigTable.grade_1_profit],
GRADE_1_ALLOCATIONRATE = it[ConfigTable.grade_1_allocationrate],
GRADE_2_ALLOCATIONRATE = it[ConfigTable.grade_2_allocationrate],
GRADE_3_ALLOCATIONRATE = it[ConfigTable.grade_3_allocationrate],
GRADE_4_ALLOCATIONRATE = it[ConfigTable.grade_4_allocationrate],
GRADE_5_ALLOCATIONRATE = it[ConfigTable.grade_5_allocationrate],
MAX_COUNT = it[ConfigTable.max_count], MAX_COUNT = it[ConfigTable.max_count],
) )
} }
@ -300,6 +324,12 @@ object DatabaseFactory {
it[grade_2_profit] = config.GRADE_2_PROFIT it[grade_2_profit] = config.GRADE_2_PROFIT
it[grade_1_buy] = config.GRADE_1_BUY it[grade_1_buy] = config.GRADE_1_BUY
it[grade_1_profit] = config.GRADE_1_PROFIT it[grade_1_profit] = config.GRADE_1_PROFIT
it[grade_5_allocationrate] = config.GRADE_5_ALLOCATIONRATE
it[grade_4_allocationrate] = config.GRADE_4_ALLOCATIONRATE
it[grade_3_allocationrate] = config.GRADE_3_ALLOCATIONRATE
it[grade_2_allocationrate] = config.GRADE_2_ALLOCATIONRATE
it[grade_1_allocationrate] = config.GRADE_1_ALLOCATIONRATE
it[max_count] = config.MAX_COUNT it[max_count] = config.MAX_COUNT
} }
} }

View File

@ -25,7 +25,15 @@ enum class ConfigIndex(val index : Int,val label : String) {
GRADE_4_PROFIT(GRADE_5_PROFIT.index + 1, "안정적 투자 목표 수익 비율"), GRADE_4_PROFIT(GRADE_5_PROFIT.index + 1, "안정적 투자 목표 수익 비율"),
GRADE_3_PROFIT(GRADE_4_PROFIT.index + 1, "보수적 투자 목표 수익 비율"), GRADE_3_PROFIT(GRADE_4_PROFIT.index + 1, "보수적 투자 목표 수익 비율"),
GRADE_2_PROFIT(GRADE_3_PROFIT.index + 1, "하이리스크,리턴 목표 수익 비율"), GRADE_2_PROFIT(GRADE_3_PROFIT.index + 1, "하이리스크,리턴 목표 수익 비율"),
GRADE_1_PROFIT(GRADE_2_PROFIT.index + 1, "공격적초단기 목표 수익 비율"); GRADE_1_PROFIT(GRADE_2_PROFIT.index + 1, "공격적초단기 목표 수익 비율"),
GRADE_5_ALLOCATIONRATE(GRADE_1_PROFIT.index + 1, "강력 추천 투자 목표 투자금 비율"),
GRADE_4_ALLOCATIONRATE(GRADE_5_ALLOCATIONRATE.index + 1, "안정적 투자 목표 투자금 비율"),
GRADE_3_ALLOCATIONRATE(GRADE_4_ALLOCATIONRATE.index + 1, "보수적 투자 목표 투자금 비율"),
GRADE_2_ALLOCATIONRATE(GRADE_3_ALLOCATIONRATE.index + 1, "하이리스크,리턴 목표 투자금 비율"),
GRADE_1_ALLOCATIONRATE(GRADE_2_ALLOCATIONRATE.index + 1, "공격적초단기 목표 투자금 비율");
companion object { companion object {
fun get(index : Int) = ConfigIndex.entries[index] fun get(index : Int) = ConfigIndex.entries[index]
} }
@ -82,6 +90,13 @@ data class AppConfig(
var GRADE_3_PROFIT : Double = 0.9, var GRADE_3_PROFIT : Double = 0.9,
var GRADE_2_PROFIT : Double = 0.7, var GRADE_2_PROFIT : Double = 0.7,
var GRADE_1_PROFIT : Double = 0.5, var GRADE_1_PROFIT : Double = 0.5,
var GRADE_5_ALLOCATIONRATE : Double = 1.0,
var GRADE_4_ALLOCATIONRATE : Double = 0.8,
var GRADE_3_ALLOCATIONRATE : Double = 0.6,
var GRADE_2_ALLOCATIONRATE : Double = 0.4,
var GRADE_1_ALLOCATIONRATE : Double = 0.3,
var MAX_COUNT : Int = 20, var MAX_COUNT : Int = 20,
) { ) {
@ -111,7 +126,15 @@ data class AppConfig(
ConfigIndex.GRADE_3_BUY -> {GRADE_3_BUY = value.toInt()} ConfigIndex.GRADE_3_BUY -> {GRADE_3_BUY = value.toInt()}
ConfigIndex.GRADE_2_BUY -> {GRADE_2_BUY = value.toInt()} ConfigIndex.GRADE_2_BUY -> {GRADE_2_BUY = value.toInt()}
ConfigIndex.GRADE_1_BUY -> {GRADE_1_BUY = value.toInt()} ConfigIndex.GRADE_1_BUY -> {GRADE_1_BUY = value.toInt()}
ConfigIndex.MAX_COUNT_INDEX -> {MAX_COUNT = value.toInt()} ConfigIndex.MAX_COUNT_INDEX -> {MAX_COUNT = value.toInt()}
ConfigIndex.GRADE_5_ALLOCATIONRATE -> {GRADE_5_ALLOCATIONRATE = value}
ConfigIndex.GRADE_4_ALLOCATIONRATE -> {GRADE_4_ALLOCATIONRATE = value}
ConfigIndex.GRADE_3_ALLOCATIONRATE -> {GRADE_3_ALLOCATIONRATE = value}
ConfigIndex.GRADE_2_ALLOCATIONRATE -> {GRADE_2_ALLOCATIONRATE = value}
ConfigIndex.GRADE_1_ALLOCATIONRATE -> {GRADE_1_ALLOCATIONRATE = value}
} }
} }
fun getValues(index :ConfigIndex) : Double { fun getValues(index :ConfigIndex) : Double {
@ -148,6 +171,14 @@ data class AppConfig(
ConfigIndex.GRADE_3_PROFIT -> {GRADE_3_PROFIT} ConfigIndex.GRADE_3_PROFIT -> {GRADE_3_PROFIT}
ConfigIndex.GRADE_2_PROFIT -> {GRADE_2_PROFIT} ConfigIndex.GRADE_2_PROFIT -> {GRADE_2_PROFIT}
ConfigIndex.GRADE_1_PROFIT -> {GRADE_1_PROFIT} ConfigIndex.GRADE_1_PROFIT -> {GRADE_1_PROFIT}
ConfigIndex.GRADE_5_ALLOCATIONRATE -> {GRADE_5_ALLOCATIONRATE}
ConfigIndex.GRADE_4_ALLOCATIONRATE -> {GRADE_4_ALLOCATIONRATE}
ConfigIndex.GRADE_3_ALLOCATIONRATE -> {GRADE_3_ALLOCATIONRATE}
ConfigIndex.GRADE_2_ALLOCATIONRATE -> {GRADE_2_ALLOCATIONRATE}
ConfigIndex.GRADE_1_ALLOCATIONRATE -> {GRADE_1_ALLOCATIONRATE}
ConfigIndex.MAX_COUNT_INDEX -> {MAX_COUNT.toDouble()} ConfigIndex.MAX_COUNT_INDEX -> {MAX_COUNT.toDouble()}
} }
} }

View File

@ -215,59 +215,3 @@ data class ExecutionData(
val qty: String, val qty: String,
val isFilled: Boolean val isFilled: Boolean
) )
enum class InvestmentGrade(
val displayName: String,
val description: String,
val shortWeight: Double = 0.0,
val midWeight: Double = 0.0,
val longWeight: Double = 0.0,
val profitGuide: ConfigIndex,
val buyGuide: ConfigIndex,
) {
LEVEL_5_STRONG_RECOMMEND(
displayName = "최상급 추천",
description = "단기·중기·장기 모두 우수하고, 신뢰도 매우 높은 범용 매수 추천",
shortWeight = 1.0,
midWeight = 1.0,
longWeight = 1.0,
profitGuide = ConfigIndex.GRADE_5_PROFIT,
buyGuide = ConfigIndex.GRADE_5_BUY,
),
LEVEL_4_BALANCED_RECOMMEND(
displayName = "균형 추천",
description = "중기·장기 기본은 양호하고, 단기 성과도 준수한 안정형 추천",
shortWeight = 0.8,
midWeight = 1.0,
longWeight = 1.0,
profitGuide = ConfigIndex.GRADE_4_PROFIT,
buyGuide = ConfigIndex.GRADE_4_BUY,
),
LEVEL_3_CAUTIOUS_RECOMMEND(
displayName = "보수적 추천",
description = "중기/장기 기본은 양호하지만, 단기 변동성이 높아 신중히 접근해야 함",
shortWeight = 0.6,
midWeight = 1.0,
longWeight = 1.0,
profitGuide = ConfigIndex.GRADE_3_PROFIT,
buyGuide = ConfigIndex.GRADE_3_BUY,
),
LEVEL_2_HIGH_RISK(
displayName = "고위험 추천",
description = "단기/초단기 성과만 강하고, 중기·장기가 애매하여 리스크가 큰 투자",
shortWeight = 1.0,
midWeight = 0.4,
longWeight = 0.4,
profitGuide = ConfigIndex.GRADE_2_PROFIT,
buyGuide = ConfigIndex.GRADE_2_BUY,
),
LEVEL_1_SPECULATIVE(
displayName = "순수 공격적 선택",
description = "단기/초단기 성과에만 의존하는 단기 급등형 공격적 투자",
shortWeight = 1.0,
midWeight = 0.2,
longWeight = 0.2,
profitGuide = ConfigIndex.GRADE_1_PROFIT,
buyGuide = ConfigIndex.GRADE_1_BUY,
)
}

View File

@ -31,6 +31,7 @@ import kotlinx.serialization.json.putJsonArray
import kotlinx.serialization.json.putJsonObject import kotlinx.serialization.json.putJsonObject
import model.ConfigIndex import model.ConfigIndex
import model.KisSession import model.KisSession
import model.RankingStock
import okhttp3.MediaType.Companion.toMediaType import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
@ -258,12 +259,16 @@ object RagService {
} else { } else {
println("✋ [$stockName] 기술 점수 미달로 분석 중단 ${scores.toString()}") println("✋ [$stockName] 기술 점수 미달로 분석 중단 ${scores.toString()}")
TradingLogStore.addAnalyzer(stockName, stockCode, "기술 점수 미달로 분석 중단") TradingLogStore.addAnalyzer(stockName, stockCode, "기술 점수 미달로 분석 중단")
if (FinancialAnalyzer.isBuyConsiderationMet(financialStmt)) {
TradingLogStore.addLog(tradingDecision,"WATCH","우량주로 판단되나 거래량 혹은 최근 거래 점수 미달로 재분석 대상에 추가")
AutoTradingManager.addToReanalysis(RankingStock(mksc_shrn_iscd = stockCode,hts_kor_isnm = stockName))
}
tradingDecision.confidence = 1.0 tradingDecision.confidence = 1.0
result(tradingDecision, false) result(tradingDecision, false)
} }
} else { } else {
println("🚨 [$stockName] ${FinancialAnalyzer.toString(financialStmt)} 재무 안전벨트 미달") println("🚨 [$stockName] ${FinancialAnalyzer.toString(financialStmt)} 재무 안전벨트 미달")
TradingLogStore.addAnalyzer(stockName, stockCode, "재무 안전벨트 미달로 분석 중단 ${FinancialAnalyzer.getInvestmentStatus(financialStmt)}") TradingLogStore.addAnalyzer(stockName, stockCode, "재무 안전벨트 미달로 분석 중단 ${FinancialAnalyzer.toString(financialStmt)}")
tradingDecision.confidence = 1.0 tradingDecision.confidence = 1.0
result(tradingDecision, false) result(tradingDecision, false)
} }

View File

@ -28,7 +28,6 @@ import kotlinx.serialization.Serializable
import model.CandleData import model.CandleData
import model.ConfigIndex import model.ConfigIndex
import model.ExecutionData import model.ExecutionData
import model.InvestmentGrade
import model.KisSession import model.KisSession
import model.RankingStock import model.RankingStock
import model.RankingType import model.RankingType
@ -103,7 +102,7 @@ object AutoTradingManager {
println("🚀 [자동매수 실행] ${completeTradingDecision.stockName}") println("🚀 [자동매수 실행] ${completeTradingDecision.stockName}")
if (completeTradingDecision.confidence < 10) { if (completeTradingDecision.confidence < 10) {
addToReanalysis(RankingStock(mksc_shrn_iscd = completeTradingDecision.stockCode,hts_kor_isnm = completeTradingDecision.stockName)) addToReanalysis(RankingStock(mksc_shrn_iscd = completeTradingDecision.stockCode,hts_kor_isnm = completeTradingDecision.stockName))
TradingLogStore.addLog(completeTradingDecision,"HOLD","분석 신뢰도 오류 인지로 재분석 대기열에 추가") TradingLogStore.addLog(completeTradingDecision,"RETRY","분석 신뢰도 오류 인지로 재분석 대기열에 추가")
}else if (completeTradingDecision != null && !completeTradingDecision.stockCode.isNullOrEmpty()) { }else if (completeTradingDecision != null && !completeTradingDecision.stockCode.isNullOrEmpty()) {
var basePrice = completeTradingDecision.currentPrice var basePrice = completeTradingDecision.currentPrice
var stockCode = completeTradingDecision.stockCode var stockCode = completeTradingDecision.stockCode
@ -129,19 +128,10 @@ object AutoTradingManager {
var investmentGrade : InvestmentGrade = AutoTradingManager.getInvestmentGrade(completeTradingDecision,totalScore, completeTradingDecision.confidence) var investmentGrade : InvestmentGrade = AutoTradingManager.getInvestmentGrade(completeTradingDecision,totalScore, completeTradingDecision.confidence)
val finalMargin = baseProfit * KisSession.config.getValues(investmentGrade.profitGuide) val finalMargin = baseProfit * KisSession.config.getValues(investmentGrade.profitGuide)
// println("""
// 사명 : ${completeTradingDecision.corpName}
// 신뢰도 : ${completeTradingDecision.confidence + append}
// 단기성 : ${completeTradingDecision.shortPossible() + append}
// 수익성 : ${completeTradingDecision.profitPossible()+ append}
// 안전성 : ${completeTradingDecision.safePossible()+ append}
// ${investmentGrade.displayName} : ${investmentGrade.description}
// 총점 : ${totalScore}
// """.trimIndent())
println("🚀 [매수 진행] 토탈 스코어: ${String.format("%.1f", totalScore)} -> 종목: ${completeTradingDecision.stockCode}") println("🚀 [매수 진행] 토탈 스코어: ${String.format("%.1f", totalScore)} -> 종목: ${completeTradingDecision.stockCode}")
// basePrice(현재가 혹은 지정가)를 기준으로 매수 가능 수량 산출 (최소 1주 보장) // basePrice(현재가 혹은 지정가)를 기준으로 매수 가능 수량 산출 (최소 1주 보장)
val gradeRate = (1.0 - (investmentGrade.ordinal * 0.1)) val gradeRate = (1.0 - (investmentGrade.ordinal * 0.12))
val maxQty = (KisSession.config.getValues(ConfigIndex.MAX_COUNT_INDEX) * gradeRate).roundToInt() val maxQty = (KisSession.config.getValues(ConfigIndex.MAX_COUNT_INDEX) * gradeRate).roundToInt()
maxBudget = maxBudget * gradeRate maxBudget = maxBudget * gradeRate
val calculatedQty = if (basePrice > 0) { val calculatedQty = if (basePrice > 0) {
@ -159,7 +149,7 @@ object AutoTradingManager {
} else if(totalScore >= (minScore * 0.85) && completeTradingDecision.confidence + append >= (MIN_CONFIDENCE * 0.85)) { } else if(totalScore >= (minScore * 0.85) && completeTradingDecision.confidence + append >= (MIN_CONFIDENCE * 0.85)) {
addToReanalysis(RankingStock(mksc_shrn_iscd = completeTradingDecision.stockCode,hts_kor_isnm = completeTradingDecision.stockName)) addToReanalysis(RankingStock(mksc_shrn_iscd = completeTradingDecision.stockCode,hts_kor_isnm = completeTradingDecision.stockName))
TradingLogStore.addLog(completeTradingDecision,"HOLD","✋ [관망] 토탈 스코어[$totalScore] 또는 신뢰도[${completeTradingDecision.confidence}] 미달 이나 약간의 오차로 재분석 대기열에 추가") TradingLogStore.addLog(completeTradingDecision,"RETRY","✋ [관망] 토탈 스코어[$totalScore] 또는 신뢰도[${completeTradingDecision.confidence}] 미달 이나 약간의 오차로 재분석 대기열에 추가")
} else { } else {
TradingLogStore.addLog(completeTradingDecision,"HOLD","✋ [관망] 토탈 스코어(${String.format("%.1f[${minScore}]", totalScore)}) 또는 신뢰도 (${String.format("%.1f[${MIN_CONFIDENCE}]", completeTradingDecision.confidence)}) 미달") TradingLogStore.addLog(completeTradingDecision,"HOLD","✋ [관망] 토탈 스코어(${String.format("%.1f[${minScore}]", totalScore)}) 또는 신뢰도 (${String.format("%.1f[${MIN_CONFIDENCE}]", completeTradingDecision.confidence)}) 미달")
} }
@ -285,7 +275,7 @@ object AutoTradingManager {
if (it.message?.contains("주문가능금액을 초과") == true) { if (it.message?.contains("주문가능금액을 초과") == true) {
AutoTradingManager.addToReanalysis(RankingStock(mksc_shrn_iscd = stockCode,hts_kor_isnm = stockName)) AutoTradingManager.addToReanalysis(RankingStock(mksc_shrn_iscd = stockCode,hts_kor_isnm = stockName))
TradingLogStore.addLog(decision,"BUY","${it.message ?: " 매수 실패"} => 재분석 대기열에 추가") TradingLogStore.addLog(decision,"WATCH","${it.message ?: " 매수 실패"} => 재분석 대기열에 추가")
} else { } else {
TradingLogStore.addLog(decision,"BUY",it.message ?: "매수 실패") TradingLogStore.addLog(decision,"BUY",it.message ?: "매수 실패")
} }
@ -382,6 +372,11 @@ object AutoTradingManager {
balance.holdings.forEach { holding -> balance.holdings.forEach { holding ->
if (BLACKLISTEDSTOCKCODES.contains(holding.code)){ if (BLACKLISTEDSTOCKCODES.contains(holding.code)){
println("❌ 차단 처리된 주식 : ${holding.name}") println("❌ 차단 처리된 주식 : ${holding.name}")
TradingLogStore.addAnalyzer(
holding.name,
holding.code,
"거랙 차단 대상 : ${holding.currentPrice}[${holding.quantity}주] 보유, 수익률(${holding.profitRate.toDouble()})"
)
} else { } else {
if (holding != null && holding.quantity.toInt() > 0 && holding.availOrderCount.toInt() > 0 && holding.profitRate.toDouble() > KisSession.config.SELL_PROFIT) { if (holding != null && holding.quantity.toInt() > 0 && holding.availOrderCount.toInt() > 0 && holding.profitRate.toDouble() > KisSession.config.SELL_PROFIT) {
println("${holding.name} - 매수 : ${holding.avgPrice} - 현재 : ${holding.currentPrice} ") println("${holding.name} - 매수 : ${holding.avgPrice} - 현재 : ${holding.currentPrice} ")
@ -414,10 +409,13 @@ object AutoTradingManager {
"SELL", "SELL",
"🎊 보유 주식 매도 주문 실패[${it.message}] " "🎊 보유 주식 매도 주문 실패[${it.message}] "
) )
// println("❌ [재주문 실패] ${holding.name}: ${it.message}")
} }
} else { } else {
TradingLogStore.addAnalyzer(
"보유주식[${holding.name}]",
holding.code,
"수익률 미달 : ${holding.currentPrice}[${holding.quantity}주] 보유, 수익률(${holding.profitRate.toDouble()})"
)
} }
delay(200) // API 호출 부하 방지 delay(200) // API 호출 부하 방지
} }
@ -560,9 +558,14 @@ object AutoTradingManager {
return batch return batch
} }
suspend fun executeMarketLoop() { suspend fun checkBalance() : UnifiedBalance? {
val balance = KisTradeService.fetchIntegratedBalance().getOrNull() val balance = KisTradeService.fetchIntegratedBalance().getOrNull()
if (AUTOSELL) balance?.let { resumePendingSellOrders(KisTradeService, it) }
return balance
}
suspend fun executeMarketLoop() {
val balance = checkBalance()
if (AUTOSELL) balance?.let { resumePendingSellOrders(KisTradeService, it) } if (AUTOSELL) balance?.let { resumePendingSellOrders(KisTradeService, it) }
val myCash = balance?.deposit?.replace(",", "")?.toLongOrNull() ?: 0L val myCash = balance?.deposit?.replace(",", "")?.toLongOrNull() ?: 0L
@ -622,9 +625,31 @@ object AutoTradingManager {
println("남은 후보군 개수 : ${totalCount}") println("남은 후보군 개수 : ${totalCount}")
delay(100) delay(100)
} }
sellSchedule()
} }
println("⏱️ [Cycle End] ${LocalTime.now()}") println("⏱️ [Cycle End] ${LocalTime.now()}")
} }
private var lastForceCheckMinute = -1 // 마지막으로 강제 체크를 수행한 '분'을 저장
suspend fun sellSchedule() {
val now = LocalTime.now()
val currentMinute = now.minute
println("매도 스케줄 체크")
if (now.hour == 9 && (currentMinute == 1 || currentMinute == 15 || currentMinute == 40)) {
if (lastForceCheckMinute != currentMinute) {
TradingLogStore.addAnalyzer(" - ", " - ", "⏰ [강제 스케줄 실행] 오전 9시 ${currentMinute}분 - 보유주식 매도 체크를 시작합니다.", true)
println("⏰ [강제 스케줄 실행] 오전 9시 ${currentMinute}분 - 보유주식 매도 체크를 시작합니다.")
checkBalance()
lastForceCheckMinute = currentMinute // 실행 완료 기록
}
}
// else if(now.hour % 2 == 1 && (currentMinute == 43)) {
// TradingLogStore.addAnalyzer(" - ", " - ", "⏰ [강제 스케줄 실행] 오전 9시 ${currentMinute}분 - 보유주식 매도 체크를 시작합니다.", true)
// println("⏰ [강제 스케줄 실행] 오전 ${now.hour}시 ${currentMinute}분 - 보유주식 매도 체크를 시작합니다.")
// checkBalance()
// lastForceCheckMinute = currentMinute // 실행 완료 기록
// }
}
suspend fun finalizeMarketClose(now: LocalTime) { suspend fun finalizeMarketClose(now: LocalTime) {
when { when {
@ -1035,7 +1060,7 @@ class TechnicalAnalyzer {
val disparityDaily = (currentPrice / ma20Daily) * 100 val disparityDaily = (currentPrice / ma20Daily) * 100
if (disparityDaily > 115.0) { // 20일선보다 15% 이상 떠 있으면 감점 시작 if (disparityDaily > 115.0) { // 20일선보다 15% 이상 떠 있으면 감점 시작
val penalty = ((disparityDaily - 115.0) * 1).toInt() // 초과분 1%당 2점 감점 val penalty = ((disparityDaily - 115.0) * 0.5).toInt() // 초과분 1%당 2점 감점
short -= penalty short -= penalty
ultra -= (penalty / 2) // 초단기에도 영향 ultra -= (penalty / 2) // 초단기에도 영향
println("⚠️ [과열 감점] 일봉 이격도(${String.format("%.1f", disparityDaily)}%): -${penalty}") println("⚠️ [과열 감점] 일봉 이격도(${String.format("%.1f", disparityDaily)}%): -${penalty}")
@ -1046,9 +1071,9 @@ class TechnicalAnalyzer {
if (weekly.size >= 3) { if (weekly.size >= 3) {
val weeklyChange = calculateChange(weekly.takeLast(3)) val weeklyChange = calculateChange(weekly.takeLast(3))
if (weeklyChange > 30.0) { // 3주간 30% 이상 급등 시 if (weeklyChange > 30.0) { // 3주간 30% 이상 급등 시
mid -= 15 mid -= 10
short -= 7 short -= 5
println("⚠️ [과열 감점] 주봉 급등(${String.format("%.1f", weeklyChange)}%): -15") println("⚠️ [과열 감점] 주봉 급등(${String.format("%.1f", weeklyChange)}%): -10")
} }
} }
@ -1436,3 +1461,67 @@ fun CandleData.toScalpingCandle(): Candle {
fun List<CandleData>.toScalpingList(): List<Candle> { fun List<CandleData>.toScalpingList(): List<Candle> {
return this.map { it.toScalpingCandle() } return this.map { it.toScalpingCandle() }
} }
enum class InvestmentGrade(
val displayName: String,
val description: String,
val shortWeight: Double = 0.0,
val midWeight: Double = 0.0,
val longWeight: Double = 0.0,
val profitGuide: ConfigIndex,
val buyGuide: ConfigIndex,
val allocationRate: ConfigIndex,
) {
LEVEL_5_STRONG_RECOMMEND(
displayName = "최상급 추천",
description = "단기·중기·장기 모두 우수하고, 신뢰도 매우 높은 범용 매수 추천",
shortWeight = 1.0,
midWeight = 1.0,
longWeight = 1.0,
profitGuide = ConfigIndex.GRADE_5_PROFIT,
buyGuide = ConfigIndex.GRADE_5_BUY,
allocationRate = ConfigIndex.GRADE_5_ALLOCATIONRATE,
),
LEVEL_4_BALANCED_RECOMMEND(
displayName = "균형 추천",
description = "중기·장기 기본은 양호하고, 단기 성과도 준수한 안정형 추천",
shortWeight = 0.8,
midWeight = 1.0,
longWeight = 1.0,
profitGuide = ConfigIndex.GRADE_4_PROFIT,
buyGuide = ConfigIndex.GRADE_4_BUY,
allocationRate = ConfigIndex.GRADE_4_ALLOCATIONRATE,
),
LEVEL_3_CAUTIOUS_RECOMMEND(
displayName = "보수적 추천",
description = "중기/장기 기본은 양호하지만, 단기 변동성이 높아 신중히 접근해야 함",
shortWeight = 0.6,
midWeight = 1.0,
longWeight = 1.0,
profitGuide = ConfigIndex.GRADE_3_PROFIT,
buyGuide = ConfigIndex.GRADE_3_BUY,
allocationRate = ConfigIndex.GRADE_3_ALLOCATIONRATE,
),
LEVEL_2_HIGH_RISK(
displayName = "고위험 추천",
description = "단기/초단기 성과만 강하고, 중기·장기가 애매하여 리스크가 큰 투자",
shortWeight = 1.0,
midWeight = 0.4,
longWeight = 0.4,
profitGuide = ConfigIndex.GRADE_2_PROFIT,
buyGuide = ConfigIndex.GRADE_2_BUY,
allocationRate = ConfigIndex.GRADE_2_ALLOCATIONRATE,
),
LEVEL_1_SPECULATIVE(
displayName = "순수 공격적 선택",
description = "단기/초단기 성과에만 의존하는 단기 급등형 공격적 투자",
shortWeight = 1.0,
midWeight = 0.2,
longWeight = 0.2,
profitGuide = ConfigIndex.GRADE_1_PROFIT,
buyGuide = ConfigIndex.GRADE_1_BUY,
allocationRate = ConfigIndex.GRADE_1_ALLOCATIONRATE,
)
}

View File

@ -21,11 +21,11 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import model.ConfigIndex import model.ConfigIndex
import model.InvestmentGrade
import model.KisSession import model.KisSession
import model.RankingStock import model.RankingStock
import network.KisTradeService import network.KisTradeService
import service.AutoTradingManager import service.AutoTradingManager
import service.InvestmentGrade
import util.MarketUtil import util.MarketUtil
import kotlin.math.min import kotlin.math.min
import kotlin.math.roundToInt import kotlin.math.roundToInt

View File

@ -35,7 +35,7 @@ fun TradingDecisionLog() {
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
var searchQuery by remember { mutableStateOf("") } var searchQuery by remember { mutableStateOf("") }
var selectedFilters by remember { mutableStateOf(setOf("전체")) } var selectedFilters by remember { mutableStateOf(setOf("전체")) }
val filterOptions = listOf("전체", "BUY", "SELL", "HOLD", "SETTING","ANALYZER","PASS") val filterOptions = listOf("전체", "BUY", "SELL", "HOLD", "SETTING","ANALYZER","PASS","WATCH","RETRY")
var llmAnalyser by remember { mutableStateOf(AutoTradingManager.llmAnalyser) } var llmAnalyser by remember { mutableStateOf(AutoTradingManager.llmAnalyser) }
LaunchedEffect(AutoTradingManager.llmAnalyser) { LaunchedEffect(AutoTradingManager.llmAnalyser) {
llmAnalyser = AutoTradingManager.llmAnalyser llmAnalyser = AutoTradingManager.llmAnalyser
@ -162,6 +162,8 @@ fun TradingDecisionLog() {
"HOLD" -> Color.DarkGray "HOLD" -> Color.DarkGray
"ANALYZER" -> Color.Green "ANALYZER" -> Color.Green
"PASS" -> Color.Yellow "PASS" -> Color.Yellow
"RETRY" -> Color(0xFF00BCD4) // [추가] 하늘색 (재분석/대기열)
"WATCH" -> Color(0xFF4CAF50) // [추가] 연초록 (관심 종목 감시)
else -> Color.DarkGray else -> Color.DarkGray
}, },
fontWeight = FontWeight.ExtraBold fontWeight = FontWeight.ExtraBold
@ -182,7 +184,7 @@ fun TradingDecisionLog() {
columns = GridCells.Fixed(2), // 2열 병렬 배치 columns = GridCells.Fixed(2), // 2열 병렬 배치
horizontalArrangement = Arrangement.spacedBy(6.dp), horizontalArrangement = Arrangement.spacedBy(6.dp),
verticalArrangement = Arrangement.spacedBy(6.dp), verticalArrangement = Arrangement.spacedBy(6.dp),
modifier = Modifier.fillMaxWidth().fillMaxHeight().background(Color.White) modifier = Modifier.fillMaxWidth().weight(0.5f).background(Color.White)
) { ) {
var firstSet = mutableSetOf<ConfigIndex>() var firstSet = mutableSetOf<ConfigIndex>()
item(span = { GridItemSpan(maxLineSpan) }) { // 2열을 모두 차지함 item(span = { GridItemSpan(maxLineSpan) }) { // 2열을 모두 차지함
@ -260,6 +262,15 @@ fun TradingDecisionLog() {
singleLine = true singleLine = true
) )
} }
}
LazyVerticalGrid(
columns = GridCells.Fixed(3), // 2열 병렬 배치
horizontalArrangement = Arrangement.spacedBy(6.dp),
verticalArrangement = Arrangement.spacedBy(6.dp),
modifier = Modifier.fillMaxWidth().weight(0.5f).background(Color.White)
) {
var firstSet = mutableSetOf<ConfigIndex>()
item(span = { GridItemSpan(maxLineSpan) }) { // 2열을 모두 차지함 item(span = { GridItemSpan(maxLineSpan) }) { // 2열을 모두 차지함
Text( Text(
"💰매수 정책 및 기대 수익률", "💰매수 정책 및 기대 수익률",
@ -268,16 +279,11 @@ fun TradingDecisionLog() {
) )
} }
var defaults2 = arrayOf( var defaults2 = arrayOf(
arrayOf(ConfigIndex.GRADE_5_BUY, arrayOf(ConfigIndex.GRADE_5_BUY, ConfigIndex.GRADE_5_PROFIT,ConfigIndex.GRADE_5_ALLOCATIONRATE),
ConfigIndex.GRADE_5_PROFIT,), arrayOf(ConfigIndex.GRADE_4_BUY, ConfigIndex.GRADE_4_PROFIT,ConfigIndex.GRADE_4_ALLOCATIONRATE),
arrayOf(ConfigIndex.GRADE_4_BUY, arrayOf(ConfigIndex.GRADE_3_BUY, ConfigIndex.GRADE_3_PROFIT,ConfigIndex.GRADE_3_ALLOCATIONRATE),
ConfigIndex.GRADE_4_PROFIT,), arrayOf(ConfigIndex.GRADE_2_BUY, ConfigIndex.GRADE_2_PROFIT,ConfigIndex.GRADE_2_ALLOCATIONRATE),
arrayOf(ConfigIndex.GRADE_3_BUY, arrayOf(ConfigIndex.GRADE_1_BUY, ConfigIndex.GRADE_1_PROFIT,ConfigIndex.GRADE_1_ALLOCATIONRATE),
ConfigIndex.GRADE_3_PROFIT,),
arrayOf(ConfigIndex.GRADE_2_BUY,
ConfigIndex.GRADE_2_PROFIT,),
arrayOf(ConfigIndex.GRADE_1_BUY,
ConfigIndex.GRADE_1_PROFIT,),
) )
for (items in defaults2) { for (items in defaults2) {
val common = findLongestCommonSubstring(items.first().label,items.last().label) val common = findLongestCommonSubstring(items.first().label,items.last().label)
@ -316,6 +322,8 @@ fun TradingDecisionLog() {
getRemaining(configKey.label,common) + ": 기준율(${KisSession.config.getValues(ConfigIndex.PROFIT_INDEX)}) * 성향별 비율(${KisSession.config.getValues(configKey)}) + 세금제비용(${KisSession.config.getValues( getRemaining(configKey.label,common) + ": 기준율(${KisSession.config.getValues(ConfigIndex.PROFIT_INDEX)}) * 성향별 비율(${KisSession.config.getValues(configKey)}) + 세금제비용(${KisSession.config.getValues(
ConfigIndex.TAX_INDEX)}) = ${(localText.toDouble() * KisSession.config.getValues(ConfigIndex.PROFIT_INDEX)) + KisSession.config.getValues( ConfigIndex.TAX_INDEX)}) = ${(localText.toDouble() * KisSession.config.getValues(ConfigIndex.PROFIT_INDEX)) + KisSession.config.getValues(
ConfigIndex.TAX_INDEX)}" ConfigIndex.TAX_INDEX)}"
} else if (configKey.name.contains("ALLOCATIONRATE")) {
getRemaining(configKey.label,common) + ": 최대 ${KisSession.config.getValues(ConfigIndex.MAX_BUDGET_INDEX) * newValue}원 투자}"
} else { } else {
getRemaining(configKey.label,common) + ": -${localText} 호가 매수}" getRemaining(configKey.label,common) + ": -${localText} 호가 매수}"
} }
@ -325,6 +333,8 @@ fun TradingDecisionLog() {
getRemaining(configKey.label,common) + ": 기준율(${KisSession.config.getValues(ConfigIndex.PROFIT_INDEX)}) * 성향별 비율(${KisSession.config.getValues(configKey)}) + 세금제비용(${KisSession.config.getValues( getRemaining(configKey.label,common) + ": 기준율(${KisSession.config.getValues(ConfigIndex.PROFIT_INDEX)}) * 성향별 비율(${KisSession.config.getValues(configKey)}) + 세금제비용(${KisSession.config.getValues(
ConfigIndex.TAX_INDEX)}) = ${(localText.toDouble() * KisSession.config.getValues(ConfigIndex.PROFIT_INDEX)) + KisSession.config.getValues( ConfigIndex.TAX_INDEX)}) = ${(localText.toDouble() * KisSession.config.getValues(ConfigIndex.PROFIT_INDEX)) + KisSession.config.getValues(
ConfigIndex.TAX_INDEX)} " ConfigIndex.TAX_INDEX)} "
} else if (configKey.name.contains("ALLOCATIONRATE")) {
getRemaining(configKey.label,common) + ": 최대 ${KisSession.config.getValues(ConfigIndex.MAX_BUDGET_INDEX) * localText.toDouble() }원 투자}"
} else { } else {
getRemaining(configKey.label,common) + ": -${localText} 호가 매수}" getRemaining(configKey.label,common) + ": -${localText} 호가 매수}"
} }