...
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 {
|
||||||
@ -365,68 +370,68 @@ fun DashboardScreen() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
items(items.size) { index ->
|
items(items.size) { index ->
|
||||||
val configKey = items.get(index)
|
val configKey = items.get(index)
|
||||||
|
|
||||||
// 1. 키보드 입력을 실시간으로 보여줄 로컬 상태 (String)
|
// 1. 키보드 입력을 실시간으로 보여줄 로컬 상태 (String)
|
||||||
var localText by remember(configKey) {
|
var localText by remember(configKey) {
|
||||||
mutableStateOf(KisSession.config.getValues(configKey)?.toString() ?: "")
|
mutableStateOf(KisSession.config.getValues(configKey)?.toString() ?: "")
|
||||||
}
|
}
|
||||||
|
|
||||||
var labelText by remember(configKey) {
|
var labelText by remember(configKey) {
|
||||||
mutableStateOf(KisSession.config.getValues(configKey)?.toString() ?: "")
|
mutableStateOf(KisSession.config.getValues(configKey)?.toString() ?: "")
|
||||||
}
|
}
|
||||||
|
|
||||||
val saveAction = {
|
val saveAction = {
|
||||||
var newValue = localText.toDoubleOrNull() ?: 0.0
|
var newValue = localText.toDoubleOrNull() ?: 0.0
|
||||||
// if (configKey.name.contains("PROFIT")) {
|
// if (configKey.name.contains("PROFIT")) {
|
||||||
// newValue = newValue / KisSession.config.getValues(ConfigIndex.PROFIT_INDEX)
|
// newValue = newValue / KisSession.config.getValues(ConfigIndex.PROFIT_INDEX)
|
||||||
// }
|
// }
|
||||||
KisSession.config.setValues(configKey, newValue)
|
KisSession.config.setValues(configKey, newValue)
|
||||||
DatabaseFactory.saveConfig(KisSession.config)
|
DatabaseFactory.saveConfig(KisSession.config)
|
||||||
println("💾 저장됨: ${configKey.label} = $newValue")
|
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")) {
|
labelText = if (configKey.name.contains("PROFIT")) {
|
||||||
getRemaining(configKey.label,common) + ": 기준율(${KisSession.config.getValues(ConfigIndex.PROFIT_INDEX)}) * 성향별 비율(${KisSession.config.getValues(configKey)}) + 세금제비용(${KisSession.config.getValues(
|
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)}) = ${localText.toDouble() * KisSession.config.getValues(configKey) + KisSession.config.getValues(
|
||||||
ConfigIndex.TAX_INDEX)}"
|
ConfigIndex.TAX_INDEX)} "
|
||||||
} else {
|
} else {
|
||||||
getRemaining(configKey.label,common) + ": -${localText} 호가 매수}"
|
getRemaining(configKey.label,common) + ": -${localText} 호가 매수}"
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
labelText = if (configKey.name.contains("PROFIT")) {
|
OutlinedTextField(
|
||||||
getRemaining(configKey.label,common) + ": 기준율(${KisSession.config.getValues(ConfigIndex.PROFIT_INDEX)}) * 성향별 비율(${KisSession.config.getValues(configKey)}) + 세금제비용(${KisSession.config.getValues(
|
value = localText,
|
||||||
ConfigIndex.TAX_INDEX)}) = ${localText.toDouble() * KisSession.config.getValues(configKey) + KisSession.config.getValues(
|
onValueChange = { localText = it }, // 화면에는 즉시 반영
|
||||||
ConfigIndex.TAX_INDEX)} "
|
label = { Text(labelText) },
|
||||||
} else {
|
modifier = Modifier
|
||||||
getRemaining(configKey.label,common) + ": -${localText} 호가 매수}"
|
.fillMaxWidth()
|
||||||
}
|
.onFocusChanged { focusState ->
|
||||||
|
// 2. 포커스를 잃었을 때 저장
|
||||||
OutlinedTextField(
|
if (!focusState.isFocused) {
|
||||||
value = localText,
|
saveAction()
|
||||||
onValueChange = { localText = it }, // 화면에는 즉시 반영
|
}
|
||||||
label = { Text(labelText) },
|
},
|
||||||
modifier = Modifier
|
keyboardOptions = KeyboardOptions(
|
||||||
.fillMaxWidth()
|
imeAction = ImeAction.Done,
|
||||||
.onFocusChanged { focusState ->
|
keyboardType = KeyboardType.Decimal
|
||||||
// 2. 포커스를 잃었을 때 저장
|
),
|
||||||
if (!focusState.isFocused) {
|
keyboardActions = KeyboardActions(
|
||||||
|
// 3. 엔터(Done) 키를 눌렀을 때 저장
|
||||||
|
onDone = {
|
||||||
saveAction()
|
saveAction()
|
||||||
}
|
}
|
||||||
},
|
),
|
||||||
keyboardOptions = KeyboardOptions(
|
singleLine = true
|
||||||
imeAction = ImeAction.Done,
|
)
|
||||||
keyboardType = KeyboardType.Decimal
|
}
|
||||||
),
|
|
||||||
keyboardActions = KeyboardActions(
|
|
||||||
// 3. 엔터(Done) 키를 눌렀을 때 저장
|
|
||||||
onDone = {
|
|
||||||
saveAction()
|
|
||||||
}
|
|
||||||
),
|
|
||||||
singleLine = true
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user