This commit is contained in:
lunaticbum 2026-02-24 13:14:11 +09:00
parent 55b82c23af
commit 80a9aa574d
5 changed files with 44 additions and 35 deletions

View File

@ -3,6 +3,8 @@ package network
import io.ktor.client.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import model.RankingStock
import service.AutoTradingManager
import java.io.ByteArrayInputStream
import java.io.File
import java.util.zip.ZipInputStream
@ -75,6 +77,7 @@ object DartCodeManager {
// 종목코드(stock_code)가 있는 상장사만 매핑에 추가
if (stockCode.isNotEmpty()) {
corpCodeMap[stockCode] = CorpInfo(corpCode, corpName, stockCode)
// AutoTradingManager.addToReanalysis(RankingStock(mksc_shrn_iscd = stockCode, hts_kor_isnm = corpName))
}
}
}

View File

@ -48,8 +48,8 @@ object AutoTradingManager {
private const val MAX_RISE_RATE = 21.0
private const val CYCLE_TIMEOUT = 30 * 60 * 1000L // 한 사이클 최대 10분
private const val WATCHDOG_CHECK_INTERVAL = 30 * 1000L // 30초마다 생존 확인
private const val STUCK_THRESHOLD = 5 * 60 * 1000L // 5분간 반응 없으면 'Stuck'으로 판단
private const val STUCK_THRESHOLD = 3 * 60 * 1000L // 5분간 반응 없으면 'Stuck'으로 판단
private const val ONE_STOCK_ALYSIS_TIME = 90000L
fun isRunning(): Boolean = discoveryJob?.isActive == true
private var remainingCandidates = mutableListOf<RankingStock>()
// private val processedCodes = mutableSetOf<String>() // 중복 처리 방지용 (선택 사항)
@ -82,32 +82,36 @@ object AutoTradingManager {
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 ->
// val pendingSells = DatabaseFactory.getAutoTradesByStatus(listOf(TradeStatus.SELLING, TradeStatus.EXPIRED))
// println("pendingSells >>> ${pendingSells.size}")
balance.holdings.forEach { holding ->
// 2. 실제로 잔고에 해당 종목이 있는지 확인 (안전장치)
// 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 && holding.availOrderCount.toInt() > 0 && holding.profitRate.toDouble() > 0.8) {
println("${holding.name} - 매수 : ${holding.avgPrice} - 현재 : ${holding.currentPrice} ")
if (holding != null && holding.quantity.toInt() > 0 && holding.availOrderCount.toInt() > 0) {
var final = MarketUtil.roundToTickSize(item.targetPrice)
println("🔄 [재주문] ${item.name} (${item.code}) ${item.orderedPrice} ${final} 전날 미체결 매도 건 재주문 시도")
// 3. 기존 목표가(targetPrice)로 다시 매도 주문 전송
var targetPrice = holding.currentPrice.toDouble()
targetPrice = MarketUtil.roundToTickSize(targetPrice + MarketUtil.getTickSize(targetPrice))
println("🔄 [재주문] ${holding.name} (${holding.code}) 매도 목표 ${targetPrice} 미체결 매도 건 재주문 시도")
tradeService.postOrder(
stockCode = item.code,
qty = min(item.quantity,holding.availOrderCount.toInt()).toString(),
price = final.toLong().toString(),
stockCode = holding.code,
qty = holding.availOrderCount,
price = targetPrice.toInt().toString(),
isBuy = false
).onSuccess { newOrderNo ->
// 4. 새로운 주문번호로 DB 업데이트 및 상태를 SELLING으로 유지
DatabaseFactory.updateStatusAndOrderNo(item.id!!, TradeStatus.SELLING, newOrderNo)
println("✅ [재주문 완료] ${item.name}: $newOrderNo")
// DatabaseFactory.updateStatusAndOrderNo(item.id!!, TradeStatus.SELLING, newOrderNo)
println("✅ [재주문 완료] ${holding.name}: $newOrderNo")
}.onFailure {
println("❌ [재주문 실패] ${item.name}: ${it.message}")
println("❌ [재주문 실패] ${holding.name}: ${it.message}")
}
} else {
// 잔고에 없다면 이미 매도된 것으로 간주하고 완료 처리
DatabaseFactory.updateStatusAndOrderNo(item.id!!, TradeStatus.COMPLETED)
// DatabaseFactory.updateStatusAndOrderNo(item.id!!, TradeStatus.COMPLETED)
}
delay(200) // API 호출 부하 방지
}
@ -127,11 +131,11 @@ object AutoTradingManager {
// [프로세스 1] 장 마감 및 잔고 체크
val now = LocalTime.now(ZoneId.of("Asia/Seoul"))
//&& now.isBefore(LocalTime.of(15, 30))
if (now.isAfter(LocalTime.of(15, 15)) ) {
if (now.isAfter(LocalTime.of(15, 20)) ) {
executeClosingLiquidation(tradeService)
return@withTimeout
}
// addToReanalysis(RankingStock(mksc_shrn_iscd = ,hts_kor_isnm = ))
val balance = tradeService.fetchIntegratedBalance().getOrNull()
balance?.let { resumePendingSellOrders(tradeService,it) }
@ -158,7 +162,7 @@ object AutoTradingManager {
.toMutableList()
if (reanalysisList.isNotEmpty()) {
candidates.addAll(reanalysisList)
candidates.addAll(reanalysisList.asReversed())
}
reanalysisList.clear()
remainingCandidates.addAll(candidates.filter { it.code !in myHoldings && it.code !in pendingStocks }.distinctBy { it.code })
@ -186,26 +190,25 @@ object AutoTradingManager {
iterator.remove()
}
println("남은 후보군 개수 : ${totalCount}")
delay(250)
delay(100)
}
println("⏱️ [Cycle End] ${LocalTime.now()}")
}
} catch (e: TimeoutCancellationException) {
println("⏳ [Cycle Timeout] 사이클이 너무 길어져 초기화 후 재시작합니다.")
} catch (e: Exception) {
println("⚠️ [Loop Error] ${e.message}")
delay(3000)
delay(1500)
}
waitForNextCycle(0.3)
waitForNextCycle(0.2)
}
}
}
fun addToReanalysis(stock: RankingStock) {
val count = retryCountMap.getOrDefault(stock.code, 0)
if (count < 2) { // 최대 2회까지만 재시도하여 무한 루프 방지
if (count < 10) { // 최대 2회까지만 재시도하여 무한 루프 방지
retryCountMap[stock.code] = count + 1
reanalysisList.add(stock)
println("📝 [Memory] ${stock.name} 관망 판정 -> 차기 루프 재분석 리스트 등록")
@ -218,7 +221,7 @@ object AutoTradingManager {
val maxPrice = KisSession.config.getValues(ConfigIndex.MAX_PRICE_INDEX)
val minPrice = KisSession.config.getValues(ConfigIndex.MIN_PRICE_INDEX)
// 개별 종목 분석은 최대 2분으로 제한
withTimeout(120000L) {
withTimeout(ONE_STOCK_ALYSIS_TIME) {
val corpInfo = DartCodeManager.getCorpCode(stock.code)
if (corpInfo?.cName.isNullOrEmpty()) {
print("-> 기업명을 못찾아서 제외 | ")
@ -359,8 +362,8 @@ object FinancialAnalyzer {
val isDebtSafe = fs.debtRatio < 200.0 // 부채비율 200% 미만
val isLiquiditySafe = fs.quickRatio > 80.0 // 당좌비율 80% 이상
val isNotDeficit = fs.isNetIncomePositive // 당기순이익은 일단 흑자여야 함
return isDebtSafe && isLiquiditySafe && isNotDeficit
//&& isNotDeficit
return isDebtSafe && isLiquiditySafe
}
/**

View File

@ -384,15 +384,13 @@ fun DashboardScreen() {
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")
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)}) = ${(localText.toDouble() * KisSession.config.getValues(ConfigIndex.PROFIT_INDEX)) + KisSession.config.getValues(
ConfigIndex.TAX_INDEX)}"
} else {
getRemaining(configKey.label,common) + ": -${localText} 호가 매수}"
@ -401,7 +399,7 @@ fun DashboardScreen() {
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)}) = ${(localText.toDouble() * KisSession.config.getValues(ConfigIndex.PROFIT_INDEX)) + KisSession.config.getValues(
ConfigIndex.TAX_INDEX)} "
} else {
getRemaining(configKey.label,common) + ": -${localText} 호가 매수}"

View File

@ -266,8 +266,8 @@ fun IntegratedOrderSection(
fun resultCheck(completeTradingDecision :TradingDecision) {
val weights = mapOf(
"short" to 0.3, // 초단기 점수가 낮아도 전체에 미치는 영향 감소
"profit" to 0.3,
"short" to 0.2, // 초단기 점수가 낮아도 전체에 미치는 영향 감소
"profit" to 0.4,
"safe" to 0.4 // 중장기 점수 비중 강화
)
@ -322,11 +322,15 @@ fun IntegratedOrderSection(
resultCheck(completeTradingDecision)
}
"SELL" -> println("[$stockCode] 매도: ${completeTradingDecision?.reason}")
else -> {
"HOLD" -> {
append = 0.0
resultCheck(completeTradingDecision)
println("[$stockCode] 관망 유지 resultCheck: ${completeTradingDecision?.reason}")
}
else -> {
append = 0.0
println("[$stockCode] ${completeTradingDecision?.decision} resultCheck: ${completeTradingDecision?.reason}")
}
}
}
}

View File

@ -40,4 +40,5 @@ object MarketUtil {
return Math.round(price / tick) * tick
}
}