.....
This commit is contained in:
@@ -13,16 +13,17 @@ import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import kotlinx.serialization.Serializable
|
||||
import model.CandleData
|
||||
import model.MAX_BUDGET
|
||||
import model.MAX_PRICE
|
||||
import model.MIN_PRICE
|
||||
import model.ConfigIndex
|
||||
import model.KisSession
|
||||
import model.RankingStock
|
||||
import model.RankingType
|
||||
import network.DartCodeManager
|
||||
import network.FinancialMapper
|
||||
import network.FinancialStatement
|
||||
import network.KisTradeService
|
||||
import util.MarketUtil
|
||||
import java.time.LocalDateTime
|
||||
import java.time.LocalTime
|
||||
import java.time.ZoneId
|
||||
@@ -76,6 +77,40 @@ object AutoTradingManager {
|
||||
runDiscoveryLoop(tradeService, callback)
|
||||
}
|
||||
|
||||
|
||||
suspend fun resumePendingSellOrders(tradeService: KisTradeService) {
|
||||
// 1. DB에서 매도 중(SELLING)이거나 만료(EXPIRED)된 매도 건을 가져옵니다.
|
||||
val pendingSells = DatabaseFactory.getAutoTradesByStatus(listOf(TradeStatus.SELLING, TradeStatus.EXPIRED))
|
||||
|
||||
pendingSells.forEach { item ->
|
||||
// 2. 실제로 잔고에 해당 종목이 있는지 확인 (안전장치)
|
||||
val balance = tradeService.fetchIntegratedBalance().getOrNull()
|
||||
val holding = balance?.holdings?.find { it.code == item.code }
|
||||
|
||||
if (holding != null && holding.quantity.toInt() > 0) {
|
||||
var final = MarketUtil.roundToTickSize(item.targetPrice)
|
||||
println("🔄 [재주문] ${item.name} (${item.code}) ${item.orderedPrice} ${final} 전날 미체결 매도 건 재주문 시도")
|
||||
// 3. 기존 목표가(targetPrice)로 다시 매도 주문 전송
|
||||
tradeService.postOrder(
|
||||
stockCode = item.code,
|
||||
qty = item.quantity.toString(),
|
||||
price = final.toLong().toString(),
|
||||
isBuy = false
|
||||
).onSuccess { newOrderNo ->
|
||||
// 4. 새로운 주문번호로 DB 업데이트 및 상태를 SELLING으로 유지
|
||||
DatabaseFactory.updateStatusAndOrderNo(item.id!!, TradeStatus.SELLING, newOrderNo)
|
||||
println("✅ [재주문 완료] ${item.name}: $newOrderNo")
|
||||
}.onFailure {
|
||||
println("❌ [재주문 실패] ${item.name}: ${it.message}")
|
||||
}
|
||||
} else {
|
||||
// 잔고에 없다면 이미 매도된 것으로 간주하고 완료 처리
|
||||
DatabaseFactory.updateStatusAndOrderNo(item.id!!, TradeStatus.COMPLETED)
|
||||
}
|
||||
delay(200) // API 호출 부하 방지
|
||||
}
|
||||
}
|
||||
|
||||
private fun runDiscoveryLoop(tradeService: KisTradeService, callback: TradingDecisionCallback) {
|
||||
discoveryJob = scope.launch {
|
||||
println("🚀 [AutoTrading] 발굴 루프 시작: ${LocalDateTime.now()}")
|
||||
@@ -91,7 +126,7 @@ object AutoTradingManager {
|
||||
val now = LocalTime.now(ZoneId.of("Asia/Seoul"))
|
||||
//&& now.isBefore(LocalTime.of(15, 30))
|
||||
if (now.isAfter(LocalTime.of(15, 30)) ) {
|
||||
executeClosingLiquidation(tradeService)
|
||||
// executeClosingLiquidation(tradeService)
|
||||
return@withTimeout
|
||||
}
|
||||
|
||||
@@ -136,7 +171,7 @@ object AutoTradingManager {
|
||||
iterator.remove()
|
||||
}
|
||||
println("남은 후보군 개수 : ${totalCount}")
|
||||
delay(150)
|
||||
delay(250)
|
||||
}
|
||||
|
||||
println("⏱️ [Cycle End] ${LocalTime.now()}")
|
||||
@@ -145,10 +180,10 @@ object AutoTradingManager {
|
||||
println("⏳ [Cycle Timeout] 사이클이 너무 길어져 초기화 후 재시작합니다.")
|
||||
} catch (e: Exception) {
|
||||
println("⚠️ [Loop Error] ${e.message}")
|
||||
delay(5000)
|
||||
delay(3000)
|
||||
}
|
||||
|
||||
waitForNextCycle(0.5)
|
||||
waitForNextCycle(0.3)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -164,6 +199,9 @@ object AutoTradingManager {
|
||||
|
||||
private suspend fun processSingleStock(stock: RankingStock, myCash: Long, tradeService: KisTradeService, callback: TradingDecisionCallback) {
|
||||
try {
|
||||
val maxBudget = KisSession.config.getValues(ConfigIndex.MAX_BUDGET_INDEX)
|
||||
val maxPrice = KisSession.config.getValues(ConfigIndex.MAX_PRICE_INDEX)
|
||||
val minPrice = KisSession.config.getValues(ConfigIndex.MIN_PRICE_INDEX)
|
||||
// 개별 종목 분석은 최대 2분으로 제한
|
||||
withTimeout(120000L) {
|
||||
val corpInfo = DartCodeManager.getCorpCode(stock.code)
|
||||
@@ -185,8 +223,8 @@ object AutoTradingManager {
|
||||
}
|
||||
val currentPrice = today.stck_prpr.toDouble()
|
||||
|
||||
if (currentPrice > myCash || currentPrice > MAX_BUDGET || currentPrice > MAX_PRICE || currentPrice < MIN_PRICE) {
|
||||
print("-> 가격 정책으로 제외 [1주:${currentPrice}, 자산:${myCash}, 최소 기준:${MIN_PRICE}, 최대 기준:${MAX_PRICE}] | ")
|
||||
if (currentPrice > myCash || currentPrice > maxBudget || currentPrice > maxPrice || currentPrice < minPrice) {
|
||||
print("-> 가격 정책으로 제외 [1주:${currentPrice}, 자산:${myCash}, 최소 기준:${minPrice}, 최대 기준:${maxPrice}] | ")
|
||||
return@withTimeout
|
||||
}
|
||||
|
||||
@@ -251,7 +289,7 @@ object AutoTradingManager {
|
||||
while (System.currentTimeMillis() < endWait && isRunning()) {
|
||||
lastTickTime.set(System.currentTimeMillis()) // 대기 중에도 Watchdog에 생존 신고
|
||||
println("💤 대기 모드 상태 확인...")
|
||||
delay(5000)
|
||||
delay(1000)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -382,13 +420,41 @@ data class InvestmentScores(
|
||||
""".trimIndent()
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class TechnicalAnalyzer {
|
||||
var monthly: List<CandleData> = emptyList()
|
||||
var weekly: List<CandleData> = emptyList()
|
||||
var daily: List<CandleData> = emptyList()
|
||||
var min30: List<CandleData> = emptyList()
|
||||
|
||||
fun isOverheatedStock(): Boolean {
|
||||
if (min30.size < 20 || daily.size < 20) return false
|
||||
|
||||
val currentPrice = min30.last().stck_prpr.toDouble()
|
||||
|
||||
// 1. 일봉 기준 이격도 체크 (20일 이평선 대비)
|
||||
val ma20Daily = daily.takeLast(20).map { it.stck_prpr.toDouble() }.average()
|
||||
val disparityDaily = (currentPrice / ma20Daily) * 100
|
||||
// 20일 평균선보다 25% 이상 떠 있다면 매우 위험 (과열)
|
||||
if (disparityDaily > 125.0) return true
|
||||
|
||||
// 2. 분봉(30분봉) 기준 단기 급등 체크
|
||||
val startPrice30 = min30.first().stck_oprc.toDouble()
|
||||
val riseRate30 = ((currentPrice - startPrice30) / startPrice30) * 100
|
||||
// 최근 30분봉 데이터(약 수 시간) 내에서 15% 이상 급등했다면 추격 매수 위험
|
||||
if (riseRate30 > 15.0) return true
|
||||
|
||||
// 3. 비정상적 거래량 폭발 (매집봉 없는 단기 펌핑)
|
||||
val avgVol = min30.dropLast(3).map { it.cntg_vol.toDouble() }.average()
|
||||
val recentVol = min30.last().cntg_vol.toDouble()
|
||||
// 평균 거래량보다 10배 이상 갑자기 터진 거래량은 세력의 털기(Exhaustion)일 수 있음
|
||||
if (recentVol > avgVol * 10) return true
|
||||
|
||||
// 4. 볼린저 밴드 상단 이탈 강도
|
||||
// ScalpingAnalyzer의 bollingerBands를 활용해 bbUpper보다 크게 이탈했는지 확인
|
||||
return false
|
||||
}
|
||||
|
||||
fun calculateScores(
|
||||
financialScore: Int // 재무제표 점수 (성장률 등 기반)
|
||||
|
||||
Reference in New Issue
Block a user