....
This commit is contained in:
parent
e94869b9e7
commit
1cca11edc4
@ -624,7 +624,7 @@ object TradingLogStore {
|
|||||||
val lastSentTime = noticeFilter[code.uppercase()]
|
val lastSentTime = noticeFilter[code.uppercase()]
|
||||||
|
|
||||||
// 기록이 없거나(처음 보냄), 마지막 발송 기준 30분이 지났다면
|
// 기록이 없거나(처음 보냄), 마지막 발송 기준 30분이 지났다면
|
||||||
if (lastSentTime == null || (current - lastSentTime > 1000 * 60 * 30L)) {
|
if (lastSentTime == null || (current - lastSentTime > (1000 * 60) * KisSession.tradeConfig.noticeGapTime)) {
|
||||||
isSendable = true
|
isSendable = true
|
||||||
noticeFilter[code.uppercase()] = current // 즉시 발송 시간 갱신하여 중복 방지
|
noticeFilter[code.uppercase()] = current // 즉시 발송 시간 갱신하여 중복 방지
|
||||||
}
|
}
|
||||||
|
|||||||
@ -255,6 +255,9 @@ class TradeConfig {
|
|||||||
var excuteCountOnMin : Int = 2
|
var excuteCountOnMin : Int = 2
|
||||||
var autoSellOrder : Boolean = false
|
var autoSellOrder : Boolean = false
|
||||||
var excuteMinCheck : Int = 2
|
var excuteMinCheck : Int = 2
|
||||||
|
var noticeGapTime : Int = 60
|
||||||
|
var lowerAveragePrice : Boolean = false
|
||||||
|
var lowerAverageStockCount : Int = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -250,7 +250,7 @@ object RagService {
|
|||||||
|
|
||||||
if (isSafetyBeltStockCodes.contains(stockCode)) {
|
if (isSafetyBeltStockCodes.contains(stockCode)) {
|
||||||
// 로그를 남기고 싶다면 주석 해제, 아니면 조용히 패스
|
// 로그를 남기고 싶다면 주석 해제, 아니면 조용히 패스
|
||||||
// logTime(stockName, "재무 미달 (캐시) 조기 종료", 0, System.currentTimeMillis() - totalStartTime)
|
println("재무 안정성 부족 (캐시)")
|
||||||
result(tradingDecision.apply { decision = "HOLD"; reason = "재무 안정성 부족 (캐시)" }, false)
|
result(tradingDecision.apply { decision = "HOLD"; reason = "재무 안정성 부족 (캐시)" }, false)
|
||||||
return@coroutineScope
|
return@coroutineScope
|
||||||
}
|
}
|
||||||
|
|||||||
@ -38,6 +38,7 @@ import network.KisAuthService
|
|||||||
import network.KisTradeService
|
import network.KisTradeService
|
||||||
import network.KisWebSocketManager
|
import network.KisWebSocketManager
|
||||||
import network.RagService
|
import network.RagService
|
||||||
|
import network.RagService.isSafetyBeltStockCodes
|
||||||
import network.StockUniverseLoader
|
import network.StockUniverseLoader
|
||||||
import report.SnapshotType
|
import report.SnapshotType
|
||||||
import report.TradingReportManager
|
import report.TradingReportManager
|
||||||
@ -108,9 +109,9 @@ object AutoTradingManager {
|
|||||||
if (KisSession.isAvailBuyTime(now) && isSuccess && completeTradingDecision != null) {
|
if (KisSession.isAvailBuyTime(now) && isSuccess && completeTradingDecision != null) {
|
||||||
val decision = completeTradingDecision
|
val decision = completeTradingDecision
|
||||||
|
|
||||||
|
println("${decision.stockName} ${decision.decision} ${buysCodes.contains(decision.stockCode) == false}")
|
||||||
// 1. 이미 AI가 결정한 decision과 confidence를 신뢰함
|
// 1. 이미 AI가 결정한 decision과 confidence를 신뢰함
|
||||||
if (decision.decision == "BUY") {
|
if (decision.decision == "BUY" && buysCodes.contains(decision.stockCode) == false) {
|
||||||
val minScore = KisSession.config.getValues(ConfigIndex.MIN_PURCHASE_SCORE_INDEX)
|
|
||||||
|
|
||||||
// AI가 이미 검증한 등급을 사용 (재계산 불필요)
|
// AI가 이미 검증한 등급을 사용 (재계산 불필요)
|
||||||
val grade = decision.investmentGrade ?: InvestmentGrade.LEVEL_1_SPECULATIVE
|
val grade = decision.investmentGrade ?: InvestmentGrade.LEVEL_1_SPECULATIVE
|
||||||
@ -118,8 +119,14 @@ object AutoTradingManager {
|
|||||||
// 2. 최종 매수 실행
|
// 2. 최종 매수 실행
|
||||||
val gradeRate = KisSession.config.getValues(grade.allocationRate)
|
val gradeRate = KisSession.config.getValues(grade.allocationRate)
|
||||||
val maxBudget = KisSession.config.getValues(ConfigIndex.MAX_BUDGET_INDEX) * gradeRate
|
val maxBudget = KisSession.config.getValues(ConfigIndex.MAX_BUDGET_INDEX) * gradeRate
|
||||||
val calculatedQty = (maxBudget / decision.currentPrice).toInt().coerceAtLeast(1)
|
|
||||||
TradingLogStore.addLog(decision,"BUY",decision.summary())
|
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(
|
excuteTrade(
|
||||||
decision = decision,
|
decision = decision,
|
||||||
orderQty = calculatedQty.toString(),
|
orderQty = calculatedQty.toString(),
|
||||||
@ -202,7 +209,7 @@ object AutoTradingManager {
|
|||||||
rawGrade
|
rawGrade
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
var buysCodes = arrayListOf<String>()
|
||||||
fun excuteTrade(decision: TradingDecision, orderQty: String, profitRate1: Double?, investmentGrade: InvestmentGrade = InvestmentGrade.LEVEL_2_HIGH_RISK) {
|
fun excuteTrade(decision: TradingDecision, orderQty: String, profitRate1: Double?, investmentGrade: InvestmentGrade = InvestmentGrade.LEVEL_2_HIGH_RISK) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
var basePrice = decision.currentPrice
|
var basePrice = decision.currentPrice
|
||||||
@ -219,22 +226,29 @@ object AutoTradingManager {
|
|||||||
AutoTradingManager.addToReanalysis(RankingStock(mksc_shrn_iscd = stockCode,hts_kor_isnm = stockName))
|
AutoTradingManager.addToReanalysis(RankingStock(mksc_shrn_iscd = stockCode,hts_kor_isnm = stockName))
|
||||||
TradingLogStore.addWatchLog(decision,"WATCH","매수 실패 : 최대 보유 종목 도달로 신규 매수 일시 중단 => 재분석 대기열에 추가")
|
TradingLogStore.addWatchLog(decision,"WATCH","매수 실패 : 최대 보유 종목 도달로 신규 매수 일시 중단 => 재분석 대기열에 추가")
|
||||||
} else if (KisSession.isAvailBuyTime(LocalTime.now())){
|
} else if (KisSession.isAvailBuyTime(LocalTime.now())){
|
||||||
println("basePrice : $basePrice, oneTickLowerPrice : $oneTickLowerPrice, finalPrice : $finalPrice")
|
println("basePrice : $basePrice, oneTickLowerPrice : $oneTickLowerPrice, finalPrice : $finalPrice hasStocks : ${stockCode.contains(stockCode)}" )
|
||||||
|
var realOrderQty = orderQty
|
||||||
KisTradeService.postOrder(stockCode, orderQty, finalPrice.toLong().toString(), isBuy = true)
|
KisTradeService.postOrder(stockCode, realOrderQty, finalPrice.toLong().toString(), isBuy = true)
|
||||||
.onSuccess { realOrderNo ->
|
.onSuccess { realOrderNo ->
|
||||||
println("[${investmentGrade.displayName}] 주문 성공: $realOrderNo $stockCode $orderQty $finalPrice")
|
println("[${investmentGrade.displayName}] 주문 성공: $realOrderNo $stockCode $orderQty $finalPrice")
|
||||||
TradingLogStore.addLog(decision, "BUY", "[${investmentGrade.displayName}] 주문 성공: $realOrderNo")
|
TradingLogStore.addLog(
|
||||||
|
decision,
|
||||||
|
"BUY",
|
||||||
|
"[${investmentGrade.displayName}] 주문 성공: $realOrderNo"
|
||||||
|
)
|
||||||
|
|
||||||
val sRate = -1.5
|
val sRate = -1.5
|
||||||
var tax = KisSession.config.getValues(ConfigIndex.TAX_INDEX)
|
var tax = KisSession.config.getValues(ConfigIndex.TAX_INDEX)
|
||||||
val effectiveProfitRate = (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 calculatedStop = MarketUtil.roundToTickSize(basePrice * (1 + sRate / 100.0))
|
||||||
val inputQty = orderQty.replace(",", "").toIntOrNull() ?: 0
|
val inputQty = orderQty.replace(",", "").toIntOrNull() ?: 0
|
||||||
|
|
||||||
DatabaseFactory.saveAutoTrade(AutoTradeItem(
|
DatabaseFactory.saveAutoTrade(
|
||||||
|
AutoTradeItem(
|
||||||
orderNo = realOrderNo,
|
orderNo = realOrderNo,
|
||||||
code = stockCode,
|
code = stockCode,
|
||||||
name = stockName,
|
name = stockName,
|
||||||
@ -245,7 +259,8 @@ object AutoTradingManager {
|
|||||||
stopLossPrice = calculatedStop,
|
stopLossPrice = calculatedStop,
|
||||||
status = "PENDING_BUY",
|
status = "PENDING_BUY",
|
||||||
isDomestic = true
|
isDomestic = true
|
||||||
))
|
)
|
||||||
|
)
|
||||||
|
|
||||||
TradingReportManager.recordTradeDecision(
|
TradingReportManager.recordTradeDecision(
|
||||||
orderNo = realOrderNo,
|
orderNo = realOrderNo,
|
||||||
@ -260,16 +275,34 @@ object AutoTradingManager {
|
|||||||
syncAndExecute(realOrderNo)
|
syncAndExecute(realOrderNo)
|
||||||
|
|
||||||
// 💡 [개선 3] 감시 설정 로그에도 등급 정보 노출
|
// 💡 [개선 3] 감시 설정 로그에도 등급 정보 노출
|
||||||
TradingLogStore.addLog(decision, "BUY", "[${investmentGrade.displayName}] 매수 및 감시 설정 완료 (목표 수익률: ${String.format("%.4f", effectiveProfitRate)}%): $realOrderNo")
|
TradingLogStore.addLog(
|
||||||
|
decision,
|
||||||
|
"BUY",
|
||||||
|
"[${investmentGrade.displayName}] 매수 및 감시 설정 완료 (목표 수익률: ${
|
||||||
|
String.format(
|
||||||
|
"%.4f",
|
||||||
|
effectiveProfitRate
|
||||||
|
)
|
||||||
|
}%): $realOrderNo"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
.onFailure {
|
.onFailure {
|
||||||
println("매수 실패: ${it.message} ${stockCode} $orderQty $finalPrice")
|
println("매수 실패: ${it.message} ${stockCode} $orderQty $finalPrice")
|
||||||
|
|
||||||
if (it.message?.contains("주문가능금액을 초과") == true) {
|
if (it.message?.contains("주문가능금액을 초과") == true) {
|
||||||
AutoTradingManager.addToReanalysis(RankingStock(mksc_shrn_iscd = stockCode,hts_kor_isnm = stockName))
|
AutoTradingManager.addToReanalysis(
|
||||||
TradingLogStore.addWatchLog(decision,"WATCH","${it.message ?: " 매수 실패"} => 재분석 대기열에 추가")
|
RankingStock(
|
||||||
|
mksc_shrn_iscd = stockCode,
|
||||||
|
hts_kor_isnm = stockName
|
||||||
|
)
|
||||||
|
)
|
||||||
|
TradingLogStore.addWatchLog(
|
||||||
|
decision,
|
||||||
|
"WATCH",
|
||||||
|
"${it.message ?: " 매수 실패"} => 재분석 대기열에 추가"
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
TradingLogStore.addLog(decision,"BUY",it.message ?: "매수 실패")
|
TradingLogStore.addLog(decision, "BUY", it.message ?: "매수 실패")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -342,7 +375,6 @@ object AutoTradingManager {
|
|||||||
// ✅ 2. 매도 완료 시점 (실제 매도 체결가)
|
// ✅ 2. 매도 완료 시점 (실제 매도 체결가)
|
||||||
val actualSellPrice = execData.price.toDoubleOrNull() ?: 0.0
|
val actualSellPrice = execData.price.toDoubleOrNull() ?: 0.0
|
||||||
val actualSellQty = execData.qty.toIntOrNull() ?: dbItem.quantity
|
val actualSellQty = execData.qty.toIntOrNull() ?: dbItem.quantity
|
||||||
|
|
||||||
// 💡 매도 주문번호에 대해 '진짜 판 가격'을 기록
|
// 💡 매도 주문번호에 대해 '진짜 판 가격'을 기록
|
||||||
TradingReportManager.updateExecution(orderNo, actualSellPrice, actualSellQty)
|
TradingReportManager.updateExecution(orderNo, actualSellPrice, actualSellQty)
|
||||||
|
|
||||||
@ -914,7 +946,20 @@ object AutoTradingManager {
|
|||||||
candidates.addAll(reanalysisList)
|
candidates.addAll(reanalysisList)
|
||||||
}
|
}
|
||||||
reanalysisList.clear()
|
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 })
|
.distinctBy { it.code })
|
||||||
} else {
|
} else {
|
||||||
println("미확인 데이터 ${remainingCandidates.size}")
|
println("미확인 데이터 ${remainingCandidates.size}")
|
||||||
@ -1001,6 +1046,7 @@ object AutoTradingManager {
|
|||||||
}
|
}
|
||||||
if (isExecuted) { executionCountMap[timeKey] = currentCount + 1 }
|
if (isExecuted) { executionCountMap[timeKey] = currentCount + 1 }
|
||||||
if (now.hour >= 20) {
|
if (now.hour >= 20) {
|
||||||
|
buysCodes.clear()
|
||||||
executionCountMap.clear()
|
executionCountMap.clear()
|
||||||
noticeFilter.clear()
|
noticeFilter.clear()
|
||||||
}
|
}
|
||||||
@ -1034,7 +1080,8 @@ object AutoTradingManager {
|
|||||||
this.stockName = stock.name
|
this.stockName = stock.name
|
||||||
}, false)
|
}, 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
|
val today = dailyData.lastOrNull() ?: null
|
||||||
if (today == null) {
|
if (today == null) {
|
||||||
failList.add(stock.code)
|
failList.add(stock.code)
|
||||||
@ -1049,23 +1096,40 @@ object AutoTradingManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
println("🔍 [분석 진입] ${stock.name} (${LocalTime.now()})")
|
println("🔍 [분석 진입] ${stock.name} (${LocalTime.now()})")
|
||||||
|
if (!isSafetyBeltStockCodes.contains(stock.code)) {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
val analyzer = coroutineScope {
|
val analyzer = coroutineScope {
|
||||||
val min30 = async { tradeService.fetchChartData(stock.code, true).getOrDefault(emptyList()) }
|
val min30 = async { tradeService.fetchChartData(stock.code, true).getOrDefault(emptyList()) }
|
||||||
val weekly = async { tradeService.fetchPeriodChartData(stock.code, "W", true).getOrDefault(emptyList()) }
|
delay(20)
|
||||||
val monthly = async { tradeService.fetchPeriodChartData(stock.code, "M", true).getOrDefault(emptyList()) }
|
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 {
|
TechnicalAnalyzer().apply {
|
||||||
this.daily = dailyData
|
this.daily = dailyData
|
||||||
|
delay(50)
|
||||||
this.min30 = min30.await()
|
this.min30 = min30.await()
|
||||||
|
delay(50)
|
||||||
this.weekly = weekly.await()
|
this.weekly = weekly.await()
|
||||||
|
delay(50)
|
||||||
this.monthly = monthly.await()
|
this.monthly = monthly.await()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (analyzer.isValid()) {
|
if (analyzer.isValid()) {
|
||||||
|
println("✅ [분석 시작] ${stock.name} (${LocalTime.now()} 분석 데이터 정합성 -> ${analyzer.isValid()})")
|
||||||
RagService.processStock(currentPrice, analyzer, stock.name, stock.code) { decision, isSuccess ->
|
RagService.processStock(currentPrice, analyzer, stock.name, stock.code) { decision, isSuccess ->
|
||||||
callback(decision?.apply { this.currentPrice = currentPrice }, isSuccess)
|
callback(decision?.apply { this.currentPrice = currentPrice }, isSuccess)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
println("✅ [분석 실패] ${stock.name} (${LocalTime.now()} 분석 데이터 정합성 -> ${analyzer.isValid()})")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
println("재무 안정성 부족 (캐시)")
|
||||||
}
|
}
|
||||||
println("✅ [분석 종료] ${stock.name} (${LocalTime.now()})")
|
println("✅ [분석 종료] ${stock.name} (${LocalTime.now()})")
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user