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_profit
import ConfigTable.grade_2_allocationrate
import ConfigTable.grade_2_buy
import ConfigTable.grade_2_profit
import ConfigTable.grade_3_allocationrate
import ConfigTable.grade_3_buy
import ConfigTable.grade_3_profit
import ConfigTable.grade_4_allocationrate
import ConfigTable.grade_4_buy
import ConfigTable.grade_4_profit
import ConfigTable.grade_5_allocationrate
import ConfigTable.grade_5_buy
import ConfigTable.grade_5_profit
import ConfigTable.max_count
@ -195,6 +200,11 @@ fun main() = application {
GRADE_2_PROFIT = it[grade_2_profit],
GRADE_1_BUY = it[grade_1_buy],
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],
)
}

View File

@ -47,15 +47,34 @@ object ConfigTable : Table("app_config") {
val min_purchase_score = double("min_purchase_score").default( 65.0)
val sell_profit = double("sell_profit").default( 1.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_profit = double("grade_4_profit").default(1.3)
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_profit = double("grade_2_profit").default(0.7)
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_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)
override val primaryKey = PrimaryKey(id)
}
@ -260,6 +279,11 @@ object DatabaseFactory {
GRADE_2_PROFIT = it[ConfigTable.grade_2_profit],
GRADE_1_BUY = it[ConfigTable.grade_1_buy],
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],
)
}
@ -300,6 +324,12 @@ object DatabaseFactory {
it[grade_2_profit] = config.GRADE_2_PROFIT
it[grade_1_buy] = config.GRADE_1_BUY
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
}
}

View File

