This commit is contained in:
lunaticbum 2026-02-05 14:26:02 +09:00
parent e0e328ea97
commit 3f209dcd4d
9 changed files with 194 additions and 107 deletions

View File

@ -59,6 +59,32 @@ enum class RankingType(
VOLUME_POWER("체결강도순", "FHPST01680000", "20168", "/uapi/domestic-stock/v1/ranking/volume-power", "0"), VOLUME_POWER("체결강도순", "FHPST01680000", "20168", "/uapi/domestic-stock/v1/ranking/volume-power", "0"),
// BEFORE("장전예상", "FHPST01820000", "20182", "/uapi/domestic-stock/v1/ranking/exp-trans-updown", "0"), // BEFORE("장전예상", "FHPST01820000", "20182", "/uapi/domestic-stock/v1/ranking/exp-trans-updown", "0"),
// AFTER("장후예상", "FHPST01820000", "20182", "/uapi/domestic-stock/v1/ranking/exp-trans-updown", "1") // AFTER("장후예상", "FHPST01820000", "20182", "/uapi/domestic-stock/v1/ranking/exp-trans-updown", "1")
FOREIGNER_BUY("외인순매수", "FHPST01720000", "20172", "uapidomestic-stockv1quotationsfrgn-buy-rank", "0"),
INSTITUTION_BUY("기관순매수", "FHPST01730000", "20173", "uapidomestic-stockv1quotationsinst-buy-rank", "0"),
// 신규: 재무/지표
PER_RANK("PER", "FHPST01760000", "20176", "uapidomestic-stockv1quotationsper-rank", "0"),
PBR_RANK("PBR", "FHPST01770000", "20177", "uapidomestic-stockv1quotationspbr-rank", "0"),
DIVIDEND("배당률", "FHPST01800000", "20180", "uapidomestic-stockv1quotationsdividend-rank", "0"),
// 신규: 시간외/예상
AFTER_HOURS_VOLUME("시간외거래량", "FHPST01810000", "20181", "uapidomestic-stockv1rankingafterhours-volume", "0"),
EXPECTED_RISE("예상상승", "FHPST01820000", "20182", "uapidomestic-stockv1rankingexp-trans-updown", "0"),
EXPECTED_FALL("예상하락", "FHPST01820000", "20182", "uapidomestic-stockv1rankingexp-trans-updown", "1"),
// 신규: 체결/호가/신고가 등
EXEC_STRENGTH("체결강도", "FHPST01780000", "20178", "uapidomestic-stockv1quotationsexec-strength", "0"),
BID_ASK_VOLUME("호가잔량", "FHPST01790000", "20179", "uapidomestic-stockv1quotationsbid-ask-volume", "0"),
NEW_HIGH("52주신고가", "FHPST01690000", "20169", "uapidomestic-stockv1rankingsh-52w-high", "0"),
// 신규: 신용/공매도/대량
MARGIN_BALANCE("신용잔고", "FHPST01830000", "20183", "uapidomestic-stockv1quotationsmargin-balance", "0"),
SHORT_SELL("공매도", "FHPST01840000", "20184", "uapidomestic-stockv1quotationsshort-sell", "0"),
LARGE_DEAL("대량체결", "FHPST01850000", "20185", "uapidomestic-stockv1quotationslarge-deal", "0"),
// 기타 인기 (KIS HTS 순위분석 기반)
INTEREST_TOP("관심순", "FHPST01860000", "20186", "uapidomestic-stockv1rankinginterest-top", "0"),
COMPANY_TRADE("당사매매", "FHPST01870000", "20187", "uapidomestic-stockv1rankingcompany-trade", "0")
} }
@Serializable @Serializable

View File

