Compare commits

..

No commits in common. "488e4e72b3fa186041aed36f58cde4dccb6064ed" and "b1d334a6cdd1edde1f2972a02d68943dd212ed7c" have entirely different histories.

11 changed files with 125 additions and 448 deletions

View File

@ -88,12 +88,7 @@ fun getLlamaBinPath(): String {
}
// Windows NUC
os.contains("win") -> {
if (KisSession.tradeConfig.isLowPerformanceMonitoring) {
"$basePath/win-x64/llama-server.exe"
}
else {
"$basePath/win-x64-n/llama-server.exe"
}
"$basePath/win-x64-n/llama-server.exe"
}
else -> "$basePath/llama-server"
}
@ -116,7 +111,6 @@ private var isAppStarted = false
fun main() = application {
if (!isAppStarted) {
initLogger(DETAILLOG)
KisSession.tradeConfig = KisSession.loadTradeConfig()
try {
val (port1, port2) = PortFinder.findAvailablePortPair(18080, false)
if (port1 > 18000 && port2 > port1) {

View File

@ -5,7 +5,6 @@ import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import kotlinx.serialization.Serializable
import model.AppConfig
import model.KisSession
import model.TradingDecision
import network.NewsService
import org.jetbrains.exposed.sql.*
@ -491,18 +490,7 @@ object TradingLogStore {
decision = decision.decision ?: "HOLD",
confidence = decision.confidence,
reason = decision.reason ?: ""
).apply {
if (KisSession.tradeConfig.useTagsShare.contains(this.decision) && KisSession.tradeConfig.useLogKeywordsShare.any {
reason.contains(
it
)
}) {
CoroutineScope(Dispatchers.Default).launch {
NewsService.sendTelegramMessage("${this@apply.decision}$stockName ${reason}")
}
}
}
)
))
}
}
@ -516,25 +504,11 @@ object TradingLogStore {
decision = decision,
confidence = 100.0,
reason = log
).apply {
if (KisSession.tradeConfig.useTagsShare.contains(decision) && KisSession.tradeConfig.useLogKeywordsShare.any {
log.contains(
it
)
}) {
CoroutineScope(Dispatchers.Default).launch {
println("CALLED sendTelegramMessage")
NewsService.sendTelegramMessage("${this@apply.decision}$stockName ${log}")
}
}
}
)
)
}
}
fun addLog(tradingDecision: TradingDecision, decision: String, log: String) {
synchronized(this) {
if (decisionLogs.size > 1000) decisionLogs.removeAt(0)
@ -547,11 +521,11 @@ object TradingLogStore {
reason = log
).apply {
CoroutineScope(Dispatchers.Default).launch {
if (((tradingDecision.investmentGrade?.name?.length ?: 0) > 0 && KisSession.tradeConfig.useGradeShare.any {
tradingDecision.investmentGrade?.name?.contains(
it
) ?: false
})) {
println("CALLED sendTelegramMessage -1")
if (decision.contains("WATCH") || ((tradingDecision.investmentGrade?.ordinal
?: 0) < 2)
) {
println("CALLED sendTelegramMessage OK")
NewsService.sendTelegramMessage("${this@apply.decision} ${tradingDecision.stockName}[${tradingDecision.currentPrice}] ${log}")
}
}
@ -560,32 +534,6 @@ object TradingLogStore {
}
}
fun addWatchLog(tradingDecision: TradingDecision, decision: String, log: String) {
synchronized(this) {
if (decisionLogs.size > 1000) decisionLogs.removeAt(0)
decisionLogs.add(
LogEntry(
time = LocalTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss")),
stockName = "${tradingDecision.stockName}[${tradingDecision.currentPrice}]",
decision = decision,
confidence = tradingDecision.confidence,
reason = log
).apply {
CoroutineScope(Dispatchers.Default).launch {
if (KisSession.tradeConfig.useTagsShare.contains(decision) && KisSession.tradeConfig.useLogKeywordsShare.any {
log.contains(
it
)
}) {
NewsService.sendTelegramMessage("${this@apply.decision} ${tradingDecision.stockName}[${tradingDecision.currentPrice}] ${log}")
}
}
}
)
}
}
fun addAnalyzer(name : String, code : String, log: String, positive : Boolean = false) {
synchronized(this) {
if (decisionLogs.size > 1000) decisionLogs.removeAt(0)
@ -597,17 +545,7 @@ object TradingLogStore {
decision = if (positive) "ANALYZER" else "PASS",
confidence = 100.0,
reason = log
).apply {
if (KisSession.tradeConfig.useTagsShare.contains(decision) && KisSession.tradeConfig.useLogKeywordsShare.any {
log.contains(
it
)
}) {
CoroutineScope(Dispatchers.Default).launch {
NewsService.sendTelegramMessage("${this@apply.decision}$name[$code] ${log}")
}
}
}
)
)
}
}
@ -624,19 +562,9 @@ object TradingLogStore {
confidence = 100.0,
reason = log
).apply {
if (KisSession.tradeConfig.useTagsShare.contains(decision) && KisSession.tradeConfig.useLogKeywordsShare.any {
log.contains(
it
)
}) {
var current = System.currentTimeMillis()
var sendable = noticeFilter.filter { it.key.equals(code, true) && ((current - it.value) > 1000 * 60 * 30L)}.isNotEmpty()
if (sendable) {
CoroutineScope(Dispatchers.Default).launch {
NewsService.sendTelegramMessage("${this@apply.decision}$name[$code] ${log}")
}}
noticeFilter[code] = current
CoroutineScope(Dispatchers.Default).launch {
println("CALLED sendTelegramMessage")
NewsService.sendTelegramMessage("${this@apply.decision}$name[$code] ${log}")
}
}
)
@ -644,8 +572,6 @@ object TradingLogStore {
}
}
var noticeFilter = hashMapOf<String, Long>()
fun addNotice(name : String, code : String, log: String, qty: Int? = null) {
synchronized(this) {
if (decisionLogs.size > 1000) decisionLogs.removeAt(0)

View File

@ -241,19 +241,6 @@ class TradeConfig {
var end_buy_time : String = "15:10"
var enableOverSea : Boolean = false
var tlg_id : String = ""
var CYCLE_TIMEOUT = 15 * 60 * 1000L // 한 사이클 최대 10분
var WATCHDOG_CHECK_INTERVAL = 30 * 1000L // 30초마다 생존 확인
var STUCK_THRESHOLD = 7 * 60 * 1000L // 5분간 반응 없으면 'Stuck'으로 판단
var ONE_STOCK_ALYSIS_TIME = 180000L
var isLowPerformanceMonitoring: Boolean = false
var useGradeShare : List<String> = listOf("LEVEL_4","LEVEL_5")
var useTagsShare : List<String> = listOf("NOTICE", "WATCH")
var useLogKeywordsShare : List<String> = listOf("재분석")
var useAutoRepost : Boolean = false
var minusFilter : Double = 15.0
var plusFilter : Double = 15.0
var excuteCountOnMin : Int = 2
var autoSellOrder : Boolean = false
}

View File

@ -331,7 +331,7 @@ object KisTradeService {
stockCode: String,
qty: String,
price: String,
isBuy: Boolean = false,
isBuy: Boolean,
orderDivision: String = "",
marketCode : String = "KRX"
): Result<String> {
@ -766,64 +766,4 @@ object KisTradeService {
}
suspend fun reserveSell(stockCode: String,
qty: String,
price: String,
targetDate : String) :Result<String> {
val config = KisSession.config
val isDomestic = stockCode.length == 6 && stockCode.all { it.isDigit() }
val baseUrl = if (config.isSimulation) vtsUrl else prodUrl
// 계좌번호 처리: 8자리면 01 자동 추가
var pureAccount = config.accountNo.replace("-", "").trim()
if (pureAccount.length == 8) pureAccount += "01"
val cano = pureAccount.take(8)
val acntPrdtCd = pureAccount.takeLast(2)
val trId = "CTSC0008U"
return try {
val response = client.post("$baseUrl/uapi/${if(isDomestic) "domestic" else "overseas"}-stock/v1/trading/order-resv") {
header("authorization", "Bearer ${config.tradeToken}")
header("appkey", if (config.isSimulation) config.vtsAppKey else config.realAppKey)
header("appsecret", if (config.isSimulation) config.vtsSecretKey else config.realSecretKey)
header("tr_id", trId)
header("custtype", "P") // [해결] 필수 헤더 추가
header("Content-Type", "application/json")
setBody(mapOf(
"CANO" to cano,
"ACNT_PRDT_CD" to acntPrdtCd,
"PDNO" to stockCode,
"ORD_DVSN_CD" to "00",
"SLL_BUY_DVSN_CD" to "01",
"ORD_QTY" to qty,
"ORD_UNPR" to price,
"ORD_OBJT_CBLC_DVSN_CD" to "10",
"RSVN_ORD_END_DT" to targetDate
))
}
val body = response.body<JsonObject>() // [해결] Polymorphic 직렬화 에러 방지
val rtCd = body["rt_cd"]?.jsonPrimitive?.content
val msg = body["msg1"]?.jsonPrimitive?.content ?: "메시지 없음"
if (rtCd == "0") {
// 응답의 output 객체에서 주문 번호(ODNO) 추출
val orderNo = body["output"]?.jsonObject?.get("ODNO")?.jsonPrimitive?.content
?: body["output"]?.jsonObject?.get("odno")?.jsonPrimitive?.content // API마다 대소문자가 다를 수 있음
?: ""
Result.success(orderNo) // 성공 시 주문 번호 반환
} else {
val msg = body["msg1"]?.jsonPrimitive?.content ?: "메시지 없음"
Result.failure(Exception("❌ 오류 ($rtCd): $msg"))
}
} catch (e: Exception) { Result.failure(e) }
}
}

View File

@ -671,6 +671,9 @@ object RagService {
val alignmentBonus = if (s.ultraShort > s.shortTerm && s.shortTerm > s.midTerm) 3.0 else 0.0
return (base + alignmentBonus).coerceIn(0.0, 25.0)
}
}

View File

@ -59,9 +59,6 @@ object TradingReportManager : TradingReportService {
private val activePositions = mutableMapOf<String, String>()
override fun recordAssetSnapshot(type: SnapshotType, balance: UnifiedBalance, remark: String?) {
// if (!KisSession.tradeConfig.useAutoRepost) {
// return
// }
CoroutineScope(Dispatchers.IO).launch {
println("❌ [Report] 리포트 비동기 생성 중 오류 발생: gggg")
val todayDate = LocalDate.now().toString()

View File

@ -61,10 +61,10 @@ object AutoTradingManager {
private val lastTickTime = AtomicLong(System.currentTimeMillis())
private var watchdogJob: Job? = null
var CYCLE_TIMEOUT = KisSession.tradeConfig.CYCLE_TIMEOUT
var WATCHDOG_CHECK_INTERVAL = KisSession.tradeConfig.WATCHDOG_CHECK_INTERVAL
var STUCK_THRESHOLD = KisSession.tradeConfig.STUCK_THRESHOLD
var ONE_STOCK_ALYSIS_TIME = KisSession.tradeConfig.ONE_STOCK_ALYSIS_TIME
private const val CYCLE_TIMEOUT = 15 * 60 * 1000L // 한 사이클 최대 10분
private const val WATCHDOG_CHECK_INTERVAL = 30 * 1000L // 30초마다 생존 확인
private const val STUCK_THRESHOLD = 7 * 60 * 1000L // 5분간 반응 없으면 'Stuck'으로 판단
private const val ONE_STOCK_ALYSIS_TIME = 180000L
fun isRunning(): Boolean = discoveryJob?.isActive == true
private var remainingCandidates = mutableListOf<RankingStock>()
// private val processedCodes = mutableSetOf<String>() // 중복 처리 방지용 (선택 사항)
@ -216,8 +216,8 @@ object AutoTradingManager {
println("🚫 [안전 장치 작동] 현재 포지션이 가득 찼습니다. (최대 ${myOredsAndBalanceCodes.size}/${maxStocks}종목). 신규 매수를 일시 중단하고 매도에 집중합니다.")
TradingLogStore.addNotice("SYSTEM", "LIMIT", "최대 보유 종목 도달로 신규 매수 일시 중단")
AutoTradingManager.addToReanalysis(RankingStock(mksc_shrn_iscd = stockCode,hts_kor_isnm = stockName))
TradingLogStore.addWatchLog(decision,"WATCH","매수 실패 : 최대 보유 종목 도달로 신규 매수 일시 중단 => 재분석 대기열에 추가")
} else if (KisSession.isAvailBuyTime(LocalTime.now())){
TradingLogStore.addLog(decision,"WATCH","매수 실패 : 최대 보유 종목 도달로 신규 매수 일시 중단 => 재분석 대기열에 추가")
} else {
println("basePrice : $basePrice, oneTickLowerPrice : $oneTickLowerPrice, finalPrice : $finalPrice")
KisTradeService.postOrder(stockCode, orderQty, finalPrice.toLong().toString(), isBuy = true)
@ -266,22 +266,11 @@ object AutoTradingManager {
if (it.message?.contains("주문가능금액을 초과") == true) {
AutoTradingManager.addToReanalysis(RankingStock(mksc_shrn_iscd = stockCode,hts_kor_isnm = stockName))
TradingLogStore.addWatchLog(decision,"WATCH","${it.message ?: " 매수 실패"} => 재분석 대기열에 추가")
TradingLogStore.addLog(decision,"WATCH","${it.message ?: " 매수 실패"} => 재분석 대기열에 추가")
} else {
TradingLogStore.addLog(decision,"BUY",it.message ?: "매수 실패")
}
}
} else {
val unfilledResult = KisTradeService.fetchUnfilledOrders()
unfilledResult.onSuccess { response ->
response.filter { it.sll_buy_dvsn_cd == "02" }.forEach { order ->
TradingLogStore.addNotice(order.prdt_name,order.pdno,"[주문 취소] 매수시간 종료 후 모든 매수 취소")
KisTradeService.cancelOrder(
order.ord_no, // 원주문번호
order.pdno
)
}
}
}
}
}
@ -453,32 +442,11 @@ object AutoTradingManager {
&& holding.valuationProfitAmount.toDouble() >= KisSession.config.getValues(ConfigIndex.LOSS_MAX_MONEY)) {
println("${holding.name} ${holding.profitRate.toDouble()} ${holding.valuationProfitAmount.toDouble()} ${KisSession.config.getValues(ConfigIndex.LOSS_MAX_MONEY)} , ${KisSession.config.getValues(ConfigIndex.LOSS_MINRATE)} , ${KisSession.config.getValues(ConfigIndex.STOP_LOSS)}")
val profit = holding.profitRate.toDouble()
// TradingLogStore.addNotice(
// "보유주식[${holding.name}]",
// holding.code,
// "수익률($profit%) -> ${holding.valuationProfitAmount} 손해 중이며 현제 손절 가이드에 적합함."
// )
var targetPrice = holding.avgPrice.toDouble()
tradeService.postOrder(
stockCode = holding.code,
qty = holding.availOrderCount,
price = targetPrice.toInt().toString(),
isBuy = false,
orderDivision = if (marketCode.equals("Y")) "07" else "",
marketCode = if (marketCode.equals("Y")) "KRX" else "NXT"
).onSuccess { newOrderNo ->
println("✅ [${if(marketCode.equals("Y"))"시간외 단일가" else "대체거래소"} 손절가이드에 따라 매매 주문 완료] ${holding.name}: $newOrderNo")
TradingLogStore.addSellLog(
holding.code,
targetPrice.toString(),
"SELL",
"☠️ 보유 주식 손절 처리 [수익률 : ${profit}%] ${holding.valuationProfitAmount} 손해 중이며 ${if(marketCode.equals("Y"))"시간외 단일가" else "대체거래소"}에 손절가이드에 따라 매매 주문 완료."
)
}.onFailure { err->
println("✅ [${if(marketCode.equals("Y"))"시간외 단일가" else "대체거래소"} 손절가이드에 따라 매매 주문 실패] ${holding.name}: $err")
}
TradingLogStore.addNotice(
"보유주식[${holding.name}]",
holding.code,
"수익률($profit%) -> ${holding.valuationProfitAmount} 손해 중이며 현제 손절 가이드에 적합함."
)
}
analyzeDeepLossHoldingsAfterMarket(holding)
}
@ -514,9 +482,7 @@ object AutoTradingManager {
targetPrice = targetPrice
isBefore930 = true
} else {
targetPrice = MarketUtil.roundToTickSize(
targetPrice + MarketUtil.getTickSize(targetPrice)
)
targetPrice = MarketUtil.roundToTickSize(targetPrice + MarketUtil.getTickSize(targetPrice))
}
println("🔄 [보유 주식 주문] ${holding.name} (${holding.code}) 매도 목표 ${targetPrice} 미체결 매도 건 재주문 시도")
tradeService.postOrder(
@ -532,20 +498,18 @@ object AutoTradingManager {
"SELL",
"🎊 보유 주식[예상수익 : ${holding.profitRate}] ${if (isBefore930) "09:30 이전 현시세{${holding.currentPrice}}로 매도[$targetPrice] 주문" else "09:30 이후 시세{${holding.currentPrice}} 기준 호가 위 매도[$targetPrice] 주문"} 완료"
)
DatabaseFactory.saveAutoTrade(
AutoTradeItem(
orderNo = newOrderNo,
code = holding.code,
name = holding.name,
quantity = holding.quantity.toInt(),
profitRate = 0.0,
stopLossRate = 0.0,
targetPrice = targetPrice.toDouble(),
stopLossPrice = 0.0,
status = "SELLING",
isDomestic = true
)
)
DatabaseFactory.saveAutoTrade(AutoTradeItem(
orderNo = newOrderNo,
code = holding.code,
name = holding.name,
quantity = holding.quantity.toInt(),
profitRate = 0.0,
stopLossRate = 0.0,
targetPrice = targetPrice.toDouble(),
stopLossPrice = 0.0,
status = "SELLING",
isDomestic = true
))
syncAndExecute(newOrderNo)
}.onFailure {
TradingLogStore.addSellLog(
@ -564,9 +528,8 @@ object AutoTradingManager {
&& holding.valuationProfitAmount.toDouble() >= KisSession.config.getValues(ConfigIndex.LOSS_MAX_MONEY)) {
println("${holding.name} ${holding.profitRate.toDouble()} ${holding.valuationProfitAmount.toDouble()} ${KisSession.config.getValues(ConfigIndex.LOSS_MAX_MONEY)} , ${KisSession.config.getValues(ConfigIndex.LOSS_MINRATE)} , ${KisSession.config.getValues(ConfigIndex.STOP_LOSS)}")
val profit = holding.profitRate.toDouble()
var targetPrice = if (KisSession.tradeConfig.autoSellOrder ) holding.avgPrice.toDouble() else holding.currentPrice.toDouble()
targetPrice = MarketUtil.roundToTickSize(targetPrice + MarketUtil.getTickSize(targetPrice) * 3.0)
var targetPrice = holding.currentPrice.toDouble()
targetPrice = MarketUtil.roundToTickSize(targetPrice + (MarketUtil.getTickSize(targetPrice) * 3.0))
tradeService.postOrder(
stockCode = holding.code,
@ -579,7 +542,7 @@ object AutoTradingManager {
holding.code,
targetPrice.toString(),
"SELL",
"☠️ 보유 주식 손절 처리 [수익률 : ${profit}%] ${holding.valuationProfitAmount} 손해 중이며 현제 손절 가이드에 적합함 시장가 매도."
"☠️ 보유 주식 손절 처리 [수익률 : ${profit}%] ${holding.valuationProfitAmount} 손해 중이며 현시세{${holding.currentPrice}}로 기준 호가 위 매도[$targetPrice] 주문 완료"
)
}.onFailure { err->
println("✅ [보유 주식 손절 처리] 실패 ${err.message}")
@ -602,9 +565,7 @@ object AutoTradingManager {
private suspend fun analyzeDeepLossHoldingsAfterMarket(holding: UnifiedStockHolding, isForce : Boolean = false) { // 💡 [신규 추가] 수익률이 크게 마이너스인 종목(-5.0% 이하) 심층 가이드 분석
val now = LocalTime.now()
val currentMinute = now.minute
if ((holding.availOrderCount.toInt()
?: 0) > 0 && ((!isForce && (now.hour == 8 || now.hour == 16 || now.hour == 17)) || (isForce && (currentMinute % 5 == 0)))
) {
if ((!isForce && (now.hour == 8 || now.hour == 16 || now.hour == 17)) || (isForce && (currentMinute % 5 == 0))) {
val profit = holding.profitRate.toDouble()
val lossThreshold = -5.0 // 가이드를 작동시킬 손실 기준선 (필요시 ConfigIndex 로 빼셔도 좋습니다)
if (profit <= lossThreshold) {
@ -807,6 +768,15 @@ object AutoTradingManager {
suspend fun checkBalance(isMorning: Boolean = true) {
if (isMorning) {
currentBalance = KisTradeService.fetchIntegratedBalance().getOrNull()
// currentBalance?.let { currentBalance ->
// if (LocalTime.now().isBefore(LocalTime.of(18,1))) {
// TradingReportManager.recordAssetSnapshot(
// if (LocalTime.now().isAfter(LocalTime.of(18, 0))
// ) SnapshotType.END else SnapshotType.MIDDLE, currentBalance, ""
// )
// }
// }
if (KisSession.config.take_profit) currentBalance?.let { resumePendingSellOrders(KisTradeService, it) }
if (KisSession.tradeConfig.auto_cancel_pending_buy) { checkAndCancelPendingBuyOrders() }
} else {
@ -825,15 +795,19 @@ object AutoTradingManager {
val orderTimeMillis = parseOrderTime(order.ord_tmd)
val elapsedMillis = currentTime - orderTimeMillis
if (elapsedMillis >= KisSession.tradeConfig.auto_cancel_pending_time ) {
// 조건 A: 설정된 대기 시간 경과 여부
if (elapsedMillis >= KisSession.tradeConfig.auto_cancel_pending_time) {
// 2. 현재가 조회 (가격을 비교하기 위해)
val currentPrice = KisTradeService.fetchCurrentPrice(order.pdno).getOrNull()?.stck_prpr?.toDouble() ?: 0.0
val orderedPrice = order.ord_unpr.toDoubleOrNull() ?: 0.0
// 조건 B: 현재가와 주문가의 괴리율 체크 (현재가가 너무 올라갔거나 내려갔을 때)
val priceGap = Math.abs(currentPrice - orderedPrice) / orderedPrice
println("checkAndCancelPendingBuyOrders order $order ${elapsedMillis / 1000L}${priceGap}% 차이")
if (priceGap >= KisSession.tradeConfig.auto_cancel_pending_rate) {
TradingLogStore.addNotice(order.prdt_name,order.pdno,"[주문 취소] ${order.prdt_name} (${order.pdno}) - 시간경과 및 가격괴리(${String.format("%.2f", priceGap)}%)로 취소 시도")
TradingLogStore.addNotice(order.prdt_name,order.pdno,"[주문 취소] ${order.prdt_name} (${order.pdno}) - 시간경과 및 가격괴리(${String.format("%.2f", priceGap * 100)}%)로 취소 시도")
KisTradeService.cancelOrder(
order.ord_no, // 원주문번호
order.pdno
@ -844,21 +818,6 @@ object AutoTradingManager {
}
}
suspend fun cancelAllPendingSellOrders(
) {
// 1. 미체결 내역 조회
val unfilledResult = KisTradeService.fetchUnfilledOrders()
unfilledResult.onSuccess { response ->
response.filter { it.sll_buy_dvsn_cd == "01" }.forEach { order ->
TradingLogStore.addNotice(order.prdt_name,order.pdno,"[주문 취소] 정규장 시작전 모든 매도 주문 취소")
KisTradeService.cancelOrder(
order.ord_no, // 원주문번호
order.pdno
)
}
}
}
// 주문 시간 문자열을 Millis로 변환하는 유틸리티 (당일 주문 기준)
fun parseOrderTime(ordTmd: String): Long {
return try {
@ -900,7 +859,7 @@ object AutoTradingManager {
}.filter {
val rate = it.prdy_ctrt.toDouble()
val corpInfo = DartCodeManager.getCorpCode(it.code)
val isOk = (rate > 0 && rate < KisSession.tradeConfig.plusFilter) || (rate < 0 && rate > (KisSession.tradeConfig.minusFilter * -1))
val isOk = (rate > 0 && rate < 15) || (rate < 0 && rate > -15)
if (corpInfo?.cName.isNullOrEmpty()) {
false
} else {
@ -946,62 +905,47 @@ object AutoTradingManager {
}
println("⏱️ [Cycle End] ${LocalTime.now()}")
}
// private var lastForceCheckMinute = -1 // 마지막으로 강제 체크를 수행한 '분'을 저장
private val executionCountMap = mutableMapOf<String, Int>()
private var lastForceCheckMinute = -1 // 마지막으로 강제 체크를 수행한 '분'을 저장
suspend fun sellSchedule() {
if (KisSession.config.take_profit == false) return
val now = LocalTime.now()
val timeKey = String.format("%02d:%02d", now.hour, now.minute) // 예: "09:05"
val currentCount = executionCountMap.getOrDefault(timeKey, 0)
if (currentCount >= KisSession.tradeConfig.excuteCountOnMin) { return }
if (KisSession.config.take_profit == false) {
var isExecuted = false
val currentMinute = now.minute
if (now.isBefore(LocalTime.of(8,50)) && now.isAfter(LocalTime.of(8,45))) {
cancelAllPendingSellOrders()
isExecuted = true
} else if ( (now.isBefore(LocalTime.of(16,0)) && now.isAfter(KisSession.endBuyTime())) ) {
val unfilledResult = KisTradeService.fetchUnfilledOrders()
unfilledResult.onSuccess { response ->
response.filter { it.sll_buy_dvsn_cd == "02" }.forEach { order ->
TradingLogStore.addNotice(order.prdt_name,order.pdno,"[주문 취소] 정규장 후 모든 매수 취소")
KisTradeService.cancelOrder(
order.ord_no, // 원주문번호
order.pdno
} else {
val now = LocalTime.now()
val currentMinute = now.minute
if (now.hour == 9 && currentMinute % 2 == 1
) {
if (lastForceCheckMinute != currentMinute) {
TradingLogStore.addAnalyzer(
" - ",
" - ",
"⏰ [강제 스케줄 실행] 오전 9시 ${currentMinute}분 - 보유주식 매도 체크를 시작합니다.",
true
)
println("⏰ [강제 스케줄 실행] 오전 9시 ${currentMinute}분 - 보유주식 매도 체크를 시작합니다.")
checkBalance()
lastForceCheckMinute = currentMinute // 실행 완료 기록
}
} else if (((now.hour == 8 && KisSession.tradeConfig.before_nxt) || (now.hour >= 16 && now.hour < 20 && KisSession.tradeConfig.after_nxt)) && (currentMinute % 2 == 1)) {
if (lastForceCheckMinute != currentMinute) {
TradingLogStore.addAnalyzer(
" - ",
" - ",
"⏰ [강제 스케줄 실행] 오후 ${now.hour}${currentMinute}분 - 보유주식 시간외 단일가 또는 대체마켓 체크를 시작합니다.",
true
)
var list = mutableListOf<String>("X")
if (now.hour != 8 && now.hour < 18) {
list.add("Y")
}
list.forEach { code ->
KisTradeService.fetchIntegratedBalance(code).getOrNull()?.let {
sellingAfterMarketOnePrice(KisTradeService, it, code)
}
}
lastForceCheckMinute = currentMinute // 실행 완료 기록
}
}
isExecuted = true
} else if (now.hour == 9) {
TradingLogStore.addAnalyzer(
" - ",
" - ",
"⏰ [강제 스케줄 실행] 오전 9시 ${currentMinute}분 - 보유주식 매도 체크를 시작합니다.",
true
)
println("⏰ [강제 스케줄 실행] 오전 9시 ${currentMinute}분 - 보유주식 매도 체크를 시작합니다.")
checkBalance()
isExecuted = true
} else if (((now.hour == 8 && KisSession.tradeConfig.before_nxt && currentMinute < 45) || (now.hour >= 16 && now.hour < 20 && KisSession.tradeConfig.after_nxt)) && (currentMinute % 2 == 1)) {
TradingLogStore.addAnalyzer(
" - ",
" - ",
"⏰ [강제 스케줄 실행] 오후 ${now.hour}${currentMinute}분 - 보유주식 시간외 단일가 또는 대체마켓 체크를 시작합니다.",
true
)
var list = mutableListOf<String>("X")
if (now.hour != 8 && now.hour < 18) {
list.add("Y")
}
list.forEach { code ->
KisTradeService.fetchIntegratedBalance(code).getOrNull()?.let {
sellingAfterMarketOnePrice(KisTradeService, it, code)
}
}
isExecuted = true
}
if (isExecuted) { executionCountMap[timeKey] = currentCount + 1 }
if (now.hour >= 20) { executionCountMap.clear() }
}

View File

@ -291,7 +291,7 @@ object SafeScraper {
private val totalRam = HardwareDetector.getTotalRamGb()
// RAM 8GB당 1개 수준으로 설정하되, 최대 10~12개로 제한 (CPU 부하 방지)
private val maxParallel = totalRam.div(4).toInt()
private val maxParallel = totalRam.div(6).toInt()
// 동시 처리를 1개로 줄여서 안정성을 극대화 (추천)
// Playwright는 여러 페이지를 띄울 때 CPU/메모리 점유율이 매우 높습니다.

View File

@ -82,12 +82,22 @@ object SystemSleepPreventer {
}
if (process?.isAlive == true) return
if (!isWin) {
// try {
// // -i: 시스템 절전 방지, -d: 디스플레이 취침 방지, -m: 디스크 유휴 상태 방지
// val command = listOf("caffeinate", "-i", "-d", "-m")
// process = ProcessBuilder(command).start()
// println("☕ [System] caffeinate 실행됨: 앱이 켜져 있는 동안 절전 모드가 방지됩니다.")
// } catch (e: Exception) {
// println("⚠️ [System] caffeinate 실행 실패: ${e.message}")
// }
}
start2()
}
fun start2() {
println("🚀 화면 잠금 방지 프로그램이 시작되었습니다. (작동 시간: $startTime ~ $endTime)")
// 1분(60초)마다 체크
scheduler.scheduleAtFixedRate({
if (isWorkingTime()) {
@ -95,7 +105,7 @@ object SystemSleepPreventer {
} else {
println("💤 현재는 휴식 시간입니다. (${LocalTime.now().withNano(0)})")
}
}, 0, 150, TimeUnit.SECONDS)
}, 0, 60 * 2, TimeUnit.SECONDS)
}
private fun isWorkingTime(): Boolean {
@ -126,6 +136,21 @@ object SystemSleepPreventer {
private val osName = System.getProperty("os.name").lowercase()
// 설정 시간
private val dimTime = LocalTime.of(16, 0) // 오후 4시 이후 최저 밝기
fun start3() {
scheduler.scheduleAtFixedRate({
val now = LocalTime.now()
// 16:00 이후라면 밝기를 낮춤
if (now.isAfter(dimTime) || now.isBefore(LocalTime.of(8, 30))) {
setBrightness(0)
} else {
setBrightness(100) // 업무 시간에는 다시 밝게 (80%)
}
}, 0, 10, TimeUnit.MINUTES) // 10분마다 체크
}
private fun setBrightness(level: Int) {
try {

View File

@ -68,6 +68,7 @@ fun TradingDecisionLog() {
val filterOptions = listOf("전체", "BUY", "SELL", "SETTING","ANALYZER","WATCH","AFTER","NOTICE")//"PASS",,"RETRY""HOLD",
var llmAnalyser by remember { mutableStateOf(AutoTradingManager.llmAnalyser) }
val tradeConfig by remember {
KisSession.tradeConfig = KisSession.loadTradeConfig()
CoroutineScope(Dispatchers.Default).launch {
println("CALLED sendTelegramMessage -1")
val now = java.time.LocalTime.now(java.time.ZoneId.of("Asia/Seoul"))

View File

@ -11238,145 +11238,5 @@
{
"code": "238490",
"name": "힘스"
},
{
"code": "000980",
"name": "교보19호스팩"
},
{
"code": "001320",
"name": "교보20호스팩"
},
{
"code": "478340",
"name": "나라스페이스테크놀로지"
},
{
"code": "403850",
"name": "더핑크퐁컴퍼니"
},
{
"code": "000010",
"name": "덕양에너젠"
},
{
"code": "491000",
"name": "리브스메드"
},
{
"code": "394420",
"name": "리센스메디컬"
},
{
"code": "000930",
"name": "미래에셋비전스팩8호"
},
{
"code": "000960",
"name": "미래에셋비전스팩9호"
},
{
"code": "488900",
"name": "비츠로넥스텍"
},
{
"code": "001150",
"name": "삼성스팩13호"
},
{
"code": "000130",
"name": "삼진식품"
},
{
"code": "061090",
"name": "세나테크놀로지"
},
{
"code": "490470",
"name": "세미파이브"
},
{
"code": "001300",
"name": "신한제17호스팩"
},
{
"code": "388210",
"name": "씨엠티엑스"
},
{
"code": "493280",
"name": "아이엠바이오로직스"
},
{
"code": "476830",
"name": "알지노믹스"
},
{
"code": "459550",
"name": "알트"
},
{
"code": "000110",
"name": "액스비스"
},
{
"code": "458350",
"name": "에스팀"
},
{
"code": "000090",
"name": "에임드바이오"
},
{
"code": "001050",
"name": "유진스팩12호"
},
{
"code": "469610",
"name": "이노테크"
},
{
"code": "261520",
"name": "이지스"
},
{
"code": "493330",
"name": "지에프아이"
},
{
"code": "000820",
"name": "카나프테라퓨틱스"
},
{
"code": "439960",
"name": "코스모로보틱스"
},
{
"code": "464490",
"name": "쿼드메디슨"
},
{
"code": "494120",
"name": "큐리오시스"
},
{
"code": "466690",
"name": "키움히어로제1호스팩"
},
{
"code": "001310",
"name": "키움히어로제2호스팩"
},
{
"code": "487580",
"name": "폴레드"
},
{
"code": "001010",
"name": "하나36호스팩"
},
{
"code": "408470",
"name": "한패스"
}
]