This commit is contained in:
2026-01-23 17:05:09 +09:00
parent ac50737ea8
commit d89d793efa
16 changed files with 665 additions and 256 deletions
+80 -44
View File
@@ -3,13 +3,8 @@ package service
import TradingDecision
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import model.CandleData
import network.KisTradeService
import network.NewsService
import java.time.LocalDateTime
import java.time.LocalTime
import java.time.ZoneId
@@ -18,31 +13,34 @@ import kotlin.collections.List
import kotlin.math.*
// service/AutoTradingManager.kt
typealias TradingDecisionCallback = (TradingDecision?, Boolean)->Unit
object AutoTradingManager {
private val scope = CoroutineScope(Dispatchers.Default)
val targetStocks = mutableListOf<String>()
val targetStocks = mutableListOf<Pair<String, String>>()
fun addStock(stockCode : String, result :(String, Boolean)->Unit) {
targetStocks.add(stockCode)
startTradingLoop(result)
fun addStock(stockName : String,stockCode : String, result :TradingDecisionCallback) {
targetStocks.add(Pair(stockName, stockCode))
startTradingLoop(stockName,stockCode,result)
}
fun startTradingLoop(result :(String, Boolean)->Unit) {
fun startTradingLoop(stockName : String, stockCode : String, result :TradingDecisionCallback) {
scope.launch {
println("🚀 10분 주기 자동 분석 및 매매 시작: ${LocalTime.now()}")
targetStocks.forEach { stockCode ->
// targetStocks.forEach { stockCode ->
launch { // 종목별 병렬 분석 (M3 Pro 파워 활용)
RagService.processStock(stockCode,result) {code ,decision ->
when (decision?.decision) {
"BUY" -> if (decision.confidence > 70) executeOrder(stockCode, "매수")
"SELL" -> executeOrder(stockCode, "매도")
else -> println("[$stockCode] 관망 유지: ${decision?.reason}")
}
result(decision.toString(),true)
}
RagService.processStock(stockName, stockCode,result)
// {decision,b ->
//// when (decision?.decision) {
//// "BUY" -> if (decision.confidence > 70) executeOrder(stockCode, "매수")
//// "SELL" -> executeOrder(stockCode, "매도")
//// else -> println("[$stockCode] 관망 유지: ${decision?.reason}")
//// }
// result(decision,b)
// }
}
}
delay(10 * 60 * 1000) // 10분 대기
// }
// targetStocks.re
// delay(10 * 60 * 1000) // 10분 대기
}
}
@@ -112,7 +110,7 @@ object TechnicalAnalyzer {
// [3] 이평선 및 가격 위치
val ma5 = m10.takeLast(5).map { it.stck_prpr.toDouble() }.average()
val currentPrice = min30.last().stck_prpr.toDouble()
val signal = ScalpingAnalyzer().analyze(min30.toScalpingList())
val signal = ScalpingAnalyzer().analyze(min30.toScalpingList(),isDailyBullish())
// [4] 거래량 강도
val avgVol30 = min30.map { it.cntg_vol.toLong() }.average()
val recentVol5 = m10.takeLast(5).map { it.cntg_vol.toLong() }.average()
@@ -121,30 +119,29 @@ object TechnicalAnalyzer {
val stochK = calculateStochastic(min30)
val priceRange30 = min30.maxOf { it.stck_hgpr.toDouble() } - min30.minOf { it.stck_lwpr.toDouble() }
return """
[초단기 기술적 스켈핑 분석]
- 종합 스코어: ${signal.compositeScore} / 100
- 매수 신호 발생 여부: ${if (signal.buySignal) "YES" else "NO"}
- 성공 확률 예측: ${signal.successProbPct}%
- 위험 등급: ${signal.riskLevel} (ATR 변동성 기반)
- RSI: ${"%.1f".format(signal.rsi)} / 거래량 비율: ${"%.1f".format(signal.volRatio)}
- 권장 가격: 손절가(${signal.suggestedSlPrice.toInt()}원), 익절가(${signal.suggestedTpPrice.toInt()}원)
- 초/단타 종합 스코어: ${signal.compositeScore} / 100
- 초/단타 매수 신호 발생 여부: ${if (signal.buySignal) "YES" else "NO"}
- 초/단타 성공 확률 예측: ${signal.successProbPct}%
- 초/단타 위험 등급: ${signal.riskLevel} (ATR 변동성 기반)
- 초/단타 RSI: ${"%.1f".format(signal.rsi)} / 거래량 비율: ${"%.1f".format(signal.volRatio)}
- 초/단타 권장 가격: 손절가(${signal.suggestedSlPrice.toInt()}원), 익절가(${signal.suggestedTpPrice.toInt()}원)
- 월봉/주봉 위치: ${if(calculateChange(monthly) > 0) "장기 상승" else "장기 하락"} / ${if(calculateChange(weekly) > 0) "중기 상승" else "중기 하락"}
- 일봉 대비: ${ "%.2f".format(changeDaily) }% 변동
- 30분 대비: ${ "%.2f".format(change30) }% 변동
- 10분 대비: ${ "%.2f".format(change10) }% 변동
- 이평선 상태: 현재가(${currentPrice.toInt()}) vs MA5(${ma5.toInt()}) -> ${if(currentPrice > ma5) "상단 위치" else "하단 위치"}
- OBV (누적 거래량 에너지): ${ "%.0f".format(obv) } (${if(obv > 0) "누적 매수 우위" else "누적 매도 우위"})
- MFI (자금 유입 지수): ${ "%.1f".format(mfi) } (과매수 기준: 80 / 과매도 기준: 20)
- A/D (누적 분산 라인): ${ "%.0f".format(adLine) } (종가 형성 위치와 거래량 결합 수치)
- OBV (누적 거래량 에너지): ${ "%.0f".format(obv) }
- MFI (자금 유입 지수): ${ "%.1f".format(mfi) }
- A/D (누적 분산 라인): ${ "%.0f".format(adLine) }
- 거래량 강도: 최근 5분 평균이 30분 평균의 ${ "%.1f".format(volStrength) }배 수준
- ATR (평균 변동폭): ${"%.0f".format(atr)}(최근 캔들 하나가 평균적으로 움직이는 크기)
- 30분 내 최대 진폭: ${"%.0f".format(priceRange30)}(최고가-최저가 차이)
- 스토캐스틱(%K): ${"%.1f".format(stochK)} (100에 가까울수록 최근 파동의 고점, 0에 가까울수록 저점)
- 변동성 강도: 현재 진폭이 ATR 대비 ${"%.1f".format(priceRange30 / atr)}배 수준으로 전개 중
- ATR (평균 변동폭): ${"%.0f".format(atr)}
- 30분 내 최대 진폭: ${"%.0f".format(priceRange30)}
- 스토캐스틱(%K): ${"%.1f".format(stochK)}
- 변동성 강도: 현재 진폭이 ATR 대비 ${"%.1f".format(priceRange30 / atr)}배 수준
- 30분봉 최고가: ${min30.maxOf { it.stck_hgpr.toInt() }}
- 30분봉 최저가: ${min30.minOf { it.stck_lwpr.toInt() }}
- RSI(14): ${ "%.1f".format(calculateRSI(min30)) }
""".trimIndent()
""".trimIndent()
}
/**
@@ -194,6 +191,25 @@ object TechnicalAnalyzer {
return if (gains + losses == 0.0) 50.0 else (gains / (gains + losses)) * 100
}
fun isDailyBullish(): Boolean {
if (daily.size < 20) return true // 데이터 부족 시 보수적으로 true 혹은 예외처리
val currentPrice = daily.last().stck_prpr.toDouble()
// 1. MA20 (한 달 생명선) 계산
val ma20 = daily.takeLast(20).map { it.stck_prpr.toDouble() }.average()
// 2. MA5 (단기 가속도) 계산
val ma5 = daily.takeLast(5).map { it.stck_prpr.toDouble() }.average()
// 3. 방향성 (어제 MA5 vs 오늘 MA5)
val prevMa5 = daily.dropLast(1).takeLast(5).map { it.stck_prpr.toDouble() }.average()
val isMa5Rising = ma5 > prevMa5
// [최종 판별]: 현재가가 생명선 위에 있고, 단기 이평선이 고개를 들었을 때만 'Bull(상승)'로 간주
return currentPrice > ma20 && isMa5Rising
}
fun calculateOBV(candles: List<CandleData>): Double {
var obv = 0.0
for (i in 1 until candles.size) {
@@ -298,7 +314,7 @@ class ScalpingAnalyzer {
return Triple(upper, sma, lower)
}
fun analyze(candles: List<Candle>): ScalpingSignalModel {
fun analyze(candles: List<Candle>, isDailyBullish: Boolean): ScalpingSignalModel {
if (candles.size < SMA_LONG) throw IllegalArgumentException("최소 20봉 필요")
val closes = candles.map { it.close }
@@ -323,16 +339,34 @@ class ScalpingAnalyzer {
(currentClose - bbLower.last()) / (bbUpper.last() - bbLower.last())
} else 0.5
// 신호 조건
val maBull = currentClose > sma10Now && sma10Now > sma20Now
val nearHigh = candles.takeLast(6).dropLast(1).maxOf { it.high }
val isBreakout = currentClose > nearHigh
// [추가] 2. 캔들 패턴: 망치형/역망치형 등 꼬리 분석 (하단 지지력 확인)
val bodySize = abs(current.close - current.open)
val lowerShadow = minOf(current.close, current.open) - current.low
val isBottomSupport = lowerShadow > bodySize * 1.5 // 밑꼬리가 몸통보다 긴 경우
// 신호 조건 고도화
// 일봉 추세(dailyTrend)가 살아있고, 전고점을 돌파(isBreakout)할 때 더 높은 점수
// val maBull = currentClose > sma10Now && sma10Now > sma20Now
val rsiBull = rsiNow > RSI_THRESHOLD
val volSurge = volRatioNow > VOL_SURGE_THRESHOLD
val bbGood = bbPos > BB_LOWER_POS && bbPos < BB_UPPER_POS
val buySignal = maBull && rsiBull && volSurge && bbGood
val maBull = currentClose > sma10Now && sma10Now > sma20Now
val buySignal = maBull && rsiBull && volSurge && bbGood && isBreakout
// val buySignal = maBull && rsiBull && volSurge && bbGood
// 종합 스코어 (가중: MA 30%, RSI 20%, Vol 30%, BB 20%)
val score = (if (maBull) 30 else 0) + (if (rsiBull) 20 else 0) +
(minOf((volRatioNow - 1.0) * 30, 30.0)).toInt() + (if (bbGood) 20 else 0)
val score = (if (maBull) 25 else 0) +
(if (rsiBull) 15 else 0) +
(if (isBreakout) 20 else 0) + // 돌파 에너지 가중치
(minOf((volRatioNow - 1.0) * 20, 20.0)).toInt() +
(if (bbGood) 10 else 0) +
(if (isDailyBullish) 10 else 0) // 단타/장기 정렬 점수
// 위험도 (ATR proxy)
val returns = closes.mapIndexed { i, c -> if (i > 0) (c - closes[i-1])/closes[i-1] * 100 else 0.0 }
@@ -351,6 +385,8 @@ class ScalpingAnalyzer {
val tpPrice = currentClose * (1 + DEFAULT_TP_PCT / 100)
val rrRatio = abs(DEFAULT_TP_PCT / DEFAULT_SL_PCT)
return ScalpingSignalModel(
currentPrice = currentClose,
buySignal = buySignal,