347 lines
14 KiB
Kotlin
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())
|
|
}
|
|
} |