This commit is contained in:
lunaticbum 2026-04-09 13:17:52 +09:00
parent 8bb099e4f6
commit 95e44b18d0
6 changed files with 11832 additions and 9160 deletions

View File

@ -2,12 +2,12 @@
https://huggingface.co/MLP-KTLim/llama-3-Korean-Bllossom-8B-gguf-Q4_K_M https://huggingface.co/MLP-KTLim/llama-3-Korean-Bllossom-8B-gguf-Q4_K_M
llama-3-Korean-Bllossom-8B-Q4_K_M.gguf https://gofile.me/6z2kw/ypnQYrMl3 분석 모델
뉴스용 임베디드 llm 뉴스용 임베디드 llm
https://huggingface.co/Jackrong/Qwen3.5-27B-Claude-4.6-Opus-Reasoning-Distilled https://gofile.me/6z2kw/xMbFVg1f1 임베딩 모델
bge-m3-q4_k_m.gguf bge-m3-q4_k_m.gguf

View File

@ -16,34 +16,96 @@ import java.io.File
import java.util.zip.ZipInputStream import java.util.zip.ZipInputStream
import javax.xml.parsers.DocumentBuilderFactory import javax.xml.parsers.DocumentBuilderFactory
import kotlinx.serialization.encodeToString // 추가 필요
@Serializable @Serializable
data class StockItem( data class StockItem(
val code: String, val code: String,
val name: String val name: String
){} ){}
object StockUniverseLoader {
private val json = Json { ignoreUnknownKeys = true }
fun loadUniverse(filePath: String = "stocks_universe.json"): List<Pair<String, String>> { object StockUniverseLoader {
// prettyPrint = true 를 주면 JSON 파일이 한 줄로 안 뭉치고 예쁘게 저장됩니다.
private val json = Json { ignoreUnknownKeys = true; prettyPrint = true }
private const val DEFAULT_FILE_PATH = "stocks_universe.json"
fun loadUniverse(filePath: String = DEFAULT_FILE_PATH): List<Pair<String, String>> {
return try { return try {
val file = File(filePath) val file = File(filePath)
if (!file.exists()) { if (!file.exists()) {
println("⚠️ 파일을 찾을 수 없습니다: ${file.absolutePath}") println("⚠️ 파일을 찾을 수 없습니다: ${file.absolutePath}")
return emptyList() return emptyList()
} }
// 1. JSON 파일 읽기 및 역직렬화
val rawJson = file.readText() val rawJson = file.readText()
val stockItems = json.decodeFromString<List<StockItem>>(rawJson) val stockItems = json.decodeFromString<List<StockItem>>(rawJson)
// 2. Pair(코드, 이름) 튜플 리스트로 변환
stockItems.map { it.code to it.name } stockItems.map { it.code to it.name }
} catch (e: Exception) { } catch (e: Exception) {
println("❌ 유니버스 로드 실패: ${e.message}") println("❌ 유니버스 로드 실패: ${e.message}")
emptyList() emptyList()
} }
} }
// 💡 [신규] JSON 파일로 덮어쓰기 저장
fun saveUniverse(items: List<Pair<String, String>>, filePath: String = DEFAULT_FILE_PATH) {
try {
val stockItems = items.map { StockItem(it.first, it.second) }
val jsonString = json.encodeToString(stockItems)
File(filePath).writeText(jsonString)
println("💾 [System] 유니버스 영구 저장 완료: 총 ${items.size}종목")
} catch (e: Exception) {
println("❌ 유니버스 저장 실패: ${e.message}")
}
}
// 💡 [신규] CSV 파일을 받아 파싱, 중복 제거, 저장까지 원스톱으로 처리
fun parseAndMergeCsv(file: File, targetJsonPath: String = DEFAULT_FILE_PATH): List<Pair<String, String>> {
val newItems = mutableListOf<Pair<String, String>>()
try {
val lines = file.readLines()
if (lines.isEmpty()) return loadUniverse(targetJsonPath)
// 헤더 자동 추적
val headers = lines[0].split(",").map { it.replace("\"", "").trim() }
val codeIndex = headers.indexOfFirst { it.contains("종목코드") || it.contains("코드") }
val nameIndex = headers.indexOfFirst { it.contains("종목명") || it.contains("이름") }
val finalCodeIdx = if (codeIndex != -1) codeIndex else 0
val finalNameIdx = if (nameIndex != -1) nameIndex else 1
for (i in 1 until lines.size) {
val line = lines[i]
if (line.isBlank()) continue
val parts = line.split(",").map { it.replace("\"", "").trim() }
if (parts.size > maxOf(finalCodeIdx, finalNameIdx)) {
// 엑셀이 날려먹은 앞자리 '0' 복원 (6자리 맞춤)
val rawCode = parts[finalCodeIdx].replace(Regex("[^0-9]"), "")
val code = rawCode.padStart(6, '0')
val name = parts[finalNameIdx]
if (code.length == 6) {
newItems.add(code to name)
}
}
}
// 1. 기존 데이터 불러오기
val existing = loadUniverse(targetJsonPath)
// 2. 종목코드(it.first) 기준으로 완벽하게 중복 제거 병합
val mergedList = (existing + newItems).distinctBy { it.first }
// 3. 파일에 덮어쓰기 (영구 저장)
saveUniverse(mergedList, targetJsonPath)
println("✅ CSV 병합 성공! (신규 ${mergedList.size - existing.size}건 추가됨)")
return mergedList
} catch (e: Exception) {
println("❌ CSV 파싱 및 병합 실패: ${e.message}")
return loadUniverse(targetJsonPath) // 실패 시 기존 데이터라도 반환
}
}
} }

