|
|
|
@@ -38,6 +38,7 @@ import network.KisAuthService
|
|
|
|
|
import network.KisTradeService
|
|
|
|
|
import network.KisWebSocketManager
|
|
|
|
|
import network.RagService
|
|
|
|
|
import network.RagService.isSafetyBeltStockCodes
|
|
|
|
|
import network.StockUniverseLoader
|
|
|
|
|
import report.SnapshotType
|
|
|
|
|
import report.TradingReportManager
|
|
|
|
@@ -108,9 +109,9 @@ object AutoTradingManager {
|
|
|
|
|
if (KisSession.isAvailBuyTime(now) && isSuccess && completeTradingDecision != null) {
|
|
|
|
|
val decision = completeTradingDecision
|
|
|
|
|
|
|
|
|
|
println("${decision.stockName} ${decision.decision} ${buysCodes.contains(decision.stockCode) == false}")
|
|
|
|
|
// 1. 이미 AI가 결정한 decision과 confidence를 신뢰함
|
|
|
|
|
if (decision.decision == "BUY") {
|
|
|
|
|
val minScore = KisSession.config.getValues(ConfigIndex.MIN_PURCHASE_SCORE_INDEX)
|
|
|
|
|
if (decision.decision == "BUY" && buysCodes.contains(decision.stockCode) == false) {
|
|
|
|
|
|
|
|
|
|
// AI가 이미 검증한 등급을 사용 (재계산 불필요)
|
|
|
|
|
val grade = decision.investmentGrade ?: InvestmentGrade.LEVEL_1_SPECULATIVE
|
|
|
|
@@ -118,8 +119,14 @@ object AutoTradingManager {
|
|
|
|
|
// 2. 최종 매수 실행
|
|
|
|
|
val gradeRate = KisSession.config.getValues(grade.allocationRate)
|
|
|
|
|
val maxBudget = KisSession.config.getValues(ConfigIndex.MAX_BUDGET_INDEX) * gradeRate
|
|
|
|
|
val calculatedQty = (maxBudget / decision.currentPrice).toInt().coerceAtLeast(1)
|
|
|
|
|
|
|
|
|
|
TradingLogStore.addLog(decision,"BUY",decision.summary())
|
|
|
|
|
var hasCodes = currentBalance?.getHoldings()?.any { it.code.equals(decision.stockCode) && it.quantity.toInt() > 2 && it.isTodayEntry == false}
|
|
|
|
|
if (hasCodes == true) {
|
|
|
|
|
buysCodes.add(decision.stockCode)
|
|
|
|
|
TradingLogStore.addNotice(decision.stockName,decision.stockCode,"물타기 시도 1주 매수")
|
|
|
|
|
}
|
|
|
|
|
val calculatedQty = if(hasCodes == true) KisSession.tradeConfig.lowerAverageStockCount else (maxBudget / decision.currentPrice).toInt().coerceAtLeast(1)
|
|
|
|
|
excuteTrade(
|
|
|
|
|
decision = decision,
|
|
|
|
|
orderQty = calculatedQty.toString(),
|
|
|
|
@@ -202,7 +209,7 @@ object AutoTradingManager {
|
|
|
|
|
rawGrade
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var buysCodes = arrayListOf<String>()
|
|
|
|
|
fun excuteTrade(decision: TradingDecision, orderQty: String, profitRate1: Double?, investmentGrade: InvestmentGrade = InvestmentGrade.LEVEL_2_HIGH_RISK) {
|
|
|
|
|
scope.launch {
|
|
|
|
|
var basePrice = decision.currentPrice
|
|
|
|
@@ -219,33 +226,41 @@ object AutoTradingManager {
|
|
|
|
|
AutoTradingManager.addToReanalysis(RankingStock(mksc_shrn_iscd = stockCode,hts_kor_isnm = stockName))
|
|
|
|
|
TradingLogStore.addWatchLog(decision,"WATCH","매수 실패 : 최대 보유 종목 도달로 신규 매수 일시 중단 => 재분석 대기열에 추가")
|
|
|
|
|
} else if (KisSession.isAvailBuyTime(LocalTime.now())){
|
|
|
|
|
println("basePrice : $basePrice, oneTickLowerPrice : $oneTickLowerPrice, finalPrice : $finalPrice")
|
|
|
|
|
|
|
|
|
|
KisTradeService.postOrder(stockCode, orderQty, finalPrice.toLong().toString(), isBuy = true)
|
|
|
|
|
println("basePrice : $basePrice, oneTickLowerPrice : $oneTickLowerPrice, finalPrice : $finalPrice hasStocks : ${stockCode.contains(stockCode)}" )
|
|
|
|
|
var realOrderQty = orderQty
|
|
|
|
|
KisTradeService.postOrder(stockCode, realOrderQty, finalPrice.toLong().toString(), isBuy = true)
|
|
|
|
|
.onSuccess { realOrderNo ->
|
|
|
|
|
println("[${investmentGrade.displayName}] 주문 성공: $realOrderNo $stockCode $orderQty $finalPrice")
|
|
|
|
|
TradingLogStore.addLog(decision, "BUY", "[${investmentGrade.displayName}] 주문 성공: $realOrderNo")
|
|
|
|
|
TradingLogStore.addLog(
|
|
|
|
|
decision,
|
|
|
|
|
"BUY",
|
|
|
|
|
"[${investmentGrade.displayName}] 주문 성공: $realOrderNo"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
val sRate = -1.5
|
|
|
|
|
var tax = KisSession.config.getValues(ConfigIndex.TAX_INDEX)
|
|
|
|
|
val effectiveProfitRate = (profitRate1 ?: KisSession.config.getValues(ConfigIndex.PROFIT_INDEX)) + tax
|
|
|
|
|
val effectiveProfitRate =
|
|
|
|
|
(profitRate1 ?: KisSession.config.getValues(ConfigIndex.PROFIT_INDEX)) + tax
|
|
|
|
|
|
|
|
|
|
val calculatedTarget = MarketUtil.roundToTickSize(basePrice * (1 + effectiveProfitRate / 100.0))
|
|
|
|
|
val calculatedTarget =
|
|
|
|
|
MarketUtil.roundToTickSize(basePrice * (1 + effectiveProfitRate / 100.0))
|
|
|
|
|
val calculatedStop = MarketUtil.roundToTickSize(basePrice * (1 + sRate / 100.0))
|
|
|
|
|
val inputQty = orderQty.replace(",", "").toIntOrNull() ?: 0
|
|
|
|
|
|
|
|
|
|
DatabaseFactory.saveAutoTrade(AutoTradeItem(
|
|
|
|
|
orderNo = realOrderNo,
|
|
|
|
|
code = stockCode,
|
|
|
|
|
name = stockName,
|
|
|
|
|
quantity = inputQty,
|
|
|
|
|
profitRate = effectiveProfitRate,
|
|
|
|
|
stopLossRate = sRate,
|
|
|
|
|
targetPrice = calculatedTarget,
|
|
|
|
|
stopLossPrice = calculatedStop,
|
|
|
|
|
status = "PENDING_BUY",
|
|
|
|
|
isDomestic = true
|
|
|
|
|
))
|
|
|
|
|
DatabaseFactory.saveAutoTrade(
|
|
|
|
|
AutoTradeItem(
|
|
|
|
|
orderNo = realOrderNo,
|
|
|
|
|
code = stockCode,
|
|
|
|
|
name = stockName,
|
|
|
|
|
quantity = inputQty,
|
|
|
|
|
profitRate = effectiveProfitRate,
|
|
|
|
|
stopLossRate = sRate,
|
|
|
|
|
targetPrice = calculatedTarget,
|
|
|
|
|
stopLossPrice = calculatedStop,
|
|
|
|
|
status = "PENDING_BUY",
|
|
|
|
|
isDomestic = true
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
TradingReportManager.recordTradeDecision(
|
|
|
|
|
orderNo = realOrderNo,
|
|
|
|
@@ -260,16 +275,34 @@ object AutoTradingManager {
|
|
|
|
|
syncAndExecute(realOrderNo)
|
|
|
|
|
|
|
|
|
|
// 💡 [개선 3] 감시 설정 로그에도 등급 정보 노출
|
|
|
|
|
TradingLogStore.addLog(decision, "BUY", "[${investmentGrade.displayName}] 매수 및 감시 설정 완료 (목표 수익률: ${String.format("%.4f", effectiveProfitRate)}%): $realOrderNo")
|
|
|
|
|
TradingLogStore.addLog(
|
|
|
|
|
decision,
|
|
|
|
|
"BUY",
|
|
|
|
|
"[${investmentGrade.displayName}] 매수 및 감시 설정 완료 (목표 수익률: ${
|
|
|
|
|
String.format(
|
|
|
|
|
"%.4f",
|
|
|
|
|
effectiveProfitRate
|
|
|
|
|
)
|
|
|
|
|
}%): $realOrderNo"
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
.onFailure {
|
|
|
|
|
println("매수 실패: ${it.message} ${stockCode} $orderQty $finalPrice")
|
|
|
|
|
|
|
|
|
|
if (it.message?.contains("주문가능금액을 초과") == true) {
|
|
|
|
|
AutoTradingManager.addToReanalysis(RankingStock(mksc_shrn_iscd = stockCode,hts_kor_isnm = stockName))
|
|
|
|
|
TradingLogStore.addWatchLog(decision,"WATCH","${it.message ?: " 매수 실패"} => 재분석 대기열에 추가")
|
|
|
|
|
AutoTradingManager.addToReanalysis(
|
|
|
|
|
RankingStock(
|
|
|
|
|
mksc_shrn_iscd = stockCode,
|
|
|
|
|
hts_kor_isnm = stockName
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
TradingLogStore.addWatchLog(
|
|
|
|
|
decision,
|
|
|
|
|
"WATCH",
|
|
|
|
|
"${it.message ?: " 매수 실패"} => 재분석 대기열에 추가"
|
|
|
|
|
)
|
|
|
|
|
} else {
|
|
|
|
|
TradingLogStore.addLog(decision,"BUY",it.message ?: "매수 실패")
|
|
|
|
|
TradingLogStore.addLog(decision, "BUY", it.message ?: "매수 실패")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
@@ -342,7 +375,6 @@ object AutoTradingManager {
|
|
|
|
|
// ✅ 2. 매도 완료 시점 (실제 매도 체결가)
|
|
|
|
|
val actualSellPrice = execData.price.toDoubleOrNull() ?: 0.0
|
|
|
|
|
val actualSellQty = execData.qty.toIntOrNull() ?: dbItem.quantity
|
|
|
|
|
|
|
|
|
|
// 💡 매도 주문번호에 대해 '진짜 판 가격'을 기록
|
|
|
|
|
TradingReportManager.updateExecution(orderNo, actualSellPrice, actualSellQty)
|
|
|
|
|
|
|
|
|
@@ -534,7 +566,7 @@ object AutoTradingManager {
|
|
|
|
|
} else {
|
|
|
|
|
var errMsg = ""
|
|
|
|
|
var isSuccess = false
|
|
|
|
|
if (KisSession.tradeConfig.autoSellOrder
|
|
|
|
|
if (KisSession.tradeConfig.autoSellOrder
|
|
|
|
|
&& holding != null && holding.quantity.toInt() > 0
|
|
|
|
|
&& holding.availOrderCount.toInt() > 0
|
|
|
|
|
&& holding.profitRate.toDouble() <= -15.0
|
|
|
|
@@ -561,34 +593,34 @@ object AutoTradingManager {
|
|
|
|
|
"매수가 기준 (${holding.avgPrice.toDouble()} 3호가 위[${targetPrice}] 매도 주문 ${if (isSuccess) "성공" else "실패[${errMsg}]"}"
|
|
|
|
|
)
|
|
|
|
|
} 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)}")
|
|
|
|
|
val profit = holding.profitRate.toDouble()
|
|
|
|
|
var targetPrice = holding.currentPrice.toDouble()
|
|
|
|
|
targetPrice = MarketUtil.roundToTickSize(targetPrice + MarketUtil.getTickSize(targetPrice) * 3.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)}")
|
|
|
|
|
val profit = holding.profitRate.toDouble()
|
|
|
|
|
var targetPrice = holding.currentPrice.toDouble()
|
|
|
|
|
targetPrice = MarketUtil.roundToTickSize(targetPrice + MarketUtil.getTickSize(targetPrice) * 3.0)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tradeService.postOrder(
|
|
|
|
|
stockCode = holding.code,
|
|
|
|
|
qty = holding.availOrderCount,
|
|
|
|
|
price = targetPrice.toInt().toString(),
|
|
|
|
|
isBuy = false,
|
|
|
|
|
).onSuccess { newOrderNo ->
|
|
|
|
|
println("✅ [보유 주식 손절 처리] 수익률($profit%) -> ${holding.valuationProfitAmount} 손해 중이며 현제 손절 가이드에 적합함 시장가 매도.")
|
|
|
|
|
}.onFailure { err->
|
|
|
|
|
println("✅ [보유 주식 손절 처리] 실패 ${err.message}")
|
|
|
|
|
}
|
|
|
|
|
tradeService.postOrder(
|
|
|
|
|
stockCode = holding.code,
|
|
|
|
|
qty = holding.availOrderCount,
|
|
|
|
|
price = targetPrice.toInt().toString(),
|
|
|
|
|
isBuy = false,
|
|
|
|
|
).onSuccess { newOrderNo ->
|
|
|
|
|
println("✅ [보유 주식 손절 처리] 수익률($profit%) -> ${holding.valuationProfitAmount} 손해 중이며 현제 손절 가이드에 적합함 시장가 매도.")
|
|
|
|
|
}.onFailure { err->
|
|
|
|
|
println("✅ [보유 주식 손절 처리] 실패 ${err.message}")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TradingLogStore.addNotice(
|
|
|
|
|
"보유주식[${holding.name}]",
|
|
|
|
|
holding.code,
|
|
|
|
|
"수익률($profit%) -> ${holding.valuationProfitAmount} 손해 중이며 현제 손절 가이드에 적합함 시장가 매도."
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
TradingLogStore.addNotice(
|
|
|
|
|
"보유주식[${holding.name}]",
|
|
|
|
|
holding.code,
|
|
|
|
|
"수익률($profit%) -> ${holding.valuationProfitAmount} 손해 중이며 현제 손절 가이드에 적합함 시장가 매도."
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
analyzeDeepLossHoldingsAfterMarket(holding , true)
|
|
|
|
|
}
|
|
|
|
|
delay(200) // API 호출 부하 방지
|
|
|
|
@@ -914,7 +946,20 @@ object AutoTradingManager {
|
|
|
|
|
candidates.addAll(reanalysisList)
|
|
|
|
|
}
|
|
|
|
|
reanalysisList.clear()
|
|
|
|
|
remainingCandidates.addAll(candidates.filter { it.code !in myHoldings && it.code !in pendingStocks && it.code !in executionCache.values.map { it.code } && it.code !in failList}
|
|
|
|
|
if (KisSession.tradeConfig.lowerAveragePrice) {
|
|
|
|
|
currentBalance?.getHoldings()?.map {
|
|
|
|
|
if(!it.isTodayEntry && it.quantity.toInt() > 2) {
|
|
|
|
|
remainingCandidates.add(RankingStock(mksc_shrn_iscd = it.code, hts_kor_isnm = it.name))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
remainingCandidates.addAll(candidates.filter {
|
|
|
|
|
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 &&
|
|
|
|
|
it.code !in isSafetyBeltStockCodes
|
|
|
|
|
}
|
|
|
|
|
.distinctBy { it.code })
|
|
|
|
|
} else {
|
|
|
|
|
println("미확인 데이터 ${remainingCandidates.size}")
|
|
|
|
@@ -1001,6 +1046,7 @@ object AutoTradingManager {
|
|
|
|
|
}
|
|
|
|
|
if (isExecuted) { executionCountMap[timeKey] = currentCount + 1 }
|
|
|
|
|
if (now.hour >= 20) {
|
|
|
|
|
buysCodes.clear()
|
|
|
|
|
executionCountMap.clear()
|
|
|
|
|
noticeFilter.clear()
|
|
|
|
|
}
|
|
|
|
@@ -1034,7 +1080,8 @@ object AutoTradingManager {
|
|
|
|
|
this.stockName = stock.name
|
|
|
|
|
}, false)
|
|
|
|
|
|
|
|
|
|
val dailyData = tradeService.fetchPeriodChartData(stock.code, "D", true).getOrNull() ?: return@withTimeout
|
|
|
|
|
val dailyData =
|
|
|
|
|
tradeService.fetchPeriodChartData(stock.code, "D", true).getOrNull() ?: return@withTimeout
|
|
|
|
|
val today = dailyData.lastOrNull() ?: null
|
|
|
|
|
if (today == null) {
|
|
|
|
|
failList.add(stock.code)
|
|
|
|
@@ -1043,29 +1090,46 @@ object AutoTradingManager {
|
|
|
|
|
}
|
|
|
|
|
val currentPrice = today.stck_prpr.toDouble()
|
|
|
|
|
|
|
|
|
|
if (currentPrice > myCash || currentPrice > maxBudget || currentPrice > maxPrice || currentPrice < minPrice) {
|
|
|
|
|
if (currentPrice > myCash || currentPrice > maxBudget || currentPrice > maxPrice || currentPrice < minPrice) {
|
|
|
|
|
print("-> 가격 정책으로 제외 [1주:${currentPrice}, 자산:${myCash}, 최소 기준:${minPrice}, 최대 기준:${maxPrice}] | ")
|
|
|
|
|
return@withTimeout
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
println("🔍 [분석 진입] ${stock.name} (${LocalTime.now()})")
|
|
|
|
|
if (!isSafetyBeltStockCodes.contains(stock.code)) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
val analyzer = coroutineScope {
|
|
|
|
|
val min30 = async { tradeService.fetchChartData(stock.code, true).getOrDefault(emptyList()) }
|
|
|
|
|
val weekly = async { tradeService.fetchPeriodChartData(stock.code, "W", true).getOrDefault(emptyList()) }
|
|
|
|
|
val monthly = async { tradeService.fetchPeriodChartData(stock.code, "M", true).getOrDefault(emptyList()) }
|
|
|
|
|
TechnicalAnalyzer().apply {
|
|
|
|
|
this.daily = dailyData
|
|
|
|
|
this.min30 = min30.await()
|
|
|
|
|
this.weekly = weekly.await()
|
|
|
|
|
this.monthly = monthly.await()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
val analyzer = coroutineScope {
|
|
|
|
|
val min30 = async { tradeService.fetchChartData(stock.code, true).getOrDefault(emptyList()) }
|
|
|
|
|
delay(20)
|
|
|
|
|
val weekly =
|
|
|
|
|
async { tradeService.fetchPeriodChartData(stock.code, "W", true).getOrDefault(emptyList()) }
|
|
|
|
|
delay(20)
|
|
|
|
|
val monthly =
|
|
|
|
|
async { tradeService.fetchPeriodChartData(stock.code, "M", true).getOrDefault(emptyList()) }
|
|
|
|
|
delay(20)
|
|
|
|
|
TechnicalAnalyzer().apply {
|
|
|
|
|
this.daily = dailyData
|
|
|
|
|
delay(50)
|
|
|
|
|
this.min30 = min30.await()
|
|
|
|
|
delay(50)
|
|
|
|
|
this.weekly = weekly.await()
|
|
|
|
|
delay(50)
|
|
|
|
|
this.monthly = monthly.await()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (analyzer.isValid()) {
|
|
|
|
|
RagService.processStock(currentPrice, analyzer, stock.name, stock.code) { decision, isSuccess ->
|
|
|
|
|
callback(decision?.apply { this.currentPrice = currentPrice }, isSuccess)
|
|
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
println("✅ [분석 실패] ${stock.name} (${LocalTime.now()} 분석 데이터 정합성 -> ${analyzer.isValid()})")
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
println("재무 안정성 부족 (캐시)")
|
|
|
|
|
}
|
|
|
|
|
println("✅ [분석 종료] ${stock.name} (${LocalTime.now()})")
|
|
|
|
|
}
|
|
|
|
|