atrade/src/main/kotlin/model/AppConfig.kt
2026-05-13 11:37:57 +09:00

347 lines
14 KiB
Kotlin

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<ConfigIndex>()
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<String> = listOf("LEVEL_4","LEVEL_5")
var useTagsShare : List<String> = listOf("NOTICE", "WATCH")
var useLogKeywordsShare : List<String> = 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())
}
}