....
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user