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
llama-3-Korean-Bllossom-8B-Q4_K_M.gguf
https://gofile.me/6z2kw/ypnQYrMl3 분석 모델
뉴스용 임베디드 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

View File

@ -16,34 +16,96 @@ import java.io.File
import java.util.zip.ZipInputStream
import javax.xml.parsers.DocumentBuilderFactory
import kotlinx.serialization.encodeToString // 추가 필요
@Serializable
data class StockItem(
val code: 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 {
val file = File(filePath)
if (!file.exists()) {
println("⚠️ 파일을 찾을 수 없습니다: ${file.absolutePath}")
return emptyList()
}
// 1. JSON 파일 읽기 및 역직렬화
val rawJson = file.readText()
val stockItems = json.decodeFromString<List<StockItem>>(rawJson)
// 2. Pair(코드, 이름) 튜플 리스트로 변환
stockItems.map { it.code to it.name }
} catch (e: Exception) {
println("❌ 유니버스 로드 실패: ${e.message}")
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 {
val raw = callLlamaWithSchema(prompt)
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 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"))
// 08:30 ~ 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() // 모니터 켜기
statusMessage = "⏰ 자동 실행 시간(08:30)입니다. 시스템을 가동합니다."
authenticateAndStart()

View File

@ -2,6 +2,7 @@ package ui
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
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.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Search
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.focus.onFocusChanged
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.onExternalDrag
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
@ -26,7 +32,10 @@ import androidx.compose.ui.unit.sp
import kotlinx.coroutines.launch
import model.ConfigIndex
import model.KisSession
import network.StockUniverseLoader
import service.AutoTradingManager
import java.io.File
import java.net.URI
@OptIn(ExperimentalMaterialApi::class)
@Composable
@ -140,6 +149,19 @@ fun TradingDecisionLog() {
}
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를 사용하여 최신 로그가 위로 오게 표시
LazyColumn(
@ -426,4 +448,88 @@ fun getRemaining(original: String, common: String): String {
if (common.isEmpty()) return original
// 가장 처음 발견되는 공통 문자열을 한 번만 제거
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