...
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.client.statement.bodyAsText
|
||||||
import io.ktor.http.*
|
import io.ktor.http.*
|
||||||
import io.ktor.serialization.kotlinx.json.*
|
import io.ktor.serialization.kotlinx.json.*
|
||||||
|
import io.ktor.utils.io.CancellationException
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import model.CandleData
|
import model.CandleData
|
||||||
import model.RankingResponse
|
import model.RankingResponse
|
||||||
@ -22,6 +23,7 @@ import model.StockBalanceResponse
|
|||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.coroutineScope
|
import kotlinx.coroutines.coroutineScope
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.isActive
|
||||||
import kotlinx.serialization.json.JsonObject
|
import kotlinx.serialization.json.JsonObject
|
||||||
import kotlinx.serialization.json.jsonArray
|
import kotlinx.serialization.json.jsonArray
|
||||||
import kotlinx.serialization.json.jsonObject
|
import kotlinx.serialization.json.jsonObject
|
||||||
@ -30,6 +32,7 @@ import model.*
|
|||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
import java.time.LocalTime
|
import java.time.LocalTime
|
||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
|
import kotlin.coroutines.coroutineContext
|
||||||
|
|
||||||
object KisTradeService {
|
object KisTradeService {
|
||||||
private val client = HttpClient(CIO) {
|
private val client = HttpClient(CIO) {
|
||||||
@ -525,6 +528,7 @@ object KisTradeService {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
do {
|
do {
|
||||||
|
if (!coroutineContext.isActive) throw _root_ide_package_.io.ktor.utils.io.CancellationException("UI에서 작업을 취소함") // [추가]
|
||||||
println("📡 [Step $pageCount] 요청 전송 중... (tr_cont: $trCont)")
|
println("📡 [Step $pageCount] 요청 전송 중... (tr_cont: $trCont)")
|
||||||
val response = client.get("$baseUrl/uapi/domestic-stock/v1/trading/inquire-balance") {
|
val response = client.get("$baseUrl/uapi/domestic-stock/v1/trading/inquire-balance") {
|
||||||
header("authorization", "Bearer ${config.tradeToken}")
|
header("authorization", "Bearer ${config.tradeToken}")
|
||||||
@ -582,6 +586,10 @@ object KisTradeService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
if (e is CancellationException) {
|
||||||
|
println("ℹ️ [잔고조회] 사용자가 화면을 벗어나 조회를 중단합니다.")
|
||||||
|
throw e
|
||||||
|
}
|
||||||
println("💥 [Fatal Error] 잔고 조회 중 예외 발생: ${e.message}")
|
println("💥 [Fatal Error] 잔고 조회 중 예외 발생: ${e.message}")
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
return Result.failure(e)
|
return Result.failure(e)
|
||||||
|
|||||||
@ -19,6 +19,7 @@ import model.ConfigIndex
|
|||||||
import model.KisSession
|
import model.KisSession
|
||||||
import model.RankingStock
|
import model.RankingStock
|
||||||
import model.RankingType
|
import model.RankingType
|
||||||
|
import model.UnifiedBalance
|
||||||
import network.DartCodeManager
|
import network.DartCodeManager
|
||||||
import network.FinancialMapper
|
import network.FinancialMapper
|
||||||
import network.FinancialStatement
|
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)된 매도 건을 가져옵니다.
|
// 1. DB에서 매도 중(SELLING)이거나 만료(EXPIRED)된 매도 건을 가져옵니다.
|
||||||
|
println("resumePendingSellOrders")
|
||||||
val pendingSells = DatabaseFactory.getAutoTradesByStatus(listOf(TradeStatus.SELLING, TradeStatus.EXPIRED))
|
val pendingSells = DatabaseFactory.getAutoTradesByStatus(listOf(TradeStatus.SELLING, TradeStatus.EXPIRED))
|
||||||
|
println("pendingSells >>> ${pendingSells.size}")
|
||||||
pendingSells.forEach { item ->
|
pendingSells.forEach { item ->
|
||||||
// 2. 실제로 잔고에 해당 종목이 있는지 확인 (안전장치)
|
// 2. 실제로 잔고에 해당 종목이 있는지 확인 (안전장치)
|
||||||
val balance = tradeService.fetchIntegratedBalance().getOrNull()
|
// val balance = tradeService.fetchIntegratedBalance().getOrNull()
|
||||||
val holding = balance?.holdings?.find { it.code == item.code }
|
val holding = balance?.holdings?.find { it.code == item.code }
|
||||||
|
|
||||||
if (holding != null && holding.quantity.toInt() > 0) {
|
if (holding != null && holding.quantity.toInt() > 0) {
|
||||||
@ -123,14 +125,16 @@ object AutoTradingManager {
|
|||||||
println("⏱️ [Cycle Start] ${LocalTime.now()}")
|
println("⏱️ [Cycle Start] ${LocalTime.now()}")
|
||||||
|
|
||||||
// [프로세스 1] 장 마감 및 잔고 체크
|
// [프로세스 1] 장 마감 및 잔고 체크
|
||||||
val now = LocalTime.now(ZoneId.of("Asia/Seoul"))
|
// val now = LocalTime.now(ZoneId.of("Asia/Seoul"))
|
||||||
//&& now.isBefore(LocalTime.of(15, 30))
|
// //&& now.isBefore(LocalTime.of(15, 30))
|
||||||
if (now.isAfter(LocalTime.of(15, 30)) ) {
|
// if (now.isAfter(LocalTime.of(15, 30)) ) {
|
||||||
// executeClosingLiquidation(tradeService)
|
//// executeClosingLiquidation(tradeService)
|
||||||
return@withTimeout
|
// return@withTimeout
|
||||||
}
|
// }
|
||||||
|
|
||||||
val balance = tradeService.fetchIntegratedBalance().getOrNull()
|
val balance = tradeService.fetchIntegratedBalance().getOrNull()
|
||||||
|
|
||||||
|
balance?.let { resumePendingSellOrders(tradeService,it) }
|
||||||
val myCash = balance?.deposit?.replace(",", "")?.toLongOrNull() ?: 0L
|
val myCash = balance?.deposit?.replace(",", "")?.toLongOrNull() ?: 0L
|
||||||
val myHoldings = balance?.holdings?.filter { it.quantity.toInt() > 0 }?.map { it.code }?.toSet() ?: emptySet()
|
val myHoldings = balance?.holdings?.filter { it.quantity.toInt() > 0 }?.map { it.code }?.toSet() ?: emptySet()
|
||||||
val pendingStocks = DatabaseFactory.findAllMonitoringTrades().map { it.code }
|
val pendingStocks = DatabaseFactory.findAllMonitoringTrades().map { it.code }
|
||||||
@ -295,25 +299,25 @@ object AutoTradingManager {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
private suspend fun executeClosingLiquidation(tradeService: KisTradeService) {
|
// private suspend fun executeClosingLiquidation(tradeService: KisTradeService) {
|
||||||
val activeTrades = DatabaseFactory.findAllMonitoringTrades()
|
// val activeTrades = DatabaseFactory.findAllMonitoringTrades()
|
||||||
val balanceResult = tradeService.fetchIntegratedBalance().getOrNull()
|
// val balanceResult = tradeService.fetchIntegratedBalance().getOrNull()
|
||||||
val realHoldings = balanceResult?.holdings?.associateBy { it.code } ?: emptyMap()
|
// val realHoldings = balanceResult?.holdings?.associateBy { it.code } ?: emptyMap()
|
||||||
|
//
|
||||||
activeTrades.forEach { trade ->
|
// activeTrades.forEach { trade ->
|
||||||
try {
|
// try {
|
||||||
if (!realHoldings.containsKey(trade.code)) {
|
// if (!realHoldings.containsKey(trade.code)) {
|
||||||
DatabaseFactory.updateStatusAndOrderNo(trade.id!!, TradeStatus.COMPLETED)
|
// DatabaseFactory.updateStatusAndOrderNo(trade.id!!, TradeStatus.COMPLETED)
|
||||||
return@forEach
|
// return@forEach
|
||||||
}
|
// }
|
||||||
// 마감 정리 로직 (필요 시 주석 해제하여 사용)
|
// // 마감 정리 로직 (필요 시 주석 해제하여 사용)
|
||||||
println("📢 [마감 정리 체크] ${trade.name}")
|
// println("📢 [마감 정리 체크] ${trade.name}")
|
||||||
} catch (e: Exception) {
|
// } catch (e: Exception) {
|
||||||
println("⚠️ [마감 에러] ${trade.name}: ${e.message}")
|
// println("⚠️ [마감 에러] ${trade.name}: ${e.message}")
|
||||||
}
|
// }
|
||||||
delay(200)
|
// delay(200)
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
fun stopDiscovery() {
|
fun stopDiscovery() {
|
||||||
discoveryJob?.cancel()
|
discoveryJob?.cancel()
|
||||||
|
|||||||
@ -20,7 +20,9 @@ import androidx.compose.ui.text.input.ImeAction
|
|||||||
import androidx.compose.ui.text.input.KeyboardType
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.CoroutineStart
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import model.CandleData
|
import model.CandleData
|
||||||
import model.ConfigIndex
|
import model.ConfigIndex
|
||||||
@ -89,13 +91,14 @@ fun DashboardScreen() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DisposableEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
// 1. 화면 진입 시: 자동 발굴 루프 시작
|
// 화면이 완전히 그려지고 안정화될 때까지 1초 대기
|
||||||
// AI 분석 결과(decision)가 나오면 completeTradingDecision 상태를 업데이트하여
|
delay(1000)
|
||||||
// IntegratedOrderSection에서 자동으로 매수 로직이 실행되도록 연결합니다.
|
AutoTradingManager.startAutoDiscoveryLoop(tradeService, callback)
|
||||||
AutoTradingManager.startAutoDiscoveryLoop(tradeService,callback)
|
}
|
||||||
|
|
||||||
// 2. 화면 이탈 시(앱 종료 등): 루프 중단 (리소스 정리)
|
// 리소스 정리는 여전히 DisposableEffect에서 수행
|
||||||
|
DisposableEffect(Unit) {
|
||||||
onDispose {
|
onDispose {
|
||||||
AutoTradingManager.stopDiscovery()
|
AutoTradingManager.stopDiscovery()
|
||||||
}
|
}
|
||||||
@ -109,7 +112,7 @@ fun DashboardScreen() {
|
|||||||
// [중앙 관리 함수] 체결 정보와 DB 정보를 매칭하여 실행
|
// [중앙 관리 함수] 체결 정보와 DB 정보를 매칭하여 실행
|
||||||
|
|
||||||
LaunchedEffect(refreshTrigger) {
|
LaunchedEffect(refreshTrigger) {
|
||||||
setupAutoTradingWatchdog(tradeService,callback)
|
// setupAutoTradingWatchdog(tradeService,callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun syncAndExecute(orderNo: String) {
|
suspend fun syncAndExecute(orderNo: String) {
|
||||||
@ -181,10 +184,12 @@ fun DashboardScreen() {
|
|||||||
val monitoringTrades = DatabaseFactory.getAutoTradesByStatus(listOf(TradeStatus.MONITORING, TradeStatus.PENDING_BUY))
|
val monitoringTrades = DatabaseFactory.getAutoTradesByStatus(listOf(TradeStatus.MONITORING, TradeStatus.PENDING_BUY))
|
||||||
val monitoringCodes = monitoringTrades.map { it.code }.toSet()
|
val monitoringCodes = monitoringTrades.map { it.code }.toSet()
|
||||||
wsManager.updateSubscriptions(monitoringCodes)
|
wsManager.updateSubscriptions(monitoringCodes)
|
||||||
AutoTradingManager.resumePendingSellOrders(tradeService)
|
|
||||||
refreshTrigger++
|
refreshTrigger++
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 3. 실시간 체결 통보 핸들러 (주문번호 중심)
|
// 3. 실시간 체결 통보 핸들러 (주문번호 중심)
|
||||||
wsManager.onExecutionReceived = {code, qty, price,orderNo, isBuy ->
|
wsManager.onExecutionReceived = {code, qty, price,orderNo, isBuy ->
|
||||||
scope.launch {
|
scope.launch {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user