...
This commit is contained in:
@@ -26,10 +26,11 @@ import androidx.compose.ui.unit.dp
|
||||
import kotlinx.coroutines.launch
|
||||
import model.KisSession
|
||||
import service.AutoTradingManager
|
||||
import service.TechnicalAnalyzer
|
||||
import service.TradingDecisionCallback
|
||||
|
||||
@Composable
|
||||
fun AiAnalysisView(stockCode:String,stockName: String, currentPrice: String, trades: List<model.RealTimeTrade>, tradingDecisionCallback: TradingDecisionCallback) {
|
||||
fun AiAnalysisView(technicalAnalyzer: TechnicalAnalyzer,stockCode:String,stockName: String, currentPrice: String, trades: List<model.RealTimeTrade>, tradingDecisionCallback: TradingDecisionCallback) {
|
||||
var aiOpinion by remember { mutableStateOf("분석 대기 중...") }
|
||||
var code by remember(stockCode) {
|
||||
aiOpinion = ""
|
||||
@@ -67,7 +68,7 @@ fun AiAnalysisView(stockCode:String,stockName: String, currentPrice: String, tra
|
||||
scope.launch {
|
||||
isAnalyzing = true
|
||||
try {
|
||||
AutoTradingManager.addStock(stockName,stockCode) { decision,success ->
|
||||
AutoTradingManager.addStock(technicalAnalyzer,stockName,stockCode) { decision,success ->
|
||||
aiOpinion = decision.toString()
|
||||
isAnalyzing = !success
|
||||
tradingDecisionCallback.invoke(decision,success)
|
||||
|
||||
@@ -12,12 +12,18 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.coroutines.launch
|
||||
import model.CandleData
|
||||
import model.ExecutionData
|
||||
import model.KisSession
|
||||
import model.StockBasicInfo
|
||||
import model.feesAndTaxRate
|
||||
import model.minimumNetProfit
|
||||
import network.KisTradeService
|
||||
import network.KisWebSocketManager
|
||||
import service.AutoTradingManager
|
||||
import service.TechnicalAnalyzer
|
||||
import util.MarketUtil
|
||||
import kotlin.collections.mutableListOf
|
||||
|
||||
@Composable
|
||||
fun DashboardScreen() {
|
||||
@@ -33,17 +39,33 @@ fun DashboardScreen() {
|
||||
var selectedStockInfo by remember { mutableStateOf<StockBasicInfo?>(null) } // 단순 종목 선택 시
|
||||
var completeTradingDecision by remember { mutableStateOf<TradingDecision?>(null) } // 단순 종목 선택 시
|
||||
|
||||
|
||||
var min30 by remember { mutableStateOf<MutableList<CandleData>>(mutableListOf()) }
|
||||
var daySummary by remember { mutableStateOf<MutableList<CandleData>>(mutableListOf()) }
|
||||
var weekSummary by remember { mutableStateOf<MutableList<CandleData>>(mutableListOf()) }
|
||||
var monthSummary by remember { mutableStateOf<MutableList<CandleData>>(mutableListOf()) }
|
||||
var yearSummary by remember { mutableStateOf<MutableList<CandleData>>(mutableListOf()) }
|
||||
|
||||
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 고정
|
||||
if (!isSuccess && decision?.confidence ?: 0.0 < 0.0) {
|
||||
decision?.stockCode?.let { stockCode ->
|
||||
decision?.stockName?.let { stockName ->
|
||||
selectedStockCode = stockCode
|
||||
selectedStockName = stockName
|
||||
isDomestic = true // 발굴 로직은 국내주식 기준이므로 true 고정
|
||||
}
|
||||
}
|
||||
|
||||
}else if (isSuccess && decision != null) {
|
||||
if (!selectedStockCode.equals(decision.stockCode) && selectedStockName.equals(decision.stockName)) {
|
||||
selectedStockCode = decision.stockCode
|
||||
selectedStockName = decision.stockName
|
||||
isDomestic = true // 발굴 로직은 국내주식 기준이므로 true 고정
|
||||
}
|
||||
// 2. 결정 객체 업데이트 -> IntegratedOrderSection의 LaunchedEffect 트리거
|
||||
completeTradingDecision = decision
|
||||
}
|
||||
@@ -62,29 +84,40 @@ fun DashboardScreen() {
|
||||
val processingIds = remember { mutableSetOf<String>() } // 주문번호 기준 잠금
|
||||
// [중앙 관리 함수] 체결 정보와 DB 정보를 매칭하여 실행
|
||||
suspend fun syncAndExecute(orderNo: String) {
|
||||
// 이미 다른 코루틴에서 이 주문을 처리 중이라면 즉시 종료 (중복 방지)
|
||||
if (processingIds.contains(orderNo)) return
|
||||
processingIds.add(orderNo)
|
||||
|
||||
try {
|
||||
// DB 아이템과 체결 데이터(캐시)를 모두 가져옴
|
||||
val dbItem = DatabaseFactory.findByOrderNo(orderNo)
|
||||
val execData = executionCache[orderNo]
|
||||
|
||||
// 둘 다 존재할 때만 로직 실행
|
||||
if (dbItem != null && execData != null && execData.isFilled) {
|
||||
if (dbItem.status == TradeStatus.PENDING_BUY) {
|
||||
println("🎯 [매칭 성공] 익절 주문 실행: ${dbItem.name} (${orderNo})")
|
||||
// 1. 실제 매수 체결가 가져오기 (문자열인 경우 숫자로 변환)
|
||||
val actualBuyPrice = execData.price.toDoubleOrNull() ?: dbItem.targetPrice
|
||||
|
||||
// 2. 최소 마진 설정 (수수료/세금 0.3% + 순수익 1.5% = 1.8%)
|
||||
|
||||
val minEffectiveRate = minimumNetProfit + feesAndTaxRate
|
||||
|
||||
// 3. DB에 설정된 목표 수익률과 최소 보장 수익률 중 큰 값 선택
|
||||
val finalProfitRate = maxOf(dbItem.profitRate, minEffectiveRate)
|
||||
|
||||
// 4. 실제 체결가 기준 익절 가격 재계산 및 틱 사이즈 보정
|
||||
val finalTargetPrice = MarketUtil.roundToTickSize(actualBuyPrice * (1 + finalProfitRate / 100.0))
|
||||
|
||||
println("🎯 [매칭 성공] 익절 주문 실행: ${dbItem.name} | 매수가: ${actualBuyPrice.toInt()} -> 목표가: ${finalTargetPrice.toInt()} (${String.format("%.2f", finalProfitRate)}% 적용)")
|
||||
|
||||
val sellPrice = dbItem.targetPrice.toLong().toString()
|
||||
tradeService.postOrder(
|
||||
stockCode = dbItem.code,
|
||||
qty = dbItem.quantity.toString(),
|
||||
price = sellPrice,
|
||||
price = finalTargetPrice.toLong().toString(),
|
||||
isBuy = false
|
||||
).onSuccess { newSellOrderNo ->
|
||||
// 익절가 업데이트 및 상태 변경
|
||||
DatabaseFactory.updateStatusAndOrderNo(dbItem.id!!, TradeStatus.SELLING, newSellOrderNo)
|
||||
// 처리가 완료된 체결 데이터는 캐시에서 삭제
|
||||
// (선택 사항) 실제 계산된 익절가를 DB에 기록하고 싶다면 별도 update 로직 추가 가능
|
||||
|
||||
executionCache.remove(orderNo)
|
||||
refreshTrigger++
|
||||
}.onFailure {
|
||||
@@ -98,7 +131,6 @@ fun DashboardScreen() {
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
// 처리가 끝나면(성공/실패/매칭실패 모두) 잠금 해제
|
||||
processingIds.remove(orderNo)
|
||||
}
|
||||
}
|
||||
@@ -155,6 +187,11 @@ fun DashboardScreen() {
|
||||
Column(modifier = Modifier.weight(0.40f).fillMaxHeight().background(Color.White)) {
|
||||
if (selectedStockCode.isNotEmpty()) {
|
||||
StockDetailSection(
|
||||
min30 = min30,
|
||||
daySummary = daySummary,
|
||||
monthSummary = monthSummary,
|
||||
weekSummary = weekSummary,
|
||||
yearSummary = yearSummary,
|
||||
stockCode = selectedStockCode,
|
||||
stockName = selectedStockName,
|
||||
holdingQuantity = selectedStockQuantity,
|
||||
@@ -166,7 +203,7 @@ fun DashboardScreen() {
|
||||
syncAndExecute(orderNo) // 매칭 시도
|
||||
}
|
||||
},
|
||||
completeTradingDecision
|
||||
completeTradingDecision = completeTradingDecision,
|
||||
)
|
||||
} else {
|
||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
@@ -178,6 +215,13 @@ fun DashboardScreen() {
|
||||
VerticalDivider()
|
||||
Column(modifier = Modifier.weight(0.2f).fillMaxHeight().padding(8.dp)) {
|
||||
AiAnalysisView(
|
||||
technicalAnalyzer = TechnicalAnalyzer().apply {
|
||||
this.min30 = min30
|
||||
this.daily = daySummary
|
||||
this.weekly = weekSummary
|
||||
this.monthly = monthSummary
|
||||
this.weekly = weekSummary
|
||||
},
|
||||
stockCode = selectedStockCode,
|
||||
stockName = selectedStockName,
|
||||
currentPrice = wsManager.currentPrice.value,
|
||||
|
||||
@@ -20,6 +20,8 @@ import androidx.compose.ui.unit.TextUnit
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import kotlinx.coroutines.launch
|
||||
import model.feesAndTaxRate
|
||||
import model.minimumNetProfit
|
||||
import network.KisTradeService
|
||||
import util.MarketUtil
|
||||
|
||||
@@ -70,10 +72,10 @@ fun IntegratedOrderSection(
|
||||
}
|
||||
|
||||
var profitRate by remember(monitoringItem) {
|
||||
mutableStateOf(monitoringItem?.profitRate?.toString() ?: "2.0")
|
||||
mutableStateOf(monitoringItem?.profitRate?.toString() ?: "0.8")
|
||||
}
|
||||
var stopLossRate by remember(monitoringItem) {
|
||||
mutableStateOf(monitoringItem?.stopLossRate?.toString() ?: "-2.0")
|
||||
mutableStateOf(monitoringItem?.stopLossRate?.toString() ?: "-1.5")
|
||||
}
|
||||
|
||||
// 계산용 변수
|
||||
@@ -83,31 +85,51 @@ fun IntegratedOrderSection(
|
||||
|
||||
fun excuteTrade(willEnableAutoSell: Boolean,orderQty: String) {
|
||||
scope.launch {
|
||||
val finalPrice = if (orderPrice.isBlank()) "0" else orderPrice
|
||||
val tickSize = MarketUtil.getTickSize(basePrice)
|
||||
val oneTickLowerPrice = basePrice - tickSize
|
||||
|
||||
// 2. 주문 가격 설정 (직접 입력값이 없으면 한 틱 낮은 가격 사용)
|
||||
val finalPrice = if (orderPrice.isBlank()) {
|
||||
oneTickLowerPrice.toLong().toString()
|
||||
} else {
|
||||
orderPrice
|
||||
}
|
||||
println("basePrice : $basePrice, oneTickLowerPrice : $oneTickLowerPrice, finalPrice : $finalPrice")
|
||||
tradeService.postOrder(stockCode, orderQty, finalPrice, isBuy = true)
|
||||
.onSuccess { realOrderNo -> // KIS 서버에서 생성된 실제 주문번호
|
||||
onOrderResult("주문 성공: $realOrderNo", true)
|
||||
if (willEnableAutoSell) {
|
||||
// 1. 기본 설정값 파싱
|
||||
val pRate = profitRate.toDoubleOrNull() ?: 0.0
|
||||
val sRate = stopLossRate.toDoubleOrNull() ?: 0.0
|
||||
val calculatedTarget = MarketUtil.roundToTickSize(basePrice * (1 + pRate / 100.0))
|
||||
|
||||
// 2. 수수료 및 세금 보정치 설정 (국내 주식 기준 약 0.25% ~ 0.3%)
|
||||
// 유관기관 수수료 및 매도세금을 고려하여 안전하게 0.3%로 잡거나, 필요시 더 높게 설정 가능합니다.
|
||||
|
||||
// 3. 실질 목표 수익률 계산
|
||||
// 사용자가 입력한 pRate와 (최소 순수익 + 제반 비용) 중 큰 값을 선택합니다.
|
||||
val effectiveProfitRate = maxOf(pRate, minimumNetProfit + feesAndTaxRate)
|
||||
|
||||
// 4. 보정된 수익률을 적용하여 목표가 계산
|
||||
val calculatedTarget = MarketUtil.roundToTickSize(basePrice * (1 + effectiveProfitRate / 100.0))
|
||||
val calculatedStop = MarketUtil.roundToTickSize(basePrice * (1 + sRate / 100.0))
|
||||
|
||||
// 5. DB 저장 (effectiveProfitRate를 저장하여 분석 시 실제 목표치를 확인 가능하게 함)
|
||||
DatabaseFactory.saveAutoTrade(AutoTradeItem(
|
||||
orderNo = realOrderNo, // 실제 주문번호 저장 (중심 관리 원칙)
|
||||
orderNo = realOrderNo,
|
||||
code = stockCode,
|
||||
name = stockName,
|
||||
quantity = inputQty,
|
||||
profitRate = pRate,
|
||||
profitRate = effectiveProfitRate, // 보정된 수익률 저장
|
||||
stopLossRate = sRate,
|
||||
targetPrice = calculatedTarget,
|
||||
stopLossPrice = calculatedStop,
|
||||
status = "PENDING_BUY", // 체결 전까지 PENDING_BUY 상태
|
||||
status = "PENDING_BUY",
|
||||
isDomestic = isDomestic
|
||||
))
|
||||
monitoringItem = DatabaseFactory.findConfigByCode(stockCode)
|
||||
onOrderSaved(realOrderNo)
|
||||
onOrderResult("매수 및 즉시 체결 확인: $realOrderNo", true)
|
||||
onOrderResult("매수 및 감시 설정 완료 (목표 수익률: ${String.format("%.2f", effectiveProfitRate)}%): $realOrderNo", true)
|
||||
}
|
||||
}
|
||||
.onFailure { onOrderResult(it.message ?: "매수 실패", false) }
|
||||
@@ -115,22 +137,27 @@ fun IntegratedOrderSection(
|
||||
}
|
||||
LaunchedEffect(completeTradingDecision) {
|
||||
val MIN_CONFIDENCE = 70.0 // 최소 신뢰도
|
||||
val MIN_MID_SCORE = 65.0 // 최소 중기 점수 (주봉/재무)
|
||||
println("completeTradingDecision = $completeTradingDecision")
|
||||
val MIN_SAFE_SCORE = 65.0 // 최소 중기 점수 (주봉/재무)
|
||||
val MIN_POSSIBLE_SCORE = 55.0 // 최소 중기 점수 (주봉/재무)
|
||||
val MIN_SHORT_SCORE = 60.0 // 최소 중기 점수 (주봉/재무)
|
||||
var append = 0.0
|
||||
if (completeTradingDecision != null &&
|
||||
completeTradingDecision.stockCode.equals(stockCode)) {
|
||||
println(completeTradingDecision?.decision)
|
||||
|
||||
fun resultCheck(completeTradingDecision :TradingDecision) {
|
||||
println("""
|
||||
${completeTradingDecision.corpName}
|
||||
${completeTradingDecision.confidence}
|
||||
${completeTradingDecision.profitPossible()}
|
||||
${completeTradingDecision.safePossible()}
|
||||
corpName : ${completeTradingDecision.corpName}
|
||||
confidence : ${completeTradingDecision.confidence + append}
|
||||
shortPossible : ${completeTradingDecision.shortPossible() + append}
|
||||
profitPossible : ${completeTradingDecision.profitPossible()+ append}
|
||||
safePossible : ${completeTradingDecision.safePossible()+ append}
|
||||
""".trimIndent())
|
||||
// 2. 조건 검사: 신뢰도 80 이상 AND 중기 점수 70 이상
|
||||
if (completeTradingDecision.confidence >= MIN_CONFIDENCE &&
|
||||
completeTradingDecision.profitPossible() >= MIN_MID_SCORE &&
|
||||
completeTradingDecision.safePossible() > MIN_MID_SCORE) {
|
||||
if (completeTradingDecision.confidence + append >= MIN_CONFIDENCE &&
|
||||
completeTradingDecision.shortPossible() + append >= MIN_SHORT_SCORE &&
|
||||
completeTradingDecision.profitPossible() + append >= MIN_POSSIBLE_SCORE &&
|
||||
completeTradingDecision.safePossible() + append >= MIN_SAFE_SCORE
|
||||
) {
|
||||
|
||||
println("🚀 [조건 만족] 강력 매수 시그널 포착 -> 자동 매수 진행 (1주) ${completeTradingDecision.stockCode}")
|
||||
// 3. 매수 실행 (자동 감시 켜기: true, 수량: 1주)
|
||||
@@ -143,11 +170,13 @@ fun IntegratedOrderSection(
|
||||
}
|
||||
when (completeTradingDecision?.decision) {
|
||||
"BUY" -> {
|
||||
append = 3.0
|
||||
println("[$stockCode] 매수 추천 resultCheck: ${completeTradingDecision?.reason}")
|
||||
resultCheck(completeTradingDecision)
|
||||
}
|
||||
"SELL" -> println("[$stockCode] 매도: ${completeTradingDecision?.reason}")
|
||||
else -> {
|
||||
append = 0.0
|
||||
resultCheck(completeTradingDecision)
|
||||
println("[$stockCode] 관망 유지 resultCheck: ${completeTradingDecision?.reason}")
|
||||
}
|
||||
|
||||
@@ -36,7 +36,12 @@ fun StockDetailSection(
|
||||
tradeService: KisTradeService,
|
||||
wsManager: KisWebSocketManager,
|
||||
onOrderSaved: (String) -> Unit,
|
||||
completeTradingDecision: TradingDecision?
|
||||
completeTradingDecision: TradingDecision?,
|
||||
min30 : MutableList<CandleData>,
|
||||
daySummary : MutableList<CandleData>,
|
||||
weekSummary : MutableList<CandleData>,
|
||||
monthSummary : MutableList<CandleData>,
|
||||
yearSummary : MutableList<CandleData>
|
||||
) {
|
||||
|
||||
var openPrice by remember { mutableStateOf("0") }
|
||||
@@ -44,10 +49,7 @@ fun StockDetailSection(
|
||||
var isLoading by remember { mutableStateOf(false) }
|
||||
var resultMessage by remember { mutableStateOf("") }
|
||||
var isSuccess by remember { mutableStateOf(true) }
|
||||
var daySummary by remember { mutableStateOf<List<CandleData>>(emptyList()) }
|
||||
var weekSummary by remember { mutableStateOf<List<CandleData>>(emptyList()) }
|
||||
var monthSummary by remember { mutableStateOf<List<CandleData>>(emptyList()) }
|
||||
var yearSummary by remember { mutableStateOf<List<CandleData>>(emptyList()) }
|
||||
|
||||
|
||||
val todayOpen = remember(daySummary) {
|
||||
daySummary.lastOrNull()?.stck_oprc ?: "0"
|
||||
@@ -79,12 +81,13 @@ fun StockDetailSection(
|
||||
// 2. 차트 데이터 로드 (KisSession 기반으로 파라미터 간소화)
|
||||
|
||||
coroutineScope {
|
||||
TechnicalAnalyzer.clear()
|
||||
|
||||
launch {tradeService.fetchChartData(stockCode, isDomestic)
|
||||
.onSuccess { data ->
|
||||
println("✅ 차트 데이터 로드 성공: ${data.size}개") // ${} 사용하여 정확히 출력
|
||||
chartData = data
|
||||
TechnicalAnalyzer.min30 = chartData
|
||||
min30.clear()
|
||||
min30.addAll(chartData)
|
||||
}
|
||||
.onFailure { error ->
|
||||
println("❌ 차트 데이터 로드 실패: ${error.localizedMessage}")
|
||||
@@ -92,22 +95,21 @@ fun StockDetailSection(
|
||||
}
|
||||
}
|
||||
launch { tradeService.fetchPeriodChartData(stockCode, "D").onSuccess {
|
||||
daySummary = it.takeLast(7)
|
||||
TechnicalAnalyzer.daily = it
|
||||
// println("daySummary ${daySummary.size} total: ${it.size} ${it.firstOrNull()?.toString()}")
|
||||
daySummary.clear()
|
||||
daySummary.addAll(it)
|
||||
}
|
||||
} // 최근 7일
|
||||
launch { tradeService.fetchPeriodChartData(stockCode, "W").onSuccess {
|
||||
weekSummary = it.takeLast(4)
|
||||
TechnicalAnalyzer.weekly = it
|
||||
weekSummary.clear()
|
||||
weekSummary.addAll(it.takeLast(4))
|
||||
// println("weekSummary ${weekSummary.size} total: ${it.size} ${it.firstOrNull()?.toString()}")
|
||||
}
|
||||
} // 최근 4주
|
||||
launch { tradeService.fetchPeriodChartData(stockCode, "M").onSuccess {
|
||||
monthSummary = it.takeLast(6) // 최근 6개월
|
||||
yearSummary = it.takeLast(36) // 최근 3년
|
||||
TechnicalAnalyzer.monthly = it
|
||||
// println("monthSummary ${monthSummary.size} yearSummary ${yearSummary.size} total: ${it.size} ${it.firstOrNull()?.toString()}")
|
||||
monthSummary.clear()
|
||||
monthSummary.addAll(it.takeLast(6))
|
||||
yearSummary.clear()
|
||||
yearSummary.addAll(it.takeLast(36))
|
||||
}
|
||||
}
|
||||
launch {
|
||||
|
||||
Reference in New Issue
Block a user