....
This commit is contained in:
parent
55b82c23af
commit
80a9aa574d
@ -3,6 +3,8 @@ package network
|
|||||||
import io.ktor.client.*
|
import io.ktor.client.*
|
||||||
import io.ktor.client.request.*
|
import io.ktor.client.request.*
|
||||||
import io.ktor.client.statement.*
|
import io.ktor.client.statement.*
|
||||||
|
import model.RankingStock
|
||||||
|
import service.AutoTradingManager
|
||||||
import java.io.ByteArrayInputStream
|
import java.io.ByteArrayInputStream
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.zip.ZipInputStream
|
import java.util.zip.ZipInputStream
|
||||||
@ -75,6 +77,7 @@ object DartCodeManager {
|
|||||||
// 종목코드(stock_code)가 있는 상장사만 매핑에 추가
|
// 종목코드(stock_code)가 있는 상장사만 매핑에 추가
|
||||||
if (stockCode.isNotEmpty()) {
|
if (stockCode.isNotEmpty()) {
|
||||||
corpCodeMap[stockCode] = CorpInfo(corpCode, corpName, stockCode)
|
corpCodeMap[stockCode] = CorpInfo(corpCode, corpName, stockCode)
|
||||||
|
// AutoTradingManager.addToReanalysis(RankingStock(mksc_shrn_iscd = stockCode, hts_kor_isnm = corpName))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -48,8 +48,8 @@ object AutoTradingManager {
|
|||||||
private const val MAX_RISE_RATE = 21.0
|
private const val MAX_RISE_RATE = 21.0
|
||||||
private const val CYCLE_TIMEOUT = 30 * 60 * 1000L // 한 사이클 최대 10분
|
private const val CYCLE_TIMEOUT = 30 * 60 * 1000L // 한 사이클 최대 10분
|
||||||
private const val WATCHDOG_CHECK_INTERVAL = 30 * 1000L // 30초마다 생존 확인
|
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
|
fun isRunning(): Boolean = discoveryJob?.isActive == true
|
||||||
private var remainingCandidates = mutableListOf<RankingStock>()
|
private var remainingCandidates = mutableListOf<RankingStock>()
|
||||||
// private val processedCodes = mutableSetOf<String>() // 중복 처리 방지용 (선택 사항)
|
// private val processedCodes = mutableSetOf<String>() // 중복 처리 방지용 (선택 사항)
|
||||||
@ -82,32 +82,36 @@ object AutoTradingManager {
|
|||||||
suspend fun resumePendingSellOrders(tradeService: KisTradeService,balance : UnifiedBalance) {
|
suspend fun resumePendingSellOrders(tradeService: KisTradeService,balance : UnifiedBalance) {
|
||||||
// 1. DB에서 매도 중(SELLING)이거나 만료(EXPIRED)된 매도 건을 가져옵니다.
|
// 1. DB에서 매도 중(SELLING)이거나 만료(EXPIRED)된 매도 건을 가져옵니다.
|
||||||
println("resumePendingSellOrders")
|
println("resumePendingSellOrders")
|
||||||
val pendingSells = DatabaseFactory.getAutoTradesByStatus(listOf(TradeStatus.SELLING, TradeStatus.EXPIRED))
|
// val pendingSells = DatabaseFactory.getAutoTradesByStatus(listOf(TradeStatus.SELLING, TradeStatus.EXPIRED))
|
||||||
println("pendingSells >>> ${pendingSells.size}")
|
// println("pendingSells >>> ${pendingSells.size}")
|
||||||
pendingSells.forEach { item ->
|
balance.holdings.forEach { holding ->
|
||||||
// 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 && 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)로 다시 매도 주문 전송
|
// 3. 기존 목표가(targetPrice)로 다시 매도 주문 전송
|
||||||
|
var targetPrice = holding.currentPrice.toDouble()
|
||||||
|
targetPrice = MarketUtil.roundToTickSize(targetPrice + MarketUtil.getTickSize(targetPrice))
|
||||||
|
|
||||||
|
println("🔄 [재주문] ${holding.name} (${holding.code}) 매도 목표 ${targetPrice} 미체결 매도 건 재주문 시도")
|
||||||
tradeService.postOrder(
|
tradeService.postOrder(
|
||||||
stockCode = item.code,
|
stockCode = holding.code,
|
||||||
qty = min(item.quantity,holding.availOrderCount.toInt()).toString(),
|
qty = holding.availOrderCount,
|
||||||
price = final.toLong().toString(),
|
price = targetPrice.toInt().toString(),
|
||||||
isBuy = false
|
isBuy = false
|
||||||
).onSuccess { newOrderNo ->
|
).onSuccess { newOrderNo ->
|
||||||
// 4. 새로운 주문번호로 DB 업데이트 및 상태를 SELLING으로 유지
|
// 4. 새로운 주문번호로 DB 업데이트 및 상태를 SELLING으로 유지
|
||||||
DatabaseFactory.updateStatusAndOrderNo(item.id!!, TradeStatus.SELLING, newOrderNo)
|
// DatabaseFactory.updateStatusAndOrderNo(item.id!!, TradeStatus.SELLING, newOrderNo)
|
||||||
println("✅ [재주문 완료] ${item.name}: $newOrderNo")
|
println("✅ [재주문 완료] ${holding.name}: $newOrderNo")
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
println("❌ [재주문 실패] ${item.name}: ${it.message}")
|
println("❌ [재주문 실패] ${holding.name}: ${it.message}")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 잔고에 없다면 이미 매도된 것으로 간주하고 완료 처리
|
// 잔고에 없다면 이미 매도된 것으로 간주하고 완료 처리
|
||||||
DatabaseFactory.updateStatusAndOrderNo(item.id!!, TradeStatus.COMPLETED)
|
// DatabaseFactory.updateStatusAndOrderNo(item.id!!, TradeStatus.COMPLETED)
|
||||||
}
|
}
|
||||||
delay(200) // API 호출 부하 방지
|
delay(200) // API 호출 부하 방지
|
||||||
}
|
}
|
||||||
@ -127,11 +131,11 @@ object AutoTradingManager {
|
|||||||
// [프로세스 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, 15)) ) {
|
if (now.isAfter(LocalTime.of(15, 20)) ) {
|
||||||
executeClosingLiquidation(tradeService)
|
executeClosingLiquidation(tradeService)
|
||||||
return@withTimeout
|
return@withTimeout
|
||||||
}
|
}
|
||||||
|
// addToReanalysis(RankingStock(mksc_shrn_iscd = ,hts_kor_isnm = ))
|
||||||
val balance = tradeService.fetchIntegratedBalance().getOrNull()
|
val balance = tradeService.fetchIntegratedBalance().getOrNull()
|
||||||
|
|
||||||
balance?.let { resumePendingSellOrders(tradeService,it) }
|
balance?.let { resumePendingSellOrders(tradeService,it) }
|
||||||
@ -158,7 +162,7 @@ object AutoTradingManager {
|
|||||||
.toMutableList()
|
.toMutableList()
|
||||||
|
|
||||||
if (reanalysisList.isNotEmpty()) {
|
if (reanalysisList.isNotEmpty()) {
|
||||||
candidates.addAll(reanalysisList)
|
candidates.addAll(reanalysisList.asReversed())
|
||||||
}
|
}
|
||||||
reanalysisList.clear()
|
reanalysisList.clear()
|
||||||
remainingCandidates.addAll(candidates.filter { it.code !in myHoldings && it.code !in pendingStocks }.distinctBy { it.code })
|
remainingCandidates.addAll(candidates.filter { it.code !in myHoldings && it.code !in pendingStocks }.distinctBy { it.code })
|
||||||
@ -186,26 +190,25 @@ object AutoTradingManager {
|
|||||||
iterator.remove()
|
iterator.remove()
|
||||||
}
|
}
|
||||||
println("남은 후보군 개수 : ${totalCount}")
|
println("남은 후보군 개수 : ${totalCount}")
|
||||||
delay(250)
|
delay(100)
|
||||||
}
|
}
|
||||||
|
|
||||||
println("⏱️ [Cycle End] ${LocalTime.now()}")
|
println("⏱️ [Cycle End] ${LocalTime.now()}")
|
||||||
}
|
}
|
||||||
} catch (e: TimeoutCancellationException) {
|
} catch (e: TimeoutCancellationException) {
|
||||||
println("⏳ [Cycle Timeout] 사이클이 너무 길어져 초기화 후 재시작합니다.")
|
println("⏳ [Cycle Timeout] 사이클이 너무 길어져 초기화 후 재시작합니다.")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
println("⚠️ [Loop Error] ${e.message}")
|
println("⚠️ [Loop Error] ${e.message}")
|
||||||
delay(3000)
|
delay(1500)
|
||||||
}
|
}
|
||||||
|
|
||||||
waitForNextCycle(0.3)
|
waitForNextCycle(0.2)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addToReanalysis(stock: RankingStock) {
|
fun addToReanalysis(stock: RankingStock) {
|
||||||
val count = retryCountMap.getOrDefault(stock.code, 0)
|
val count = retryCountMap.getOrDefault(stock.code, 0)
|
||||||
if (count < 2) { // 최대 2회까지만 재시도하여 무한 루프 방지
|
if (count < 10) { // 최대 2회까지만 재시도하여 무한 루프 방지
|
||||||
retryCountMap[stock.code] = count + 1
|
retryCountMap[stock.code] = count + 1
|
||||||
reanalysisList.add(stock)
|
reanalysisList.add(stock)
|
||||||
println("📝 [Memory] ${stock.name} 관망 판정 -> 차기 루프 재분석 리스트 등록")
|
println("📝 [Memory] ${stock.name} 관망 판정 -> 차기 루프 재분석 리스트 등록")
|
||||||
@ -218,7 +221,7 @@ object AutoTradingManager {
|
|||||||
val maxPrice = KisSession.config.getValues(ConfigIndex.MAX_PRICE_INDEX)
|
val maxPrice = KisSession.config.getValues(ConfigIndex.MAX_PRICE_INDEX)
|
||||||
val minPrice = KisSession.config.getValues(ConfigIndex.MIN_PRICE_INDEX)
|
val minPrice = KisSession.config.getValues(ConfigIndex.MIN_PRICE_INDEX)
|
||||||
// 개별 종목 분석은 최대 2분으로 제한
|
// 개별 종목 분석은 최대 2분으로 제한
|
||||||
withTimeout(120000L) {
|
withTimeout(ONE_STOCK_ALYSIS_TIME) {
|
||||||
val corpInfo = DartCodeManager.getCorpCode(stock.code)
|
val corpInfo = DartCodeManager.getCorpCode(stock.code)
|
||||||
if (corpInfo?.cName.isNullOrEmpty()) {
|
if (corpInfo?.cName.isNullOrEmpty()) {
|
||||||
print("-> 기업명을 못찾아서 제외 | ")
|
print("-> 기업명을 못찾아서 제외 | ")
|
||||||
@ -359,8 +362,8 @@ object FinancialAnalyzer {
|
|||||||
val isDebtSafe = fs.debtRatio < 200.0 // 부채비율 200% 미만
|
val isDebtSafe = fs.debtRatio < 200.0 // 부채비율 200% 미만
|
||||||
val isLiquiditySafe = fs.quickRatio > 80.0 // 당좌비율 80% 이상
|
val isLiquiditySafe = fs.quickRatio > 80.0 // 당좌비율 80% 이상
|
||||||
val isNotDeficit = fs.isNetIncomePositive // 당기순이익은 일단 흑자여야 함
|
val isNotDeficit = fs.isNetIncomePositive // 당기순이익은 일단 흑자여야 함
|
||||||
|
//&& isNotDeficit
|
||||||
return isDebtSafe && isLiquiditySafe && isNotDeficit
|
return isDebtSafe && isLiquiditySafe
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -384,15 +384,13 @@ fun DashboardScreen() {
|
|||||||
|
|
||||||
val saveAction = {
|
val saveAction = {
|
||||||
var newValue = localText.toDoubleOrNull() ?: 0.0
|
var newValue = localText.toDoubleOrNull() ?: 0.0
|
||||||
// if (configKey.name.contains("PROFIT")) {
|
//
|
||||||
// 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")) {
|
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(ConfigIndex.PROFIT_INDEX)) + KisSession.config.getValues(
|
||||||
ConfigIndex.TAX_INDEX)}"
|
ConfigIndex.TAX_INDEX)}"
|
||||||
} else {
|
} else {
|
||||||
getRemaining(configKey.label,common) + ": -${localText} 호가 매수}"
|
getRemaining(configKey.label,common) + ": -${localText} 호가 매수}"
|
||||||
@ -401,7 +399,7 @@ fun DashboardScreen() {
|
|||||||
|
|
||||||
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(ConfigIndex.PROFIT_INDEX)) + KisSession.config.getValues(
|
||||||
ConfigIndex.TAX_INDEX)} "
|
ConfigIndex.TAX_INDEX)} "
|
||||||
} else {
|
} else {
|
||||||
getRemaining(configKey.label,common) + ": -${localText} 호가 매수}"
|
getRemaining(configKey.label,common) + ": -${localText} 호가 매수}"
|
||||||
|
|||||||
@ -266,8 +266,8 @@ fun IntegratedOrderSection(
|
|||||||
|
|
||||||
fun resultCheck(completeTradingDecision :TradingDecision) {
|
fun resultCheck(completeTradingDecision :TradingDecision) {
|
||||||
val weights = mapOf(
|
val weights = mapOf(
|
||||||
"short" to 0.3, // 초단기 점수가 낮아도 전체에 미치는 영향 감소
|
"short" to 0.2, // 초단기 점수가 낮아도 전체에 미치는 영향 감소
|
||||||
"profit" to 0.3,
|
"profit" to 0.4,
|
||||||
"safe" to 0.4 // 중장기 점수 비중 강화
|
"safe" to 0.4 // 중장기 점수 비중 강화
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -322,11 +322,15 @@ fun IntegratedOrderSection(
|
|||||||
resultCheck(completeTradingDecision)
|
resultCheck(completeTradingDecision)
|
||||||
}
|
}
|
||||||
"SELL" -> println("[$stockCode] 매도: ${completeTradingDecision?.reason}")
|
"SELL" -> println("[$stockCode] 매도: ${completeTradingDecision?.reason}")
|
||||||
else -> {
|
"HOLD" -> {
|
||||||
append = 0.0
|
append = 0.0
|
||||||
resultCheck(completeTradingDecision)
|
resultCheck(completeTradingDecision)
|
||||||
println("[$stockCode] 관망 유지 resultCheck: ${completeTradingDecision?.reason}")
|
println("[$stockCode] 관망 유지 resultCheck: ${completeTradingDecision?.reason}")
|
||||||
}
|
}
|
||||||
|
else -> {
|
||||||
|
append = 0.0
|
||||||
|
println("[$stockCode] ${completeTradingDecision?.decision} resultCheck: ${completeTradingDecision?.reason}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -40,4 +40,5 @@ object MarketUtil {
|
|||||||
return Math.round(price / tick) * tick
|
return Math.round(price / tick) * tick
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user