This commit is contained in:
2026-09-08 11:30:43 +09:00
parent 5e90ee39bc
commit 3998cf159a
+411 -103
View File
@@ -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()
}