...
This commit is contained in:
parent
e0e328ea97
commit
3f209dcd4d
@ -59,6 +59,32 @@ enum class RankingType(
|
||||
VOLUME_POWER("체결강도순", "FHPST01680000", "20168", "/uapi/domestic-stock/v1/ranking/volume-power", "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")
|
||||
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
|
||||
|
||||
@ -147,9 +147,22 @@ object KisTradeService {
|
||||
// RankingType.BEFORE -> {
|
||||
// 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_APLY_RANG_PRC_1", "")
|
||||
|
||||
@ -139,7 +139,11 @@ object RagService {
|
||||
corpInfo?.stockName = stockName
|
||||
tradingDecision.stockName = stockName
|
||||
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 ?: "") }
|
||||
|
||||
|
||||
@ -4,9 +4,11 @@ import TradingDecision
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.TimeoutCancellationException
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import model.CandleData
|
||||
import model.RankingType
|
||||
import network.KisTradeService
|
||||
@ -26,7 +28,8 @@ object AutoTradingManager {
|
||||
private var discoveryJob: Job? = null
|
||||
|
||||
|
||||
|
||||
val MIN = 0.1
|
||||
val MAX = 15.0
|
||||
fun startAutoDiscoveryLoop(
|
||||
tradeService: KisTradeService,
|
||||
callback: TradingDecisionCallback
|
||||
@ -56,100 +59,113 @@ object AutoTradingManager {
|
||||
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 // 너무 과열되지 않은 주도주
|
||||
}
|
||||
|
||||
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. 리스트 합치기 (중복 제거)
|
||||
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}개 (급등주 제외) 검증 시작...")
|
||||
|
||||
candidates.forEach { stock ->
|
||||
// [조건 1] 이미 보유한 종목 제외
|
||||
if (myHoldings.contains(stock.code)) return@forEach
|
||||
try {
|
||||
|
||||
val currentPrice = stock.stck_prpr.replace(",", "").toDoubleOrNull() ?: 0.0
|
||||
|
||||
// [조건 2] 최소 1주 매수 가능 여부
|
||||
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 입력용)
|
||||
val dailyResult = tradeService.fetchPeriodChartData(stock.code, "D", true)
|
||||
val dailyData = dailyResult.getOrNull()
|
||||
val todayCandle = dailyData?.lastOrNull()
|
||||
// [조건 1] 이미 보유한 종목 제외
|
||||
if (myHoldings.contains(stock.code)) return@forEach
|
||||
|
||||
if (dailyData != null && todayCandle != null) {
|
||||
val currentPrice = stock.stck_prpr.replace(",", "").toDoubleOrNull() ?: 0.0
|
||||
|
||||
val open = todayCandle.stck_oprc.toDoubleOrNull() ?: 0.0
|
||||
val current = todayCandle.stck_prpr.toDoubleOrNull() ?: 0.0
|
||||
// [조건 2] 최소 1주 매수 가능 여부
|
||||
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) {
|
||||
val riseRate = (current - open) / open * 100
|
||||
if (dailyData != null && todayCandle != null) {
|
||||
|
||||
// [조건 3] 상승 중(양봉)이면서 20% 이하 상승
|
||||
if (riseRate > 0 && riseRate <= 20.0) {
|
||||
println("✨ [발굴] ${stock.name} (+${String.format("%.1f", riseRate)}%) -> 데이터 수집 및 분석")
|
||||
val open = todayCandle.stck_oprc.toDoubleOrNull() ?: 0.0
|
||||
val current = todayCandle.stck_prpr.toDoubleOrNull() ?: 0.0
|
||||
|
||||
// [핵심 수정] AI 분석 전 필요한 차트 데이터(30분, 주봉, 월봉)를 모두 가져와 TechnicalAnalyzer에 주입
|
||||
// 비동기로 동시에 요청하여 속도 향상
|
||||
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()) }
|
||||
if (open > 0) {
|
||||
val riseRate = (current - open) / open * 100
|
||||
|
||||
val min30Data = min30Def.await()
|
||||
val weeklyData = weekDef.await()
|
||||
val monthlyData = monthDef.await()
|
||||
// [조건 3] 상승 중(양봉)이면서 20% 이하 상승
|
||||
if (riseRate > 0 && riseRate <= 20.0) {
|
||||
println(
|
||||
"✨ [발굴] ${stock.name} (+${
|
||||
String.format(
|
||||
"%.1f",
|
||||
riseRate
|
||||
)
|
||||
}%) -> 데이터 수집 및 분석"
|
||||
)
|
||||
|
||||
// TechnicalAnalyzer 상태 업데이트 (싱글톤이므로 순차 처리 필수)
|
||||
val t = TechnicalAnalyzer()
|
||||
t.daily = dailyData
|
||||
t.weekly = weeklyData
|
||||
t.monthly = monthlyData
|
||||
t.min30 = min30Data
|
||||
|
||||
// 데이터 준비 완료 후 AI 분석 요청 (suspend 함수이므로 완료될 때까지 대기 -> 데이터 섞임 방지)
|
||||
RagService.processStock(t,stock.name, stock.code) { decision, isSuccess ->
|
||||
if (decision != null) {
|
||||
decision.stockName = stock.name
|
||||
decision.currentPrice = current // 차트에서 확인한 최신 현재가 주입
|
||||
// [핵심 수정] AI 분석 전 필요한 차트 데이터(30분, 주봉, 월봉)를 모두 가져와 TechnicalAnalyzer에 주입
|
||||
// 비동기로 동시에 요청하여 속도 향상
|
||||
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())
|
||||
}
|
||||
callback(decision, isSuccess) // DashboardScreen으로 전달
|
||||
}
|
||||
|
||||
// 분석 후 잠시 대기 (서버 부하 조절)
|
||||
delay(2000)
|
||||
val min30Data = min30Def.await()
|
||||
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)
|
||||
currentWait += tickMillis
|
||||
val leftSec = (totalWaitMillis - currentWait) / 1000
|
||||
// 1분 단위 혹은 10초 단위로 자유롭게 로그 조절 가능
|
||||
if (leftSec % 30 == 0L || leftSec <= 30) {
|
||||
println("📡 [AutoTrading] 시스템 정상 작동 중... (다음 분석 ${leftSec}초 전)")
|
||||
if (leftSec % 60 == 0L) {
|
||||
val runtime = Runtime.getRuntime()
|
||||
val usedMem = (runtime.totalMemory() - runtime.freeMemory()) / 1024 / 1024
|
||||
println("📡 [System] 정상 작동 중... (남은 시간: ${leftSec}초 | 메모리 사용: ${usedMem}MB)")
|
||||
}
|
||||
}
|
||||
|
||||
@ -199,19 +216,19 @@ object AutoTradingManager {
|
||||
println("🔥 [주문 집행] $code $type 완료")
|
||||
}
|
||||
}
|
||||
|
||||
data class InvestmentScores(
|
||||
val ultraShort: Int, // 초단기 (분봉/에너지)
|
||||
val shortTerm: Int, // 단기 (일봉/뉴스)
|
||||
val midTerm: Int, // 중기 (주봉/재무)
|
||||
val longTerm: Int // 장기 (월봉/펀더멘털)
|
||||
)
|
||||
class TechnicalAnalyzer {
|
||||
var monthly: List<CandleData> = emptyList()
|
||||
var weekly: List<CandleData> = emptyList()
|
||||
var daily: 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(
|
||||
financialScore: Int // 재무제표 점수 (성장률 등 기반)
|
||||
|
||||
@ -12,6 +12,7 @@ import kotlinx.coroutines.sync.Semaphore
|
||||
import kotlinx.coroutines.sync.withPermit
|
||||
import model.NewsItem
|
||||
import network.CorpInfo
|
||||
import java.net.URL
|
||||
import kotlin.random.Random
|
||||
|
||||
object DynamicNewsScraper {
|
||||
@ -91,20 +92,25 @@ object DynamicNewsScraper {
|
||||
|
||||
return page.evaluate(script) as String
|
||||
}
|
||||
|
||||
var failDomainList = arrayListOf<String>()
|
||||
suspend fun fetchFullContent(url: String): String {
|
||||
// browser.newContext().use { ... } 대신 직접 변수를 선언하고 제어합니다.
|
||||
val domain = URL(url).host
|
||||
val context = browser.newContext()
|
||||
if(failDomainList.contains(domain)) {
|
||||
println("실패한 도메인 스크래핑 종료 $domain ")
|
||||
return ""
|
||||
}
|
||||
return try {
|
||||
context.use { ctx ->
|
||||
ctx.newPage().use { page ->
|
||||
delay(Random.nextInt(1000).toLong())
|
||||
delay(Random.nextInt(2000).toLong())
|
||||
|
||||
// 1. 리스너 설정 시 예외 처리 강화
|
||||
blockUnnecessaryResources(page)
|
||||
|
||||
// 2. 타임아웃을 설정하여 무한 대기 방지
|
||||
val options = Page.NavigateOptions().setTimeout(30000.0)
|
||||
val options = Page.NavigateOptions().setTimeout(8000.0)
|
||||
page.navigate(url, options)
|
||||
|
||||
// 3. 페이지가 완전히 닫히기 전에 모든 대기 중인 이벤트를 해제하기 위해 LOAD 상태 대기
|
||||
@ -119,6 +125,7 @@ object DynamicNewsScraper {
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
failDomainList.add(domain)
|
||||
println("❌ [Playwright] 스크래핑 실패 ($url): ${e.message}")
|
||||
""
|
||||
} finally {
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
package service
|
||||
|
||||
import java.util.concurrent.TimeUnit
|
||||
import org.slf4j.LoggerFactory
|
||||
import ch.qos.logback.classic.Level
|
||||
import ch.qos.logback.classic.Logger
|
||||
|
||||
object SystemSleepPreventer {
|
||||
private var process: Process? = null
|
||||
@ -9,6 +12,9 @@ object SystemSleepPreventer {
|
||||
* 맥의 절전 모드 및 디스플레이 취침을 방지하는 명령 실행
|
||||
*/
|
||||
fun start() {
|
||||
val root = LoggerFactory.getLogger("Exposed") as Logger
|
||||
root.level = Level.ERROR
|
||||
|
||||
if (process?.isAlive == true) return
|
||||
|
||||
try {
|
||||
|
||||
@ -159,7 +159,6 @@ fun DashboardScreen() {
|
||||
// 3. 실시간 체결 통보 핸들러 (주문번호 중심)
|
||||
wsManager.onExecutionReceived = {code, qty, price,orderNo, isBuy ->
|
||||
scope.launch {
|
||||
println("$orderNo, $code, $price, $qty, $isBuy")
|
||||
val exec = ExecutionData(orderNo, code, price, qty, isBuy)
|
||||
executionCache[orderNo] = exec
|
||||
syncAndExecute(orderNo)
|
||||
|
||||
@ -82,12 +82,12 @@ fun IntegratedOrderSection(
|
||||
// 계산용 변수
|
||||
val curPriceNum = currentPrice.replace(",", "").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 {
|
||||
val tickSize = MarketUtil.getTickSize(basePrice)
|
||||
val oneTickLowerPrice = basePrice - tickSize
|
||||
val oneTickLowerPrice = basePrice - (tickSize * if (confidence) { 1} else {2})
|
||||
|
||||
// 2. 주문 가격 설정 (직접 입력값이 없으면 한 틱 낮은 가격 사용)
|
||||
val finalPrice = if (orderPrice.isBlank()) {
|
||||
@ -114,7 +114,7 @@ fun IntegratedOrderSection(
|
||||
// 4. 보정된 수익률을 적용하여 목표가 계산
|
||||
val calculatedTarget = MarketUtil.roundToTickSize(basePrice * (1 + effectiveProfitRate / 100.0))
|
||||
val calculatedStop = MarketUtil.roundToTickSize(basePrice * (1 + sRate / 100.0))
|
||||
|
||||
val inputQty = orderQty.replace(",", "").toIntOrNull() ?: 0
|
||||
// 5. DB 저장 (effectiveProfitRate를 저장하여 분석 시 실제 목표치를 확인 가능하게 함)
|
||||
DatabaseFactory.saveAutoTrade(AutoTradeItem(
|
||||
orderNo = realOrderNo,
|
||||
@ -146,13 +146,7 @@ fun IntegratedOrderSection(
|
||||
completeTradingDecision.stockCode.equals(stockCode)) {
|
||||
|
||||
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(
|
||||
"short" to 0.3, // 초단기 점수가 낮아도 전체에 미치는 영향 감소
|
||||
@ -169,8 +163,15 @@ fun IntegratedOrderSection(
|
||||
// 3. 매수 결정 문턱값 (예: 70점 이상이면 매수 가능)
|
||||
val MIN_PURCHASE_SCORE = 68.0
|
||||
val HIGH_QUALITY_SCORE = 85.0 // 강력 추천 기준
|
||||
|
||||
if (totalScore >= MIN_PURCHASE_SCORE && completeTradingDecision.confidence > MIN_CONFIDENCE) {
|
||||
println("""
|
||||
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. 점수에 따른 가변 마진 적용
|
||||
// 토탈 스코어가 85점 이상이면 마진을 3.0으로 고정하거나 추가 가산(append) 적용
|
||||
@ -182,16 +183,23 @@ fun IntegratedOrderSection(
|
||||
}
|
||||
|
||||
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 전달)
|
||||
excuteTrade(
|
||||
willEnableAutoSell = true,
|
||||
orderQty = "1",
|
||||
profitRate1 = finalMargin
|
||||
orderQty = calculatedQty.toString(),
|
||||
profitRate1 = finalMargin,
|
||||
confidence = totalScore >= HIGH_QUALITY_SCORE
|
||||
)
|
||||
|
||||
} else {
|
||||
println("✋ [관망] 토탈 스코어(${String.format("%.1f", totalScore)})가 기준치($MIN_PURCHASE_SCORE) 미달")
|
||||
println("✋ [관망] 토탈 스코어(${String.format("%.1f", totalScore)})가 기준치($MIN_PURCHASE_SCORE) 미달 또는 신뢰도 ${completeTradingDecision.confidence}가 기준치 ${MIN_CONFIDENCE} 미달")
|
||||
}
|
||||
}
|
||||
when (completeTradingDecision?.decision) {
|
||||
@ -228,7 +236,7 @@ fun IntegratedOrderSection(
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
|
||||
val inputQty = orderQty.replace(",", "").toIntOrNull() ?: 0
|
||||
// 수익률 시뮬레이션 표
|
||||
if (basePrice > 0 && inputQty > 0) {
|
||||
SimulationCard(basePrice, inputQty.toDouble())
|
||||
|
||||
7
src/main/resources/logback.xml
Normal file
7
src/main/resources/logback.xml
Normal file
@ -0,0 +1,7 @@
|
||||
<configuration>
|
||||
<logger name="Exposed" level="OFF" />
|
||||
|
||||
<root level="INFO">
|
||||
<appender-ref ref="STDOUT" />
|
||||
</root>
|
||||
</configuration>
|
||||
Loading…
x
Reference in New Issue
Block a user