Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ad6d00ac39 |
@ -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) {
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -103,32 +103,6 @@ object LocalReportGenerator {
|
||||
}
|
||||
}
|
||||
|
||||
fun generateAndOpenAsyncDirectly(
|
||||
summary: RawSummaryData,
|
||||
rawHoldings: List<RawHoldingData>,
|
||||
rawTrades: List<RawTradeData>
|
||||
) {
|
||||
reportScope.launch {
|
||||
try {
|
||||
// 1. [핵심] 대시보드 통계 지표 추출 (Generator가 직접 계산)
|
||||
val stats = calculateDashboardStats(rawHoldings, rawTrades)
|
||||
|
||||
// 2. 탭 2 & 3 HTML 가공
|
||||
val holdingsHtml = processHoldings(rawHoldings)
|
||||
val tradesHtml = processTrades(rawTrades)
|
||||
|
||||
// 3. 전체 HTML 조립
|
||||
val htmlContent = buildHtml(summary, stats, holdingsHtml, tradesHtml)
|
||||
if (summary.type.equals("END", true) || summary.type.equals("MIDDLE", true)) {
|
||||
saveAndOpen(summary.type, htmlContent)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
println("❌ [Report] 리포트 비동기 생성 중 오류 발생: ${e.message}")
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- [새로운 통계 계산 로직] ---
|
||||
private fun calculateDashboardStats(holdings: List<RawHoldingData>, trades: List<RawTradeData>): DashboardStats {
|
||||
val tradesByStock = trades.groupBy { it.stockCode }
|
||||
|
||||
@ -59,8 +59,10 @@ 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()
|
||||
|
||||
// 1. 중복 없는 전체 종목 코드 리스트 추출
|
||||
@ -230,7 +232,7 @@ object TradingReportManager : TradingReportService {
|
||||
}
|
||||
|
||||
// 6. 코루틴 기반 제너레이터 호출
|
||||
LocalReportGenerator.generateAndOpenAsyncDirectly(summaryData, holdingLogs, tradeLogs)
|
||||
LocalReportGenerator.generateAndOpenAsync(summaryData, holdingLogs, tradeLogs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 ?: "매수 실패")
|
||||
}
|
||||
@ -538,21 +538,15 @@ object AutoTradingManager {
|
||||
isBuy = false,
|
||||
).onSuccess { newOrderNo ->
|
||||
println("✅ [보유 주식 손절 처리] 수익률($profit%) -> ${holding.valuationProfitAmount} 손해 중이며 현제 손절 가이드에 적합함 시장가 매도.")
|
||||
TradingLogStore.addSellLog(
|
||||
holding.code,
|
||||
targetPrice.toString(),
|
||||
"SELL",
|
||||
"☠️ 보유 주식 손절 처리 [수익률 : ${profit}%] ${holding.valuationProfitAmount} 손해 중이며 현시세{${holding.currentPrice}}로 기준 호가 위 매도[$targetPrice] 주문 완료"
|
||||
)
|
||||
}.onFailure { err->
|
||||
println("✅ [보유 주식 손절 처리] 실패 ${err.message}")
|
||||
}
|
||||
|
||||
// TradingLogStore.addNotice(
|
||||
// "보유주식[${holding.name}]",
|
||||
// holding.code,
|
||||
// "수익률($profit%) -> ${holding.valuationProfitAmount} 손해 중이며 현제 손절 가이드에 적합함 시장가 매도."
|
||||
// )
|
||||
TradingLogStore.addNotice(
|
||||
"보유주식[${holding.name}]",
|
||||
holding.code,
|
||||
"수익률($profit%) -> ${holding.valuationProfitAmount} 손해 중이며 현제 손절 가이드에 적합함 시장가 매도."
|
||||
)
|
||||
}
|
||||
analyzeDeepLossHoldingsAfterMarket(holding , true)
|
||||
}
|
||||
@ -565,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) {
|
||||
@ -807,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
|
||||
@ -859,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 {
|
||||
|
||||
@ -50,13 +50,9 @@ import model.KisSession
|
||||
import network.KisTradeService
|
||||
import network.NewsService
|
||||
import network.StockUniverseLoader
|
||||
import report.SnapshotType
|
||||
import report.TradingReportManager
|
||||
import service.AutoTradingManager
|
||||
import service.AutoTradingManager.currentBalance
|
||||
import java.io.File
|
||||
import java.net.URI
|
||||
import java.time.LocalTime
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
@ -68,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"))
|
||||
@ -109,34 +104,14 @@ fun TradingDecisionLog() {
|
||||
|
||||
Row(modifier = Modifier.fillMaxSize().background(Color(0xFFF2F2F2))) {
|
||||
Column(modifier = Modifier.weight(1f).padding(8.dp).fillMaxHeight().background(Color.White)) {
|
||||
Row(modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
Button(
|
||||
onClick = {
|
||||
coroutineScope.launch {
|
||||
// index 0으로 부드럽게 스크롤 (즉시 이동은 scrollToItem(0))
|
||||
listState.animateScrollToItem(if (filteredLogs.size - 1 >= 0) filteredLogs.size - 1 else 0)
|
||||
}
|
||||
Button(
|
||||
onClick = {
|
||||
coroutineScope.launch {
|
||||
// index 0으로 부드럽게 스크롤 (즉시 이동은 scrollToItem(0))
|
||||
listState.animateScrollToItem(filteredLogs.size - 1)
|
||||
}
|
||||
) { Text("AI 자동매매 실시간 로그", style = MaterialTheme.typography.h6) }
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
coroutineScope.launch {
|
||||
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, ""
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
) { Text("Open the report", style = MaterialTheme.typography.body2) }
|
||||
}
|
||||
}
|
||||
) { Text("AI 자동매매 실시간 로그", style = MaterialTheme.typography.h6) }
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp),
|
||||
@ -248,9 +223,9 @@ fun TradingDecisionLog() {
|
||||
Text(
|
||||
text = log.decision,
|
||||
color = when (log.decision) {
|
||||
"BUY" -> Color(0xFF800080)
|
||||
"BUY" -> Color.Red
|
||||
"SETTING" -> Color(0xFFFFA500)
|
||||
"SELL" -> if (log.reason.contains("손절 처리")) Color.Blue else Color.Red
|
||||
"SELL" -> Color(0xFF800080)
|
||||
"HOLD" -> Color.DarkGray
|
||||
"ANALYZER" -> Color.Green
|
||||
"PASS" -> Color.Yellow
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user