...
This commit is contained in:
parent
d4926646f9
commit
a51ffb6193
@ -28,7 +28,8 @@ data class BalanceSummary(
|
|||||||
val tot_evlu_amt: String = "0", // 총 평가금액
|
val tot_evlu_amt: String = "0", // 총 평가금액
|
||||||
val evlu_pfls_rt: String = "0.0", // 총 수익률 (에러 발생 지점: 기본값 추가로 해결)
|
val evlu_pfls_rt: String = "0.0", // 총 수익률 (에러 발생 지점: 기본값 추가로 해결)
|
||||||
val asst_icrt: String = "0.0", // 일부 환경에서 수익률 필드명
|
val asst_icrt: String = "0.0", // 일부 환경에서 수익률 필드명
|
||||||
val nass_amt: String = "0" // 순자산 금액
|
val nass_amt: String = "0" , // 순자산 금액
|
||||||
|
val dnca_tot_amt: String = "0"
|
||||||
)
|
)
|
||||||
@Serializable
|
@Serializable
|
||||||
data class RankingResponse(
|
data class RankingResponse(
|
||||||
@ -116,6 +117,7 @@ data class UnifiedStockHolding(
|
|||||||
data class UnifiedBalance(
|
data class UnifiedBalance(
|
||||||
val totalAsset: String, // 총 평가자산
|
val totalAsset: String, // 총 평가자산
|
||||||
val totalProfitRate: String, // 총 수익률
|
val totalProfitRate: String, // 총 수익률
|
||||||
|
val deposit: String,
|
||||||
val holdings: List<UnifiedStockHolding> // 통합 보유 종목 리스트
|
val holdings: List<UnifiedStockHolding> // 통합 보유 종목 리스트
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -41,7 +41,7 @@ object KisTradeService {
|
|||||||
// [수정] 모든 로그(Headers + Body)를 찍도록 설정
|
// [수정] 모든 로그(Headers + Body)를 찍도록 설정
|
||||||
install(Logging) {
|
install(Logging) {
|
||||||
logger = Logger.DEFAULT
|
logger = Logger.DEFAULT
|
||||||
level = LogLevel.BODY
|
level = LogLevel.NONE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -84,10 +84,12 @@ object KisTradeService {
|
|||||||
|
|
||||||
val totalAmt = (domRes?.output2?.firstOrNull()?.tot_evlu_amt?.toLongOrNull() ?: 0L) +
|
val totalAmt = (domRes?.output2?.firstOrNull()?.tot_evlu_amt?.toLongOrNull() ?: 0L) +
|
||||||
(ovsRes?.output2?.firstOrNull()?.tot_evlu_amt?.toLongOrNull() ?: 0L)
|
(ovsRes?.output2?.firstOrNull()?.tot_evlu_amt?.toLongOrNull() ?: 0L)
|
||||||
|
val depositAmt = domRes?.output2?.firstOrNull()?.dnca_tot_amt?.toLongOrNull() ?: 0L
|
||||||
|
|
||||||
Result.success(UnifiedBalance(
|
Result.success(UnifiedBalance(
|
||||||
totalAsset = String.format("%,d", totalAmt),
|
totalAsset = String.format("%,d", totalAmt),
|
||||||
totalProfitRate = domRes?.output2?.firstOrNull()?.evlu_pfls_rt ?: "0.0",
|
totalProfitRate = domRes?.output2?.firstOrNull()?.evlu_pfls_rt ?: "0.0",
|
||||||
|
deposit = String.format("%,d", depositAmt),
|
||||||
holdings = combinedHoldings
|
holdings = combinedHoldings
|
||||||
))
|
))
|
||||||
} catch (e: Exception) { Result.failure(e) }
|
} catch (e: Exception) { Result.failure(e) }
|
||||||
@ -220,7 +222,7 @@ object KisTradeService {
|
|||||||
|
|
||||||
val body = response.body<JsonObject>()
|
val body = response.body<JsonObject>()
|
||||||
val output2 = body["output2"]?.jsonArray
|
val output2 = body["output2"]?.jsonArray
|
||||||
println("output2 ${output2}")
|
// println("output2 ${output2}")
|
||||||
val candles = output2?.map { element ->
|
val candles = output2?.map { element ->
|
||||||
val obj = element.jsonObject
|
val obj = element.jsonObject
|
||||||
CandleData(
|
CandleData(
|
||||||
@ -394,7 +396,7 @@ object KisTradeService {
|
|||||||
val cano = pureAccount.take(8)
|
val cano = pureAccount.take(8)
|
||||||
val acntPrdtCd = pureAccount.takeLast(2)
|
val acntPrdtCd = pureAccount.takeLast(2)
|
||||||
return try {
|
return try {
|
||||||
println("orgNo")
|
// println("orgNo")
|
||||||
val response = client.post("$baseUrl/uapi/domestic-stock/v1/trading/order-rvsecncl") {
|
val response = client.post("$baseUrl/uapi/domestic-stock/v1/trading/order-rvsecncl") {
|
||||||
header("authorization", "Bearer ${config.tradeToken}")
|
header("authorization", "Bearer ${config.tradeToken}")
|
||||||
header("appkey", if (config.isSimulation) config.vtsAppKey else config.realAppKey)
|
header("appkey", if (config.isSimulation) config.vtsAppKey else config.realAppKey)
|
||||||
|
|||||||
@ -29,7 +29,7 @@ object NewsService {
|
|||||||
}
|
}
|
||||||
install(Logging) {
|
install(Logging) {
|
||||||
logger = Logger.DEFAULT
|
logger = Logger.DEFAULT
|
||||||
level = LogLevel.ALL
|
level = LogLevel.NONE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -52,9 +52,9 @@ object RagService {
|
|||||||
|
|
||||||
|
|
||||||
fun active() {
|
fun active() {
|
||||||
println("[Cache] Active")
|
// println("[Cache] Active")
|
||||||
if (UrlCacheManager.isInitialized()) return
|
if (UrlCacheManager.isInitialized()) return
|
||||||
println("[Cache] initialize")
|
// println("[Cache] initialize")
|
||||||
UrlCacheManager.initialize(embeddingStore, embeddingModel)
|
UrlCacheManager.initialize(embeddingStore, embeddingModel)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -130,32 +130,39 @@ object RagService {
|
|||||||
suspend fun processStock(stockName: String,stockCode: String,result : TradingDecisionCallback) {
|
suspend fun processStock(stockName: String,stockCode: String,result : TradingDecisionCallback) {
|
||||||
// 1. 10분간의 데이터 가져오기 (API 호출)
|
// 1. 10분간의 데이터 가져오기 (API 호출)
|
||||||
coroutineScope {
|
coroutineScope {
|
||||||
var tradingDecision : TradingDecision = TradingDecision()
|
try {
|
||||||
tradingDecision.stockCode = stockCode
|
|
||||||
var corpInfo = DartCodeManager.getCorpCode(stockCode)
|
|
||||||
corpInfo?.stockName = stockName
|
|
||||||
corpInfo?.let { NewsService.fetchAndIngestNews(it) }
|
|
||||||
|
|
||||||
val financialDataDeferred = async { NewsService.fetchFinancialGrowth(corpInfo?.cCode ?: "") }
|
|
||||||
|
|
||||||
tradingDecision.financialData = financialDataDeferred.await()
|
var tradingDecision: TradingDecision = TradingDecision()
|
||||||
result(tradingDecision,false)
|
tradingDecision.stockCode = stockCode
|
||||||
|
var corpInfo = DartCodeManager.getCorpCode(stockCode)
|
||||||
|
corpInfo?.stockName = stockName
|
||||||
|
tradingDecision.stockName = stockName
|
||||||
|
tradingDecision.corpName = corpInfo?.cName ?: ""
|
||||||
|
corpInfo?.let { NewsService.fetchAndIngestNews(it) }
|
||||||
|
|
||||||
tradingDecision.techSummary = TechnicalAnalyzer.generateComprehensiveReport()
|
val financialDataDeferred = async { NewsService.fetchFinancialGrowth(corpInfo?.cCode ?: "") }
|
||||||
result(tradingDecision,false)
|
|
||||||
|
|
||||||
val question = "${corpInfo?.cName} $stockName[$stockCode]의 향후 실적 전망과 관련된 핵심 뉴스"
|
tradingDecision.financialData = financialDataDeferred.await()
|
||||||
val questionEmbedding = embeddingModel.embed(question).content()
|
result(tradingDecision, false)
|
||||||
val searchResult = embeddingStore.search(
|
|
||||||
EmbeddingSearchRequest.builder()
|
|
||||||
.queryEmbedding(questionEmbedding)
|
|
||||||
.maxResults(3)
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
tradingDecision.newsContext = searchResult.matches().joinToString("\n") { it.embedded().text() }
|
|
||||||
result(tradingDecision,false)
|
|
||||||
result(decideTrading(stockCode, tradingDecision),true)
|
|
||||||
|
|
||||||
|
tradingDecision.techSummary = TechnicalAnalyzer.generateComprehensiveReport()
|
||||||
|
result(tradingDecision, false)
|
||||||
|
|
||||||
|
val question = "${corpInfo?.cName} $stockName[$stockCode]의 향후 실적 전망과 관련된 핵심 뉴스"
|
||||||
|
val questionEmbedding = embeddingModel.embed(question).content()
|
||||||
|
val searchResult = embeddingStore.search(
|
||||||
|
EmbeddingSearchRequest.builder()
|
||||||
|
.queryEmbedding(questionEmbedding)
|
||||||
|
.maxResults(3)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
tradingDecision.newsContext = searchResult.matches().joinToString("\n") { it.embedded().text() }
|
||||||
|
result(tradingDecision, false)
|
||||||
|
result(decideTrading(stockCode, tradingDecision), true)
|
||||||
|
}catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -220,7 +227,7 @@ object RagService {
|
|||||||
""".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()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -271,7 +278,7 @@ object RagService {
|
|||||||
val response = chatModel.chat(UserMessage.from(prompt))
|
val response = chatModel.chat(UserMessage.from(prompt))
|
||||||
val rawResponse = response.aiMessage().text()
|
val rawResponse = response.aiMessage().text()
|
||||||
val jsonResponse = JsonSanitizer.formatJson(rawResponse)
|
val jsonResponse = JsonSanitizer.formatJson(rawResponse)
|
||||||
println("📥 [AI Raw JSON]:\n$jsonResponse")
|
// println("📥 [AI Raw JSON]:\n$jsonResponse")
|
||||||
|
|
||||||
// 2. 유연한 파서 설정 (소수점 및 예외 상황 대응)
|
// 2. 유연한 파서 설정 (소수점 및 예외 상황 대응)
|
||||||
val lenientJson = Json {
|
val lenientJson = Json {
|
||||||
@ -283,12 +290,14 @@ object RagService {
|
|||||||
|
|
||||||
// JSON 파싱 (Kotlinx Serialization 활용)
|
// JSON 파싱 (Kotlinx Serialization 활용)
|
||||||
return try {
|
return try {
|
||||||
println(jsonResponse)
|
// println(jsonResponse)
|
||||||
val decision = lenientJson.decodeFromString<TradingDecision>(jsonResponse)
|
val decision = lenientJson.decodeFromString<TradingDecision>(jsonResponse)
|
||||||
decision.financialData = tempDecision.financialData
|
decision.financialData = tempDecision.financialData
|
||||||
decision.newsContext = tempDecision.newsContext
|
decision.newsContext = tempDecision.newsContext
|
||||||
decision.techSummary = tempDecision.techSummary
|
decision.techSummary = tempDecision.techSummary
|
||||||
decision.stockCode = tempDecision.stockCode
|
decision.stockCode = tempDecision.stockCode
|
||||||
|
decision.corpName = tempDecision.corpName
|
||||||
|
decision.stockName = tempDecision.stockName
|
||||||
decision
|
decision
|
||||||
} catch (e: dev.langchain4j.exception.InternalServerException) {
|
} catch (e: dev.langchain4j.exception.InternalServerException) {
|
||||||
// 서버 에러 (컨텍스트 초과 등) 발생 시 로그 남기고 null 반환 혹은 커스텀 에러 처리
|
// 서버 에러 (컨텍스트 초과 등) 발생 시 로그 남기고 null 반환 혹은 커스텀 에러 처리
|
||||||
@ -308,11 +317,14 @@ object RagService {
|
|||||||
}
|
}
|
||||||
@Serializable
|
@Serializable
|
||||||
class TradingDecision {
|
class TradingDecision {
|
||||||
|
var corpName : String = ""
|
||||||
|
var stockName : String = ""
|
||||||
val ultraShortScore: Double = 0.0 // 초단기 (분봉/에너지)
|
val ultraShortScore: Double = 0.0 // 초단기 (분봉/에너지)
|
||||||
val shortTermScore: Double = 0.0 // 단기 (일봉/뉴스)
|
val shortTermScore: Double = 0.0 // 단기 (일봉/뉴스)
|
||||||
val midTermScore: Double = 0.0 // 중기 (주봉/재무)
|
val midTermScore: Double = 0.0 // 중기 (주봉/재무)
|
||||||
val longTermScore: Double = 0.0
|
val longTermScore: Double = 0.0
|
||||||
|
// [추가] 화면 전환용 종목명
|
||||||
|
var currentPrice: Double = 0.0
|
||||||
var stockCode: String = ""
|
var stockCode: String = ""
|
||||||
var decision: String? = null
|
var decision: String? = null
|
||||||
var reason: String? = null
|
var reason: String? = null
|
||||||
@ -323,12 +335,18 @@ class TradingDecision {
|
|||||||
|
|
||||||
fun profitPossible() =
|
fun profitPossible() =
|
||||||
listOf<Double>(ultraShortScore,
|
listOf<Double>(ultraShortScore,
|
||||||
shortTermScore,
|
shortTermScore,
|
||||||
midTermScore,
|
midTermScore,
|
||||||
longTermScore).average()
|
longTermScore).average()
|
||||||
|
|
||||||
|
fun safePossible() =
|
||||||
|
listOf<Double>(
|
||||||
|
midTermScore,
|
||||||
|
longTermScore).average()
|
||||||
|
|
||||||
override fun toString(): String {
|
override fun toString(): String {
|
||||||
return """
|
return """
|
||||||
|
$corpName($stockName)
|
||||||
수익실현 가능성 : ${profitPossible()}
|
수익실현 가능성 : ${profitPossible()}
|
||||||
ultraShortScore :$ultraShortScore
|
ultraShortScore :$ultraShortScore
|
||||||
shortTermScore :$shortTermScore
|
shortTermScore :$shortTermScore
|
||||||
|
|||||||
@ -3,8 +3,13 @@ package service
|
|||||||
import TradingDecision
|
import TradingDecision
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import model.CandleData
|
import model.CandleData
|
||||||
|
import model.RankingType
|
||||||
|
import network.KisTradeService
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
import java.time.LocalTime
|
import java.time.LocalTime
|
||||||
import java.time.ZoneId
|
import java.time.ZoneId
|
||||||
@ -17,30 +22,139 @@ typealias TradingDecisionCallback = (TradingDecision?, Boolean)->Unit
|
|||||||
object AutoTradingManager {
|
object AutoTradingManager {
|
||||||
private val scope = CoroutineScope(Dispatchers.Default)
|
private val scope = CoroutineScope(Dispatchers.Default)
|
||||||
val targetStocks = mutableListOf<Pair<String, String>>()
|
val targetStocks = mutableListOf<Pair<String, String>>()
|
||||||
|
// 자동 발굴 루프 제어용 Job
|
||||||
|
private var discoveryJob: Job? = null
|
||||||
|
|
||||||
fun addStock(stockName : String,stockCode : String, result :TradingDecisionCallback) {
|
|
||||||
targetStocks.add(Pair(stockName, stockCode))
|
|
||||||
startTradingLoop(stockName,stockCode,result)
|
fun startAutoDiscoveryLoop(
|
||||||
|
tradeService: KisTradeService,
|
||||||
|
callback: TradingDecisionCallback
|
||||||
|
) {
|
||||||
|
if (discoveryJob?.isActive == true) return
|
||||||
|
|
||||||
|
discoveryJob = scope.launch {
|
||||||
|
println("🚀 [AutoTrading] 5분 주기 자동 발굴 시작")
|
||||||
|
|
||||||
|
while (discoveryJob?.isActive == true) {
|
||||||
|
try {
|
||||||
|
// 1. [체크] 현재 잔고 및 보유 종목 조회
|
||||||
|
val balanceResult = tradeService.fetchIntegratedBalance().getOrNull()
|
||||||
|
val myHoldings = balanceResult?.holdings?.map { it.code }?.toSet() ?: emptySet()
|
||||||
|
val myCash = balanceResult?.deposit?.replace(",", "")?.toLongOrNull() ?: 0L
|
||||||
|
|
||||||
|
println("💰 보유 현금: ${String.format("%,d", myCash)}원 | 보유 종목 수: ${myHoldings.size}")
|
||||||
|
|
||||||
|
// 2. 랭킹 데이터 가져오기
|
||||||
|
// 1. 랭킹 데이터 가져오기 (비동기)
|
||||||
|
val volRankDeferred = async { tradeService.fetchMarketRanking(RankingType.VOLUME, true).getOrDefault(emptyList()) }
|
||||||
|
val riseRankDeferred = async { tradeService.fetchMarketRanking(RankingType.RISE, true).getOrDefault(emptyList()) }
|
||||||
|
|
||||||
|
val volList = volRankDeferred.await()
|
||||||
|
val riseList = riseRankDeferred.await()
|
||||||
|
|
||||||
|
// [수정] 2. 의미 있는 후보군 선정 (단순 상위 15개가 아님)
|
||||||
|
|
||||||
|
// (A) 거래량 상위 종목 중: 현재가 기준 등락률이 0% ~ 20% 사이인 것만 필터링 -> 상위 10개
|
||||||
|
val volCandidates = volList
|
||||||
|
.filter { stock ->
|
||||||
|
val rate = stock.prdy_ctrt.toDoubleOrNull() ?: 0.0
|
||||||
|
rate in 0.0..20.0 // 0% 초과 20% 이하
|
||||||
|
}
|
||||||
|
.take(10)
|
||||||
|
|
||||||
|
// (B) 상승률 상위 종목 중: 너무 급등한(20% 초과) 종목은 제외하고, 적당히 오르고 있는 종목만 필터링 -> 상위 10개
|
||||||
|
// 보통 상승률 랭킹은 상한가(30%)부터 내려오므로, 앞쪽의 급등주를 건너뛰어야 함
|
||||||
|
val riseCandidates = riseList
|
||||||
|
.filter { stock ->
|
||||||
|
val rate = stock.prdy_ctrt.toDoubleOrNull() ?: 0.0
|
||||||
|
rate in 3.0..20.0 // 최소 3% 이상은 올라야 의미 있음, 20% 이하는 안전 구간
|
||||||
|
}
|
||||||
|
.take(10)
|
||||||
|
|
||||||
|
// 3. 두 리스트 합치기 (중복 제거)
|
||||||
|
val candidates = (volCandidates + riseCandidates).distinctBy { it.code }
|
||||||
|
|
||||||
|
println("🔎 1차 필터링 후보 ${candidates.size}개 (급등주 제외) 검증 시작...")
|
||||||
|
|
||||||
|
candidates.forEach { stock ->
|
||||||
|
// [조건 1] 이미 보유한 종목 제외
|
||||||
|
if (myHoldings.contains(stock.code)) return@forEach
|
||||||
|
|
||||||
|
val currentPrice = stock.stck_prpr.replace(",", "").toDoubleOrNull() ?: 0.0
|
||||||
|
|
||||||
|
// [조건 2] 최소 1주 매수 가능 여부
|
||||||
|
if (currentPrice > myCash) return@forEach
|
||||||
|
|
||||||
|
// 3. 일봉 데이터 조회 (필터링 용도 + TechnicalAnalyzer 입력용)
|
||||||
|
val dailyResult = tradeService.fetchPeriodChartData(stock.code, "D", true)
|
||||||
|
val dailyData = dailyResult.getOrNull()
|
||||||
|
val todayCandle = dailyData?.lastOrNull()
|
||||||
|
|
||||||
|
if (dailyData != null && todayCandle != null) {
|
||||||
|
val open = todayCandle.stck_oprc.toDoubleOrNull() ?: 0.0
|
||||||
|
val current = todayCandle.stck_prpr.toDoubleOrNull() ?: 0.0
|
||||||
|
|
||||||
|
if (open > 0) {
|
||||||
|
val riseRate = (current - open) / open * 100
|
||||||
|
|
||||||
|
// [조건 3] 상승 중(양봉)이면서 20% 이하 상승
|
||||||
|
if (riseRate > 0 && riseRate <= 20.0) {
|
||||||
|
println("✨ [발굴] ${stock.name} (+${String.format("%.1f", riseRate)}%) -> 데이터 수집 및 분석")
|
||||||
|
|
||||||
|
// [핵심 수정] AI 분석 전 필요한 차트 데이터(30분, 주봉, 월봉)를 모두 가져와 TechnicalAnalyzer에 주입
|
||||||
|
// 비동기로 동시에 요청하여 속도 향상
|
||||||
|
val min30Def = async { tradeService.fetchChartData(stock.code, true).getOrDefault(emptyList()) }
|
||||||
|
val weekDef = async { tradeService.fetchPeriodChartData(stock.code, "W", true).getOrDefault(emptyList()) }
|
||||||
|
val monthDef = async { tradeService.fetchPeriodChartData(stock.code, "M", true).getOrDefault(emptyList()) }
|
||||||
|
|
||||||
|
val min30Data = min30Def.await()
|
||||||
|
val weeklyData = weekDef.await()
|
||||||
|
val monthlyData = monthDef.await()
|
||||||
|
|
||||||
|
// TechnicalAnalyzer 상태 업데이트 (싱글톤이므로 순차 처리 필수)
|
||||||
|
TechnicalAnalyzer.clear()
|
||||||
|
TechnicalAnalyzer.daily = dailyData
|
||||||
|
TechnicalAnalyzer.weekly = weeklyData
|
||||||
|
TechnicalAnalyzer.monthly = monthlyData
|
||||||
|
TechnicalAnalyzer.min30 = min30Data
|
||||||
|
|
||||||
|
// 데이터 준비 완료 후 AI 분석 요청 (suspend 함수이므로 완료될 때까지 대기 -> 데이터 섞임 방지)
|
||||||
|
RagService.processStock(stock.name, stock.code) { decision, isSuccess ->
|
||||||
|
if (decision != null) {
|
||||||
|
decision.stockName = stock.name
|
||||||
|
decision.currentPrice = current // 차트에서 확인한 최신 현재가 주입
|
||||||
|
}
|
||||||
|
callback(decision, isSuccess) // DashboardScreen으로 전달
|
||||||
|
}
|
||||||
|
|
||||||
|
// 분석 후 잠시 대기 (서버 부하 조절)
|
||||||
|
delay(2000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delay(100) // 종목 간 API 호출 간격
|
||||||
|
}
|
||||||
|
println("💤 사이클 종료. 5분 대기...")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
println("⚠️ 루프 오류: ${e.message}")
|
||||||
|
}
|
||||||
|
delay(5 * 60 * 1000) // 5분
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startTradingLoop(stockName : String, stockCode : String, result :TradingDecisionCallback) {
|
// 루프 중단 함수
|
||||||
|
fun stopDiscovery() {
|
||||||
|
discoveryJob?.cancel()
|
||||||
|
discoveryJob = null
|
||||||
|
println("🛑 [AutoTrading] 자동 발굴 중단됨")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 기존 단일 종목 추가 로직 (유지)
|
||||||
|
fun addStock(stockName: String, stockCode: String, result: TradingDecisionCallback) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
println("🚀 10분 주기 자동 분석 및 매매 시작: ${LocalTime.now()}")
|
RagService.processStock(stockName, stockCode, result)
|
||||||
// targetStocks.forEach { stockCode ->
|
|
||||||
launch { // 종목별 병렬 분석 (M3 Pro 파워 활용)
|
|
||||||
RagService.processStock(stockName, stockCode,result)
|
|
||||||
// {decision,b ->
|
|
||||||
//// when (decision?.decision) {
|
|
||||||
//// "BUY" -> if (decision.confidence > 70) executeOrder(stockCode, "매수")
|
|
||||||
//// "SELL" -> executeOrder(stockCode, "매도")
|
|
||||||
//// else -> println("[$stockCode] 관망 유지: ${decision?.reason}")
|
|
||||||
//// }
|
|
||||||
// result(decision,b)
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
// }
|
|
||||||
// targetStocks.re
|
|
||||||
// delay(10 * 60 * 1000) // 10분 대기
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -70,6 +70,7 @@ fun AiAnalysisView(stockCode:String,stockName: String, currentPrice: String, tra
|
|||||||
AutoTradingManager.addStock(stockName,stockCode) { decision,success ->
|
AutoTradingManager.addStock(stockName,stockCode) { decision,success ->
|
||||||
aiOpinion = decision.toString()
|
aiOpinion = decision.toString()
|
||||||
isAnalyzing = !success
|
isAnalyzing = !success
|
||||||
|
tradingDecisionCallback.invoke(decision,success)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
aiOpinion = "분석 중 오류 발생: ${e.message}"
|
aiOpinion = "분석 중 오류 발생: ${e.message}"
|
||||||
|
|||||||
@ -17,6 +17,7 @@ import model.KisSession
|
|||||||
import model.StockBasicInfo
|
import model.StockBasicInfo
|
||||||
import network.KisTradeService
|
import network.KisTradeService
|
||||||
import network.KisWebSocketManager
|
import network.KisWebSocketManager
|
||||||
|
import service.AutoTradingManager
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DashboardScreen() {
|
fun DashboardScreen() {
|
||||||
@ -32,6 +33,27 @@ fun DashboardScreen() {
|
|||||||
var selectedStockInfo by remember { mutableStateOf<StockBasicInfo?>(null) } // 단순 종목 선택 시
|
var selectedStockInfo by remember { mutableStateOf<StockBasicInfo?>(null) } // 단순 종목 선택 시
|
||||||
var completeTradingDecision by remember { mutableStateOf<TradingDecision?>(null) } // 단순 종목 선택 시
|
var completeTradingDecision by remember { mutableStateOf<TradingDecision?>(null) } // 단순 종목 선택 시
|
||||||
|
|
||||||
|
DisposableEffect(Unit) {
|
||||||
|
// 1. 화면 진입 시: 자동 발굴 루프 시작
|
||||||
|
// AI 분석 결과(decision)가 나오면 completeTradingDecision 상태를 업데이트하여
|
||||||
|
// IntegratedOrderSection에서 자동으로 매수 로직이 실행되도록 연결합니다.
|
||||||
|
AutoTradingManager.startAutoDiscoveryLoop(tradeService) { decision, isSuccess ->
|
||||||
|
if (isSuccess && decision != null) {
|
||||||
|
|
||||||
|
selectedStockCode = decision.stockCode
|
||||||
|
selectedStockName = decision.stockName
|
||||||
|
isDomestic = true // 발굴 로직은 국내주식 기준이므로 true 고정
|
||||||
|
|
||||||
|
// 2. 결정 객체 업데이트 -> IntegratedOrderSection의 LaunchedEffect 트리거
|
||||||
|
completeTradingDecision = decision
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 화면 이탈 시(앱 종료 등): 루프 중단 (리소스 정리)
|
||||||
|
onDispose {
|
||||||
|
AutoTradingManager.stopDiscovery()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 중앙 관리용 상태들
|
// 중앙 관리용 상태들
|
||||||
var refreshTrigger by remember { mutableStateOf(0) }
|
var refreshTrigger by remember { mutableStateOf(0) }
|
||||||
|
|||||||
@ -70,7 +70,7 @@ fun IntegratedOrderSection(
|
|||||||
}
|
}
|
||||||
|
|
||||||
var profitRate by remember(monitoringItem) {
|
var profitRate by remember(monitoringItem) {
|
||||||
mutableStateOf(monitoringItem?.profitRate?.toString() ?: "3.0")
|
mutableStateOf(monitoringItem?.profitRate?.toString() ?: "2.0")
|
||||||
}
|
}
|
||||||
var stopLossRate by remember(monitoringItem) {
|
var stopLossRate by remember(monitoringItem) {
|
||||||
mutableStateOf(monitoringItem?.stopLossRate?.toString() ?: "-2.0")
|
mutableStateOf(monitoringItem?.stopLossRate?.toString() ?: "-2.0")
|
||||||
@ -114,12 +114,43 @@ fun IntegratedOrderSection(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
LaunchedEffect(completeTradingDecision) {
|
LaunchedEffect(completeTradingDecision) {
|
||||||
|
val MIN_CONFIDENCE = 70.0 // 최소 신뢰도
|
||||||
|
val MIN_MID_SCORE = 65.0 // 최소 중기 점수 (주봉/재무)
|
||||||
|
println("completeTradingDecision = $completeTradingDecision")
|
||||||
if (completeTradingDecision != null &&
|
if (completeTradingDecision != null &&
|
||||||
completeTradingDecision.stockCode.equals(stockCode)) {
|
completeTradingDecision.stockCode.equals(stockCode)) {
|
||||||
|
println(completeTradingDecision?.decision)
|
||||||
|
fun resultCheck(completeTradingDecision :TradingDecision) {
|
||||||
|
println("""
|
||||||
|
${completeTradingDecision.corpName}
|
||||||
|
${completeTradingDecision.confidence}
|
||||||
|
${completeTradingDecision.profitPossible()}
|
||||||
|
${completeTradingDecision.safePossible()}
|
||||||
|
""".trimIndent())
|
||||||
|
// 2. 조건 검사: 신뢰도 80 이상 AND 중기 점수 70 이상
|
||||||
|
if (completeTradingDecision.confidence >= MIN_CONFIDENCE &&
|
||||||
|
completeTradingDecision.profitPossible() >= MIN_MID_SCORE &&
|
||||||
|
completeTradingDecision.safePossible() > MIN_MID_SCORE) {
|
||||||
|
|
||||||
|
println("🚀 [조건 만족] 강력 매수 시그널 포착 -> 자동 매수 진행 (1주) ${completeTradingDecision.stockCode}")
|
||||||
|
// 3. 매수 실행 (자동 감시 켜기: true, 수량: 1주)
|
||||||
|
// 수량은 필요에 따라 로직으로 계산하여 변경 가능 (예: 자산의 10% 등)
|
||||||
|
excuteTrade(willEnableAutoSell = true, orderQty = "1")
|
||||||
|
|
||||||
|
} else {
|
||||||
|
println("✋ [조건 미달] 매수 의견이나 점수 부족으로 관망")
|
||||||
|
}
|
||||||
|
}
|
||||||
when (completeTradingDecision?.decision) {
|
when (completeTradingDecision?.decision) {
|
||||||
"BUY" -> if (completeTradingDecision.confidence > 70) excuteTrade(true, "1")
|
"BUY" -> {
|
||||||
|
println("[$stockCode] 매수 추천 resultCheck: ${completeTradingDecision?.reason}")
|
||||||
|
resultCheck(completeTradingDecision)
|
||||||
|
}
|
||||||
"SELL" -> println("[$stockCode] 매도: ${completeTradingDecision?.reason}")
|
"SELL" -> println("[$stockCode] 매도: ${completeTradingDecision?.reason}")
|
||||||
else -> println("[$stockCode] 관망 유지: ${completeTradingDecision?.reason}")
|
else -> {
|
||||||
|
resultCheck(completeTradingDecision)
|
||||||
|
println("[$stockCode] 관망 유지 resultCheck: ${completeTradingDecision?.reason}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -94,20 +94,20 @@ fun StockDetailSection(
|
|||||||
launch { tradeService.fetchPeriodChartData(stockCode, "D").onSuccess {
|
launch { tradeService.fetchPeriodChartData(stockCode, "D").onSuccess {
|
||||||
daySummary = it.takeLast(7)
|
daySummary = it.takeLast(7)
|
||||||
TechnicalAnalyzer.daily = it
|
TechnicalAnalyzer.daily = it
|
||||||
println("daySummary ${daySummary.size} total: ${it.size} ${it.firstOrNull()?.toString()}")
|
// println("daySummary ${daySummary.size} total: ${it.size} ${it.firstOrNull()?.toString()}")
|
||||||
}
|
}
|
||||||
} // 최근 7일
|
} // 최근 7일
|
||||||
launch { tradeService.fetchPeriodChartData(stockCode, "W").onSuccess {
|
launch { tradeService.fetchPeriodChartData(stockCode, "W").onSuccess {
|
||||||
weekSummary = it.takeLast(4)
|
weekSummary = it.takeLast(4)
|
||||||
TechnicalAnalyzer.weekly = it
|
TechnicalAnalyzer.weekly = it
|
||||||
println("weekSummary ${weekSummary.size} total: ${it.size} ${it.firstOrNull()?.toString()}")
|
// println("weekSummary ${weekSummary.size} total: ${it.size} ${it.firstOrNull()?.toString()}")
|
||||||
}
|
}
|
||||||
} // 최근 4주
|
} // 최근 4주
|
||||||
launch { tradeService.fetchPeriodChartData(stockCode, "M").onSuccess {
|
launch { tradeService.fetchPeriodChartData(stockCode, "M").onSuccess {
|
||||||
monthSummary = it.takeLast(6) // 최근 6개월
|
monthSummary = it.takeLast(6) // 최근 6개월
|
||||||
yearSummary = it.takeLast(36) // 최근 3년
|
yearSummary = it.takeLast(36) // 최근 3년
|
||||||
TechnicalAnalyzer.monthly = it
|
TechnicalAnalyzer.monthly = it
|
||||||
println("monthSummary ${monthSummary.size} yearSummary ${yearSummary.size} total: ${it.size} ${it.firstOrNull()?.toString()}")
|
// println("monthSummary ${monthSummary.size} yearSummary ${yearSummary.size} total: ${it.size} ${it.firstOrNull()?.toString()}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
launch {
|
launch {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user