...
This commit is contained in:
@@ -246,7 +246,7 @@ fun DashboardScreen() {
|
||||
},
|
||||
stockCode = selectedStockCode,
|
||||
stockName = selectedStockName,
|
||||
currentPrice = wsManager.currentPrice.value,
|
||||
currentPrice = "0",
|
||||
trades = wsManager.tradeLogs,
|
||||
tradingDecisionCallback = { decision,bool ->
|
||||
if (bool && decision != null && KisSession.config.isSimulation) {
|
||||
|
||||
@@ -22,10 +22,12 @@ import androidx.compose.ui.unit.sp
|
||||
import kotlinx.coroutines.launch
|
||||
import model.MAX_BUDGET
|
||||
import model.MIN_PURCHASE_SCORE
|
||||
import model.RankingStock
|
||||
import model.buyWeight
|
||||
import model.feesAndTaxRate
|
||||
import model.minimumNetProfit
|
||||
import network.KisTradeService
|
||||
import service.AutoTradingManager
|
||||
import util.MarketUtil
|
||||
|
||||
enum class InvestmentGrade(
|
||||
@@ -50,7 +52,7 @@ enum class InvestmentGrade(
|
||||
shortWeight = 0.8,
|
||||
midWeight = 1.0,
|
||||
longWeight = 1.0,
|
||||
profitGuide = 1.4,
|
||||
profitGuide = 1.3,
|
||||
),
|
||||
LEVEL_3_CAUTIOUS_RECOMMEND(
|
||||
displayName = "보수적 추천",
|
||||
@@ -58,7 +60,7 @@ enum class InvestmentGrade(
|
||||
shortWeight = 0.6,
|
||||
midWeight = 1.0,
|
||||
longWeight = 1.0,
|
||||
profitGuide = 1.0,
|
||||
profitGuide = 0.9,
|
||||
),
|
||||
LEVEL_2_HIGH_RISK(
|
||||
displayName = "고위험 추천",
|
||||
@@ -66,7 +68,7 @@ enum class InvestmentGrade(
|
||||
shortWeight = 1.0,
|
||||
midWeight = 0.4,
|
||||
longWeight = 0.4,
|
||||
profitGuide = 0.8,
|
||||
profitGuide = 0.7,
|
||||
),
|
||||
LEVEL_1_SPECULATIVE(
|
||||
displayName = "순수 공격적 선택",
|
||||
@@ -74,7 +76,7 @@ enum class InvestmentGrade(
|
||||
shortWeight = 1.0,
|
||||
midWeight = 0.2,
|
||||
longWeight = 0.2,
|
||||
profitGuide = 0.6,
|
||||
profitGuide = 0.5,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -131,10 +133,14 @@ fun IntegratedOrderSection(
|
||||
var stopLossRate by remember(monitoringItem) {
|
||||
mutableStateOf(monitoringItem?.stopLossRate?.toString() ?: "-1.5")
|
||||
}
|
||||
|
||||
var basePrice: Double = 0.0
|
||||
LaunchedEffect(currentPrice) {
|
||||
val curPriceNum = currentPrice.replace(",", "").toDoubleOrNull() ?: 0.0
|
||||
basePrice = curPriceNum
|
||||
}
|
||||
// 계산용 변수
|
||||
val curPriceNum = currentPrice.replace(",", "").toDoubleOrNull() ?: 0.0
|
||||
val basePrice = (if (orderPrice.isEmpty()) curPriceNum else orderPrice.toDoubleOrNull() ?: 0.0)
|
||||
|
||||
|
||||
|
||||
fun getInvestmentGrade(
|
||||
ts: TradingDecision,
|
||||
@@ -187,9 +193,9 @@ fun IntegratedOrderSection(
|
||||
val tickSize = MarketUtil.getTickSize(basePrice)
|
||||
val oneTickLowerPrice = basePrice - (tickSize * when(investmentGrade) {
|
||||
InvestmentGrade.LEVEL_5_STRONG_RECOMMEND -> 1
|
||||
InvestmentGrade.LEVEL_4_BALANCED_RECOMMEND -> 2
|
||||
InvestmentGrade.LEVEL_3_CAUTIOUS_RECOMMEND -> 3
|
||||
InvestmentGrade.LEVEL_2_HIGH_RISK -> 3
|
||||
InvestmentGrade.LEVEL_4_BALANCED_RECOMMEND -> 1
|
||||
InvestmentGrade.LEVEL_3_CAUTIOUS_RECOMMEND -> 2
|
||||
InvestmentGrade.LEVEL_2_HIGH_RISK -> 2
|
||||
InvestmentGrade.LEVEL_1_SPECULATIVE -> 4
|
||||
else -> 4
|
||||
})
|
||||
@@ -249,10 +255,8 @@ fun IntegratedOrderSection(
|
||||
var append = 0.0
|
||||
if (completeTradingDecision != null &&
|
||||
completeTradingDecision.stockCode.equals(stockCode)) {
|
||||
|
||||
println("basePrice $basePrice")
|
||||
fun resultCheck(completeTradingDecision :TradingDecision) {
|
||||
|
||||
|
||||
val weights = mapOf(
|
||||
"short" to 0.3, // 초단기 점수가 낮아도 전체에 미치는 영향 감소
|
||||
"profit" to 0.3,
|
||||
@@ -263,23 +267,25 @@ fun IntegratedOrderSection(
|
||||
(completeTradingDecision.shortPossible() * weights["short"]!!) +
|
||||
(completeTradingDecision.profitPossible() * weights["profit"]!!) +
|
||||
(completeTradingDecision.safePossible() * weights["safe"]!!)
|
||||
println("""
|
||||
corpName : ${completeTradingDecision.corpName}
|
||||
confidence : ${completeTradingDecision.confidence + append}
|
||||
shortPossible : ${completeTradingDecision.shortPossible() + append}
|
||||
profitPossible : ${completeTradingDecision.profitPossible()+ append}
|
||||
safePossible : ${completeTradingDecision.safePossible()+ append}
|
||||
totalScore : ${totalScore}
|
||||
""".trimIndent())
|
||||
|
||||
if (totalScore >= MIN_PURCHASE_SCORE && completeTradingDecision.confidence >= MIN_CONFIDENCE) {
|
||||
var investmentGrade : InvestmentGrade = getInvestmentGrade(completeTradingDecision,totalScore, completeTradingDecision.confidence)
|
||||
// 4. 점수에 따른 가변 마진 적용
|
||||
// 토탈 스코어가 85점 이상이면 마진을 3.0으로 고정하거나 추가 가산(append) 적용
|
||||
val finalMargin = minimumNetProfit * investmentGrade.profitGuide
|
||||
|
||||
println("""
|
||||
사명 : ${completeTradingDecision.corpName}
|
||||
신뢰도 : ${completeTradingDecision.confidence + append}
|
||||
단기성 : ${completeTradingDecision.shortPossible() + append}
|
||||
수익성 : ${completeTradingDecision.profitPossible()+ append}
|
||||
안전성 : ${completeTradingDecision.safePossible()+ append}
|
||||
${investmentGrade.displayName} : ${investmentGrade.description}
|
||||
총점 : ${totalScore}
|
||||
""".trimIndent())
|
||||
println("🚀 [매수 진행] 토탈 스코어: ${String.format("%.1f", totalScore)} -> 종목: ${completeTradingDecision.stockCode}")
|
||||
|
||||
// basePrice(현재가 혹은 지정가)를 기준으로 매수 가능 수량 산출 (최소 1주 보장)
|
||||
|
||||
val calculatedQty = if (basePrice > 0) {
|
||||
(MAX_BUDGET / basePrice).toInt().coerceAtLeast(1)
|
||||
} else {
|
||||
@@ -293,8 +299,11 @@ fun IntegratedOrderSection(
|
||||
investmentGrade = investmentGrade,
|
||||
)
|
||||
|
||||
} else {
|
||||
println("✋ [관망] 토탈 스코어(${String.format("%.1f", totalScore)})가 기준치($MIN_PURCHASE_SCORE) 미달 또는 신뢰도 ${completeTradingDecision.confidence}가 기준치 ${MIN_CONFIDENCE} 미달")
|
||||
} else if(totalScore >= (MIN_PURCHASE_SCORE * 0.85) && completeTradingDecision.confidence >= (MIN_CONFIDENCE * 0.85)) {
|
||||
AutoTradingManager.addToReanalysis(RankingStock(mksc_shrn_iscd = completeTradingDecision.stockCode,hts_kor_isnm = completeTradingDecision.stockName))
|
||||
println("✋ [관망] 토탈 스코어 또는 신뢰도 미달 이나 약간의 오차로 재분석 대기열에 추가")
|
||||
} else {
|
||||
println("✋ [관망] 토탈 스코어(${String.format("%.1f", totalScore)}) 또는 신뢰도 ${completeTradingDecision.confidence} 미달")
|
||||
}
|
||||
}
|
||||
when (completeTradingDecision?.decision) {
|
||||
|
||||
@@ -21,8 +21,6 @@ import model.CandleData
|
||||
import network.DartCodeManager
|
||||
import network.KisTradeService
|
||||
import network.KisWebSocketManager
|
||||
import network.NewsService
|
||||
import service.TechnicalAnalyzer
|
||||
import java.time.LocalTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
import kotlin.collections.isNotEmpty
|
||||
@@ -44,7 +42,7 @@ fun StockDetailSection(
|
||||
yearSummary : MutableList<CandleData>
|
||||
) {
|
||||
|
||||
var openPrice by remember { mutableStateOf("0") }
|
||||
// var openPrice by remember { mutableStateOf("0") }
|
||||
var chartData by remember { mutableStateOf<List<CandleData>>(emptyList()) }
|
||||
var isLoading by remember { mutableStateOf(false) }
|
||||
var resultMessage by remember { mutableStateOf("") }
|
||||
@@ -62,6 +60,8 @@ fun StockDetailSection(
|
||||
|
||||
// 이전 종목 코드를 기억하기 위한 상태
|
||||
var previousCode by remember { mutableStateOf("") }
|
||||
var latestPrice by remember { mutableStateOf("0") }
|
||||
|
||||
|
||||
// 종목 변경 시 데이터 로드 및 웹소켓 구독 관리
|
||||
LaunchedEffect(stockCode) {
|
||||
@@ -81,16 +81,62 @@ fun StockDetailSection(
|
||||
// 2. 차트 데이터 로드 (KisSession 기반으로 파라미터 간소화)
|
||||
|
||||
coroutineScope {
|
||||
launch {
|
||||
wsManager.onPriceUpdate = {tradeLog ->
|
||||
|
||||
|
||||
if (tradeLog.code.equals(stockCode)) {
|
||||
val code = tradeLog.code
|
||||
val price = tradeLog.price
|
||||
wsManager.tradeLogs.add(tradeLog)
|
||||
if (wsManager.tradeLogs.size > 50) wsManager.tradeLogs.removeLast()
|
||||
println("code $code ,price $price")
|
||||
val currentPrice = price
|
||||
if (chartData.isNotEmpty() && currentPrice != "0") {
|
||||
val priceDouble = currentPrice.replace(",", "").toDoubleOrNull() ?: 0.0
|
||||
val lastCandle = chartData.last()
|
||||
|
||||
// 현재 시간(분 단위) 확인
|
||||
val currentMinute = LocalTime.now().format(DateTimeFormatter.ofPattern("HHmm00"))
|
||||
|
||||
if (lastCandle.stck_bsop_date != currentMinute) {
|
||||
// [개선] 시간이 바뀌었으면 새로운 캔들 추가 (차트가 밀려나는 효과)
|
||||
val newCandle = CandleData(
|
||||
stck_bsop_date = currentMinute,
|
||||
stck_oprc = currentPrice,
|
||||
stck_hgpr = currentPrice,
|
||||
stck_lwpr = currentPrice,
|
||||
stck_prpr = currentPrice,
|
||||
stck_cntg_hour = currentMinute,
|
||||
cntg_vol = "1",
|
||||
acml_tr_pbmn = "1",
|
||||
)
|
||||
// 최대 100개까지만 유지하여 성능 최적화
|
||||
chartData = (chartData + newCandle).takeLast(100)
|
||||
} else {
|
||||
// 같은 분 내에서는 기존 마지막 캔들만 업데이트
|
||||
val updatedCandle = lastCandle.copy(
|
||||
stck_prpr = currentPrice,
|
||||
stck_hgpr = if (priceDouble > (lastCandle.stck_hgpr.toDoubleOrNull() ?: 0.0)) currentPrice else lastCandle.stck_hgpr,
|
||||
stck_lwpr = if (priceDouble < (lastCandle.stck_lwpr.toDoubleOrNull() ?: Double.MAX_VALUE)) currentPrice else lastCandle.stck_lwpr
|
||||
)
|
||||
chartData = chartData.dropLast(1) + updatedCandle
|
||||
}
|
||||
}
|
||||
latestPrice = currentPrice
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
launch {tradeService.fetchChartData(stockCode, isDomestic)
|
||||
.onSuccess { data ->
|
||||
println("✅ 차트 데이터 로드 성공: ${data.size}개") // ${} 사용하여 정확히 출력
|
||||
// println("✅ 차트 데이터 로드 성공: ${data.size}개") // ${} 사용하여 정확히 출력
|
||||
chartData = data
|
||||
min30.clear()
|
||||
min30.addAll(chartData)
|
||||
}
|
||||
.onFailure { error ->
|
||||
println("❌ 차트 데이터 로드 실패: ${error.localizedMessage}")
|
||||
// println("❌ 차트 데이터 로드 실패: ${error.localizedMessage}")
|
||||
chartData = emptyList()
|
||||
}
|
||||
}
|
||||
@@ -122,41 +168,43 @@ fun StockDetailSection(
|
||||
isLoading = false
|
||||
}
|
||||
|
||||
val latestPrice by wsManager.currentPrice // 웹소켓에서 업데이트되는 현재가
|
||||
|
||||
LaunchedEffect(latestPrice) {
|
||||
if (chartData.isNotEmpty() && latestPrice != "0") {
|
||||
val priceDouble = latestPrice.replace(",", "").toDoubleOrNull() ?: return@LaunchedEffect
|
||||
val lastCandle = chartData.last()
|
||||
|
||||
// 현재 시간(분 단위) 확인
|
||||
val currentMinute = LocalTime.now().format(DateTimeFormatter.ofPattern("HHmm00"))
|
||||
|
||||
if (lastCandle.stck_bsop_date != currentMinute) {
|
||||
// [개선] 시간이 바뀌었으면 새로운 캔들 추가 (차트가 밀려나는 효과)
|
||||
val newCandle = CandleData(
|
||||
stck_bsop_date = currentMinute,
|
||||
stck_oprc = latestPrice,
|
||||
stck_hgpr = latestPrice,
|
||||
stck_lwpr = latestPrice,
|
||||
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_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
|
||||
)
|
||||
chartData = chartData.dropLast(1) + updatedCandle
|
||||
}
|
||||
}
|
||||
}
|
||||
// LaunchedEffect(latestPrice) {
|
||||
// println("latestPrice >>> $latestPrice")
|
||||
// if (chartData.isNotEmpty() && latestPrice != "0") {
|
||||
// val latestPrice = latestPrice ?: "0"
|
||||
// val priceDouble = latestPrice?.replace(",", "")?.toDoubleOrNull() ?: return@LaunchedEffect
|
||||
// val lastCandle = chartData.last()
|
||||
//
|
||||
// // 현재 시간(분 단위) 확인
|
||||
// val currentMinute = LocalTime.now().format(DateTimeFormatter.ofPattern("HHmm00"))
|
||||
//
|
||||
// if (lastCandle.stck_bsop_date != currentMinute) {
|
||||
// // [개선] 시간이 바뀌었으면 새로운 캔들 추가 (차트가 밀려나는 효과)
|
||||
// val newCandle = CandleData(
|
||||
// stck_bsop_date = currentMinute,
|
||||
// stck_oprc = latestPrice,
|
||||
// stck_hgpr = latestPrice,
|
||||
// stck_lwpr = latestPrice,
|
||||
// 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_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
|
||||
// )
|
||||
// chartData = chartData.dropLast(1) + updatedCandle
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
|
||||
// [상단] 종목명 및 상태 메시지
|
||||
@@ -170,7 +218,7 @@ fun StockDetailSection(
|
||||
code = stockCode,
|
||||
isDomestic = isDomestic,
|
||||
previousClose = previousClose,
|
||||
openPrice = openPrice,
|
||||
openPrice = latestPrice,
|
||||
resultMessage = resultMessage,
|
||||
resultMessageClear = {resultMessage = ""},
|
||||
isSuccess = isSuccess
|
||||
@@ -179,10 +227,10 @@ fun StockDetailSection(
|
||||
// 실시간 가격 표시 (WebSocket 데이터)
|
||||
Column(horizontalAlignment = Alignment.End) {
|
||||
Text(
|
||||
text = "${wsManager.currentPrice.value} 원",
|
||||
text = "${latestPrice} 원",
|
||||
style = MaterialTheme.typography.h4,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = if (wsManager.currentPrice.value.contains("-")) Color.Blue else Color.Red
|
||||
color = if (latestPrice?.contains("-") ?: false) Color.Blue else Color.Red
|
||||
)
|
||||
Text("실시간 체결가", style = MaterialTheme.typography.caption, color = Color.Gray)
|
||||
}
|
||||
@@ -228,7 +276,7 @@ fun StockDetailSection(
|
||||
stockCode = stockCode,
|
||||
stockName = stockName,
|
||||
isDomestic = isDomestic,
|
||||
currentPrice = wsManager.currentPrice.value,
|
||||
currentPrice = latestPrice,
|
||||
holdingQuantity = holdingQuantity,
|
||||
tradeService = tradeService,
|
||||
onOrderSaved = onOrderSaved,
|
||||
|
||||
Reference in New Issue
Block a user