.....
This commit is contained in:
@@ -51,8 +51,8 @@ fun AiAnalysisView(technicalAnalyzer: TechnicalAnalyzer,stockCode:String,stockNa
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Column(modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
.fillMaxWidth()
|
||||
.fillMaxHeight(0.15F)
|
||||
.verticalScroll(rememberScrollState()) // 스크롤 활성화
|
||||
.padding(16.dp)
|
||||
.background(Color(0xFFF5F5F5), RoundedCornerShape(8.dp))) {
|
||||
|
||||
@@ -5,22 +5,28 @@ import AutoTradeItem
|
||||
import TradingDecision
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
import androidx.compose.foundation.lazy.grid.GridItemSpan
|
||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import model.CandleData
|
||||
import model.ConfigIndex
|
||||
import model.ExecutionData
|
||||
import model.KisSession
|
||||
import model.StockBasicInfo
|
||||
import model.feesAndTaxRate
|
||||
import model.minimumNetProfit
|
||||
import network.KisTradeService
|
||||
import network.KisWebSocketManager
|
||||
import service.AutoTradingManager
|
||||
@@ -54,7 +60,7 @@ fun DashboardScreen() {
|
||||
CoroutineScope(Dispatchers.Default).launch {
|
||||
// while (true) {
|
||||
// delay(60000) // 1분마다 체크
|
||||
AutoTradingManager.checkAndRestart(tradeService, callback)
|
||||
AutoTradingManager.checkAndRestart(tradeService, callback)
|
||||
// }
|
||||
}
|
||||
}
|
||||
@@ -121,7 +127,7 @@ fun DashboardScreen() {
|
||||
|
||||
// 2. 최소 마진 설정 (수수료/세금 0.3% + 순수익 1.5% = 1.8%)
|
||||
|
||||
val minEffectiveRate = minimumNetProfit + feesAndTaxRate
|
||||
val minEffectiveRate = KisSession.config.getValues(ConfigIndex.PROFIT_INDEX) + KisSession.config.getValues(ConfigIndex.TAX_INDEX)
|
||||
|
||||
// 3. DB에 설정된 목표 수익률과 최소 보장 수익률 중 큰 값 선택
|
||||
val finalProfitRate = maxOf(dbItem.profitRate, minEffectiveRate)
|
||||
@@ -175,7 +181,7 @@ fun DashboardScreen() {
|
||||
val monitoringTrades = DatabaseFactory.getAutoTradesByStatus(listOf(TradeStatus.MONITORING, TradeStatus.PENDING_BUY))
|
||||
val monitoringCodes = monitoringTrades.map { it.code }.toSet()
|
||||
wsManager.updateSubscriptions(monitoringCodes)
|
||||
|
||||
AutoTradingManager.resumePendingSellOrders(tradeService)
|
||||
refreshTrigger++
|
||||
}
|
||||
|
||||
@@ -191,7 +197,7 @@ fun DashboardScreen() {
|
||||
|
||||
Row(modifier = Modifier.fillMaxSize().background(Color(0xFFF2F2F2))) {
|
||||
// [좌측 25%] 내 자산 및 통합 잔고
|
||||
Column(modifier = Modifier.weight(0.125f).fillMaxHeight().padding(8.dp)) {
|
||||
Column(modifier = Modifier.weight(0.12f).fillMaxHeight().padding(8.dp)) {
|
||||
BalanceSection(tradeService,
|
||||
onRefresh = { refreshTrigger++ },
|
||||
refreshTrigger = refreshTrigger) { code, name, isDom,qty ->
|
||||
@@ -235,7 +241,8 @@ fun DashboardScreen() {
|
||||
}
|
||||
|
||||
VerticalDivider()
|
||||
Column(modifier = Modifier.weight(0.2f).fillMaxHeight().padding(8.dp)) {
|
||||
|
||||
Column(modifier = Modifier.weight(0.25f).padding(8.dp).fillMaxHeight().background(Color.White)) {
|
||||
AiAnalysisView(
|
||||
technicalAnalyzer = TechnicalAnalyzer().apply {
|
||||
this.min30 = min30
|
||||
@@ -254,9 +261,177 @@ fun DashboardScreen() {
|
||||
}
|
||||
}
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text("설정값 관리", style = MaterialTheme.typography.subtitle2, modifier = Modifier.padding(bottom = 4.dp))
|
||||
LazyVerticalGrid(
|
||||
columns = GridCells.Fixed(2), // 2열 병렬 배치
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = Modifier.fillMaxWidth().weight(0.3f)
|
||||
) {
|
||||
item(span = { GridItemSpan(maxLineSpan) }) { // 2열을 모두 차지함
|
||||
Text(
|
||||
"💰 거래 기본 설정",
|
||||
style = MaterialTheme.typography.h6,
|
||||
modifier = Modifier.padding(top = 16.dp, bottom = 8.dp)
|
||||
)
|
||||
}
|
||||
var defaults = arrayOf(
|
||||
ConfigIndex.TAX_INDEX,
|
||||
ConfigIndex.PROFIT_INDEX,
|
||||
ConfigIndex.BUY_WEIGHT_INDEX,
|
||||
ConfigIndex.MAX_BUDGET_INDEX,
|
||||
ConfigIndex.MAX_PRICE_INDEX,
|
||||
ConfigIndex.MIN_PRICE_INDEX,
|
||||
ConfigIndex.MIN_PURCHASE_SCORE_INDEX,
|
||||
ConfigIndex.MAX_COUNT_INDEX,
|
||||
)
|
||||
items(defaults.size) { index ->
|
||||
val configKey = defaults.get(index)
|
||||
|
||||
// 1. 키보드 입력을 실시간으로 보여줄 로컬 상태 (String)
|
||||
var localText by remember(configKey) {
|
||||
mutableStateOf(KisSession.config.getValues(configKey)?.toString() ?: "")
|
||||
}
|
||||
|
||||
// 저장 로직을 공통 함수로 분리
|
||||
val saveAction = {
|
||||
var newValue = localText.toDoubleOrNull() ?: 0.0
|
||||
if (configKey.label.contains("PROFIT")) {
|
||||
newValue = newValue / KisSession.config.getValues(ConfigIndex.PROFIT_INDEX)
|
||||
}
|
||||
KisSession.config.setValues(configKey, newValue)
|
||||
DatabaseFactory.saveConfig(KisSession.config)
|
||||
println("💾 저장됨: ${configKey.label} = $newValue")
|
||||
}
|
||||
|
||||
var text = if (configKey.label.contains("PROFIT")) {
|
||||
"${(localText.toDoubleOrNull() ?: 1.0) * KisSession.config.getValues(ConfigIndex.PROFIT_INDEX)}"
|
||||
} else {
|
||||
localText
|
||||
}
|
||||
|
||||
OutlinedTextField(
|
||||
value = text,
|
||||
onValueChange = { localText = it }, // 화면에는 즉시 반영
|
||||
label = { Text(configKey.label) },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.onFocusChanged { focusState ->
|
||||
// 2. 포커스를 잃었을 때 저장
|
||||
if (!focusState.isFocused) {
|
||||
saveAction()
|
||||
}
|
||||
},
|
||||
keyboardOptions = KeyboardOptions(
|
||||
imeAction = ImeAction.Done,
|
||||
keyboardType = KeyboardType.Decimal
|
||||
),
|
||||
keyboardActions = KeyboardActions(
|
||||
// 3. 엔터(Done) 키를 눌렀을 때 저장
|
||||
onDone = {
|
||||
saveAction()
|
||||
}
|
||||
),
|
||||
singleLine = true
|
||||
)
|
||||
}
|
||||
item(span = { GridItemSpan(maxLineSpan) }) { // 2열을 모두 차지함
|
||||
Text(
|
||||
"💰매수 정책 및 기대 수익률",
|
||||
style = MaterialTheme.typography.h6,
|
||||
modifier = Modifier.padding(top = 16.dp, bottom = 8.dp)
|
||||
)
|
||||
}
|
||||
var defaults2 = arrayOf(
|
||||
arrayOf(ConfigIndex.GRADE_5_BUY,
|
||||
ConfigIndex.GRADE_5_PROFIT,),
|
||||
arrayOf(ConfigIndex.GRADE_4_BUY,
|
||||
ConfigIndex.GRADE_4_PROFIT,),
|
||||
arrayOf(ConfigIndex.GRADE_3_BUY,
|
||||
ConfigIndex.GRADE_3_PROFIT,),
|
||||
arrayOf(ConfigIndex.GRADE_2_BUY,
|
||||
ConfigIndex.GRADE_2_PROFIT,),
|
||||
arrayOf(ConfigIndex.GRADE_1_BUY,
|
||||
ConfigIndex.GRADE_1_PROFIT,),
|
||||
)
|
||||
for (items in defaults2) {
|
||||
val common = findLongestCommonSubstring(items.first().label,items.last().label)
|
||||
item(span = { GridItemSpan(maxLineSpan) }) { // 2열을 모두 차지함
|
||||
Text(
|
||||
common,
|
||||
style = MaterialTheme.typography.h6,
|
||||
modifier = Modifier.padding(top = 16.dp, bottom = 8.dp)
|
||||
)
|
||||
}
|
||||
|
||||
items(items.size) { index ->
|
||||
val configKey = items.get(index)
|
||||
|
||||
// 1. 키보드 입력을 실시간으로 보여줄 로컬 상태 (String)
|
||||
var localText by remember(configKey) {
|
||||
mutableStateOf(KisSession.config.getValues(configKey)?.toString() ?: "")
|
||||
}
|
||||
|
||||
var labelText by remember(configKey) {
|
||||
mutableStateOf(KisSession.config.getValues(configKey)?.toString() ?: "")
|
||||
}
|
||||
|
||||
val saveAction = {
|
||||
var newValue = localText.toDoubleOrNull() ?: 0.0
|
||||
// if (configKey.name.contains("PROFIT")) {
|
||||
// newValue = newValue / KisSession.config.getValues(ConfigIndex.PROFIT_INDEX)
|
||||
// }
|
||||
KisSession.config.setValues(configKey, newValue)
|
||||
DatabaseFactory.saveConfig(KisSession.config)
|
||||
println("💾 저장됨: ${configKey.label} = $newValue")
|
||||
labelText = if (configKey.name.contains("PROFIT")) {
|
||||
getRemaining(configKey.label,common) + ": 기준율(${KisSession.config.getValues(ConfigIndex.PROFIT_INDEX)}) * 성향별 비율(${KisSession.config.getValues(configKey)}) + 세금제비용(${KisSession.config.getValues(
|
||||
ConfigIndex.TAX_INDEX)}) = ${localText.toDouble() * KisSession.config.getValues(configKey) + KisSession.config.getValues(
|
||||
ConfigIndex.TAX_INDEX)}"
|
||||
} else {
|
||||
getRemaining(configKey.label,common) + ": -${localText} 호가 매수}"
|
||||
}
|
||||
}
|
||||
|
||||
labelText = if (configKey.name.contains("PROFIT")) {
|
||||
getRemaining(configKey.label,common) + ": 기준율(${KisSession.config.getValues(ConfigIndex.PROFIT_INDEX)}) * 성향별 비율(${KisSession.config.getValues(configKey)}) + 세금제비용(${KisSession.config.getValues(
|
||||
ConfigIndex.TAX_INDEX)}) = ${localText.toDouble() * KisSession.config.getValues(configKey) + KisSession.config.getValues(
|
||||
ConfigIndex.TAX_INDEX)} "
|
||||
} else {
|
||||
getRemaining(configKey.label,common) + ": -${localText} 호가 매수}"
|
||||
}
|
||||
|
||||
OutlinedTextField(
|
||||
value = localText,
|
||||
onValueChange = { localText = it }, // 화면에는 즉시 반영
|
||||
label = { Text(labelText) },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.onFocusChanged { focusState ->
|
||||
// 2. 포커스를 잃었을 때 저장
|
||||
if (!focusState.isFocused) {
|
||||
saveAction()
|
||||
}
|
||||
},
|
||||
keyboardOptions = KeyboardOptions(
|
||||
imeAction = ImeAction.Done,
|
||||
keyboardType = KeyboardType.Decimal
|
||||
),
|
||||
keyboardActions = KeyboardActions(
|
||||
// 3. 엔터(Done) 키를 눌렀을 때 저장
|
||||
onDone = {
|
||||
saveAction()
|
||||
}
|
||||
),
|
||||
singleLine = true
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
VerticalDivider()
|
||||
Column(modifier = Modifier.weight(0.125f).fillMaxHeight().padding(8.dp)) {
|
||||
Column(modifier = Modifier.weight(0.12f).fillMaxHeight().padding(8.dp)) {
|
||||
AutoTradeSection(
|
||||
isDomestic = isDomestic,
|
||||
tradeService = tradeService,
|
||||
@@ -277,7 +452,7 @@ fun DashboardScreen() {
|
||||
}
|
||||
VerticalDivider()
|
||||
// [우측 30%] 시장 추천 TOP 20 (실전 데이터)
|
||||
Column(modifier = Modifier.weight(0.125f).fillMaxHeight().padding(8.dp)) {
|
||||
Column(modifier = Modifier.weight(0.12f).fillMaxHeight().padding(8.dp)) {
|
||||
MarketSection(tradeService) { code, name, isDom ->
|
||||
val info = StockBasicInfo(
|
||||
code = code,
|
||||
@@ -301,4 +476,34 @@ fun DashboardScreen() {
|
||||
@Composable
|
||||
fun VerticalDivider() {
|
||||
Box(Modifier.fillMaxHeight().width(1.dp).background(Color.LightGray))
|
||||
}
|
||||
|
||||
fun findLongestCommonSubstring(s1: String, s2: String): String {
|
||||
if (s1.isEmpty() || s2.isEmpty()) return ""
|
||||
|
||||
var longest = ""
|
||||
// 더 짧은 문자열을 기준으로 삼아 반복 횟수를 줄임
|
||||
val reference = if (s1.length <= s2.length) s1 else s2
|
||||
val target = if (s1.length <= s2.length) s2 else s1
|
||||
|
||||
for (i in reference.indices) {
|
||||
for (j in (i + longest.length + 1)..reference.length) {
|
||||
val sub = reference.substring(i, j)
|
||||
if (target.contains(sub)) {
|
||||
if (sub.length > longest.length) {
|
||||
longest = sub
|
||||
}
|
||||
} else {
|
||||
// target에 포함되지 않으면 더 긴 substring은 존재할 수 없으므로 탈출
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return longest
|
||||
}
|
||||
|
||||
fun getRemaining(original: String, common: String): String {
|
||||
if (common.isEmpty()) return original
|
||||
// 가장 처음 발견되는 공통 문자열을 한 번만 제거
|
||||
return original.replaceFirst(common, "").trim()
|
||||
}
|
||||
@@ -18,17 +18,16 @@ 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.min
|
||||
import androidx.compose.ui.unit.sp
|
||||
import kotlinx.coroutines.launch
|
||||
import model.MAX_BUDGET
|
||||
import model.MIN_PURCHASE_SCORE
|
||||
import model.ConfigIndex
|
||||
import model.KisSession
|
||||
import model.RankingStock
|
||||
import model.buyWeight
|
||||
import model.feesAndTaxRate
|
||||
import model.minimumNetProfit
|
||||
import network.KisTradeService
|
||||
import service.AutoTradingManager
|
||||
import util.MarketUtil
|
||||
import kotlin.math.min
|
||||
|
||||
enum class InvestmentGrade(
|
||||
val displayName: String,
|
||||
@@ -36,7 +35,8 @@ enum class InvestmentGrade(
|
||||
val shortWeight: Double = 0.0,
|
||||
val midWeight: Double = 0.0,
|
||||
val longWeight: Double = 0.0,
|
||||
val profitGuide: Double = 0.0,
|
||||
val profitGuide: ConfigIndex,
|
||||
val buyGuide: ConfigIndex,
|
||||
) {
|
||||
LEVEL_5_STRONG_RECOMMEND(
|
||||
displayName = "최상급 추천",
|
||||
@@ -44,7 +44,8 @@ enum class InvestmentGrade(
|
||||
shortWeight = 1.0,
|
||||
midWeight = 1.0,
|
||||
longWeight = 1.0,
|
||||
profitGuide = 1.8,
|
||||
profitGuide = ConfigIndex.GRADE_5_PROFIT,
|
||||
buyGuide = ConfigIndex.GRADE_5_BUY,
|
||||
),
|
||||
LEVEL_4_BALANCED_RECOMMEND(
|
||||
displayName = "균형 추천",
|
||||
@@ -52,7 +53,8 @@ enum class InvestmentGrade(
|
||||
shortWeight = 0.8,
|
||||
midWeight = 1.0,
|
||||
longWeight = 1.0,
|
||||
profitGuide = 1.3,
|
||||
profitGuide = ConfigIndex.GRADE_4_PROFIT,
|
||||
buyGuide = ConfigIndex.GRADE_4_BUY,
|
||||
),
|
||||
LEVEL_3_CAUTIOUS_RECOMMEND(
|
||||
displayName = "보수적 추천",
|
||||
@@ -60,7 +62,8 @@ enum class InvestmentGrade(
|
||||
shortWeight = 0.6,
|
||||
midWeight = 1.0,
|
||||
longWeight = 1.0,
|
||||
profitGuide = 0.9,
|
||||
profitGuide = ConfigIndex.GRADE_3_PROFIT,
|
||||
buyGuide = ConfigIndex.GRADE_3_BUY,
|
||||
),
|
||||
LEVEL_2_HIGH_RISK(
|
||||
displayName = "고위험 추천",
|
||||
@@ -68,7 +71,8 @@ enum class InvestmentGrade(
|
||||
shortWeight = 1.0,
|
||||
midWeight = 0.4,
|
||||
longWeight = 0.4,
|
||||
profitGuide = 0.7,
|
||||
profitGuide = ConfigIndex.GRADE_2_PROFIT,
|
||||
buyGuide = ConfigIndex.GRADE_2_BUY,
|
||||
),
|
||||
LEVEL_1_SPECULATIVE(
|
||||
displayName = "순수 공격적 선택",
|
||||
@@ -76,7 +80,8 @@ enum class InvestmentGrade(
|
||||
shortWeight = 1.0,
|
||||
midWeight = 0.2,
|
||||
longWeight = 0.2,
|
||||
profitGuide = 0.5,
|
||||
profitGuide = ConfigIndex.GRADE_1_PROFIT,
|
||||
buyGuide = ConfigIndex.GRADE_1_BUY,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -128,7 +133,7 @@ fun IntegratedOrderSection(
|
||||
}
|
||||
|
||||
var profitRate by remember(monitoringItem) {
|
||||
mutableStateOf(monitoringItem?.profitRate?.toString() ?: minimumNetProfit.toString())
|
||||
mutableStateOf(monitoringItem?.profitRate?.toString() ?: KisSession.config.getValues(ConfigIndex.PROFIT_INDEX).toString())
|
||||
}
|
||||
var stopLossRate by remember(monitoringItem) {
|
||||
mutableStateOf(monitoringItem?.stopLossRate?.toString() ?: "-1.5")
|
||||
@@ -164,19 +169,19 @@ fun IntegratedOrderSection(
|
||||
return when {
|
||||
// LEVEL_5: 단기·중기·장기 모두 매우 높고, 신뢰도까지 높음
|
||||
shortAvg >= 85.0 && midLongAvg >= 80.0 ->
|
||||
InvestmentGrade.LEVEL_5_STRONG_RECOMMEND
|
||||
if (ts.analyzer?.isOverheatedStock() ?: true) InvestmentGrade.LEVEL_4_BALANCED_RECOMMEND else InvestmentGrade.LEVEL_5_STRONG_RECOMMEND
|
||||
|
||||
// LEVEL_4: 중기·장기 기본 준수, 단기까지 양호
|
||||
midLongAvg >= 75.0 && shortAvg >= 70.0 ->
|
||||
InvestmentGrade.LEVEL_4_BALANCED_RECOMMEND
|
||||
if (ts.analyzer?.isOverheatedStock() ?: true) InvestmentGrade.LEVEL_3_CAUTIOUS_RECOMMEND else InvestmentGrade.LEVEL_4_BALANCED_RECOMMEND
|
||||
|
||||
// LEVEL_3: 중기·장기 기본 이상, 단기만 단기 변동성 높은 보수형
|
||||
midLongAvg >= 70.0 && shortAvg in 60.0..70.0 ->
|
||||
InvestmentGrade.LEVEL_3_CAUTIOUS_RECOMMEND
|
||||
if (ts.analyzer?.isOverheatedStock() ?: true) InvestmentGrade.LEVEL_2_HIGH_RISK else InvestmentGrade.LEVEL_3_CAUTIOUS_RECOMMEND
|
||||
|
||||
// LEVEL_2: 단기/초단기만 강하고, 중기·장기 애매
|
||||
shortAvg >= 75.0 && midLongAvg < 65.0 ->
|
||||
InvestmentGrade.LEVEL_2_HIGH_RISK
|
||||
if (ts.analyzer?.isOverheatedStock() ?: true) InvestmentGrade.LEVEL_1_SPECULATIVE else InvestmentGrade.LEVEL_2_HIGH_RISK
|
||||
|
||||
// LEVEL_1: 단기/초단기만 의미 있고, 중기·장기 심각히 약함
|
||||
shortAvg >= 70.0 && midLongAvg < 55.0 ->
|
||||
@@ -184,30 +189,24 @@ fun IntegratedOrderSection(
|
||||
|
||||
// 기본 조건은 충족했지만, 패턴에 잘 맞지 않을 때 (예: 중립)
|
||||
else ->
|
||||
InvestmentGrade.LEVEL_3_CAUTIOUS_RECOMMEND
|
||||
if (ts.analyzer?.isOverheatedStock() ?: true) InvestmentGrade.LEVEL_1_SPECULATIVE else InvestmentGrade.LEVEL_2_HIGH_RISK
|
||||
}
|
||||
}
|
||||
|
||||
fun excuteTrade(willEnableAutoSell: Boolean, orderQty: String, profitRate1: Double?,investmentGrade: InvestmentGrade = InvestmentGrade.LEVEL_2_HIGH_RISK) {
|
||||
scope.launch {
|
||||
val tickSize = MarketUtil.getTickSize(basePrice)
|
||||
val oneTickLowerPrice = basePrice - (tickSize * when(investmentGrade) {
|
||||
InvestmentGrade.LEVEL_5_STRONG_RECOMMEND -> 0
|
||||
InvestmentGrade.LEVEL_4_BALANCED_RECOMMEND -> 1
|
||||
InvestmentGrade.LEVEL_3_CAUTIOUS_RECOMMEND -> 2
|
||||
InvestmentGrade.LEVEL_2_HIGH_RISK -> 2
|
||||
InvestmentGrade.LEVEL_1_SPECULATIVE -> 3
|
||||
else -> 3
|
||||
})
|
||||
val oneTickLowerPrice = basePrice - (tickSize * KisSession.config.getValues(investmentGrade.buyGuide).toInt())
|
||||
|
||||
// 2. 주문 가격 설정 (직접 입력값이 없으면 한 틱 낮은 가격 사용)
|
||||
val finalPrice = if (orderPrice.isBlank()) {
|
||||
oneTickLowerPrice.toLong().toString()
|
||||
val finalPrice = MarketUtil.roundToTickSize(if (orderPrice.isBlank()) {
|
||||
oneTickLowerPrice.toDouble()
|
||||
} else {
|
||||
orderPrice
|
||||
}
|
||||
orderPrice.toDouble()
|
||||
})
|
||||
|
||||
println("basePrice : $basePrice, oneTickLowerPrice : $oneTickLowerPrice, finalPrice : $finalPrice")
|
||||
tradeService.postOrder(stockCode, orderQty, finalPrice, isBuy = true)
|
||||
tradeService.postOrder(stockCode, orderQty, finalPrice.toLong().toString(), isBuy = true)
|
||||
.onSuccess { realOrderNo -> // KIS 서버에서 생성된 실제 주문번호
|
||||
onOrderResult("주문 성공: $realOrderNo", true)
|
||||
if (willEnableAutoSell) {
|
||||
@@ -220,7 +219,9 @@ fun IntegratedOrderSection(
|
||||
|
||||
// 3. 실질 목표 수익률 계산
|
||||
// 사용자가 입력한 pRate와 (최소 순수익 + 제반 비용) 중 큰 값을 선택합니다.
|
||||
val effectiveProfitRate = maxOf((profitRate1 ?: pRate) + feesAndTaxRate, minimumNetProfit + feesAndTaxRate)
|
||||
var tax = KisSession.config.getValues(ConfigIndex.TAX_INDEX)
|
||||
val effectiveProfitRate = maxOf(((profitRate1 ?: pRate) + tax), (KisSession.config.getValues(
|
||||
ConfigIndex.PROFIT_INDEX) + tax))
|
||||
|
||||
// 4. 보정된 수익률을 적용하여 목표가 계산
|
||||
val calculatedTarget = MarketUtil.roundToTickSize(basePrice * (1 + effectiveProfitRate / 100.0))
|
||||
@@ -241,7 +242,7 @@ fun IntegratedOrderSection(
|
||||
))
|
||||
monitoringItem = DatabaseFactory.findConfigByCode(stockCode)
|
||||
onOrderSaved(realOrderNo)
|
||||
onOrderResult("매수 및 감시 설정 완료 (목표 수익률: ${String.format("%.2f", effectiveProfitRate)}%): $realOrderNo", true)
|
||||
onOrderResult("매수 및 감시 설정 완료 (목표 수익률: ${String.format("%.4f", effectiveProfitRate)}%): $realOrderNo", true)
|
||||
}
|
||||
}
|
||||
.onFailure { onOrderResult(it.message ?: "매수 실패", false) }
|
||||
@@ -257,6 +258,11 @@ fun IntegratedOrderSection(
|
||||
completeTradingDecision.stockCode.equals(stockCode)) {
|
||||
basePrice = completeTradingDecision.currentPrice
|
||||
println("basePrice $basePrice")
|
||||
val minScore = KisSession.config.getValues(ConfigIndex.MIN_PURCHASE_SCORE_INDEX)
|
||||
val maxBudget = KisSession.config.getValues(ConfigIndex.MAX_BUDGET_INDEX)
|
||||
val buyWeight = KisSession.config.getValues(ConfigIndex.BUY_WEIGHT_INDEX)
|
||||
val baseProfit = KisSession.config.getValues(ConfigIndex.PROFIT_INDEX)
|
||||
|
||||
fun resultCheck(completeTradingDecision :TradingDecision) {
|
||||
val weights = mapOf(
|
||||
"short" to 0.3, // 초단기 점수가 낮아도 전체에 미치는 영향 감소
|
||||
@@ -269,10 +275,10 @@ fun IntegratedOrderSection(
|
||||
((completeTradingDecision.profitPossible() + append) * weights["profit"]!!) +
|
||||
((completeTradingDecision.safePossible() + append) * weights["safe"]!!)
|
||||
|
||||
if (totalScore >= MIN_PURCHASE_SCORE && completeTradingDecision.confidence >= MIN_CONFIDENCE) {
|
||||
if (totalScore >= minScore && completeTradingDecision.confidence >= MIN_CONFIDENCE) {
|
||||
var investmentGrade : InvestmentGrade = getInvestmentGrade(completeTradingDecision,totalScore, completeTradingDecision.confidence)
|
||||
|
||||
val finalMargin = minimumNetProfit * investmentGrade.profitGuide
|
||||
val finalMargin = baseProfit * KisSession.config.getValues(investmentGrade.profitGuide)
|
||||
println("""
|
||||
사명 : ${completeTradingDecision.corpName}
|
||||
신뢰도 : ${completeTradingDecision.confidence + append}
|
||||
@@ -287,19 +293,19 @@ fun IntegratedOrderSection(
|
||||
// basePrice(현재가 혹은 지정가)를 기준으로 매수 가능 수량 산출 (최소 1주 보장)
|
||||
|
||||
val calculatedQty = if (basePrice > 0) {
|
||||
(MAX_BUDGET / basePrice).toInt().coerceAtLeast(1)
|
||||
(maxBudget / basePrice).toInt().coerceAtLeast(1)
|
||||
} else {
|
||||
1
|
||||
}
|
||||
// 5. 매수 실행 (계산된 finalMargin 전달)
|
||||
excuteTrade(
|
||||
willEnableAutoSell = true,
|
||||
orderQty = calculatedQty.toString(),
|
||||
orderQty = min(calculatedQty, KisSession.config.getValues(ConfigIndex.MAX_COUNT_INDEX).toInt()).toString(),
|
||||
profitRate1 = finalMargin,
|
||||
investmentGrade = investmentGrade,
|
||||
)
|
||||
|
||||
} else if(totalScore >= (MIN_PURCHASE_SCORE * 0.85) && completeTradingDecision.confidence + append >= (MIN_CONFIDENCE * 0.85)) {
|
||||
} else if(totalScore >= (minScore * 0.85) && completeTradingDecision.confidence + append >= (MIN_CONFIDENCE * 0.85)) {
|
||||
AutoTradingManager.addToReanalysis(RankingStock(mksc_shrn_iscd = completeTradingDecision.stockCode,hts_kor_isnm = completeTradingDecision.stockName))
|
||||
println("✋ [관망] 토탈 스코어 또는 신뢰도 미달 이나 약간의 오차로 재분석 대기열에 추가")
|
||||
} else {
|
||||
|
||||
@@ -47,16 +47,16 @@ fun SettingsScreen(onAuthSuccess: () -> Unit) {
|
||||
item {
|
||||
Text("거래 방식 선택", style = MaterialTheme.typography.h6)
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
RadioButton(selected = !config.isSimulation, onClick = { config = config.copy(isSimulation = false) })
|
||||
RadioButton(selected = !config.isSimulation, onClick = { config = config.copy(isSimulation = false,) })
|
||||
Text("실전투자")
|
||||
Spacer(Modifier.width(16.dp))
|
||||
RadioButton(selected = config.isSimulation, onClick = { config = config.copy(isSimulation = true) })
|
||||
RadioButton(selected = config.isSimulation, onClick = { config = config.copy() })
|
||||
Text("모의투자")
|
||||
}
|
||||
Divider(Modifier.padding(vertical = 12.dp))
|
||||
OutlinedTextField(
|
||||
value = config.htsId,
|
||||
onValueChange = { config = config.copy(htsId = it) },
|
||||
onValueChange = { config = config.copy(htsId = it,) },
|
||||
label = { Text("HTS ID (실시간 체결 통보용)") },
|
||||
modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp),
|
||||
placeholder = { Text("한국투자증권 HTS 접속 ID를 입력하세요") }
|
||||
@@ -64,22 +64,22 @@ fun SettingsScreen(onAuthSuccess: () -> Unit) {
|
||||
// 실전 3종 입력
|
||||
Text("실전투자 정보 (시세 조회 필수)", fontWeight = FontWeight.Bold)
|
||||
OutlinedTextField(value = config.realAccountNo, onValueChange = {
|
||||
config = config.copy(realAccountNo = it)
|
||||
config = config.copy(realAccountNo = it,)
|
||||
if(it.length >= 8) checkAndLoadConfig(it, true)
|
||||
}, label = { Text("실전 계좌번호") }, modifier = Modifier.fillMaxWidth())
|
||||
OutlinedTextField(value = config.realAppKey, onValueChange = { config = config.copy(realAppKey = it) }, label = { Text("실전 App Key") }, modifier = Modifier.fillMaxWidth())
|
||||
OutlinedTextField(value = config.realSecretKey, onValueChange = { config = config.copy(realSecretKey = it) }, label = { Text("실전 Secret Key") }, modifier = Modifier.fillMaxWidth(), visualTransformation = PasswordVisualTransformation())
|
||||
OutlinedTextField(value = config.realAppKey, onValueChange = { config = config.copy(realAppKey = it,) }, label = { Text("실전 App Key") }, modifier = Modifier.fillMaxWidth())
|
||||
OutlinedTextField(value = config.realSecretKey, onValueChange = { config = config.copy(realSecretKey = it,) }, label = { Text("실전 Secret Key") }, modifier = Modifier.fillMaxWidth(), visualTransformation = PasswordVisualTransformation())
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
// 모의 3종 입력
|
||||
Text("모의투자 정보", fontWeight = FontWeight.Bold)
|
||||
OutlinedTextField(value = config.vtsAccountNo, onValueChange = {
|
||||
config = config.copy(vtsAccountNo = it)
|
||||
config = config.copy(vtsAccountNo = it,)
|
||||
if(it.length >= 8) checkAndLoadConfig(it, false)
|
||||
}, label = { Text("모의 계좌번호") }, modifier = Modifier.fillMaxWidth())
|
||||
OutlinedTextField(value = config.vtsAppKey, onValueChange = { config = config.copy(vtsAppKey = it) }, label = { Text("모의 App Key") }, modifier = Modifier.fillMaxWidth())
|
||||
OutlinedTextField(value = config.vtsSecretKey, onValueChange = { config = config.copy(vtsSecretKey = it) }, label = { Text("모의 Secret Key") }, modifier = Modifier.fillMaxWidth(), visualTransformation = PasswordVisualTransformation())
|
||||
OutlinedTextField(value = config.vtsAppKey, onValueChange = { config = config.copy(vtsAppKey = it,) }, label = { Text("모의 App Key") }, modifier = Modifier.fillMaxWidth())
|
||||
OutlinedTextField(value = config.vtsSecretKey, onValueChange = { config = config.copy(vtsSecretKey = it,) }, label = { Text("모의 Secret Key") }, modifier = Modifier.fillMaxWidth(), visualTransformation = PasswordVisualTransformation())
|
||||
|
||||
Divider(Modifier.padding(vertical = 16.dp))
|
||||
|
||||
@@ -91,7 +91,7 @@ fun SettingsScreen(onAuthSuccess: () -> Unit) {
|
||||
val data = state.dragData
|
||||
if (data is DragData.FilesList) {
|
||||
val path = data.readFiles().firstOrNull()?.removePrefix("file:")
|
||||
if (path?.endsWith(".gguf") == true) config = config.copy(modelPath = path)
|
||||
if (path?.endsWith(".gguf") == true) config = config.copy(modelPath = path,)
|
||||
}
|
||||
}),
|
||||
contentAlignment = Alignment.Center
|
||||
@@ -104,7 +104,7 @@ fun SettingsScreen(onAuthSuccess: () -> Unit) {
|
||||
val data = state.dragData
|
||||
if (data is DragData.FilesList) {
|
||||
val embedModelPath = data.readFiles().firstOrNull()?.removePrefix("file:")
|
||||
if (embedModelPath?.endsWith(".gguf") == true) config = config.copy(embedModelPath = embedModelPath)
|
||||
if (embedModelPath?.endsWith(".gguf") == true) config = config.copy(embedModelPath = embedModelPath,)
|
||||
}
|
||||
}),
|
||||
contentAlignment = Alignment.Center
|
||||
|
||||
Reference in New Issue
Block a user