This commit is contained in:
lunaticbum 2026-03-26 13:48:26 +09:00
parent b289b4d9a0
commit af0dc6b15f
4 changed files with 187 additions and 156 deletions

View File

@ -21,6 +21,7 @@ import model.DartFinancialResponse
import model.KisSession
import model.NaverNewsResponse
import service.DynamicNewsScraper
import service.FinancialAnalyzer
import service.SafeScraper
import service.UrlCacheManager
import kotlin.Double
@ -94,7 +95,7 @@ object NewsService {
var buffer : StringBuffer = StringBuffer()
buffer.append("[재무 분석 데이터]").append("\n")
response.list.forEach { it
buffer.append("${it.account_nm} (당기)${it?.thstrm_amount}, (전기)${it?.frmtrm_amount}").append("\n")
buffer.append("${it.account_nm} (당기)${it.thstrm_amount} (전기)${it.frmtrm_amount}").append("\n")
}
return buffer.toString()
} catch (e: Exception) {
@ -114,6 +115,7 @@ object FinancialMapper {
if (rawText.isBlank()) {
return FinancialStatement()
}
// println(rawText)
val currentValues = extractYearlyValues(rawText, "당기")
val previousValues = extractYearlyValues(rawText, "전기")
@ -148,17 +150,23 @@ object FinancialMapper {
quickRatio = quickRatio,
isOperatingProfitPositive = opCurrent > 0,
isNetIncomePositive = niCurrent > 0
)
).apply {
println("당기순이익: ${niCurrent} , isSafetyBeltMet ${FinancialAnalyzer.isSafetyBeltMet(this)}")
}
}
private fun extractYearlyValues(text: String, type: String): Map<String, Double> {
val result = mutableMapOf<String, Double>()
// 정규식 설명: 항목명 뒤의 (당기/전기) 괄호 안의 숫자와 콤마를 찾아 숫자로 변환
val regex = Regex("""([가-힣\s()]+)\s\(?$type\)?([-0-9,.]+)""")
// 핵심 수정: 항목명 뒤에 (당기) 또는 (전기)가 오고, 그 직후의 숫자(마이너스, 쉼표 포함)를 캡처
// 쉼표나 공백으로 끝나는 지점까지 찾습니다.
val regex = Regex("""([가-힣\s()]+)\s\($type\)([-0-9,.]+)""")
regex.findAll(text).forEach { match ->
val key = match.groupValues[1].trim()
val value = match.groupValues[2].replace(",", "").toDoubleOrNull() ?: 0.0
result[key] = value
// 숫자 내 쉼표 제거 후 Double 변환
val rawValue = match.groupValues[2].replace(",", "").toDoubleOrNull() ?: 0.0
result[key] = rawValue
}
return result
}

View File

