This commit is contained in:
lunaticbum 2026-02-20 15:21:38 +09:00
parent d5ab55b336
commit 55b82c23af
5 changed files with 101 additions and 80 deletions

View File

@ -22,7 +22,8 @@ data class StockHolding(
val pchs_avg_pric: String = "0", // 매입평균가 val pchs_avg_pric: String = "0", // 매입평균가
val prpr: String = "0", // 현재가 val prpr: String = "0", // 현재가
val evlu_pfls_rt: String = "0.0", // 평가손익률 val evlu_pfls_rt: String = "0.0", // 평가손익률
val evlu_amt: String = "0" // 평가금액 val evlu_amt: String = "0" , // 평가금액
val ord_psbl_qty : String = "0",
) )
@Serializable @Serializable
@ -148,7 +149,9 @@ data class UnifiedStockHolding(
val currentPrice: String, // 현재가 val currentPrice: String, // 현재가
val profitRate: String, // 수익률 val profitRate: String, // 수익률
val evalAmount: String, // 평가금액 val evalAmount: String, // 평가금액
val isDomestic: Boolean // 국내/해외 구분 val isDomestic: Boolean, // 국내/해외 구분
val availOrderCount : String,
) )
@Serializable @Serializable

View File

@ -71,10 +71,11 @@ object KisTradeService {
combinedHoldings.add(UnifiedStockHolding( combinedHoldings.add(UnifiedStockHolding(
code = it.pdno, name = it.prdt_name, quantity = it.hldg_qty, code = it.pdno, name = it.prdt_name, quantity = it.hldg_qty,
avgPrice = it.pchs_avg_pric, currentPrice = it.prpr, avgPrice = it.pchs_avg_pric, currentPrice = it.prpr,
profitRate = it.evlu_pfls_rt, evalAmount = it.evlu_amt, isDomestic = true profitRate = it.evlu_pfls_rt, evalAmount = it.evlu_amt, isDomestic = true,
availOrderCount = it.ord_psbl_qty
).apply { ).apply {
if (it.hldg_qty.toLong() > 0) { if (it.hldg_qty.toLong() > 0) {
println("보유 종목 : ${it.prdt_name} , 수량 : ${it.hldg_qty}") // println("보유 종목 : ${it.prdt_name} , 수량 : ${it.hldg_qty}")
} }
}) })
} }
@ -84,7 +85,8 @@ object KisTradeService {
combinedHoldings.add(UnifiedStockHolding( combinedHoldings.add(UnifiedStockHolding(
code = it.pdno, name = it.prdt_name, quantity = it.hldg_qty, code = it.pdno, name = it.prdt_name, quantity = it.hldg_qty,
avgPrice = it.pchs_avg_pric, currentPrice = it.prpr, avgPrice = it.pchs_avg_pric, currentPrice = it.prpr,
profitRate = it.evlu_pfls_rt, evalAmount = it.evlu_amt, isDomestic = false profitRate = it.evlu_pfls_rt, evalAmount = it.evlu_amt, isDomestic = false,
availOrderCount = it.ord_psbl_qty
)) ))
} }

View File

