....
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user