...
This commit is contained in:
parent
59c8ff4ebb
commit
535989b8ae
@ -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)
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user