This commit is contained in:
2026-04-06 15:07:14 +09:00
parent 72b99769b5
commit a700d54dfe
5 changed files with 198 additions and 101 deletions
+27 -17
View File
@@ -374,8 +374,6 @@ object AutoTradingManager {
}
suspend fun sellingAfterMarketOnePrice(tradeService: KisTradeService,balance : UnifiedBalance,marketCode : String = "Y") {
// if (true) return
balance.holdings.forEach { holding ->
if (BLACKLISTEDSTOCKCODES.contains(holding.code)){
println("❌ 차단 처리된 주식 : ${holding.name}")
@@ -641,19 +639,27 @@ object AutoTradingManager {
if (AUTOSELL) balance?.let { resumePendingSellOrders(KisTradeService, it) }
return balance
} else {
// listOf<String>("Y","X").forEach { code ->
// val now = LocalTime.now()
// val currentMinute = now.minute
// if((now.hour == 16 || now.hour == 17) && (currentMinute % 10 == 3 || currentMinute % 10 == 9)) {
// if (lastForceCheckMinute != currentMinute) {
// listOf<String>("Y","X").forEach { code ->
// KisTradeService.fetchIntegratedBalance(code).getOrNull()?.let {
// sellingAfterMarketOnePrice(KisTradeService, it, code)
// }
// delay(1000)
// }
// lastForceCheckMinute = currentMinute // 실행 완료 기록
// }
// }
//
}
return null
}
suspend fun executeMarketLoop() {
val balance = checkBalance()
if (AUTOSELL) balance?.let { resumePendingSellOrders(KisTradeService, it) }
// if (AUTOSELL) balance?.let { resumePendingSellOrders(KisTradeService, it) }
val myCash = balance?.deposit?.replace(",", "")?.toLongOrNull() ?: 0L
val myHoldings = balance?.holdings?.filter { it.quantity.toInt() > 0 }?.map { it.code }?.toSet() ?: emptySet()
@@ -733,16 +739,20 @@ object AutoTradingManager {
}
}
else if((now.hour == 16 || now.hour == 17) && (currentMinute % 10 == 3 || currentMinute % 10 == 9)) {
// if (lastForceCheckMinute != currentMinute) {
// TradingLogStore.addAnalyzer(
// " - ",
// " - ",
// "⏰ [강제 스케줄 실행] 오후 ${now.hour}${currentMinute}분 - 보유주식 시간외 단일가 또는 대체마켓 체크를 시작합니다.",
// true
// )
// checkBalance(false)
// lastForceCheckMinute = currentMinute // 실행 완료 기록
// }
if (lastForceCheckMinute != currentMinute) {
TradingLogStore.addAnalyzer(
" - ",
" - ",
"⏰ [강제 스케줄 실행] 오후 ${now.hour}${currentMinute}분 - 보유주식 시간외 단일가 또는 대체마켓 체크를 시작합니다.",
true
)
listOf<String>("Y","X").forEach { code ->
KisTradeService.fetchIntegratedBalance(code).getOrNull()?.let {
sellingAfterMarketOnePrice(KisTradeService, it, code)
}
}
lastForceCheckMinute = currentMinute // 실행 완료 기록
}
}
}
@@ -1155,7 +1165,7 @@ class TechnicalAnalyzer {
val disparityDaily = (currentPrice / ma20Daily) * 100
if (disparityDaily > 115.0) { // 20일선보다 15% 이상 떠 있으면 감점 시작
val penalty = ((disparityDaily - 115.0) * 0.5).toInt() // 초과분 1%당 2점 감점
val penalty = ((disparityDaily - 115.0) * 0.3).toInt() // 초과분 1%당 2점 감점
short -= penalty
ultra -= (penalty / 2) // 초단기에도 영향
println("⚠️ [과열 감점] 일봉 이격도(${String.format("%.1f", disparityDaily)}%): -${penalty}")
@@ -1166,8 +1176,8 @@ class TechnicalAnalyzer {
if (weekly.size >= 3) {
val weeklyChange = calculateChange(weekly.takeLast(3))
if (weeklyChange > 30.0) { // 3주간 30% 이상 급등 시
mid -= 10
short -= 5
mid -= 6
short -= 3
println("⚠️ [과열 감점] 주봉 급등(${String.format("%.1f", weeklyChange)}%): -10점")
}
}
+82 -58
View File
@@ -111,71 +111,75 @@ object DynamicNewsScraper {
fun extractSmartContentWithLineFilter(page: Page): String {
val script = """
() => {
// 1. 선제적 노이즈 제거: 분석에 방해되는 태그들을 DOM에서 아예 삭제
const junkTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'IFRAME', 'SVG', 'HEADER', 'FOOTER', 'NAV'];
document.querySelectorAll(junkTags.join(',')).forEach(el => el.remove());
// 1. 선제적 노이즈 제거: ASIDE(사이드바/광고) 태그 추가
const junkTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'IFRAME', 'SVG', 'HEADER', 'FOOTER', 'NAV', 'ASIDE'];
document.querySelectorAll(junkTags.join(',')).forEach(el => el.remove());
const MIN_LINE_LENGTH = 10;
const MIN_TOTAL_LENGTH = 100;
const CONSECUTIVE_THRESHOLD = 2;
const MIN_LINE_LENGTH = 10;
const MIN_TOTAL_LENGTH = 100;
const CONSECUTIVE_THRESHOLD = 2;
// 2. 라인별 정제 함수 (짧은 라인 연속 시 예외 처리)
const getRefinedText = (el) => {
// 실제 텍스트만 추출하여 라인별로 분리
const lines = el.innerText.split('\n').map(l => l.trim()).filter(l => l.length > 0);
let resultLines = [];
let tempBuffer = [];
let consecutiveShort = 0;
// 2. 라인별 정제 함수 (예외 처리 강화)
const getRefinedText = (el) => {
if (!el || !el.innerText) return "";
const lines = el.innerText.split('\n').map(l => l.trim()).filter(l => l.length > 0);
let resultLines = [];
let tempBuffer = [];
let consecutiveShort = 0;
lines.forEach(line => {
if (line.length <= MIN_LINE_LENGTH) {
consecutiveShort++;
tempBuffer.push(line);
} else {
if (consecutiveShort < CONSECUTIVE_THRESHOLD) {
resultLines = resultLines.concat(tempBuffer);
}
resultLines.push(line);
tempBuffer = [];
consecutiveShort = 0;
}
});
lines.forEach(line => {
if (line.length <= MIN_LINE_LENGTH) {
consecutiveShort++;
tempBuffer.push(line);
} else {
// 짧은 줄이 연속되지 않았을 때만 버퍼를 결과에 합침
if (consecutiveShort < CONSECUTIVE_THRESHOLD) {
resultLines = resultLines.concat(tempBuffer);
}
resultLines.push(line);
tempBuffer = [];
consecutiveShort = 0;
return resultLines.join('\n');
};
// 3. 후보 블록 탐색
const candidates = Array.from(document.querySelectorAll('div, section, article, p, main, td'))
.map(el => ({
el: el,
refinedText: getRefinedText(el)
}))
.filter(item => {
if (item.refinedText.length < MIN_TOTAL_LENGTH) return false;
const linkLength = Array.from(item.el.querySelectorAll('a'))
.reduce((acc, a) => acc + (a.innerText || "").length, 0);
return (linkLength / item.refinedText.length) < 0.3;
});
// 4. 최적의 본문 컨테이너 선정
const best = candidates.find(parent =>
!candidates.some(child =>
parent.el !== child.el &&
parent.el.contains(child.el) &&
child.refinedText.length > parent.refinedText.length * 0.8
)
);
let finalResult = best ? best.refinedText : (candidates.sort((a,b) => b.refinedText.length - a.refinedText.length)[0]?.refinedText || "");
// ⭐ 5. Fallback 로직: 스마트 추출에 실패했거나 너무 짧으면, 노이즈가 제거된 body 원문을 통째로 반환
if (!finalResult || finalResult.length < MIN_TOTAL_LENGTH) {
finalResult = getRefinedText(document.body);
}
});
// 마지막 남은 버퍼 처리 (본문 끝에 짧은 정보가 있을 경우 대비)
if (consecutiveShort < CONSECUTIVE_THRESHOLD) {
resultLines = resultLines.concat(tempBuffer);
return finalResult;
}
return resultLines.join('\n');
};
// 3. 후보 블록 탐색 및 텍스트 밀도 기반 분석
const candidates = Array.from(document.querySelectorAll('div, section, article, p, main, td'))
.map(el => ({
el: el,
refinedText: getRefinedText(el)
}))
.filter(item => {
if (item.refinedText.length < MIN_TOTAL_LENGTH) return false;
// 링크 밀도 체크: 기사 본문은 보통 링크보다 텍스트 비중이 훨씬 높음
const linkLength = Array.from(item.el.querySelectorAll('a'))
.reduce((acc, a) => acc + (a.innerText || "").length, 0);
return (linkLength / item.refinedText.length) < 0.3;
});
// 4. 가장 최적의(가장 깊은 계층의) 본문 컨테이너 선정
const best = candidates.find(parent =>
!candidates.some(child =>
parent.el !== child.el &&
parent.el.contains(child.el) &&
child.refinedText.length > parent.refinedText.length * 0.8
)
);
return best ? best.refinedText : (candidates.sort((a,b) => b.refinedText.length - a.refinedText.length)[0]?.refinedText || "");
}
""".trimIndent()
""".trimIndent()
return page.evaluate(script) as String
}
@@ -259,8 +263,26 @@ object DynamicNewsScraper {
}
private fun cleanText(text: String): String {
return text.replace(Regex("(?m)^.*기자.*$"), "") // 기자 정보 제거
.replace(Regex("(?m)^.*무단 전재.*$"), "") // 저작권 문구 제거
// 1. 제거할 불필요한 노이즈 키워드 목록
val noiseKeywords = listOf(
"기자", "무단 전재", "재배포 금지", "Copyright", "",
"ADVERTISEMENT", "돌아가기", "topstarnews",
"구독하기", "제보하기", "광고", "Sponsored"
)
var cleanedText = text
// 2. 키워드가 포함된 '한 줄 전체'를 삭제
for (keyword in noiseKeywords) {
// (?im): 대소문자 무시(i) 및 다중행 모드(m)
// 해당 키워드가 포함된 라인을 찾아서 빈 문자열로 치환
val regex = Regex("(?im)^.*${Regex.escape(keyword)}.*\$")
cleanedText = cleanedText.replace(regex, "")
}
// 3. 연속된 빈 줄(엔터)들을 하나의 줄바꿈으로 압축하여 RAG 청크 품질 향상
return cleanedText
.replace(Regex("\\n{2,}"), "\n")
.trim()
}
}
@@ -286,7 +308,9 @@ object SafeScraper {
try {
withTimeout(25000L) { // 타임아웃 약간 증가
val content = DynamicNewsScraper.fetchFullContent(item.originallink)
println("📝 [News Raw Text Length] ${item.originallink.take(30)}... : ${content.length}자 추출됨")
if (content.isNotBlank() && content.length > 100) {
UrlCacheManager.addToCache(item.originallink)
RagService.ingestWithChunking(
text = content,
newsLink = item.originallink,
@@ -114,7 +114,7 @@ object LlamaServerManager {
val pb = ProcessBuilder(command)
// 2. 윈도우 Vulkan 환경 변수 설정
if (isWin && binPath.endsWith("win-x64")) {
if (isWin ) {
val env = pb.environment()
// 특정 GPU 선택 (내장 GPU가 여러 개일 경우)
env["GGML_VULKAN_DEVICE"] = "0"
@@ -158,10 +158,11 @@ object LlamaServerManager {
}
if (port == EMBEDDING_PORT){
AutoTradingManager.llmNews = true
RagService.active()
}
if (processes.size > 1) {
println("[Cache] ${processes.size}")
RagService.active()
}
}
}