@ -147,9 +147,22 @@ object KisTradeService {
// RankingType.BEFORE -> { // RankingType.BEFORE -> {
// parameter("FID_MKOP_CLS_CODE", type.sortCode) // parameter("FID_MKOP_CLS_CODE", type.sortCode)
// } // }
else -> { RankingType.FOREIGNER_BUY, RankingType.INSTITUTION_BUY -> {
parameter("FID_BLNG_CLS_CODE", type.sortCode) // 순매수용
parameter("FID_INPUT_CNT_1", "5") // 5일 누적 등 조정 가능
} }
RankingType.PER_RANK, RankingType.PBR_RANK -> {
parameter("FID_FINCL_CLS_CODE", type.sortCode) // 재무비율용
}
RankingType.AFTER_HOURS_VOLUME -> {
parameter("FID_TIME_OUT_CLS_CODE", "1") // 시간외 구분
}
RankingType.EXPECTED_RISE -> parameter("FID_MK_OP_CLS_CODE", type.sortCode)
// 체결강도/호가 등은 FID_EXEC_CLS_CODE 추가
else -> parameter("FID_BLNG_CLS_CODE", "0") // 기본
// else -> {
//
// }
} }
parameter("FID_PBMN", "") parameter("FID_PBMN", "")
parameter("FID_APLY_RANG_PRC_1", "") parameter("FID_APLY_RANG_PRC_1", "")

View File

