..
This commit is contained in:
parent
d5ab55b336
commit
55b82c23af
@ -22,7 +22,8 @@ data class StockHolding(
|
||||
val pchs_avg_pric: String = "0", // 매입평균가
|
||||
val prpr: String = "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
|
||||
@ -148,7 +149,9 @@ data class UnifiedStockHolding(
|
||||
val currentPrice: String, // 현재가
|
||||
val profitRate: String, // 수익률
|
||||
val evalAmount: String, // 평가금액
|
||||
val isDomestic: Boolean // 국내/해외 구분
|
||||
val isDomestic: Boolean, // 국내/해외 구분
|
||||
val availOrderCount : String,
|
||||
|
||||
)
|
||||
|
||||
@Serializable
|
||||
|
||||
@ -71,10 +71,11 @@ object KisTradeService {
|
||||
combinedHoldings.add(UnifiedStockHolding(
|
||||
code = it.pdno, name = it.prdt_name, quantity = it.hldg_qty,
|
||||
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 {
|
||||
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(
|
||||
code = it.pdno, name = it.prdt_name, quantity = it.hldg_qty,
|
||||
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
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
@ -203,50 +203,50 @@ object RagService {
|
||||
/**
|
||||
* 질문과 가장 유사한 정보를 H2에서 검색하여 AI 답변을 생성합니다.
|
||||
*/
|
||||
fun askWithContext(question: String,
|
||||
corpInfo: String,
|
||||
financialData: String,
|
||||
days : List<CandleData>,
|
||||
weeks : List<CandleData>,
|
||||
monthly : List<CandleData>): String {
|
||||
val questionEmbedding = embeddingModel.embed(question).content()
|
||||
val searchResult = embeddingStore.search(
|
||||
EmbeddingSearchRequest.builder()
|
||||
.queryEmbedding(questionEmbedding)
|
||||
.maxResults(5)
|
||||
.build()
|
||||
)
|
||||
val newsContext = searchResult.matches().joinToString("\n") { it.embedded().text() }
|
||||
|
||||
// 2. 종합 분석 프롬프트 구성
|
||||
val finalPrompt = """
|
||||
<|begin_of_text|><|start_header_id|>system<|end_header_id|>
|
||||
당신은 뉴스(심리), 재무(본질), 차트(추세)를 통합 분석하는 'AI 수석 애널리스트'입니다.
|
||||
제공된 데이터를 바탕으로 아래 형식을 엄격히 지켜 분석 리포트를 작성하세요.
|
||||
|
||||
[데이터 세트]
|
||||
1. 기업 기본 정보: $corpInfo
|
||||
2. 재무 성장성: $financialData
|
||||
3. 기술적 추세: ${monthly}, ${weeks}, ${days}
|
||||
4. 최신 이슈(뉴스): $newsContext
|
||||
|
||||
[분석 요청 사항]
|
||||
1. **업계 상황**: 해당 종목이 속한 업종의 현재 전체적인 흐름을 먼저 정리하세요.
|
||||
2. **종목 이슈 분석**: 뉴스에서 포착된 핵심 키워드와 시장의 반응을 요약하세요.
|
||||
3. **장기/단기 전략**:
|
||||
- 장기(재무/월봉 기반): 추천 혹은 비추천 사유
|
||||
- 단기(뉴스/일봉 기반): 추천 혹은 비추천 사유
|
||||
4. **최종 결론**: '매수/관망/매도' 의견과 그에 따른 근거를 단호하게 제시하세요.
|
||||
<|eot_id|>
|
||||
<|start_header_id|>user<|end_header_id|>
|
||||
질문: $question
|
||||
<|eot_id|><|start_header_id|>assistant<|end_header_id|>
|
||||
""".trimIndent()
|
||||
|
||||
val response = chatModel.chat(UserMessage.from(finalPrompt))
|
||||
// println(response)
|
||||
return response.aiMessage().text()
|
||||
}
|
||||
// fun askWithContext(question: String,
|
||||
// corpInfo: String,
|
||||
// financialData: String,
|
||||
// days : List<CandleData>,
|
||||
// weeks : List<CandleData>,
|
||||
// monthly : List<CandleData>): String {
|
||||
// val questionEmbedding = embeddingModel.embed(question).content()
|
||||
// val searchResult = embeddingStore.search(
|
||||
// EmbeddingSearchRequest.builder()
|
||||
// .queryEmbedding(questionEmbedding)
|
||||
// .maxResults(5)
|
||||
// .build()
|
||||
// )
|
||||
// val newsContext = searchResult.matches().joinToString("\n") { it.embedded().text() }
|
||||
//
|
||||
// // 2. 종합 분석 프롬프트 구성
|
||||
// val finalPrompt = """
|
||||
// <|begin_of_text|><|start_header_id|>system<|end_header_id|>
|
||||
// 당신은 뉴스(심리), 재무(본질), 차트(추세)를 통합 분석하는 'AI 수석 애널리스트'입니다.
|
||||
// 제공된 데이터를 바탕으로 아래 형식을 엄격히 지켜 분석 리포트를 작성하세요.
|
||||
//
|
||||
// [데이터 세트]
|
||||
// 1. 기업 기본 정보: $corpInfo
|
||||
// 2. 재무 성장성: $financialData
|
||||
// 3. 기술적 추세: ${monthly}, ${weeks}, ${days}
|
||||
// 4. 최신 이슈(뉴스): $newsContext
|
||||
//
|
||||
// [분석 요청 사항]
|
||||
// 1. **업계 상황**: 해당 종목이 속한 업종의 현재 전체적인 흐름을 먼저 정리하세요.
|
||||
// 2. **종목 이슈 분석**: 뉴스에서 포착된 핵심 키워드와 시장의 반응을 요약하세요.
|
||||
// 3. **장기/단기 전략**:
|
||||
// - 장기(재무/월봉 기반): 추천 혹은 비추천 사유
|
||||
// - 단기(뉴스/일봉 기반): 추천 혹은 비추천 사유
|
||||
// 4. **최종 결론**: '매수/관망/매도' 의견과 그에 따른 근거를 단호하게 제시하세요.
|
||||
// <|eot_id|>
|
||||
// <|start_header_id|>user<|end_header_id|>
|
||||
// 질문: $question
|
||||
// <|eot_id|><|start_header_id|>assistant<|end_header_id|>
|
||||
// """.trimIndent()
|
||||
//
|
||||
// val response = chatModel.chat(UserMessage.from(finalPrompt))
|
||||
//// println(response)
|
||||
// return response.aiMessage().text()
|
||||
// }
|
||||
|
||||
suspend fun decideTrading(
|
||||
stockName: String,
|
||||
@ -342,6 +342,8 @@ object RagService {
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Serializable
|
||||
class TradingDecision {
|
||||
var corpName : String = ""
|
||||
|
||||
@ -89,13 +89,13 @@ object AutoTradingManager {
|
||||
// val balance = tradeService.fetchIntegratedBalance().getOrNull()
|
||||
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)
|
||||
println("🔄 [재주문] ${item.name} (${item.code}) ${item.orderedPrice} ${final} 전날 미체결 매도 건 재주문 시도")
|
||||
// 3. 기존 목표가(targetPrice)로 다시 매도 주문 전송
|
||||
tradeService.postOrder(
|
||||
stockCode = item.code,
|
||||
qty = item.quantity.toString(),
|
||||
qty = min(item.quantity,holding.availOrderCount.toInt()).toString(),
|
||||
price = final.toLong().toString(),
|
||||
isBuy = false
|
||||
).onSuccess { newOrderNo ->
|
||||
@ -125,12 +125,12 @@ object AutoTradingManager {
|
||||
println("⏱️ [Cycle Start] ${LocalTime.now()}")
|
||||
|
||||
// [프로세스 1] 장 마감 및 잔고 체크
|
||||
// val now = LocalTime.now(ZoneId.of("Asia/Seoul"))
|
||||
// //&& now.isBefore(LocalTime.of(15, 30))
|
||||
// if (now.isAfter(LocalTime.of(15, 30)) ) {
|
||||
//// executeClosingLiquidation(tradeService)
|
||||
// return@withTimeout
|
||||
// }
|
||||
val now = LocalTime.now(ZoneId.of("Asia/Seoul"))
|
||||
//&& now.isBefore(LocalTime.of(15, 30))
|
||||
if (now.isAfter(LocalTime.of(15, 15)) ) {
|
||||
executeClosingLiquidation(tradeService)
|
||||
return@withTimeout
|
||||
}
|
||||
|
||||
val balance = tradeService.fetchIntegratedBalance().getOrNull()
|
||||
|
||||
@ -142,7 +142,18 @@ object AutoTradingManager {
|
||||
if (remainingCandidates.isEmpty()) {
|
||||
val candidates: MutableList<RankingStock> = fetchCandidates(tradeService).apply {
|
||||
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) }
|
||||
.toMutableList()
|
||||
|
||||
@ -299,25 +310,25 @@ object AutoTradingManager {
|
||||
|
||||
|
||||
|
||||
// private suspend fun executeClosingLiquidation(tradeService: KisTradeService) {
|
||||
// val activeTrades = DatabaseFactory.findAllMonitoringTrades()
|
||||
// val balanceResult = tradeService.fetchIntegratedBalance().getOrNull()
|
||||
// val realHoldings = balanceResult?.holdings?.associateBy { it.code } ?: emptyMap()
|
||||
//
|
||||
// activeTrades.forEach { trade ->
|
||||
// try {
|
||||
// if (!realHoldings.containsKey(trade.code)) {
|
||||
// DatabaseFactory.updateStatusAndOrderNo(trade.id!!, TradeStatus.COMPLETED)
|
||||
// return@forEach
|
||||
// }
|
||||
// // 마감 정리 로직 (필요 시 주석 해제하여 사용)
|
||||
// println("📢 [마감 정리 체크] ${trade.name}")
|
||||
// } catch (e: Exception) {
|
||||
// println("⚠️ [마감 에러] ${trade.name}: ${e.message}")
|
||||
// }
|
||||
// delay(200)
|
||||
// }
|
||||
// }
|
||||
private suspend fun executeClosingLiquidation(tradeService: KisTradeService) {
|
||||
val activeTrades = DatabaseFactory.findAllMonitoringTrades()
|
||||
val balanceResult = tradeService.fetchIntegratedBalance().getOrNull()
|
||||
val realHoldings = balanceResult?.holdings?.associateBy { it.code } ?: emptyMap()
|
||||
|
||||
activeTrades.forEach { trade ->
|
||||
try {
|
||||
if (!realHoldings.containsKey(trade.code)) {
|
||||
DatabaseFactory.updateStatusAndOrderNo(trade.id!!, TradeStatus.EXPIRED)
|
||||
return@forEach
|
||||
}
|
||||
// 마감 정리 로직 (필요 시 주석 해제하여 사용)
|
||||
println("📢 [마감 정리 체크] ${trade.name}")
|
||||
} catch (e: Exception) {
|
||||
println("⚠️ [마감 에러] ${trade.name}: ${e.message}")
|
||||
}
|
||||
delay(200)
|
||||
}
|
||||
}
|
||||
|
||||
fun stopDiscovery() {
|
||||
discoveryJob?.cancel()
|
||||
|
||||
@ -28,6 +28,7 @@ import network.KisTradeService
|
||||
import service.AutoTradingManager
|
||||
import util.MarketUtil
|
||||
import kotlin.math.min
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
enum class InvestmentGrade(
|
||||
val displayName: String,
|
||||
@ -259,7 +260,7 @@ fun IntegratedOrderSection(
|
||||
basePrice = completeTradingDecision.currentPrice
|
||||
println("basePrice $basePrice")
|
||||
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 baseProfit = KisSession.config.getValues(ConfigIndex.PROFIT_INDEX)
|
||||
|
||||
@ -291,7 +292,9 @@ fun IntegratedOrderSection(
|
||||
println("🚀 [매수 진행] 토탈 스코어: ${String.format("%.1f", totalScore)} -> 종목: ${completeTradingDecision.stockCode}")
|
||||
|
||||
// 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) {
|
||||
(maxBudget / basePrice).toInt().coerceAtLeast(1)
|
||||
} else {
|
||||
@ -300,7 +303,7 @@ fun IntegratedOrderSection(
|
||||
// 5. 매수 실행 (계산된 finalMargin 전달)
|
||||
excuteTrade(
|
||||
willEnableAutoSell = true,
|
||||
orderQty = min(calculatedQty, KisSession.config.getValues(ConfigIndex.MAX_COUNT_INDEX).toInt()).toString(),
|
||||
orderQty = min(calculatedQty, maxQty).toString(),
|
||||
profitRate1 = finalMargin,
|
||||
investmentGrade = investmentGrade,
|
||||
)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user