...
This commit is contained in:
@@ -28,7 +28,6 @@ import kotlinx.serialization.Serializable
|
||||
import model.CandleData
|
||||
import model.ConfigIndex
|
||||
import model.ExecutionData
|
||||
import model.InvestmentGrade
|
||||
import model.KisSession
|
||||
import model.RankingStock
|
||||
import model.RankingType
|
||||
@@ -103,7 +102,7 @@ object AutoTradingManager {
|
||||
println("🚀 [자동매수 실행] ${completeTradingDecision.stockName}")
|
||||
if (completeTradingDecision.confidence < 10) {
|
||||
addToReanalysis(RankingStock(mksc_shrn_iscd = completeTradingDecision.stockCode,hts_kor_isnm = completeTradingDecision.stockName))
|
||||
TradingLogStore.addLog(completeTradingDecision,"HOLD","분석 신뢰도 오류 인지로 재분석 대기열에 추가")
|
||||
TradingLogStore.addLog(completeTradingDecision,"RETRY","분석 신뢰도 오류 인지로 재분석 대기열에 추가")
|
||||
}else if (completeTradingDecision != null && !completeTradingDecision.stockCode.isNullOrEmpty()) {
|
||||
var basePrice = completeTradingDecision.currentPrice
|
||||
var stockCode = completeTradingDecision.stockCode
|
||||
@@ -129,19 +128,10 @@ object AutoTradingManager {
|
||||
var investmentGrade : InvestmentGrade = AutoTradingManager.getInvestmentGrade(completeTradingDecision,totalScore, completeTradingDecision.confidence)
|
||||
|
||||
val finalMargin = baseProfit * KisSession.config.getValues(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 gradeRate = (1.0 - (investmentGrade.ordinal * 0.1))
|
||||
val gradeRate = (1.0 - (investmentGrade.ordinal * 0.12))
|
||||
val maxQty = (KisSession.config.getValues(ConfigIndex.MAX_COUNT_INDEX) * gradeRate).roundToInt()
|
||||
maxBudget = maxBudget * gradeRate
|
||||
val calculatedQty = if (basePrice > 0) {
|
||||
@@ -159,7 +149,7 @@ object AutoTradingManager {
|
||||
|
||||
} else if(totalScore >= (minScore * 0.85) && completeTradingDecision.confidence + append >= (MIN_CONFIDENCE * 0.85)) {
|
||||
addToReanalysis(RankingStock(mksc_shrn_iscd = completeTradingDecision.stockCode,hts_kor_isnm = completeTradingDecision.stockName))
|
||||
TradingLogStore.addLog(completeTradingDecision,"HOLD","✋ [관망] 토탈 스코어[$totalScore] 또는 신뢰도[${completeTradingDecision.confidence}] 미달 이나 약간의 오차로 재분석 대기열에 추가")
|
||||
TradingLogStore.addLog(completeTradingDecision,"RETRY","✋ [관망] 토탈 스코어[$totalScore] 또는 신뢰도[${completeTradingDecision.confidence}] 미달 이나 약간의 오차로 재분석 대기열에 추가")
|
||||
} else {
|
||||
TradingLogStore.addLog(completeTradingDecision,"HOLD","✋ [관망] 토탈 스코어(${String.format("%.1f[${minScore}]", totalScore)}) 또는 신뢰도 (${String.format("%.1f[${MIN_CONFIDENCE}]", completeTradingDecision.confidence)}) 미달")
|
||||
}
|
||||
@@ -285,7 +275,7 @@ object AutoTradingManager {
|
||||
|
||||
if (it.message?.contains("주문가능금액을 초과") == true) {
|
||||
AutoTradingManager.addToReanalysis(RankingStock(mksc_shrn_iscd = stockCode,hts_kor_isnm = stockName))
|
||||
TradingLogStore.addLog(decision,"BUY","${it.message ?: " 매수 실패"} => 재분석 대기열에 추가")
|
||||
TradingLogStore.addLog(decision,"WATCH","${it.message ?: " 매수 실패"} => 재분석 대기열에 추가")
|
||||
} else {
|
||||
TradingLogStore.addLog(decision,"BUY",it.message ?: "매수 실패")
|
||||
}
|
||||
@@ -382,6 +372,11 @@ object AutoTradingManager {
|
||||
balance.holdings.forEach { holding ->
|
||||
if (BLACKLISTEDSTOCKCODES.contains(holding.code)){
|
||||
println("❌ 차단 처리된 주식 : ${holding.name}")
|
||||
TradingLogStore.addAnalyzer(
|
||||
holding.name,
|
||||
holding.code,
|
||||
"거랙 차단 대상 : ${holding.currentPrice}[${holding.quantity}주] 보유, 수익률(${holding.profitRate.toDouble()})"
|
||||
)
|
||||
} else {
|
||||
if (holding != null && holding.quantity.toInt() > 0 && holding.availOrderCount.toInt() > 0 && holding.profitRate.toDouble() > KisSession.config.SELL_PROFIT) {
|
||||
println("${holding.name} - 매수 : ${holding.avgPrice} - 현재 : ${holding.currentPrice} ")
|
||||
@@ -414,10 +409,13 @@ object AutoTradingManager {
|
||||
"SELL",
|
||||
"🎊 보유 주식 매도 주문 실패[${it.message}] "
|
||||
)
|
||||
// println("❌ [재주문 실패] ${holding.name}: ${it.message}")
|
||||
}
|
||||
} else {
|
||||
|
||||
TradingLogStore.addAnalyzer(
|
||||
"보유주식[${holding.name}]",
|
||||
holding.code,
|
||||
"수익률 미달 : ${holding.currentPrice}[${holding.quantity}주] 보유, 수익률(${holding.profitRate.toDouble()})"
|
||||
)
|
||||
}
|
||||
delay(200) // API 호출 부하 방지
|
||||
}
|
||||
@@ -560,9 +558,14 @@ object AutoTradingManager {
|
||||
return batch
|
||||
}
|
||||
|
||||
suspend fun executeMarketLoop() {
|
||||
suspend fun checkBalance() : UnifiedBalance? {
|
||||
val balance = KisTradeService.fetchIntegratedBalance().getOrNull()
|
||||
if (AUTOSELL) balance?.let { resumePendingSellOrders(KisTradeService, it) }
|
||||
return balance
|
||||
}
|
||||
|
||||
suspend fun executeMarketLoop() {
|
||||
val balance = checkBalance()
|
||||
if (AUTOSELL) balance?.let { resumePendingSellOrders(KisTradeService, it) }
|
||||
|
||||
val myCash = balance?.deposit?.replace(",", "")?.toLongOrNull() ?: 0L
|
||||
@@ -622,9 +625,31 @@ object AutoTradingManager {
|
||||
println("남은 후보군 개수 : ${totalCount}")
|
||||
delay(100)
|
||||
}
|
||||
sellSchedule()
|
||||
}
|
||||
println("⏱️ [Cycle End] ${LocalTime.now()}")
|
||||
}
|
||||
private var lastForceCheckMinute = -1 // 마지막으로 강제 체크를 수행한 '분'을 저장
|
||||
suspend fun sellSchedule() {
|
||||
|
||||
val now = LocalTime.now()
|
||||
val currentMinute = now.minute
|
||||
println("매도 스케줄 체크")
|
||||
if (now.hour == 9 && (currentMinute == 1 || currentMinute == 15 || currentMinute == 40)) {
|
||||
if (lastForceCheckMinute != currentMinute) {
|
||||
TradingLogStore.addAnalyzer(" - ", " - ", "⏰ [강제 스케줄 실행] 오전 9시 ${currentMinute}분 - 보유주식 매도 체크를 시작합니다.", true)
|
||||
println("⏰ [강제 스케줄 실행] 오전 9시 ${currentMinute}분 - 보유주식 매도 체크를 시작합니다.")
|
||||
checkBalance()
|
||||
lastForceCheckMinute = currentMinute // 실행 완료 기록
|
||||
}
|
||||
}
|
||||
// else if(now.hour % 2 == 1 && (currentMinute == 43)) {
|
||||
// TradingLogStore.addAnalyzer(" - ", " - ", "⏰ [강제 스케줄 실행] 오전 9시 ${currentMinute}분 - 보유주식 매도 체크를 시작합니다.", true)
|
||||
// println("⏰ [강제 스케줄 실행] 오전 ${now.hour}시 ${currentMinute}분 - 보유주식 매도 체크를 시작합니다.")
|
||||
// checkBalance()
|
||||
// lastForceCheckMinute = currentMinute // 실행 완료 기록
|
||||
// }
|
||||
}
|
||||
|
||||
suspend fun finalizeMarketClose(now: LocalTime) {
|
||||
when {
|
||||
@@ -1035,7 +1060,7 @@ class TechnicalAnalyzer {
|
||||
val disparityDaily = (currentPrice / ma20Daily) * 100
|
||||
|
||||
if (disparityDaily > 115.0) { // 20일선보다 15% 이상 떠 있으면 감점 시작
|
||||
val penalty = ((disparityDaily - 115.0) * 1).toInt() // 초과분 1%당 2점 감점
|
||||
val penalty = ((disparityDaily - 115.0) * 0.5).toInt() // 초과분 1%당 2점 감점
|
||||
short -= penalty
|
||||
ultra -= (penalty / 2) // 초단기에도 영향
|
||||
println("⚠️ [과열 감점] 일봉 이격도(${String.format("%.1f", disparityDaily)}%): -${penalty}점")
|
||||
@@ -1046,9 +1071,9 @@ class TechnicalAnalyzer {
|
||||
if (weekly.size >= 3) {
|
||||
val weeklyChange = calculateChange(weekly.takeLast(3))
|
||||
if (weeklyChange > 30.0) { // 3주간 30% 이상 급등 시
|
||||
mid -= 15
|
||||
short -= 7
|
||||
println("⚠️ [과열 감점] 주봉 급등(${String.format("%.1f", weeklyChange)}%): -15점")
|
||||
mid -= 10
|
||||
short -= 5
|
||||
println("⚠️ [과열 감점] 주봉 급등(${String.format("%.1f", weeklyChange)}%): -10점")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1436,3 +1461,67 @@ fun CandleData.toScalpingCandle(): Candle {
|
||||
fun List<CandleData>.toScalpingList(): List<Candle> {
|
||||
return this.map { it.toScalpingCandle() }
|
||||
}
|
||||
|
||||
|
||||
enum class InvestmentGrade(
|
||||
val displayName: String,
|
||||
val description: String,
|
||||
val shortWeight: Double = 0.0,
|
||||
val midWeight: Double = 0.0,
|
||||
val longWeight: Double = 0.0,
|
||||
val profitGuide: ConfigIndex,
|
||||
val buyGuide: ConfigIndex,
|
||||
val allocationRate: ConfigIndex,
|
||||
) {
|
||||
LEVEL_5_STRONG_RECOMMEND(
|
||||
displayName = "최상급 추천",
|
||||
description = "단기·중기·장기 모두 우수하고, 신뢰도 매우 높은 범용 매수 추천",
|
||||
shortWeight = 1.0,
|
||||
midWeight = 1.0,
|
||||
longWeight = 1.0,
|
||||
profitGuide = ConfigIndex.GRADE_5_PROFIT,
|
||||
buyGuide = ConfigIndex.GRADE_5_BUY,
|
||||
allocationRate = ConfigIndex.GRADE_5_ALLOCATIONRATE,
|
||||
),
|
||||
LEVEL_4_BALANCED_RECOMMEND(
|
||||
displayName = "균형 추천",
|
||||
description = "중기·장기 기본은 양호하고, 단기 성과도 준수한 안정형 추천",
|
||||
shortWeight = 0.8,
|
||||
midWeight = 1.0,
|
||||
longWeight = 1.0,
|
||||
profitGuide = ConfigIndex.GRADE_4_PROFIT,
|
||||
buyGuide = ConfigIndex.GRADE_4_BUY,
|
||||
allocationRate = ConfigIndex.GRADE_4_ALLOCATIONRATE,
|
||||
),
|
||||
LEVEL_3_CAUTIOUS_RECOMMEND(
|
||||
displayName = "보수적 추천",
|
||||
description = "중기/장기 기본은 양호하지만, 단기 변동성이 높아 신중히 접근해야 함",
|
||||
shortWeight = 0.6,
|
||||
midWeight = 1.0,
|
||||
longWeight = 1.0,
|
||||
profitGuide = ConfigIndex.GRADE_3_PROFIT,
|
||||
buyGuide = ConfigIndex.GRADE_3_BUY,
|
||||
allocationRate = ConfigIndex.GRADE_3_ALLOCATIONRATE,
|
||||
),
|
||||
LEVEL_2_HIGH_RISK(
|
||||
displayName = "고위험 추천",
|
||||
description = "단기/초단기 성과만 강하고, 중기·장기가 애매하여 리스크가 큰 투자",
|
||||
shortWeight = 1.0,
|
||||
midWeight = 0.4,
|
||||
longWeight = 0.4,
|
||||
profitGuide = ConfigIndex.GRADE_2_PROFIT,
|
||||
buyGuide = ConfigIndex.GRADE_2_BUY,
|
||||
allocationRate = ConfigIndex.GRADE_2_ALLOCATIONRATE,
|
||||
),
|
||||
LEVEL_1_SPECULATIVE(
|
||||
displayName = "순수 공격적 선택",
|
||||
description = "단기/초단기 성과에만 의존하는 단기 급등형 공격적 투자",
|
||||
shortWeight = 1.0,
|
||||
midWeight = 0.2,
|
||||
longWeight = 0.2,
|
||||
profitGuide = ConfigIndex.GRADE_1_PROFIT,
|
||||
buyGuide = ConfigIndex.GRADE_1_BUY,
|
||||
allocationRate = ConfigIndex.GRADE_1_ALLOCATIONRATE,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user