Compare commits
2 Commits
23d505eabe
...
d7efc433bd
| Author | SHA1 | Date | |
|---|---|---|---|
| d7efc433bd | |||
| 0413fa3e2e |
@ -328,6 +328,7 @@ fun main() = application {
|
|||||||
// DashboardScreen()
|
// DashboardScreen()
|
||||||
}
|
}
|
||||||
AppScreen.TradingDecision -> {
|
AppScreen.TradingDecision -> {
|
||||||
|
|
||||||
TradingDecisionLog()
|
TradingDecisionLog()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,16 @@
|
|||||||
import androidx.compose.runtime.mutableStateListOf
|
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 kotlinx.serialization.Serializable
|
||||||
import model.AppConfig
|
import model.AppConfig
|
||||||
import model.TradingDecision
|
import model.TradingDecision
|
||||||
|
import network.NewsService
|
||||||
import org.jetbrains.exposed.sql.*
|
import org.jetbrains.exposed.sql.*
|
||||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
||||||
import org.jetbrains.exposed.sql.javatime.datetime
|
import org.jetbrains.exposed.sql.javatime.datetime
|
||||||
|
import org.jetbrains.exposed.sql.transactions.experimental.suspendedTransactionAsync
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
import report.TradingReportManager
|
import report.TradingReportManager
|
||||||
import report.TradingReportService
|
import report.TradingReportService
|
||||||
@ -509,11 +515,21 @@ object TradingLogStore {
|
|||||||
decisionLogs.add(
|
decisionLogs.add(
|
||||||
LogEntry(
|
LogEntry(
|
||||||
time = LocalTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss")),
|
time = LocalTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss")),
|
||||||
stockName = "${tradingDecision.stockName}[${tradingDecision.currentPrice}][]",
|
stockName = "${tradingDecision.stockName}[${tradingDecision.currentPrice}]",
|
||||||
decision = decision,
|
decision = decision,
|
||||||
confidence = tradingDecision.confidence,
|
confidence = tradingDecision.confidence,
|
||||||
reason = log
|
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",
|
decision = "NOTICE",
|
||||||
confidence = 100.0,
|
confidence = 100.0,
|
||||||
reason = log
|
reason = log
|
||||||
)
|
).apply {
|
||||||
|
CoroutineScope(Dispatchers.Default).launch {
|
||||||
|
println("CALLED sendTelegramMessage")
|
||||||
|
NewsService.sendTelegramMessage("${this@apply.decision}$name[$code] ${log}")
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -240,6 +240,7 @@ class TradeConfig {
|
|||||||
var start_buy_time : String = "08:55"
|
var start_buy_time : String = "08:55"
|
||||||
var end_buy_time : String = "15:10"
|
var end_buy_time : String = "15:10"
|
||||||
var enableOverSea : Boolean = false
|
var enableOverSea : Boolean = false
|
||||||
|
var tlg_id : String = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -17,6 +17,8 @@ import java.util.zip.ZipInputStream
|
|||||||
import javax.xml.parsers.DocumentBuilderFactory
|
import javax.xml.parsers.DocumentBuilderFactory
|
||||||
|
|
||||||
import kotlinx.serialization.encodeToString // 추가 필요
|
import kotlinx.serialization.encodeToString // 추가 필요
|
||||||
|
import java.nio.charset.Charset
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class StockItem(
|
data class StockItem(
|
||||||
val code: String,
|
val code: String,
|
||||||
@ -29,6 +31,25 @@ object StockUniverseLoader {
|
|||||||
private val json = Json { ignoreUnknownKeys = true; prettyPrint = true }
|
private val json = Json { ignoreUnknownKeys = true; prettyPrint = true }
|
||||||
private const val DEFAULT_FILE_PATH = "stocks_universe.json"
|
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>> {
|
fun loadUniverse(filePath: String = DEFAULT_FILE_PATH): List<Pair<String, String>> {
|
||||||
return try {
|
return try {
|
||||||
val file = File(filePath)
|
val file = File(filePath)
|
||||||
@ -50,7 +71,8 @@ object StockUniverseLoader {
|
|||||||
try {
|
try {
|
||||||
val stockItems = items.map { StockItem(it.first, it.second) }
|
val stockItems = items.map { StockItem(it.first, it.second) }
|
||||||
val jsonString = json.encodeToString(stockItems)
|
val jsonString = json.encodeToString(stockItems)
|
||||||
File(filePath).writeText(jsonString)
|
// File(filePath).writeText(jsonString)
|
||||||
|
File(filePath).writeText(jsonString, Charsets.UTF_8)
|
||||||
println("💾 [System] 유니버스 영구 저장 완료: 총 ${items.size}종목")
|
println("💾 [System] 유니버스 영구 저장 완료: 총 ${items.size}종목")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
println("❌ 유니버스 저장 실패: ${e.message}")
|
println("❌ 유니버스 저장 실패: ${e.message}")
|
||||||
@ -61,7 +83,7 @@ object StockUniverseLoader {
|
|||||||
fun parseAndMergeCsv(file: File, targetJsonPath: String = DEFAULT_FILE_PATH): List<Pair<String, String>> {
|
fun parseAndMergeCsv(file: File, targetJsonPath: String = DEFAULT_FILE_PATH): List<Pair<String, String>> {
|
||||||
val newItems = mutableListOf<Pair<String, String>>()
|
val newItems = mutableListOf<Pair<String, String>>()
|
||||||
try {
|
try {
|
||||||
val lines = file.readLines()
|
val lines = readSafeLines(file)
|
||||||
if (lines.isEmpty()) return loadUniverse(targetJsonPath)
|
if (lines.isEmpty()) return loadUniverse(targetJsonPath)
|
||||||
|
|
||||||
// 헤더 자동 추적
|
// 헤더 자동 추적
|
||||||
|
|||||||
@ -45,7 +45,7 @@ object KisTradeService {
|
|||||||
// [수정] 모든 로그(Headers + Body)를 찍도록 설정
|
// [수정] 모든 로그(Headers + Body)를 찍도록 설정
|
||||||
install(Logging) {
|
install(Logging) {
|
||||||
logger = Logger.DEFAULT
|
logger = Logger.DEFAULT
|
||||||
level = LogLevel.NONE
|
level = LogLevel.ALL
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -17,10 +17,14 @@ import io.ktor.client.request.post
|
|||||||
import io.ktor.client.request.setBody
|
import io.ktor.client.request.setBody
|
||||||
import io.ktor.client.statement.HttpResponse
|
import io.ktor.client.statement.HttpResponse
|
||||||
import io.ktor.client.statement.bodyAsText
|
import io.ktor.client.statement.bodyAsText
|
||||||
|
import io.ktor.http.ContentType
|
||||||
import io.ktor.http.ContentType.Application.Json
|
import io.ktor.http.ContentType.Application.Json
|
||||||
import io.ktor.http.HttpHeaders
|
import io.ktor.http.HttpHeaders
|
||||||
|
import io.ktor.http.HttpStatusCode
|
||||||
import io.ktor.http.Parameters
|
import io.ktor.http.Parameters
|
||||||
import io.ktor.http.Url
|
import io.ktor.http.Url
|
||||||
|
import io.ktor.http.contentType
|
||||||
|
import io.ktor.network.tls.TLSConfigBuilder
|
||||||
import io.ktor.serialization.kotlinx.json.json
|
import io.ktor.serialization.kotlinx.json.json
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
@ -29,10 +33,16 @@ import model.KisSession
|
|||||||
import model.NaverNewsResponse
|
import model.NaverNewsResponse
|
||||||
import service.SafeScraper
|
import service.SafeScraper
|
||||||
import service.UrlCacheManager
|
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.ZonedDateTime
|
||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
import java.time.temporal.ChronoUnit
|
import java.time.temporal.ChronoUnit
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
import javax.net.ssl.HttpsURLConnection
|
||||||
|
import javax.net.ssl.SSLContext
|
||||||
import kotlin.Double
|
import kotlin.Double
|
||||||
|
|
||||||
object NewsService {
|
object NewsService {
|
||||||
@ -122,4 +132,62 @@ object NewsService {
|
|||||||
return ""
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -707,7 +707,7 @@ object AutoTradingManager {
|
|||||||
println("⏳ [Cycle Timeout] 사이클이 너무 길어져 초기화 후 재시작합니다.")
|
println("⏳ [Cycle Timeout] 사이클이 너무 길어져 초기화 후 재시작합니다.")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
println("⚠️ [Loop Error] ${e.message}")
|
println("⚠️ [Loop Error] ${e.message}")
|
||||||
delay(1500)
|
delay(1000)
|
||||||
}
|
}
|
||||||
waitForNextCycle(waitTime)
|
waitForNextCycle(waitTime)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -45,6 +45,7 @@ import java.awt.Toolkit
|
|||||||
import java.awt.datatransfer.DataFlavor
|
import java.awt.datatransfer.DataFlavor
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import androidx.compose.ui.input.key.*
|
import androidx.compose.ui.input.key.*
|
||||||
|
import network.NewsService
|
||||||
|
|
||||||
fun getPastedPathFromClipboard(): String? {
|
fun getPastedPathFromClipboard(): String? {
|
||||||
val clipboard = Toolkit.getDefaultToolkit().systemClipboard
|
val clipboard = Toolkit.getDefaultToolkit().systemClipboard
|
||||||
@ -138,6 +139,7 @@ fun SettingsScreen(onAuthSuccess: () -> Unit) {
|
|||||||
SystemSleepPreventer.wakeDisplay() // 모니터 켜기
|
SystemSleepPreventer.wakeDisplay() // 모니터 켜기
|
||||||
statusMessage = "⏰ 자동 실행 시간(08:30)입니다. 시스템을 가동합니다."
|
statusMessage = "⏰ 자동 실행 시간(08:30)입니다. 시스템을 가동합니다."
|
||||||
authenticateAndStart()
|
authenticateAndStart()
|
||||||
|
|
||||||
break // 성공하면 루프 탈출
|
break // 성공하면 루프 탈출
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -42,10 +42,13 @@ import androidx.compose.ui.text.input.ImeAction
|
|||||||
import androidx.compose.ui.text.input.KeyboardType
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import model.ConfigIndex
|
import model.ConfigIndex
|
||||||
import model.KisSession
|
import model.KisSession
|
||||||
import network.KisTradeService
|
import network.KisTradeService
|
||||||
|
import network.NewsService
|
||||||
import network.StockUniverseLoader
|
import network.StockUniverseLoader
|
||||||
import service.AutoTradingManager
|
import service.AutoTradingManager
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@ -62,7 +65,13 @@ fun TradingDecisionLog() {
|
|||||||
var llmAnalyser by remember { mutableStateOf(AutoTradingManager.llmAnalyser) }
|
var llmAnalyser by remember { mutableStateOf(AutoTradingManager.llmAnalyser) }
|
||||||
val tradeConfig by remember {
|
val tradeConfig by remember {
|
||||||
KisSession.tradeConfig = KisSession.loadTradeConfig()
|
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)
|
mutableStateOf(KisSession.tradeConfig)
|
||||||
|
|
||||||
}
|
}
|
||||||
LaunchedEffect(AutoTradingManager.llmAnalyser) {
|
LaunchedEffect(AutoTradingManager.llmAnalyser) {
|
||||||
llmAnalyser = AutoTradingManager.llmAnalyser
|
llmAnalyser = AutoTradingManager.llmAnalyser
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user