@ -203,50 +203,50 @@ object RagService {
/** /**
* 질문과 가장 유사한 정보를 H2에서 검색하여 AI 답변을 생성합니다. * 질문과 가장 유사한 정보를 H2에서 검색하여 AI 답변을 생성합니다.
*/ */
fun askWithContext(question: String, // fun askWithContext(question: String,
corpInfo: String, // corpInfo: String,
financialData: String, // financialData: String,
days : List<CandleData>, // days : List<CandleData>,
weeks : List<CandleData>, // weeks : List<CandleData>,
monthly : List<CandleData>): String { // monthly : List<CandleData>): String {
val questionEmbedding = embeddingModel.embed(question).content() // val questionEmbedding = embeddingModel.embed(question).content()
val searchResult = embeddingStore.search( // val searchResult = embeddingStore.search(
EmbeddingSearchRequest.builder() // EmbeddingSearchRequest.builder()
.queryEmbedding(questionEmbedding) // .queryEmbedding(questionEmbedding)
.maxResults(5) // .maxResults(5)
.build() // .build()
) // )
val newsContext = searchResult.matches().joinToString("\n") { it.embedded().text() } // val newsContext = searchResult.matches().joinToString("\n") { it.embedded().text() }
//
// 2. 종합 분석 프롬프트 구성 // // 2. 종합 분석 프롬프트 구성
val finalPrompt = """ // val finalPrompt = """
<|begin_of_text|><|start_header_id|>system<|end_header_id|> // <|begin_of_text|><|start_header_id|>system<|end_header_id|>
당신은 뉴스(심리), 재무(본질), 차트(추세) 통합 분석하는 'AI 수석 애널리스트'입니다. // 당신은 뉴스(심리), 재무(본질), 차트(추세)를 통합 분석하는 'AI 수석 애널리스트'입니다.
제공된 데이터를 바탕으로 아래 형식을 엄격히 지켜 분석 리포트를 작성하세요. // 제공된 데이터를 바탕으로 아래 형식을 엄격히 지켜 분석 리포트를 작성하세요.
//
[데이터 세트] // [데이터 세트]
1. 기업 기본 정보: $corpInfo // 1. 기업 기본 정보: $corpInfo
2. 재무 성장성: $financialData // 2. 재무 성장성: $financialData
3. 기술적 추세: ${monthly}, ${weeks}, ${days} // 3. 기술적 추세: ${monthly}, ${weeks}, ${days}
4. 최신 이슈(뉴스): $newsContext // 4. 최신 이슈(뉴스): $newsContext
//
[분석 요청 사항] // [분석 요청 사항]
1. **업계 상황**: 해당 종목이 속한 업종의 현재 전체적인 흐름을 먼저 정리하세요. // 1. **업계 상황**: 해당 종목이 속한 업종의 현재 전체적인 흐름을 먼저 정리하세요.
2. **종목 이슈 분석**: 뉴스에서 포착된 핵심 키워드와 시장의 반응을 요약하세요. // 2. **종목 이슈 분석**: 뉴스에서 포착된 핵심 키워드와 시장의 반응을 요약하세요.
3. **장기/단기 전략**: // 3. **장기/단기 전략**:
- 장기(재무/월봉 기반): 추천 혹은 비추천 사유 // - 장기(재무/월봉 기반): 추천 혹은 비추천 사유
- 단기(뉴스/일봉 기반): 추천 혹은 비추천 사유 // - 단기(뉴스/일봉 기반): 추천 혹은 비추천 사유
4. **최종 결론**: '매수/관망/매도' 의견과 그에 따른 근거를 단호하게 제시하세요. // 4. **최종 결론**: '매수/관망/매도' 의견과 그에 따른 근거를 단호하게 제시하세요.
<|eot_id|> // <|eot_id|>
<|start_header_id|>user<|end_header_id|> // <|start_header_id|>user<|end_header_id|>
질문: $question // 질문: $question
<|eot_id|><|start_header_id|>assistant<|end_header_id|> // <|eot_id|><|start_header_id|>assistant<|end_header_id|>
""".trimIndent() // """.trimIndent()
//
val response = chatModel.chat(UserMessage.from(finalPrompt)) // val response = chatModel.chat(UserMessage.from(finalPrompt))
// println(response) //// println(response)
return response.aiMessage().text() // return response.aiMessage().text()
} // }
suspend fun decideTrading( suspend fun decideTrading(
stockName: String, stockName: String,
@ -342,6 +342,8 @@ object RagService {
} }
@Serializable @Serializable
class TradingDecision { class TradingDecision {
var corpName : String = "" var corpName : String = ""

View File

@ -89,13 +89,13 @@ object AutoTradingManager {
// val balance = tradeService.fetchIntegratedBalance().getOrNull() // val balance = tradeService.fetchIntegratedBalance().getOrNull()
val holding = balance?.holdings?.find { it.code == item.code } val holding = balance?.holdings?.find { it.code == item.code }
if (holding != null && holding.quantity.toInt() > 0) { if (holding != null && holding.quantity.toInt() > 0 && holding.availOrderCount.toInt() > 0) {
var final = MarketUtil.roundToTickSize(item.targetPrice) var final = MarketUtil.roundToTickSize(item.targetPrice)
println("🔄 [재주문] ${item.name} (${item.code}) ${item.orderedPrice} ${final} 전날 미체결 매도 건 재주문 시도") println("🔄 [재주문] ${item.name} (${item.code}) ${item.orderedPrice} ${final} 전날 미체결 매도 건 재주문 시도")
// 3. 기존 목표가(targetPrice)로 다시 매도 주문 전송 // 3. 기존 목표가(targetPrice)로 다시 매도 주문 전송
tradeService.postOrder( tradeService.postOrder(
stockCode = item.code, stockCode = item.code,
qty = item.quantity.toString(), qty = min(item.quantity,holding.availOrderCount.toInt()).toString(),
price = final.toLong().toString(), price = final.toLong().toString(),
isBuy = false isBuy = false
).onSuccess { newOrderNo -> ).onSuccess { newOrderNo ->
@ -125,12 +125,12 @@ object AutoTradingManager {
println("⏱️ [Cycle Start] ${LocalTime.now()}") println("⏱️ [Cycle Start] ${LocalTime.now()}")
// [프로세스 1] 장 마감 및 잔고 체크 // [프로세스 1] 장 마감 및 잔고 체크
// val now = LocalTime.now(ZoneId.of("Asia/Seoul")) val now = LocalTime.now(ZoneId.of("Asia/Seoul"))
// //&& now.isBefore(LocalTime.of(15, 30)) //&& now.isBefore(LocalTime.of(15, 30))
// if (now.isAfter(LocalTime.of(15, 30)) ) { if (now.isAfter(LocalTime.of(15, 15)) ) {
//// executeClosingLiquidation(tradeService) executeClosingLiquidation(tradeService)
// return@withTimeout return@withTimeout
// } }
val balance = tradeService.fetchIntegratedBalance().getOrNull() val balance = tradeService.fetchIntegratedBalance().getOrNull()
@ -142,7 +142,18 @@ object AutoTradingManager {
if (remainingCandidates.isEmpty()) { if (remainingCandidates.isEmpty()) {
val candidates: MutableList<RankingStock> = fetchCandidates(tradeService).apply { val candidates: MutableList<RankingStock> = fetchCandidates(tradeService).apply {
println("후보군 총 개수 : $size") println("후보군 총 개수 : $size")
}.filter { !it.name.contains("호스팩", true) } }.filter {
val rate = it.prdy_ctrt.toDouble()
val corpInfo = DartCodeManager.getCorpCode(it.code)
val isOk = (rate > 0 && rate < 15) || (rate < 0 && rate > -15)
// if (isOk) {println("${it.name} : ${it.prdy_ctrt}")}
if (corpInfo?.cName.isNullOrEmpty()) {
false
}else {
isOk
}
}
.filter { !it.name.contains("호스팩", true) }
.sortedBy { (it.prdy_ctrt.toDoubleOrNull() ?: 0.0) } .sortedBy { (it.prdy_ctrt.toDoubleOrNull() ?: 0.0) }
.toMutableList() .toMutableList()
@ -299,25 +310,25 @@ object AutoTradingManager {
// private suspend fun executeClosingLiquidation(tradeService: KisTradeService) { private suspend fun executeClosingLiquidation(tradeService: KisTradeService) {
// val activeTrades = DatabaseFactory.findAllMonitoringTrades() val activeTrades = DatabaseFactory.findAllMonitoringTrades()
// val balanceResult = tradeService.fetchIntegratedBalance().getOrNull() val balanceResult = tradeService.fetchIntegratedBalance().getOrNull()
// val realHoldings = balanceResult?.holdings?.associateBy { it.code } ?: emptyMap() val realHoldings = balanceResult?.holdings?.associateBy { it.code } ?: emptyMap()
//
// activeTrades.forEach { trade -> activeTrades.forEach { trade ->
// try { try {
// if (!realHoldings.containsKey(trade.code)) { if (!realHoldings.containsKey(trade.code)) {
// DatabaseFactory.updateStatusAndOrderNo(trade.id!!, TradeStatus.COMPLETED) DatabaseFactory.updateStatusAndOrderNo(trade.id!!, TradeStatus.EXPIRED)
// return@forEach return@forEach
// } }
// // 마감 정리 로직 (필요 시 주석 해제하여 사용) // 마감 정리 로직 (필요 시 주석 해제하여 사용)
// println("📢 [마감 정리 체크] ${trade.name}") println("📢 [마감 정리 체크] ${trade.name}")
// } catch (e: Exception) { } catch (e: Exception) {
// println("⚠️ [마감 에러] ${trade.name}: ${e.message}") println("⚠️ [마감 에러] ${trade.name}: ${e.message}")
// } }
// delay(200) delay(200)
// } }
// } }
fun stopDiscovery() { fun stopDiscovery() {
discoveryJob?.cancel() discoveryJob?.cancel()

View File

@ -28,6 +28,7 @@ import network.KisTradeService
import service.AutoTradingManager import service.AutoTradingManager
import util.MarketUtil import util.MarketUtil
import kotlin.math.min import kotlin.math.min
import kotlin.math.roundToInt
enum class InvestmentGrade( enum class InvestmentGrade(
val displayName: String, val displayName: String,
@ -259,7 +260,7 @@ fun IntegratedOrderSection(
basePrice = completeTradingDecision.currentPrice basePrice = completeTradingDecision.currentPrice
println("basePrice $basePrice") println("basePrice $basePrice")
val minScore = KisSession.config.getValues(ConfigIndex.MIN_PURCHASE_SCORE_INDEX) val minScore = KisSession.config.getValues(ConfigIndex.MIN_PURCHASE_SCORE_INDEX)
val maxBudget = KisSession.config.getValues(ConfigIndex.MAX_BUDGET_INDEX) var maxBudget = KisSession.config.getValues(ConfigIndex.MAX_BUDGET_INDEX)
val buyWeight = KisSession.config.getValues(ConfigIndex.BUY_WEIGHT_INDEX) val buyWeight = KisSession.config.getValues(ConfigIndex.BUY_WEIGHT_INDEX)
val baseProfit = KisSession.config.getValues(ConfigIndex.PROFIT_INDEX) val baseProfit = KisSession.config.getValues(ConfigIndex.PROFIT_INDEX)
@ -291,7 +292,9 @@ fun IntegratedOrderSection(
println("🚀 [매수 진행] 토탈 스코어: ${String.format("%.1f", totalScore)} -> 종목: ${completeTradingDecision.stockCode}") println("🚀 [매수 진행] 토탈 스코어: ${String.format("%.1f", totalScore)} -> 종목: ${completeTradingDecision.stockCode}")
// basePrice(현재가 혹은 지정가)를 기준으로 매수 가능 수량 산출 (최소 1주 보장) // basePrice(현재가 혹은 지정가)를 기준으로 매수 가능 수량 산출 (최소 1주 보장)
val gradeRate = (1.0 - (investmentGrade.ordinal * 0.1))
val maxQty = (KisSession.config.getValues(ConfigIndex.MAX_COUNT_INDEX) * gradeRate).roundToInt()
maxBudget = maxBudget * gradeRate
val calculatedQty = if (basePrice > 0) { val calculatedQty = if (basePrice > 0) {
(maxBudget / basePrice).toInt().coerceAtLeast(1) (maxBudget / basePrice).toInt().coerceAtLeast(1)
} else { } else {
@ -300,7 +303,7 @@ fun IntegratedOrderSection(
// 5. 매수 실행 (계산된 finalMargin 전달) // 5. 매수 실행 (계산된 finalMargin 전달)
excuteTrade( excuteTrade(
willEnableAutoSell = true, willEnableAutoSell = true,
orderQty = min(calculatedQty, KisSession.config.getValues(ConfigIndex.MAX_COUNT_INDEX).toInt()).toString(), orderQty = min(calculatedQty, maxQty).toString(),
profitRate1 = finalMargin, profitRate1 = finalMargin,
investmentGrade = investmentGrade, investmentGrade = investmentGrade,
) )