Compare commits
12
Commits
302802a53d
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3827698b6f | ||
|
|
c84f4f0cd1 | ||
|
|
a53beb1e18 | ||
|
|
15793d1852 | ||
|
|
84f2202f17 | ||
|
|
1a2fe8b6e6 | ||
|
|
0e241205ee | ||
|
|
8e567f5b69 | ||
|
|
442cdf0877 | ||
|
|
3998cf159a | ||
|
|
5e90ee39bc | ||
|
|
5882f07e42 |
@@ -254,31 +254,61 @@ class TechnicalAnalyzer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 억울한 HOLD를 막아주는 유연한 과열 판별 로직
|
* 억울한 HOLD를 막아주는 유연한 과열 판별 로직 (주가 및 변동성 기반 세분화)
|
||||||
*/
|
*/
|
||||||
fun isOverheatedStock(): Boolean {
|
fun isOverheatedStock(): Boolean {
|
||||||
if (daily.size < 20) return false
|
if (daily.size < 20) return false
|
||||||
val currentPrice = daily.last().stck_prpr.toDouble()
|
val currentPrice = daily.last().stck_prpr.toDouble()
|
||||||
|
|
||||||
// 1. 일봉 20일선 이격도
|
// 1. 기본 지표 계산
|
||||||
val ma20Daily = daily.takeLast(20).map { it.stck_prpr.toDouble() }.average()
|
val ma20Daily = daily.takeLast(20).map { it.stck_prpr.toDouble() }.average()
|
||||||
val disparityDaily = (currentPrice / ma20Daily) * 100
|
val disparityDaily = (currentPrice / ma20Daily) * 100
|
||||||
|
|
||||||
// 2. 일봉 RSI (단순 이격도 외의 과열 여부 확인)
|
|
||||||
val rsiDaily = calculateRSI(daily)
|
val rsiDaily = calculateRSI(daily)
|
||||||
|
|
||||||
// 3. 초단기(최근 10분) 순간 급등 꼭지 확인 (min30 재활용)
|
// 2. 가격대별(시가총액 및 호가단위 대용) 동적 임계값 세팅
|
||||||
|
val maxDisparity: Double // 절대 진입 금지 (초과열)
|
||||||
|
val warningDisparity: Double // 경고 수준 (보조지표 결합 시 차단)
|
||||||
|
val microPumpThreshold: Double // 10분 내 단기 펌핑 기준
|
||||||
|
|
||||||
|
when {
|
||||||
|
currentPrice >= 50000.0 -> {
|
||||||
|
// [대형주/우량주] 무거워서 20일선 대비 15%만 떠도 역사적 과열
|
||||||
|
maxDisparity = 115.0
|
||||||
|
warningDisparity = 110.0
|
||||||
|
microPumpThreshold = 103.0 // 10분 만에 3% 급등하면 꼭지
|
||||||
|
}
|
||||||
|
currentPrice in 5000.0..49999.0 -> {
|
||||||
|
// [일반 중소형주] 표준적인 변동성
|
||||||
|
maxDisparity = 125.0
|
||||||
|
warningDisparity = 115.0
|
||||||
|
microPumpThreshold = 105.0 // 10분 만에 5% 급등
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
// [소형주/동전주] 가벼워서 상한가 한 방에 130% 쉽게 도달
|
||||||
|
maxDisparity = 135.0
|
||||||
|
warningDisparity = 120.0
|
||||||
|
microPumpThreshold = 107.0 // 10분 만에 7% 급등
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 종목 고유 변동성(ATR)을 통한 임계값 미세 보정 (Smart Adjustment)
|
||||||
|
// 평소 하루에 2~3% 움직이는 얌전한 주식이 갑자기 튀면 임계값을 더 빡빡하게 죔
|
||||||
|
val atrPct = (calculateATR(daily) / currentPrice) * 100.0
|
||||||
|
val adjustedMaxDisparity = if (atrPct < 3.0) maxDisparity * 0.96 else maxDisparity
|
||||||
|
val adjustedWarnDisparity = if (atrPct < 3.0) warningDisparity * 0.97 else warningDisparity
|
||||||
|
|
||||||
|
// 4. 초단기(최근 10분) 순간 급등 꼭지 확인 (min30 재활용)
|
||||||
var isMicroOverheated = false
|
var isMicroOverheated = false
|
||||||
if (min30.size >= 10) {
|
if (min30.size >= 10) {
|
||||||
val ma10Min = min30.takeLast(10).map { it.stck_prpr.toDouble() }.average()
|
val ma10Min = min30.takeLast(10).map { it.stck_prpr.toDouble() }.average()
|
||||||
val disparityMin = (currentPrice / ma10Min) * 100
|
val disparityMin = (currentPrice / ma10Min) * 100
|
||||||
isMicroOverheated = disparityMin > 105.0 // 10분 평균가 대비 순간적으로 5% 이상 폭등 시
|
isMicroOverheated = disparityMin > microPumpThreshold
|
||||||
}
|
}
|
||||||
|
|
||||||
// 단순히 이격도 115%라고 막는 것이 아니라 3가지 깐깐한 조건 중 하나라도 충족될 때만 과열 판정
|
// 5. 최종 세분화 판정 로직
|
||||||
return disparityDaily > 130.0 || // (A) 역대급 폭등 상태
|
return disparityDaily > adjustedMaxDisparity || // (A) 종목 체급 대비 역대급 폭등 상태
|
||||||
(disparityDaily > 115.0 && rsiDaily > 75.0) || // (B) 급등 중이면서 일봉 과매수
|
(disparityDaily > adjustedWarnDisparity && rsiDaily > 75.0) || // (B) 체급별 경고 수준 + 일봉 과매수
|
||||||
(disparityDaily > 115.0 && isMicroOverheated) // (C) 급등 중이면서 10분 내 순간 펌핑(꼭지)
|
(disparityDaily > adjustedWarnDisparity && isMicroOverheated) // (C) 체급별 경고 수준 + 초단기 펌핑(꼭지)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun calculateScores(financialScore100: Int): InvestmentScores {
|
fun calculateScores(financialScore100: Int): InvestmentScores {
|
||||||
@@ -322,7 +352,7 @@ class TechnicalAnalyzer {
|
|||||||
candles: List<CandleData>,
|
candles: List<CandleData>,
|
||||||
avgReboundTerm: Double,
|
avgReboundTerm: Double,
|
||||||
dropThreshold: Double = 5.0,
|
dropThreshold: Double = 5.0,
|
||||||
timeTolerance: Double = 1.5
|
timeTolerance: Double = 2.0
|
||||||
): Boolean {
|
): Boolean {
|
||||||
if (candles.size < 20 || avgReboundTerm <= 0.0) return false
|
if (candles.size < 20 || avgReboundTerm <= 0.0) return false
|
||||||
|
|
||||||
@@ -343,18 +373,15 @@ class TechnicalAnalyzer {
|
|||||||
|
|
||||||
// 🌟 2. 3가지 핵심 조건 분리
|
// 🌟 2. 3가지 핵심 조건 분리
|
||||||
val isPriceDropped = currentDropRate <= -dropThreshold
|
val isPriceDropped = currentDropRate <= -dropThreshold
|
||||||
// 조건 A: 가격이 통계적 하락폭만큼 충분히 빠졌는가?
|
|
||||||
|
|
||||||
val isPastMinTime = daysSincePeak >= (avgReboundTerm - timeTolerance)
|
val isPastMinTime = daysSincePeak >= (avgReboundTerm - timeTolerance)
|
||||||
// 조건 B: 반등 '최소' 기간을 채웠는가? (떨어지는 칼날을 너무 일찍 잡는 것 방지)
|
|
||||||
|
|
||||||
val isWithinMaxTime = daysSincePeak <= (avgReboundTerm + (timeTolerance * 2))
|
// 최대 기간 조건은 참고용으로 남겨두되 매수 차단 로직에서는 제외합니다.
|
||||||
// 조건 C: 반등 '최대' 기간을 넘기지 않았는가? (죽은 주식처럼 너무 오래 횡보하는 것 방지)
|
// val isWithinMaxTime = daysSincePeak <= (avgReboundTerm + (timeTolerance * 2))
|
||||||
|
|
||||||
// 🌟 3. 3개 중 2개 이상 만족 시 반등 임박(Approaching)으로 판단
|
// 🌟 3. 현실적인 타점 판별 (필수 2가지만 강력하게 요구)
|
||||||
val passedConditions = listOf(isPriceDropped, isPastMinTime, isWithinMaxTime).count { it }
|
// 필수 1: 가격이 통계적 하락폭만큼 충분히 빠졌는가? (눌림목 대전제)
|
||||||
|
// 필수 2: 최소한의 반등 준비 기간(평균 기간 - 오차)은 지났는가? (떨어지는 칼날 방지)
|
||||||
return passedConditions >= 2
|
return isPriceDropped && isPastMinTime
|
||||||
}
|
}
|
||||||
|
|
||||||
fun calculateMFI(candles: List<CandleData>, period: Int = 14): Double {
|
fun calculateMFI(candles: List<CandleData>, period: Int = 14): Double {
|
||||||
@@ -670,7 +697,8 @@ $standardizedScores
|
|||||||
val remainingDropRate = reboundStats.avgDropRate - currentDropRate
|
val remainingDropRate = reboundStats.avgDropRate - currentDropRate
|
||||||
|
|
||||||
// 바닥권 인정 마진 (ATR 기반)
|
// 바닥권 인정 마진 (ATR 기반)
|
||||||
val bottomMargin = currentAtr * 0.7
|
//B. 바닥권 판정 마진predictDropBottom currentAtr * 0.7 여유 마진 마진을 축소(currentAtr * 0.3)하여 예상 바닥에 더 근접해야 인정
|
||||||
|
val bottomMargin = currentAtr * 0.6
|
||||||
|
|
||||||
// 저점 평균(smoothedBottom)도 마지노선 계산에 추가 가중치로 섞어줌으로써 저점에 더 밀착시킴
|
// 저점 평균(smoothedBottom)도 마지노선 계산에 추가 가중치로 섞어줌으로써 저점에 더 밀착시킴
|
||||||
val adjustedExtremeLow = (volatility.extremeLow + smoothedBottom) / 2.0
|
val adjustedExtremeLow = (volatility.extremeLow + smoothedBottom) / 2.0
|
||||||
@@ -743,7 +771,7 @@ data class VolatilityForecast(
|
|||||||
)
|
)
|
||||||
data class ReboundStats(
|
data class ReboundStats(
|
||||||
val avgReboundPeriod: Double = 0.0, // 평균 반등 소요 캔들 (일/주/월)
|
val avgReboundPeriod: Double = 0.0, // 평균 반등 소요 캔들 (일/주/월)
|
||||||
val timeTolerance: Double = 1.5, // 오차 허용 범위 (표준편차)
|
val timeTolerance: Double = 2.0, // 오차 허용 범위 (표준편차)
|
||||||
val avgDropRate: Double = 5.0, // 평균 하락폭
|
val avgDropRate: Double = 5.0, // 평균 하락폭
|
||||||
val avgReboundAmplitude: Double = 0.0, // 🌟 [신규] 바닥 찍고 평균적으로 몇 % 올랐는가?
|
val avgReboundAmplitude: Double = 0.0, // 🌟 [신규] 바닥 찍고 평균적으로 몇 % 올랐는가?
|
||||||
val isValid: Boolean = false
|
val isValid: Boolean = false
|
||||||
|
|||||||
@@ -360,4 +360,13 @@ object KisSession {
|
|||||||
fun isAvailBuyTime(now: LocalTime) : Boolean {
|
fun isAvailBuyTime(now: LocalTime) : Boolean {
|
||||||
return now.isBefore(endBuyTime()) && now.isAfter(startBuyTime())
|
return now.isBefore(endBuyTime()) && now.isAfter(startBuyTime())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun isMarketOpenTime(now: LocalTime) : Boolean {
|
||||||
|
return now.isBefore(LocalTime.of(15,30)) && now.isAfter(LocalTime.of(9,0))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isMarketAnalyzerTime(now: LocalTime) : Boolean {
|
||||||
|
return now.isBefore(LocalTime.of(19,0)) && now.isAfter(LocalTime.of(8,0))
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -258,27 +258,27 @@ data class CurrentPriceResponse(
|
|||||||
@Serializable
|
@Serializable
|
||||||
data class CurrentPriceOutput(
|
data class CurrentPriceOutput(
|
||||||
// --- 📊 기존 시세 및 기본 펀더멘털 필드 ---
|
// --- 📊 기존 시세 및 기본 펀더멘털 필드 ---
|
||||||
val stck_shrn_iscd: String, // 종목 코드
|
val stck_shrn_iscd: String = "", // 종목 코드
|
||||||
val stck_prpr: String, // 주식 현재가
|
val stck_prpr: String = "0", // 주식 현재가
|
||||||
val prdy_vrss: String, // 전일 대비
|
val prdy_vrss: String = "0", // 전일 대비
|
||||||
val prdy_ctrt: String, // 전일 대비율
|
val prdy_ctrt: String = "0.0", // 전일 대비율
|
||||||
val acml_vol: String, // 누적 거래량
|
val acml_vol: String = "0", // 누적 거래량
|
||||||
val stck_oprc: String, // 시가
|
val stck_oprc: String = "0", // 시가
|
||||||
val stck_hgpr: String, // 고가
|
val stck_hgpr: String = "0", // 고가
|
||||||
val stck_lwpr: String, // 저가
|
val stck_lwpr: String = "0", // 저가
|
||||||
val hts_avls: String, // 시가총액
|
val hts_avls: String = "0", // 시가총액
|
||||||
val per: String,
|
val per: String = "0.0",
|
||||||
val pbr: String,
|
val pbr: String = "0.0",
|
||||||
|
|
||||||
// --- 🚨 신규 추가: 리스크 필터링 및 상태 필드 ---
|
// --- 🚨 신규 추가: 리스크 필터링 및 상태 필드 ---
|
||||||
val rprs_mrkt_kor_name: String, // 대표 시장 한글 명 (KOSPI, KOSDAQ 등)
|
val rprs_mrkt_kor_name: String = "", // 대표 시장 한글 명 (KOSPI, KOSDAQ 등)
|
||||||
val iscd_stat_cls_code: String, // 종목 상태 구분 코드 (51:관리, 52:위험, 58:정지 등)
|
val iscd_stat_cls_code: String = "", // 종목 상태 구분 코드 (51:관리, 52:위험, 58:정지 등)
|
||||||
val mrkt_warn_cls_code: String, // 시장경고코드 (보통 "00"이 정상)
|
val mrkt_warn_cls_code: String = "", // 시장경고코드 (보통 "00"이 정상)
|
||||||
val short_over_yn: String, // 단기과열여부 (Y/N)
|
val short_over_yn: String = "", // 단기과열여부 (Y/N)
|
||||||
val sltr_yn: String, // 정리매매여부 (Y/N)
|
val sltr_yn: String = "", // 정리매매여부 (Y/N)
|
||||||
val mang_issu_cls_code: String, // 관리종목여부 (1/0 또는 Y/N)
|
val mang_issu_cls_code: String = "", // 관리종목여부 (1/0 또는 Y/N)
|
||||||
val temp_stop_yn: String, // 임시 정지 여부 (Y/N)
|
val temp_stop_yn: String = "", // 임시 정지 여부 (Y/N)
|
||||||
val invt_caful_yn: String // 투자유의여부 (Y/N)
|
val invt_caful_yn: String = "" // 투자유의여부 (Y/N)
|
||||||
)
|
)
|
||||||
|
|
||||||
// 필터링 결과를 담을 객체
|
// 필터링 결과를 담을 객체
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ class TradingDecision {
|
|||||||
var signalModel : ScalpingSignalModel? = null
|
var signalModel : ScalpingSignalModel? = null
|
||||||
var maxRealisticProfitRate :Double = 0.0
|
var maxRealisticProfitRate :Double = 0.0
|
||||||
var reboundDaysDaily: Double = 0.0 // 일봉 기준 평균 반등 소요일
|
var reboundDaysDaily: Double = 0.0 // 일봉 기준 평균 반등 소요일
|
||||||
|
var isWatering: Boolean = false
|
||||||
var reboundWeeksWeekly: Double = 0.0 // 주봉 기준 평균 반등 소요주
|
var reboundWeeksWeekly: Double = 0.0 // 주봉 기준 평균 반등 소요주
|
||||||
var isReboundApproaching: Boolean = false // 반등 주기에 근접했는지 여부
|
var isReboundApproaching: Boolean = false // 반등 주기에 근접했는지 여부
|
||||||
var reboundGuideMessage: String = "반등 주기 데이터 없음" // UI나 로그에 노출할 가이드 메시지
|
var reboundGuideMessage: String = "반등 주기 데이터 없음" // UI나 로그에 노출할 가이드 메시지
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import kotlinx.serialization.json.jsonPrimitive
|
|||||||
import model.*
|
import model.*
|
||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
import java.time.LocalTime
|
import java.time.LocalTime
|
||||||
|
import java.time.ZoneId
|
||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
import kotlin.coroutines.coroutineContext
|
import kotlin.coroutines.coroutineContext
|
||||||
|
|
||||||
@@ -334,7 +335,7 @@ object KisTradeService {
|
|||||||
|
|
||||||
parameter("FID_INPUT_DATE_1", startDate)
|
parameter("FID_INPUT_DATE_1", startDate)
|
||||||
parameter("FID_INPUT_DATE_2", endDate)
|
parameter("FID_INPUT_DATE_2", endDate)
|
||||||
parameter("FID_COND_MRKT_DIV_CODE", "J")
|
parameter("FID_COND_MRKT_DIV_CODE", if(LocalTime.now(ZoneId.of("Asia/Seoul")).isAfter(LocalTime.of(16,0))) "UN" else "J")
|
||||||
parameter("FID_INPUT_ISCD", stockCode)
|
parameter("FID_INPUT_ISCD", stockCode)
|
||||||
parameter("FID_PERIOD_DIV_CODE", periodCode) // D, W, M
|
parameter("FID_PERIOD_DIV_CODE", periodCode) // D, W, M
|
||||||
parameter("FID_ORG_ADJ_PRC", "0")
|
parameter("FID_ORG_ADJ_PRC", "0")
|
||||||
@@ -426,12 +427,14 @@ object KisTradeService {
|
|||||||
isDomestic && !config.isSimulation -> if (isBuy) "TTTC0802U" else "TTTC0801U"
|
isDomestic && !config.isSimulation -> if (isBuy) "TTTC0802U" else "TTTC0801U"
|
||||||
else -> if (isBuy) "TTTS3002U" else "TTTS3001U"
|
else -> if (isBuy) "TTTS3002U" else "TTTS3001U"
|
||||||
}
|
}
|
||||||
val finalOrderDivision = when {
|
var finalOrderDivision = when {
|
||||||
orderDivision.isNotEmpty() -> orderDivision
|
orderDivision.isNotEmpty() -> orderDivision
|
||||||
marketCode.equals("SOR") || price == "0" || price.isEmpty() -> "01" // 시장가
|
marketCode.equals("SOR") || price == "0" || price.isEmpty() -> "01" // 시장가
|
||||||
else -> "00" // 지정가
|
else -> "00" // 지정가
|
||||||
}
|
}
|
||||||
|
if (marketCode.equals("KRX") && LocalTime.now(ZoneId.of("Asia/Seoul")).isAfter(LocalTime.of(16,0))) {
|
||||||
|
finalOrderDivision = "41"
|
||||||
|
}
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
val response = client.post("$baseUrl/uapi/${if(isDomestic) "domestic" else "overseas"}-stock/v1/trading/order-cash") {
|
val response = client.post("$baseUrl/uapi/${if(isDomestic) "domestic" else "overseas"}-stock/v1/trading/order-cash") {
|
||||||
@@ -563,14 +566,15 @@ object KisTradeService {
|
|||||||
header("Content-Type", "application/json; charset=utf-8")
|
header("Content-Type", "application/json; charset=utf-8")
|
||||||
|
|
||||||
// 파라미터 설정
|
// 파라미터 설정
|
||||||
parameter("FID_COND_MRKT_DIV_CODE", "J") // J: 주식
|
// parameter("FID_COND_MRKT_DIV_CODE", "J") // J: 주식
|
||||||
|
parameter("FID_COND_MRKT_DIV_CODE", if(LocalTime.now(ZoneId.of("Asia/Seoul")).isAfter(LocalTime.of(16,0))) "UN" else "J")
|
||||||
parameter("FID_INPUT_ISCD", stockCode) // 종목코드
|
parameter("FID_INPUT_ISCD", stockCode) // 종목코드
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.status.isSuccess()) {
|
if (response.status.isSuccess()) {
|
||||||
val body = response.body<CurrentPriceResponse>()
|
val body = response.body<CurrentPriceResponse>()
|
||||||
if (body.rt_cd == "0") {
|
if (body.rt_cd == "0") {
|
||||||
println("${body.output}")
|
// println("${body.output}")
|
||||||
Result.success(body.output)
|
Result.success(body.output)
|
||||||
} else {
|
} else {
|
||||||
println("API 에러: ${body.msg1}")
|
println("API 에러: ${body.msg1}")
|
||||||
@@ -684,7 +688,7 @@ object KisTradeService {
|
|||||||
try {
|
try {
|
||||||
do {
|
do {
|
||||||
if (!coroutineContext.isActive) throw _root_ide_package_.io.ktor.utils.io.CancellationException("UI에서 작업을 취소함") // [추가]
|
if (!coroutineContext.isActive) throw _root_ide_package_.io.ktor.utils.io.CancellationException("UI에서 작업을 취소함") // [추가]
|
||||||
println("📡 [Step $pageCount] 요청 전송 중... (tr_cont: $trCont)")
|
// println("📡 [Step $pageCount] 요청 전송 중... (tr_cont: $trCont)")
|
||||||
val response = client.get("$baseUrl/uapi/domestic-stock/v1/trading/inquire-balance") {
|
val response = client.get("$baseUrl/uapi/domestic-stock/v1/trading/inquire-balance") {
|
||||||
header("authorization", "Bearer ${config.tradeToken}")
|
header("authorization", "Bearer ${config.tradeToken}")
|
||||||
header("appkey", config.realAppKey)
|
header("appkey", config.realAppKey)
|
||||||
@@ -714,8 +718,8 @@ object KisTradeService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val body = response.body<StockBalanceResponse>()
|
val body = response.body<StockBalanceResponse>()
|
||||||
println("✅ [Step $pageCount] $markgetCode 수신 완료 - 종목 수: ${body.output1.size}\n${body.output2}\n\n")
|
// println("✅ [Step $pageCount] $markgetCode 수신 완료 - 종목 수: ${body.output1.size}\n${body.output2}\n\n")
|
||||||
// println("✅ [Step $pageCount] $markgetCode 수신 완료 - 종목 수: ${body.output1.size}")
|
println("✅ [Step $pageCount] $markgetCode 수신 완료 - 종목 수: ${body.output1.size}")
|
||||||
|
|
||||||
allHoldings.addAll(body.output1)
|
allHoldings.addAll(body.output1)
|
||||||
if (totalBalance == null) totalBalance = body
|
if (totalBalance == null) totalBalance = body
|
||||||
@@ -725,7 +729,7 @@ object KisTradeService {
|
|||||||
ctxAreaFk = body.ctx_area_fk100 ?: ""
|
ctxAreaFk = body.ctx_area_fk100 ?: ""
|
||||||
ctxAreaNk = body.ctx_area_nk100 ?: ""
|
ctxAreaNk = body.ctx_area_nk100 ?: ""
|
||||||
|
|
||||||
println("📝 [Header Check] tr_cont: $trCont, ctx_area_nk100: $ctxAreaNk")
|
// println("📝 [Header Check] tr_cont: $trCont, ctx_area_nk100: $ctxAreaNk")
|
||||||
|
|
||||||
if ( trCont == "M") {
|
if ( trCont == "M") {
|
||||||
pageCount++
|
pageCount++
|
||||||
|
|||||||
@@ -237,7 +237,7 @@ object RagService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun processStock(currentPrice: Double, technicalAnalyzer: TechnicalAnalyzer, stockName: String, stockCode: String, result: TradingDecisionCallback) {
|
suspend fun processStock(currentPrice: Double, technicalAnalyzer: TechnicalAnalyzer, stockName: String, stockCode: String, isWatering : Boolean, result: TradingDecisionCallback) {
|
||||||
val totalStartTime = System.currentTimeMillis()
|
val totalStartTime = System.currentTimeMillis()
|
||||||
|
|
||||||
coroutineScope {
|
coroutineScope {
|
||||||
@@ -246,6 +246,7 @@ object RagService {
|
|||||||
this.stockCode = stockCode
|
this.stockCode = stockCode
|
||||||
this.analyzer = technicalAnalyzer
|
this.analyzer = technicalAnalyzer
|
||||||
this.currentPrice = currentPrice
|
this.currentPrice = currentPrice
|
||||||
|
this.isWatering = isWatering
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isSafetyBeltStockCodes.contains(stockCode)) {
|
if (isSafetyBeltStockCodes.contains(stockCode)) {
|
||||||
@@ -277,8 +278,10 @@ object RagService {
|
|||||||
isSafetyBeltStockCodes.add(stockCode)
|
isSafetyBeltStockCodes.add(stockCode)
|
||||||
return@coroutineScope
|
return@coroutineScope
|
||||||
}
|
}
|
||||||
|
val techScore = tradingDecision.signalModel?.compositeScore ?: 0
|
||||||
if ((tradingDecision.signalModel?.compositeScore ?: 0) < 40) {
|
// 🌟 신규 매수는 40점 컷오프, 물타기는 20점(또는 제한 없음)으로 하향
|
||||||
|
val minTechCutoff = if (tradingDecision.isWatering) 15 else 40
|
||||||
|
if (techScore < minTechCutoff) {
|
||||||
logTime(stockName, "기술 점수 미달 조기 종료 ${tradingDecision.signalModel?.compositeScore} , ${tradingDecision.signalModel?.successProbPct} ", techDuration, System.currentTimeMillis() - totalStartTime)
|
logTime(stockName, "기술 점수 미달 조기 종료 ${tradingDecision.signalModel?.compositeScore} , ${tradingDecision.signalModel?.successProbPct} ", techDuration, System.currentTimeMillis() - totalStartTime)
|
||||||
if (FinancialAnalyzer.isBuyConsiderationMet(financialStmt) && financialScore > 70) {
|
if (FinancialAnalyzer.isBuyConsiderationMet(financialStmt) && financialScore > 70) {
|
||||||
TradingLogStore.addAnalyzer(stockName, stockCode, "매수 타점 미도달 (재무 우량주로 감시 지속)", true)
|
TradingLogStore.addAnalyzer(stockName, stockCode, "매수 타점 미도달 (재무 우량주로 감시 지속)", true)
|
||||||
@@ -432,7 +435,7 @@ object RagService {
|
|||||||
put("type", "json_object")
|
put("type", "json_object")
|
||||||
}
|
}
|
||||||
}.toString()
|
}.toString()
|
||||||
println("requestBodyJson =>> $requestBodyJson")
|
// println("requestBodyJson =>> $requestBodyJson")
|
||||||
val request = Request.Builder()
|
val request = Request.Builder()
|
||||||
.url(LLM_API_URL())
|
.url(LLM_API_URL())
|
||||||
.post(requestBodyJson.toRequestBody(jsonMediaType))
|
.post(requestBodyJson.toRequestBody(jsonMediaType))
|
||||||
@@ -487,9 +490,27 @@ object RagService {
|
|||||||
val synthStartTime = System.currentTimeMillis()
|
val synthStartTime = System.currentTimeMillis()
|
||||||
val sysScore100 = calculateSystemPoint(scores) * 4.0
|
val sysScore100 = calculateSystemPoint(scores) * 4.0
|
||||||
|
|
||||||
// 가중치 합성 (Tech 35% : Fin 25% : News 20% : Sys 20%)
|
val hasNews = tempDecision.newsContext != null && tempDecision.newsContext!!.isNotBlank()
|
||||||
var finalConfidence = (finScore100 * 0.25) + (techScore100 * 0.25) + (newsScore100 * 0.30) + (sysScore100 * 0.20)
|
|
||||||
// var finalConfidence = (finScore100 * 0.25) + (techScore100 * 0.35) + (newsScore100 * 0.20) + (sysScore100 * 0.20)
|
var finalConfidence = if (hasNews) {
|
||||||
|
// 뉴스가 있을 때: 원래 로직
|
||||||
|
(finScore100 * 0.25) + (techScore100 * 0.25) + (newsScore100 * 0.30) + (sysScore100 * 0.20)
|
||||||
|
} else {
|
||||||
|
// 뉴스가 없을 때: 기술 45%, 재무 35%, 시스템 20%로 배분 (뉴스 영향력 제거)
|
||||||
|
(finScore100 * 0.35) + (techScore100 * 0.45) + (sysScore100 * 0.20)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tempDecision.isWatering) {
|
||||||
|
// 물타기 대상이 여기까지 왔다는 건 '볼린저 하단 터치'나 '초과매도(RSI<35)' 등 바닥 확인이 끝났다는 뜻임.
|
||||||
|
// 깎인 기술 점수를 보완하기 위해 강한 턴어라운드 기대 가점을 부여
|
||||||
|
finalConfidence += 20.0
|
||||||
|
|
||||||
|
// 물타기 전용 등급 강제 상향 (잡주가 아니라는 전제 하에)
|
||||||
|
if (finalConfidence >= KisSession.config.getValues(ConfigIndex.MIN_PURCHASE_SCORE_INDEX) * 0.8) {
|
||||||
|
// 물타기 전용 등급(예: 신규 매수 로직에 안 잡히게 LEVEL_3 정도로 고정)
|
||||||
|
tempDecision.investmentGrade = InvestmentGrade.LEVEL_3_CAUTIOUS_RECOMMEND
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 보너스 및 패널티 로직
|
// 보너스 및 패널티 로직
|
||||||
if (finScore100 >= 80.0 && techScore100 >= 70.0) finalConfidence += 8.0
|
if (finScore100 >= 80.0 && techScore100 >= 70.0) finalConfidence += 8.0
|
||||||
@@ -569,6 +590,7 @@ object RagService {
|
|||||||
this.stockName = stockName
|
this.stockName = stockName
|
||||||
this.currentPrice = tempDecision.currentPrice
|
this.currentPrice = tempDecision.currentPrice
|
||||||
this.techSummary = tempDecision.techSummary
|
this.techSummary = tempDecision.techSummary
|
||||||
|
this.isWatering = tempDecision.isWatering
|
||||||
this.ultraShortScore = scores.ultraShort.toDouble()
|
this.ultraShortScore = scores.ultraShort.toDouble()
|
||||||
this.shortTermScore = scores.shortTerm.toDouble()
|
this.shortTermScore = scores.shortTerm.toDouble()
|
||||||
this.midTermScore = scores.midTerm.toDouble()
|
this.midTermScore = scores.midTerm.toDouble()
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user