atrade/src/main/kotlin/network/DartCodeManager.kt
2026-05-04 11:19:52 +09:00

255 lines
10 KiB
Kotlin
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package network
import io.ktor.client.*
import io.ktor.client.request.*
import io.ktor.client.request.forms.FormDataContent
import io.ktor.client.statement.*
import io.ktor.http.HttpHeaders
import io.ktor.http.Parameters
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import model.KisSession
import model.RankingStock
import service.AutoTradingManager
import java.io.ByteArrayInputStream
import java.io.File
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,
val name: String
){}
object StockUniverseLoader {
// prettyPrint = true 를 주면 JSON 파일이 한 줄로 안 뭉치고 예쁘게 저장됩니다.
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)
if (!file.exists()) {
println("⚠️ 파일을 찾을 수 없습니다: ${file.absolutePath}")
return emptyList()
}
val rawJson = file.readText()
val stockItems = json.decodeFromString<List<StockItem>>(rawJson)
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)
File(filePath).writeText(jsonString, Charsets.UTF_8)
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 = readSafeLines(file)
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) // 실패 시 기존 데이터라도 반환
}
}
}
data class CorpInfo(
var cCode : String = "",
var cName : String = "",
var stockCode : String = "",
var stockName : String = "",
)
object DartCodeManager {
private val corpCodeMap = mutableMapOf<String, CorpInfo>()
private var DART_API_KEY = KisSession.config.dAppKey // 지범님의 API 키 입력
private fun saveXmlDebugFile(xmlBytes: ByteArray) {
try {
val debugFile = java.io.File("debug_CORPCODE.xml")
debugFile.writeBytes(xmlBytes)
println("💾 [디버그] XML 파일 저장 완료: ${debugFile.absolutePath}")
// M3 Pro 환경에서는 파일 쓰기가 거의 즉시 완료됩니다.
} catch (e: Exception) {
println("⚠️ [디버그] 파일 저장 실패: ${e.message}")
}
}
// suspend fun fetchKrxData(client: HttpClient) {
// val url = "https://data.krx.co.kr/comm/bldAttendant/getJsonData.cmd"
//
// val response: HttpResponse = client.post(url) {
// // Headers 설정
// header(HttpHeaders.Accept, "application/json, text/javascript, */*; q=0.01")
// header(HttpHeaders.AcceptLanguage, "en-US,en;q=0.9,ko-KR;q=0.8,ko;q=0.7")
// header("X-Requested-With", "XMLHttpRequest")
// header(HttpHeaders.Referrer, "https://data.krx.co.kr/contents/MDC/MDI/mdiLoader/index.cmd?menuId=MDC0302")
// header(HttpHeaders.UserAgent, "Mozilla/5.0 (Linux; Android) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Mobile Safari/537.36")
//
// // Body 설정 (application/x-www-form-urlencoded)
// setBody(FormDataContent(Parameters.build {
// append("bld", "dbms/MDC/EASY/ranking/MDCEASY01601")
// append("locale", "ko_KR")
// append("mktId", "ALL")
// append("itmTpCd3", "2")
// append("itmTpCd2", "1")
// append("strtDd", "20250402")
// append("endDd", "20260402")
// append("stkprcTpCd", "Y")
// append("share", "1")
// append("money", "1")
// append("csvxls_isNo", "false")
// }))
// }
//
// val responseBody = response.bodyAsText()
// println(responseBody)
// }
/**
* 앱 실행 시 호출하여 매핑 테이블 업데이트
*/
suspend fun updateCorpCodes(client: HttpClient) {
println("📂 [DART] 법인코드 매핑 데이터 업데이트 시작...")
try {
val url = "https://opendart.fss.or.kr/api/corpCode.xml?crtfc_key=$DART_API_KEY"
val response: HttpResponse = client.get(url)
val zipBytes = response.readBytes()
// val zipFile = File("dart_corp_codes.zip")
// zipFile.writeBytes(zipBytes)
// println("💾 [디버그] 원본 ZIP 저장 완료: ${zipFile.absolutePath} (${zipBytes.size} bytes)")
ZipInputStream(ByteArrayInputStream(zipBytes)).use { zis ->
var entry = zis.nextEntry
while (entry != null) {
if (entry.name == "CORPCODE.xml") {
// saveXmlDebugFile(zipBytes)
parseXml(zis.readAllBytes())
break
}
entry = zis.nextEntry
}
}
println("✅ [DART] 매핑 완료: ${corpCodeMap.size}개의 상장사 로드됨")
} catch (e: Exception) {
println("❌ [DART] 법인코드 업데이트 실패: ${e.message}")
}
}
private fun parseXml(xmlBytes: ByteArray) {
val factory = DocumentBuilderFactory.newInstance()
val builder = factory.newDocumentBuilder()
val doc = builder.parse(ByteArrayInputStream(xmlBytes))
val nodeList = doc.getElementsByTagName("list")
for (i in 0 until nodeList.length) {
val element = nodeList.item(i) as org.w3c.dom.Element
val stockCode = element.getElementsByTagName("stock_code").item(0)?.textContent?.trim() ?: ""
val corpCode = element.getElementsByTagName("corp_code").item(0)?.textContent ?: ""
val corpName = element.getElementsByTagName("corp_name").item(0)?.textContent ?: ""
// println("[$corpName]stockCode: $stockCode , corpCode: $corpCode")
// 종목코드(stock_code)가 있는 상장사만 매핑에 추가
if (stockCode.isNotEmpty()) {
corpCodeMap[stockCode] = CorpInfo(corpCode, corpName, stockCode)
// AutoTradingManager.addToReanalysis(RankingStock(mksc_shrn_iscd = stockCode, hts_kor_isnm = corpName))
}
}
}
fun getStockCodez() : Array<String> = corpCodeMap.keys.toTypedArray()
/**
* 6자리 종목코드로 8자리 법인코드 반환
*/
fun getCorpCode(stockCode: String): CorpInfo? {
// 1. 직접 매칭 시도
corpCodeMap[stockCode]?.let { return it }
// 2. 우선주 규칙 적용 (마지막 자리가 5, 7, 9인 경우 0으로 변경)
if (stockCode.length == 6 && stockCode.last() in listOf('5', '7', '9')) {
val commonStockCode = stockCode.substring(0, 5) + "0"
corpCodeMap[commonStockCode]?.let {
println(" [DART] 우선주($stockCode)를 보통주($commonStockCode) 코드로 매핑 성공")
return it
}
}
return null
}
}