...
This commit is contained in:
parent
3f209dcd4d
commit
0b855188a9
@ -132,6 +132,22 @@ object DatabaseFactory {
|
|||||||
AutoTradeTable.deleteWhere { AutoTradeTable.id eq id }
|
AutoTradeTable.deleteWhere { AutoTradeTable.id eq id }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun findAllPendingBuyCodes(): Set<String> {
|
||||||
|
return transaction {
|
||||||
|
AutoTradeTable.select {
|
||||||
|
(AutoTradeTable.status eq "PENDING_BUY") or (AutoTradeTable.status eq "ORDERED")
|
||||||
|
}.map { it[AutoTradeTable.stockCode] }.toSet()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun findAllMonitoringTrades(): List<AutoTradeItem> {
|
||||||
|
return transaction {
|
||||||
|
AutoTradeTable.select {
|
||||||
|
AutoTradeTable.status neq "COMPLETED"
|
||||||
|
}.map { mapToAutoTradeItem(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun mapToAutoTradeItem(it: ResultRow) = AutoTradeItem(
|
private fun mapToAutoTradeItem(it: ResultRow) = AutoTradeItem(
|
||||||
id = it[AutoTradeTable.id],
|
id = it[AutoTradeTable.id],
|
||||||
code = it[AutoTradeTable.stockCode],
|
code = it[AutoTradeTable.stockCode],
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import kotlinx.coroutines.launch
|
|||||||
import kotlinx.coroutines.withTimeout
|
import kotlinx.coroutines.withTimeout
|
||||||
import model.CandleData
|
import model.CandleData
|
||||||
import model.RankingType
|
import model.RankingType
|
||||||
|
import network.DartCodeManager
|
||||||
import network.KisTradeService
|
import network.KisTradeService
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
import java.time.LocalTime
|
import java.time.LocalTime
|
||||||
@ -28,6 +29,78 @@ object AutoTradingManager {
|
|||||||
private var discoveryJob: Job? = null
|
private var discoveryJob: Job? = null
|
||||||
|
|
||||||
|
|
||||||
|
private suspend fun executeClosingLiquidation(tradeService: KisTradeService) {
|
||||||
|
// 1. DB에서 현재 감시 중인(보유 중인) 모든 종목 가져오기
|
||||||
|
val activeTrades = DatabaseFactory.findAllMonitoringTrades()
|
||||||
|
// 2. [추가] 실시간 증권사 잔고 조회 (실제 보유 주식인지 확인용)
|
||||||
|
val balanceResult = tradeService.fetchIntegratedBalance().getOrNull()
|
||||||
|
val realHoldings = balanceResult?.holdings
|
||||||
|
?.filter {
|
||||||
|
println("[${it.name}(${it.code})]: evalAmount ${it.evalAmount} , currentPrice : ${it.currentPrice} , ${it.quantity}")
|
||||||
|
it.quantity.toInt() > 0 && it.evalAmount.toDouble() > (it.currentPrice.toDouble() * it.quantity.toDouble()) } // 수량이 0보다 큰 것만
|
||||||
|
?.associateBy { it.code } ?: emptyMap()
|
||||||
|
|
||||||
|
activeTrades.forEach { trade ->
|
||||||
|
try {
|
||||||
|
// [검증] DB에는 MONITORING이지만 실제 잔고에는 없는 경우 처리
|
||||||
|
if (!realHoldings.containsKey(trade.code)) {
|
||||||
|
println("ℹ️ [제외] ${trade.name}: DB에는 감시 중이나 실제 잔고에 수량이 없어 스킵합니다.")
|
||||||
|
// 필요시 DB 상태를 COMPLETED 등으로 동기화
|
||||||
|
DatabaseFactory.updateStatusAndOrderNo(trade.id!!, TradeStatus.COMPLETED)
|
||||||
|
return@forEach
|
||||||
|
}
|
||||||
|
// 2. 수익 상태 먼저 체크 (현재가 조회)
|
||||||
|
val currentResult = tradeService.fetchChartData(trade.code, true).getOrNull()
|
||||||
|
val currentPrice = currentResult?.lastOrNull()?.stck_prpr?.toDouble() ?: 0.0
|
||||||
|
|
||||||
|
if (currentPrice > 0) {
|
||||||
|
|
||||||
|
// 매수가 역산 (목표가와 설정 수익률 기반)
|
||||||
|
val buyPrice = trade.targetPrice / (1 + trade.profitRate / 100.0)
|
||||||
|
val netProfitRate = ((currentPrice - buyPrice) / buyPrice * 100) - 0.3 // 수수료/세금 0.3% 차감
|
||||||
|
|
||||||
|
// 3. 매도 조건 판단 (최소 수익 0.1% 이상 확보 여부)
|
||||||
|
val isMinimumProfitSecured = netProfitRate >= 0.1
|
||||||
|
val isUrgent = LocalTime.now(ZoneId.of("Asia/Seoul")).isAfter(LocalTime.of(15, 20))
|
||||||
|
println("orderedPrice ${trade.orderedPrice}, currentPrice ${currentPrice} ")
|
||||||
|
// 수익이 났거나, 15:20분 이후 긴급 상황인 경우에만 진행
|
||||||
|
if (isMinimumProfitSecured || isUrgent) {
|
||||||
|
val reason = if (isUrgent) "시간 임박(탈출)" else "수익 확보(${String.format("%.2f", netProfitRate)}%)"
|
||||||
|
println("📢 [마감 정리 대상 포착] ${trade.name} | 사유: $reason")
|
||||||
|
|
||||||
|
// 4. [순서 변경] 수익 확인 후 기존 익절/손절 주문 취소 실행
|
||||||
|
// if (!trade.orderNo.isNullOrBlank()) {
|
||||||
|
// tradeService.cancelOrder(trade.orderNo, trade.code).onSuccess {
|
||||||
|
// println("✅ [취소 완료] ${trade.name} 기존 주문 취소됨")
|
||||||
|
// }.onFailure {
|
||||||
|
// println("ℹ️ [취소 건너뜀] ${trade.name}: ${it.message}")
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // 5. 즉시 시장가 매도 실행 (price를 "0"으로 전달)
|
||||||
|
// tradeService.postOrder(
|
||||||
|
// stockCode = trade.code,
|
||||||
|
// qty = trade.quantity.toString(),
|
||||||
|
// price = "0", // 시장가 주문
|
||||||
|
// isBuy = false
|
||||||
|
// ).onSuccess {
|
||||||
|
// DatabaseFactory.updateStatusAndOrderNo(trade.id!!, TradeStatus.COMPLETED)
|
||||||
|
// println("✨ [정리 완료] ${trade.name} 시장가 매도 성공")
|
||||||
|
// }.onFailure {
|
||||||
|
// println("❌ [매도 실패] ${trade.name}: ${it.message}")
|
||||||
|
// }
|
||||||
|
} else {
|
||||||
|
// 수익권이 아니면 그대로 유지 (기존 지정가 익절/손절 주문 유지)
|
||||||
|
// println("⏭️ [유지] ${trade.name}: 현재 수익권 아님 (${String.format("%.2f", netProfitRate)}%)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
println("⚠️ [마감 정리 중 에러] ${trade.name}: ${e.message}")
|
||||||
|
}
|
||||||
|
delay(200) // API 호출 간격 조절
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val MIN = 0.1
|
val MIN = 0.1
|
||||||
val MAX = 15.0
|
val MAX = 15.0
|
||||||
fun startAutoDiscoveryLoop(
|
fun startAutoDiscoveryLoop(
|
||||||
@ -41,9 +114,23 @@ object AutoTradingManager {
|
|||||||
|
|
||||||
while (discoveryJob?.isActive == true) {
|
while (discoveryJob?.isActive == true) {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
|
val now = LocalTime.now(ZoneId.of("Asia/Seoul"))
|
||||||
|
val isClosingTime = now.isAfter(LocalTime.of(15, 0)) && now.isBefore(LocalTime.of(15, 30))
|
||||||
|
|
||||||
|
if (isClosingTime) {
|
||||||
|
println("🕒 [장 마감 모드] 추가 매수를 중단하고 보유 종목 정리를 시작합니다.")
|
||||||
|
executeClosingLiquidation(tradeService) // 마감 정리 함수 호출
|
||||||
|
|
||||||
|
// 마감 중에는 1분 단위로 짧게 체크하며 대기
|
||||||
|
delay(60 * 1000)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
// 1. [체크] 현재 잔고 및 보유 종목 조회
|
// 1. [체크] 현재 잔고 및 보유 종목 조회
|
||||||
val balanceResult = tradeService.fetchIntegratedBalance().getOrNull()
|
val balanceResult = tradeService.fetchIntegratedBalance().getOrNull()
|
||||||
val myHoldings = balanceResult?.holdings?.filter { it.quantity.toInt() > 0 }?.map { it.code }?.toSet() ?: emptySet()
|
val myHoldings = balanceResult?.holdings?.filter { it.quantity.toInt() > 0 }?.map { it.code }?.toSet() ?: emptySet()
|
||||||
|
val pendingStocks = DatabaseFactory.findAllPendingBuyCodes()
|
||||||
val myCash = balanceResult?.deposit?.replace(",", "")?.toLongOrNull() ?: 0L
|
val myCash = balanceResult?.deposit?.replace(",", "")?.toLongOrNull() ?: 0L
|
||||||
|
|
||||||
println("💰 보유 현금: ${String.format("%,d", myCash)}원 | 보유 종목 수: ${myHoldings.size}")
|
println("💰 보유 현금: ${String.format("%,d", myCash)}원 | 보유 종목 수: ${myHoldings.size}")
|
||||||
@ -68,7 +155,7 @@ object AutoTradingManager {
|
|||||||
val candidates = (volList + riseList + amountList + volumeList + async { tradeService.fetchMarketRanking(RankingType.FOREIGNER_BUY, true).getOrDefault(emptyList()) + async { tradeService.fetchMarketRanking(RankingType.INSTITUTION_BUY, true).getOrDefault(emptyList()) }.await()}.await()).filter {stock ->
|
val candidates = (volList + riseList + amountList + volumeList + async { tradeService.fetchMarketRanking(RankingType.FOREIGNER_BUY, true).getOrDefault(emptyList()) + async { tradeService.fetchMarketRanking(RankingType.INSTITUTION_BUY, true).getOrDefault(emptyList()) }.await()}.await()).filter {stock ->
|
||||||
val rate = stock.prdy_ctrt.toDoubleOrNull() ?: 0.0
|
val rate = stock.prdy_ctrt.toDoubleOrNull() ?: 0.0
|
||||||
rate in MIN..MAX // 너무 과열되지 않은 주도주
|
rate in MIN..MAX // 너무 과열되지 않은 주도주
|
||||||
}.distinctBy { it.code }
|
}.filter { myHoldings.contains(it.code) == false && pendingStocks.contains(it.code) == false}.distinctBy { it.code }
|
||||||
|
|
||||||
println("🔎 1차 필터링 후보 ${candidates.size}개 (급등주 제외) 검증 시작...")
|
println("🔎 1차 필터링 후보 ${candidates.size}개 (급등주 제외) 검증 시작...")
|
||||||
|
|
||||||
@ -77,7 +164,12 @@ object AutoTradingManager {
|
|||||||
|
|
||||||
|
|
||||||
// [조건 1] 이미 보유한 종목 제외
|
// [조건 1] 이미 보유한 종목 제외
|
||||||
if (myHoldings.contains(stock.code)) return@forEach
|
|
||||||
|
var corpInfo = DartCodeManager.getCorpCode(stock.code)
|
||||||
|
if (corpInfo?.cName?.isNullOrEmpty() ?: true) {
|
||||||
|
println("⏭️ [제외] ${stock.name}: 법인명이 없음")
|
||||||
|
return@forEach
|
||||||
|
}
|
||||||
|
|
||||||
val currentPrice = stock.stck_prpr.replace(",", "").toDoubleOrNull() ?: 0.0
|
val currentPrice = stock.stck_prpr.replace(",", "").toDoubleOrNull() ?: 0.0
|
||||||
|
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import com.microsoft.playwright.Playwright
|
|||||||
import com.microsoft.playwright.BrowserType
|
import com.microsoft.playwright.BrowserType
|
||||||
import com.microsoft.playwright.Page
|
import com.microsoft.playwright.Page
|
||||||
import com.microsoft.playwright.options.LoadState
|
import com.microsoft.playwright.options.LoadState
|
||||||
|
import com.microsoft.playwright.options.WaitUntilState
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.awaitAll
|
import kotlinx.coroutines.awaitAll
|
||||||
import kotlinx.coroutines.coroutineScope
|
import kotlinx.coroutines.coroutineScope
|
||||||
@ -104,14 +105,15 @@ object DynamicNewsScraper {
|
|||||||
return try {
|
return try {
|
||||||
context.use { ctx ->
|
context.use { ctx ->
|
||||||
ctx.newPage().use { page ->
|
ctx.newPage().use { page ->
|
||||||
|
page.setDefaultNavigationTimeout(8000.0)
|
||||||
delay(Random.nextInt(2000).toLong())
|
delay(Random.nextInt(2000).toLong())
|
||||||
|
|
||||||
// 1. 리스너 설정 시 예외 처리 강화
|
// 1. 리스너 설정 시 예외 처리 강화
|
||||||
blockUnnecessaryResources(page)
|
blockUnnecessaryResources(page)
|
||||||
|
|
||||||
// 2. 타임아웃을 설정하여 무한 대기 방지
|
// 2. 타임아웃을 설정하여 무한 대기 방지
|
||||||
val options = Page.NavigateOptions().setTimeout(8000.0)
|
|
||||||
page.navigate(url, options)
|
page.navigate(url, Page.NavigateOptions().setWaitUntil(WaitUntilState.DOMCONTENTLOADED))
|
||||||
|
|
||||||
// 3. 페이지가 완전히 닫히기 전에 모든 대기 중인 이벤트를 해제하기 위해 LOAD 상태 대기
|
// 3. 페이지가 완전히 닫히기 전에 모든 대기 중인 이벤트를 해제하기 위해 LOAD 상태 대기
|
||||||
page.waitForLoadState(LoadState.LOAD)
|
page.waitForLoadState(LoadState.LOAD)
|
||||||
|
|||||||
@ -94,6 +94,15 @@ fun ActiveTradeRow(
|
|||||||
) {
|
) {
|
||||||
Text("취소", fontSize = 11.sp)
|
Text("취소", fontSize = 11.sp)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
Button(
|
||||||
|
onClick = { onCancelClick() },
|
||||||
|
contentPadding = PaddingValues(horizontal = 8.dp),
|
||||||
|
modifier = Modifier.height(28.dp),
|
||||||
|
colors = ButtonDefaults.buttonColors(backgroundColor = Color.LightGray)
|
||||||
|
) {
|
||||||
|
Text("취소", fontSize = 11.sp)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user