...
This commit is contained in:
parent
da4ab01d5f
commit
ad5e7e0ccb
@ -3,10 +3,10 @@ package model
|
|||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
|
|
||||||
const val feesAndTaxRate = 0.33
|
const val feesAndTaxRate = 0.33
|
||||||
const val minimumNetProfit = 0.35
|
const val minimumNetProfit = 0.8
|
||||||
const val buyWeight = 2.0
|
const val buyWeight = 2.0
|
||||||
val MAX_BUDGET = 60000.0
|
val MAX_BUDGET = 80000.0
|
||||||
val MAX_PRICE = 30000
|
val MAX_PRICE = 40000
|
||||||
val MIN_PRICE = 1000
|
val MIN_PRICE = 1000
|
||||||
val MIN_PURCHASE_SCORE = 65.0
|
val MIN_PURCHASE_SCORE = 65.0
|
||||||
data class AppConfig(
|
data class AppConfig(
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import kotlinx.serialization.Serializable
|
|||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class RealTimeTrade(
|
data class RealTimeTrade(
|
||||||
|
val code: String,
|
||||||
val time: String, // 체결 시간 (HH:mm:ss)
|
val time: String, // 체결 시간 (HH:mm:ss)
|
||||||
val price: String, // 체결가
|
val price: String, // 체결가
|
||||||
val change: String, // 전일 대비 (대비 기호 포함)
|
val change: String, // 전일 대비 (대비 기호 포함)
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import kotlinx.serialization.json.Json
|
|||||||
import kotlinx.serialization.json.jsonObject
|
import kotlinx.serialization.json.jsonObject
|
||||||
import kotlinx.serialization.json.jsonPrimitive
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
import model.KisSession
|
import model.KisSession
|
||||||
|
import model.RealTimeTrade
|
||||||
import util.AesCrypto
|
import util.AesCrypto
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
|
||||||
@ -24,7 +25,8 @@ class KisWebSocketManager {
|
|||||||
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||||
|
|
||||||
// 콜백 리스너
|
// 콜백 리스너
|
||||||
var onPriceUpdate: ((String, Double) -> Unit)? = null
|
var onPriceUpdate: ((RealTimeTrade) -> Unit)? = null
|
||||||
|
|
||||||
var onExecutionReceived: ((String, String, String, String, Boolean) -> Unit)? = null
|
var onExecutionReceived: ((String, String, String, String, Boolean) -> Unit)? = null
|
||||||
|
|
||||||
suspend fun connect() {
|
suspend fun connect() {
|
||||||
@ -70,8 +72,9 @@ class KisWebSocketManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private val _currentPrice = mutableStateOf("0")
|
// private val _currentPrice = mutableStateOf("0")
|
||||||
val currentPrice = _currentPrice
|
private val currentPrice = androidx.compose.runtime.mutableStateMapOf<String, String>()
|
||||||
|
// val currentPrice = _currentPrice
|
||||||
|
|
||||||
val tradeLogs = androidx.compose.runtime.mutableStateListOf<model.RealTimeTrade>()
|
val tradeLogs = androidx.compose.runtime.mutableStateListOf<model.RealTimeTrade>()
|
||||||
|
|
||||||
@ -111,20 +114,19 @@ class KisWebSocketManager {
|
|||||||
if (trId == "H0STCNT0") {
|
if (trId == "H0STCNT0") {
|
||||||
val dataRows = parts[3].split("^")
|
val dataRows = parts[3].split("^")
|
||||||
val price = dataRows[2]
|
val price = dataRows[2]
|
||||||
_currentPrice.value = price // 상태 업데이트
|
currentPrice[dataRows[0]] = price // 상태 업데이트
|
||||||
onPriceUpdate?.invoke(dataRows[0], price.toDoubleOrNull() ?: 0.0)
|
onPriceUpdate?.invoke(RealTimeTrade(
|
||||||
|
code = dataRows[0],
|
||||||
|
time = dataRows[1],
|
||||||
|
price = price,
|
||||||
|
change = dataRows[4],
|
||||||
|
volume = dataRows[12],
|
||||||
|
type = model.TradeType.NEUTRAL
|
||||||
|
))
|
||||||
|
|
||||||
// 로그 추가 (예시)
|
// 로그 추가 (예시)
|
||||||
tradeLogs.add(
|
// if (tradeLogs.isNotEmpty() && tradeLogs.last().code)
|
||||||
0, model.RealTimeTrade(
|
|
||||||
time = dataRows[1],
|
|
||||||
price = price,
|
|
||||||
change = dataRows[4],
|
|
||||||
volume = dataRows[12],
|
|
||||||
type = model.TradeType.NEUTRAL
|
|
||||||
)
|
|
||||||
)
|
|
||||||
if (tradeLogs.size > 50) tradeLogs.removeLast()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -133,7 +135,7 @@ class KisWebSocketManager {
|
|||||||
// AES 복호화 실행
|
// AES 복호화 실행
|
||||||
val decryptedData = AesCrypto.decrypt(parts[3], aesKey, aesIv)
|
val decryptedData = AesCrypto.decrypt(parts[3], aesKey, aesIv)
|
||||||
val dataRows = decryptedData.split("^")
|
val dataRows = decryptedData.split("^")
|
||||||
println("🔔 복호화된 체결 통보: ${if (dataRows[4] == "01") {"매도"} else {"매수"}} ${dataRows[8]} ${dataRows[9]}주 ${dataRows[13]} 체결")
|
println("🔔 복호화된 체결 통보: ${if (dataRows[4] == "01") {"매도"} else {"매수"}} ${dataRows[8]} ${dataRows[9]}주 ${if(dataRows[13] == "01"){"체결"}else{"접수"} }")
|
||||||
|
|
||||||
// UI 콜백 호출 (종목코드, 체결량, 체결가, 주문번호, 체결여부)
|
// UI 콜백 호출 (종목코드, 체결량, 체결가, 주문번호, 체결여부)
|
||||||
onExecutionReceived?.invoke(
|
onExecutionReceived?.invoke(
|
||||||
@ -150,13 +152,13 @@ class KisWebSocketManager {
|
|||||||
|
|
||||||
fun clearData() {
|
fun clearData() {
|
||||||
tradeLogs.clear()
|
tradeLogs.clear()
|
||||||
_currentPrice.value = "0"
|
currentPrice.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun subscribeStock(code: String, isSubscribe: Boolean = true) {
|
suspend fun subscribeStock(code: String, isSubscribe: Boolean = true) {
|
||||||
val trType = if (isSubscribe) "1" else "2"
|
val trType = if (isSubscribe) "1" else "2"
|
||||||
sendRequest(trType, "H0STCNT0", code)
|
sendRequest(trType, "H0STCNT0", code)
|
||||||
if (isSubscribe) println("📡 구독 등록: $code") else println("📴 구독 해제: $code")
|
// if (isSubscribe) println("📡 구독 등록: $code") else println("📴 구독 해제: $code")
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun sendRequest(trType: String, trId: String, trKey: String) {
|
private suspend fun sendRequest(trType: String, trId: String, trKey: String) {
|
||||||
|
|||||||
@ -42,7 +42,7 @@ object AutoTradingManager {
|
|||||||
|
|
||||||
// 설정 상수
|
// 설정 상수
|
||||||
private const val MIN_RISE_RATE = 0.1
|
private const val MIN_RISE_RATE = 0.1
|
||||||
private const val MAX_RISE_RATE = 19.0
|
private const val MAX_RISE_RATE = 21.0
|
||||||
private const val CYCLE_TIMEOUT = 30 * 60 * 1000L // 한 사이클 최대 10분
|
private const val CYCLE_TIMEOUT = 30 * 60 * 1000L // 한 사이클 최대 10분
|
||||||
private const val WATCHDOG_CHECK_INTERVAL = 30 * 1000L // 30초마다 생존 확인
|
private const val WATCHDOG_CHECK_INTERVAL = 30 * 1000L // 30초마다 생존 확인
|
||||||
private const val STUCK_THRESHOLD = 5 * 60 * 1000L // 5분간 반응 없으면 'Stuck'으로 판단
|
private const val STUCK_THRESHOLD = 5 * 60 * 1000L // 5분간 반응 없으면 'Stuck'으로 판단
|
||||||
@ -50,7 +50,8 @@ object AutoTradingManager {
|
|||||||
fun isRunning(): Boolean = discoveryJob?.isActive == true
|
fun isRunning(): Boolean = discoveryJob?.isActive == true
|
||||||
private var remainingCandidates = mutableListOf<RankingStock>()
|
private var remainingCandidates = mutableListOf<RankingStock>()
|
||||||
// private val processedCodes = mutableSetOf<String>() // 중복 처리 방지용 (선택 사항)
|
// private val processedCodes = mutableSetOf<String>() // 중복 처리 방지용 (선택 사항)
|
||||||
|
private val reanalysisList = mutableListOf<RankingStock>()
|
||||||
|
private val retryCountMap = mutableMapOf<String, Int>()
|
||||||
/**
|
/**
|
||||||
* 자동 발굴 루프 시작 및 Watchdog 실행
|
* 자동 발굴 루프 시작 및 Watchdog 실행
|
||||||
*/
|
*/
|
||||||
@ -88,7 +89,7 @@ object AutoTradingManager {
|
|||||||
// [프로세스 1] 장 마감 및 잔고 체크
|
// [프로세스 1] 장 마감 및 잔고 체크
|
||||||
val now = LocalTime.now(ZoneId.of("Asia/Seoul"))
|
val now = LocalTime.now(ZoneId.of("Asia/Seoul"))
|
||||||
//&& now.isBefore(LocalTime.of(15, 30))
|
//&& now.isBefore(LocalTime.of(15, 30))
|
||||||
if (now.isAfter(LocalTime.of(15, 0)) ) {
|
if (now.isAfter(LocalTime.of(15, 30)) ) {
|
||||||
executeClosingLiquidation(tradeService)
|
executeClosingLiquidation(tradeService)
|
||||||
return@withTimeout
|
return@withTimeout
|
||||||
}
|
}
|
||||||
@ -99,25 +100,31 @@ object AutoTradingManager {
|
|||||||
val pendingStocks = DatabaseFactory.findAllMonitoringTrades().map { it.code }
|
val pendingStocks = DatabaseFactory.findAllMonitoringTrades().map { it.code }
|
||||||
// [프로세스 2] 후보군 수집
|
// [프로세스 2] 후보군 수집
|
||||||
if (remainingCandidates.isEmpty()) {
|
if (remainingCandidates.isEmpty()) {
|
||||||
val candidates = fetchCandidates(tradeService).apply {
|
val candidates: MutableList<RankingStock> = fetchCandidates(tradeService).apply {
|
||||||
println("후보군 총 개수 : $size")
|
println("후보군 총 개수 : $size")
|
||||||
}
|
}
|
||||||
.filter { (it.prdy_ctrt.toDoubleOrNull() ?: 0.0) in MIN_RISE_RATE..MAX_RISE_RATE }
|
.filter { (it.prdy_ctrt.toDoubleOrNull() ?: 0.0) in MIN_RISE_RATE..MAX_RISE_RATE }
|
||||||
.filter { it.code !in myHoldings && it.code !in pendingStocks }
|
.filter { it.code !in myHoldings && it.code !in pendingStocks }
|
||||||
.distinctBy { it.code }
|
.filter { !it.name.contains("호스팩", true) }
|
||||||
.sortedBy { (it.prdy_ctrt.toDoubleOrNull() ?: 0.0) }
|
.sortedBy { (it.prdy_ctrt.toDoubleOrNull() ?: 0.0) }
|
||||||
.apply {
|
.toMutableList()
|
||||||
println("후보군 조건 충족 총 개수 : $size")
|
if (reanalysisList.isNotEmpty()) {
|
||||||
}
|
candidates.addAll(reanalysisList)
|
||||||
remainingCandidates.addAll(candidates)
|
}
|
||||||
|
remainingCandidates.addAll(candidates.distinctBy { it.code })
|
||||||
} else {
|
} else {
|
||||||
println("미확인 데이터 ${remainingCandidates.size}")
|
println("미확인 데이터 ${remainingCandidates.size}")
|
||||||
}
|
}
|
||||||
// [프로세스 3] 종목별 순회 분석
|
|
||||||
val iterator = remainingCandidates.iterator()
|
|
||||||
while (iterator.hasNext()) {
|
|
||||||
val stock = iterator.next()
|
|
||||||
|
|
||||||
|
|
||||||
|
// [프로세스 3] 종목별 순회 분석
|
||||||
|
var totalCount = remainingCandidates.size
|
||||||
|
println("후보군 조건 충족 총 개수 : ${totalCount}")
|
||||||
|
val iterator = remainingCandidates.iterator()
|
||||||
|
|
||||||
|
while (iterator.hasNext()) {
|
||||||
|
totalCount--
|
||||||
|
val stock = iterator.next()
|
||||||
try {
|
try {
|
||||||
processSingleStock(stock, myCash, tradeService, callback)
|
processSingleStock(stock, myCash, tradeService, callback)
|
||||||
// 성공적으로 처리(또는 분석 완료) 후 리스트에서 제거
|
// 성공적으로 처리(또는 분석 완료) 후 리스트에서 제거
|
||||||
@ -128,6 +135,7 @@ object AutoTradingManager {
|
|||||||
} finally {
|
} finally {
|
||||||
iterator.remove()
|
iterator.remove()
|
||||||
}
|
}
|
||||||
|
println("남은 후보군 개수 : ${totalCount}")
|
||||||
delay(300)
|
delay(300)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -145,29 +153,40 @@ object AutoTradingManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun addToReanalysis(stock: RankingStock) {
|
||||||
|
val count = retryCountMap.getOrDefault(stock.code, 0)
|
||||||
|
if (count < 2) { // 최대 2회까지만 재시도하여 무한 루프 방지
|
||||||
|
retryCountMap[stock.code] = count + 1
|
||||||
|
reanalysisList.add(stock)
|
||||||
|
println("📝 [Memory] ${stock.name} 관망 판정 -> 차기 루프 재분석 리스트 등록")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun processSingleStock(stock: RankingStock, myCash: Long, tradeService: KisTradeService, callback: TradingDecisionCallback) {
|
private suspend fun processSingleStock(stock: RankingStock, myCash: Long, tradeService: KisTradeService, callback: TradingDecisionCallback) {
|
||||||
try {
|
try {
|
||||||
// 개별 종목 분석은 최대 2분으로 제한
|
// 개별 종목 분석은 최대 2분으로 제한
|
||||||
withTimeout(120000L) {
|
withTimeout(120000L) {
|
||||||
val corpInfo = DartCodeManager.getCorpCode(stock.code)
|
val corpInfo = DartCodeManager.getCorpCode(stock.code)
|
||||||
if (corpInfo?.cName.isNullOrEmpty()) {
|
if (corpInfo?.cName.isNullOrEmpty()) {
|
||||||
// println("⏭️ [제외] ${stock.name}: 법인명 정보를 찾을 수 없음")
|
|
||||||
return@withTimeout
|
return@withTimeout
|
||||||
}
|
}
|
||||||
|
|
||||||
val dailyData = tradeService.fetchPeriodChartData(stock.code, "D", true).getOrNull() ?: return@withTimeout
|
|
||||||
val today = dailyData.lastOrNull() ?: return@withTimeout
|
|
||||||
val currentPrice = today.stck_prpr.toDouble()
|
|
||||||
|
|
||||||
if (currentPrice > myCash || currentPrice > MAX_PRICE || currentPrice < MIN_PRICE) return@withTimeout
|
|
||||||
|
|
||||||
println("🔍 [분석 진입] ${stock.name} (${LocalTime.now()})")
|
|
||||||
callback(TradingDecision().apply {
|
callback(TradingDecision().apply {
|
||||||
this.stockCode = stock.code
|
this.stockCode = stock.code
|
||||||
this.confidence = -1.0
|
this.confidence = -1.0
|
||||||
this.stockName = stock.name
|
this.stockName = stock.name
|
||||||
}, false)
|
}, false)
|
||||||
|
|
||||||
|
val dailyData = tradeService.fetchPeriodChartData(stock.code, "D", true).getOrNull() ?: return@withTimeout
|
||||||
|
val today = dailyData.lastOrNull() ?: return@withTimeout
|
||||||
|
val currentPrice = today.stck_prpr.toDouble()
|
||||||
|
|
||||||
|
if (currentPrice > myCash || currentPrice > MAX_PRICE || currentPrice < MIN_PRICE) {
|
||||||
|
return@withTimeout
|
||||||
|
}
|
||||||
|
|
||||||
|
println("🔍 [분석 진입] ${stock.name} (${LocalTime.now()})")
|
||||||
|
|
||||||
|
|
||||||
val analyzer = coroutineScope {
|
val analyzer = coroutineScope {
|
||||||
val min30 = async { tradeService.fetchChartData(stock.code, true).getOrDefault(emptyList()) }
|
val min30 = async { tradeService.fetchChartData(stock.code, true).getOrDefault(emptyList()) }
|
||||||
val weekly = async { tradeService.fetchPeriodChartData(stock.code, "W", true).getOrDefault(emptyList()) }
|
val weekly = async { tradeService.fetchPeriodChartData(stock.code, "W", true).getOrDefault(emptyList()) }
|
||||||
|
|||||||
@ -282,7 +282,6 @@ object SafeScraper {
|
|||||||
delay(Random.nextLong(500, 1500))
|
delay(Random.nextLong(500, 1500))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
println("🏁 뉴스 처리 완료")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -246,7 +246,7 @@ fun DashboardScreen() {
|
|||||||
},
|
},
|
||||||
stockCode = selectedStockCode,
|
stockCode = selectedStockCode,
|
||||||
stockName = selectedStockName,
|
stockName = selectedStockName,
|
||||||
currentPrice = wsManager.currentPrice.value,
|
currentPrice = "0",
|
||||||
trades = wsManager.tradeLogs,
|
trades = wsManager.tradeLogs,
|
||||||
tradingDecisionCallback = { decision,bool ->
|
tradingDecisionCallback = { decision,bool ->
|
||||||
if (bool && decision != null && KisSession.config.isSimulation) {
|
if (bool && decision != null && KisSession.config.isSimulation) {
|
||||||
|
|||||||
@ -22,10 +22,12 @@ import androidx.compose.ui.unit.sp
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import model.MAX_BUDGET
|
import model.MAX_BUDGET
|
||||||
import model.MIN_PURCHASE_SCORE
|
import model.MIN_PURCHASE_SCORE
|
||||||
|
import model.RankingStock
|
||||||
import model.buyWeight
|
import model.buyWeight
|
||||||
import model.feesAndTaxRate
|
import model.feesAndTaxRate
|
||||||
import model.minimumNetProfit
|
import model.minimumNetProfit
|
||||||
import network.KisTradeService
|
import network.KisTradeService
|
||||||
|
import service.AutoTradingManager
|
||||||
import util.MarketUtil
|
import util.MarketUtil
|
||||||
|
|
||||||
enum class InvestmentGrade(
|
enum class InvestmentGrade(
|
||||||
@ -50,7 +52,7 @@ enum class InvestmentGrade(
|
|||||||
shortWeight = 0.8,
|
shortWeight = 0.8,
|
||||||
midWeight = 1.0,
|
midWeight = 1.0,
|
||||||
longWeight = 1.0,
|
longWeight = 1.0,
|
||||||
profitGuide = 1.4,
|
profitGuide = 1.3,
|
||||||
),
|
),
|
||||||
LEVEL_3_CAUTIOUS_RECOMMEND(
|
LEVEL_3_CAUTIOUS_RECOMMEND(
|
||||||
displayName = "보수적 추천",
|
displayName = "보수적 추천",
|
||||||
@ -58,7 +60,7 @@ enum class InvestmentGrade(
|
|||||||
shortWeight = 0.6,
|
shortWeight = 0.6,
|
||||||
midWeight = 1.0,
|
midWeight = 1.0,
|
||||||
longWeight = 1.0,
|
longWeight = 1.0,
|
||||||
profitGuide = 1.0,
|
profitGuide = 0.9,
|
||||||
),
|
),
|
||||||
LEVEL_2_HIGH_RISK(
|
LEVEL_2_HIGH_RISK(
|
||||||
displayName = "고위험 추천",
|
displayName = "고위험 추천",
|
||||||
@ -66,7 +68,7 @@ enum class InvestmentGrade(
|
|||||||
shortWeight = 1.0,
|
shortWeight = 1.0,
|
||||||
midWeight = 0.4,
|
midWeight = 0.4,
|
||||||
longWeight = 0.4,
|
longWeight = 0.4,
|
||||||
profitGuide = 0.8,
|
profitGuide = 0.7,
|
||||||
),
|
),
|
||||||
LEVEL_1_SPECULATIVE(
|
LEVEL_1_SPECULATIVE(
|
||||||
displayName = "순수 공격적 선택",
|
displayName = "순수 공격적 선택",
|
||||||
@ -74,7 +76,7 @@ enum class InvestmentGrade(
|
|||||||
shortWeight = 1.0,
|
shortWeight = 1.0,
|
||||||
midWeight = 0.2,
|
midWeight = 0.2,
|
||||||
longWeight = 0.2,
|
longWeight = 0.2,
|
||||||
profitGuide = 0.6,
|
profitGuide = 0.5,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -131,10 +133,14 @@ fun IntegratedOrderSection(
|
|||||||
var stopLossRate by remember(monitoringItem) {
|
var stopLossRate by remember(monitoringItem) {
|
||||||
mutableStateOf(monitoringItem?.stopLossRate?.toString() ?: "-1.5")
|
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(
|
fun getInvestmentGrade(
|
||||||
ts: TradingDecision,
|
ts: TradingDecision,
|
||||||
@ -187,9 +193,9 @@ fun IntegratedOrderSection(
|
|||||||
val tickSize = MarketUtil.getTickSize(basePrice)
|
val tickSize = MarketUtil.getTickSize(basePrice)
|
||||||
val oneTickLowerPrice = basePrice - (tickSize * when(investmentGrade) {
|
val oneTickLowerPrice = basePrice - (tickSize * when(investmentGrade) {
|
||||||
InvestmentGrade.LEVEL_5_STRONG_RECOMMEND -> 1
|
InvestmentGrade.LEVEL_5_STRONG_RECOMMEND -> 1
|
||||||
InvestmentGrade.LEVEL_4_BALANCED_RECOMMEND -> 2
|
InvestmentGrade.LEVEL_4_BALANCED_RECOMMEND -> 1
|
||||||
InvestmentGrade.LEVEL_3_CAUTIOUS_RECOMMEND -> 3
|
InvestmentGrade.LEVEL_3_CAUTIOUS_RECOMMEND -> 2
|
||||||
InvestmentGrade.LEVEL_2_HIGH_RISK -> 3
|
InvestmentGrade.LEVEL_2_HIGH_RISK -> 2
|
||||||
InvestmentGrade.LEVEL_1_SPECULATIVE -> 4
|
InvestmentGrade.LEVEL_1_SPECULATIVE -> 4
|
||||||
else -> 4
|
else -> 4
|
||||||
})
|
})
|
||||||
@ -249,10 +255,8 @@ fun IntegratedOrderSection(
|
|||||||
var append = 0.0
|
var append = 0.0
|
||||||
if (completeTradingDecision != null &&
|
if (completeTradingDecision != null &&
|
||||||
completeTradingDecision.stockCode.equals(stockCode)) {
|
completeTradingDecision.stockCode.equals(stockCode)) {
|
||||||
|
println("basePrice $basePrice")
|
||||||
fun resultCheck(completeTradingDecision :TradingDecision) {
|
fun resultCheck(completeTradingDecision :TradingDecision) {
|
||||||
|
|
||||||
|
|
||||||
val weights = mapOf(
|
val weights = mapOf(
|
||||||
"short" to 0.3, // 초단기 점수가 낮아도 전체에 미치는 영향 감소
|
"short" to 0.3, // 초단기 점수가 낮아도 전체에 미치는 영향 감소
|
||||||
"profit" to 0.3,
|
"profit" to 0.3,
|
||||||
@ -263,23 +267,25 @@ fun IntegratedOrderSection(
|
|||||||
(completeTradingDecision.shortPossible() * weights["short"]!!) +
|
(completeTradingDecision.shortPossible() * weights["short"]!!) +
|
||||||
(completeTradingDecision.profitPossible() * weights["profit"]!!) +
|
(completeTradingDecision.profitPossible() * weights["profit"]!!) +
|
||||||
(completeTradingDecision.safePossible() * weights["safe"]!!)
|
(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) {
|
if (totalScore >= MIN_PURCHASE_SCORE && completeTradingDecision.confidence >= MIN_CONFIDENCE) {
|
||||||
var investmentGrade : InvestmentGrade = getInvestmentGrade(completeTradingDecision,totalScore, completeTradingDecision.confidence)
|
var investmentGrade : InvestmentGrade = getInvestmentGrade(completeTradingDecision,totalScore, completeTradingDecision.confidence)
|
||||||
// 4. 점수에 따른 가변 마진 적용
|
// 4. 점수에 따른 가변 마진 적용
|
||||||
// 토탈 스코어가 85점 이상이면 마진을 3.0으로 고정하거나 추가 가산(append) 적용
|
// 토탈 스코어가 85점 이상이면 마진을 3.0으로 고정하거나 추가 가산(append) 적용
|
||||||
val finalMargin = minimumNetProfit * investmentGrade.profitGuide
|
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}")
|
println("🚀 [매수 진행] 토탈 스코어: ${String.format("%.1f", totalScore)} -> 종목: ${completeTradingDecision.stockCode}")
|
||||||
|
|
||||||
// basePrice(현재가 혹은 지정가)를 기준으로 매수 가능 수량 산출 (최소 1주 보장)
|
// basePrice(현재가 혹은 지정가)를 기준으로 매수 가능 수량 산출 (최소 1주 보장)
|
||||||
|
|
||||||
val calculatedQty = if (basePrice > 0) {
|
val calculatedQty = if (basePrice > 0) {
|
||||||
(MAX_BUDGET / basePrice).toInt().coerceAtLeast(1)
|
(MAX_BUDGET / basePrice).toInt().coerceAtLeast(1)
|
||||||
} else {
|
} else {
|
||||||
@ -293,8 +299,11 @@ fun IntegratedOrderSection(
|
|||||||
investmentGrade = investmentGrade,
|
investmentGrade = investmentGrade,
|
||||||
)
|
)
|
||||||
|
|
||||||
} else {
|
} else if(totalScore >= (MIN_PURCHASE_SCORE * 0.85) && completeTradingDecision.confidence >= (MIN_CONFIDENCE * 0.85)) {
|
||||||
println("✋ [관망] 토탈 스코어(${String.format("%.1f", totalScore)})가 기준치($MIN_PURCHASE_SCORE) 미달 또는 신뢰도 ${completeTradingDecision.confidence}가 기준치 ${MIN_CONFIDENCE} 미달")
|
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) {
|
when (completeTradingDecision?.decision) {
|
||||||
|
|||||||
@ -21,8 +21,6 @@ import model.CandleData
|
|||||||
import network.DartCodeManager
|
import network.DartCodeManager
|
||||||
import network.KisTradeService
|
import network.KisTradeService
|
||||||
import network.KisWebSocketManager
|
import network.KisWebSocketManager
|
||||||
import network.NewsService
|
|
||||||
import service.TechnicalAnalyzer
|
|
||||||
import java.time.LocalTime
|
import java.time.LocalTime
|
||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
import kotlin.collections.isNotEmpty
|
import kotlin.collections.isNotEmpty
|
||||||
@ -44,7 +42,7 @@ fun StockDetailSection(
|
|||||||
yearSummary : MutableList<CandleData>
|
yearSummary : MutableList<CandleData>
|
||||||
) {
|
) {
|
||||||
|
|
||||||
var openPrice by remember { mutableStateOf("0") }
|
// var openPrice by remember { mutableStateOf("0") }
|
||||||
var chartData by remember { mutableStateOf<List<CandleData>>(emptyList()) }
|
var chartData by remember { mutableStateOf<List<CandleData>>(emptyList()) }
|
||||||
var isLoading by remember { mutableStateOf(false) }
|
var isLoading by remember { mutableStateOf(false) }
|
||||||
var resultMessage by remember { mutableStateOf("") }
|
var resultMessage by remember { mutableStateOf("") }
|
||||||
@ -62,6 +60,8 @@ fun StockDetailSection(
|
|||||||
|
|
||||||
// 이전 종목 코드를 기억하기 위한 상태
|
// 이전 종목 코드를 기억하기 위한 상태
|
||||||
var previousCode by remember { mutableStateOf("") }
|
var previousCode by remember { mutableStateOf("") }
|
||||||
|
var latestPrice by remember { mutableStateOf("0") }
|
||||||
|
|
||||||
|
|
||||||
// 종목 변경 시 데이터 로드 및 웹소켓 구독 관리
|
// 종목 변경 시 데이터 로드 및 웹소켓 구독 관리
|
||||||
LaunchedEffect(stockCode) {
|
LaunchedEffect(stockCode) {
|
||||||
@ -81,16 +81,62 @@ fun StockDetailSection(
|
|||||||
// 2. 차트 데이터 로드 (KisSession 기반으로 파라미터 간소화)
|
// 2. 차트 데이터 로드 (KisSession 기반으로 파라미터 간소화)
|
||||||
|
|
||||||
coroutineScope {
|
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)
|
launch {tradeService.fetchChartData(stockCode, isDomestic)
|
||||||
.onSuccess { data ->
|
.onSuccess { data ->
|
||||||
println("✅ 차트 데이터 로드 성공: ${data.size}개") // ${} 사용하여 정확히 출력
|
// println("✅ 차트 데이터 로드 성공: ${data.size}개") // ${} 사용하여 정확히 출력
|
||||||
chartData = data
|
chartData = data
|
||||||
min30.clear()
|
min30.clear()
|
||||||
min30.addAll(chartData)
|
min30.addAll(chartData)
|
||||||
}
|
}
|
||||||
.onFailure { error ->
|
.onFailure { error ->
|
||||||
println("❌ 차트 데이터 로드 실패: ${error.localizedMessage}")
|
// println("❌ 차트 데이터 로드 실패: ${error.localizedMessage}")
|
||||||
chartData = emptyList()
|
chartData = emptyList()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -122,41 +168,43 @@ fun StockDetailSection(
|
|||||||
isLoading = false
|
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()
|
|
||||||
|
|
||||||
// 현재 시간(분 단위) 확인
|
// LaunchedEffect(latestPrice) {
|
||||||
val currentMinute = LocalTime.now().format(DateTimeFormatter.ofPattern("HHmm00"))
|
// println("latestPrice >>> $latestPrice")
|
||||||
|
// if (chartData.isNotEmpty() && latestPrice != "0") {
|
||||||
if (lastCandle.stck_bsop_date != currentMinute) {
|
// val latestPrice = latestPrice ?: "0"
|
||||||
// [개선] 시간이 바뀌었으면 새로운 캔들 추가 (차트가 밀려나는 효과)
|
// val priceDouble = latestPrice?.replace(",", "")?.toDoubleOrNull() ?: return@LaunchedEffect
|
||||||
val newCandle = CandleData(
|
// val lastCandle = chartData.last()
|
||||||
stck_bsop_date = currentMinute,
|
//
|
||||||
stck_oprc = latestPrice,
|
// // 현재 시간(분 단위) 확인
|
||||||
stck_hgpr = latestPrice,
|
// val currentMinute = LocalTime.now().format(DateTimeFormatter.ofPattern("HHmm00"))
|
||||||
stck_lwpr = latestPrice,
|
//
|
||||||
stck_prpr = latestPrice,
|
// if (lastCandle.stck_bsop_date != currentMinute) {
|
||||||
stck_cntg_hour = currentMinute,
|
// // [개선] 시간이 바뀌었으면 새로운 캔들 추가 (차트가 밀려나는 효과)
|
||||||
cntg_vol = "1",
|
// val newCandle = CandleData(
|
||||||
acml_tr_pbmn = "1",
|
// stck_bsop_date = currentMinute,
|
||||||
)
|
// stck_oprc = latestPrice,
|
||||||
// 최대 100개까지만 유지하여 성능 최적화
|
// stck_hgpr = latestPrice,
|
||||||
chartData = (chartData + newCandle).takeLast(100)
|
// stck_lwpr = latestPrice,
|
||||||
} else {
|
// stck_prpr = latestPrice,
|
||||||
// 같은 분 내에서는 기존 마지막 캔들만 업데이트
|
// stck_cntg_hour = currentMinute,
|
||||||
val updatedCandle = lastCandle.copy(
|
// cntg_vol = "1",
|
||||||
stck_prpr = latestPrice,
|
// acml_tr_pbmn = "1",
|
||||||
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
|
// // 최대 100개까지만 유지하여 성능 최적화
|
||||||
)
|
// chartData = (chartData + newCandle).takeLast(100)
|
||||||
chartData = chartData.dropLast(1) + updatedCandle
|
// } 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)) {
|
Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
|
||||||
// [상단] 종목명 및 상태 메시지
|
// [상단] 종목명 및 상태 메시지
|
||||||
@ -170,7 +218,7 @@ fun StockDetailSection(
|
|||||||
code = stockCode,
|
code = stockCode,
|
||||||
isDomestic = isDomestic,
|
isDomestic = isDomestic,
|
||||||
previousClose = previousClose,
|
previousClose = previousClose,
|
||||||
openPrice = openPrice,
|
openPrice = latestPrice,
|
||||||
resultMessage = resultMessage,
|
resultMessage = resultMessage,
|
||||||
resultMessageClear = {resultMessage = ""},
|
resultMessageClear = {resultMessage = ""},
|
||||||
isSuccess = isSuccess
|
isSuccess = isSuccess
|
||||||
@ -179,10 +227,10 @@ fun StockDetailSection(
|
|||||||
// 실시간 가격 표시 (WebSocket 데이터)
|
// 실시간 가격 표시 (WebSocket 데이터)
|
||||||
Column(horizontalAlignment = Alignment.End) {
|
Column(horizontalAlignment = Alignment.End) {
|
||||||
Text(
|
Text(
|
||||||
text = "${wsManager.currentPrice.value} 원",
|
text = "${latestPrice} 원",
|
||||||
style = MaterialTheme.typography.h4,
|
style = MaterialTheme.typography.h4,
|
||||||
fontWeight = FontWeight.Bold,
|
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)
|
Text("실시간 체결가", style = MaterialTheme.typography.caption, color = Color.Gray)
|
||||||
}
|
}
|
||||||
@ -228,7 +276,7 @@ fun StockDetailSection(
|
|||||||
stockCode = stockCode,
|
stockCode = stockCode,
|
||||||
stockName = stockName,
|
stockName = stockName,
|
||||||
isDomestic = isDomestic,
|
isDomestic = isDomestic,
|
||||||
currentPrice = wsManager.currentPrice.value,
|
currentPrice = latestPrice,
|
||||||
holdingQuantity = holdingQuantity,
|
holdingQuantity = holdingQuantity,
|
||||||
tradeService = tradeService,
|
tradeService = tradeService,
|
||||||
onOrderSaved = onOrderSaved,
|
onOrderSaved = onOrderSaved,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user