package model import com.google.gson.Gson import com.google.gson.GsonBuilder import report.TradingReportManager import java.io.File import java.time.LocalDateTime import java.time.LocalTime import kotlin.math.abs enum class ConfigIndex(val index : Int,val label : String) { TAX_INDEX(0,"세금 및 수수료"), PROFIT_INDEX(TAX_INDEX.index + 1 , "기준 수익률"), BUY_WEIGHT_INDEX(PROFIT_INDEX.index + 1, "매수 가중치"), MAX_BUDGET_INDEX(BUY_WEIGHT_INDEX.index + 1, "종목 당 최대 투자금"), MAX_PRICE_INDEX(MAX_BUDGET_INDEX.index + 1 , "단일 종목 주당 최대 금액"), MIN_PRICE_INDEX(MAX_PRICE_INDEX.index + 1, "단일 종목 주당 최소 금액"), MIN_PURCHASE_SCORE_INDEX(MIN_PRICE_INDEX.index + 1, "주문 최소 기준 점수"), MAX_COUNT_INDEX(MIN_PURCHASE_SCORE_INDEX.index + 1, "단일 종목 최대 주문 개수"), SELL_PROFIT(MAX_COUNT_INDEX.index + 1, "보유 주식 매도 기준 수익율"), GRADE_5_BUY(SELL_PROFIT.index + 1, "강력 추천 투자 매수 기준"), GRADE_4_BUY(GRADE_5_BUY.index + 1, "안정적 투자 매수 기준"), GRADE_3_BUY(GRADE_4_BUY.index + 1, "보수적 투자 매수 기준"), GRADE_2_BUY(GRADE_3_BUY.index + 10, "하이리스크,리턴 매수 기준"), GRADE_1_BUY(GRADE_2_BUY.index + 1, "공격적초단기 매수 기준"), GRADE_5_PROFIT(GRADE_1_BUY.index + 1, "강력 추천 투자 목표 수익 비율"), 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_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, "공격적초단기 목표 투자금 비율"), TAKE_PROFIT(GRADE_1_ALLOCATIONRATE.index + 1, "보유 주식 자동 매매 활성") , STOP_LOSS(TAKE_PROFIT.index + 1, "손절 활성 여부 (최소율~최대율 사이의 최대 금액 이하로 자동 손절)") , LOSS_MINRATE(STOP_LOSS.index + 1, "손절 최소 기준") , LOSS_MAXRATE(LOSS_MINRATE.index + 1, "손절 최소 기준") , LOSS_MAX_MONEY(LOSS_MAXRATE.index + 1, "손절 최대 금액") , MAX_HOLDING_COUNT(LOSS_MAX_MONEY.index + 1, "최대 보유 가능 종목 수"), ; companion object { fun get(index : Int) = ConfigIndex.entries[index] } } data class AppConfig( // [DB 저장 데이터] // 실전 3종 val realAppKey: String = "", val realSecretKey: String = "", val realAccountNo: String = "", // 모의 3종 val vtsAppKey: String = "", val vtsSecretKey: String = "", val vtsAccountNo: String = "", // 모의 3종 val nAppKey: String = "", val nSecretKey: String = "", val dAppKey: String = "", // [세션 데이터 - 메모리에서만 관리] var marketToken: String = "", var marketTokenExpiredAt: LocalDateTime? = null, // 만료 시간 추가 var tradeToken: String = "", var tradeTokenExpiredAt: LocalDateTime? = null, val htsId: String = "", var websocketToken: String = "", val isSimulation: Boolean = true, val modelPath: String = "", val embedModelPath: String = "", var FEES_AND_TAXRATE: Double = 0.33, var MINIMUM_NET_PROFIT: Double = 0.8, var BUY_WEIGHT: Double = 2.0, var MAX_BUDGET: Double = 80000.0, var MAX_PRICE: Double = 40000.0, var MIN_PRICE: Double = 800.0, var MIN_PURCHASE_SCORE: Double = 65.0, var SELL_PROFIT: Double = 1.3, var GRADE_5_BUY : Int = 0, var GRADE_4_BUY : Int = 1, var GRADE_3_BUY : Int = 2, var GRADE_2_BUY : Int = 2, var GRADE_1_BUY : Int = 3, var GRADE_5_PROFIT : Double = 1.8, var GRADE_4_PROFIT : Double = 1.3, 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, var take_profit : Boolean = false, var stop_Loss : Boolean = false, var loss_min : Double = 3.5, var loss_max : Double = 10.0, var loss_money : Double = 10000.0, var max_holding_count : Double = 100.0, ) { val accountNo : String get() { return if (isSimulation) vtsAccountNo else realAccountNo } var firstSet = mutableSetOf() fun setValues(index :ConfigIndex , value : Double) { val oldValue = getValues(index) when (index) { ConfigIndex.TAX_INDEX -> {FEES_AND_TAXRATE = value} ConfigIndex.PROFIT_INDEX -> {MINIMUM_NET_PROFIT = value} ConfigIndex.MAX_PRICE_INDEX -> {MAX_PRICE = value} ConfigIndex.MIN_PRICE_INDEX -> {MIN_PRICE = value} ConfigIndex.BUY_WEIGHT_INDEX -> {BUY_WEIGHT = value} ConfigIndex.MAX_BUDGET_INDEX -> {MAX_BUDGET = value} ConfigIndex.MIN_PURCHASE_SCORE_INDEX -> {MIN_PURCHASE_SCORE = value} ConfigIndex.SELL_PROFIT -> {SELL_PROFIT = value} ConfigIndex.GRADE_5_PROFIT -> {GRADE_5_PROFIT = value} ConfigIndex.GRADE_4_PROFIT -> {GRADE_4_PROFIT = value} ConfigIndex.GRADE_3_PROFIT -> {GRADE_3_PROFIT = value} ConfigIndex.GRADE_2_PROFIT -> {GRADE_2_PROFIT = value} ConfigIndex.GRADE_1_PROFIT -> {GRADE_1_PROFIT = value} ConfigIndex.GRADE_5_BUY -> {GRADE_5_BUY = value.toInt()} ConfigIndex.GRADE_4_BUY -> {GRADE_4_BUY = value.toInt()} 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} ConfigIndex.LOSS_MAXRATE -> { loss_max = value} ConfigIndex.LOSS_MINRATE -> { loss_min = value} ConfigIndex.LOSS_MAX_MONEY -> { loss_money = value} ConfigIndex.STOP_LOSS -> { stop_Loss = value > 0.1} ConfigIndex.TAKE_PROFIT -> { take_profit = value > 0.1 } ConfigIndex.MAX_HOLDING_COUNT -> { max_holding_count = value } } if (firstSet.contains(index)) { DatabaseFactory.saveConfig(KisSession.config) TradingLogStore.addSettingLog( index.label, oldValue.toString(), value.toString(), "💾 저장됨: ${index.label} = ${getValues(index)}" ) TradingReportManager.recordConfigChange() } else { firstSet.add(index) } } fun getValues(index :ConfigIndex) : Double { return when (index) { ConfigIndex.TAX_INDEX -> { FEES_AND_TAXRATE } ConfigIndex.PROFIT_INDEX -> { MINIMUM_NET_PROFIT } ConfigIndex.MAX_PRICE_INDEX -> { MAX_PRICE } ConfigIndex.MIN_PRICE_INDEX -> { MIN_PRICE } ConfigIndex.BUY_WEIGHT_INDEX -> { BUY_WEIGHT } ConfigIndex.MAX_BUDGET_INDEX -> { MAX_BUDGET } ConfigIndex.MIN_PURCHASE_SCORE_INDEX -> { MIN_PURCHASE_SCORE } ConfigIndex.SELL_PROFIT -> {SELL_PROFIT } ConfigIndex.GRADE_5_BUY -> {GRADE_5_BUY.toDouble()} ConfigIndex.GRADE_4_BUY -> {GRADE_4_BUY.toDouble()} ConfigIndex.GRADE_3_BUY -> {GRADE_3_BUY.toDouble()} ConfigIndex.GRADE_2_BUY -> {GRADE_2_BUY.toDouble()} ConfigIndex.GRADE_1_BUY -> {GRADE_1_BUY.toDouble()} ConfigIndex.GRADE_5_PROFIT -> {GRADE_5_PROFIT} ConfigIndex.GRADE_4_PROFIT -> {GRADE_4_PROFIT} 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.LOSS_MAXRATE -> { abs(loss_max) * -1} ConfigIndex.LOSS_MINRATE -> { abs(loss_min) * -1} ConfigIndex.LOSS_MAX_MONEY -> { abs(loss_money) * -1} ConfigIndex.STOP_LOSS -> {if(!stop_Loss) 0.0 else 1.0} ConfigIndex.TAKE_PROFIT -> {if(!take_profit) 0.0 else 1.0} ConfigIndex.MAX_COUNT_INDEX -> {MAX_COUNT.toDouble()} ConfigIndex.MAX_HOLDING_COUNT -> { max_holding_count} } } } class TradeConfig { var auto_cancel_pending_rate : Double = 3.0 var auto_cancel_pending_time : Long = 60L * 60L * 1000L var auto_cancel_pending_buy : Boolean = false var auto_start_time : String = "08:00" var auto_end_time : String = "20:00" var before_nxt : Boolean = false var after_nxt : Boolean = false var start_buy_time : String = "08:55" var end_buy_time : String = "15:10" var enableOverSea : Boolean = false var tlg_id : String = "" var CYCLE_TIMEOUT = 15 * 60 * 1000L // 한 사이클 최대 10분 var WATCHDOG_CHECK_INTERVAL = 30 * 1000L // 30초마다 생존 확인 var STUCK_THRESHOLD = 7 * 60 * 1000L // 5분간 반응 없으면 'Stuck'으로 판단 var ONE_STOCK_ALYSIS_TIME = 180000L var isLowPerformanceMonitoring: Boolean = false var useGradeShare : List = listOf("LEVEL_4","LEVEL_5") var useTagsShare : List = listOf("NOTICE", "WATCH") var useLogKeywordsShare : List = listOf("재분석") var useAutoRepost : Boolean = false var minusFilter : Double = 15.0 var plusFilter : Double = 15.0 } // [신규] 전역에서 참조할 단일 세션 객체 object KisSession { var config: AppConfig = AppConfig() var tradeConfig : TradeConfig = TradeConfig() fun getWebSocketKey() = config.websocketToken // 시장 데이터 토큰 유효성 검사 (만료 5분 전부터는 유효하지 않은 것으로 간주) fun isMarketTokenValid(): Boolean { return config.marketToken.isNotEmpty() && config.marketTokenExpiredAt?.isAfter(LocalDateTime.now().plusMinutes(5)) ?: false } // 매매용 토큰 유효성 검사 fun isTradeTokenValid(): Boolean { return config.tradeToken.isNotEmpty() && config.tradeTokenExpiredAt?.isAfter(LocalDateTime.now().plusMinutes(5)) ?: false } private val configFile = File("trade_config.json") private val gson: Gson = GsonBuilder().setPrettyPrinting().create() /** * JSON 파일로부터 설정을 불러옵니다. * 파일이 없으면 기본 설정 객체를 생성하고 저장한 뒤 반환합니다. */ fun loadTradeConfig(): TradeConfig { return if (configFile.exists()) { try { val jsonString = configFile.readText() gson.fromJson(jsonString, TradeConfig::class.java) } catch (e: Exception) { println("설정 로드 실패: ${e.message}") TradeConfig() // 오류 발생 시 기본값 반환 } } else { // 파일이 없으면 새로 만들어서 저장 val newConfig = TradeConfig() saveTradeConfig() newConfig } } /** * TradeConfig 객체를 JSON 파일로 저장합니다. */ fun saveTradeConfig() { try { val jsonString = gson.toJson(tradeConfig) configFile.writeText(jsonString) println("설정이 성공적으로 저장되었습니다. $jsonString") } catch (e: Exception) { println("설정 저장 실패: ${e.message}") } } fun startTime() : LocalTime { return getStartTimeAsLocalTime(tradeConfig.auto_start_time) } fun endTime() : LocalTime { return getStartTimeAsLocalTime(tradeConfig.auto_end_time) } fun startBuyTime() : LocalTime { return getStartTimeAsLocalTime(tradeConfig.start_buy_time) } fun endBuyTime() : LocalTime { return getStartTimeAsLocalTime(tradeConfig.end_buy_time) } fun getStartTimeAsLocalTime(config: String): java.time.LocalTime { return try { java.time.LocalTime.parse(config) } catch (e: Exception) { java.time.LocalTime.of(9, 0) // 파싱 실패 시 기본값 } } // 2. 오늘 해당 시간까지의 밀리초(Long) 계산 fun getTimeMillis(timeStr: String): Long { val parts = timeStr.split(":") if (parts.size != 2) return 0L val hour = parts[0].toLongOrNull() ?: 0L val min = parts[1].toLongOrNull() ?: 0L return (hour * 3600 + min * 60) * 1000L } fun isAvailBuyTime(now: LocalTime) : Boolean { return now.isBefore(endBuyTime()) && now.isAfter(startBuyTime()) } }