@ -139,7 +139,11 @@ object RagService {
corpInfo?.stockName = stockName corpInfo?.stockName = stockName
tradingDecision.stockName = stockName tradingDecision.stockName = stockName
tradingDecision.corpName = corpInfo?.cName ?: "" tradingDecision.corpName = corpInfo?.cName ?: ""
corpInfo?.let { NewsService.fetchAndIngestNews(it) } corpInfo?.let {
try {
NewsService.fetchAndIngestNews(it)
} catch (e: Exception) {}
}
val financialDataDeferred = async { NewsService.fetchFinancialGrowth(corpInfo?.cCode ?: "") } val financialDataDeferred = async { NewsService.fetchFinancialGrowth(corpInfo?.cCode ?: "") }

View File

@ -4,9 +4,11 @@ import TradingDecision
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeout
import model.CandleData import model.CandleData
import model.RankingType import model.RankingType
import network.KisTradeService import network.KisTradeService
@ -26,7 +28,8 @@ object AutoTradingManager {
private var discoveryJob: Job? = null private var discoveryJob: Job? = null
val MIN = 0.1
val MAX = 15.0
fun startAutoDiscoveryLoop( fun startAutoDiscoveryLoop(
tradeService: KisTradeService, tradeService: KisTradeService,
callback: TradingDecisionCallback callback: TradingDecisionCallback
@ -56,100 +59,113 @@ object AutoTradingManager {
val riseList = riseRankDeferred.await() val riseList = riseRankDeferred.await()
val amountList = amountRankDeferred.await() val amountList = amountRankDeferred.await()
val volumeList = volumePowerDeferred.await() val volumeList = volumePowerDeferred.await()
// (C) 거래대금 상위 종목 필터링 (시장의 주도주)
val amountCandidates = amountList
.filter { stock ->
val rate = stock.prdy_ctrt.toDoubleOrNull() ?: 0.0
rate in 1.0..15.0 // 너무 과열되지 않은 주도주
}
val volCandidates = volList
.filter { stock ->
val rate = stock.prdy_ctrt.toDoubleOrNull() ?: 0.0
rate in 1.0..18.0 // 0% 초과 20% 이하
}
// (B) 상승률 상위 종목 중: 너무 급등한(20% 초과) 종목은 제외하고, 적당히 오르고 있는 종목만 필터링 -> 상위 10개
// 보통 상승률 랭킹은 상한가(30%)부터 내려오므로, 앞쪽의 급등주를 건너뛰어야 함
val riseCandidates = riseList
.filter { stock ->
val rate = stock.prdy_ctrt.toDoubleOrNull() ?: 0.0
rate in 2.0..18.0 // 최소 3% 이상은 올라야 의미 있음, 20% 이하는 안전 구간
}
val volumeCandidates = volumeList .filter { stock ->
val rate = stock.prdy_ctrt.toDoubleOrNull() ?: 0.0
rate in 1.0..18.0 // 최소 3% 이상은 올라야 의미 있음, 20% 이하는 안전 구간
}
// 3. 리스트 합치기 (중복 제거) // 3. 리스트 합치기 (중복 제거)
val candidates = (volCandidates + riseCandidates + amountCandidates + volumeCandidates).distinctBy { it.code } val candidates = (volList + riseList + amountList + volumeList + async { tradeService.fetchMarketRanking(RankingType.FOREIGNER_BUY, true).getOrDefault(emptyList()) + async { tradeService.fetchMarketRanking(RankingType.INSTITUTION_BUY, true).getOrDefault(emptyList()) }.await()}.await()).filter {stock ->
val rate = stock.prdy_ctrt.toDoubleOrNull() ?: 0.0
rate in MIN..MAX // 너무 과열되지 않은 주도주
}.distinctBy { it.code }
println("🔎 1차 필터링 후보 ${candidates.size}개 (급등주 제외) 검증 시작...") println("🔎 1차 필터링 후보 ${candidates.size}개 (급등주 제외) 검증 시작...")
candidates.forEach { stock -> candidates.forEach { stock ->
// [조건 1] 이미 보유한 종목 제외 try {
if (myHoldings.contains(stock.code)) return@forEach
val currentPrice = stock.stck_prpr.replace(",", "").toDoubleOrNull() ?: 0.0
// [조건 2] 최소 1주 매수 가능 여부 // [조건 1] 이미 보유한 종목 제외
if (currentPrice > myCash || currentPrice > 5000) return@forEach if (myHoldings.contains(stock.code)) return@forEach
callback(TradingDecision().apply {
this.stockCode = stock.code
this.confidence = -1.0
this.stockName = stock.name
}, false)
// 3. 일봉 데이터 조회 (필터링 용도 + TechnicalAnalyzer 입력용)
val dailyResult = tradeService.fetchPeriodChartData(stock.code, "D", true)
val dailyData = dailyResult.getOrNull()
val todayCandle = dailyData?.lastOrNull()
if (dailyData != null && todayCandle != null) { val currentPrice = stock.stck_prpr.replace(",", "").toDoubleOrNull() ?: 0.0
val open = todayCandle.stck_oprc.toDoubleOrNull() ?: 0.0 // [조건 2] 최소 1주 매수 가능 여부
val current = todayCandle.stck_prpr.toDoubleOrNull() ?: 0.0 if (currentPrice > myCash || currentPrice > 10000) {
println("⏭️ [제외] ${stock.name}: 주가($currentPrice)가 예산 초과")
return@forEach
}
callback(TradingDecision().apply {
this.stockCode = stock.code
this.confidence = -1.0
this.stockName = stock.name
}, false)
// 3. 일봉 데이터 조회 (필터링 용도 + TechnicalAnalyzer 입력용)
val dailyResult = tradeService.fetchPeriodChartData(stock.code, "D", true)
val dailyData = dailyResult.getOrNull()
val todayCandle = dailyData?.lastOrNull()
if (open > 0) { if (dailyData != null && todayCandle != null) {
val riseRate = (current - open) / open * 100
// [조건 3] 상승 중(양봉)이면서 20% 이하 상승 val open = todayCandle.stck_oprc.toDoubleOrNull() ?: 0.0
if (riseRate > 0 && riseRate <= 20.0) { val current = todayCandle.stck_prpr.toDoubleOrNull() ?: 0.0
println("✨ [발굴] ${stock.name} (+${String.format("%.1f", riseRate)}%) -> 데이터 수집 및 분석")
// [핵심 수정] AI 분석 전 필요한 차트 데이터(30분, 주봉, 월봉)를 모두 가져와 TechnicalAnalyzer에 주입 if (open > 0) {
// 비동기로 동시에 요청하여 속도 향상 val riseRate = (current - open) / open * 100
val min30Def = async { tradeService.fetchChartData(stock.code, true).getOrDefault(emptyList()) }
val weekDef = async { tradeService.fetchPeriodChartData(stock.code, "W", true).getOrDefault(emptyList()) }
val monthDef = async { tradeService.fetchPeriodChartData(stock.code, "M", true).getOrDefault(emptyList()) }
val min30Data = min30Def.await() // [조건 3] 상승 중(양봉)이면서 20% 이하 상승
val weeklyData = weekDef.await() if (riseRate > 0 && riseRate <= 20.0) {
val monthlyData = monthDef.await() println(
"✨ [발굴] ${stock.name} (+${
String.format(
"%.1f",
riseRate
)
}%) -> 데이터 수집 분석"
)
// TechnicalAnalyzer 상태 업데이트 (싱글톤이므로 순차 처리 필수) // [핵심 수정] AI 분석 전 필요한 차트 데이터(30분, 주봉, 월봉)를 모두 가져와 TechnicalAnalyzer에 주입
val t = TechnicalAnalyzer() // 비동기로 동시에 요청하여 속도 향상
t.daily = dailyData val min30Def = async {
t.weekly = weeklyData tradeService.fetchChartData(stock.code, true).getOrDefault(emptyList())
t.monthly = monthlyData }
t.min30 = min30Data val weekDef = async {
tradeService.fetchPeriodChartData(stock.code, "W", true)
// 데이터 준비 완료 후 AI 분석 요청 (suspend 함수이므로 완료될 때까지 대기 -> 데이터 섞임 방지) .getOrDefault(emptyList())
RagService.processStock(t,stock.name, stock.code) { decision, isSuccess -> }
if (decision != null) { val monthDef = async {
decision.stockName = stock.name tradeService.fetchPeriodChartData(stock.code, "M", true)
decision.currentPrice = current // 차트에서 확인한 최신 현재가 주입 .getOrDefault(emptyList())
} }
callback(decision, isSuccess) // DashboardScreen으로 전달
}
// 분석 후 잠시 대기 (서버 부하 조절) val min30Data = min30Def.await()
delay(2000) val weeklyData = weekDef.await()
val monthlyData = monthDef.await()
// TechnicalAnalyzer 상태 업데이트 (싱글톤이므로 순차 처리 필수)
val t = TechnicalAnalyzer()
t.daily = dailyData
t.weekly = weeklyData
t.monthly = monthlyData
t.min30 = min30Data
try {
withTimeout(60000L) { // 60초 타임아웃 설정
RagService.processStock(t, stock.name, stock.code) { decision, isSuccess ->
if (decision != null) {
decision.stockName = stock.name
decision.currentPrice = current
}
callback(decision, isSuccess)
}
}
} catch (e: TimeoutCancellationException) {
println("⏳ [Timeout] ${stock.name} AI 분석 시간 초과로 스킵합니다.")
} catch (e: Exception) {
println("⚠️ [Error] AI 분석 중 오류: ${e.message}")
}
// 데이터 준비 완료 후 AI 분석 요청 (suspend 함수이므로 완료될 때까지 대기 -> 데이터 섞임 방지)
// 분석 후 잠시 대기 (서버 부하 조절)
delay(2000)
}
} }
} }
delay(300) // 종목 간 API 호출 간격
} catch (e: Exception) {
println("⚠️ [오류] ${stock.name} 분석 중 예외 발생: ${e.message}")
} }
delay(300) // 종목 간 API 호출 간격
} }
@ -164,9 +180,10 @@ object AutoTradingManager {
delay(tickMillis) delay(tickMillis)
currentWait += tickMillis currentWait += tickMillis
val leftSec = (totalWaitMillis - currentWait) / 1000 val leftSec = (totalWaitMillis - currentWait) / 1000
// 1분 단위 혹은 10초 단위로 자유롭게 로그 조절 가능 if (leftSec % 60 == 0L) {
if (leftSec % 30 == 0L || leftSec <= 30) { val runtime = Runtime.getRuntime()
println("📡 [AutoTrading] 시스템 정상 작동 중... (다음 분석 ${leftSec}초 전)") val usedMem = (runtime.totalMemory() - runtime.freeMemory()) / 1024 / 1024
println("📡 [System] 정상 작동 중... (남은 시간: ${leftSec}초 | 메모리 사용: ${usedMem}MB)")
} }
} }
@ -199,19 +216,19 @@ object AutoTradingManager {
println("🔥 [주문 집행] $code $type 완료") println("🔥 [주문 집행] $code $type 완료")
} }
} }
data class InvestmentScores(
val ultraShort: Int, // 초단기 (분봉/에너지)
val shortTerm: Int, // 단기 (일봉/뉴스)
val midTerm: Int, // 중기 (주봉/재무)
val longTerm: Int // 장기 (월봉/펀더멘털)
)
class TechnicalAnalyzer { class TechnicalAnalyzer {
var monthly: List<CandleData> = emptyList() var monthly: List<CandleData> = emptyList()
var weekly: List<CandleData> = emptyList() var weekly: List<CandleData> = emptyList()
var daily: List<CandleData> = emptyList() var daily: List<CandleData> = emptyList()
var min30: List<CandleData> = emptyList() var min30: List<CandleData> = emptyList()
data class InvestmentScores(
val ultraShort: Int, // 초단기 (분봉/에너지)
val shortTerm: Int, // 단기 (일봉/뉴스)
val midTerm: Int, // 중기 (주봉/재무)
val longTerm: Int // 장기 (월봉/펀더멘털)
)
fun calculateScores( fun calculateScores(
financialScore: Int // 재무제표 점수 (성장률 등 기반) financialScore: Int // 재무제표 점수 (성장률 등 기반)

View File

@ -12,6 +12,7 @@ import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit import kotlinx.coroutines.sync.withPermit
import model.NewsItem import model.NewsItem
import network.CorpInfo import network.CorpInfo
import java.net.URL
import kotlin.random.Random import kotlin.random.Random
object DynamicNewsScraper { object DynamicNewsScraper {
@ -91,20 +92,25 @@ object DynamicNewsScraper {
return page.evaluate(script) as String return page.evaluate(script) as String
} }
var failDomainList = arrayListOf<String>()
suspend fun fetchFullContent(url: String): String { suspend fun fetchFullContent(url: String): String {
// browser.newContext().use { ... } 대신 직접 변수를 선언하고 제어합니다. // browser.newContext().use { ... } 대신 직접 변수를 선언하고 제어합니다.
val domain = URL(url).host
val context = browser.newContext() val context = browser.newContext()
if(failDomainList.contains(domain)) {
println("실패한 도메인 스크래핑 종료 $domain ")
return ""
}
return try { return try {
context.use { ctx -> context.use { ctx ->
ctx.newPage().use { page -> ctx.newPage().use { page ->
delay(Random.nextInt(1000).toLong()) delay(Random.nextInt(2000).toLong())
// 1. 리스너 설정 시 예외 처리 강화 // 1. 리스너 설정 시 예외 처리 강화
blockUnnecessaryResources(page) blockUnnecessaryResources(page)
// 2. 타임아웃을 설정하여 무한 대기 방지 // 2. 타임아웃을 설정하여 무한 대기 방지
val options = Page.NavigateOptions().setTimeout(30000.0) val options = Page.NavigateOptions().setTimeout(8000.0)
page.navigate(url, options) page.navigate(url, options)
// 3. 페이지가 완전히 닫히기 전에 모든 대기 중인 이벤트를 해제하기 위해 LOAD 상태 대기 // 3. 페이지가 완전히 닫히기 전에 모든 대기 중인 이벤트를 해제하기 위해 LOAD 상태 대기
@ -119,6 +125,7 @@ object DynamicNewsScraper {
} }
} }
} catch (e: Exception) { } catch (e: Exception) {
failDomainList.add(domain)
println("❌ [Playwright] 스크래핑 실패 ($url): ${e.message}") println("❌ [Playwright] 스크래핑 실패 ($url): ${e.message}")
"" ""
} finally { } finally {

View File

@ -1,6 +1,9 @@
package service package service
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import org.slf4j.LoggerFactory
import ch.qos.logback.classic.Level
import ch.qos.logback.classic.Logger
object SystemSleepPreventer { object SystemSleepPreventer {
private var process: Process? = null private var process: Process? = null
@ -9,6 +12,9 @@ object SystemSleepPreventer {
* 맥의 절전 모드 디스플레이 취침을 방지하는 명령 실행 * 맥의 절전 모드 디스플레이 취침을 방지하는 명령 실행
*/ */
fun start() { fun start() {
val root = LoggerFactory.getLogger("Exposed") as Logger
root.level = Level.ERROR
if (process?.isAlive == true) return if (process?.isAlive == true) return
try { try {

View File

@ -159,7 +159,6 @@ fun DashboardScreen() {
// 3. 실시간 체결 통보 핸들러 (주문번호 중심) // 3. 실시간 체결 통보 핸들러 (주문번호 중심)
wsManager.onExecutionReceived = {code, qty, price,orderNo, isBuy -> wsManager.onExecutionReceived = {code, qty, price,orderNo, isBuy ->
scope.launch { scope.launch {
println("$orderNo, $code, $price, $qty, $isBuy")
val exec = ExecutionData(orderNo, code, price, qty, isBuy) val exec = ExecutionData(orderNo, code, price, qty, isBuy)
executionCache[orderNo] = exec executionCache[orderNo] = exec
syncAndExecute(orderNo) syncAndExecute(orderNo)

View File

@ -82,12 +82,12 @@ fun IntegratedOrderSection(
// 계산용 변수 // 계산용 변수
val curPriceNum = currentPrice.replace(",", "").toDoubleOrNull() ?: 0.0 val curPriceNum = currentPrice.replace(",", "").toDoubleOrNull() ?: 0.0
val basePrice = (if (orderPrice.isEmpty()) curPriceNum else orderPrice.toDoubleOrNull() ?: 0.0) val basePrice = (if (orderPrice.isEmpty()) curPriceNum else orderPrice.toDoubleOrNull() ?: 0.0)
val inputQty = orderQty.replace(",", "").toIntOrNull() ?: 0
fun excuteTrade(willEnableAutoSell: Boolean, orderQty: String, profitRate1: Double?) {
fun excuteTrade(willEnableAutoSell: Boolean, orderQty: String, profitRate1: Double?,confidence : Boolean = false) {
scope.launch { scope.launch {
val tickSize = MarketUtil.getTickSize(basePrice) val tickSize = MarketUtil.getTickSize(basePrice)
val oneTickLowerPrice = basePrice - tickSize val oneTickLowerPrice = basePrice - (tickSize * if (confidence) { 1} else {2})
// 2. 주문 가격 설정 (직접 입력값이 없으면 한 틱 낮은 가격 사용) // 2. 주문 가격 설정 (직접 입력값이 없으면 한 틱 낮은 가격 사용)
val finalPrice = if (orderPrice.isBlank()) { val finalPrice = if (orderPrice.isBlank()) {
@ -114,7 +114,7 @@ fun IntegratedOrderSection(
// 4. 보정된 수익률을 적용하여 목표가 계산 // 4. 보정된 수익률을 적용하여 목표가 계산
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 calculatedStop = MarketUtil.roundToTickSize(basePrice * (1 + sRate / 100.0))
val inputQty = orderQty.replace(",", "").toIntOrNull() ?: 0
// 5. DB 저장 (effectiveProfitRate를 저장하여 분석 시 실제 목표치를 확인 가능하게 함) // 5. DB 저장 (effectiveProfitRate를 저장하여 분석 시 실제 목표치를 확인 가능하게 함)
DatabaseFactory.saveAutoTrade(AutoTradeItem( DatabaseFactory.saveAutoTrade(AutoTradeItem(
orderNo = realOrderNo, orderNo = realOrderNo,
@ -146,13 +146,7 @@ fun IntegratedOrderSection(
completeTradingDecision.stockCode.equals(stockCode)) { completeTradingDecision.stockCode.equals(stockCode)) {
fun resultCheck(completeTradingDecision :TradingDecision) { fun resultCheck(completeTradingDecision :TradingDecision) {
println("""
corpName : ${completeTradingDecision.corpName}
confidence : ${completeTradingDecision.confidence + append}
shortPossible : ${completeTradingDecision.shortPossible() + append}
profitPossible : ${completeTradingDecision.profitPossible()+ append}
safePossible : ${completeTradingDecision.safePossible()+ append}
""".trimIndent())
val weights = mapOf( val weights = mapOf(
"short" to 0.3, // 초단기 점수가 낮아도 전체에 미치는 영향 감소 "short" to 0.3, // 초단기 점수가 낮아도 전체에 미치는 영향 감소
@ -169,8 +163,15 @@ fun IntegratedOrderSection(
// 3. 매수 결정 문턱값 (예: 70점 이상이면 매수 가능) // 3. 매수 결정 문턱값 (예: 70점 이상이면 매수 가능)
val MIN_PURCHASE_SCORE = 68.0 val MIN_PURCHASE_SCORE = 68.0
val HIGH_QUALITY_SCORE = 85.0 // 강력 추천 기준 val HIGH_QUALITY_SCORE = 85.0 // 강력 추천 기준
println("""
if (totalScore >= MIN_PURCHASE_SCORE && completeTradingDecision.confidence > MIN_CONFIDENCE) { corpName : ${completeTradingDecision.corpName}
confidence : ${completeTradingDecision.confidence + append}
shortPossible : ${completeTradingDecision.shortPossible() + append}
profitPossible : ${completeTradingDecision.profitPossible()+ append}
safePossible : ${completeTradingDecision.safePossible()+ append}
totalScore : ${totalScore}
""".trimIndent())
if (totalScore >= MIN_PURCHASE_SCORE && completeTradingDecision.confidence >= MIN_CONFIDENCE) {
// 4. 점수에 따른 가변 마진 적용 // 4. 점수에 따른 가변 마진 적용
// 토탈 스코어가 85점 이상이면 마진을 3.0으로 고정하거나 추가 가산(append) 적용 // 토탈 스코어가 85점 이상이면 마진을 3.0으로 고정하거나 추가 가산(append) 적용
@ -182,16 +183,23 @@ fun IntegratedOrderSection(
} }
println("🚀 [매수 진행] 토탈 스코어: ${String.format("%.1f", totalScore)} -> 종목: ${completeTradingDecision.stockCode}") println("🚀 [매수 진행] 토탈 스코어: ${String.format("%.1f", totalScore)} -> 종목: ${completeTradingDecision.stockCode}")
val MAX_BUDGET = 25000.0
// basePrice(현재가 혹은 지정가)를 기준으로 매수 가능 수량 산출 (최소 1주 보장)
val calculatedQty = if (basePrice > 0) {
(MAX_BUDGET / basePrice).toInt().coerceAtLeast(1)
} else {
1
}
// 5. 매수 실행 (계산된 finalMargin 전달) // 5. 매수 실행 (계산된 finalMargin 전달)
excuteTrade( excuteTrade(
willEnableAutoSell = true, willEnableAutoSell = true,
orderQty = "1", orderQty = calculatedQty.toString(),
profitRate1 = finalMargin profitRate1 = finalMargin,
confidence = totalScore >= HIGH_QUALITY_SCORE
) )
} else { } else {
println("✋ [관망] 토탈 스코어(${String.format("%.1f", totalScore)})가 기준치($MIN_PURCHASE_SCORE) 미달") println("✋ [관망] 토탈 스코어(${String.format("%.1f", totalScore)})가 기준치($MIN_PURCHASE_SCORE) 미달 또는 신뢰도 ${completeTradingDecision.confidence}가 기준치 ${MIN_CONFIDENCE} 미달")
} }
} }
when (completeTradingDecision?.decision) { when (completeTradingDecision?.decision) {
@ -228,7 +236,7 @@ fun IntegratedOrderSection(
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)
) )
} }
val inputQty = orderQty.replace(",", "").toIntOrNull() ?: 0
// 수익률 시뮬레이션 표 // 수익률 시뮬레이션 표
if (basePrice > 0 && inputQty > 0) { if (basePrice > 0 && inputQty > 0) {
SimulationCard(basePrice, inputQty.toDouble()) SimulationCard(basePrice, inputQty.toDouble())

View File

@ -0,0 +1,7 @@
<configuration>
<logger name="Exposed" level="OFF" />
<root level="INFO">
<appender-ref ref="STDOUT" />
</root>
</configuration>