This commit is contained in:
2026-01-23 17:05:09 +09:00
parent ac50737ea8
commit d89d793efa
16 changed files with 665 additions and 256 deletions
+4 -11
View File
@@ -26,9 +26,10 @@ import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch
import model.KisSession
import service.AutoTradingManager
import service.TradingDecisionCallback
@Composable
fun AiAnalysisView(stockCode:String,stockName: String, currentPrice: String, trades: List<model.RealTimeTrade>) {
fun AiAnalysisView(stockCode:String,stockName: String, currentPrice: String, trades: List<model.RealTimeTrade>, tradingDecisionCallback: TradingDecisionCallback) {
var aiOpinion by remember { mutableStateOf("분석 대기 중...") }
var code by remember(stockCode) {
aiOpinion = ""
@@ -66,18 +67,10 @@ fun AiAnalysisView(stockCode:String,stockName: String, currentPrice: String, tra
scope.launch {
isAnalyzing = true
try {
AutoTradingManager.addStock(stockCode) { msg,success ->
aiOpinion = msg
AutoTradingManager.addStock(stockName,stockCode) { decision,success ->
aiOpinion = decision.toString()
isAnalyzing = !success
}
// 실시간 데이터 수집부터 분석까지 한 번에 실행
// StockAnalysisManager.analyzeStockWithMultiData(
// stockCode = stockCode,
// stockName = stockName,
// result = {
// aiOpinion = it
// }
// )
} catch (e: Exception) {
aiOpinion = "분석 중 오류 발생: ${e.message}"
println(aiOpinion)
+4 -4
View File
@@ -152,10 +152,6 @@ fun UnifiedStockItemRow(holding: model.UnifiedStockHolding, onClick: () -> Unit)
}
Spacer(Modifier.width(4.dp))
Text(holding.name, fontWeight = FontWeight.Bold, maxLines = 1)
Text(
"매수: ${String.format("%,.0f", avgPrice)}${holding.quantity}",
fontSize = 11.sp, color = Color.Gray
)
}
Text(holding.code, style = MaterialTheme.typography.caption, color = androidx.compose.ui.graphics.Color.Gray)
}
@@ -174,6 +170,10 @@ fun UnifiedStockItemRow(holding: model.UnifiedStockHolding, onClick: () -> Unit)
fontSize = 12.sp,
fontWeight = FontWeight.Bold
)
Text(
"매수: ${String.format("%,.0f", avgPrice)}${holding.quantity}",
fontSize = 11.sp, color = Color.Gray
)
}
}
}
+15 -3
View File
@@ -2,6 +2,7 @@
package ui
import AutoTradeItem
import TradingDecision
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
@@ -16,7 +17,6 @@ import model.KisSession
import model.StockBasicInfo
import network.KisTradeService
import network.KisWebSocketManager
import util.MarketUtil
@Composable
fun DashboardScreen() {
@@ -30,6 +30,8 @@ fun DashboardScreen() {
var selectedItem by remember { mutableStateOf<AutoTradeItem?>(null) } // 감시/미체결 아이템 선택 시
var selectedStockInfo by remember { mutableStateOf<StockBasicInfo?>(null) } // 단순 종목 선택 시
var completeTradingDecision by remember { mutableStateOf<TradingDecision?>(null) } // 단순 종목 선택 시
// 중앙 관리용 상태들
var refreshTrigger by remember { mutableStateOf(0) }
@@ -141,7 +143,8 @@ fun DashboardScreen() {
scope.launch {
syncAndExecute(orderNo) // 매칭 시도
}
}
},
completeTradingDecision
)
} else {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
@@ -156,7 +159,12 @@ fun DashboardScreen() {
stockCode = selectedStockCode,
stockName = selectedStockName,
currentPrice = wsManager.currentPrice.value,
trades = wsManager.tradeLogs
trades = wsManager.tradeLogs,
tradingDecisionCallback = { decision,bool ->
if (bool && decision != null && KisSession.config.isSimulation) {
completeTradingDecision = decision
}
}
)
}
VerticalDivider()
@@ -196,8 +204,12 @@ fun DashboardScreen() {
}
}
}
}
@Composable
fun VerticalDivider() {
Box(Modifier.fillMaxHeight().width(1.dp).background(Color.LightGray))
+49 -33
View File
@@ -2,7 +2,7 @@
package ui
import AutoTradeItem
import androidx.compose.foundation.background
import TradingDecision
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
@@ -21,7 +21,6 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import kotlinx.coroutines.launch
import network.KisTradeService
import network.KisWebSocketManager
import util.MarketUtil
/**
@@ -40,7 +39,8 @@ fun IntegratedOrderSection(
holdingQuantity: String,
tradeService: KisTradeService,
onOrderSaved: (String) -> Unit,
onOrderResult: (String, Boolean) -> Unit
onOrderResult: (String, Boolean) -> Unit,
completeTradingDecision: TradingDecision?
) {
val scope = rememberCoroutineScope()
@@ -60,6 +60,7 @@ fun IntegratedOrderSection(
}
// UI 입력 상태
var orderPrice by remember { mutableStateOf("") } // 빈 값이면 시장가
var orderQty by remember(holdingQuantity) {
@@ -80,6 +81,48 @@ fun IntegratedOrderSection(
val basePrice = (if (orderPrice.isEmpty()) curPriceNum else orderPrice.toDoubleOrNull() ?: 0.0)
val inputQty = orderQty.replace(",", "").toIntOrNull() ?: 0
fun excuteTrade(willEnableAutoSell: Boolean,orderQty: String) {
scope.launch {
val finalPrice = if (orderPrice.isBlank()) "0" else orderPrice
tradeService.postOrder(stockCode, orderQty, finalPrice, isBuy = true)
.onSuccess { realOrderNo -> // KIS 서버에서 생성된 실제 주문번호
onOrderResult("주문 성공: $realOrderNo", true)
if (willEnableAutoSell) {
val pRate = profitRate.toDoubleOrNull() ?: 0.0
val sRate = stopLossRate.toDoubleOrNull() ?: 0.0
val calculatedTarget = MarketUtil.roundToTickSize(basePrice * (1 + pRate / 100.0))
val calculatedStop = MarketUtil.roundToTickSize(basePrice * (1 + sRate / 100.0))
DatabaseFactory.saveAutoTrade(AutoTradeItem(
orderNo = realOrderNo, // 실제 주문번호 저장 (중심 관리 원칙)
code = stockCode,
name = stockName,
quantity = inputQty,
profitRate = pRate,
stopLossRate = sRate,
targetPrice = calculatedTarget,
stopLossPrice = calculatedStop,
status = "PENDING_BUY", // 체결 전까지 PENDING_BUY 상태
isDomestic = isDomestic
))
monitoringItem = DatabaseFactory.findConfigByCode(stockCode)
onOrderSaved(realOrderNo)
onOrderResult("매수 및 즉시 체결 확인: $realOrderNo", true)
}
}
.onFailure { onOrderResult(it.message ?: "매수 실패", false) }
}
}
LaunchedEffect(completeTradingDecision) {
if (completeTradingDecision != null &&
completeTradingDecision.stockCode.equals(stockCode)) {
when (completeTradingDecision?.decision) {
"BUY" -> if (completeTradingDecision.confidence > 70) excuteTrade(true, "1")
"SELL" -> println("[$stockCode] 매도: ${completeTradingDecision?.reason}")
else -> println("[$stockCode] 관망 유지: ${completeTradingDecision?.reason}")
}
}
}
Column(modifier = Modifier.fillMaxWidth().padding(8.dp)) {
Text("주문 및 자동 매도 설정", style = MaterialTheme.typography.subtitle2, fontWeight = FontWeight.Bold)
@@ -171,36 +214,7 @@ fun IntegratedOrderSection(
// 매수 버튼
Button(
onClick = {
scope.launch {
val finalPrice = if (orderPrice.isBlank()) "0" else orderPrice
tradeService.postOrder(stockCode, orderQty, finalPrice, isBuy = true)
.onSuccess { realOrderNo -> // KIS 서버에서 생성된 실제 주문번호
onOrderResult("주문 성공: $realOrderNo", true)
if (willEnableAutoSell) {
val pRate = profitRate.toDoubleOrNull() ?: 0.0
val sRate = stopLossRate.toDoubleOrNull() ?: 0.0
val calculatedTarget = MarketUtil.roundToTickSize(basePrice * (1 + pRate / 100.0))
val calculatedStop = MarketUtil.roundToTickSize(basePrice * (1 + sRate / 100.0))
DatabaseFactory.saveAutoTrade(AutoTradeItem(
orderNo = realOrderNo, // 실제 주문번호 저장 (중심 관리 원칙)
code = stockCode,
name = stockName,
quantity = inputQty,
profitRate = pRate,
stopLossRate = sRate,
targetPrice = calculatedTarget,
stopLossPrice = calculatedStop,
status = "PENDING_BUY", // 체결 전까지 PENDING_BUY 상태
isDomestic = isDomestic
))
monitoringItem = DatabaseFactory.findConfigByCode(stockCode)
onOrderSaved(realOrderNo)
onOrderResult("매수 및 즉시 체결 확인: $realOrderNo", true)
}
}
.onFailure { onOrderResult(it.message ?: "매수 실패", false) }
}
excuteTrade(willEnableAutoSell,orderQty)
},
modifier = Modifier.weight(1f).padding(end = 4.dp),
colors = ButtonDefaults.buttonColors(backgroundColor = Color(0xFFE03E2D))
@@ -224,6 +238,8 @@ fun IntegratedOrderSection(
) { Text("매도", color = Color.White) }
}
}
}
@Composable
+32 -28
View File
@@ -2,15 +2,10 @@ package ui
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import TradingDecision
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items // 반드시 수동 import 확인
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
import androidx.compose.runtime.*
import io.ktor.client.engine.cio.CIO
// 아래 두 import가 'delegate' 에러를 해결합니다.
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
@@ -18,20 +13,15 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import model.AppConfig
import model.BalanceSummary
import model.CandleData
import model.RankingStock
import model.StockHolding
import network.DartCodeManager
import network.KisTradeService
import network.KisWebSocketManager
import network.NewsService
import service.TechnicalAnalyzer
import java.time.LocalTime
import java.time.format.DateTimeFormatter
@@ -41,11 +31,12 @@ import kotlin.collections.isNotEmpty
fun StockDetailSection(
stockCode: String,
stockName: String,
holdingQuantity : String,
holdingQuantity: String,
isDomestic: Boolean,
tradeService: KisTradeService,
wsManager: KisWebSocketManager,
onOrderSaved: (String) -> Unit
onOrderSaved: (String) -> Unit,
completeTradingDecision: TradingDecision?
) {
var openPrice by remember { mutableStateOf("0") }
@@ -65,11 +56,7 @@ fun StockDetailSection(
if (daySummary.size >= 2) daySummary[daySummary.size - 2].stck_prpr else "0"
}
fun calculateAvg(data: List<CandleData>): String {
if (data.isEmpty()) return "0"
val avg = data.map { it.stck_prpr.toDoubleOrNull() ?: 0.0 }.average()
return String.format("%,d", avg.toLong())
}
// 이전 종목 코드를 기억하기 위한 상태
var previousCode by remember { mutableStateOf("") }
@@ -88,6 +75,7 @@ fun StockDetailSection(
wsManager.subscribeStock(stockCode)
previousCode = stockCode
// 2. 차트 데이터 로드 (KisSession 기반으로 파라미터 간소화)
coroutineScope {
@@ -101,18 +89,33 @@ fun StockDetailSection(
.onFailure { error ->
println("❌ 차트 데이터 로드 실패: ${error.localizedMessage}")
chartData = emptyList()
}}
}
}
launch { tradeService.fetchPeriodChartData(stockCode, "D").onSuccess {
daySummary = it.takeLast(7) }
TechnicalAnalyzer.daily = daySummary
daySummary = it.takeLast(7)
TechnicalAnalyzer.daily = it
println("daySummary ${daySummary.size} total: ${it.size} ${it.firstOrNull()?.toString()}")
}
} // 최근 7일
launch { tradeService.fetchPeriodChartData(stockCode, "W").onSuccess { weekSummary = it.takeLast(4) }
TechnicalAnalyzer.weekly = weekSummary} // 최근 4주
launch { tradeService.fetchPeriodChartData(stockCode, "W").onSuccess {
weekSummary = it.takeLast(4)
TechnicalAnalyzer.weekly = it
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 = yearSummary
}}
TechnicalAnalyzer.monthly = it
println("monthSummary ${monthSummary.size} yearSummary ${yearSummary.size} total: ${it.size} ${it.firstOrNull()?.toString()}")
}
}
launch {
DartCodeManager.getCorpCode(stockCode)?.let {
it.stockName = stockName
NewsService.fetchAndIngestNews(it)
}
}
}
isLoading = false
}
@@ -230,7 +233,8 @@ fun StockDetailSection(
onOrderResult = { msg, success ->
resultMessage = msg
isSuccess = success
}
},
completeTradingDecision
)
}
}