2026-04-02 14:05:14 +09:00
|
|
|
import ConfigTable.grade_1_allocationrate
|
2026-02-19 15:47:31 +09:00
|
|
|
import ConfigTable.grade_1_buy
|
|
|
|
|
import ConfigTable.grade_1_profit
|
2026-04-02 14:05:14 +09:00
|
|
|
import ConfigTable.grade_2_allocationrate
|
2026-02-19 15:47:31 +09:00
|
|
|
import ConfigTable.grade_2_buy
|
|
|
|
|
import ConfigTable.grade_2_profit
|
2026-04-02 14:05:14 +09:00
|
|
|
import ConfigTable.grade_3_allocationrate
|
2026-02-19 15:47:31 +09:00
|
|
|
import ConfigTable.grade_3_buy
|
|
|
|
|
import ConfigTable.grade_3_profit
|
2026-04-02 14:05:14 +09:00
|
|
|
import ConfigTable.grade_4_allocationrate
|
2026-02-19 15:47:31 +09:00
|
|
|
import ConfigTable.grade_4_buy
|
|
|
|
|
import ConfigTable.grade_4_profit
|
2026-04-02 14:05:14 +09:00
|
|
|
import ConfigTable.grade_5_allocationrate
|
2026-02-19 15:47:31 +09:00
|
|
|
import ConfigTable.grade_5_buy
|
|
|
|
|
import ConfigTable.grade_5_profit
|
2026-04-02 15:22:38 +09:00
|
|
|
import ConfigTable.loss_max_money
|
|
|
|
|
import ConfigTable.loss_maxrate
|
|
|
|
|
import ConfigTable.loss_minrate
|
2026-02-19 15:47:31 +09:00
|
|
|
import ConfigTable.max_count
|
2026-04-02 15:22:38 +09:00
|
|
|
import ConfigTable.stop_Loss
|
|
|
|
|
import ConfigTable.take_profit
|
2026-03-27 17:54:21 +09:00
|
|
|
import Defines.DETAILLOG
|
|
|
|
|
import Defines.EMBEDDING_PORT
|
|
|
|
|
import Defines.LLM_PORT
|
2026-01-10 18:16:50 +09:00
|
|
|
import androidx.compose.foundation.layout.*
|
|
|
|
|
import androidx.compose.material.*
|
|
|
|
|
import androidx.compose.runtime.*
|
|
|
|
|
import androidx.compose.ui.Alignment
|
|
|
|
|
import androidx.compose.ui.Modifier
|
2026-03-20 17:55:27 +09:00
|
|
|
import androidx.compose.ui.res.painterResource
|
|
|
|
|
import androidx.compose.ui.window.Tray
|
2026-01-10 18:16:50 +09:00
|
|
|
import androidx.compose.ui.window.Window
|
2026-01-22 16:21:18 +09:00
|
|
|
import androidx.compose.ui.window.WindowPlacement
|
2026-01-10 18:16:50 +09:00
|
|
|
import androidx.compose.ui.window.application
|
2026-03-20 17:55:27 +09:00
|
|
|
import androidx.compose.ui.window.rememberTrayState
|
2026-01-22 16:21:18 +09:00
|
|
|
import androidx.compose.ui.window.rememberWindowState
|
|
|
|
|
import io.ktor.client.HttpClient
|
|
|
|
|
import io.ktor.client.engine.cio.CIO
|
|
|
|
|
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
|
|
|
|
import io.ktor.client.plugins.logging.DEFAULT
|
|
|
|
|
import io.ktor.client.plugins.logging.LogLevel
|
|
|
|
|
import io.ktor.client.plugins.logging.Logger
|
|
|
|
|
import io.ktor.client.plugins.logging.Logging
|
|
|
|
|
import io.ktor.serialization.kotlinx.json.json
|
2026-02-06 17:53:17 +09:00
|
|
|
import kotlinx.coroutines.CoroutineScope
|
|
|
|
|
import kotlinx.coroutines.Dispatchers
|
|
|
|
|
import kotlinx.coroutines.delay
|
|
|
|
|
import kotlinx.coroutines.launch
|
2026-01-22 16:21:18 +09:00
|
|
|
import kotlinx.serialization.json.Json
|
2026-01-10 18:16:50 +09:00
|
|
|
import org.jetbrains.exposed.sql.*
|
|
|
|
|
import org.jetbrains.exposed.sql.transactions.transaction
|
|
|
|
|
import model.AppConfig
|
2026-01-13 16:04:25 +09:00
|
|
|
import model.KisSession
|
2026-01-22 16:21:18 +09:00
|
|
|
import network.DartCodeManager
|
2026-02-06 17:53:17 +09:00
|
|
|
import network.KisTradeService
|
2026-03-13 17:34:48 +09:00
|
|
|
import network.KisWebSocketManager
|
2026-01-23 17:05:09 +09:00
|
|
|
import service.LlamaServerManager
|
2026-01-21 18:59:55 +09:00
|
|
|
import network.NewsService
|
2026-01-10 18:16:50 +09:00
|
|
|
import org.jetbrains.exposed.sql.selectAll
|
2026-02-06 17:53:17 +09:00
|
|
|
import service.AutoTradingManager
|
2026-03-13 10:41:10 +09:00
|
|
|
import service.AutoTradingManager.isSystemCleanedUpToday
|
2026-02-04 14:52:09 +09:00
|
|
|
import service.SystemSleepPreventer
|
2026-02-06 17:53:17 +09:00
|
|
|
import service.TradingDecisionCallback
|
2026-01-10 18:16:50 +09:00
|
|
|
import ui.SettingsScreen
|
2026-03-13 16:37:53 +09:00
|
|
|
import ui.TradingDecisionLog
|
2026-03-27 18:03:06 +09:00
|
|
|
import util.PortFinder
|
2026-01-10 18:16:50 +09:00
|
|
|
|
|
|
|
|
// 화면 상태 정의
|
2026-03-13 16:37:53 +09:00
|
|
|
enum class AppScreen { Settings, Dashboard, TradingDecision }
|
2026-01-10 18:16:50 +09:00
|
|
|
|
2026-02-19 16:55:59 +09:00
|
|
|
|
|
|
|
|
fun getLlamaBinPath(): String {
|
|
|
|
|
val os = System.getProperty("os.name").lowercase()
|
|
|
|
|
val arch = System.getProperty("os.arch").lowercase()
|
|
|
|
|
val basePath = "./src/main/resources/bin"
|
|
|
|
|
|
|
|
|
|
return when {
|
|
|
|
|
// Apple Silicon (M1/M2/M3)
|
|
|
|
|
os.contains("mac") && (arch.contains("aarch64") || arch.contains("arm64")) -> {
|
|
|
|
|
"$basePath/mac-arm64/llama-server"
|
|
|
|
|
}
|
|
|
|
|
// Intel Mac (2017)
|
|
|
|
|
os.contains("mac") -> {
|
|
|
|
|
"$basePath/mac-x64/llama-server"
|
|
|
|
|
}
|
|
|
|
|
// Windows NUC
|
2026-04-07 18:10:10 +09:00
|
|
|
os.contains("win-n") -> {
|
2026-04-06 15:07:14 +09:00
|
|
|
"$basePath/win-x64/llama-server.exe"
|
2026-02-19 16:55:59 +09:00
|
|
|
}
|
|
|
|
|
else -> "$basePath/llama-server"
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-20 17:55:27 +09:00
|
|
|
|
2026-03-27 17:45:51 +09:00
|
|
|
fun initLogger(isDebug: Boolean) {
|
|
|
|
|
val root = org.slf4j.LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME) as ch.qos.logback.classic.Logger
|
|
|
|
|
|
|
|
|
|
if (isDebug) {
|
|
|
|
|
root.level = ch.qos.logback.classic.Level.DEBUG
|
|
|
|
|
println("🛠️ 디버그 모드: 상세 로그를 출력합니다.")
|
|
|
|
|
} else {
|
|
|
|
|
root.level = ch.qos.logback.classic.Level.ERROR
|
|
|
|
|
// 특정 라이브러리(Exposed)만 콕 집어서 끄기
|
|
|
|
|
(org.slf4j.LoggerFactory.getLogger("Exposed") as ch.qos.logback.classic.Logger).level = ch.qos.logback.classic.Level.OFF
|
|
|
|
|
println("🤫 운영 모드: 에러 로그만 출력합니다.")
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-27 18:03:06 +09:00
|
|
|
private var isAppStarted = false
|
2026-03-27 17:45:51 +09:00
|
|
|
fun main() = application {
|
2026-03-27 18:03:06 +09:00
|
|
|
if (!isAppStarted) {
|
|
|
|
|
initLogger(DETAILLOG)
|
2026-03-30 17:00:39 +09:00
|
|
|
try {
|
|
|
|
|
val (port1, port2) = PortFinder.findAvailablePortPair(18080, false)
|
|
|
|
|
if (port1 > 18000 && port2 > port1) {
|
|
|
|
|
println("🚀 AI 서버용 포트 할당 완료: 메인($port1), 서브($port2)")
|
|
|
|
|
LLM_PORT = port1
|
|
|
|
|
EMBEDDING_PORT = port2
|
2026-03-30 16:01:38 +09:00
|
|
|
|
2026-03-30 17:00:39 +09:00
|
|
|
isAppStarted = true
|
|
|
|
|
} else {
|
|
|
|
|
println("🚀 AI 서버용 포트 할당 실패")
|
|
|
|
|
exitApplication()
|
|
|
|
|
}
|
|
|
|
|
} catch (e: Exception) {
|
|
|
|
|
println("🚀 AI 서버용 포트 할당 에러 : ${e.message}")
|
|
|
|
|
e.printStackTrace()
|
|
|
|
|
exitApplication()
|
|
|
|
|
}
|
2026-03-27 18:03:06 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
val trayState = rememberTrayState()
|
|
|
|
|
var isWindowOpen by remember { mutableStateOf(false) } // 창의 표시 상태 관리
|
2026-03-20 17:55:27 +09:00
|
|
|
LaunchedEffect(Unit) {
|
|
|
|
|
SystemSleepPreventer.start()
|
|
|
|
|
AutoTradingManager.startBackgroundScheduler()
|
|
|
|
|
}
|
|
|
|
|
LaunchedEffect(AutoTradingManager.shouldShowFullWindow) {
|
|
|
|
|
if (AutoTradingManager.shouldShowFullWindow) {
|
|
|
|
|
isWindowOpen = true
|
|
|
|
|
// 신호를 처리했으므로 다시 초기화 (트레이에서 수동으로 닫았을 때 다시 뜰 수 있게 함)
|
|
|
|
|
AutoTradingManager.shouldShowFullWindow = false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// 트레이 아이콘 설정
|
|
|
|
|
Tray(
|
|
|
|
|
state = trayState,
|
|
|
|
|
icon = painterResource("neko.png"), // resources 폴더에 아이콘 파일 필요
|
|
|
|
|
tooltip = "KIS AI 자동매매",
|
|
|
|
|
onAction = { isWindowOpen = true }, // 트레이 아이콘 더블클릭 시 창 열기
|
|
|
|
|
menu = {
|
|
|
|
|
Item("앱 열기", onClick = { isWindowOpen = true })
|
|
|
|
|
Separator()
|
|
|
|
|
Item("종료", onClick = {
|
|
|
|
|
// 종료 전 리소스 정리 호출
|
|
|
|
|
AutoTradingManager.stopDiscovery()
|
|
|
|
|
exitApplication()
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
)
|
2026-02-06 17:53:17 +09:00
|
|
|
|
2026-01-13 16:04:25 +09:00
|
|
|
// 앱 실행 시 필요한 바이너리 경로 (실행 파일 위치)
|
2026-02-19 16:55:59 +09:00
|
|
|
val binPath = getLlamaBinPath()
|
2026-01-22 16:21:18 +09:00
|
|
|
val windowState = rememberWindowState(
|
2026-03-17 10:50:13 +09:00
|
|
|
placement = WindowPlacement.Floating
|
2026-01-22 16:21:18 +09:00
|
|
|
)
|
2026-03-20 17:55:27 +09:00
|
|
|
if (isWindowOpen) {
|
|
|
|
|
|
2026-03-26 11:22:07 +09:00
|
|
|
Window(onCloseRequest = { isWindowOpen = false }, title = "KIS AI 자동매매", state = windowState) {
|
|
|
|
|
var currentScreen by remember { mutableStateOf(AppScreen.Settings) }
|
|
|
|
|
var isLoaded by remember { mutableStateOf(false) }
|
|
|
|
|
// 1. 앱 시작 시 DB에서 마지막 설정 로드 (KisSession에 주입)
|
|
|
|
|
LaunchedEffect(Unit) {
|
|
|
|
|
DatabaseFactory.init()
|
|
|
|
|
transaction {
|
|
|
|
|
ConfigTable.selectAll().lastOrNull()?.let {
|
|
|
|
|
KisSession.config = AppConfig(
|
|
|
|
|
realAppKey = it[ConfigTable.realAppKey],
|
|
|
|
|
realSecretKey = it[ConfigTable.realSecretKey],
|
|
|
|
|
realAccountNo = it[ConfigTable.realAccountNo],
|
|
|
|
|
vtsAppKey = it[ConfigTable.vtsAppKey],
|
|
|
|
|
vtsSecretKey = it[ConfigTable.vtsSecretKey],
|
|
|
|
|
vtsAccountNo = it[ConfigTable.vtsAccountNo],
|
|
|
|
|
nAppKey = it[ConfigTable.nAppKey],
|
|
|
|
|
nSecretKey = it[ConfigTable.nSecretKey],
|
|
|
|
|
dAppKey = it[ConfigTable.dAppKey],
|
|
|
|
|
isSimulation = it[ConfigTable.isSimulation],
|
|
|
|
|
htsId = it[ConfigTable.htsId],
|
|
|
|
|
modelPath = it[ConfigTable.modelPath],
|
|
|
|
|
embedModelPath = it[ConfigTable.embedModelPath],
|
|
|
|
|
FEES_AND_TAXRATE = it[ConfigTable.fees_and_taxrate],
|
|
|
|
|
MINIMUM_NET_PROFIT = it[ConfigTable.minimum_net_profit],
|
|
|
|
|
BUY_WEIGHT = it[ConfigTable.buy_weight],
|
|
|
|
|
MAX_BUDGET = it[ConfigTable.max_budget],
|
|
|
|
|
MAX_PRICE = it[ConfigTable.max_price],
|
|
|
|
|
MIN_PRICE = it[ConfigTable.min_price],
|
|
|
|
|
MIN_PURCHASE_SCORE = it[ConfigTable.min_purchase_score],
|
|
|
|
|
SELL_PROFIT = it[ConfigTable.sell_profit],
|
|
|
|
|
GRADE_5_BUY = it[grade_5_buy],
|
|
|
|
|
GRADE_5_PROFIT = it[grade_5_profit],
|
|
|
|
|
GRADE_4_BUY = it[grade_4_buy],
|
|
|
|
|
GRADE_4_PROFIT = it[grade_4_profit],
|
|
|
|
|
GRADE_3_BUY = it[grade_3_buy],
|
|
|
|
|
GRADE_3_PROFIT = it[grade_3_profit],
|
|
|
|
|
GRADE_2_BUY = it[grade_2_buy],
|
|
|
|
|
GRADE_2_PROFIT = it[grade_2_profit],
|
|
|
|
|
GRADE_1_BUY = it[grade_1_buy],
|
|
|
|
|
GRADE_1_PROFIT = it[grade_1_profit],
|
2026-04-02 14:05:14 +09:00
|
|
|
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],
|
2026-04-02 15:22:38 +09:00
|
|
|
stop_Loss = it[stop_Loss],
|
|
|
|
|
take_profit = it[take_profit],
|
|
|
|
|
loss_max = it[loss_maxrate],
|
|
|
|
|
loss_min = it[loss_minrate],
|
|
|
|
|
loss_money = it[loss_max_money],
|
2026-03-26 11:22:07 +09:00
|
|
|
MAX_COUNT = it[max_count],
|
|
|
|
|
)
|
|
|
|
|
}
|
2026-01-10 18:16:50 +09:00
|
|
|
}
|
2026-01-19 17:09:37 +09:00
|
|
|
|
2026-03-26 11:22:07 +09:00
|
|
|
isLoaded = true
|
2026-03-13 16:37:53 +09:00
|
|
|
|
2026-03-26 11:22:07 +09:00
|
|
|
}
|
2026-01-13 16:04:25 +09:00
|
|
|
|
2026-03-26 11:22:07 +09:00
|
|
|
if (!isLoaded) {
|
|
|
|
|
// 로딩 중 표시
|
|
|
|
|
CircularProgressIndicator()
|
|
|
|
|
} else {
|
|
|
|
|
when (currentScreen) {
|
|
|
|
|
AppScreen.Settings -> {
|
|
|
|
|
AutoTradingManager.onMarketClosed = {
|
|
|
|
|
println("프로그램 초기화 실행됨")
|
|
|
|
|
currentScreen = AppScreen.Settings
|
|
|
|
|
isWindowOpen = false
|
2026-01-10 18:16:50 +09:00
|
|
|
}
|
2026-03-26 11:22:07 +09:00
|
|
|
SettingsScreen(
|
|
|
|
|
onAuthSuccess = {
|
|
|
|
|
|
|
|
|
|
// 2. 설정 및 인증 완료 시점의 처리
|
|
|
|
|
val config = KisSession.config
|
|
|
|
|
AutoTradingManager.isSystemReadyToday = true
|
|
|
|
|
AutoTradingManager.isSystemCleanedUpToday = false
|
|
|
|
|
|
|
|
|
|
CoroutineScope(Dispatchers.Default).launch {
|
|
|
|
|
AutoTradingManager.startAutoDiscoveryLoop()
|
|
|
|
|
KisWebSocketManager.onExecutionReceived = AutoTradingManager.onExecutionReceived
|
|
|
|
|
KisWebSocketManager.connect()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (config.modelPath.isNotEmpty()) {
|
2026-03-27 17:54:21 +09:00
|
|
|
LlamaServerManager.startServer(binPath, config.modelPath,port = LLM_PORT)
|
2026-03-26 11:22:07 +09:00
|
|
|
}
|
|
|
|
|
if (config.embedModelPath.isNotEmpty()) {
|
2026-03-27 17:54:21 +09:00
|
|
|
LlamaServerManager.startServer(binPath, config.embedModelPath, port = EMBEDDING_PORT)
|
2026-03-26 11:22:07 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 대시보드로 화면 전환
|
|
|
|
|
currentScreen = AppScreen.TradingDecision
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
AppScreen.Dashboard -> {
|
2026-04-07 17:32:21 +09:00
|
|
|
// DashboardScreen()
|
2026-03-26 11:22:07 +09:00
|
|
|
}
|
|
|
|
|
AppScreen.TradingDecision -> {
|
|
|
|
|
TradingDecisionLog()
|
|
|
|
|
}
|
2026-03-13 16:37:53 +09:00
|
|
|
}
|
2026-01-10 18:16:50 +09:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|