Compare commits

...

2 Commits

Author SHA1 Message Date
d7efc433bd .. 2026-05-06 15:53:55 +09:00
0413fa3e2e .. 2026-05-04 11:19:52 +09:00
9 changed files with 132 additions and 7 deletions

View File

@ -328,6 +328,7 @@ fun main() = application {
// DashboardScreen()
}
AppScreen.TradingDecision -> {
TradingDecisionLog()
}
}

View File

@ -1,10 +1,16 @@
import androidx.compose.runtime.mutableStateListOf
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import kotlinx.serialization.Serializable
import model.AppConfig
import model.TradingDecision
import network.NewsService
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.javatime.datetime
import org.jetbrains.exposed.sql.transactions.experimental.suspendedTransactionAsync
import org.jetbrains.exposed.sql.transactions.transaction
import report.TradingReportManager
import report.TradingReportService
@ -509,11 +515,21 @@ object TradingLogStore {
decisionLogs.add(
LogEntry(
time = LocalTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss")),
stockName = "${tradingDecision.stockName}[${tradingDecision.currentPrice}][]",
stockName = "${tradingDecision.stockName}[${tradingDecision.currentPrice}]",
decision = decision,
confidence = tradingDecision.confidence,
reason = log
)
).apply {
CoroutineScope(Dispatchers.Default).launch {
println("CALLED sendTelegramMessage -1")
if (decision.contains("WATCH") || ((tradingDecision.investmentGrade?.ordinal
?: 0) < 2)
) {
println("CALLED sendTelegramMessage OK")
NewsService.sendTelegramMessage("${this@apply.decision} ${tradingDecision.stockName}[${tradingDecision.currentPrice}] ${log}")
}
}
}
)
}
}
@ -545,8 +561,14 @@ object TradingLogStore {
decision = "NOTICE",
confidence = 100.0,
reason = log
)
).apply {
CoroutineScope(Dispatchers.Default).launch {
println("CALLED sendTelegramMessage")
NewsService.sendTelegramMessage("${this@apply.decision}$name[$code] ${log}")
}
}
)
}
}

View File

@ -240,6 +240,7 @@ class TradeConfig {
var start_buy_time : String = "08:55"
var end_buy_time : String = "15:10"
var enableOverSea : Boolean = false
var tlg_id : String = ""
}

View File

