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