...
This commit is contained in:
parent
a51ffb6193
commit
1787b72499
@ -24,6 +24,7 @@ import network.DartCodeManager
|
|||||||
import service.LlamaServerManager
|
import service.LlamaServerManager
|
||||||
import network.NewsService
|
import network.NewsService
|
||||||
import org.jetbrains.exposed.sql.selectAll
|
import org.jetbrains.exposed.sql.selectAll
|
||||||
|
import service.SystemSleepPreventer
|
||||||
import ui.DashboardScreen
|
import ui.DashboardScreen
|
||||||
import ui.SettingsScreen
|
import ui.SettingsScreen
|
||||||
|
|
||||||
@ -31,6 +32,7 @@ import ui.SettingsScreen
|
|||||||
enum class AppScreen { Settings, Dashboard }
|
enum class AppScreen { Settings, Dashboard }
|
||||||
|
|
||||||
fun main() = application {
|
fun main() = application {
|
||||||
|
SystemSleepPreventer.start()
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
// NewsService나 KisTradeService에서 사용하는 client를 전달
|
// NewsService나 KisTradeService에서 사용하는 client를 전달
|
||||||
DartCodeManager.updateCorpCodes(HttpClient(CIO) {
|
DartCodeManager.updateCorpCodes(HttpClient(CIO) {
|
||||||
|
|||||||
@ -2,6 +2,9 @@ package model
|
|||||||
|
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
|
|
||||||
|
const val feesAndTaxRate = 0.3
|
||||||
|
const val minimumNetProfit = 0.8
|
||||||
|
|
||||||
data class AppConfig(
|
data class AppConfig(
|
||||||
// [DB 저장 데이터]
|
// [DB 저장 데이터]
|
||||||
// 실전 3종
|
// 실전 3종
|
||||||
|
|||||||
@ -127,7 +127,7 @@ object RagService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun processStock(stockName: String,stockCode: String,result : TradingDecisionCallback) {
|
suspend fun processStock(technicalAnalyzer: TechnicalAnalyzer,stockName: String,stockCode: String,result : TradingDecisionCallback) {
|
||||||
// 1. 10분간의 데이터 가져오기 (API 호출)
|
// 1. 10분간의 데이터 가져오기 (API 호출)
|
||||||
coroutineScope {
|
coroutineScope {
|
||||||
try {
|
try {
|
||||||
@ -146,7 +146,7 @@ object RagService {
|
|||||||
tradingDecision.financialData = financialDataDeferred.await()
|
tradingDecision.financialData = financialDataDeferred.await()
|
||||||
result(tradingDecision, false)
|
result(tradingDecision, false)
|
||||||
|
|
||||||
tradingDecision.techSummary = TechnicalAnalyzer.generateComprehensiveReport()
|
tradingDecision.techSummary = technicalAnalyzer.generateComprehensiveReport()
|
||||||
result(tradingDecision, false)
|
result(tradingDecision, false)
|
||||||
|
|
||||||
val question = "${corpInfo?.cName} $stockName[$stockCode]의 향후 실적 전망과 관련된 핵심 뉴스"
|
val question = "${corpInfo?.cName} $stockName[$stockCode]의 향후 실적 전망과 관련된 핵심 뉴스"
|
||||||
@ -333,6 +333,11 @@ class TradingDecision {
|
|||||||
var newsContext : String? = null
|
var newsContext : String? = null
|
||||||
var financialData : String? = null
|
var financialData : String? = null
|
||||||
|
|
||||||
|
|
||||||
|
fun shortPossible() =
|
||||||
|
listOf<Double>(ultraShortScore,
|
||||||
|
shortTermScore).average()
|
||||||
|
|
||||||
fun profitPossible() =
|
fun profitPossible() =
|
||||||
listOf<Double>(ultraShortScore,
|
listOf<Double>(ultraShortScore,
|
||||||
shortTermScore,
|
shortTermScore,
|
||||||
|
|||||||
@ -40,7 +40,7 @@ object AutoTradingManager {
|
|||||||
try {
|
try {
|
||||||
// 1. [체크] 현재 잔고 및 보유 종목 조회
|
// 1. [체크] 현재 잔고 및 보유 종목 조회
|
||||||
val balanceResult = tradeService.fetchIntegratedBalance().getOrNull()
|
val balanceResult = tradeService.fetchIntegratedBalance().getOrNull()
|
||||||
val myHoldings = balanceResult?.holdings?.map { it.code }?.toSet() ?: emptySet()
|
val myHoldings = balanceResult?.holdings?.filter { it.quantity.toInt() > 0 }?.map { it.code }?.toSet() ?: emptySet()
|
||||||
val myCash = balanceResult?.deposit?.replace(",", "")?.toLongOrNull() ?: 0L
|
val myCash = balanceResult?.deposit?.replace(",", "")?.toLongOrNull() ?: 0L
|
||||||
|
|
||||||
println("💰 보유 현금: ${String.format("%,d", myCash)}원 | 보유 종목 수: ${myHoldings.size}")
|
println("💰 보유 현금: ${String.format("%,d", myCash)}원 | 보유 종목 수: ${myHoldings.size}")
|
||||||
@ -49,31 +49,42 @@ object AutoTradingManager {
|
|||||||
// 1. 랭킹 데이터 가져오기 (비동기)
|
// 1. 랭킹 데이터 가져오기 (비동기)
|
||||||
val volRankDeferred = async { tradeService.fetchMarketRanking(RankingType.VOLUME, true).getOrDefault(emptyList()) }
|
val volRankDeferred = async { tradeService.fetchMarketRanking(RankingType.VOLUME, true).getOrDefault(emptyList()) }
|
||||||
val riseRankDeferred = async { tradeService.fetchMarketRanking(RankingType.RISE, true).getOrDefault(emptyList()) }
|
val riseRankDeferred = async { tradeService.fetchMarketRanking(RankingType.RISE, true).getOrDefault(emptyList()) }
|
||||||
|
// 거래대금(Amount) 상위 추가
|
||||||
|
val amountRankDeferred = async { tradeService.fetchMarketRanking(RankingType.VALUE, true).getOrDefault(emptyList()) }
|
||||||
|
val volumePowerDeferred = async { tradeService.fetchMarketRanking(RankingType.VOLUME_POWER, true).getOrDefault(emptyList()) }
|
||||||
val volList = volRankDeferred.await()
|
val volList = volRankDeferred.await()
|
||||||
val riseList = riseRankDeferred.await()
|
val riseList = riseRankDeferred.await()
|
||||||
|
val amountList = amountRankDeferred.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 // 너무 과열되지 않은 주도주
|
||||||
|
}
|
||||||
|
|
||||||
// [수정] 2. 의미 있는 후보군 선정 (단순 상위 15개가 아님)
|
|
||||||
|
|
||||||
// (A) 거래량 상위 종목 중: 현재가 기준 등락률이 0% ~ 20% 사이인 것만 필터링 -> 상위 10개
|
|
||||||
val volCandidates = volList
|
val volCandidates = volList
|
||||||
.filter { stock ->
|
.filter { stock ->
|
||||||
val rate = stock.prdy_ctrt.toDoubleOrNull() ?: 0.0
|
val rate = stock.prdy_ctrt.toDoubleOrNull() ?: 0.0
|
||||||
rate in 0.0..20.0 // 0% 초과 20% 이하
|
rate in 1.0..18.0 // 0% 초과 20% 이하
|
||||||
}
|
}
|
||||||
.take(10)
|
|
||||||
|
|
||||||
// (B) 상승률 상위 종목 중: 너무 급등한(20% 초과) 종목은 제외하고, 적당히 오르고 있는 종목만 필터링 -> 상위 10개
|
// (B) 상승률 상위 종목 중: 너무 급등한(20% 초과) 종목은 제외하고, 적당히 오르고 있는 종목만 필터링 -> 상위 10개
|
||||||
// 보통 상승률 랭킹은 상한가(30%)부터 내려오므로, 앞쪽의 급등주를 건너뛰어야 함
|
// 보통 상승률 랭킹은 상한가(30%)부터 내려오므로, 앞쪽의 급등주를 건너뛰어야 함
|
||||||
val riseCandidates = riseList
|
val riseCandidates = riseList
|
||||||
.filter { stock ->
|
.filter { stock ->
|
||||||
val rate = stock.prdy_ctrt.toDoubleOrNull() ?: 0.0
|
val rate = stock.prdy_ctrt.toDoubleOrNull() ?: 0.0
|
||||||
rate in 3.0..20.0 // 최소 3% 이상은 올라야 의미 있음, 20% 이하는 안전 구간
|
rate in 2.0..18.0 // 최소 3% 이상은 올라야 의미 있음, 20% 이하는 안전 구간
|
||||||
}
|
}
|
||||||
.take(10)
|
|
||||||
|
|
||||||
// 3. 두 리스트 합치기 (중복 제거)
|
val volumeCandidates = volumeList .filter { stock ->
|
||||||
val candidates = (volCandidates + riseCandidates).distinctBy { it.code }
|
val rate = stock.prdy_ctrt.toDoubleOrNull() ?: 0.0
|
||||||
|
rate in 1.0..18.0 // 최소 3% 이상은 올라야 의미 있음, 20% 이하는 안전 구간
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 리스트 합치기 (중복 제거)
|
||||||
|
val candidates = (volCandidates + riseCandidates + amountCandidates + volumeCandidates).distinctBy { it.code }
|
||||||
|
|
||||||
println("🔎 1차 필터링 후보 ${candidates.size}개 (급등주 제외) 검증 시작...")
|
println("🔎 1차 필터링 후보 ${candidates.size}개 (급등주 제외) 검증 시작...")
|
||||||
|
|
||||||
@ -84,14 +95,19 @@ object AutoTradingManager {
|
|||||||
val currentPrice = stock.stck_prpr.replace(",", "").toDoubleOrNull() ?: 0.0
|
val currentPrice = stock.stck_prpr.replace(",", "").toDoubleOrNull() ?: 0.0
|
||||||
|
|
||||||
// [조건 2] 최소 1주 매수 가능 여부
|
// [조건 2] 최소 1주 매수 가능 여부
|
||||||
if (currentPrice > myCash) return@forEach
|
if (currentPrice > myCash || currentPrice > 5000) return@forEach
|
||||||
|
callback(TradingDecision().apply {
|
||||||
|
this.stockCode = stock.code
|
||||||
|
this.confidence = -1.0
|
||||||
|
this.stockName = stock.name
|
||||||
|
}, false)
|
||||||
// 3. 일봉 데이터 조회 (필터링 용도 + TechnicalAnalyzer 입력용)
|
// 3. 일봉 데이터 조회 (필터링 용도 + TechnicalAnalyzer 입력용)
|
||||||
val dailyResult = tradeService.fetchPeriodChartData(stock.code, "D", true)
|
val dailyResult = tradeService.fetchPeriodChartData(stock.code, "D", true)
|
||||||
val dailyData = dailyResult.getOrNull()
|
val dailyData = dailyResult.getOrNull()
|
||||||
val todayCandle = dailyData?.lastOrNull()
|
val todayCandle = dailyData?.lastOrNull()
|
||||||
|
|
||||||
if (dailyData != null && todayCandle != null) {
|
if (dailyData != null && todayCandle != null) {
|
||||||
|
|
||||||
val open = todayCandle.stck_oprc.toDoubleOrNull() ?: 0.0
|
val open = todayCandle.stck_oprc.toDoubleOrNull() ?: 0.0
|
||||||
val current = todayCandle.stck_prpr.toDoubleOrNull() ?: 0.0
|
val current = todayCandle.stck_prpr.toDoubleOrNull() ?: 0.0
|
||||||
|
|
||||||
@ -113,14 +129,14 @@ object AutoTradingManager {
|
|||||||
val monthlyData = monthDef.await()
|
val monthlyData = monthDef.await()
|
||||||
|
|
||||||
// TechnicalAnalyzer 상태 업데이트 (싱글톤이므로 순차 처리 필수)
|
// TechnicalAnalyzer 상태 업데이트 (싱글톤이므로 순차 처리 필수)
|
||||||
TechnicalAnalyzer.clear()
|
val t = TechnicalAnalyzer()
|
||||||
TechnicalAnalyzer.daily = dailyData
|
t.daily = dailyData
|
||||||
TechnicalAnalyzer.weekly = weeklyData
|
t.weekly = weeklyData
|
||||||
TechnicalAnalyzer.monthly = monthlyData
|
t.monthly = monthlyData
|
||||||
TechnicalAnalyzer.min30 = min30Data
|
t.min30 = min30Data
|
||||||
|
|
||||||
// 데이터 준비 완료 후 AI 분석 요청 (suspend 함수이므로 완료될 때까지 대기 -> 데이터 섞임 방지)
|
// 데이터 준비 완료 후 AI 분석 요청 (suspend 함수이므로 완료될 때까지 대기 -> 데이터 섞임 방지)
|
||||||
RagService.processStock(stock.name, stock.code) { decision, isSuccess ->
|
RagService.processStock(t,stock.name, stock.code) { decision, isSuccess ->
|
||||||
if (decision != null) {
|
if (decision != null) {
|
||||||
decision.stockName = stock.name
|
decision.stockName = stock.name
|
||||||
decision.currentPrice = current // 차트에서 확인한 최신 현재가 주입
|
decision.currentPrice = current // 차트에서 확인한 최신 현재가 주입
|
||||||
@ -133,13 +149,31 @@ object AutoTradingManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
delay(100) // 종목 간 API 호출 간격
|
delay(300) // 종목 간 API 호출 간격
|
||||||
}
|
}
|
||||||
println("💤 사이클 종료. 5분 대기...")
|
|
||||||
|
|
||||||
|
// --- 10초 주기 로그 대기 로직 시작 ---
|
||||||
|
val waitMinutes = 3
|
||||||
|
val totalWaitMillis = waitMinutes * 60 * 1000L
|
||||||
|
val tickMillis = 10 * 1000L
|
||||||
|
var currentWait = 0L
|
||||||
|
println("💤 사이클 종료. ${waitMinutes}분 대기...")
|
||||||
|
println("✅ 이번 사이클 분석 완료.")
|
||||||
|
while (currentWait < totalWaitMillis && discoveryJob?.isActive == true) {
|
||||||
|
delay(tickMillis)
|
||||||
|
currentWait += tickMillis
|
||||||
|
val leftSec = (totalWaitMillis - currentWait) / 1000
|
||||||
|
// 1분 단위 혹은 10초 단위로 자유롭게 로그 조절 가능
|
||||||
|
if (leftSec % 30 == 0L || leftSec <= 30) {
|
||||||
|
println("📡 [AutoTrading] 시스템 정상 작동 중... (다음 분석 ${leftSec}초 전)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
println("⚠️ 루프 오류: ${e.message}")
|
println("⚠️ 루프 오류: ${e.message}")
|
||||||
|
delay(10000) // 오류 발생 시 10초 후 재시도
|
||||||
}
|
}
|
||||||
delay(5 * 60 * 1000) // 5분
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -152,9 +186,9 @@ object AutoTradingManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 기존 단일 종목 추가 로직 (유지)
|
// 기존 단일 종목 추가 로직 (유지)
|
||||||
fun addStock(stockName: String, stockCode: String, result: TradingDecisionCallback) {
|
fun addStock(technicalAnalyzer : TechnicalAnalyzer,stockName: String, stockCode: String, result: TradingDecisionCallback) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
RagService.processStock(stockName, stockCode, result)
|
RagService.processStock(technicalAnalyzer,stockName, stockCode, result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -166,7 +200,7 @@ object AutoTradingManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
object 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()
|
||||||
@ -184,21 +218,21 @@ object TechnicalAnalyzer {
|
|||||||
): InvestmentScores {
|
): InvestmentScores {
|
||||||
|
|
||||||
// 1. 초단기 (분봉 + 에너지 지표 위주)
|
// 1. 초단기 (분봉 + 에너지 지표 위주)
|
||||||
val ultra = (TechnicalAnalyzer.calculateMFI(min30, 14) * 0.4 +
|
val ultra = (calculateMFI(min30, 14) * 0.4 +
|
||||||
TechnicalAnalyzer.calculateStochastic(min30) * 0.3 +
|
calculateStochastic(min30) * 0.3 +
|
||||||
(if(TechnicalAnalyzer.calculateChange(min30.takeLast(10)) > 0) 30 else 0)).toInt()
|
(if(calculateChange(min30.takeLast(10)) > 0) 30 else 0)).toInt()
|
||||||
|
|
||||||
// 2. 단기 (일봉 추세 + OBV 에너지)
|
// 2. 단기 (일봉 추세 + OBV 에너지)
|
||||||
val short = (TechnicalAnalyzer.calculateRSI(daily) * 0.3 +
|
val short = (calculateRSI(daily) * 0.3 +
|
||||||
(if(TechnicalAnalyzer.calculateOBV(daily) > 0) 40 else 10) +
|
(if(calculateOBV(daily) > 0) 40 else 10) +
|
||||||
(if(TechnicalAnalyzer.calculateChange(daily.takeLast(3)) > 0) 30 else 0)).toInt()
|
(if(calculateChange(daily.takeLast(3)) > 0) 30 else 0)).toInt()
|
||||||
|
|
||||||
// 3. 중기 (주봉 + 재무 점수 혼합)
|
// 3. 중기 (주봉 + 재무 점수 혼합)
|
||||||
val mid = (if(TechnicalAnalyzer.calculateChange(weekly) > 0) 40 else 10) +
|
val mid = (if(calculateChange(weekly) > 0) 40 else 10) +
|
||||||
(financialScore * 0.6).toInt()
|
(financialScore * 0.6).toInt()
|
||||||
|
|
||||||
// 4. 장기 (월봉 + 섹터/기업 펀더멘털)
|
// 4. 장기 (월봉 + 섹터/기업 펀더멘털)
|
||||||
val long = (if(TechnicalAnalyzer.calculateChange(monthly) > 0) 50 else 0) +
|
val long = (if(calculateChange(monthly) > 0) 50 else 0) +
|
||||||
(financialScore * 0.5).toInt()
|
(financialScore * 0.5).toInt()
|
||||||
|
|
||||||
return InvestmentScores(
|
return InvestmentScores(
|
||||||
@ -390,8 +424,8 @@ class ScalpingAnalyzer {
|
|||||||
private const val BB_LOWER_POS = 0.2
|
private const val BB_LOWER_POS = 0.2
|
||||||
private const val BB_UPPER_POS = 0.8
|
private const val BB_UPPER_POS = 0.8
|
||||||
private const val ATR_WINDOW = 14
|
private const val ATR_WINDOW = 14
|
||||||
private const val DEFAULT_SL_PCT = -0.5
|
private const val DEFAULT_SL_PCT = -1.5
|
||||||
private const val DEFAULT_TP_PCT = 1.0
|
private const val DEFAULT_TP_PCT = 1.5
|
||||||
private const val HIGH_SCORE_THRESHOLD = 80
|
private const val HIGH_SCORE_THRESHOLD = 80
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -3,6 +3,7 @@ package service
|
|||||||
import com.microsoft.playwright.Playwright
|
import com.microsoft.playwright.Playwright
|
||||||
import com.microsoft.playwright.BrowserType
|
import com.microsoft.playwright.BrowserType
|
||||||
import com.microsoft.playwright.Page
|
import com.microsoft.playwright.Page
|
||||||
|
import com.microsoft.playwright.options.LoadState
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.awaitAll
|
import kotlinx.coroutines.awaitAll
|
||||||
import kotlinx.coroutines.coroutineScope
|
import kotlinx.coroutines.coroutineScope
|
||||||
@ -92,33 +93,58 @@ object DynamicNewsScraper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun fetchFullContent(url: String): String {
|
suspend fun fetchFullContent(url: String): String {
|
||||||
|
// browser.newContext().use { ... } 대신 직접 변수를 선언하고 제어합니다.
|
||||||
val context = browser.newContext()
|
val context = browser.newContext()
|
||||||
val page = context.newPage()
|
|
||||||
delay(Random.nextInt(1000).toLong())
|
|
||||||
return try {
|
return try {
|
||||||
// 1. 페이지 이동 및 네트워크 유휴 상태까지 대기
|
context.use { ctx ->
|
||||||
blockUnnecessaryResources(page)
|
ctx.newPage().use { page ->
|
||||||
page.navigate(url)
|
delay(Random.nextInt(1000).toLong())
|
||||||
// println(url)
|
|
||||||
page.waitForLoadState()
|
|
||||||
|
|
||||||
|
// 1. 리스너 설정 시 예외 처리 강화
|
||||||
|
blockUnnecessaryResources(page)
|
||||||
|
|
||||||
var finded = cleanText(extractSmartContentWithLineFilter(page))
|
// 2. 타임아웃을 설정하여 무한 대기 방지
|
||||||
println("finded : $finded")
|
val options = Page.NavigateOptions().setTimeout(30000.0)
|
||||||
finded
|
page.navigate(url, options)
|
||||||
|
|
||||||
|
// 3. 페이지가 완전히 닫히기 전에 모든 대기 중인 이벤트를 해제하기 위해 LOAD 상태 대기
|
||||||
|
page.waitForLoadState(LoadState.LOAD)
|
||||||
|
|
||||||
|
val content = cleanText(extractSmartContentWithLineFilter(page))
|
||||||
|
|
||||||
|
// 4. 명시적으로 route를 해제하여 close 시 발생할 수 있는 리스너 충돌 방지
|
||||||
|
page.unroute("**/*")
|
||||||
|
|
||||||
|
content
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
println("❌ [Playwright] 스크래핑 실패: ${e.message}")
|
println("❌ [Playwright] 스크래핑 실패 ($url): ${e.message}")
|
||||||
""
|
""
|
||||||
} finally {
|
} finally {
|
||||||
page.close()
|
// use 블록이 자원을 닫으려 할 때 발생하는 오류는 내부적으로 처리되거나 무시되도록 유도
|
||||||
context.close()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun blockUnnecessaryResources(page: Page) {
|
private fun blockUnnecessaryResources(page: Page) {
|
||||||
// 이미지, 폰트, CSS 등 불필요한 요청 가로채서 중단
|
// 이미지, 폰트, CSS 등 불필요한 요청 가로채서 중단
|
||||||
page.route("**/*.{png,jpg,jpeg,gif,webp,svg,css,woff,woff2}") { route ->
|
page.route("**/*") { route ->
|
||||||
route.abort()
|
try {
|
||||||
|
val req = route.request()
|
||||||
|
if (req != null) {
|
||||||
|
val type = req.resourceType()
|
||||||
|
if (type == "image" || type == "font" || type == "stylesheet") {
|
||||||
|
route.abort()
|
||||||
|
} else {
|
||||||
|
route.resume()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// request가 이미 null이면 처리를 포기
|
||||||
|
route.resume()
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -138,16 +164,25 @@ object SafeScraper {
|
|||||||
urls.map { item ->
|
urls.map { item ->
|
||||||
async {
|
async {
|
||||||
if (UrlCacheManager.isAlreadyProcessed(item.originallink) == false) {
|
if (UrlCacheManager.isAlreadyProcessed(item.originallink) == false) {
|
||||||
semaphore.withPermit {
|
try {
|
||||||
RagService.ingestWithChunking(
|
semaphore.withPermit {
|
||||||
text = DynamicNewsScraper.fetchFullContent(item.originallink),
|
try {
|
||||||
newsLink = item.originallink,
|
RagService.ingestWithChunking(
|
||||||
pubDate = item.pubDate,
|
text = DynamicNewsScraper.fetchFullContent(item.originallink),
|
||||||
stockCode = corpInfo.stockCode,
|
newsLink = item.originallink,
|
||||||
corpName = corpInfo.cName,
|
pubDate = item.pubDate,
|
||||||
corpCode = corpInfo.cCode,
|
stockCode = corpInfo.stockCode,
|
||||||
stcokName = corpInfo.stockName
|
corpName = corpInfo.cName,
|
||||||
)
|
corpCode = corpInfo.cCode,
|
||||||
|
stcokName = corpInfo.stockName
|
||||||
|
)
|
||||||
|
}catch (e: Exception) {
|
||||||
|
println("${e.message}")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}catch (e: Exception) {
|
||||||
|
println("${e.message}")
|
||||||
}
|
}
|
||||||
println("📰 '${query}' 관련 뉴스 새로운 학습 데이터 게더링")
|
println("📰 '${query}' 관련 뉴스 새로운 학습 데이터 게더링")
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
35
src/main/kotlin/service/SystemSleepPreventer.kt
Normal file
35
src/main/kotlin/service/SystemSleepPreventer.kt
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
object SystemSleepPreventer {
|
||||||
|
private var process: Process? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 맥의 절전 모드 및 디스플레이 취침을 방지하는 명령 실행
|
||||||
|
*/
|
||||||
|
fun start() {
|
||||||
|
if (process?.isAlive == true) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
// -i: 시스템 절전 방지, -d: 디스플레이 취침 방지, -m: 디스크 유휴 상태 방지
|
||||||
|
val command = listOf("caffeinate", "-i", "-d", "-m")
|
||||||
|
process = ProcessBuilder(command).start()
|
||||||
|
println("☕ [System] caffeinate 실행됨: 앱이 켜져 있는 동안 절전 모드가 방지됩니다.")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
println("⚠️ [System] caffeinate 실행 실패: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 앱 종료 시 프로세스 함께 종료
|
||||||
|
*/
|
||||||
|
fun stop() {
|
||||||
|
process?.destroy()
|
||||||
|
// 프로세스가 강제 종료되지 않을 경우를 대비해 0.5초 대기 후 강제 종료
|
||||||
|
if (process?.waitFor(500, TimeUnit.MILLISECONDS) == false) {
|
||||||
|
process?.destroyForcibly()
|
||||||
|
}
|
||||||
|
println("🛑 [System] caffeinate 종료됨: 시스템 절전 설정이 정상화됩니다.")
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -26,10 +26,11 @@ import androidx.compose.ui.unit.dp
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import model.KisSession
|
import model.KisSession
|
||||||
import service.AutoTradingManager
|
import service.AutoTradingManager
|
||||||
|
import service.TechnicalAnalyzer
|
||||||
import service.TradingDecisionCallback
|
import service.TradingDecisionCallback
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AiAnalysisView(stockCode:String,stockName: String, currentPrice: String, trades: List<model.RealTimeTrade>, tradingDecisionCallback: TradingDecisionCallback) {
|
fun AiAnalysisView(technicalAnalyzer: TechnicalAnalyzer,stockCode:String,stockName: String, currentPrice: String, trades: List<model.RealTimeTrade>, tradingDecisionCallback: TradingDecisionCallback) {
|
||||||
var aiOpinion by remember { mutableStateOf("분석 대기 중...") }
|
var aiOpinion by remember { mutableStateOf("분석 대기 중...") }
|
||||||
var code by remember(stockCode) {
|
var code by remember(stockCode) {
|
||||||
aiOpinion = ""
|
aiOpinion = ""
|
||||||
@ -67,7 +68,7 @@ fun AiAnalysisView(stockCode:String,stockName: String, currentPrice: String, tra
|
|||||||
scope.launch {
|
scope.launch {
|
||||||
isAnalyzing = true
|
isAnalyzing = true
|
||||||
try {
|
try {
|
||||||
AutoTradingManager.addStock(stockName,stockCode) { decision,success ->
|
AutoTradingManager.addStock(technicalAnalyzer,stockName,stockCode) { decision,success ->
|
||||||
aiOpinion = decision.toString()
|
aiOpinion = decision.toString()
|
||||||
isAnalyzing = !success
|
isAnalyzing = !success
|
||||||
tradingDecisionCallback.invoke(decision,success)
|
tradingDecisionCallback.invoke(decision,success)
|
||||||
|
|||||||
@ -12,12 +12,18 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import model.CandleData
|
||||||
import model.ExecutionData
|
import model.ExecutionData
|
||||||
import model.KisSession
|
import model.KisSession
|
||||||
import model.StockBasicInfo
|
import model.StockBasicInfo
|
||||||
|
import model.feesAndTaxRate
|
||||||
|
import model.minimumNetProfit
|
||||||
import network.KisTradeService
|
import network.KisTradeService
|
||||||
import network.KisWebSocketManager
|
import network.KisWebSocketManager
|
||||||
import service.AutoTradingManager
|
import service.AutoTradingManager
|
||||||
|
import service.TechnicalAnalyzer
|
||||||
|
import util.MarketUtil
|
||||||
|
import kotlin.collections.mutableListOf
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DashboardScreen() {
|
fun DashboardScreen() {
|
||||||
@ -33,17 +39,33 @@ fun DashboardScreen() {
|
|||||||
var selectedStockInfo by remember { mutableStateOf<StockBasicInfo?>(null) } // 단순 종목 선택 시
|
var selectedStockInfo by remember { mutableStateOf<StockBasicInfo?>(null) } // 단순 종목 선택 시
|
||||||
var completeTradingDecision by remember { mutableStateOf<TradingDecision?>(null) } // 단순 종목 선택 시
|
var completeTradingDecision by remember { mutableStateOf<TradingDecision?>(null) } // 단순 종목 선택 시
|
||||||
|
|
||||||
|
|
||||||
|
var min30 by remember { mutableStateOf<MutableList<CandleData>>(mutableListOf()) }
|
||||||
|
var daySummary by remember { mutableStateOf<MutableList<CandleData>>(mutableListOf()) }
|
||||||
|
var weekSummary by remember { mutableStateOf<MutableList<CandleData>>(mutableListOf()) }
|
||||||
|
var monthSummary by remember { mutableStateOf<MutableList<CandleData>>(mutableListOf()) }
|
||||||
|
var yearSummary by remember { mutableStateOf<MutableList<CandleData>>(mutableListOf()) }
|
||||||
|
|
||||||
DisposableEffect(Unit) {
|
DisposableEffect(Unit) {
|
||||||
// 1. 화면 진입 시: 자동 발굴 루프 시작
|
// 1. 화면 진입 시: 자동 발굴 루프 시작
|
||||||
// AI 분석 결과(decision)가 나오면 completeTradingDecision 상태를 업데이트하여
|
// AI 분석 결과(decision)가 나오면 completeTradingDecision 상태를 업데이트하여
|
||||||
// IntegratedOrderSection에서 자동으로 매수 로직이 실행되도록 연결합니다.
|
// IntegratedOrderSection에서 자동으로 매수 로직이 실행되도록 연결합니다.
|
||||||
AutoTradingManager.startAutoDiscoveryLoop(tradeService) { decision, isSuccess ->
|
AutoTradingManager.startAutoDiscoveryLoop(tradeService) { decision, isSuccess ->
|
||||||
if (isSuccess && decision != null) {
|
if (!isSuccess && decision?.confidence ?: 0.0 < 0.0) {
|
||||||
|
decision?.stockCode?.let { stockCode ->
|
||||||
selectedStockCode = decision.stockCode
|
decision?.stockName?.let { stockName ->
|
||||||
selectedStockName = decision.stockName
|
selectedStockCode = stockCode
|
||||||
isDomestic = true // 발굴 로직은 국내주식 기준이므로 true 고정
|
selectedStockName = stockName
|
||||||
|
isDomestic = true // 발굴 로직은 국내주식 기준이므로 true 고정
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}else if (isSuccess && decision != null) {
|
||||||
|
if (!selectedStockCode.equals(decision.stockCode) && selectedStockName.equals(decision.stockName)) {
|
||||||
|
selectedStockCode = decision.stockCode
|
||||||
|
selectedStockName = decision.stockName
|
||||||
|
isDomestic = true // 발굴 로직은 국내주식 기준이므로 true 고정
|
||||||
|
}
|
||||||
// 2. 결정 객체 업데이트 -> IntegratedOrderSection의 LaunchedEffect 트리거
|
// 2. 결정 객체 업데이트 -> IntegratedOrderSection의 LaunchedEffect 트리거
|
||||||
completeTradingDecision = decision
|
completeTradingDecision = decision
|
||||||
}
|
}
|
||||||
@ -62,29 +84,40 @@ fun DashboardScreen() {
|
|||||||
val processingIds = remember { mutableSetOf<String>() } // 주문번호 기준 잠금
|
val processingIds = remember { mutableSetOf<String>() } // 주문번호 기준 잠금
|
||||||
// [중앙 관리 함수] 체결 정보와 DB 정보를 매칭하여 실행
|
// [중앙 관리 함수] 체결 정보와 DB 정보를 매칭하여 실행
|
||||||
suspend fun syncAndExecute(orderNo: String) {
|
suspend fun syncAndExecute(orderNo: String) {
|
||||||
// 이미 다른 코루틴에서 이 주문을 처리 중이라면 즉시 종료 (중복 방지)
|
|
||||||
if (processingIds.contains(orderNo)) return
|
if (processingIds.contains(orderNo)) return
|
||||||
processingIds.add(orderNo)
|
processingIds.add(orderNo)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// DB 아이템과 체결 데이터(캐시)를 모두 가져옴
|
|
||||||
val dbItem = DatabaseFactory.findByOrderNo(orderNo)
|
val dbItem = DatabaseFactory.findByOrderNo(orderNo)
|
||||||
val execData = executionCache[orderNo]
|
val execData = executionCache[orderNo]
|
||||||
|
|
||||||
// 둘 다 존재할 때만 로직 실행
|
|
||||||
if (dbItem != null && execData != null && execData.isFilled) {
|
if (dbItem != null && execData != null && execData.isFilled) {
|
||||||
if (dbItem.status == TradeStatus.PENDING_BUY) {
|
if (dbItem.status == TradeStatus.PENDING_BUY) {
|
||||||
println("🎯 [매칭 성공] 익절 주문 실행: ${dbItem.name} (${orderNo})")
|
// 1. 실제 매수 체결가 가져오기 (문자열인 경우 숫자로 변환)
|
||||||
|
val actualBuyPrice = execData.price.toDoubleOrNull() ?: dbItem.targetPrice
|
||||||
|
|
||||||
|
// 2. 최소 마진 설정 (수수료/세금 0.3% + 순수익 1.5% = 1.8%)
|
||||||
|
|
||||||
|
val minEffectiveRate = minimumNetProfit + feesAndTaxRate
|
||||||
|
|
||||||
|
// 3. DB에 설정된 목표 수익률과 최소 보장 수익률 중 큰 값 선택
|
||||||
|
val finalProfitRate = maxOf(dbItem.profitRate, minEffectiveRate)
|
||||||
|
|
||||||
|
// 4. 실제 체결가 기준 익절 가격 재계산 및 틱 사이즈 보정
|
||||||
|
val finalTargetPrice = MarketUtil.roundToTickSize(actualBuyPrice * (1 + finalProfitRate / 100.0))
|
||||||
|
|
||||||
|
println("🎯 [매칭 성공] 익절 주문 실행: ${dbItem.name} | 매수가: ${actualBuyPrice.toInt()} -> 목표가: ${finalTargetPrice.toInt()} (${String.format("%.2f", finalProfitRate)}% 적용)")
|
||||||
|
|
||||||
val sellPrice = dbItem.targetPrice.toLong().toString()
|
|
||||||
tradeService.postOrder(
|
tradeService.postOrder(
|
||||||
stockCode = dbItem.code,
|
stockCode = dbItem.code,
|
||||||
qty = dbItem.quantity.toString(),
|
qty = dbItem.quantity.toString(),
|
||||||
price = sellPrice,
|
price = finalTargetPrice.toLong().toString(),
|
||||||
isBuy = false
|
isBuy = false
|
||||||
).onSuccess { newSellOrderNo ->
|
).onSuccess { newSellOrderNo ->
|
||||||
|
// 익절가 업데이트 및 상태 변경
|
||||||
DatabaseFactory.updateStatusAndOrderNo(dbItem.id!!, TradeStatus.SELLING, newSellOrderNo)
|
DatabaseFactory.updateStatusAndOrderNo(dbItem.id!!, TradeStatus.SELLING, newSellOrderNo)
|
||||||
// 처리가 완료된 체결 데이터는 캐시에서 삭제
|
// (선택 사항) 실제 계산된 익절가를 DB에 기록하고 싶다면 별도 update 로직 추가 가능
|
||||||
|
|
||||||
executionCache.remove(orderNo)
|
executionCache.remove(orderNo)
|
||||||
refreshTrigger++
|
refreshTrigger++
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
@ -98,7 +131,6 @@ fun DashboardScreen() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
// 처리가 끝나면(성공/실패/매칭실패 모두) 잠금 해제
|
|
||||||
processingIds.remove(orderNo)
|
processingIds.remove(orderNo)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -155,6 +187,11 @@ fun DashboardScreen() {
|
|||||||
Column(modifier = Modifier.weight(0.40f).fillMaxHeight().background(Color.White)) {
|
Column(modifier = Modifier.weight(0.40f).fillMaxHeight().background(Color.White)) {
|
||||||
if (selectedStockCode.isNotEmpty()) {
|
if (selectedStockCode.isNotEmpty()) {
|
||||||
StockDetailSection(
|
StockDetailSection(
|
||||||
|
min30 = min30,
|
||||||
|
daySummary = daySummary,
|
||||||
|
monthSummary = monthSummary,
|
||||||
|
weekSummary = weekSummary,
|
||||||
|
yearSummary = yearSummary,
|
||||||
stockCode = selectedStockCode,
|
stockCode = selectedStockCode,
|
||||||
stockName = selectedStockName,
|
stockName = selectedStockName,
|
||||||
holdingQuantity = selectedStockQuantity,
|
holdingQuantity = selectedStockQuantity,
|
||||||
@ -166,7 +203,7 @@ fun DashboardScreen() {
|
|||||||
syncAndExecute(orderNo) // 매칭 시도
|
syncAndExecute(orderNo) // 매칭 시도
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
completeTradingDecision
|
completeTradingDecision = completeTradingDecision,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||||
@ -178,6 +215,13 @@ fun DashboardScreen() {
|
|||||||
VerticalDivider()
|
VerticalDivider()
|
||||||
Column(modifier = Modifier.weight(0.2f).fillMaxHeight().padding(8.dp)) {
|
Column(modifier = Modifier.weight(0.2f).fillMaxHeight().padding(8.dp)) {
|
||||||
AiAnalysisView(
|
AiAnalysisView(
|
||||||
|
technicalAnalyzer = TechnicalAnalyzer().apply {
|
||||||
|
this.min30 = min30
|
||||||
|
this.daily = daySummary
|
||||||
|
this.weekly = weekSummary
|
||||||
|
this.monthly = monthSummary
|
||||||
|
this.weekly = weekSummary
|
||||||
|
},
|
||||||
stockCode = selectedStockCode,
|
stockCode = selectedStockCode,
|
||||||
stockName = selectedStockName,
|
stockName = selectedStockName,
|
||||||
currentPrice = wsManager.currentPrice.value,
|
currentPrice = wsManager.currentPrice.value,
|
||||||
|
|||||||
@ -20,6 +20,8 @@ import androidx.compose.ui.unit.TextUnit
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import model.feesAndTaxRate
|
||||||
|
import model.minimumNetProfit
|
||||||
import network.KisTradeService
|
import network.KisTradeService
|
||||||
import util.MarketUtil
|
import util.MarketUtil
|
||||||
|
|
||||||
@ -70,10 +72,10 @@ fun IntegratedOrderSection(
|
|||||||
}
|
}
|
||||||
|
|
||||||
var profitRate by remember(monitoringItem) {
|
var profitRate by remember(monitoringItem) {
|
||||||
mutableStateOf(monitoringItem?.profitRate?.toString() ?: "2.0")
|
mutableStateOf(monitoringItem?.profitRate?.toString() ?: "0.8")
|
||||||
}
|
}
|
||||||
var stopLossRate by remember(monitoringItem) {
|
var stopLossRate by remember(monitoringItem) {
|
||||||
mutableStateOf(monitoringItem?.stopLossRate?.toString() ?: "-2.0")
|
mutableStateOf(monitoringItem?.stopLossRate?.toString() ?: "-1.5")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 계산용 변수
|
// 계산용 변수
|
||||||
@ -83,31 +85,51 @@ fun IntegratedOrderSection(
|
|||||||
|
|
||||||
fun excuteTrade(willEnableAutoSell: Boolean,orderQty: String) {
|
fun excuteTrade(willEnableAutoSell: Boolean,orderQty: String) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
val finalPrice = if (orderPrice.isBlank()) "0" else orderPrice
|
val tickSize = MarketUtil.getTickSize(basePrice)
|
||||||
|
val oneTickLowerPrice = basePrice - tickSize
|
||||||
|
|
||||||
|
// 2. 주문 가격 설정 (직접 입력값이 없으면 한 틱 낮은 가격 사용)
|
||||||
|
val finalPrice = if (orderPrice.isBlank()) {
|
||||||
|
oneTickLowerPrice.toLong().toString()
|
||||||
|
} else {
|
||||||
|
orderPrice
|
||||||
|
}
|
||||||
|
println("basePrice : $basePrice, oneTickLowerPrice : $oneTickLowerPrice, finalPrice : $finalPrice")
|
||||||
tradeService.postOrder(stockCode, orderQty, finalPrice, isBuy = true)
|
tradeService.postOrder(stockCode, orderQty, finalPrice, isBuy = true)
|
||||||
.onSuccess { realOrderNo -> // KIS 서버에서 생성된 실제 주문번호
|
.onSuccess { realOrderNo -> // KIS 서버에서 생성된 실제 주문번호
|
||||||
onOrderResult("주문 성공: $realOrderNo", true)
|
onOrderResult("주문 성공: $realOrderNo", true)
|
||||||
if (willEnableAutoSell) {
|
if (willEnableAutoSell) {
|
||||||
|
// 1. 기본 설정값 파싱
|
||||||
val pRate = profitRate.toDoubleOrNull() ?: 0.0
|
val pRate = profitRate.toDoubleOrNull() ?: 0.0
|
||||||
val sRate = stopLossRate.toDoubleOrNull() ?: 0.0
|
val sRate = stopLossRate.toDoubleOrNull() ?: 0.0
|
||||||
val calculatedTarget = MarketUtil.roundToTickSize(basePrice * (1 + pRate / 100.0))
|
|
||||||
|
// 2. 수수료 및 세금 보정치 설정 (국내 주식 기준 약 0.25% ~ 0.3%)
|
||||||
|
// 유관기관 수수료 및 매도세금을 고려하여 안전하게 0.3%로 잡거나, 필요시 더 높게 설정 가능합니다.
|
||||||
|
|
||||||
|
// 3. 실질 목표 수익률 계산
|
||||||
|
// 사용자가 입력한 pRate와 (최소 순수익 + 제반 비용) 중 큰 값을 선택합니다.
|
||||||
|
val effectiveProfitRate = maxOf(pRate, minimumNetProfit + feesAndTaxRate)
|
||||||
|
|
||||||
|
// 4. 보정된 수익률을 적용하여 목표가 계산
|
||||||
|
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))
|
||||||
|
|
||||||
|
// 5. DB 저장 (effectiveProfitRate를 저장하여 분석 시 실제 목표치를 확인 가능하게 함)
|
||||||
DatabaseFactory.saveAutoTrade(AutoTradeItem(
|
DatabaseFactory.saveAutoTrade(AutoTradeItem(
|
||||||
orderNo = realOrderNo, // 실제 주문번호 저장 (중심 관리 원칙)
|
orderNo = realOrderNo,
|
||||||
code = stockCode,
|
code = stockCode,
|
||||||
name = stockName,
|
name = stockName,
|
||||||
quantity = inputQty,
|
quantity = inputQty,
|
||||||
profitRate = pRate,
|
profitRate = effectiveProfitRate, // 보정된 수익률 저장
|
||||||
stopLossRate = sRate,
|
stopLossRate = sRate,
|
||||||
targetPrice = calculatedTarget,
|
targetPrice = calculatedTarget,
|
||||||
stopLossPrice = calculatedStop,
|
stopLossPrice = calculatedStop,
|
||||||
status = "PENDING_BUY", // 체결 전까지 PENDING_BUY 상태
|
status = "PENDING_BUY",
|
||||||
isDomestic = isDomestic
|
isDomestic = isDomestic
|
||||||
))
|
))
|
||||||
monitoringItem = DatabaseFactory.findConfigByCode(stockCode)
|
monitoringItem = DatabaseFactory.findConfigByCode(stockCode)
|
||||||
onOrderSaved(realOrderNo)
|
onOrderSaved(realOrderNo)
|
||||||
onOrderResult("매수 및 즉시 체결 확인: $realOrderNo", true)
|
onOrderResult("매수 및 감시 설정 완료 (목표 수익률: ${String.format("%.2f", effectiveProfitRate)}%): $realOrderNo", true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onFailure { onOrderResult(it.message ?: "매수 실패", false) }
|
.onFailure { onOrderResult(it.message ?: "매수 실패", false) }
|
||||||
@ -115,22 +137,27 @@ fun IntegratedOrderSection(
|
|||||||
}
|
}
|
||||||
LaunchedEffect(completeTradingDecision) {
|
LaunchedEffect(completeTradingDecision) {
|
||||||
val MIN_CONFIDENCE = 70.0 // 최소 신뢰도
|
val MIN_CONFIDENCE = 70.0 // 최소 신뢰도
|
||||||
val MIN_MID_SCORE = 65.0 // 최소 중기 점수 (주봉/재무)
|
val MIN_SAFE_SCORE = 65.0 // 최소 중기 점수 (주봉/재무)
|
||||||
println("completeTradingDecision = $completeTradingDecision")
|
val MIN_POSSIBLE_SCORE = 55.0 // 최소 중기 점수 (주봉/재무)
|
||||||
|
val MIN_SHORT_SCORE = 60.0 // 최소 중기 점수 (주봉/재무)
|
||||||
|
var append = 0.0
|
||||||
if (completeTradingDecision != null &&
|
if (completeTradingDecision != null &&
|
||||||
completeTradingDecision.stockCode.equals(stockCode)) {
|
completeTradingDecision.stockCode.equals(stockCode)) {
|
||||||
println(completeTradingDecision?.decision)
|
|
||||||
fun resultCheck(completeTradingDecision :TradingDecision) {
|
fun resultCheck(completeTradingDecision :TradingDecision) {
|
||||||
println("""
|
println("""
|
||||||
${completeTradingDecision.corpName}
|
corpName : ${completeTradingDecision.corpName}
|
||||||
${completeTradingDecision.confidence}
|
confidence : ${completeTradingDecision.confidence + append}
|
||||||
${completeTradingDecision.profitPossible()}
|
shortPossible : ${completeTradingDecision.shortPossible() + append}
|
||||||
${completeTradingDecision.safePossible()}
|
profitPossible : ${completeTradingDecision.profitPossible()+ append}
|
||||||
|
safePossible : ${completeTradingDecision.safePossible()+ append}
|
||||||
""".trimIndent())
|
""".trimIndent())
|
||||||
// 2. 조건 검사: 신뢰도 80 이상 AND 중기 점수 70 이상
|
// 2. 조건 검사: 신뢰도 80 이상 AND 중기 점수 70 이상
|
||||||
if (completeTradingDecision.confidence >= MIN_CONFIDENCE &&
|
if (completeTradingDecision.confidence + append >= MIN_CONFIDENCE &&
|
||||||
completeTradingDecision.profitPossible() >= MIN_MID_SCORE &&
|
completeTradingDecision.shortPossible() + append >= MIN_SHORT_SCORE &&
|
||||||
completeTradingDecision.safePossible() > MIN_MID_SCORE) {
|
completeTradingDecision.profitPossible() + append >= MIN_POSSIBLE_SCORE &&
|
||||||
|
completeTradingDecision.safePossible() + append >= MIN_SAFE_SCORE
|
||||||
|
) {
|
||||||
|
|
||||||
println("🚀 [조건 만족] 강력 매수 시그널 포착 -> 자동 매수 진행 (1주) ${completeTradingDecision.stockCode}")
|
println("🚀 [조건 만족] 강력 매수 시그널 포착 -> 자동 매수 진행 (1주) ${completeTradingDecision.stockCode}")
|
||||||
// 3. 매수 실행 (자동 감시 켜기: true, 수량: 1주)
|
// 3. 매수 실행 (자동 감시 켜기: true, 수량: 1주)
|
||||||
@ -143,11 +170,13 @@ fun IntegratedOrderSection(
|
|||||||
}
|
}
|
||||||
when (completeTradingDecision?.decision) {
|
when (completeTradingDecision?.decision) {
|
||||||
"BUY" -> {
|
"BUY" -> {
|
||||||
|
append = 3.0
|
||||||
println("[$stockCode] 매수 추천 resultCheck: ${completeTradingDecision?.reason}")
|
println("[$stockCode] 매수 추천 resultCheck: ${completeTradingDecision?.reason}")
|
||||||
resultCheck(completeTradingDecision)
|
resultCheck(completeTradingDecision)
|
||||||
}
|
}
|
||||||
"SELL" -> println("[$stockCode] 매도: ${completeTradingDecision?.reason}")
|
"SELL" -> println("[$stockCode] 매도: ${completeTradingDecision?.reason}")
|
||||||
else -> {
|
else -> {
|
||||||
|
append = 0.0
|
||||||
resultCheck(completeTradingDecision)
|
resultCheck(completeTradingDecision)
|
||||||
println("[$stockCode] 관망 유지 resultCheck: ${completeTradingDecision?.reason}")
|
println("[$stockCode] 관망 유지 resultCheck: ${completeTradingDecision?.reason}")
|
||||||
}
|
}
|
||||||
|
|||||||
@ -36,7 +36,12 @@ fun StockDetailSection(
|
|||||||
tradeService: KisTradeService,
|
tradeService: KisTradeService,
|
||||||
wsManager: KisWebSocketManager,
|
wsManager: KisWebSocketManager,
|
||||||
onOrderSaved: (String) -> Unit,
|
onOrderSaved: (String) -> Unit,
|
||||||
completeTradingDecision: TradingDecision?
|
completeTradingDecision: TradingDecision?,
|
||||||
|
min30 : MutableList<CandleData>,
|
||||||
|
daySummary : MutableList<CandleData>,
|
||||||
|
weekSummary : MutableList<CandleData>,
|
||||||
|
monthSummary : MutableList<CandleData>,
|
||||||
|
yearSummary : MutableList<CandleData>
|
||||||
) {
|
) {
|
||||||
|
|
||||||
var openPrice by remember { mutableStateOf("0") }
|
var openPrice by remember { mutableStateOf("0") }
|
||||||
@ -44,10 +49,7 @@ fun StockDetailSection(
|
|||||||
var isLoading by remember { mutableStateOf(false) }
|
var isLoading by remember { mutableStateOf(false) }
|
||||||
var resultMessage by remember { mutableStateOf("") }
|
var resultMessage by remember { mutableStateOf("") }
|
||||||
var isSuccess by remember { mutableStateOf(true) }
|
var isSuccess by remember { mutableStateOf(true) }
|
||||||
var daySummary by remember { mutableStateOf<List<CandleData>>(emptyList()) }
|
|
||||||
var weekSummary by remember { mutableStateOf<List<CandleData>>(emptyList()) }
|
|
||||||
var monthSummary by remember { mutableStateOf<List<CandleData>>(emptyList()) }
|
|
||||||
var yearSummary by remember { mutableStateOf<List<CandleData>>(emptyList()) }
|
|
||||||
|
|
||||||
val todayOpen = remember(daySummary) {
|
val todayOpen = remember(daySummary) {
|
||||||
daySummary.lastOrNull()?.stck_oprc ?: "0"
|
daySummary.lastOrNull()?.stck_oprc ?: "0"
|
||||||
@ -79,12 +81,13 @@ fun StockDetailSection(
|
|||||||
// 2. 차트 데이터 로드 (KisSession 기반으로 파라미터 간소화)
|
// 2. 차트 데이터 로드 (KisSession 기반으로 파라미터 간소화)
|
||||||
|
|
||||||
coroutineScope {
|
coroutineScope {
|
||||||
TechnicalAnalyzer.clear()
|
|
||||||
launch {tradeService.fetchChartData(stockCode, isDomestic)
|
launch {tradeService.fetchChartData(stockCode, isDomestic)
|
||||||
.onSuccess { data ->
|
.onSuccess { data ->
|
||||||
println("✅ 차트 데이터 로드 성공: ${data.size}개") // ${} 사용하여 정확히 출력
|
println("✅ 차트 데이터 로드 성공: ${data.size}개") // ${} 사용하여 정확히 출력
|
||||||
chartData = data
|
chartData = data
|
||||||
TechnicalAnalyzer.min30 = chartData
|
min30.clear()
|
||||||
|
min30.addAll(chartData)
|
||||||
}
|
}
|
||||||
.onFailure { error ->
|
.onFailure { error ->
|
||||||
println("❌ 차트 데이터 로드 실패: ${error.localizedMessage}")
|
println("❌ 차트 데이터 로드 실패: ${error.localizedMessage}")
|
||||||
@ -92,22 +95,21 @@ fun StockDetailSection(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
launch { tradeService.fetchPeriodChartData(stockCode, "D").onSuccess {
|
launch { tradeService.fetchPeriodChartData(stockCode, "D").onSuccess {
|
||||||
daySummary = it.takeLast(7)
|
daySummary.clear()
|
||||||
TechnicalAnalyzer.daily = it
|
daySummary.addAll(it)
|
||||||
// println("daySummary ${daySummary.size} total: ${it.size} ${it.firstOrNull()?.toString()}")
|
|
||||||
}
|
}
|
||||||
} // 최근 7일
|
} // 최근 7일
|
||||||
launch { tradeService.fetchPeriodChartData(stockCode, "W").onSuccess {
|
launch { tradeService.fetchPeriodChartData(stockCode, "W").onSuccess {
|
||||||
weekSummary = it.takeLast(4)
|
weekSummary.clear()
|
||||||
TechnicalAnalyzer.weekly = it
|
weekSummary.addAll(it.takeLast(4))
|
||||||
// println("weekSummary ${weekSummary.size} total: ${it.size} ${it.firstOrNull()?.toString()}")
|
// println("weekSummary ${weekSummary.size} total: ${it.size} ${it.firstOrNull()?.toString()}")
|
||||||
}
|
}
|
||||||
} // 최근 4주
|
} // 최근 4주
|
||||||
launch { tradeService.fetchPeriodChartData(stockCode, "M").onSuccess {
|
launch { tradeService.fetchPeriodChartData(stockCode, "M").onSuccess {
|
||||||
monthSummary = it.takeLast(6) // 최근 6개월
|
monthSummary.clear()
|
||||||
yearSummary = it.takeLast(36) // 최근 3년
|
monthSummary.addAll(it.takeLast(6))
|
||||||
TechnicalAnalyzer.monthly = it
|
yearSummary.clear()
|
||||||
// println("monthSummary ${monthSummary.size} yearSummary ${yearSummary.size} total: ${it.size} ${it.firstOrNull()?.toString()}")
|
yearSummary.addAll(it.takeLast(36))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
launch {
|
launch {
|
||||||
|
|||||||
@ -14,6 +14,18 @@ object MarketUtil {
|
|||||||
return now.isAfter(start) && now.isBefore(end)
|
return now.isAfter(start) && now.isBefore(end)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getTickSize(price: Double): Double {
|
||||||
|
return when {
|
||||||
|
price < 2000 -> 1.0
|
||||||
|
price < 5000 -> 5.0
|
||||||
|
price < 20000 -> 10.0
|
||||||
|
price < 50000 -> 50.0
|
||||||
|
price < 200000 -> 100.0
|
||||||
|
price < 500000 -> 500.0
|
||||||
|
else -> 1000.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun roundToTickSize(price: Double): Double {
|
fun roundToTickSize(price: Double): Double {
|
||||||
val tick = when {
|
val tick = when {
|
||||||
price < 2000 -> 1.0
|
price < 2000 -> 1.0
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user