....
This commit is contained in:
parent
e94869b9e7
commit
1cca11edc4
@ -624,7 +624,7 @@ object TradingLogStore {
|
||||
val lastSentTime = noticeFilter[code.uppercase()]
|
||||
|
||||
// 기록이 없거나(처음 보냄), 마지막 발송 기준 30분이 지났다면
|
||||
if (lastSentTime == null || (current - lastSentTime > 1000 * 60 * 30L)) {
|
||||
if (lastSentTime == null || (current - lastSentTime > (1000 * 60) * KisSession.tradeConfig.noticeGapTime)) {
|
||||
isSendable = true
|
||||
noticeFilter[code.uppercase()] = current // 즉시 발송 시간 갱신하여 중복 방지
|
||||
}
|
||||
|
||||
@ -255,6 +255,9 @@ class TradeConfig {
|
||||
var excuteCountOnMin : Int = 2
|
||||
var autoSellOrder : Boolean = false
|
||||
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)) {
|
||||
// 로그를 남기고 싶다면 주석 해제, 아니면 조용히 패스
|
||||
// logTime(stockName, "재무 미달 (캐시) 조기 종료", 0, System.currentTimeMillis() - totalStartTime)
|
||||
println("재무 안정성 부족 (캐시)")
|
||||
result(tradingDecision.apply { decision = "HOLD"; reason = "재무 안정성 부족 (캐시)" }, false)
|
||||
return@coroutineScope
|
||||
}
|
||||
|
||||
@ -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,22 +226,29 @@ 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(
|
||||
DatabaseFactory.saveAutoTrade(
|
||||
AutoTradeItem(
|
||||
orderNo = realOrderNo,
|
||||
code = stockCode,
|
||||
name = stockName,
|
||||
@ -245,7 +259,8 @@ object AutoTradingManager {
|
||||
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)
|
||||
|
||||
@ -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)
|
||||
@ -1049,23 +1096,40 @@ object AutoTradingManager {
|
||||
}
|
||||
|
||||
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()) }
|
||||
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()) {
|
||||
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()})")
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user