@ -17,6 +17,8 @@ import java.util.zip.ZipInputStream
import javax.xml.parsers.DocumentBuilderFactory
import kotlinx.serialization.encodeToString // 추가 필요
import java.nio.charset.Charset
@Serializable
data class StockItem(
val code: String,
@ -29,6 +31,25 @@ object StockUniverseLoader {
private val json = Json { ignoreUnknownKeys = true; prettyPrint = true }
private const val DEFAULT_FILE_PATH = "stocks_universe.json"
fun readSafeLines(file: File): List<String> {
val eucKr = Charset.forName("EUC-KR")
val utf8 = Charsets.UTF_8
// 우선 EUC-KR로 읽어봄
val lines = file.readLines(eucKr)
// 첫 줄에서 한글이 깨졌는지 검사 (정규식 활용)
// 한글이 하나도 없고 깨진 특수문자만 있다면 UTF-8로 재시도
val hasKorean = lines.firstOrNull()?.any { it in '\uAC00'..'\uD7A3' } ?: false
return if (hasKorean) {
lines
} else {
println("⚠️ EUC-KR에서 한글 미검출. UTF-8로 재시도합니다.")
file.readLines(utf8)
}
}
fun loadUniverse(filePath: String = DEFAULT_FILE_PATH): List<Pair<String, String>> {
return try {
val file = File(filePath)
@ -50,7 +71,8 @@ object StockUniverseLoader {
try {
val stockItems = items.map { StockItem(it.first, it.second) }
val jsonString = json.encodeToString(stockItems)
File(filePath).writeText(jsonString)
// File(filePath).writeText(jsonString)
File(filePath).writeText(jsonString, Charsets.UTF_8)
println("💾 [System] 유니버스 영구 저장 완료: 총 ${items.size}종목")
} catch (e: Exception) {
println("❌ 유니버스 저장 실패: ${e.message}")
@ -61,7 +83,7 @@ object StockUniverseLoader {
fun parseAndMergeCsv(file: File, targetJsonPath: String = DEFAULT_FILE_PATH): List<Pair<String, String>> {
val newItems = mutableListOf<Pair<String, String>>()
try {
val lines = file.readLines()
val lines = readSafeLines(file)
if (lines.isEmpty()) return loadUniverse(targetJsonPath)
// 헤더 자동 추적

View File

@ -45,7 +45,7 @@ object KisTradeService {
// [수정] 모든 로그(Headers + Body)를 찍도록 설정
install(Logging) {
logger = Logger.DEFAULT
level = LogLevel.NONE
level = LogLevel.ALL
}
}

View File

@ -17,10 +17,14 @@ import io.ktor.client.request.post
import io.ktor.client.request.setBody
import io.ktor.client.statement.HttpResponse
import io.ktor.client.statement.bodyAsText
import io.ktor.http.ContentType
import io.ktor.http.ContentType.Application.Json
import io.ktor.http.HttpHeaders
import io.ktor.http.HttpStatusCode
import io.ktor.http.Parameters
import io.ktor.http.Url
import io.ktor.http.contentType
import io.ktor.network.tls.TLSConfigBuilder
import io.ktor.serialization.kotlinx.json.json
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
@ -29,10 +33,16 @@ import model.KisSession
import model.NaverNewsResponse
import service.SafeScraper
import service.UrlCacheManager
import java.io.BufferedReader
import java.io.InputStreamReader
import java.net.URL
import java.net.URLEncoder
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter
import java.time.temporal.ChronoUnit
import java.util.Locale
import javax.net.ssl.HttpsURLConnection
import javax.net.ssl.SSLContext
import kotlin.Double
object NewsService {
@ -122,4 +132,62 @@ object NewsService {
return ""
}
}
suspend fun sendTelegramMessage(data: String) {
Thread {
try {
var chatId = KisSession.tradeConfig.tlg_id
println("sendTelegramMessage $chatId")
sendViaSystemCurl("https://lunaticbum.kr/tlg/sendToMe.bjx",chatId,data)
} catch (e: Exception) {
e.printStackTrace()
}
}.start()
}
fun sendViaSystemCurl(url : String, chatId: String, message: String) {
try {
// 메시지 내 공백이나 한글이 깨지지 않도록 인코딩 (필수)
val encodedMessage = URLEncoder.encode(message, "UTF-8")
// OS 확인
val isWindows = System.getProperty("os.name").lowercase().contains("win")
val command = if (isWindows) {
// 윈도우용: 큰따옴표 이스케이프에 주의해야 합니다.
val jsonBody = "{\"id\":\"$chatId\",\"message\":\"$encodedMessage\"}"
listOf("cmd", "/c", "curl -s -X POST $url -H \"Content-Type: application/json\" -d \"$jsonBody\"")
} else {
// 맥/리눅스용: 홑따옴표를 사용하여 JSON 구조를 보호합니다.
val jsonBody = "{\"id\":\"$chatId\",\"message\":\"$message\"}"
listOf("curl", "-s", "-X", "POST", url, "-H", "Content-Type: application/json", "-d", jsonBody)
}
val process = ProcessBuilder(command)
.redirectErrorStream(true) // 에러 출력(stderr)을 표준 출력(stdout)으로 합침
.start()
// 프로세스의 출력을 읽어오는 블록
BufferedReader(InputStreamReader(process.inputStream)).use { reader ->
val output = StringBuilder()
var line: String?
while (reader.readLine().also { line = it } != null) {
output.append(line).append("\n")
}
val exitCode = process.waitFor() // 프로세스가 종료될 때까지 대기
println("--- Telegram Curl Log Start ---")
println("Exit Code: $exitCode") // 0이면 성공, 그 외는 curl 에러 코드
println("Response:\n$output")
println("--- Telegram Curl Log End ---")
}
} catch (e: Exception) {
println("시스템 명령어 실행 중 예외 발생: ${e.message}")
e.printStackTrace()
}
}
}

View File

@ -707,7 +707,7 @@ object AutoTradingManager {
println("⏳ [Cycle Timeout] 사이클이 너무 길어져 초기화 후 재시작합니다.")
} catch (e: Exception) {
println("⚠️ [Loop Error] ${e.message}")
delay(1500)
delay(1000)
}
waitForNextCycle(waitTime)
}

View File

@ -45,6 +45,7 @@ import java.awt.Toolkit
import java.awt.datatransfer.DataFlavor
import java.io.File
import androidx.compose.ui.input.key.*
import network.NewsService
fun getPastedPathFromClipboard(): String? {
val clipboard = Toolkit.getDefaultToolkit().systemClipboard
@ -138,6 +139,7 @@ fun SettingsScreen(onAuthSuccess: () -> Unit) {
SystemSleepPreventer.wakeDisplay() // 모니터 켜기
statusMessage = "⏰ 자동 실행 시간(08:30)입니다. 시스템을 가동합니다."
authenticateAndStart()
break // 성공하면 루프 탈출
}
}

View File

@ -42,10 +42,13 @@ import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import model.ConfigIndex
import model.KisSession
import network.KisTradeService
import network.NewsService
import network.StockUniverseLoader
import service.AutoTradingManager
import java.io.File
@ -62,7 +65,13 @@ fun TradingDecisionLog() {
var llmAnalyser by remember { mutableStateOf(AutoTradingManager.llmAnalyser) }
val tradeConfig by remember {
KisSession.tradeConfig = KisSession.loadTradeConfig()
CoroutineScope(Dispatchers.Default).launch {
println("CALLED sendTelegramMessage -1")
val now = java.time.LocalTime.now(java.time.ZoneId.of("Asia/Seoul"))
NewsService.sendTelegramMessage("⏰ 자동 실행 시간(${now.hour}:${now.minute})입니다. 시스템을 가동합니다.")
}
mutableStateOf(KisSession.tradeConfig)
}
LaunchedEffect(AutoTradingManager.llmAnalyser) {
llmAnalyser = AutoTradingManager.llmAnalyser