This commit is contained in:
lunaticbum 2026-05-13 11:37:57 +09:00
parent 619407966e
commit ad6d00ac39
7 changed files with 121 additions and 32 deletions

View File

@ -88,7 +88,12 @@ fun getLlamaBinPath(): String {
}
// Windows NUC
os.contains("win") -> {
"$basePath/win-x64-n/llama-server.exe"
if (KisSession.tradeConfig.isLowPerformanceMonitoring) {
"$basePath/win-x64/llama-server.exe"
}
else {
"$basePath/win-x64-n/llama-server.exe"
}
}
else -> "$basePath/llama-server"
}
@ -111,6 +116,7 @@ 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,6 +5,7 @@ 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.*
@ -490,7 +491,18 @@ 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}")
}
}
}
)
}
}
@ -504,11 +516,25 @@ 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)
@ -521,11 +547,8 @@ object TradingLogStore {
reason = log
).apply {
CoroutineScope(Dispatchers.Default).launch {
println("CALLED sendTelegramMessage -1")
if (decision.contains("WATCH") || ((tradingDecision.investmentGrade?.ordinal
?: 0) < 2)
if (((tradingDecision.investmentGrade?.name?.length ?: 0) > 0 && KisSession.tradeConfig.useGradeShare.contains(tradingDecision.investmentGrade?.name))
) {
println("CALLED sendTelegramMessage OK")
NewsService.sendTelegramMessage("${this@apply.decision} ${tradingDecision.stockName}[${tradingDecision.currentPrice}] ${log}")
}
}
@ -534,6 +557,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) {
synchronized(this) {
if (decisionLogs.size > 1000) decisionLogs.removeAt(0)
@ -545,7 +594,17 @@ 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}")
}
}
}
)
}
}
@ -562,9 +621,19 @@ object TradingLogStore {
confidence = 100.0,
reason = log
).apply {
CoroutineScope(Dispatchers.Default).launch {
println("CALLED sendTelegramMessage")
NewsService.sendTelegramMessage("${this@apply.decision}$name[$code] ${log}")
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
}
}
)
@ -572,6 +641,8 @@ 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,6 +241,17 @@ 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
}

View File

@ -671,9 +671,6 @@ 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,6 +59,9 @@ 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 {
val todayDate = LocalDate.now().toString()

View File

@ -61,10 +61,10 @@ object AutoTradingManager {
private val lastTickTime = AtomicLong(System.currentTimeMillis())
private var watchdogJob: Job? = null
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
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
fun isRunning(): Boolean = discoveryJob?.isActive == true
private var remainingCandidates = mutableListOf<RankingStock>()
// private val processedCodes = mutableSetOf<String>() // 중복 처리 방지용 (선택 사항)
@ -216,7 +216,7 @@ object AutoTradingManager {
println("🚫 [안전 장치 작동] 현재 포지션이 가득 찼습니다. (최대 ${myOredsAndBalanceCodes.size}/${maxStocks}종목). 신규 매수를 일시 중단하고 매도에 집중합니다.")
TradingLogStore.addNotice("SYSTEM", "LIMIT", "최대 보유 종목 도달로 신규 매수 일시 중단")
AutoTradingManager.addToReanalysis(RankingStock(mksc_shrn_iscd = stockCode,hts_kor_isnm = stockName))
TradingLogStore.addLog(decision,"WATCH","매수 실패 : 최대 보유 종목 도달로 신규 매수 일시 중단 => 재분석 대기열에 추가")
TradingLogStore.addWatchLog(decision,"WATCH","매수 실패 : 최대 보유 종목 도달로 신규 매수 일시 중단 => 재분석 대기열에 추가")
} else {
println("basePrice : $basePrice, oneTickLowerPrice : $oneTickLowerPrice, finalPrice : $finalPrice")
@ -266,7 +266,7 @@ object AutoTradingManager {
if (it.message?.contains("주문가능금액을 초과") == true) {
AutoTradingManager.addToReanalysis(RankingStock(mksc_shrn_iscd = stockCode,hts_kor_isnm = stockName))
TradingLogStore.addLog(decision,"WATCH","${it.message ?: " 매수 실패"} => 재분석 대기열에 추가")
TradingLogStore.addWatchLog(decision,"WATCH","${it.message ?: " 매수 실패"} => 재분석 대기열에 추가")
} else {
TradingLogStore.addLog(decision,"BUY",it.message ?: "매수 실패")
}
@ -559,7 +559,9 @@ object AutoTradingManager {
private suspend fun analyzeDeepLossHoldingsAfterMarket(holding: UnifiedStockHolding, isForce : Boolean = false) { // 💡 [신규 추가] 수익률이 크게 마이너스인 종목(-5.0% 이하) 심층 가이드 분석
val now = LocalTime.now()
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 lossThreshold = -5.0 // 가이드를 작동시킬 손실 기준선 (필요시 ConfigIndex 로 빼셔도 좋습니다)
if (profit <= lossThreshold) {
@ -762,14 +764,14 @@ 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, ""
)
}
}
// 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() }
@ -801,7 +803,7 @@ object AutoTradingManager {
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 * 100)}%)로 취소 시도")
TradingLogStore.addNotice(order.prdt_name,order.pdno,"[주문 취소] ${order.prdt_name} (${order.pdno}) - 시간경과 및 가격괴리(${String.format("%.2f", priceGap)}%)로 취소 시도")
KisTradeService.cancelOrder(
order.ord_no, // 원주문번호
order.pdno
@ -853,7 +855,7 @@ object AutoTradingManager {
}.filter {
val rate = it.prdy_ctrt.toDouble()
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()) {
false
} else {

View File

@ -64,7 +64,6 @@ 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"))