@ -201,7 +201,6 @@ object RagService {
val scores = technicalAnalyzer.calculateScores(financialScore)
if (scores.avg() > 50) {
result(tradingDecision, false)
tradingDecision.techSummary = technicalAnalyzer.generateComprehensiveReport()
result(tradingDecision, false)
@ -217,9 +216,12 @@ object RagService {
result(tradingDecision, false)
result(decideTrading(stockCode, scores, financialStmt, tradingDecision), true)
} else {
println("${corpInfo?.cName} : ${scores.toString()}")
tradingDecision.confidence = 1.0
result(tradingDecision, false)
}
} else {
tradingDecision.confidence = 1.0
result(tradingDecision, false)
}
}catch (e: Exception) {

View File

@ -3,7 +3,6 @@ package service
import AutoTradeItem
import network.TradingDecision
import TradingLogStore
import TradingLogStore.decisionLogs
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
@ -358,7 +357,7 @@ object AutoTradingManager {
}
// 2. 메인 루프 실행
runDiscoveryLoop(KisTradeService, globalCallback)
runDiscoveryLoop(globalCallback)
}
@ -375,6 +374,7 @@ object AutoTradingManager {
targetPrice = MarketUtil.roundToTickSize(targetPrice + MarketUtil.getTickSize(targetPrice))
println("🔄 [재주문] ${holding.name} (${holding.code}) 매도 목표 ${targetPrice} 미체결 매도 건 재주문 시도")
// TradingLogStore.addSellLog(holding.code,targetPrice.toString(),"SELL","🎊 보유 주식 매도 주문[예상수익 : ${holding.profitRate}] ")
tradeService.postOrder(
stockCode = holding.code,
qty = holding.availOrderCount,
@ -384,8 +384,10 @@ object AutoTradingManager {
// 4. 새로운 주문번호로 DB 업데이트 및 상태를 SELLING으로 유지
// DatabaseFactory.updateStatusAndOrderNo(item.id!!, TradeStatus.SELLING, newOrderNo)
println("✅ [재주문 완료] ${holding.name}: $newOrderNo")
TradingLogStore.addSellLog(holding.code,targetPrice.toString(),"SELL","🎊 보유 주식 매도 주문 완료[예상수익 : ${holding.profitRate}] ")
}.onFailure {
println("❌ [재주문 실패] ${holding.name}: ${it.message}")
TradingLogStore.addSellLog(holding.code,targetPrice.toString(),"SELL","🎊 보유 주식 매도 주문 실패[${it.message}] ")
// println("❌ [재주문 실패] ${holding.name}: ${it.message}")
}
} else {
@ -437,9 +439,9 @@ object AutoTradingManager {
var currentTimeMillis = System.currentTimeMillis()
var waitTime = 0.2
val H16 = LocalTime.of(16, 0)
val H08M50 = LocalTime.of(8, 50)
val H08M35 = LocalTime.of(8, 35)
val H08M30 = LocalTime.of(8, 30)
private fun runDiscoveryLoop(tradeService: KisTradeService, callback: TradingDecisionCallback) {
private fun runDiscoveryLoop(callback: TradingDecisionCallback) {
discoveryJob = scope.launch {
println("🚀 [AutoTrading] 발굴 루프 시작: ${LocalDateTime.now()}")
while (isActive) {
@ -447,34 +449,11 @@ object AutoTradingManager {
now = LocalTime.now(ZoneId.of("Asia/Seoul"))
currentTimeMillis = System.currentTimeMillis()
lastTickTime.set(System.currentTimeMillis()) // 생존 신고
// [수정] 16시 이후이거나 8시 30분 이전이면 모든 로직 중단 및 초기화
if (now.isAfter(H16) || now.isBefore(H08M30)) {
println("🌙 [System] 마감 시간 도달. 자원 정리 후 대기 모드(설정 화면)로 전환합니다.")
onMarketClosed?.invoke()
KisWebSocketManager.disconnect()
BrowserManager.closeIfIdle(0)
LlamaServerManager.stopAll() // AI 서버 완전 종료
TradingLogStore.clear()
// Main.kt에 설정 화면으로 가라고 신호 전송
stopDiscovery() // 발굴 루프 완전 폭파 (내일 8시 30분에 다시 켜짐)
return@launch
} else if (now.isAfter(H08M30) && now.isBefore(H08M50) && !isSystemReadyToday) {
if (MarketUtil.canTradeToday()) {
println("✅ [System] 오늘은 영업일입니다. 시스템을 가동합니다.")
tryRefreshToken() // 토큰 갱신 및 화면 표시 신호(shouldShowFullWindow = true)
} else {
println("💤 [System] 오늘은 휴장일(또는 주말)입니다. 대기 모드를 유지합니다.")
isSystemReadyToday = false
delay(3600_000) // 휴장일이면 1시간 뒤에 다시 체크하도록 긴 지연시간 부여
continue
}
}
when {
//장중
now.isBefore(H16) && now.isAfter(H08M50) -> {
now.isAfter(H16) || now.isBefore(H08M35) -> {
prepareMarketOpen(now)
}
now.isBefore(H16) && now.isAfter(H08M35) -> {
waitTime = 0.2
if (now.isAfter(LocalTime.of(8, 0)) && now.isBefore(LocalTime.of(15, 30))) {
if (!KisSession.isMarketTokenValid() || !KisSession.isTradeTokenValid()) {
@ -488,122 +467,18 @@ object AutoTradingManager {
}
withTimeout(CYCLE_TIMEOUT) {
println("⏱️ [Cycle Start] ${LocalTime.now()}")
val now = LocalTime.now(ZoneId.of("Asia/Seoul"))
if (now.isAfter(LocalTime.of(15, 20))) {
executeClosingLiquidation(tradeService)
return@withTimeout
}
val balance = tradeService.fetchIntegratedBalance().getOrNull()
balance?.let { resumePendingSellOrders(tradeService, it) }
val myCash = balance?.deposit?.replace(",", "")?.toLongOrNull() ?: 0L
val myHoldings = balance?.holdings?.filter { it.quantity.toInt() > 0 }?.map { it.code }?.toSet() ?: emptySet()
val pendingStocks = DatabaseFactory.findAllMonitoringTrades().map { it.code }
if (remainingCandidates.isEmpty()) {
val stocks = StockUniverseLoader.loadUniverse().shuffled().take(100)
println("✅ 총 ${stocks.size}개의 종목을 로드했습니다.")
stocks.forEach { (code, name) ->
// println("📌 로드됨: [$code] $name")
addToReanalysis(RankingStock(mksc_shrn_iscd = code, hts_kor_isnm = name))
}
val candidates: MutableList<RankingStock> = fetchCandidates(tradeService).apply {
println("후보군 총 개수 : $size")
}.filter {
val rate = it.prdy_ctrt.toDouble()
val corpInfo = DartCodeManager.getCorpCode(it.code)
val isOk = (rate > 0 && rate < 15) || (rate < 0 && rate > -15)
if (corpInfo?.cName.isNullOrEmpty()) {
false
} else {
isOk
}
}
.filter { !it.name.contains("호스팩", true) }
.sortedBy { (it.prdy_ctrt.toDoubleOrNull() ?: 0.0) }
.toMutableList()
if (reanalysisList.isNotEmpty()) {
candidates.addAll(reanalysisList.asReversed())
}
reanalysisList.clear()
remainingCandidates.addAll(candidates.filter { it.code !in myHoldings && it.code !in pendingStocks && it.code !in executionCache.values.map { it.code } && it.code !in failList}
.distinctBy { it.code })
if (now.isAfter(H16)) {
executeClosingLiquidation(KisTradeService)
} else {
println("미확인 데이터 ${remainingCandidates.size}")
// remainingCandidates.removeIf { it.code in myHoldings || it.code in pendingStocks || it.code in executionCache.values.map { it.code } || it.code in failList}
}
var totalCount = remainingCandidates.size
println("후보군 조건 충족 총 개수 : ${totalCount}")
val iterator = remainingCandidates.iterator()
while (iterator.hasNext()) {
totalCount--
val stock = iterator.next()
try {
processSingleStock(stock, myCash, tradeService, callback)
} catch (e: Exception) {
println("❌ 처리 중 오류 발생 (건너뜀): ${stock.name}")
} finally {
iterator.remove()
}
println("남은 후보군 개수 : ${totalCount}")
delay(100)
}
println("⏱️ [Cycle End] ${LocalTime.now()}")
}
}
//장외
now.isAfter(H16) || now.isBefore(H08M50) -> {
when {
(now.hour == 0 && now.minute == 0 && (isSystemReadyToday || isSystemCleanedUpToday)) -> {
waitTime = 10.0
isSystemReadyToday = false
isSystemCleanedUpToday = false
}
(now.isAfter(LocalTime.of(8, 0)) && !isSystemReadyToday) -> {
waitTime = 3.0
if (!KisSession.isMarketTokenValid() || !KisSession.isTradeTokenValid()) {
KisWebSocketManager.disconnect()
tryRefreshToken()
}
}
(now.isAfter(LocalTime.of(18, 0))) -> {
try {
waitTime = 5.0
println("current SystemCleanedUpToday is $isSystemCleanedUpToday")
if (!isSystemCleanedUpToday) {
println("🌙 [System] 업무 종료 및 자원 정리 시작...")
SystemSleepPreventer.sleepDisplay() // 모니터 끄기
KisWebSocketManager.disconnect()
BrowserManager.closeIfIdle(0) // 즉시 닫기
if (LlamaServerManager.stopAll()) {
isSystemCleanedUpToday = true
}
}
println("✅ [System] 오늘의 모든 정리가 완료되었습니다.")
} catch (e: Exception) {
}
}
(now.isAfter(LocalTime.of(18, 15)) && now.minute % 15 == 0) -> {
try {
waitTime = 5.0
SystemSleepPreventer.sleepDisplay() // 모니터 끄기
} catch (e: Exception) {
}
}
else -> {
waitTime = 5.0
executeMarketLoop()
}
}
}
//
// //장외
// now.isAfter(H16) || now.isBefore(H08M35) -> {
// finalizeMarketClose(now)
// }
else ->{
waitTime = 3.0
}
@ -619,6 +494,150 @@ object AutoTradingManager {
}
}
suspend fun prepareMarketOpen(now : LocalTime) {
if (now.isAfter(H16) || now.isBefore(H08M30)) {
println("🌙 [System] 마감 시간 도달. 자원 정리 후 대기 모드(설정 화면)로 전환합니다.")
onMarketClosed?.invoke()
KisWebSocketManager.disconnect()
BrowserManager.closeIfIdle(0)
LlamaServerManager.stopAll() // AI 서버 완전 종료
TradingLogStore.clear()
// Main.kt에 설정 화면으로 가라고 신호 전송
stopDiscovery() // 발굴 루프 완전 폭파 (내일 8시 30분에 다시 켜짐)
} else if (now.isAfter(H08M30) && now.isBefore(H08M35) && !isSystemReadyToday) {
if (MarketUtil.canTradeToday()) {
println("✅ [System] 오늘은 영업일입니다. 시스템을 가동합니다.")
tryRefreshToken() // 토큰 갱신 및 화면 표시 신호(shouldShowFullWindow = true)
} else {
println("💤 [System] 오늘은 휴장일(또는 주말)입니다. 대기 모드를 유지합니다.")
isSystemReadyToday = false
delay(3600_000) // 휴장일이면 1시간 뒤에 다시 체크하도록 긴 지연시간 부여
}
}
}
var loadedTops = mutableListOf<Pair<String, String>>()
fun poll100Stocks(): List<Pair<String, String>> {
val count = minOf(loadedTops.size, 100)
if (count == 0) return emptyList()
// 앞의 100개를 복사
val batch = loadedTops.subList(0, count).toList()
// 원본에서 삭제 (이 작업이 큐의 pop/remove 역할을 합니다)
loadedTops.subList(0, count).clear()
return batch
}
suspend fun executeMarketLoop() {
val balance = KisTradeService.fetchIntegratedBalance().getOrNull()
balance?.let { resumePendingSellOrders(KisTradeService, it) }
val myCash = balance?.deposit?.replace(",", "")?.toLongOrNull() ?: 0L
val myHoldings = balance?.holdings?.filter { it.quantity.toInt() > 0 }?.map { it.code }?.toSet() ?: emptySet()
val pendingStocks = DatabaseFactory.findAllMonitoringTrades().map { it.code }
if (remainingCandidates.isEmpty()) {
if (loadedTops.size < 100) {
loadedTops.addAll(StockUniverseLoader.loadUniverse())
loadedTops.shuffle()
println("✅ 총 ${loadedTops.size}개의 종목이 로드되있음.")
}
poll100Stocks().forEach { (code, name) ->
addToReanalysis(RankingStock(mksc_shrn_iscd = code, hts_kor_isnm = name))
}
val candidates: MutableList<RankingStock> = fetchCandidates(KisTradeService).apply {
}.filter {
val rate = it.prdy_ctrt.toDouble()
val corpInfo = DartCodeManager.getCorpCode(it.code)
val isOk = (rate > 0 && rate < 15) || (rate < 0 && rate > -15)
if (corpInfo?.cName.isNullOrEmpty()) {
false
} else {
isOk
}
}
.filter { !it.name.contains("호스팩", true) }
.sortedBy { (it.prdy_ctrt.toDoubleOrNull() ?: 0.0) }
.toMutableList()
if (reanalysisList.isNotEmpty()) {
candidates.addAll(reanalysisList)
}
reanalysisList.clear()
remainingCandidates.addAll(candidates.filter { it.code !in myHoldings && it.code !in pendingStocks && it.code !in executionCache.values.map { it.code } && it.code !in failList}
.distinctBy { it.code })
} else {
println("미확인 데이터 ${remainingCandidates.size}")
}
var totalCount = remainingCandidates.size
println("후보군 조건 충족 총 개수 : ${totalCount}")
val iterator = remainingCandidates.iterator()
while (iterator.hasNext()) {
totalCount--
val stock = iterator.next()
try {
processSingleStock(stock, myCash, KisTradeService, globalCallback)
} catch (e: Exception) {
println("❌ 처리 중 오류 발생 (건너뜀): ${stock.name}")
} finally {
iterator.remove()
}
println("남은 후보군 개수 : ${totalCount}")
delay(100)
}
println("⏱️ [Cycle End] ${LocalTime.now()}")
}
suspend fun finalizeMarketClose(now: LocalTime) {
when {
(AutoTradingManager.now.hour == 0 && AutoTradingManager.now.minute == 0 && (isSystemReadyToday || isSystemCleanedUpToday)) -> {
waitTime = 10.0
isSystemReadyToday = false
isSystemCleanedUpToday = false
}
(AutoTradingManager.now.isAfter(LocalTime.of(8, 0)) && !isSystemReadyToday) -> {
waitTime = 3.0
if (!KisSession.isMarketTokenValid() || !KisSession.isTradeTokenValid()) {
KisWebSocketManager.disconnect()
tryRefreshToken()
}
}
(AutoTradingManager.now.isAfter(LocalTime.of(18, 0))) -> {
try {
waitTime = 5.0
println("current SystemCleanedUpToday is $isSystemCleanedUpToday")
if (!isSystemCleanedUpToday) {
println("🌙 [System] 업무 종료 및 자원 정리 시작...")
SystemSleepPreventer.sleepDisplay() // 모니터 끄기
KisWebSocketManager.disconnect()
BrowserManager.closeIfIdle(0) // 즉시 닫기
if (LlamaServerManager.stopAll()) {
isSystemCleanedUpToday = true
}
}
println("✅ [System] 오늘의 모든 정리가 완료되었습니다.")
} catch (e: Exception) {
}
}
(AutoTradingManager.now.isAfter(LocalTime.of(18, 15)) && AutoTradingManager.now.minute % 15 == 0) -> {
try {
waitTime = 5.0
SystemSleepPreventer.sleepDisplay() // 모니터 끄기
} catch (e: Exception) {
}
}
else -> {
waitTime = 5.0
}
}
}
fun addToReanalysis(stock: RankingStock) {
val count = retryCountMap.getOrDefault(stock.code, 0)
if (count < 10) { // 최대 2회까지만 재시도하여 무한 루프 방지
@ -862,14 +881,16 @@ data class InvestmentScores(
) {
override fun toString(): String {
return """
ultraShort $ultraShort
shortTerm $shortTerm
midTerm $midTerm
longTerm $longTerm
AVG : ${avg()}
ultraShort : $ultraShort
shortTerm : $shortTerm
midTerm : $midTerm
longTerm : $longTerm
""".trimIndent()
}
fun avg() = listOf(ultraShort, shortTerm, midTerm, longTerm).average()
}
@Serializable

View File

@ -96,7 +96,7 @@ fun SettingsScreen(onAuthSuccess: () -> Unit) {
break // 성공하면 루프 탈출
}
}
delay(60_000 * 2) // 1분마다 시간 체크
delay(30_000) // 1분마다 시간 체크
}
}