View File

@ -618,7 +618,19 @@ object RagService {
return try { return try {
val raw = callLlamaWithSchema(prompt) val raw = callLlamaWithSchema(prompt)
println("getAiNewsScore $raw") println("getAiNewsScore $raw")
val json = Json { ignoreUnknownKeys = true }.parseToJsonElement(raw).jsonObject // 💡 [핵심 해결책] 마크다운 블록 및 앞뒤 쓸데없는 텍스트 완벽 차단
val startIndex = raw.indexOf("{")
val endIndex = raw.lastIndexOf("}")
// '{' 부터 '}' 까지만 정확하게 잘라냄
val sanitizedRaw = if (startIndex != -1 && endIndex != -1) {
raw.substring(startIndex, endIndex + 1)
} else {
raw // 중괄호를 못 찾았을 경우 대비 (기본 fallback)
}
// 정제된 문자열로 JSON 파싱
val json = Json { ignoreUnknownKeys = true }.parseToJsonElement(sanitizedRaw).jsonObject
val score = json["score"]?.jsonPrimitive?.double ?: 50.0 val score = json["score"]?.jsonPrimitive?.double ?: 50.0
val reason = json["reason"]?.jsonPrimitive?.content ?: "뉴스 분석 완료" val reason = json["reason"]?.jsonPrimitive?.content ?: "뉴스 분석 완료"

View File

@ -89,7 +89,7 @@ fun SettingsScreen(onAuthSuccess: () -> Unit) {
val now = java.time.LocalTime.now(java.time.ZoneId.of("Asia/Seoul")) val now = java.time.LocalTime.now(java.time.ZoneId.of("Asia/Seoul"))
// 08:30 ~ 15:30 사이이고, 키값이 최소한 하나라도 존재할 때 자동 실행 // 08:30 ~ 15:30 사이이고, 키값이 최소한 하나라도 존재할 때 자동 실행
if (now.isAfter(java.time.LocalTime.of(8, 30)) && now.isBefore(java.time.LocalTime.of(15, 30))) { if (now.isAfter(java.time.LocalTime.of(8, 30)) && now.isBefore(java.time.LocalTime.of(15, 30))) {
if (config.realAppKey.isNotEmpty() || config.vtsAppKey.isNotEmpty()) { if (config.realAppKey.isNotEmpty() && config.vtsAppKey.isNotEmpty() && config.embedModelPath.isNotEmpty() && config.modelPath.isNotEmpty()) {
SystemSleepPreventer.wakeDisplay() // 모니터 켜기 SystemSleepPreventer.wakeDisplay() // 모니터 켜기
statusMessage = "⏰ 자동 실행 시간(08:30)입니다. 시스템을 가동합니다." statusMessage = "⏰ 자동 실행 시간(08:30)입니다. 시스템을 가동합니다."
authenticateAndStart() authenticateAndStart()

View File

@ -2,6 +2,7 @@ package ui
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.GridCells
@ -9,15 +10,20 @@ import androidx.compose.foundation.lazy.grid.GridItemSpan
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Search import androidx.compose.material.icons.filled.Search
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.DragData
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.onExternalDrag
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.KeyboardType
@ -26,7 +32,10 @@ import androidx.compose.ui.unit.sp
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import model.ConfigIndex import model.ConfigIndex
import model.KisSession import model.KisSession
import network.StockUniverseLoader
import service.AutoTradingManager import service.AutoTradingManager
import java.io.File
import java.net.URI
@OptIn(ExperimentalMaterialApi::class) @OptIn(ExperimentalMaterialApi::class)
@Composable @Composable
@ -140,6 +149,19 @@ fun TradingDecisionLog() {
} }
Divider(Modifier.padding(bottom = 8.dp)) Divider(Modifier.padding(bottom = 8.dp))
CsvDropZone(
onUniverseUpdated = { updatedList ->
// UI 갱신 (필요한 경우)
// currentUniverse = updatedList
// 💡 봇의 실제 작업 큐인 loadedTops 에도 갱신된 데이터를 덮어씌워 줌
AutoTradingManager.loadedTops.clear()
AutoTradingManager.loadedTops.addAll(updatedList)
AutoTradingManager.loadedTops.shuffle() // 섞어주면 편향 분석 방지!
}
)
Spacer(modifier = Modifier.height(16.dp))
// [수정] filteredLogs를 사용하여 최신 로그가 위로 오게 표시 // [수정] filteredLogs를 사용하여 최신 로그가 위로 오게 표시
LazyColumn( LazyColumn(
@ -427,3 +449,87 @@ fun getRemaining(original: String, common: String): String {
// 가장 처음 발견되는 공통 문자열을 한 번만 제거 // 가장 처음 발견되는 공통 문자열을 한 번만 제거
return original.replaceFirst(common, "").trim() return original.replaceFirst(common, "").trim()
} }
fun parseSmartCsv(file: File): List<Pair<String, String>> {
val result = mutableListOf<Pair<String, String>>()
try {
val lines = file.readLines()
if (lines.isEmpty()) return result
// 1. 헤더 분석하여 열(Column) 인덱스 자동 추적
val headers = lines[0].split(",").map { it.replace("\"", "").trim() }
val codeIndex = headers.indexOfFirst { it.contains("종목코드") }
val nameIndex = headers.indexOfFirst { it.contains("종목명") }
// 헤더를 못 찾았다면 기본값 (0번: 코드, 1번: 이름)으로 폴백
val finalCodeIdx = if (codeIndex != -1) codeIndex else 0
val finalNameIdx = if (nameIndex != -1) nameIndex else 1
// 2. 데이터 추출
for (i in 1 until lines.size) {
val line = lines[i]
if (line.isBlank()) continue
val parts = line.split(",").map { it.replace("\"", "").trim() }
if (parts.size > maxOf(finalCodeIdx, finalNameIdx)) {
val code = parts[finalCodeIdx]
val name = parts[finalNameIdx]
// 6자리 숫자로 된 정상적인 종목코드인지 검증
if (code.length == 6 && code.all { it.isDigit() }) {
result.add(code to name)
}
}
}
} catch (e: Exception) {
println("❌ CSV 파싱 에러: ${e.message}")
}
return result
}
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun CsvDropZone(
onUniverseUpdated: (List<Pair<String, String>>) -> Unit
) {
var isDragging by remember { mutableStateOf(false) }
Box(
modifier = Modifier
.fillMaxWidth()
.height(40.dp)
.background(if (isDragging) Color(0xFFE3F2FD) else Color(0xFFFAFAFA))
.border(
width = 1.dp,
color = if (isDragging) Color.Blue else Color.LightGray,
shape = RoundedCornerShape(8.dp)
)
.onExternalDrag(
onDragStart = { isDragging = true },
onDragExit = { isDragging = false },
onDrop = { state ->
isDragging = false
val dragData = state.dragData
if (dragData is DragData.FilesList) {
val fileUris = dragData.readFiles()
fileUris.firstOrNull { it.endsWith(".csv", ignoreCase = true) }?.let { uri ->
// 💡 여기서 깔끔하게 StockUniverseLoader 에 처리를 위임!
val file = File(URI(uri))
val updatedUniverse = StockUniverseLoader.parseAndMergeCsv(file)
onUniverseUpdated(updatedUniverse)
}
}
}
),
contentAlignment = Alignment.Center
) {
Text(
text = if (isDragging) "📥 파일을 놓아서 업데이트!" else "📁 [CSV 추가] 파일을 드래그하여 유니버스 자동 병합",
color = if (isDragging) Color.Blue else Color.Gray,
fontSize = 12.sp,
fontWeight = FontWeight.SemiBold
)
}
}

File diff suppressed because it is too large Load Diff