Compare commits

..

9 Commits

Author SHA1 Message Date
488e4e72b3 Merge branch 'master' into g
* master:
  ...

# Conflicts:
#	src/main/kotlin/service/AutoTradingManager.kt
2026-05-19 13:35:00 +09:00
9af9f46748 ... 2026-05-18 17:56:26 +09:00
e0fbe9a9a2 Merge branch 'master' into g
* master:
  ...
2026-05-18 13:04:26 +09:00
4c27d71701 ... 2026-05-18 13:03:10 +09:00
74abc6314d Merge branch 'master' into g
* master:
  ...

# Conflicts:
#	src/main/kotlin/service/AutoTradingManager.kt
2026-05-14 14:16:03 +09:00
92d0a84629 매입단가 손절 2026-05-14 13:29:51 +09:00
b9ba8efc1a ... 2026-05-14 13:27:33 +09:00
27356f0fc2 Merge branch 'master' into g
* master:
  ...
2026-05-13 16:41:42 +09:00
ad6d00ac39 ... 2026-05-13 11:37:57 +09:00
11 changed files with 448 additions and 125 deletions

View File

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

View File

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

View File

@ -241,6 +241,19 @@ class TradeConfig {
var end_buy_time : String = "15:10" var end_buy_time : String = "15:10"
var enableOverSea : Boolean = false var enableOverSea : Boolean = false
var tlg_id : String = "" 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, stockCode: String,
qty: String, qty: String,
price: String, price: String,
isBuy: Boolean, isBuy: Boolean = false,
orderDivision: String = "", orderDivision: String = "",
marketCode : String = "KRX" marketCode : String = "KRX"
): Result<String> { ): Result<String> {
@ -766,4 +766,64 @@ 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,9 +671,6 @@ object RagService {
val alignmentBonus = if (s.ultraShort > s.shortTerm && s.shortTerm > s.midTerm) 3.0 else 0.0 val alignmentBonus = if (s.ultraShort > s.shortTerm && s.shortTerm > s.midTerm) 3.0 else 0.0
return (base + alignmentBonus).coerceIn(0.0, 25.0) return (base + alignmentBonus).coerceIn(0.0, 25.0)
} }
} }

View File

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

View File

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

View File

@ -82,22 +82,12 @@ object SystemSleepPreventer {
} }
if (process?.isAlive == true) return 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() start2()
} }
fun start2() { fun start2() {
println("🚀 화면 잠금 방지 프로그램이 시작되었습니다. (작동 시간: $startTime ~ $endTime)") println("🚀 화면 잠금 방지 프로그램이 시작되었습니다. (작동 시간: $startTime ~ $endTime)")
// 1분(60초)마다 체크 // 1분(60초)마다 체크
scheduler.scheduleAtFixedRate({ scheduler.scheduleAtFixedRate({
if (isWorkingTime()) { if (isWorkingTime()) {
@ -105,7 +95,7 @@ object SystemSleepPreventer {
} else { } else {
println("💤 현재는 휴식 시간입니다. (${LocalTime.now().withNano(0)})") println("💤 현재는 휴식 시간입니다. (${LocalTime.now().withNano(0)})")
} }
}, 0, 60 * 2, TimeUnit.SECONDS) }, 0, 150, TimeUnit.SECONDS)
} }
private fun isWorkingTime(): Boolean { private fun isWorkingTime(): Boolean {
@ -136,21 +126,6 @@ object SystemSleepPreventer {
private val osName = System.getProperty("os.name").lowercase() 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) { private fun setBrightness(level: Int) {
try { try {

View File

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

View File

@ -11238,5 +11238,145 @@
{ {
"code": "238490", "code": "238490",
"name": "힘스" "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": "한패스"
} }
] ]