@ -25,7 +25,15 @@ enum class ConfigIndex(val index : Int,val label : String) {
GRADE_4_PROFIT(GRADE_5_PROFIT.index + 1, "안정적 투자 목표 수익 비율"),
GRADE_3_PROFIT(GRADE_4_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 {
fun get(index : Int) = ConfigIndex.entries[index]
}
@ -82,6 +90,13 @@ data class AppConfig(
var GRADE_3_PROFIT : Double = 0.9,
var GRADE_2_PROFIT : Double = 0.7,
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,
) {
@ -111,7 +126,15 @@ data class AppConfig(
ConfigIndex.GRADE_3_BUY -> {GRADE_3_BUY = value.toInt()}
ConfigIndex.GRADE_2_BUY -> {GRADE_2_BUY = value.toInt()}
ConfigIndex.GRADE_1_BUY -> {GRADE_1_BUY = 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 {
@ -148,6 +171,14 @@ data class AppConfig(
ConfigIndex.GRADE_3_PROFIT -> {GRADE_3_PROFIT}
ConfigIndex.GRADE_2_PROFIT -> {GRADE_2_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()}
}
}

View File

@ -215,59 +215,3 @@ data class ExecutionData(
val qty: String,
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 model.ConfigIndex
import model.KisSession
import model.RankingStock
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
@ -258,12 +259,16 @@ object RagService {
} else {
println("✋ [$stockName] 기술 점수 미달로 분석 중단 ${scores.toString()}")
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
result(tradingDecision, false)
}
} else {
println("🚨 [$stockName] ${FinancialAnalyzer.toString(financialStmt)} 재무 안전벨트 미달")
TradingLogStore.addAnalyzer(stockName, stockCode, "재무 안전벨트 미달로 분석 중단 ${FinancialAnalyzer.getInvestmentStatus(financialStmt)}")
TradingLogStore.addAnalyzer(stockName, stockCode, "재무 안전벨트 미달로 분석 중단 ${FinancialAnalyzer.toString(financialStmt)}")
tradingDecision.confidence = 1.0
result(tradingDecision, false)
}

View File

@ -28,7 +28,6 @@ import kotlinx.serialization.Serializable
import model.CandleData
import model.ConfigIndex
import model.ExecutionData
import model.InvestmentGrade
import model.KisSession
import model.RankingStock
import model.RankingType
@ -103,7 +102,7 @@ object AutoTradingManager {
println("🚀 [자동매수 실행] ${completeTradingDecision.stockName}")
if (completeTradingDecision.confidence < 10) {
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()) {
var basePrice = completeTradingDecision.currentPrice
var stockCode = completeTradingDecision.stockCode
@ -129,19 +128,10 @@ object AutoTradingManager {
var investmentGrade : InvestmentGrade = AutoTradingManager.getInvestmentGrade(completeTradingDecision,totalScore, completeTradingDecision.confidence)
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}")
// 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()
maxBudget = maxBudget * gradeRate
val calculatedQty = if (basePrice > 0) {
@ -159,7 +149,7 @@ object AutoTradingManager {
} 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))
TradingLogStore.addLog(completeTradingDecision,"HOLD","✋ [관망] 토탈 스코어[$totalScore] 또는 신뢰도[${completeTradingDecision.confidence}] 미달 이나 약간의 오차로 재분석 대기열에 추가")
TradingLogStore.addLog(completeTradingDecision,"RETRY","✋ [관망] 토탈 스코어[$totalScore] 또는 신뢰도[${completeTradingDecision.confidence}] 미달 이나 약간의 오차로 재분석 대기열에 추가")
} else {
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) {
AutoTradingManager.addToReanalysis(RankingStock(mksc_shrn_iscd = stockCode,hts_kor_isnm = stockName))
TradingLogStore.addLog(decision,"BUY","${it.message ?: " 매수 실패"} => 재분석 대기열에 추가")
TradingLogStore.addLog(decision,"WATCH","${it.message ?: " 매수 실패"} => 재분석 대기열에 추가")
} else {
TradingLogStore.addLog(decision,"BUY",it.message ?: "매수 실패")
}
@ -382,6 +372,11 @@ object AutoTradingManager {
balance.holdings.forEach { holding ->
if (BLACKLISTEDSTOCKCODES.contains(holding.code)){
println("❌ 차단 처리된 주식 : ${holding.name}")
TradingLogStore.addAnalyzer(
holding.name,
holding.code,
"거랙 차단 대상 : ${holding.currentPrice}[${holding.quantity}주] 보유, 수익률(${holding.profitRate.toDouble()})"
)
} else {
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} ")
@ -414,10 +409,13 @@ object AutoTradingManager {
"SELL",
"🎊 보유 주식 매도 주문 실패[${it.message}] "
)
// println("❌ [재주문 실패] ${holding.name}: ${it.message}")
}
} else {
TradingLogStore.addAnalyzer(
"보유주식[${holding.name}]",
holding.code,
"수익률 미달 : ${holding.currentPrice}[${holding.quantity}주] 보유, 수익률(${holding.profitRate.toDouble()})"
)
}
delay(200) // API 호출 부하 방지
}
@ -560,9 +558,14 @@ object AutoTradingManager {
return batch
}
suspend fun executeMarketLoop() {
suspend fun checkBalance() : UnifiedBalance? {
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) }
val myCash = balance?.deposit?.replace(",", "")?.toLongOrNull() ?: 0L
@ -622,9 +625,31 @@ object AutoTradingManager {
println("남은 후보군 개수 : ${totalCount}")
delay(100)
}
sellSchedule()
}
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) {
when {
@ -1035,7 +1060,7 @@ class TechnicalAnalyzer {
val disparityDaily = (currentPrice / ma20Daily) * 100
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
ultra -= (penalty / 2) // 초단기에도 영향
println("⚠️ [과열 감점] 일봉 이격도(${String.format("%.1f", disparityDaily)}%): -${penalty}")
@ -1046,9 +1071,9 @@ class TechnicalAnalyzer {
if (weekly.size >= 3) {
val weeklyChange = calculateChange(weekly.takeLast(3))
if (weeklyChange > 30.0) { // 3주간 30% 이상 급등 시
mid -= 15
short -= 7
println("⚠️ [과열 감점] 주봉 급등(${String.format("%.1f", weeklyChange)}%): -15")
mid -= 10
short -= 5
println("⚠️ [과열 감점] 주봉 급등(${String.format("%.1f", weeklyChange)}%): -10")
}
}
@ -1436,3 +1461,67 @@ fun CandleData.toScalpingCandle(): Candle {
fun List<CandleData>.toScalpingList(): List<Candle> {
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 kotlinx.coroutines.launch
import model.ConfigIndex
import model.InvestmentGrade
import model.KisSession
import model.RankingStock
import network.KisTradeService
import service.AutoTradingManager
import service.InvestmentGrade
import util.MarketUtil
import kotlin.math.min
import kotlin.math.roundToInt

View File

@ -35,7 +35,7 @@ fun TradingDecisionLog() {
val coroutineScope = rememberCoroutineScope()
var searchQuery by remember { mutableStateOf("") }
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) }
LaunchedEffect(AutoTradingManager.llmAnalyser) {
llmAnalyser = AutoTradingManager.llmAnalyser
@ -162,6 +162,8 @@ fun TradingDecisionLog() {
"HOLD" -> Color.DarkGray
"ANALYZER" -> Color.Green
"PASS" -> Color.Yellow
"RETRY" -> Color(0xFF00BCD4) // [추가] 하늘색 (재분석/대기열)
"WATCH" -> Color(0xFF4CAF50) // [추가] 연초록 (관심 종목 감시)
else -> Color.DarkGray
},
fontWeight = FontWeight.ExtraBold
@ -182,7 +184,7 @@ fun TradingDecisionLog() {
columns = GridCells.Fixed(2), // 2열 병렬 배치
horizontalArrangement = 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>()
item(span = { GridItemSpan(maxLineSpan) }) { // 2열을 모두 차지함
@ -260,6 +262,15 @@ fun TradingDecisionLog() {
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열을 모두 차지함
Text(
"💰매수 정책 및 기대 수익률",
@ -268,16 +279,11 @@ fun TradingDecisionLog() {
)
}
var defaults2 = arrayOf(
arrayOf(ConfigIndex.GRADE_5_BUY,
ConfigIndex.GRADE_5_PROFIT,),
arrayOf(ConfigIndex.GRADE_4_BUY,
ConfigIndex.GRADE_4_PROFIT,),
arrayOf(ConfigIndex.GRADE_3_BUY,
ConfigIndex.GRADE_3_PROFIT,),
arrayOf(ConfigIndex.GRADE_2_BUY,
ConfigIndex.GRADE_2_PROFIT,),
arrayOf(ConfigIndex.GRADE_1_BUY,
ConfigIndex.GRADE_1_PROFIT,),
arrayOf(ConfigIndex.GRADE_5_BUY, ConfigIndex.GRADE_5_PROFIT,ConfigIndex.GRADE_5_ALLOCATIONRATE),
arrayOf(ConfigIndex.GRADE_4_BUY, ConfigIndex.GRADE_4_PROFIT,ConfigIndex.GRADE_4_ALLOCATIONRATE),
arrayOf(ConfigIndex.GRADE_3_BUY, ConfigIndex.GRADE_3_PROFIT,ConfigIndex.GRADE_3_ALLOCATIONRATE),
arrayOf(ConfigIndex.GRADE_2_BUY, ConfigIndex.GRADE_2_PROFIT,ConfigIndex.GRADE_2_ALLOCATIONRATE),
arrayOf(ConfigIndex.GRADE_1_BUY, ConfigIndex.GRADE_1_PROFIT,ConfigIndex.GRADE_1_ALLOCATIONRATE),
)
for (items in defaults2) {
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(
ConfigIndex.TAX_INDEX)}) = ${(localText.toDouble() * KisSession.config.getValues(ConfigIndex.PROFIT_INDEX)) + KisSession.config.getValues(
ConfigIndex.TAX_INDEX)}"
} else if (configKey.name.contains("ALLOCATIONRATE")) {
getRemaining(configKey.label,common) + ": 최대 ${KisSession.config.getValues(ConfigIndex.MAX_BUDGET_INDEX) * newValue}원 투자}"
} else {
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(
ConfigIndex.TAX_INDEX)}) = ${(localText.toDouble() * KisSession.config.getValues(ConfigIndex.PROFIT_INDEX)) + KisSession.config.getValues(
ConfigIndex.TAX_INDEX)} "
} else if (configKey.name.contains("ALLOCATIONRATE")) {
getRemaining(configKey.label,common) + ": 최대 ${KisSession.config.getValues(ConfigIndex.MAX_BUDGET_INDEX) * localText.toDouble() }원 투자}"
} else {
getRemaining(configKey.label,common) + ": -${localText} 호가 매수}"
}