ㅎㅎㅎ
This commit is contained in:
@@ -1,9 +1,12 @@
|
||||
package ui
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.Button
|
||||
import androidx.compose.material.ButtonDefaults
|
||||
import androidx.compose.material.Card
|
||||
import androidx.compose.material.CircularProgressIndicator
|
||||
import androidx.compose.material.Divider
|
||||
@@ -20,16 +23,16 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import kotlinx.coroutines.launch
|
||||
import model.KisSession
|
||||
import model.RealTimeTrade
|
||||
import network.AiService
|
||||
import service.StockAnalysisManager
|
||||
import service.AutoTradingManager
|
||||
|
||||
@Composable
|
||||
fun AiAnalysisView(stockName: String, currentPrice: String, trades: List<model.RealTimeTrade>) {
|
||||
fun AiAnalysisView(stockCode:String,stockName: String, currentPrice: String, trades: List<model.RealTimeTrade>) {
|
||||
var aiOpinion by remember { mutableStateOf("분석 대기 중...") }
|
||||
var code by remember(stockCode) {
|
||||
mutableStateOf(stockCode.isNotEmpty())
|
||||
}
|
||||
var isAnalyzing by remember { mutableStateOf(false) }
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
@@ -44,10 +47,15 @@ fun AiAnalysisView(stockName: String, currentPrice: String, trades: List<model.R
|
||||
backgroundColor = if (isModelConfigured) Color(0xFFF1F3F4) else Color(0xFFFFEBEE),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Column(modifier = Modifier.padding(12.dp)) {
|
||||
Column(modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
.fillMaxWidth()
|
||||
.verticalScroll(rememberScrollState()) // 스크롤 활성화
|
||||
.padding(16.dp)
|
||||
.background(Color(0xFFF5F5F5), RoundedCornerShape(8.dp))) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
text = if (isModelConfigured) "🤖 AI 투자 전략" else "⚠️ AI 설정 필요",
|
||||
text = if (isModelConfigured) "${stockName} AI 투자 전략" else "⚠️ AI 설정 필요",
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = if (isModelConfigured) Color(0xFF1A73E8) else Color.Red
|
||||
)
|
||||
@@ -57,26 +65,35 @@ fun AiAnalysisView(stockName: String, currentPrice: String, trades: List<model.R
|
||||
scope.launch {
|
||||
isAnalyzing = true
|
||||
try {
|
||||
AutoTradingManager.addStock(stockCode) { msg,success ->
|
||||
aiOpinion = msg
|
||||
isAnalyzing = !success
|
||||
}
|
||||
// 실시간 데이터 수집부터 분석까지 한 번에 실행
|
||||
aiOpinion = StockAnalysisManager.analyzeStockWithRealTimeData(
|
||||
stockName = stockName,
|
||||
currentPrice = currentPrice
|
||||
)
|
||||
// StockAnalysisManager.analyzeStockWithMultiData(
|
||||
// stockCode = stockCode,
|
||||
// stockName = stockName,
|
||||
// result = {
|
||||
// aiOpinion = it
|
||||
// }
|
||||
// )
|
||||
} catch (e: Exception) {
|
||||
aiOpinion = "분석 중 오류 발생: ${e.message}"
|
||||
} finally {
|
||||
println(aiOpinion)
|
||||
isAnalyzing = false
|
||||
} finally {
|
||||
// isAnalyzing = false
|
||||
}
|
||||
}
|
||||
},
|
||||
enabled = !isAnalyzing
|
||||
enabled = !isAnalyzing && code
|
||||
) {
|
||||
if (isAnalyzing) {
|
||||
CircularProgressIndicator(modifier = Modifier.size(20.dp), color = Color.White)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text("뉴스 분석 중...")
|
||||
} else {
|
||||
Text("AI 실시간 전략 분석")
|
||||
Text("분석 요청")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ fun CandleChart(data: List<CandleData>, modifier: Modifier = Modifier) {
|
||||
|
||||
data.forEachIndexed { index, candle ->
|
||||
val open = candle.stck_oprc.toDoubleOrNull() ?: 0.0
|
||||
val close = candle.stck_clpr.toDoubleOrNull() ?: 0.0
|
||||
val close = candle.stck_prpr.toDoubleOrNull() ?: 0.0
|
||||
val high = candle.stck_hgpr.toDoubleOrNull() ?: 0.0
|
||||
val low = candle.stck_lwpr.toDoubleOrNull() ?: 0.0
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ import util.MarketUtil
|
||||
|
||||
@Composable
|
||||
fun DashboardScreen() {
|
||||
val tradeService = remember { KisTradeService() }
|
||||
val tradeService = remember { KisTradeService }
|
||||
val wsManager = remember { KisWebSocketManager() }
|
||||
val scope = rememberCoroutineScope()
|
||||
var selectedStockCode by remember { mutableStateOf("") }
|
||||
@@ -113,7 +113,7 @@ fun DashboardScreen() {
|
||||
|
||||
Row(modifier = Modifier.fillMaxSize().background(Color(0xFFF2F2F2))) {
|
||||
// [좌측 25%] 내 자산 및 통합 잔고
|
||||
Column(modifier = Modifier.weight(0.18f).fillMaxHeight().padding(8.dp)) {
|
||||
Column(modifier = Modifier.weight(0.125f).fillMaxHeight().padding(8.dp)) {
|
||||
BalanceSection(tradeService,
|
||||
onRefresh = { refreshTrigger++ },
|
||||
refreshTrigger = refreshTrigger) { code, name, isDom,qty ->
|
||||
@@ -128,7 +128,7 @@ fun DashboardScreen() {
|
||||
VerticalDivider()
|
||||
|
||||
// [중앙 45%] 실시간 정보 및 주문
|
||||
Column(modifier = Modifier.weight(0.45f).fillMaxHeight().background(Color.White)) {
|
||||
Column(modifier = Modifier.weight(0.40f).fillMaxHeight().background(Color.White)) {
|
||||
if (selectedStockCode.isNotEmpty()) {
|
||||
StockDetailSection(
|
||||
stockCode = selectedStockCode,
|
||||
@@ -151,7 +151,16 @@ fun DashboardScreen() {
|
||||
}
|
||||
|
||||
VerticalDivider()
|
||||
Column(modifier = Modifier.weight(0.18f).fillMaxHeight().padding(8.dp)) {
|
||||
Column(modifier = Modifier.weight(0.2f).fillMaxHeight().padding(8.dp)) {
|
||||
AiAnalysisView(
|
||||
stockCode = selectedStockCode,
|
||||
stockName = selectedStockName,
|
||||
currentPrice = wsManager.currentPrice.value,
|
||||
trades = wsManager.tradeLogs
|
||||
)
|
||||
}
|
||||
VerticalDivider()
|
||||
Column(modifier = Modifier.weight(0.125f).fillMaxHeight().padding(8.dp)) {
|
||||
AutoTradeSection(
|
||||
isDomestic = isDomestic,
|
||||
tradeService = tradeService,
|
||||
@@ -172,7 +181,7 @@ fun DashboardScreen() {
|
||||
}
|
||||
VerticalDivider()
|
||||
// [우측 30%] 시장 추천 TOP 20 (실전 데이터)
|
||||
Column(modifier = Modifier.weight(0.18f).fillMaxHeight().padding(8.dp)) {
|
||||
Column(modifier = Modifier.weight(0.125f).fillMaxHeight().padding(8.dp)) {
|
||||
MarketSection(tradeService) { code, name, isDom ->
|
||||
val info = StockBasicInfo(
|
||||
code = code,
|
||||
|
||||
@@ -3,14 +3,20 @@ package ui
|
||||
|
||||
import AutoTradeItem
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.text.rememberTextMeasurer
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -78,14 +84,14 @@ fun IntegratedOrderSection(
|
||||
Text("주문 및 자동 매도 설정", style = MaterialTheme.typography.subtitle2, fontWeight = FontWeight.Bold)
|
||||
|
||||
// 가격 및 수량 입력 필드
|
||||
Row(modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp)) {
|
||||
OutlinedTextField(
|
||||
Row(modifier = Modifier.fillMaxWidth().padding(vertical = 2.dp)) {
|
||||
AutoResizeOutlinedTextField(
|
||||
value = orderQty,
|
||||
onValueChange = { if (it.all { c -> c.isDigit() }) orderQty = it },
|
||||
label = { Text("수량") },
|
||||
modifier = Modifier.weight(1f).padding(end = 4.dp)
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
OutlinedTextField(
|
||||
AutoResizeOutlinedTextField(
|
||||
value = orderPrice,
|
||||
onValueChange = { if (it.all { c -> c.isDigit() }) orderPrice = it },
|
||||
label = { Text("가격") },
|
||||
@@ -99,11 +105,11 @@ fun IntegratedOrderSection(
|
||||
SimulationCard(basePrice, inputQty.toDouble())
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
|
||||
// 실시간 AI 매도 감시 설정 카드
|
||||
Card(backgroundColor = Color(0xFFF8F9FA), elevation = 0.dp) {
|
||||
Column(modifier = Modifier.padding(8.dp)) {
|
||||
Column(modifier = Modifier.padding(4.dp)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Checkbox(
|
||||
checked = willEnableAutoSell,
|
||||
@@ -146,21 +152,19 @@ fun IntegratedOrderSection(
|
||||
}
|
||||
|
||||
Row {
|
||||
OutlinedTextField(
|
||||
AutoResizeOutlinedTextField(
|
||||
value = profitRate, onValueChange = { profitRate = it },
|
||||
label = { Text("익절 %") }, modifier = Modifier.weight(1f).padding(end = 4.dp),
|
||||
enabled = !willEnableAutoSell
|
||||
)
|
||||
OutlinedTextField(
|
||||
AutoResizeOutlinedTextField(
|
||||
value = stopLossRate, onValueChange = { stopLossRate = it },
|
||||
label = { Text("손절 %") }, modifier = Modifier.weight(1f),
|
||||
enabled = !willEnableAutoSell
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
|
||||
// 매수 / 매도 실행 버튼
|
||||
Row(modifier = Modifier.fillMaxWidth()) {
|
||||
@@ -252,4 +256,70 @@ fun SimulationColumn(title: String, items: List<String>) {
|
||||
Text(text = text, fontSize = 11.sp, color = color, modifier = Modifier.padding(vertical = 1.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
fun AutoResizeOutlinedTextField(
|
||||
value: String,
|
||||
onValueChange: (String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
label: @Composable (() -> Unit)? = null, // 라벨 추가
|
||||
placeholder: @Composable (() -> Unit)? = null, // 플레이스홀더 추가
|
||||
maxFontSize: TextUnit = 20.sp,
|
||||
minFontSize: TextUnit = 8.sp
|
||||
) {
|
||||
val textMeasurer = rememberTextMeasurer()
|
||||
var fontSize by remember { mutableStateOf(maxFontSize) }
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
|
||||
BoxWithConstraints(modifier = modifier) {
|
||||
val maxWidthPx = constraints.maxWidth
|
||||
|
||||
// 텍스트 너비에 따른 폰트 크기 자동 축소 로직
|
||||
LaunchedEffect(value) {
|
||||
var currentSize = maxFontSize
|
||||
while (currentSize > minFontSize) {
|
||||
val layoutResult = textMeasurer.measure(
|
||||
text = value,
|
||||
style = TextStyle(fontSize = currentSize)
|
||||
)
|
||||
if (layoutResult.size.width <= maxWidthPx) break
|
||||
currentSize = (currentSize.value - 0.5f).sp
|
||||
}
|
||||
fontSize = currentSize
|
||||
}
|
||||
|
||||
BasicTextField(
|
||||
value = value,
|
||||
onValueChange = onValueChange,
|
||||
textStyle = TextStyle(fontSize = fontSize, color = Color.Black),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
interactionSource = interactionSource,
|
||||
singleLine = true,
|
||||
decorationBox = { innerTextField ->
|
||||
TextFieldDefaults.OutlinedTextFieldDecorationBox(
|
||||
value = value,
|
||||
innerTextField = innerTextField,
|
||||
enabled = true,
|
||||
singleLine = true,
|
||||
visualTransformation = VisualTransformation.None,
|
||||
interactionSource = interactionSource,
|
||||
// [핵심] 사용자가 정의한 라벨과 플레이스홀더 연결
|
||||
label = label,
|
||||
placeholder = placeholder,
|
||||
// [핵심] 내부 패딩 0.dp 설정
|
||||
contentPadding = PaddingValues(0.dp),
|
||||
border = {
|
||||
TextFieldDefaults.BorderBox(
|
||||
enabled = true,
|
||||
isError = false,
|
||||
interactionSource = interactionSource,
|
||||
colors = TextFieldDefaults.outlinedTextFieldColors()
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ import model.CandleData
|
||||
@Composable
|
||||
fun PeriodTrendCard(label: String, data: List<CandleData>, modifier: Modifier = Modifier) {
|
||||
val avgPrice = if (data.isEmpty()) "0"
|
||||
else String.format("%,d", data.map { it.stck_clpr.toDoubleOrNull() ?: 0.0 }.average().toLong())
|
||||
else String.format("%,d", data.map { it.stck_prpr.toDoubleOrNull() ?: 0.0 }.average().toLong())
|
||||
|
||||
Card(modifier = modifier.height(80.dp), elevation = 2.dp, backgroundColor = Color.White) {
|
||||
Row(modifier = Modifier.padding(8.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
@@ -31,7 +31,7 @@ fun PeriodTrendCard(label: String, data: List<CandleData>, modifier: Modifier =
|
||||
Box(modifier = Modifier.weight(0.6f).fillMaxHeight()) {
|
||||
if (data.isNotEmpty()) {
|
||||
Canvas(modifier = Modifier.fillMaxSize()) {
|
||||
val prices = data.map { it.stck_clpr.toDoubleOrNull() ?: 0.0 }
|
||||
val prices = data.map { it.stck_prpr.toDoubleOrNull() ?: 0.0 }
|
||||
val max = prices.maxOrNull() ?: 1.0
|
||||
val min = prices.minOrNull() ?: 0.0
|
||||
val range = if (max == min) 1.0 else max - min
|
||||
|
||||
@@ -123,7 +123,7 @@ fun SettingsScreen(onAuthSuccess: () -> Unit) {
|
||||
KisSession.config = config
|
||||
DatabaseFactory.saveConfig(config)
|
||||
val authService = KisAuthService()
|
||||
val tradeService = KisTradeService()
|
||||
val tradeService = KisTradeService
|
||||
val authSuccess = authService.refreshAllTokens()
|
||||
val wsKeySuccess = tradeService.refreshWebsocketKey()
|
||||
|
||||
|
||||
@@ -32,6 +32,9 @@ import model.RankingStock
|
||||
import model.StockHolding
|
||||
import network.KisTradeService
|
||||
import network.KisWebSocketManager
|
||||
import service.TechnicalAnalyzer
|
||||
import java.time.LocalTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
import kotlin.collections.isNotEmpty
|
||||
|
||||
@Composable
|
||||
@@ -59,12 +62,12 @@ fun StockDetailSection(
|
||||
daySummary.lastOrNull()?.stck_oprc ?: "0"
|
||||
}
|
||||
val previousClose = remember(daySummary) {
|
||||
if (daySummary.size >= 2) daySummary[daySummary.size - 2].stck_clpr else "0"
|
||||
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_clpr.toDoubleOrNull() ?: 0.0 }.average()
|
||||
val avg = data.map { it.stck_prpr.toDoubleOrNull() ?: 0.0 }.average()
|
||||
return String.format("%,d", avg.toLong())
|
||||
}
|
||||
|
||||
@@ -92,17 +95,23 @@ fun StockDetailSection(
|
||||
.onSuccess { data ->
|
||||
println("✅ 차트 데이터 로드 성공: ${data.size}개") // ${} 사용하여 정확히 출력
|
||||
chartData = data
|
||||
TechnicalAnalyzer.min30 = chartData
|
||||
}
|
||||
.onFailure { error ->
|
||||
println("❌ 차트 데이터 로드 실패: ${error.localizedMessage}")
|
||||
chartData = emptyList()
|
||||
}}
|
||||
launch { tradeService.fetchPeriodChartData(stockCode, "D").onSuccess { daySummary = it.takeLast(7) } } // 최근 7일
|
||||
launch { tradeService.fetchPeriodChartData(stockCode, "W").onSuccess { weekSummary = it.takeLast(4) } } // 최근 4주
|
||||
launch { tradeService.fetchPeriodChartData(stockCode, "D").onSuccess {
|
||||
daySummary = it.takeLast(7) }
|
||||
TechnicalAnalyzer.daily = daySummary
|
||||
} // 최근 7일
|
||||
launch { tradeService.fetchPeriodChartData(stockCode, "W").onSuccess { weekSummary = it.takeLast(4) }
|
||||
TechnicalAnalyzer.weekly = weekSummary} // 최근 4주
|
||||
launch { tradeService.fetchPeriodChartData(stockCode, "M").onSuccess {
|
||||
monthSummary = it.takeLast(6) // 최근 6개월
|
||||
yearSummary = it.takeLast(36) // 최근 3년
|
||||
} }
|
||||
TechnicalAnalyzer.monthly = yearSummary
|
||||
}}
|
||||
}
|
||||
isLoading = false
|
||||
}
|
||||
@@ -115,7 +124,7 @@ fun StockDetailSection(
|
||||
val lastCandle = chartData.last()
|
||||
|
||||
// 현재 시간(분 단위) 확인
|
||||
val currentMinute = java.time.LocalTime.now().format(java.time.format.DateTimeFormatter.ofPattern("HHmm00"))
|
||||
val currentMinute = LocalTime.now().format(DateTimeFormatter.ofPattern("HHmm00"))
|
||||
|
||||
if (lastCandle.stck_bsop_date != currentMinute) {
|
||||
// [개선] 시간이 바뀌었으면 새로운 캔들 추가 (차트가 밀려나는 효과)
|
||||
@@ -124,15 +133,17 @@ fun StockDetailSection(
|
||||
stck_oprc = latestPrice,
|
||||
stck_hgpr = latestPrice,
|
||||
stck_lwpr = latestPrice,
|
||||
stck_clpr = latestPrice,
|
||||
acml_vol = "0"
|
||||
stck_prpr = latestPrice,
|
||||
stck_cntg_hour = currentMinute,
|
||||
cntg_vol = "1",
|
||||
acml_tr_pbmn = "1",
|
||||
)
|
||||
// 최대 100개까지만 유지하여 성능 최적화
|
||||
chartData = (chartData + newCandle).takeLast(100)
|
||||
} else {
|
||||
// 같은 분 내에서는 기존 마지막 캔들만 업데이트
|
||||
val updatedCandle = lastCandle.copy(
|
||||
stck_clpr = latestPrice,
|
||||
stck_prpr = latestPrice,
|
||||
stck_hgpr = if (priceDouble > (lastCandle.stck_hgpr.toDoubleOrNull() ?: 0.0)) latestPrice else lastCandle.stck_hgpr,
|
||||
stck_lwpr = if (priceDouble < (lastCandle.stck_lwpr.toDoubleOrNull() ?: Double.MAX_VALUE)) latestPrice else lastCandle.stck_lwpr
|
||||
)
|
||||
@@ -178,10 +189,10 @@ fun StockDetailSection(
|
||||
PeriodTrendCard("3년", yearSummary, Modifier.weight(1f))
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
// [중앙] 캔들 차트 (Card 내부)
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth().height(300.dp),
|
||||
modifier = Modifier.fillMaxWidth().height(320.dp),
|
||||
backgroundColor = Color(0xFF121212)
|
||||
) {
|
||||
if (isLoading) {
|
||||
@@ -191,16 +202,9 @@ fun StockDetailSection(
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
|
||||
// [중앙 하단] AI 투자 전략
|
||||
AiAnalysisView(
|
||||
stockName = stockName,
|
||||
currentPrice = wsManager.currentPrice.value,
|
||||
trades = wsManager.tradeLogs
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
// [하단] 실시간 체결 내역 및 주문 섹션
|
||||
Row(modifier = Modifier.weight(1f)) {
|
||||
|
||||
Reference in New Issue
Block a user