This commit is contained in:
lunaticbum 2026-02-19 16:20:15 +09:00
parent 59c8ff4ebb
commit 535989b8ae
3 changed files with 100 additions and 83 deletions

View File

@ -12,6 +12,7 @@ import io.ktor.client.request.*
import io.ktor.client.statement.bodyAsText
import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import io.ktor.utils.io.CancellationException
import kotlinx.serialization.json.Json
import model.CandleData
import model.RankingResponse
@ -22,6 +23,7 @@ import model.StockBalanceResponse
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
@ -30,6 +32,7 @@ import model.*
import java.time.LocalDate
import java.time.LocalTime
import java.time.format.DateTimeFormatter
import kotlin.coroutines.coroutineContext
object KisTradeService {
private val client = HttpClient(CIO) {
@ -525,6 +528,7 @@ object KisTradeService {
try {
do {
if (!coroutineContext.isActive) throw _root_ide_package_.io.ktor.utils.io.CancellationException("UI에서 작업을 취소함") // [추가]
println("📡 [Step $pageCount] 요청 전송 중... (tr_cont: $trCont)")
val response = client.get("$baseUrl/uapi/domestic-stock/v1/trading/inquire-balance") {
header("authorization", "Bearer ${config.tradeToken}")
@ -582,6 +586,10 @@ object KisTradeService {
}
} catch (e: Exception) {
if (e is CancellationException) {
println(" [잔고조회] 사용자가 화면을 벗어나 조회를 중단합니다.")
throw e
}
println("💥 [Fatal Error] 잔고 조회 중 예외 발생: ${e.message}")
e.printStackTrace()
return Result.failure(e)

View File

@ -19,6 +19,7 @@ import model.ConfigIndex
import model.KisSession
import model.RankingStock
import model.RankingType
import model.UnifiedBalance
import network.DartCodeManager
import network.FinancialMapper
import network.FinancialStatement
@ -78,13 +79,14 @@ object AutoTradingManager {
}
suspend fun resumePendingSellOrders(tradeService: KisTradeService) {
suspend fun resumePendingSellOrders(tradeService: KisTradeService,balance : UnifiedBalance) {
// 1. DB에서 매도 중(SELLING)이거나 만료(EXPIRED)된 매도 건을 가져옵니다.
println("resumePendingSellOrders")
val pendingSells = DatabaseFactory.getAutoTradesByStatus(listOf(TradeStatus.SELLING, TradeStatus.EXPIRED))
println("pendingSells >>> ${pendingSells.size}")
pendingSells.forEach { item ->
// 2. 실제로 잔고에 해당 종목이 있는지 확인 (안전장치)
val balance = tradeService.fetchIntegratedBalance().getOrNull()
// val balance = tradeService.fetchIntegratedBalance().getOrNull()
val holding = balance?.holdings?.find { it.code == item.code }
if (holding != null && holding.quantity.toInt() > 0) {
@ -123,14 +125,16 @@ object AutoTradingManager {
println("⏱️ [Cycle Start] ${LocalTime.now()}")
// [프로세스 1] 장 마감 및 잔고 체크
val now = LocalTime.now(ZoneId.of("Asia/Seoul"))
//&& now.isBefore(LocalTime.of(15, 30))
if (now.isAfter(LocalTime.of(15, 30)) ) {
// executeClosingLiquidation(tradeService)
return@withTimeout
}
// val now = LocalTime.now(ZoneId.of("Asia/Seoul"))
// //&& now.isBefore(LocalTime.of(15, 30))
// if (now.isAfter(LocalTime.of(15, 30)) ) {
//// 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 }
@ -295,25 +299,25 @@ object AutoTradingManager {
private suspend fun executeClosingLiquidation(tradeService: KisTradeService) {
val activeTrades = DatabaseFactory.findAllMonitoringTrades()
val balanceResult = tradeService.fetchIntegratedBalance().getOrNull()
val realHoldings = balanceResult?.holdings?.associateBy { it.code } ?: emptyMap()
activeTrades.forEach { trade ->
try {
if (!realHoldings.containsKey(trade.code)) {
DatabaseFactory.updateStatusAndOrderNo(trade.id!!, TradeStatus.COMPLETED)
return@forEach
}
// 마감 정리 로직 (필요 시 주석 해제하여 사용)
println("📢 [마감 정리 체크] ${trade.name}")
} catch (e: Exception) {
println("⚠️ [마감 에러] ${trade.name}: ${e.message}")
}
delay(200)
}
}
// private suspend fun executeClosingLiquidation(tradeService: KisTradeService) {
// val activeTrades = DatabaseFactory.findAllMonitoringTrades()
// val balanceResult = tradeService.fetchIntegratedBalance().getOrNull()
// val realHoldings = balanceResult?.holdings?.associateBy { it.code } ?: emptyMap()
//
// activeTrades.forEach { trade ->
// try {
// if (!realHoldings.containsKey(trade.code)) {
// DatabaseFactory.updateStatusAndOrderNo(trade.id!!, TradeStatus.COMPLETED)
// return@forEach
// }
// // 마감 정리 로직 (필요 시 주석 해제하여 사용)
// println("📢 [마감 정리 체크] ${trade.name}")
// } catch (e: Exception) {
// println("⚠️ [마감 에러] ${trade.name}: ${e.message}")
// }
// delay(200)
// }
// }
fun stopDiscovery() {
discoveryJob?.cancel()

View File

@ -20,7 +20,9 @@ import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import model.CandleData
import model.ConfigIndex
@ -89,13 +91,14 @@ fun DashboardScreen() {
}
}
DisposableEffect(Unit) {
// 1. 화면 진입 시: 자동 발굴 루프 시작
// AI 분석 결과(decision)가 나오면 completeTradingDecision 상태를 업데이트하여
// IntegratedOrderSection에서 자동으로 매수 로직이 실행되도록 연결합니다.
AutoTradingManager.startAutoDiscoveryLoop(tradeService,callback)
LaunchedEffect(Unit) {
// 화면이 완전히 그려지고 안정화될 때까지 1초 대기
delay(1000)
AutoTradingManager.startAutoDiscoveryLoop(tradeService, callback)
}
// 2. 화면 이탈 시(앱 종료 등): 루프 중단 (리소스 정리)
// 리소스 정리는 여전히 DisposableEffect에서 수행
DisposableEffect(Unit) {
onDispose {
AutoTradingManager.stopDiscovery()
}
@ -109,7 +112,7 @@ fun DashboardScreen() {
// [중앙 관리 함수] 체결 정보와 DB 정보를 매칭하여 실행
LaunchedEffect(refreshTrigger) {
setupAutoTradingWatchdog(tradeService,callback)
// setupAutoTradingWatchdog(tradeService,callback)
}
suspend fun syncAndExecute(orderNo: String) {
@ -181,10 +184,12 @@ fun DashboardScreen() {
val monitoringTrades = DatabaseFactory.getAutoTradesByStatus(listOf(TradeStatus.MONITORING, TradeStatus.PENDING_BUY))
val monitoringCodes = monitoringTrades.map { it.code }.toSet()
wsManager.updateSubscriptions(monitoringCodes)
AutoTradingManager.resumePendingSellOrders(tradeService)
refreshTrigger++
}
// 3. 실시간 체결 통보 핸들러 (주문번호 중심)
wsManager.onExecutionReceived = {code, qty, price,orderNo, isBuy ->
scope.launch {
@ -365,68 +370,68 @@ fun DashboardScreen() {
)
}
items(items.size) { index ->
val configKey = items.get(index)
items(items.size) { index ->
val configKey = items.get(index)
// 1. 키보드 입력을 실시간으로 보여줄 로컬 상태 (String)
var localText by remember(configKey) {
mutableStateOf(KisSession.config.getValues(configKey)?.toString() ?: "")
}
// 1. 키보드 입력을 실시간으로 보여줄 로컬 상태 (String)
var localText by remember(configKey) {
mutableStateOf(KisSession.config.getValues(configKey)?.toString() ?: "")
}
var labelText by remember(configKey) {
mutableStateOf(KisSession.config.getValues(configKey)?.toString() ?: "")
}
var labelText by remember(configKey) {
mutableStateOf(KisSession.config.getValues(configKey)?.toString() ?: "")
}
val saveAction = {
var newValue = localText.toDoubleOrNull() ?: 0.0
val saveAction = {
var newValue = localText.toDoubleOrNull() ?: 0.0
// if (configKey.name.contains("PROFIT")) {
// newValue = newValue / KisSession.config.getValues(ConfigIndex.PROFIT_INDEX)
// }
KisSession.config.setValues(configKey, newValue)
DatabaseFactory.saveConfig(KisSession.config)
println("💾 저장됨: ${configKey.label} = $newValue")
KisSession.config.setValues(configKey, newValue)
DatabaseFactory.saveConfig(KisSession.config)
println("💾 저장됨: ${configKey.label} = $newValue")
labelText = if (configKey.name.contains("PROFIT")) {
getRemaining(configKey.label,common) + ": 기준율(${KisSession.config.getValues(ConfigIndex.PROFIT_INDEX)}) * 성향별 비율(${KisSession.config.getValues(configKey)}) + 세금제비용(${KisSession.config.getValues(
ConfigIndex.TAX_INDEX)}) = ${localText.toDouble() * KisSession.config.getValues(configKey) + KisSession.config.getValues(
ConfigIndex.TAX_INDEX)}"
} else {
getRemaining(configKey.label,common) + ": -${localText} 호가 매수}"
}
}
labelText = if (configKey.name.contains("PROFIT")) {
getRemaining(configKey.label,common) + ": 기준율(${KisSession.config.getValues(ConfigIndex.PROFIT_INDEX)}) * 성향별 비율(${KisSession.config.getValues(configKey)}) + 세금제비용(${KisSession.config.getValues(
ConfigIndex.TAX_INDEX)}) = ${localText.toDouble() * KisSession.config.getValues(configKey) + KisSession.config.getValues(
ConfigIndex.TAX_INDEX)}"
ConfigIndex.TAX_INDEX)} "
} else {
getRemaining(configKey.label,common) + ": -${localText} 호가 매수}"
}
}
labelText = if (configKey.name.contains("PROFIT")) {
getRemaining(configKey.label,common) + ": 기준율(${KisSession.config.getValues(ConfigIndex.PROFIT_INDEX)}) * 성향별 비율(${KisSession.config.getValues(configKey)}) + 세금제비용(${KisSession.config.getValues(
ConfigIndex.TAX_INDEX)}) = ${localText.toDouble() * KisSession.config.getValues(configKey) + KisSession.config.getValues(
ConfigIndex.TAX_INDEX)} "
} else {
getRemaining(configKey.label,common) + ": -${localText} 호가 매수}"
}
OutlinedTextField(
value = localText,
onValueChange = { localText = it }, // 화면에는 즉시 반영
label = { Text(labelText) },
modifier = Modifier
.fillMaxWidth()
.onFocusChanged { focusState ->
// 2. 포커스를 잃었을 때 저장
if (!focusState.isFocused) {
OutlinedTextField(
value = localText,
onValueChange = { localText = it }, // 화면에는 즉시 반영
label = { Text(labelText) },
modifier = Modifier
.fillMaxWidth()
.onFocusChanged { focusState ->
// 2. 포커스를 잃었을 때 저장
if (!focusState.isFocused) {
saveAction()
}
},
keyboardOptions = KeyboardOptions(
imeAction = ImeAction.Done,
keyboardType = KeyboardType.Decimal
),
keyboardActions = KeyboardActions(
// 3. 엔터(Done) 키를 눌렀을 때 저장
onDone = {
saveAction()
}
},
keyboardOptions = KeyboardOptions(
imeAction = ImeAction.Done,
keyboardType = KeyboardType.Decimal
),
keyboardActions = KeyboardActions(
// 3. 엔터(Done) 키를 눌렀을 때 저장
onDone = {
saveAction()
}
),
singleLine = true
)
}
),
singleLine = true
)
}
}
}
}