163 lines
6.8 KiB
Kotlin
163 lines
6.8 KiB
Kotlin
|
|
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.LocalTime
|
||
|
|
|
||
|
|
// service/AutoTradingManager.kt
|
||
|
|
object AutoTradingManager {
|
||
|
|
private val scope = CoroutineScope(Dispatchers.Default)
|
||
|
|
val targetStocks = mutableListOf<String>()
|
||
|
|
|
||
|
|
fun addStock(stockCode : String, result :(String, Boolean)->Unit) {
|
||
|
|
targetStocks.add(stockCode)
|
||
|
|
startTradingLoop(result)
|
||
|
|
}
|
||
|
|
|
||
|
|
fun startTradingLoop(result :(String, Boolean)->Unit) {
|
||
|
|
scope.launch {
|
||
|
|
println("🚀 10분 주기 자동 분석 및 매매 시작: ${LocalTime.now()}")
|
||
|
|
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)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
delay(10 * 60 * 1000) // 10분 대기
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
private fun executeOrder(code: String, type: String) {
|
||
|
|
// 실제 증권사 API 호출 로직 (한국투자증권, 키움 등)
|
||
|
|
println("🔥 [주문 집행] $code $type 완료")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
object TechnicalAnalyzer {
|
||
|
|
var monthly: List<CandleData> = emptyList()
|
||
|
|
var weekly: List<CandleData> = emptyList()
|
||
|
|
var daily: List<CandleData> = emptyList()
|
||
|
|
var min30: List<CandleData> = emptyList()
|
||
|
|
fun generateComprehensiveReport(): String {
|
||
|
|
// [1] 단기 에너지 지표 계산 (최근 30분봉 기준)
|
||
|
|
val obv = calculateOBV(min30)
|
||
|
|
val mfi = calculateMFI(min30, 14)
|
||
|
|
val adLine = calculateADLine(min30)
|
||
|
|
|
||
|
|
// [2] 시계열별 가격 변동 및 추세 요약
|
||
|
|
val m10 = min30.takeLast(10)
|
||
|
|
val change10 = calculateChange(m10)
|
||
|
|
val change30 = calculateChange(min30)
|
||
|
|
val changeDaily = calculateChange(daily.takeLast(2)) // 전일 대비
|
||
|
|
|
||
|
|
// [3] 이평선 및 가격 위치
|
||
|
|
val ma5 = m10.takeLast(5).map { it.stck_prpr.toDouble() }.average()
|
||
|
|
val currentPrice = min30.last().stck_prpr.toDouble()
|
||
|
|
|
||
|
|
// [4] 거래량 강도
|
||
|
|
val avgVol30 = min30.map { it.cntg_vol.toLong() }.average()
|
||
|
|
val recentVol5 = m10.takeLast(5).map { it.cntg_vol.toLong() }.average()
|
||
|
|
val volStrength = if (avgVol30 > 0) recentVol5 / avgVol30 else 1.0
|
||
|
|
|
||
|
|
return """
|
||
|
|
[종합 시계열 및 에너지 분석 보고서]
|
||
|
|
|
||
|
|
1. 가격 및 추세 현황
|
||
|
|
- 월봉/주봉 위치: ${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 "하단 위치"}
|
||
|
|
|
||
|
|
2. 자금 흐름 및 에너지 지표
|
||
|
|
- OBV (누적 거래량 에너지): ${ "%.0f".format(obv) } (${if(obv > 0) "누적 매수 우위" else "누적 매도 우위"})
|
||
|
|
- MFI (자금 유입 지수): ${ "%.1f".format(mfi) } (과매수 기준: 80 / 과매도 기준: 20)
|
||
|
|
- A/D (누적 분산 라인): ${ "%.0f".format(adLine) } (종가 형성 위치와 거래량 결합 수치)
|
||
|
|
- 거래량 강도: 최근 5분 평균이 30분 평균의 ${ "%.1f".format(volStrength) }배 수준
|
||
|
|
|
||
|
|
3. 가격 변동 범위
|
||
|
|
- 30분봉 최고가: ${min30.maxOf { it.stck_hgpr.toInt() }}
|
||
|
|
- 30분봉 최저가: ${min30.minOf { it.stck_lwpr.toInt() }}
|
||
|
|
- RSI(14): ${ "%.1f".format(calculateRSI(min30)) }
|
||
|
|
""".trimIndent()
|
||
|
|
}
|
||
|
|
|
||
|
|
private fun calculateChange(list: List<CandleData>): Double {
|
||
|
|
val start = list.first().stck_oprc.toDouble()
|
||
|
|
val end = list.last().stck_prpr.toDouble()
|
||
|
|
return if (start != 0.0) ((end - start) / start) * 100 else 0.0
|
||
|
|
}
|
||
|
|
|
||
|
|
private fun calculateRSI(list: List<CandleData>): Double {
|
||
|
|
if (list.size < 2) return 50.0
|
||
|
|
var gains = 0.0
|
||
|
|
var losses = 0.0
|
||
|
|
for (i in 1 until list.size) {
|
||
|
|
val diff = list[i].stck_prpr.toDouble() - list[i - 1].stck_prpr.toDouble()
|
||
|
|
if (diff > 0) gains += diff else losses -= diff
|
||
|
|
}
|
||
|
|
return if (gains + losses == 0.0) 50.0 else (gains / (gains + losses)) * 100
|
||
|
|
}
|
||
|
|
|
||
|
|
fun calculateOBV(candles: List<CandleData>): Double {
|
||
|
|
var obv = 0.0
|
||
|
|
for (i in 1 until candles.size) {
|
||
|
|
val prevClose = candles[i - 1].stck_prpr.toDouble()
|
||
|
|
val currClose = candles[i].stck_prpr.toDouble()
|
||
|
|
val currVol = candles[i].cntg_vol.toDouble()
|
||
|
|
|
||
|
|
when {
|
||
|
|
currClose > prevClose -> obv += currVol
|
||
|
|
currClose < prevClose -> obv -= currVol
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return obv
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* MFI (Money Flow Index) 계산 (기간: 보통 14일)
|
||
|
|
*/
|
||
|
|
fun calculateMFI(candles: List<CandleData>, period: Int = 14): Double {
|
||
|
|
val subList = candles.takeLast(period + 1)
|
||
|
|
var posFlow = 0.0
|
||
|
|
var negFlow = 0.0
|
||
|
|
|
||
|
|
for (i in 1 until subList.size) {
|
||
|
|
val prevTypical = (subList[i-1].stck_hgpr.toDouble() + subList[i-1].stck_lwpr.toDouble() + subList[i-1].stck_prpr.toDouble()) / 3
|
||
|
|
val currTypical = (subList[i].stck_hgpr.toDouble() + subList[i].stck_lwpr.toDouble() + subList[i].stck_prpr.toDouble()) / 3
|
||
|
|
val moneyFlow = currTypical * subList[i].cntg_vol.toDouble()
|
||
|
|
|
||
|
|
if (currTypical > prevTypical) posFlow += moneyFlow
|
||
|
|
else if (currTypical < prevTypical) negFlow += moneyFlow
|
||
|
|
}
|
||
|
|
|
||
|
|
return if (negFlow == 0.0) 100.0 else 100 - (100 / (1+ (posFlow / negFlow)))
|
||
|
|
}
|
||
|
|
|
||
|
|
private fun calculateADLine(candles: List<CandleData>): Double {
|
||
|
|
var ad = 0.0
|
||
|
|
candles.forEach {
|
||
|
|
val high = it.stck_hgpr.toDouble(); val low = it.stck_lwpr.toDouble(); val close = it.stck_prpr.toDouble()
|
||
|
|
val mfv = if (high != low) ((close - low) - (high - close)) / (high - low) else 0.0
|
||
|
|
ad += mfv * it.cntg_vol.toDouble()
|
||
|
|
}
|
||
|
|
return ad
|
||
|
|
}
|
||
|
|
|
||
|
|
}
|