....
This commit is contained in:
@@ -55,6 +55,7 @@ import kotlin.math.max
|
|||||||
|
|
||||||
// service/AutoTradingManager.kt
|
// service/AutoTradingManager.kt
|
||||||
typealias TradingDecisionCallback = (TradingDecision?, Boolean) -> Unit
|
typealias TradingDecisionCallback = (TradingDecision?, Boolean) -> Unit
|
||||||
|
|
||||||
object AutoTradingManager {
|
object AutoTradingManager {
|
||||||
|
|
||||||
|
|
||||||
@@ -71,6 +72,7 @@ object AutoTradingManager {
|
|||||||
var ONE_STOCK_ALYSIS_TIME = KisSession.tradeConfig.ONE_STOCK_ALYSIS_TIME
|
var ONE_STOCK_ALYSIS_TIME = KisSession.tradeConfig.ONE_STOCK_ALYSIS_TIME
|
||||||
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 reanalysisList = mutableListOf<RankingStock>()
|
||||||
private val retryCountMap = mutableMapOf<String, Int>()
|
private val retryCountMap = mutableMapOf<String, Int>()
|
||||||
@@ -92,7 +94,13 @@ object AutoTradingManager {
|
|||||||
if (isTradingDay && now.isAfter(KisSession.startTime()) && now.isBefore(KisSession.endTime()) && !shouldShowFullWindow) {
|
if (isTradingDay && now.isAfter(KisSession.startTime()) && now.isBefore(KisSession.endTime()) && !shouldShowFullWindow) {
|
||||||
shouldShowFullWindow = true
|
shouldShowFullWindow = true
|
||||||
// SystemSleepPreventer.wakeDisplay()
|
// SystemSleepPreventer.wakeDisplay()
|
||||||
} else if ((now.isAfter(LocalTime.of(23, 50)) && now.isBefore(LocalTime.of(8, 0)))) {
|
} else if ((now.isAfter(LocalTime.of(23, 50)) && now.isBefore(
|
||||||
|
LocalTime.of(
|
||||||
|
8,
|
||||||
|
0
|
||||||
|
)
|
||||||
|
))
|
||||||
|
) {
|
||||||
// SystemSleepPreventer.sleepDisplay()
|
// SystemSleepPreventer.sleepDisplay()
|
||||||
}
|
}
|
||||||
// if (!isTradingDay) {
|
// if (!isTradingDay) {
|
||||||
@@ -119,31 +127,54 @@ object AutoTradingManager {
|
|||||||
decision.analyzer?.let { a ->
|
decision.analyzer?.let { a ->
|
||||||
val volatility = a?.calculateVolatilityForecast(a.daily, 20)
|
val volatility = a?.calculateVolatilityForecast(a.daily, 20)
|
||||||
volatility?.let {
|
volatility?.let {
|
||||||
maxRealisticProfitRate = ((volatility.realisticHigh - decision.currentPrice) / decision.currentPrice) * 100.0
|
maxRealisticProfitRate =
|
||||||
|
((volatility.realisticHigh - decision.currentPrice) / decision.currentPrice) * 100.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 1. 통계적으로 도달 가능한 현실적인 최대 수익률 계산 (1표준편차 상단 기준)
|
// 1. 통계적으로 도달 가능한 현실적인 최대 수익률 계산 (1표준편차 상단 기준)
|
||||||
|
|
||||||
|
|
||||||
// 2. 시스템 기본 설정 수익률과 비교
|
// 2. 시스템 기본 설정 수익률과 비교
|
||||||
val baseProfitRate = KisSession.config.getValues(ConfigIndex.PROFIT_INDEX) + KisSession.config.getValues(grade.profitGuide)
|
val baseProfitRate =
|
||||||
|
KisSession.config.getValues(ConfigIndex.PROFIT_INDEX) + KisSession.config.getValues(
|
||||||
|
grade.profitGuide
|
||||||
|
)
|
||||||
|
|
||||||
// 3. 스마트 익절률 결정: 시스템 설정값이 통계적 한계를 넘어서면, 통계적 한계치로 눈높이를 낮춤
|
// 3. 스마트 익절률 결정: 시스템 설정값이 통계적 한계를 넘어서면, 통계적 한계치로 눈높이를 낮춤
|
||||||
val finalProfitRate = if (maxRealisticProfitRate > 0.0 && baseProfitRate > maxRealisticProfitRate) {
|
val finalProfitRate =
|
||||||
|
if (maxRealisticProfitRate > 0.0 && baseProfitRate > maxRealisticProfitRate) {
|
||||||
max(maxRealisticProfitRate, 0.05)
|
max(maxRealisticProfitRate, 0.05)
|
||||||
} else {
|
} else {
|
||||||
baseProfitRate // 변동성이 충분히 크다면 원래 시스템 설정대로 진행
|
baseProfitRate // 변동성이 충분히 크다면 원래 시스템 설정대로 진행
|
||||||
}
|
}
|
||||||
// 2. 최종 매수 실행
|
// 2. 최종 매수 실행
|
||||||
val gradeRate = KisSession.config.getValues(grade.allocationRate)
|
val gradeRate = KisSession.config.getValues(grade.allocationRate)
|
||||||
val maxBudget = KisSession.config.getValues(ConfigIndex.MAX_BUDGET_INDEX) * gradeRate
|
val maxBudget =
|
||||||
|
KisSession.config.getValues(ConfigIndex.MAX_BUDGET_INDEX) * gradeRate
|
||||||
decision.maxRealisticProfitRate = maxRealisticProfitRate
|
decision.maxRealisticProfitRate = maxRealisticProfitRate
|
||||||
TradingLogStore.addLog(decision,"BUY",decision.summary(KisSession.config.getValues(ConfigIndex.PROFIT_INDEX) * KisSession.config.getValues(grade.profitGuide)))
|
TradingLogStore.addLog(
|
||||||
var hasCodes = KisSession.tradeConfig.lowerAveragePrice && currentBalance?.getHoldings()?.any { it.code.equals(decision.stockCode) && it.quantity.toInt() > 2 && it.availOrderCount.toInt() > 0} ?: false
|
decision,
|
||||||
|
"BUY",
|
||||||
|
decision.summary(
|
||||||
|
KisSession.config.getValues(ConfigIndex.PROFIT_INDEX) * KisSession.config.getValues(
|
||||||
|
grade.profitGuide
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
var hasCodes =
|
||||||
|
KisSession.tradeConfig.lowerAveragePrice && currentBalance?.getHoldings()
|
||||||
|
?.any { it.code.equals(decision.stockCode) && it.quantity.toInt() > 2 && it.availOrderCount.toInt() > 0 } ?: false
|
||||||
|
|
||||||
|
val calculatedQty =
|
||||||
|
if (hasCodes == true) KisSession.tradeConfig.lowerAverageStockCount else (maxBudget / decision.currentPrice).toInt()
|
||||||
|
.coerceAtLeast(1)
|
||||||
if (hasCodes == true) {
|
if (hasCodes == true) {
|
||||||
TradingLogStore.addNotice(decision.stockName,decision.stockCode,"물타기 시도 1주 매수")
|
TradingLogStore.addNotice(
|
||||||
|
decision.stockName,
|
||||||
|
decision.stockCode,
|
||||||
|
"물타기 시도 ${calculatedQty}주 매수"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
val calculatedQty = if(hasCodes == true) KisSession.tradeConfig.lowerAverageStockCount else (maxBudget / decision.currentPrice).toInt().coerceAtLeast(1)
|
|
||||||
excuteTrade(
|
excuteTrade(
|
||||||
decision = decision,
|
decision = decision,
|
||||||
orderQty = calculatedQty.toString(),
|
orderQty = calculatedQty.toString(),
|
||||||
@@ -183,11 +214,13 @@ object AutoTradingManager {
|
|||||||
else if (shortAvg >= 65.0) InvestmentGrade.LEVEL_4_BALANCED_RECOMMEND
|
else if (shortAvg >= 65.0) InvestmentGrade.LEVEL_4_BALANCED_RECOMMEND
|
||||||
else InvestmentGrade.LEVEL_3_CAUTIOUS_RECOMMEND
|
else InvestmentGrade.LEVEL_3_CAUTIOUS_RECOMMEND
|
||||||
}
|
}
|
||||||
|
|
||||||
midLongAvg >= 60.0 -> {
|
midLongAvg >= 60.0 -> {
|
||||||
if (shortAvg >= 70.0) InvestmentGrade.LEVEL_4_BALANCED_RECOMMEND
|
if (shortAvg >= 70.0) InvestmentGrade.LEVEL_4_BALANCED_RECOMMEND
|
||||||
else if (shortAvg >= 60.0) InvestmentGrade.LEVEL_3_CAUTIOUS_RECOMMEND
|
else if (shortAvg >= 60.0) InvestmentGrade.LEVEL_3_CAUTIOUS_RECOMMEND
|
||||||
else InvestmentGrade.LEVEL_2_HIGH_RISK
|
else InvestmentGrade.LEVEL_2_HIGH_RISK
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
if (shortAvg >= 70.0) InvestmentGrade.LEVEL_2_HIGH_RISK
|
if (shortAvg >= 70.0) InvestmentGrade.LEVEL_2_HIGH_RISK
|
||||||
else InvestmentGrade.LEVEL_1_SPECULATIVE
|
else InvestmentGrade.LEVEL_1_SPECULATIVE
|
||||||
@@ -202,6 +235,7 @@ object AutoTradingManager {
|
|||||||
rawGrade = when (rawGrade) {
|
rawGrade = when (rawGrade) {
|
||||||
InvestmentGrade.LEVEL_1_SPECULATIVE,
|
InvestmentGrade.LEVEL_1_SPECULATIVE,
|
||||||
InvestmentGrade.LEVEL_2_HIGH_RISK -> InvestmentGrade.LEVEL_3_CAUTIOUS_RECOMMEND
|
InvestmentGrade.LEVEL_2_HIGH_RISK -> InvestmentGrade.LEVEL_3_CAUTIOUS_RECOMMEND
|
||||||
|
|
||||||
InvestmentGrade.LEVEL_3_CAUTIOUS_RECOMMEND -> InvestmentGrade.LEVEL_4_BALANCED_RECOMMEND
|
InvestmentGrade.LEVEL_3_CAUTIOUS_RECOMMEND -> InvestmentGrade.LEVEL_4_BALANCED_RECOMMEND
|
||||||
else -> rawGrade
|
else -> rawGrade
|
||||||
}
|
}
|
||||||
@@ -210,8 +244,10 @@ object AutoTradingManager {
|
|||||||
InvestmentGrade.LEVEL_5_STRONG_RECOMMEND,
|
InvestmentGrade.LEVEL_5_STRONG_RECOMMEND,
|
||||||
InvestmentGrade.LEVEL_4_BALANCED_RECOMMEND,
|
InvestmentGrade.LEVEL_4_BALANCED_RECOMMEND,
|
||||||
InvestmentGrade.LEVEL_3_CAUTIOUS_RECOMMEND -> InvestmentGrade.LEVEL_1_SPECULATIVE
|
InvestmentGrade.LEVEL_3_CAUTIOUS_RECOMMEND -> InvestmentGrade.LEVEL_1_SPECULATIVE
|
||||||
|
|
||||||
InvestmentGrade.LEVEL_2_HIGH_RISK,
|
InvestmentGrade.LEVEL_2_HIGH_RISK,
|
||||||
InvestmentGrade.LEVEL_1_SPECULATIVE -> InvestmentGrade.LEVEL_0_SPECULATIVE
|
InvestmentGrade.LEVEL_1_SPECULATIVE -> InvestmentGrade.LEVEL_0_SPECULATIVE
|
||||||
|
|
||||||
else -> InvestmentGrade.LEVEL_0_SPECULATIVE
|
else -> InvestmentGrade.LEVEL_0_SPECULATIVE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -228,11 +264,19 @@ object AutoTradingManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun excuteTrade(decision: TradingDecision, orderQty: String, profitRate1: Double?, investmentGrade: InvestmentGrade = InvestmentGrade.LEVEL_2_HIGH_RISK, hasCode: Boolean) {
|
fun excuteTrade(
|
||||||
|
decision: TradingDecision,
|
||||||
|
orderQty: String,
|
||||||
|
profitRate1: Double?,
|
||||||
|
investmentGrade: InvestmentGrade = InvestmentGrade.LEVEL_2_HIGH_RISK,
|
||||||
|
hasCode: Boolean
|
||||||
|
) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
var basePrice = decision.currentPrice
|
var basePrice = decision.currentPrice
|
||||||
val tickSize = MarketUtil.getTickSize(basePrice)
|
val tickSize = MarketUtil.getTickSize(basePrice)
|
||||||
val oneTickLowerPrice = basePrice - (tickSize * KisSession.config.getValues(investmentGrade.buyGuide).toInt())
|
val oneTickLowerPrice =
|
||||||
|
basePrice - (tickSize * KisSession.config.getValues(investmentGrade.buyGuide)
|
||||||
|
.toInt())
|
||||||
var stockCode = decision.stockCode
|
var stockCode = decision.stockCode
|
||||||
var stockName = decision.stockName
|
var stockName = decision.stockName
|
||||||
val finalPrice = MarketUtil.roundToTickSize(oneTickLowerPrice.toDouble())
|
val finalPrice = MarketUtil.roundToTickSize(oneTickLowerPrice.toDouble())
|
||||||
@@ -241,11 +285,26 @@ object AutoTradingManager {
|
|||||||
if (!canAddNewPosition(maxStocks)) {
|
if (!canAddNewPosition(maxStocks)) {
|
||||||
TradingLogStore.addNotice("SYSTEM", "LIMIT", "최대 보유 종목 도달로 신규 매수 일시 중단")
|
TradingLogStore.addNotice("SYSTEM", "LIMIT", "최대 보유 종목 도달로 신규 매수 일시 중단")
|
||||||
addToReanalysis(RankingStock(mksc_shrn_iscd = stockCode, hts_kor_isnm = stockName))
|
addToReanalysis(RankingStock(mksc_shrn_iscd = stockCode, hts_kor_isnm = stockName))
|
||||||
TradingLogStore.addWatchLog(decision,"WATCH","매수 실패 : 최대 보유 종목 도달로 신규 매수 일시 중단 => 재분석 대기열에 추가")
|
TradingLogStore.addWatchLog(
|
||||||
|
decision,
|
||||||
|
"WATCH",
|
||||||
|
"매수 실패 : 최대 보유 종목 도달로 신규 매수 일시 중단 => 재분석 대기열에 추가"
|
||||||
|
)
|
||||||
} else if (KisSession.isAvailBuyTime(LocalTime.now()) || hasCode) {
|
} else if (KisSession.isAvailBuyTime(LocalTime.now()) || hasCode) {
|
||||||
println("basePrice : $basePrice, oneTickLowerPrice : $oneTickLowerPrice, finalPrice : $finalPrice hasStocks : ${stockCode.contains(stockCode)}" )
|
println(
|
||||||
|
"basePrice : $basePrice, oneTickLowerPrice : $oneTickLowerPrice, finalPrice : $finalPrice hasStocks : ${
|
||||||
|
stockCode.contains(
|
||||||
|
stockCode
|
||||||
|
)
|
||||||
|
}"
|
||||||
|
)
|
||||||
var realOrderQty = orderQty
|
var realOrderQty = orderQty
|
||||||
KisTradeService.postOrder(stockCode, realOrderQty, finalPrice.toLong().toString(), isBuy = true)
|
KisTradeService.postOrder(
|
||||||
|
stockCode,
|
||||||
|
realOrderQty,
|
||||||
|
finalPrice.toLong().toString(),
|
||||||
|
isBuy = true
|
||||||
|
)
|
||||||
.onSuccess { realOrderNo ->
|
.onSuccess { realOrderNo ->
|
||||||
println("[${investmentGrade.displayName}] 주문 성공: $realOrderNo $stockCode $orderQty $finalPrice")
|
println("[${investmentGrade.displayName}] 주문 성공: $realOrderNo $stockCode $orderQty $finalPrice")
|
||||||
TradingLogStore.addLog(
|
TradingLogStore.addLog(
|
||||||
@@ -257,21 +316,27 @@ object AutoTradingManager {
|
|||||||
val sRate = -1.5
|
val sRate = -1.5
|
||||||
var tax = KisSession.config.getValues(ConfigIndex.TAX_INDEX)
|
var tax = KisSession.config.getValues(ConfigIndex.TAX_INDEX)
|
||||||
val effectiveProfitRate =
|
val effectiveProfitRate =
|
||||||
(profitRate1 ?: KisSession.config.getValues(ConfigIndex.PROFIT_INDEX)) + tax
|
(profitRate1
|
||||||
|
?: KisSession.config.getValues(ConfigIndex.PROFIT_INDEX)) + tax
|
||||||
try {
|
try {
|
||||||
var oldTarget = currentBalance?.getHoldings()?.first { it.availOrderCount.toInt() > 0 && it.code.equals(decision.stockCode) }
|
var oldTarget = currentBalance?.getHoldings()
|
||||||
|
?.first { it.availOrderCount.toInt() > 0 && it.code.equals(decision.stockCode) }
|
||||||
if (KisSession.tradeConfig.lowerAveragePrice && hasCode && oldTarget != null) {
|
if (KisSession.tradeConfig.lowerAveragePrice && hasCode && oldTarget != null) {
|
||||||
var avgPrive = oldTarget.avgPrice.toDouble()
|
var avgPrive = oldTarget.avgPrice.toDouble()
|
||||||
var qty = oldTarget.quantity.toDouble()
|
var qty = oldTarget.quantity.toDouble()
|
||||||
basePrice = avgPrive * 1.5//((avgPrive * qty) + (decision.currentPrice * orderQty.toInt())).div(qty!!.toInt() + (orderQty.toInt()))
|
basePrice =
|
||||||
|
avgPrive * 1.5//((avgPrive * qty) + (decision.currentPrice * orderQty.toInt())).div(qty!!.toInt() + (orderQty.toInt()))
|
||||||
println("물타기 ${avgPrive}, ${qty} ${basePrice}")
|
println("물타기 ${avgPrive}, ${qty} ${basePrice}")
|
||||||
}
|
}
|
||||||
} catch (e:Exception) {e.printStackTrace()}
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
val calculatedTarget =
|
val calculatedTarget =
|
||||||
MarketUtil.roundToTickSize(basePrice * (1 + effectiveProfitRate / 100.0))
|
MarketUtil.roundToTickSize(basePrice * (1 + effectiveProfitRate / 100.0))
|
||||||
val calculatedStop = MarketUtil.roundToTickSize(basePrice * (1 + sRate / 100.0))
|
val calculatedStop =
|
||||||
|
MarketUtil.roundToTickSize(basePrice * (1 + sRate / 100.0))
|
||||||
val inputQty = orderQty.replace(",", "").toIntOrNull() ?: 0
|
val inputQty = orderQty.replace(",", "").toIntOrNull() ?: 0
|
||||||
if (!hasCode) {
|
if (!hasCode) {
|
||||||
DatabaseFactory.saveAutoTrade(
|
DatabaseFactory.saveAutoTrade(
|
||||||
@@ -330,11 +395,27 @@ object AutoTradingManager {
|
|||||||
TradingLogStore.addLog(decision, "BUY", it.message ?: "매수 실패")
|
TradingLogStore.addLog(decision, "BUY", it.message ?: "매수 실패")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if (!hasCode && KisSession.isAvailBuyTime(LocalTime.now())) {
|
||||||
|
AutoTradingManager.addToReanalysis(
|
||||||
|
RankingStock(
|
||||||
|
mksc_shrn_iscd = stockCode,
|
||||||
|
hts_kor_isnm = stockName
|
||||||
|
)
|
||||||
|
)
|
||||||
|
TradingLogStore.addWatchLog(
|
||||||
|
decision,
|
||||||
|
"WATCH",
|
||||||
|
"매수 시간 외 분석 => 재분석 대기열에 추가"
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
val unfilledResult = KisTradeService.fetchUnfilledOrders()
|
val unfilledResult = KisTradeService.fetchUnfilledOrders()
|
||||||
unfilledResult.onSuccess { response ->
|
unfilledResult.onSuccess { response ->
|
||||||
response.filter { it.sll_buy_dvsn_cd == "02" }.forEach { order ->
|
response.filter { it.sll_buy_dvsn_cd == "02" }.forEach { order ->
|
||||||
TradingLogStore.addNotice(order.prdt_name,order.pdno,"[주문 취소] 매수시간 종료 후 모든 매수 취소")
|
TradingLogStore.addNotice(
|
||||||
|
order.prdt_name,
|
||||||
|
order.pdno,
|
||||||
|
"[주문 취소] 매수시간 종료 후 모든 매수 취소"
|
||||||
|
)
|
||||||
KisTradeService.cancelOrder(
|
KisTradeService.cancelOrder(
|
||||||
order.ord_no, // 원주문번호
|
order.ord_no, // 원주문번호
|
||||||
order.pdno
|
order.pdno
|
||||||
@@ -342,9 +423,12 @@ object AutoTradingManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var onExecutionReceived : ((String, String, String, String, Boolean) -> Unit)? = {code, qty, price,orderNo, isBuy ->
|
|
||||||
|
var onExecutionReceived: ((String, String, String, String, Boolean) -> Unit)? =
|
||||||
|
{ code, qty, price, orderNo, isBuy ->
|
||||||
scope.launch {
|
scope.launch {
|
||||||
val exec = ExecutionData(orderNo, code, price, qty, isBuy)
|
val exec = ExecutionData(orderNo, code, price, qty, isBuy)
|
||||||
println("exec >> ${exec}")
|
println("exec >> ${exec}")
|
||||||
@@ -371,13 +455,16 @@ object AutoTradingManager {
|
|||||||
// 💡 [수정] 매수 주문(orderNo)에 대해 '진짜 산 가격'을 기록해야 합니다.
|
// 💡 [수정] 매수 주문(orderNo)에 대해 '진짜 산 가격'을 기록해야 합니다.
|
||||||
// 기존에는 여기에 finalTargetPrice를 넣으셨는데, 그러면 매수 단가가 오염됩니다.
|
// 기존에는 여기에 finalTargetPrice를 넣으셨는데, 그러면 매수 단가가 오염됩니다.
|
||||||
TradingReportManager.updateExecution(orderNo, actualBuyPrice, dbItem.quantity)
|
TradingReportManager.updateExecution(orderNo, actualBuyPrice, dbItem.quantity)
|
||||||
var hasCodes = KisSession.tradeConfig.lowerAveragePrice && currentBalance?.getHoldings()?.any { it.code.equals(dbItem.code) && it.quantity.toInt() > 2 && dbItem.quantity == KisSession.tradeConfig.lowerAverageStockCount } ?: false
|
var hasCodes =
|
||||||
|
KisSession.tradeConfig.lowerAveragePrice && currentBalance?.getHoldings()
|
||||||
|
?.any { it.code.equals(dbItem.code) && it.quantity.toInt() > 2 && dbItem.quantity == KisSession.tradeConfig.lowerAverageStockCount } ?: false
|
||||||
if (hasCodes) {
|
if (hasCodes) {
|
||||||
actualBuyPrice = actualBuyPrice * 1.1
|
actualBuyPrice = actualBuyPrice * 1.1
|
||||||
}
|
}
|
||||||
val absoluteMinRate = KisSession.config.getValues(ConfigIndex.TAX_INDEX) + 0.05
|
val absoluteMinRate = KisSession.config.getValues(ConfigIndex.TAX_INDEX) + 0.05
|
||||||
val finalProfitRate = maxOf(dbItem.profitRate, absoluteMinRate)
|
val finalProfitRate = maxOf(dbItem.profitRate, absoluteMinRate)
|
||||||
val finalTargetPrice = MarketUtil.roundToTickSize(actualBuyPrice * (1 + finalProfitRate / 100.0))
|
val finalTargetPrice =
|
||||||
|
MarketUtil.roundToTickSize(actualBuyPrice * (1 + finalProfitRate / 100.0))
|
||||||
|
|
||||||
println("🎯 [매수 확정] ${dbItem.name} | 매수가: ${actualBuyPrice.toInt()} -> 목표가 설정: ${finalTargetPrice.toInt()}")
|
println("🎯 [매수 확정] ${dbItem.name} | 매수가: ${actualBuyPrice.toInt()} -> 목표가 설정: ${finalTargetPrice.toInt()}")
|
||||||
|
|
||||||
@@ -394,11 +481,20 @@ object AutoTradingManager {
|
|||||||
stockName = dbItem.name,
|
stockName = dbItem.name,
|
||||||
isBuy = false,
|
isBuy = false,
|
||||||
orderQty = dbItem.quantity,
|
orderQty = dbItem.quantity,
|
||||||
reason = "🎯 목표 수익률 ${String.format("%.2f", finalProfitRate)}% 도달을 위한 익절 주문",
|
reason = "🎯 목표 수익률 ${
|
||||||
|
String.format(
|
||||||
|
"%.2f",
|
||||||
|
finalProfitRate
|
||||||
|
)
|
||||||
|
}% 도달을 위한 익절 주문",
|
||||||
holdingAvgPrice = actualBuyPrice, // 👈 여기서 매수단가를 넘겨줘야 매도 리포트가 정확해집니다!
|
holdingAvgPrice = actualBuyPrice, // 👈 여기서 매수단가를 넘겨줘야 매도 리포트가 정확해집니다!
|
||||||
decision = null
|
decision = null
|
||||||
)
|
)
|
||||||
DatabaseFactory.updateStatusAndOrderNo(dbItem.id!!, TradeStatus.SELLING, newSellOrderNo)
|
DatabaseFactory.updateStatusAndOrderNo(
|
||||||
|
dbItem.id!!,
|
||||||
|
TradeStatus.SELLING,
|
||||||
|
newSellOrderNo
|
||||||
|
)
|
||||||
executionCache.remove(orderNo)
|
executionCache.remove(orderNo)
|
||||||
}
|
}
|
||||||
} else if (dbItem.status == TradeStatus.SELLING) {
|
} else if (dbItem.status == TradeStatus.SELLING) {
|
||||||
@@ -409,7 +505,12 @@ object AutoTradingManager {
|
|||||||
TradingReportManager.updateExecution(orderNo, actualSellPrice, actualSellQty)
|
TradingReportManager.updateExecution(orderNo, actualSellPrice, actualSellQty)
|
||||||
|
|
||||||
println("🎊 [매칭 성공] 매도 완료: ${dbItem.name} | 매도가: ${actualSellPrice.toInt()}")
|
println("🎊 [매칭 성공] 매도 완료: ${dbItem.name} | 매도가: ${actualSellPrice.toInt()}")
|
||||||
TradingLogStore.addSellLog(dbItem.name,actualSellPrice.toString(),"SELL","매도 완료")
|
TradingLogStore.addSellLog(
|
||||||
|
dbItem.name,
|
||||||
|
actualSellPrice.toString(),
|
||||||
|
"SELL",
|
||||||
|
"매도 완료"
|
||||||
|
)
|
||||||
|
|
||||||
TradingReportManager.closePositionCycle(dbItem.code) // 사이클 종료 알림
|
TradingReportManager.closePositionCycle(dbItem.code) // 사이클 종료 알림
|
||||||
|
|
||||||
@@ -433,7 +534,7 @@ object AutoTradingManager {
|
|||||||
watchdogJob = scope.launch {
|
watchdogJob = scope.launch {
|
||||||
val activeTrades = DatabaseFactory.findAllMonitoringTrades()
|
val activeTrades = DatabaseFactory.findAllMonitoringTrades()
|
||||||
var now = LocalTime.now(ZoneId.of("Asia/Seoul"))
|
var now = LocalTime.now(ZoneId.of("Asia/Seoul"))
|
||||||
if (doStart && activeTrades.isNotEmpty() && !KisSession.isAvailBuyTime(now)) {
|
if (doStart && activeTrades.isNotEmpty() && !KisSession.isMarketOpenTime(now)) {
|
||||||
executeClosingLiquidation(activeTrades)
|
executeClosingLiquidation(activeTrades)
|
||||||
}
|
}
|
||||||
while (isActive) {
|
while (isActive) {
|
||||||
@@ -450,7 +551,11 @@ object AutoTradingManager {
|
|||||||
runDiscoveryLoop(globalCallback)
|
runDiscoveryLoop(globalCallback)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun sellingAfterMarketOnePrice(tradeService: KisTradeService,balance : UnifiedBalance,marketCode : String = "Y") {
|
suspend fun sellingAfterMarketOnePrice(
|
||||||
|
tradeService: KisTradeService,
|
||||||
|
balance: UnifiedBalance,
|
||||||
|
marketCode: String = "Y"
|
||||||
|
) {
|
||||||
balance.getHoldings().forEach { holding ->
|
balance.getHoldings().forEach { holding ->
|
||||||
if (BLACKLISTEDSTOCKCODES.contains(holding.code)) {
|
if (BLACKLISTEDSTOCKCODES.contains(holding.code)) {
|
||||||
println("❌ 차단 처리된 주식 : ${holding.name}")
|
println("❌ 차단 처리된 주식 : ${holding.name}")
|
||||||
@@ -462,9 +567,12 @@ object AutoTradingManager {
|
|||||||
} else {
|
} else {
|
||||||
val now = LocalTime.now()
|
val now = LocalTime.now()
|
||||||
|
|
||||||
val targetProfitLimit = if (holding.isTodayEntry && now.isBefore(LocalTime.of(16, 0))) {
|
val targetProfitLimit =
|
||||||
|
if (holding.isTodayEntry && now.isBefore(LocalTime.of(16, 0))) {
|
||||||
// 당일 매수 종목: 짧은 익절 (예: 1.0% 이상이면 즉시 매도)
|
// 당일 매수 종목: 짧은 익절 (예: 1.0% 이상이면 즉시 매도)
|
||||||
KisSession.config.getValues(ConfigIndex.PROFIT_INDEX)+ KisSession.config.getValues(ConfigIndex.TAX_INDEX)
|
KisSession.config.getValues(ConfigIndex.PROFIT_INDEX) + KisSession.config.getValues(
|
||||||
|
ConfigIndex.TAX_INDEX
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
// 오래 보유한 종목: 기존 설정값 준수 (예: 3.0% 등)
|
// 오래 보유한 종목: 기존 설정값 준수 (예: 3.0% 등)
|
||||||
KisSession.config.SELL_PROFIT
|
KisSession.config.SELL_PROFIT
|
||||||
@@ -492,7 +600,8 @@ object AutoTradingManager {
|
|||||||
"SELL",
|
"SELL",
|
||||||
"🎊 ${if (marketCode.equals("Y")) "시간외 단일가" else "대체거래소"} 주식 재고털이 주문 완료"
|
"🎊 ${if (marketCode.equals("Y")) "시간외 단일가" else "대체거래소"} 주식 재고털이 주문 완료"
|
||||||
)
|
)
|
||||||
DatabaseFactory.saveAutoTrade(AutoTradeItem(
|
DatabaseFactory.saveAutoTrade(
|
||||||
|
AutoTradeItem(
|
||||||
orderNo = newOrderNo,
|
orderNo = newOrderNo,
|
||||||
code = holding.code,
|
code = holding.code,
|
||||||
name = holding.name,
|
name = holding.name,
|
||||||
@@ -503,7 +612,8 @@ object AutoTradingManager {
|
|||||||
stopLossPrice = 0.0,
|
stopLossPrice = 0.0,
|
||||||
status = "SELLING",
|
status = "SELLING",
|
||||||
isDomestic = true
|
isDomestic = true
|
||||||
))
|
)
|
||||||
|
)
|
||||||
syncAndExecute(newOrderNo)
|
syncAndExecute(newOrderNo)
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
TradingLogStore.addSellLog(
|
TradingLogStore.addSellLog(
|
||||||
@@ -518,10 +628,27 @@ object AutoTradingManager {
|
|||||||
if (KisSession.config.getValues(ConfigIndex.STOP_LOSS) > 0.0
|
if (KisSession.config.getValues(ConfigIndex.STOP_LOSS) > 0.0
|
||||||
&& holding != null && holding.quantity.toInt() > 0
|
&& holding != null && holding.quantity.toInt() > 0
|
||||||
&& holding.availOrderCount.toInt() > 0
|
&& holding.availOrderCount.toInt() > 0
|
||||||
&& holding.profitRate.toDouble() <= KisSession.config.getValues(ConfigIndex.LOSS_MINRATE)
|
&& holding.profitRate.toDouble() <= KisSession.config.getValues(
|
||||||
&& holding.profitRate.toDouble() >= KisSession.config.getValues(ConfigIndex.LOSS_MAXRATE)
|
ConfigIndex.LOSS_MINRATE
|
||||||
&& holding.valuationProfitAmount.toDouble() >= KisSession.config.getValues(ConfigIndex.LOSS_MAX_MONEY)) {
|
)
|
||||||
println("${holding.name} ${holding.profitRate.toDouble()} ${holding.valuationProfitAmount.toDouble()} ${KisSession.config.getValues(ConfigIndex.LOSS_MAX_MONEY)} , ${KisSession.config.getValues(ConfigIndex.LOSS_MINRATE)} , ${KisSession.config.getValues(ConfigIndex.STOP_LOSS)}")
|
&& holding.profitRate.toDouble() >= KisSession.config.getValues(
|
||||||
|
ConfigIndex.LOSS_MAXRATE
|
||||||
|
)
|
||||||
|
&& holding.valuationProfitAmount.toDouble() >= KisSession.config.getValues(
|
||||||
|
ConfigIndex.LOSS_MAX_MONEY
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
println(
|
||||||
|
"${holding.name} ${holding.profitRate.toDouble()} ${holding.valuationProfitAmount.toDouble()} ${
|
||||||
|
KisSession.config.getValues(
|
||||||
|
ConfigIndex.LOSS_MAX_MONEY
|
||||||
|
)
|
||||||
|
} , ${KisSession.config.getValues(ConfigIndex.LOSS_MINRATE)} , ${
|
||||||
|
KisSession.config.getValues(
|
||||||
|
ConfigIndex.STOP_LOSS
|
||||||
|
)
|
||||||
|
}"
|
||||||
|
)
|
||||||
val profit = holding.profitRate.toDouble()
|
val profit = holding.profitRate.toDouble()
|
||||||
TradingLogStore.addNotice(
|
TradingLogStore.addNotice(
|
||||||
"보유주식[${holding.name}]",
|
"보유주식[${holding.name}]",
|
||||||
@@ -563,7 +690,9 @@ object AutoTradingManager {
|
|||||||
targetPrice = targetPrice
|
targetPrice = targetPrice
|
||||||
isBefore930 = true
|
isBefore930 = true
|
||||||
} else {
|
} else {
|
||||||
targetPrice = MarketUtil.roundToTickSize(targetPrice + MarketUtil.getTickSize(targetPrice))
|
targetPrice = MarketUtil.roundToTickSize(
|
||||||
|
targetPrice + MarketUtil.getTickSize(targetPrice)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
println("🔄 [보유 주식 주문] ${holding.name} (${holding.code}) 매도 목표 ${targetPrice} 미체결 매도 건 재주문 시도")
|
println("🔄 [보유 주식 주문] ${holding.name} (${holding.code}) 매도 목표 ${targetPrice} 미체결 매도 건 재주문 시도")
|
||||||
tradeService.postOrder(
|
tradeService.postOrder(
|
||||||
@@ -579,7 +708,8 @@ object AutoTradingManager {
|
|||||||
"SELL",
|
"SELL",
|
||||||
"🎊 보유 주식[예상수익 : ${holding.profitRate}] ${if (isBefore930) "09:30 이전 현시세{${holding.currentPrice}}로 매도[$targetPrice] 주문" else "09:30 이후 시세{${holding.currentPrice}} 기준 호가 위 매도[$targetPrice] 주문"} 완료"
|
"🎊 보유 주식[예상수익 : ${holding.profitRate}] ${if (isBefore930) "09:30 이전 현시세{${holding.currentPrice}}로 매도[$targetPrice] 주문" else "09:30 이후 시세{${holding.currentPrice}} 기준 호가 위 매도[$targetPrice] 주문"} 완료"
|
||||||
)
|
)
|
||||||
DatabaseFactory.saveAutoTrade(AutoTradeItem(
|
DatabaseFactory.saveAutoTrade(
|
||||||
|
AutoTradeItem(
|
||||||
orderNo = newOrderNo,
|
orderNo = newOrderNo,
|
||||||
code = holding.code,
|
code = holding.code,
|
||||||
name = holding.name,
|
name = holding.name,
|
||||||
@@ -590,7 +720,8 @@ object AutoTradingManager {
|
|||||||
stopLossPrice = 0.0,
|
stopLossPrice = 0.0,
|
||||||
status = "SELLING",
|
status = "SELLING",
|
||||||
isDomestic = true
|
isDomestic = true
|
||||||
))
|
)
|
||||||
|
)
|
||||||
syncAndExecute(newOrderNo)
|
syncAndExecute(newOrderNo)
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
TradingLogStore.addSellLog(
|
TradingLogStore.addSellLog(
|
||||||
@@ -608,9 +739,12 @@ object AutoTradingManager {
|
|||||||
&& holding.availOrderCount.toInt() > 0
|
&& holding.availOrderCount.toInt() > 0
|
||||||
&& holding.profitRate.toDouble() <= KisSession.tradeConfig.autoSellOrderMin
|
&& holding.profitRate.toDouble() <= KisSession.tradeConfig.autoSellOrderMin
|
||||||
&& holding.profitRate.toDouble() >= KisSession.tradeConfig.autoSellOrderMax
|
&& holding.profitRate.toDouble() >= KisSession.tradeConfig.autoSellOrderMax
|
||||||
&& holding.avgPrice.toDouble() > holding.currentPrice.toDouble()) {
|
&& holding.avgPrice.toDouble() > holding.currentPrice.toDouble()
|
||||||
|
) {
|
||||||
var targetPrice = holding.avgPrice.toDouble()
|
var targetPrice = holding.avgPrice.toDouble()
|
||||||
targetPrice = MarketUtil.roundToTickSize(targetPrice + MarketUtil.getTickSize(targetPrice) * KisSession.tradeConfig.autoSellOrderAppend)
|
targetPrice = MarketUtil.roundToTickSize(
|
||||||
|
targetPrice + MarketUtil.getTickSize(targetPrice) * KisSession.tradeConfig.autoSellOrderAppend
|
||||||
|
)
|
||||||
tradeService.postOrder(
|
tradeService.postOrder(
|
||||||
stockCode = holding.code,
|
stockCode = holding.code,
|
||||||
qty = holding.availOrderCount,
|
qty = holding.availOrderCount,
|
||||||
@@ -632,13 +766,32 @@ object AutoTradingManager {
|
|||||||
} else if (KisSession.config.stop_Loss
|
} else if (KisSession.config.stop_Loss
|
||||||
&& holding != null && holding.quantity.toInt() > 0
|
&& holding != null && holding.quantity.toInt() > 0
|
||||||
&& holding.availOrderCount.toInt() > 0
|
&& holding.availOrderCount.toInt() > 0
|
||||||
&& holding.profitRate.toDouble() <= KisSession.config.getValues(ConfigIndex.LOSS_MINRATE)
|
&& holding.profitRate.toDouble() <= KisSession.config.getValues(
|
||||||
&& holding.profitRate.toDouble() >= KisSession.config.getValues(ConfigIndex.LOSS_MAXRATE)
|
ConfigIndex.LOSS_MINRATE
|
||||||
&& holding.valuationProfitAmount.toDouble() >= KisSession.config.getValues(ConfigIndex.LOSS_MAX_MONEY)) {
|
)
|
||||||
println("${holding.name} ${holding.profitRate.toDouble()} ${holding.valuationProfitAmount.toDouble()} ${KisSession.config.getValues(ConfigIndex.LOSS_MAX_MONEY)} , ${KisSession.config.getValues(ConfigIndex.LOSS_MINRATE)} , ${KisSession.config.getValues(ConfigIndex.STOP_LOSS)}")
|
&& holding.profitRate.toDouble() >= KisSession.config.getValues(
|
||||||
|
ConfigIndex.LOSS_MAXRATE
|
||||||
|
)
|
||||||
|
&& holding.valuationProfitAmount.toDouble() >= KisSession.config.getValues(
|
||||||
|
ConfigIndex.LOSS_MAX_MONEY
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
println(
|
||||||
|
"${holding.name} ${holding.profitRate.toDouble()} ${holding.valuationProfitAmount.toDouble()} ${
|
||||||
|
KisSession.config.getValues(
|
||||||
|
ConfigIndex.LOSS_MAX_MONEY
|
||||||
|
)
|
||||||
|
} , ${KisSession.config.getValues(ConfigIndex.LOSS_MINRATE)} , ${
|
||||||
|
KisSession.config.getValues(
|
||||||
|
ConfigIndex.STOP_LOSS
|
||||||
|
)
|
||||||
|
}"
|
||||||
|
)
|
||||||
val profit = holding.profitRate.toDouble()
|
val profit = holding.profitRate.toDouble()
|
||||||
var targetPrice = holding.currentPrice.toDouble()
|
var targetPrice = holding.currentPrice.toDouble()
|
||||||
targetPrice = MarketUtil.roundToTickSize(targetPrice + MarketUtil.getTickSize(targetPrice) * KisSession.tradeConfig.autoSellOrderAppend)
|
targetPrice = MarketUtil.roundToTickSize(
|
||||||
|
targetPrice + MarketUtil.getTickSize(targetPrice) * KisSession.tradeConfig.autoSellOrderAppend
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
tradeService.postOrder(
|
tradeService.postOrder(
|
||||||
@@ -666,7 +819,10 @@ object AutoTradingManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun analyzeDeepLossHoldingsAfterMarket(holding: UnifiedStockHolding, isForce : Boolean = false) { // 💡 [신규 추가] 수익률이 크게 마이너스인 종목(-5.0% 이하) 심층 가이드 분석
|
private suspend fun analyzeDeepLossHoldingsAfterMarket(
|
||||||
|
holding: UnifiedStockHolding,
|
||||||
|
isForce: Boolean = false
|
||||||
|
) { // 💡 [신규 추가] 수익률이 크게 마이너스인 종목(-5.0% 이하) 심층 가이드 분석
|
||||||
val now = LocalTime.now()
|
val now = LocalTime.now()
|
||||||
val currentMinute = now.minute
|
val currentMinute = now.minute
|
||||||
if ((holding.availOrderCount.toInt()
|
if ((holding.availOrderCount.toInt()
|
||||||
@@ -677,7 +833,8 @@ object AutoTradingManager {
|
|||||||
if (profit <= lossThreshold) {
|
if (profit <= lossThreshold) {
|
||||||
println("🔍 [손실 종목 분석] ${holding.name} (수익률: $profit%) - 가이드 산출 중...")
|
println("🔍 [손실 종목 분석] ${holding.name} (수익률: $profit%) - 가이드 산출 중...")
|
||||||
|
|
||||||
val dailyData = KisTradeService.fetchPeriodChartData(holding.code, "D", true).getOrNull()
|
val dailyData =
|
||||||
|
KisTradeService.fetchPeriodChartData(holding.code, "D", true).getOrNull()
|
||||||
|
|
||||||
if (!dailyData.isNullOrEmpty()) {
|
if (!dailyData.isNullOrEmpty()) {
|
||||||
val analyzer = TechnicalAnalyzer().apply { this.daily = dailyData }
|
val analyzer = TechnicalAnalyzer().apply { this.daily = dailyData }
|
||||||
@@ -694,7 +851,8 @@ object AutoTradingManager {
|
|||||||
|
|
||||||
// 🟢 [추매 타점] 볼린저 하단 터치(1.05배 이내) + RSI 과매도(35 이하) 구간
|
// 🟢 [추매 타점] 볼린저 하단 터치(1.05배 이내) + RSI 과매도(35 이하) 구간
|
||||||
if (lowerBand > 0 && currentPrice <= lowerBand * 1.05 && rsiDaily < 35.0) {
|
if (lowerBand > 0 && currentPrice <= lowerBand * 1.05 && rsiDaily < 35.0) {
|
||||||
advice = "📉 [추매 권장] 볼린저 밴드 하단 터치 및 RSI 과매도(${"%.1f".format(rsiDaily)}). 기술적 반등 확률이 매우 높은 통계적 바닥권입니다. (물타기 고려)"
|
advice =
|
||||||
|
"📉 [추매 권장] 볼린저 밴드 하단 터치 및 RSI 과매도(${"%.1f".format(rsiDaily)}). 기술적 반등 확률이 매우 높은 통계적 바닥권입니다. (물타기 고려)"
|
||||||
TradingLogStore.addNotice(
|
TradingLogStore.addNotice(
|
||||||
"보유주식[${holding.name}]",
|
"보유주식[${holding.name}]",
|
||||||
holding.code,
|
holding.code,
|
||||||
@@ -703,7 +861,8 @@ object AutoTradingManager {
|
|||||||
}
|
}
|
||||||
// 🔴 [손절 타점] 추세가 완전히 깨졌는데, 바닥(볼린저 하단)까지 한참 남았을 때
|
// 🔴 [손절 타점] 추세가 완전히 깨졌는데, 바닥(볼린저 하단)까지 한참 남았을 때
|
||||||
else if (isTrendBroken && currentPrice > lowerBand * 1.1) {
|
else if (isTrendBroken && currentPrice > lowerBand * 1.1) {
|
||||||
advice = "🚨 [손절 경고] 20일 추세가 완전히 무너졌으며, 아직 바닥(하단 밴드)도 확인되지 않았습니다. 추가 하락(지하실) 위험이 크므로 리스크 관리(손절)가 필요합니다."
|
advice =
|
||||||
|
"🚨 [손절 경고] 20일 추세가 완전히 무너졌으며, 아직 바닥(하단 밴드)도 확인되지 않았습니다. 추가 하락(지하실) 위험이 크므로 리스크 관리(손절)가 필요합니다."
|
||||||
TradingLogStore.addNotice(
|
TradingLogStore.addNotice(
|
||||||
"보유주식[${holding.name}]",
|
"보유주식[${holding.name}]",
|
||||||
holding.code,
|
holding.code,
|
||||||
@@ -763,7 +922,11 @@ object AutoTradingManager {
|
|||||||
LlamaServerManager.startServer(binPath, config.modelPath, port = LLM_PORT)
|
LlamaServerManager.startServer(binPath, config.modelPath, port = LLM_PORT)
|
||||||
}
|
}
|
||||||
if (config.embedModelPath.isNotEmpty()) {
|
if (config.embedModelPath.isNotEmpty()) {
|
||||||
LlamaServerManager.startServer(binPath, config.embedModelPath, port = EMBEDDING_PORT)
|
LlamaServerManager.startServer(
|
||||||
|
binPath,
|
||||||
|
config.embedModelPath,
|
||||||
|
port = EMBEDDING_PORT
|
||||||
|
)
|
||||||
}
|
}
|
||||||
KisWebSocketManager.connect()
|
KisWebSocketManager.connect()
|
||||||
isSystemReadyToday = true
|
isSystemReadyToday = true
|
||||||
@@ -772,7 +935,8 @@ object AutoTradingManager {
|
|||||||
println("❌ [System] 토큰 갱신 실패. 2분 후 재시도합니다.")
|
println("❌ [System] 토큰 갱신 실패. 2분 후 재시도합니다.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {}
|
} catch (e: Exception) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var onMarketClosed: (() -> Unit)? = null
|
var onMarketClosed: (() -> Unit)? = null
|
||||||
@@ -795,9 +959,16 @@ object AutoTradingManager {
|
|||||||
now.isAfter(KisSession.endTime()) || now.isBefore(KisSession.startTime()) -> {
|
now.isAfter(KisSession.endTime()) || now.isBefore(KisSession.startTime()) -> {
|
||||||
prepareMarketOpen(now)
|
prepareMarketOpen(now)
|
||||||
}
|
}
|
||||||
|
|
||||||
now.isBefore(KisSession.endTime()) && now.isAfter(KisSession.startTime()) -> {
|
now.isBefore(KisSession.endTime()) && now.isAfter(KisSession.startTime()) -> {
|
||||||
waitTime = 0.2
|
waitTime = 0.2
|
||||||
if (now.isAfter(LocalTime.of(8, 0)) && now.isBefore(LocalTime.of(15, 30))) {
|
if (now.isAfter(LocalTime.of(8, 0)) && now.isBefore(
|
||||||
|
LocalTime.of(
|
||||||
|
15,
|
||||||
|
30
|
||||||
|
)
|
||||||
|
)
|
||||||
|
) {
|
||||||
if (!KisSession.isMarketTokenValid() || !KisSession.isTradeTokenValid()) {
|
if (!KisSession.isMarketTokenValid() || !KisSession.isTradeTokenValid()) {
|
||||||
if (isSystemReadyToday) {
|
if (isSystemReadyToday) {
|
||||||
println("⚠️ [System] 토큰 만료 감지. 재발급 프로세스를 가동합니다.")
|
println("⚠️ [System] 토큰 만료 감지. 재발급 프로세스를 가동합니다.")
|
||||||
@@ -817,6 +988,7 @@ object AutoTradingManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
waitTime = 3.0
|
waitTime = 3.0
|
||||||
}
|
}
|
||||||
@@ -844,7 +1016,10 @@ object AutoTradingManager {
|
|||||||
isSystemReadyToday = false
|
isSystemReadyToday = false
|
||||||
shouldShowFullWindow = false
|
shouldShowFullWindow = false
|
||||||
stopDiscovery() // 발굴 루프 완전 폭파 (내일 8시 30분에 다시 켜짐)
|
stopDiscovery() // 발굴 루프 완전 폭파 (내일 8시 30분에 다시 켜짐)
|
||||||
} else if (now.isAfter(KisSession.startTime().minusMinutes(20)) && now.isBefore(KisSession.startTime()) && !shouldShowFullWindow) {
|
} else if (now.isAfter(
|
||||||
|
KisSession.startTime().minusMinutes(20)
|
||||||
|
) && now.isBefore(KisSession.startTime()) && !shouldShowFullWindow
|
||||||
|
) {
|
||||||
if (MarketUtil.canTradeToday()) {
|
if (MarketUtil.canTradeToday()) {
|
||||||
shouldShowFullWindow = true
|
shouldShowFullWindow = true
|
||||||
println("✅ [System] 오늘은 영업일입니다. 시스템을 가동합니다.")
|
println("✅ [System] 오늘은 영업일입니다. 시스템을 가동합니다.")
|
||||||
@@ -855,13 +1030,14 @@ object AutoTradingManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var loadedTops = mutableListOf<Pair<String, String>>()
|
var loadedTops = mutableListOf<Pair<String, String>>()
|
||||||
var defaultStockCount = 30
|
var defaultStockCount = 30
|
||||||
|
|
||||||
|
|
||||||
var currentBalance: UnifiedBalance? = null
|
var currentBalance: UnifiedBalance? = null
|
||||||
private var lastFetchTime: Long = 0L // 마지막 성공 시간 (Millisecond)
|
private var lastFetchTime: Long = 0L // 마지막 성공 시간 (Millisecond)
|
||||||
private val FETCH_INTERVAL = 2 * 60 * 1000 // 30분을 밀리초로 환산 (1800000 ms)
|
private val FETCH_INTERVAL = 1 * 60 * 1000 // 30분을 밀리초로 환산 (1800000 ms)
|
||||||
|
|
||||||
suspend fun checkBalance() {
|
suspend fun checkBalance() {
|
||||||
val currentTime = System.currentTimeMillis()
|
val currentTime = System.currentTimeMillis()
|
||||||
@@ -878,8 +1054,15 @@ object AutoTradingManager {
|
|||||||
// 30분이 지나지 않았다면 기존에 저장된 currentBalance를 그대로 사용
|
// 30분이 지나지 않았다면 기존에 저장된 currentBalance를 그대로 사용
|
||||||
println("${(FETCH_INTERVAL / (1000 * 60))}분이 지나지 않아 기존 잔고 데이터 유지 (남은 시간: ${(FETCH_INTERVAL - (currentTime - lastFetchTime)) / 1000}초)")
|
println("${(FETCH_INTERVAL / (1000 * 60))}분이 지나지 않아 기존 잔고 데이터 유지 (남은 시간: ${(FETCH_INTERVAL - (currentTime - lastFetchTime)) / 1000}초)")
|
||||||
}
|
}
|
||||||
if (KisSession.config.take_profit) currentBalance?.let { resumePendingSellOrders(KisTradeService, it) }
|
if (KisSession.config.take_profit) currentBalance?.let {
|
||||||
if (KisSession.tradeConfig.auto_cancel_pending_buy) { checkAndCancelPendingBuyOrders() }
|
resumePendingSellOrders(
|
||||||
|
KisTradeService,
|
||||||
|
it
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (KisSession.tradeConfig.auto_cancel_pending_buy) {
|
||||||
|
checkAndCancelPendingBuyOrders()
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -897,13 +1080,23 @@ object AutoTradingManager {
|
|||||||
val elapsedMillis = currentTime - orderTimeMillis
|
val elapsedMillis = currentTime - orderTimeMillis
|
||||||
if (elapsedMillis >= KisSession.tradeConfig.auto_cancel_pending_time) {
|
if (elapsedMillis >= KisSession.tradeConfig.auto_cancel_pending_time) {
|
||||||
// 2. 현재가 조회 (가격을 비교하기 위해)
|
// 2. 현재가 조회 (가격을 비교하기 위해)
|
||||||
val currentPrice = KisTradeService.fetchCurrentPrice(order.pdno).getOrNull()?.stck_prpr?.toDouble() ?: 0.0
|
val currentPrice = KisTradeService.fetchCurrentPrice(order.pdno)
|
||||||
|
.getOrNull()?.stck_prpr?.toDouble() ?: 0.0
|
||||||
val orderedPrice = order.ord_unpr.toDoubleOrNull() ?: 0.0
|
val orderedPrice = order.ord_unpr.toDoubleOrNull() ?: 0.0
|
||||||
// 조건 B: 현재가와 주문가의 괴리율 체크 (현재가가 너무 올라갔거나 내려갔을 때)
|
// 조건 B: 현재가와 주문가의 괴리율 체크 (현재가가 너무 올라갔거나 내려갔을 때)
|
||||||
val priceGap = Math.abs(currentPrice - orderedPrice) / orderedPrice
|
val priceGap = Math.abs(currentPrice - orderedPrice) / orderedPrice
|
||||||
println("checkAndCancelPendingBuyOrders order $order ${elapsedMillis / 1000L}초 ${priceGap}% 차이")
|
println("checkAndCancelPendingBuyOrders order $order ${elapsedMillis / 1000L}초 ${priceGap}% 차이")
|
||||||
if (priceGap >= KisSession.tradeConfig.auto_cancel_pending_rate) {
|
if (priceGap >= KisSession.tradeConfig.auto_cancel_pending_rate) {
|
||||||
TradingLogStore.addNotice(order.prdt_name,order.pdno,"[주문 취소] ${order.prdt_name} (${order.pdno}) - 시간경과 및 가격괴리(${String.format("%.2f", priceGap)}%)로 취소 시도")
|
TradingLogStore.addNotice(
|
||||||
|
order.prdt_name,
|
||||||
|
order.pdno,
|
||||||
|
"[주문 취소] ${order.prdt_name} (${order.pdno}) - 시간경과 및 가격괴리(${
|
||||||
|
String.format(
|
||||||
|
"%.2f",
|
||||||
|
priceGap
|
||||||
|
)
|
||||||
|
}%)로 취소 시도"
|
||||||
|
)
|
||||||
KisTradeService.cancelOrder(
|
KisTradeService.cancelOrder(
|
||||||
order.ord_no, // 원주문번호
|
order.ord_no, // 원주문번호
|
||||||
order.pdno
|
order.pdno
|
||||||
@@ -920,7 +1113,11 @@ object AutoTradingManager {
|
|||||||
val unfilledResult = KisTradeService.fetchUnfilledOrders()
|
val unfilledResult = KisTradeService.fetchUnfilledOrders()
|
||||||
unfilledResult.onSuccess { response ->
|
unfilledResult.onSuccess { response ->
|
||||||
response.filter { it.sll_buy_dvsn_cd == "01" }.forEach { order ->
|
response.filter { it.sll_buy_dvsn_cd == "01" }.forEach { order ->
|
||||||
TradingLogStore.addNotice(order.prdt_name,order.pdno,"[주문 취소] 정규장 시작전 모든 매도 주문 취소")
|
TradingLogStore.addNotice(
|
||||||
|
order.prdt_name,
|
||||||
|
order.pdno,
|
||||||
|
"[주문 취소] 정규장 시작전 모든 매도 주문 취소"
|
||||||
|
)
|
||||||
KisTradeService.cancelOrder(
|
KisTradeService.cancelOrder(
|
||||||
order.ord_no, // 원주문번호
|
order.ord_no, // 원주문번호
|
||||||
order.pdno
|
order.pdno
|
||||||
@@ -946,9 +1143,12 @@ object AutoTradingManager {
|
|||||||
|
|
||||||
suspend fun executeMarketLoop() {
|
suspend fun executeMarketLoop() {
|
||||||
checkBalance()
|
checkBalance()
|
||||||
var myCash = currentBalance?.deposit?.replace(",", "")?.toLongOrNull() ?: KisSession.config.getValues(ConfigIndex.MAX_PRICE_INDEX).toLong()
|
var myCash = currentBalance?.deposit?.replace(",", "")?.toLongOrNull()
|
||||||
|
?: KisSession.config.getValues(ConfigIndex.MAX_PRICE_INDEX).toLong()
|
||||||
myCash = max(myCash, KisSession.config.getValues(ConfigIndex.MAX_PRICE_INDEX).toLong())
|
myCash = max(myCash, KisSession.config.getValues(ConfigIndex.MAX_PRICE_INDEX).toLong())
|
||||||
val myHoldings = currentBalance?.getHoldings()?.filter { !it.isTodayEntry }?.map { it.code }?.toSet() ?: emptySet()
|
val myHoldings =
|
||||||
|
currentBalance?.getHoldings()?.filter { !it.isTodayEntry }?.map { it.code }?.toSet()
|
||||||
|
?: emptySet()
|
||||||
val pendingStocks = DatabaseFactory.findAllMonitoringTrades().map { it.code }
|
val pendingStocks = DatabaseFactory.findAllMonitoringTrades().map { it.code }
|
||||||
var now = LocalTime.now(ZoneId.of("Asia/Seoul"))
|
var now = LocalTime.now(ZoneId.of("Asia/Seoul"))
|
||||||
if (remainingCandidates.isEmpty()) {
|
if (remainingCandidates.isEmpty()) {
|
||||||
@@ -960,7 +1160,12 @@ object AutoTradingManager {
|
|||||||
val count = minOf(loadedTops.size, defaultStockCount)
|
val count = minOf(loadedTops.size, defaultStockCount)
|
||||||
for (i in 0..<count) {
|
for (i in 0..<count) {
|
||||||
loadedTops.removeFirst().let {
|
loadedTops.removeFirst().let {
|
||||||
addToReanalysis(RankingStock(mksc_shrn_iscd = it.first, hts_kor_isnm = it.second))
|
addToReanalysis(
|
||||||
|
RankingStock(
|
||||||
|
mksc_shrn_iscd = it.first,
|
||||||
|
hts_kor_isnm = it.second
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -968,7 +1173,10 @@ object AutoTradingManager {
|
|||||||
}.filter {
|
}.filter {
|
||||||
val rate = it.prdy_ctrt.toDouble()
|
val rate = it.prdy_ctrt.toDouble()
|
||||||
val corpInfo = DartCodeManager.getCorpCode(it.code)
|
val corpInfo = DartCodeManager.getCorpCode(it.code)
|
||||||
val isOk = (rate > 0 && rate < KisSession.tradeConfig.plusFilter) || (rate < 0 && rate > (abs(KisSession.tradeConfig.minusFilter) * -1))
|
val isOk =
|
||||||
|
(rate > 0 && rate < KisSession.tradeConfig.plusFilter) || (rate < 0 && rate > (abs(
|
||||||
|
KisSession.tradeConfig.minusFilter
|
||||||
|
) * -1))
|
||||||
|
|
||||||
if (corpInfo?.cName.isNullOrEmpty()) {
|
if (corpInfo?.cName.isNullOrEmpty()) {
|
||||||
false
|
false
|
||||||
@@ -976,7 +1184,8 @@ object AutoTradingManager {
|
|||||||
it.code !in pendingStocks &&
|
it.code !in pendingStocks &&
|
||||||
it.code !in executionCache.values.map { it.code } &&
|
it.code !in executionCache.values.map { it.code } &&
|
||||||
it.code !in failList &&
|
it.code !in failList &&
|
||||||
it.code !in isSafetyBeltStockCodes){
|
it.code !in isSafetyBeltStockCodes
|
||||||
|
) {
|
||||||
isOk
|
isOk
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
@@ -995,23 +1204,33 @@ object AutoTradingManager {
|
|||||||
it.quantity.toInt() > KisSession.tradeConfig.lowerAverageTargetCount &&
|
it.quantity.toInt() > KisSession.tradeConfig.lowerAverageTargetCount &&
|
||||||
it.profitRate.toDouble() < 0.0 &&
|
it.profitRate.toDouble() < 0.0 &&
|
||||||
it.profitRate.toDouble() < (abs(KisSession.tradeConfig.lowerAverageMaxRate) * -1) &&
|
it.profitRate.toDouble() < (abs(KisSession.tradeConfig.lowerAverageMaxRate) * -1) &&
|
||||||
it.profitRate.toDouble() > (abs(KisSession.tradeConfig.lowerAverageMinRate) * -1))
|
it.profitRate.toDouble() > (abs(KisSession.tradeConfig.lowerAverageMinRate) * -1)
|
||||||
{
|
) {
|
||||||
candidates.add(RankingStock(mksc_shrn_iscd = it.code, hts_kor_isnm = it.name))
|
candidates.add(
|
||||||
|
RankingStock(
|
||||||
|
mksc_shrn_iscd = it.code,
|
||||||
|
hts_kor_isnm = it.name
|
||||||
|
)
|
||||||
|
)
|
||||||
println("물타기 대상 추가 ${it.name}[${it.code}]")
|
println("물타기 대상 추가 ${it.name}[${it.code}]")
|
||||||
var oldTarget = it
|
var oldTarget = it
|
||||||
if (oldTarget != null) {
|
if (oldTarget != null) {
|
||||||
var avgPrive = oldTarget.avgPrice.toDouble()
|
var avgPrive = oldTarget.avgPrice.toDouble()
|
||||||
|
|
||||||
var qty = oldTarget.quantity.toDouble()
|
var qty = oldTarget.quantity.toDouble()
|
||||||
var basePrice = ((avgPrive * qty) + it.currentPrice.toDouble()).div(qty!!.toInt() + 1)
|
var basePrice =
|
||||||
|
((avgPrive * qty) + it.currentPrice.toDouble()).div(qty!!.toInt() + 1)
|
||||||
println("물타기 ${avgPrive}, ${qty} ${basePrice}")
|
println("물타기 ${avgPrive}, ${qty} ${basePrice}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
remainingCandidates.addAll(candidates.filter {
|
remainingCandidates.addAll(candidates.filter {
|
||||||
(if (KisSession.tradeConfig.lowerAveragePrice) { true } else {it.code !in myHoldings}) &&
|
(if (KisSession.tradeConfig.lowerAveragePrice) {
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
it.code !in myHoldings
|
||||||
|
}) &&
|
||||||
it.code !in pendingStocks &&
|
it.code !in pendingStocks &&
|
||||||
it.code !in executionCache.values.map { it.code } &&
|
it.code !in executionCache.values.map { it.code } &&
|
||||||
it.code !in failList &&
|
it.code !in failList &&
|
||||||
@@ -1047,6 +1266,7 @@ object AutoTradingManager {
|
|||||||
}
|
}
|
||||||
println("⏱️ [Cycle End] ${LocalTime.now()}")
|
println("⏱️ [Cycle End] ${LocalTime.now()}")
|
||||||
}
|
}
|
||||||
|
|
||||||
// private var lastForceCheckMinute = -1 // 마지막으로 강제 체크를 수행한 '분'을 저장
|
// private var lastForceCheckMinute = -1 // 마지막으로 강제 체크를 수행한 '분'을 저장
|
||||||
private val executionCountMap = mutableMapOf<String, Int>()
|
private val executionCountMap = mutableMapOf<String, Int>()
|
||||||
suspend fun sellSchedule() {
|
suspend fun sellSchedule() {
|
||||||
@@ -1054,7 +1274,9 @@ object AutoTradingManager {
|
|||||||
val now = LocalTime.now()
|
val now = LocalTime.now()
|
||||||
val timeKey = String.format("%02d:%02d", now.hour, now.minute) // 예: "09:05"
|
val timeKey = String.format("%02d:%02d", now.hour, now.minute) // 예: "09:05"
|
||||||
val currentCount = executionCountMap.getOrDefault(timeKey, 0)
|
val currentCount = executionCountMap.getOrDefault(timeKey, 0)
|
||||||
if (currentCount >= KisSession.tradeConfig.excuteCountOnMin) { return }
|
if (currentCount >= KisSession.tradeConfig.excuteCountOnMin) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
var isExecuted = false
|
var isExecuted = false
|
||||||
val currentMinute = now.minute
|
val currentMinute = now.minute
|
||||||
@@ -1086,8 +1308,14 @@ object AutoTradingManager {
|
|||||||
} else if (
|
} else if (
|
||||||
(
|
(
|
||||||
(now.hour == 8 && KisSession.tradeConfig.before_nxt && currentMinute < 45) ||
|
(now.hour == 8 && KisSession.tradeConfig.before_nxt && currentMinute < 45) ||
|
||||||
(now.isAfter(LocalTime.of(15,40)) && now.isBefore(LocalTime.of(20,0)) && KisSession.tradeConfig.after_nxt)
|
(now.isAfter(LocalTime.of(15, 40)) && now.isBefore(
|
||||||
) && (currentMinute % 2 == 0)) {
|
LocalTime.of(
|
||||||
|
20,
|
||||||
|
0
|
||||||
|
)
|
||||||
|
) && KisSession.tradeConfig.after_nxt)
|
||||||
|
) && (currentMinute % 2 == 0)
|
||||||
|
) {
|
||||||
TradingLogStore.addAnalyzer(
|
TradingLogStore.addAnalyzer(
|
||||||
" - ",
|
" - ",
|
||||||
" - ",
|
" - ",
|
||||||
@@ -1105,7 +1333,9 @@ object AutoTradingManager {
|
|||||||
}
|
}
|
||||||
isExecuted = true
|
isExecuted = true
|
||||||
}
|
}
|
||||||
if (isExecuted) { executionCountMap[timeKey] = currentCount + 1 }
|
if (isExecuted) {
|
||||||
|
executionCountMap[timeKey] = currentCount + 1
|
||||||
|
}
|
||||||
if (now.hour >= 20) {
|
if (now.hour >= 20) {
|
||||||
executionCountMap.clear()
|
executionCountMap.clear()
|
||||||
noticeFilter.clear()
|
noticeFilter.clear()
|
||||||
@@ -1121,8 +1351,14 @@ object AutoTradingManager {
|
|||||||
// println("📝 [Memory] ${stock.name} 관망 판정 -> 차기 루프 재분석 리스트 등록")
|
// println("📝 [Memory] ${stock.name} 관망 판정 -> 차기 루프 재분석 리스트 등록")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val failList = arrayListOf<String>()
|
val failList = arrayListOf<String>()
|
||||||
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 {
|
||||||
val maxBudget = KisSession.config.getValues(ConfigIndex.MAX_BUDGET_INDEX)
|
val maxBudget = KisSession.config.getValues(ConfigIndex.MAX_BUDGET_INDEX)
|
||||||
val maxPrice = KisSession.config.getValues(ConfigIndex.MAX_PRICE_INDEX)
|
val maxPrice = KisSession.config.getValues(ConfigIndex.MAX_PRICE_INDEX)
|
||||||
@@ -1134,7 +1370,9 @@ object AutoTradingManager {
|
|||||||
print("-> 기업명을 못찾아서 제외 | ")
|
print("-> 기업명을 못찾아서 제외 | ")
|
||||||
return@withTimeout
|
return@withTimeout
|
||||||
}
|
}
|
||||||
if(currentBalance?.getHoldings()?.any { it.code.equals(stock.code) && it.quantity.toInt() > 2} == true) {
|
if (currentBalance?.getHoldings()
|
||||||
|
?.any { it.code.equals(stock.code) && it.quantity.toInt() > 2 } == true
|
||||||
|
) {
|
||||||
println("물타기 대상 분석")
|
println("물타기 대상 분석")
|
||||||
}
|
}
|
||||||
callback(TradingDecision().apply {
|
callback(TradingDecision().apply {
|
||||||
@@ -1144,10 +1382,12 @@ object AutoTradingManager {
|
|||||||
}, false)
|
}, false)
|
||||||
|
|
||||||
val dailyData =
|
val dailyData =
|
||||||
tradeService.fetchPeriodChartData(stock.code, "D", true).getOrNull() ?: return@withTimeout
|
tradeService.fetchPeriodChartData(stock.code, "D", true).getOrNull()
|
||||||
|
?: return@withTimeout
|
||||||
val today = dailyData.lastOrNull() ?: null
|
val today = dailyData.lastOrNull() ?: null
|
||||||
var rate = today?.getFluctuationRate() ?: 0.0
|
var rate = today?.getFluctuationRate() ?: 0.0
|
||||||
val isOk = ((rate < KisSession.tradeConfig.plusFilter) && (rate > (abs(KisSession.tradeConfig.minusFilter) * -1)))
|
val isOk =
|
||||||
|
((rate < KisSession.tradeConfig.plusFilter) && (rate > (abs(KisSession.tradeConfig.minusFilter) * -1)))
|
||||||
delay(50)
|
delay(50)
|
||||||
// 1. var 대신 val을 사용해야 아래에서 스마트 캐스트가 작동하여 !!를 안 써도 됩니다.
|
// 1. var 대신 val을 사용해야 아래에서 스마트 캐스트가 작동하여 !!를 안 써도 됩니다.
|
||||||
val currentStock = KisTradeService.fetchCurrentPrice(stock.code).getOrNull()
|
val currentStock = KisTradeService.fetchCurrentPrice(stock.code).getOrNull()
|
||||||
@@ -1176,14 +1416,14 @@ object AutoTradingManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 🌟 [추가] 고도화된 사전 필터링 (검문소)
|
// 🌟 [추가] 고도화된 사전 필터링 (검문소)
|
||||||
val tempAnalyzer = TechnicalAnalyzer().apply { this.daily = dailyData }
|
val tempAnalyzer = TechnicalAnalyzer().apply { this.daily = dailyData }
|
||||||
|
|
||||||
// 1. 변동성 기반 수익률 검증 (2% 이상 열려있는가?)
|
// 1. 변동성 기반 수익률 검증 (2% 이상 열려있는가?)
|
||||||
println("(dailyData.size * 0.8).toInt() ${(dailyData.size * 0.3).toInt()}")
|
println("(dailyData.size * 0.8).toInt() ${(dailyData.size * 0.3).toInt()}")
|
||||||
val volatility = tempAnalyzer.calculateVolatilityForecast(dailyData, 20)
|
val volatility = tempAnalyzer.calculateVolatilityForecast(dailyData, 20)
|
||||||
val expectedProfitRate = ((volatility.realisticHigh - currentPrice) / currentPrice) * 100.0
|
val expectedProfitRate =
|
||||||
|
((volatility.realisticHigh - currentPrice) / currentPrice) * 100.0
|
||||||
|
|
||||||
// 2. 일봉 기준 반등 주기 통계 추출 (일주일 내 승부 가능한가?)
|
// 2. 일봉 기준 반등 주기 통계 추출 (일주일 내 승부 가능한가?)
|
||||||
val dailyStats = tempAnalyzer.calculateDynamicReboundStats(dailyData, 5.0)
|
val dailyStats = tempAnalyzer.calculateDynamicReboundStats(dailyData, 5.0)
|
||||||
@@ -1198,17 +1438,26 @@ object AutoTradingManager {
|
|||||||
|
|
||||||
|
|
||||||
// 🌟 [수정] 조건 통합 (OR 조건)
|
// 🌟 [수정] 조건 통합 (OR 조건)
|
||||||
val isProfitable = expectedProfitRate >= KisSession.tradeConfig.minExpectedProfitRate || dailyStats.avgReboundAmplitude >= KisSession.tradeConfig.minExpectedProfitRate
|
val isProfitable =
|
||||||
|
expectedProfitRate >= KisSession.tradeConfig.minExpectedProfitRate || dailyStats.avgReboundAmplitude >= KisSession.tradeConfig.minExpectedProfitRate
|
||||||
|
|
||||||
// 반등 주기에 도달했거나(Mean Reversion), 안정적으로 뻗어나가는 우상향 종목(Trend Following)이면 통과
|
// 반등 주기에 도달했거나(Mean Reversion), 안정적으로 뻗어나가는 우상향 종목(Trend Following)이면 통과
|
||||||
val isValidEntryTiming = (dailyStats.isValid && isApproaching && dailyStats.avgReboundPeriod <= KisSession.tradeConfig.maxExpectedReboundDays && dailyStats.avgReboundPeriod >= KisSession.tradeConfig.minExpectedReboundDays) || isSteadyUptrend
|
val isValidEntryTiming =
|
||||||
|
(dailyStats.isValid && isApproaching && dailyStats.avgReboundPeriod <= KisSession.tradeConfig.maxExpectedReboundDays && dailyStats.avgReboundPeriod >= KisSession.tradeConfig.minExpectedReboundDays) || isSteadyUptrend
|
||||||
|
|
||||||
val currentAtr = tempAnalyzer.calculateATR(dailyData)
|
val currentAtr = tempAnalyzer.calculateATR(dailyData)
|
||||||
if (!isProfitable || !isValidEntryTiming) {
|
if (!isProfitable || !isValidEntryTiming) {
|
||||||
print("-> [${stock.name}] 조건 미달 필터링 (예측수익: ${"%.1f".format(expectedProfitRate)}%, 주기: ${"%.1f".format(dailyStats.avgReboundPeriod)}일, 진입권: $isValidEntryTiming) | ")
|
print(
|
||||||
|
"-> [${stock.name}] 조건 미달 필터링 (예측수익: ${"%.1f".format(expectedProfitRate)}%, 주기: ${
|
||||||
|
"%.1f".format(
|
||||||
|
dailyStats.avgReboundPeriod
|
||||||
|
)
|
||||||
|
}일, 진입권: $isValidEntryTiming) | "
|
||||||
|
)
|
||||||
return@withTimeout // 조건에 맞지 않으면 주봉/월봉 API 호출 및 LLM 분석 없이 즉시 다음 종목으로 넘어감
|
return@withTimeout // 조건에 맞지 않으면 주봉/월봉 API 호출 및 LLM 분석 없이 즉시 다음 종목으로 넘어감
|
||||||
}
|
}
|
||||||
val dropPrediction = tempAnalyzer.predictDropBottom(dailyData, dailyStats, volatility, currentAtr)
|
val dropPrediction =
|
||||||
|
tempAnalyzer.predictDropBottom(dailyData, dailyStats, volatility, currentAtr)
|
||||||
|
|
||||||
if (dropPrediction != null) {
|
if (dropPrediction != null) {
|
||||||
// 💡 [방어 로직 1] 아직 바닥까지 한참 남았다면 지하실 방지
|
// 💡 [방어 로직 1] 아직 바닥까지 한참 남았다면 지하실 방지
|
||||||
@@ -1239,16 +1488,30 @@ object AutoTradingManager {
|
|||||||
println("[${stock.name}] 배당 정보 무관 함.")
|
println("[${stock.name}] 배당 정보 무관 함.")
|
||||||
}
|
}
|
||||||
|
|
||||||
println("🔍 [분석 진입] ${stock.name} (${LocalTime.now()}) (예측수익: ${"%.1f".format(expectedProfitRate)}%, 주기: ${"%.1f".format(dailyStats.avgReboundPeriod)}일, 진입권: $isValidEntryTiming)")
|
println(
|
||||||
|
"🔍 [분석 진입] ${stock.name} (${LocalTime.now()}) (예측수익: ${
|
||||||
|
"%.1f".format(
|
||||||
|
expectedProfitRate
|
||||||
|
)
|
||||||
|
}%, 주기: ${"%.1f".format(dailyStats.avgReboundPeriod)}일, 진입권: $isValidEntryTiming)"
|
||||||
|
)
|
||||||
if (!isSafetyBeltStockCodes.contains(stock.code)) {
|
if (!isSafetyBeltStockCodes.contains(stock.code)) {
|
||||||
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())
|
||||||
|
}
|
||||||
delay(20)
|
delay(20)
|
||||||
val weekly =
|
val weekly =
|
||||||
async { tradeService.fetchPeriodChartData(stock.code, "W", true).getOrDefault(emptyList()) }
|
async {
|
||||||
|
tradeService.fetchPeriodChartData(stock.code, "W", true)
|
||||||
|
.getOrDefault(emptyList())
|
||||||
|
}
|
||||||
delay(20)
|
delay(20)
|
||||||
val monthly =
|
val monthly =
|
||||||
async { tradeService.fetchPeriodChartData(stock.code, "M", true).getOrDefault(emptyList()) }
|
async {
|
||||||
|
tradeService.fetchPeriodChartData(stock.code, "M", true)
|
||||||
|
.getOrDefault(emptyList())
|
||||||
|
}
|
||||||
delay(20)
|
delay(20)
|
||||||
TechnicalAnalyzer().apply {
|
TechnicalAnalyzer().apply {
|
||||||
this.daily = dailyData
|
this.daily = dailyData
|
||||||
@@ -1263,8 +1526,16 @@ object AutoTradingManager {
|
|||||||
if (analyzer.isValid()) {
|
if (analyzer.isValid()) {
|
||||||
|
|
||||||
println("✅ [분석 시작] ${stock.name} (${LocalTime.now()} 분석 데이터 정합성 -> ${analyzer.isValid()})")
|
println("✅ [분석 시작] ${stock.name} (${LocalTime.now()} 분석 데이터 정합성 -> ${analyzer.isValid()})")
|
||||||
RagService.processStock(currentPrice, analyzer, stock.name, stock.code) { decision, isSuccess ->
|
RagService.processStock(
|
||||||
callback(decision?.apply { this.currentPrice = currentPrice }, isSuccess)
|
currentPrice,
|
||||||
|
analyzer,
|
||||||
|
stock.name,
|
||||||
|
stock.code
|
||||||
|
) { decision, isSuccess ->
|
||||||
|
callback(
|
||||||
|
decision?.apply { this.currentPrice = currentPrice },
|
||||||
|
isSuccess
|
||||||
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
println("✅ [분석 실패] ${stock.name} (${LocalTime.now()} 분석 데이터 정합성 -> ${analyzer.isValid()})")
|
println("✅ [분석 실패] ${stock.name} (${LocalTime.now()} 분석 데이터 정합성 -> ${analyzer.isValid()})")
|
||||||
@@ -1280,20 +1551,57 @@ object AutoTradingManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun fetchCandidates(tradeService: KisTradeService): List<RankingStock> = coroutineScope {
|
private suspend fun fetchCandidates(tradeService: KisTradeService): List<RankingStock> =
|
||||||
|
coroutineScope {
|
||||||
listOf(
|
listOf(
|
||||||
async { tradeService.fetchMarketRanking(RankingType.VOLUME, true).getOrDefault(emptyList()) },
|
async {
|
||||||
async { tradeService.fetchMarketRanking(RankingType.VOLUME0, true).getOrDefault(emptyList()) },
|
tradeService.fetchMarketRanking(RankingType.VOLUME, true)
|
||||||
async { tradeService.fetchMarketRanking(RankingType.VOLUME1, true).getOrDefault(emptyList()) },
|
.getOrDefault(emptyList())
|
||||||
async { tradeService.fetchMarketRanking(RankingType.VOLUME4, true).getOrDefault(emptyList()) },
|
},
|
||||||
async { tradeService.fetchMarketRanking(RankingType.RISE, true).getOrDefault(emptyList()) },
|
async {
|
||||||
async { tradeService.fetchMarketRanking(RankingType.FALL, true).getOrDefault(emptyList()) },
|
tradeService.fetchMarketRanking(RankingType.VOLUME0, true)
|
||||||
async { tradeService.fetchMarketRanking(RankingType.VALUE, true).getOrDefault(emptyList()) },
|
.getOrDefault(emptyList())
|
||||||
async { tradeService.fetchMarketRanking(RankingType.VOLUME_POWER, true).getOrDefault(emptyList()) },
|
},
|
||||||
async { tradeService.fetchMarketRanking(RankingType.COMPANY_TRADE, true).getOrDefault(emptyList()) },
|
async {
|
||||||
async { tradeService.fetchMarketRanking(RankingType.FINANCE, true).getOrDefault(emptyList()) },
|
tradeService.fetchMarketRanking(RankingType.VOLUME1, true)
|
||||||
async { tradeService.fetchMarketRanking(RankingType.MARKET_VALUE, true).getOrDefault(emptyList()) },
|
.getOrDefault(emptyList())
|
||||||
async { tradeService.fetchMarketRanking(RankingType.SHORT_SALE, true).getOrDefault(emptyList()) },
|
},
|
||||||
|
async {
|
||||||
|
tradeService.fetchMarketRanking(RankingType.VOLUME4, true)
|
||||||
|
.getOrDefault(emptyList())
|
||||||
|
},
|
||||||
|
async {
|
||||||
|
tradeService.fetchMarketRanking(RankingType.RISE, true)
|
||||||
|
.getOrDefault(emptyList())
|
||||||
|
},
|
||||||
|
async {
|
||||||
|
tradeService.fetchMarketRanking(RankingType.FALL, true)
|
||||||
|
.getOrDefault(emptyList())
|
||||||
|
},
|
||||||
|
async {
|
||||||
|
tradeService.fetchMarketRanking(RankingType.VALUE, true)
|
||||||
|
.getOrDefault(emptyList())
|
||||||
|
},
|
||||||
|
async {
|
||||||
|
tradeService.fetchMarketRanking(RankingType.VOLUME_POWER, true)
|
||||||
|
.getOrDefault(emptyList())
|
||||||
|
},
|
||||||
|
async {
|
||||||
|
tradeService.fetchMarketRanking(RankingType.COMPANY_TRADE, true)
|
||||||
|
.getOrDefault(emptyList())
|
||||||
|
},
|
||||||
|
async {
|
||||||
|
tradeService.fetchMarketRanking(RankingType.FINANCE, true)
|
||||||
|
.getOrDefault(emptyList())
|
||||||
|
},
|
||||||
|
async {
|
||||||
|
tradeService.fetchMarketRanking(RankingType.MARKET_VALUE, true)
|
||||||
|
.getOrDefault(emptyList())
|
||||||
|
},
|
||||||
|
async {
|
||||||
|
tradeService.fetchMarketRanking(RankingType.SHORT_SALE, true)
|
||||||
|
.getOrDefault(emptyList())
|
||||||
|
},
|
||||||
).awaitAll().flatten()
|
).awaitAll().flatten()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user