.
This commit is contained in:
parent
44e14dd207
commit
1eb89bcc3f
@ -80,7 +80,7 @@ fun main() = application {
|
|||||||
// 앱 실행 시 필요한 바이너리 경로 (실행 파일 위치)
|
// 앱 실행 시 필요한 바이너리 경로 (실행 파일 위치)
|
||||||
val binPath = getLlamaBinPath()
|
val binPath = getLlamaBinPath()
|
||||||
val windowState = rememberWindowState(
|
val windowState = rememberWindowState(
|
||||||
placement = WindowPlacement.Fullscreen
|
placement = WindowPlacement.Floating
|
||||||
)
|
)
|
||||||
Window(onCloseRequest = ::exitApplication, title = "KIS AI 자동매매", state = windowState) {
|
Window(onCloseRequest = ::exitApplication, title = "KIS AI 자동매매", state = windowState) {
|
||||||
var currentScreen by remember { mutableStateOf(AppScreen.Settings) }
|
var currentScreen by remember { mutableStateOf(AppScreen.Settings) }
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import androidx.compose.runtime.mutableStateListOf
|
import androidx.compose.runtime.mutableStateListOf
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import model.AppConfig
|
import model.AppConfig
|
||||||
|
import network.TradingDecision
|
||||||
import org.jetbrains.exposed.sql.*
|
import org.jetbrains.exposed.sql.*
|
||||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
||||||
import org.jetbrains.exposed.sql.javatime.datetime
|
import org.jetbrains.exposed.sql.javatime.datetime
|
||||||
@ -413,7 +414,7 @@ object TradingLogStore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addLog(tradingDecision: TradingDecision , decision: String, log: String) {
|
fun addLog(tradingDecision: TradingDecision, decision: String, log: String) {
|
||||||
synchronized(this) {
|
synchronized(this) {
|
||||||
if (decisionLogs.size > 1000) decisionLogs.removeAt(0)
|
if (decisionLogs.size > 1000) decisionLogs.removeAt(0)
|
||||||
decisionLogs.add(
|
decisionLogs.add(
|
||||||
|
|||||||
@ -1,23 +1,36 @@
|
|||||||
// src/main/kotlin/network/RagService.kt
|
package network// src/main/kotlin/network/RagService.kt
|
||||||
|
|
||||||
import dev.langchain4j.community.rag.content.retriever.lucene.LuceneEmbeddingStore
|
import dev.langchain4j.community.rag.content.retriever.lucene.LuceneEmbeddingStore
|
||||||
import dev.langchain4j.data.document.Metadata
|
import dev.langchain4j.data.document.Metadata
|
||||||
import dev.langchain4j.data.message.UserMessage
|
|
||||||
import dev.langchain4j.data.segment.TextSegment
|
import dev.langchain4j.data.segment.TextSegment
|
||||||
|
import dev.langchain4j.exception.InternalServerException
|
||||||
import dev.langchain4j.model.openai.OpenAiChatModel
|
import dev.langchain4j.model.openai.OpenAiChatModel
|
||||||
import dev.langchain4j.model.openai.OpenAiEmbeddingModel
|
import dev.langchain4j.model.openai.OpenAiEmbeddingModel
|
||||||
|
import dev.langchain4j.service.AiServices
|
||||||
|
import dev.langchain4j.service.SystemMessage
|
||||||
import dev.langchain4j.store.embedding.EmbeddingSearchRequest
|
import dev.langchain4j.store.embedding.EmbeddingSearchRequest
|
||||||
import dev.langchain4j.store.embedding.filter.MetadataFilterBuilder
|
import dev.langchain4j.store.embedding.filter.MetadataFilterBuilder
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.coroutineScope
|
import kotlinx.coroutines.coroutineScope
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import model.CandleData
|
import kotlinx.serialization.json.add
|
||||||
import network.DartCodeManager
|
import kotlinx.serialization.json.addJsonObject
|
||||||
import network.FinancialMapper
|
import kotlinx.serialization.json.buildJsonObject
|
||||||
import network.FinancialStatement
|
import kotlinx.serialization.json.jsonArray
|
||||||
import network.NewsService
|
import kotlinx.serialization.json.jsonObject
|
||||||
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
|
import kotlinx.serialization.json.put
|
||||||
|
import kotlinx.serialization.json.putJsonArray
|
||||||
|
import kotlinx.serialization.json.putJsonObject
|
||||||
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
import org.apache.lucene.store.MMapDirectory
|
import org.apache.lucene.store.MMapDirectory
|
||||||
|
import org.slf4j.MDC.put
|
||||||
import service.FinancialAnalyzer
|
import service.FinancialAnalyzer
|
||||||
import service.InvestmentScores
|
import service.InvestmentScores
|
||||||
import service.TechnicalAnalyzer
|
import service.TechnicalAnalyzer
|
||||||
@ -25,9 +38,10 @@ import service.TradingDecisionCallback
|
|||||||
import service.UrlCacheManager
|
import service.UrlCacheManager
|
||||||
import java.nio.file.Paths
|
import java.nio.file.Paths
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
interface TradingAnalyst {
|
interface TradingAnalyst {
|
||||||
@dev.langchain4j.service.SystemMessage("""
|
@SystemMessage("""
|
||||||
You are a Senior Stock Analyst.
|
You are a Senior Stock Analyst.
|
||||||
Analyze the data and provide a decision in JSON format.
|
Analyze the data and provide a decision in JSON format.
|
||||||
You must respond ONLY with a valid JSON object.
|
You must respond ONLY with a valid JSON object.
|
||||||
@ -48,13 +62,13 @@ object RagService {
|
|||||||
.apiKey("unused")
|
.apiKey("unused")
|
||||||
.temperature(0.0) // [중요] 0.0으로 설정하여 결정론적 응답 유도
|
.temperature(0.0) // [중요] 0.0으로 설정하여 결정론적 응답 유도
|
||||||
.timeout(Duration.ofSeconds(60))
|
.timeout(Duration.ofSeconds(60))
|
||||||
.frequencyPenalty(1.1)
|
// .frequencyPenalty(1.1)
|
||||||
.maxTokens(500) // 👈 루프 방지를 위해 반드시 짧게 제한!
|
.maxTokens(400) // 👈 루프 방지를 위해 반드시 짧게 제한!
|
||||||
// 1.x 버전에서는 responseFormat이 아래처럼 바뀔 수 있으니 체크하세요
|
// 1.x 버전에서는 responseFormat이 아래처럼 바뀔 수 있으니 체크하세요
|
||||||
.responseFormat("json_object")
|
.responseFormat("json_object")
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
private val analyst = dev.langchain4j.service.AiServices.builder(TradingAnalyst::class.java)
|
private val analyst = AiServices.builder(TradingAnalyst::class.java)
|
||||||
.chatModel(chatModel)
|
.chatModel(chatModel)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
@ -139,8 +153,18 @@ object RagService {
|
|||||||
|
|
||||||
object JsonSanitizer {
|
object JsonSanitizer {
|
||||||
fun formatJson(raw: String): String {
|
fun formatJson(raw: String): String {
|
||||||
|
// 실제 응답 로그 출력 (디버깅용)
|
||||||
|
println("📥 [AI Raw Response]:\n$raw")
|
||||||
|
|
||||||
val regex = Regex("""\{.*\}""", RegexOption.DOT_MATCHES_ALL)
|
val regex = Regex("""\{.*\}""", RegexOption.DOT_MATCHES_ALL)
|
||||||
return raw.trim()
|
val match = regex.find(raw)?.value
|
||||||
|
|
||||||
|
if (match == null) {
|
||||||
|
println("⚠️ [JsonSanitizer] JSON 형식을 찾을 수 없습니다.")
|
||||||
|
return "{}" // 빈 객체라도 반환하여 EOF 방지
|
||||||
|
}
|
||||||
|
|
||||||
|
return match.trim()
|
||||||
.removePrefix("```json")
|
.removePrefix("```json")
|
||||||
.removePrefix("```")
|
.removePrefix("```")
|
||||||
.removeSuffix("```")
|
.removeSuffix("```")
|
||||||
@ -264,13 +288,80 @@ object RagService {
|
|||||||
//// println(response)
|
//// println(response)
|
||||||
// return response.aiMessage().text()
|
// return response.aiMessage().text()
|
||||||
// }
|
// }
|
||||||
|
private const val LLM_API_URL = "http://127.0.0.1:8080/v1/chat/completions"
|
||||||
|
|
||||||
|
private suspend fun callLlamaWithSchema(prompt: String): String {
|
||||||
|
val jsonMediaType = "application/json; charset=utf-8".toMediaType()
|
||||||
|
|
||||||
|
// 문자열 치환 대신 안전한 JSON 객체 빌더 사용
|
||||||
|
val requestBodyJson = buildJsonObject {
|
||||||
|
put("model", "local-model")
|
||||||
|
put("temperature", 0.1) // 0.1 유지 (결정론적 응답)
|
||||||
|
put("top_p", 0.9)
|
||||||
|
put("max_tokens", 500)
|
||||||
|
|
||||||
|
putJsonArray("messages") {
|
||||||
|
addJsonObject {
|
||||||
|
put("role", "system")
|
||||||
|
put("content", "You are a helpful AI financial analyst. You must output responses ONLY in valid JSON format.")
|
||||||
|
}
|
||||||
|
addJsonObject {
|
||||||
|
put("role", "user")
|
||||||
|
put("content", prompt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 💡 복잡한 json_schema를 지우고, 단순히 JSON 형식으로만 내보내라고 지시합니다.
|
||||||
|
putJsonObject("response_format") {
|
||||||
|
put("type", "json_object")
|
||||||
|
}
|
||||||
|
}.toString()
|
||||||
|
println("requestBodyJson =>> $requestBodyJson")
|
||||||
|
val request = Request.Builder()
|
||||||
|
.url(LLM_API_URL)
|
||||||
|
.post(requestBodyJson.toRequestBody(jsonMediaType))
|
||||||
|
.build()
|
||||||
|
|
||||||
|
return kotlinx.coroutines.Dispatchers.IO.let {
|
||||||
|
kotlinx.coroutines.withContext(it) {
|
||||||
|
httpClient.newCall(request).execute().use { response ->
|
||||||
|
if (!response.isSuccessful) throw Exception("LLM API Error: ${response.code} ${response.message}")
|
||||||
|
|
||||||
|
val responseBody = response.body?.string() ?: "{}"
|
||||||
|
val json = Json.parseToJsonElement(responseBody).jsonObject
|
||||||
|
json["choices"]?.jsonArray?.get(0)?.jsonObject?.get("message")?.jsonObject?.get("content")?.jsonPrimitive?.content ?: "{}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val httpClient = OkHttpClient.Builder()
|
||||||
|
.connectTimeout(60, TimeUnit.SECONDS)
|
||||||
|
.readTimeout(120, TimeUnit.SECONDS)
|
||||||
|
.build()
|
||||||
suspend fun decideTrading(
|
suspend fun decideTrading(
|
||||||
stockName: String,
|
stockName: String,
|
||||||
scores: InvestmentScores, // 직접 계산한 점수 객체
|
scores: InvestmentScores, // 직접 계산한 점수 객체
|
||||||
financialStmt: FinancialStatement, // 매핑된 재무 수치 객체
|
financialStmt: FinancialStatement, // 매핑된 재무 수치 객체
|
||||||
tempDecision: TradingDecision
|
tempDecision: TradingDecision
|
||||||
): TradingDecision? {
|
): TradingDecision? {
|
||||||
|
// 💡 1. 뉴스 데이터가 유효한지(100자 이상인지) 확인
|
||||||
|
val validNews = tempDecision.newsContext?.takeIf { it.trim().length >= 100 }
|
||||||
|
|
||||||
|
// 💡 2. 동적 데이터 섹션 구성
|
||||||
|
val newsDataSection = if (validNews != null) {
|
||||||
|
"3. News Context: $validNews"
|
||||||
|
} else {
|
||||||
|
"3. News Context: None available. Base your decision ONLY on System Scores and Financials."
|
||||||
|
}
|
||||||
|
|
||||||
|
// 💡 3. 동적 제약 조건 구성
|
||||||
|
val newsConstraint = if (validNews != null) {
|
||||||
|
"- Match Financials with News: If profit is negative but news is hyped, stay CAUTIOUS (HOLD)."
|
||||||
|
} else {
|
||||||
|
"- No news data is available. Rely strictly on Financials and System Scores for your 'decision' and 'reason'."
|
||||||
|
}
|
||||||
|
|
||||||
val prompt = """
|
val prompt = """
|
||||||
# Task: Senior AI Investment Analyst
|
# Task: Senior AI Investment Analyst
|
||||||
Analyze the stock '$stockName' and determine the final trading decision based on the data below.
|
Analyze the stock '$stockName' and determine the final trading decision based on the data below.
|
||||||
@ -278,51 +369,52 @@ Analyze the stock '$stockName' and determine the final trading decision based on
|
|||||||
# Data
|
# Data
|
||||||
1. System Scores: Scalping(${scores.ultraShort}), Short(${scores.shortTerm}), Mid(${scores.midTerm}), Long(${scores.longTerm})
|
1. System Scores: Scalping(${scores.ultraShort}), Short(${scores.shortTerm}), Mid(${scores.midTerm}), Long(${scores.longTerm})
|
||||||
2. Financials: Operating Profit ${if(financialStmt.isOperatingProfitPositive) "PROFIT" else "LOSS"} (Growth: ${"%.2f".format(financialStmt.operatingProfitGrowth)}%), ROE: ${"%.2f".format(financialStmt.roe)}%, Debt: ${"%.2f".format(financialStmt.debtRatio)}%
|
2. Financials: Operating Profit ${if(financialStmt.isOperatingProfitPositive) "PROFIT" else "LOSS"} (Growth: ${"%.2f".format(financialStmt.operatingProfitGrowth)}%), ROE: ${"%.2f".format(financialStmt.roe)}%, Debt: ${"%.2f".format(financialStmt.debtRatio)}%
|
||||||
3. News Context: ${tempDecision.newsContext?.take(400)} // 👈 뉴스 길이를 물리적으로 제한
|
$newsDataSection
|
||||||
|
|
||||||
# Constraints
|
# Constraints
|
||||||
1. 모든 점수와 confidence는 0에서 100 사이의 **정수(Integer)**로만 작성하십시오.
|
- Copy the exact 'System Scores' from the Data section into the output JSON.
|
||||||
- Match Financials with News: If profit is negative but news is hyped, stay CAUTIOUS (HOLD).
|
- Match Financials with News: If profit is negative but news is hyped, stay CAUTIOUS (HOLD).
|
||||||
- Synchronization: High scalping score + positive news momentum = Higher BUY confidence.
|
- The "reason" field MUST be written in KOREAN and MUST NOT exceed 50 characters. Keep it concise.
|
||||||
- Output: Response ONLY in valid JSON format. No extra text.
|
- Output ONLY a valid JSON object matching the exact structure below. DO NOT output placeholder text like '(integer)'.
|
||||||
|
|
||||||
# Output JSON Format (Reason must be in Korean)
|
# Example Output JSON Format
|
||||||
{
|
{
|
||||||
"ultraShortScore": ${scores.ultraShort},
|
"ultraShortScore": 0,
|
||||||
"shortTermScore": ${scores.shortTerm},
|
"shortTermScore": 0,
|
||||||
"midTermScore": ${scores.midTerm},
|
"midTermScore": 0,
|
||||||
"longTermScore": ${scores.longTerm},
|
"longTermScore": 0,
|
||||||
"decision": "BUY/SELL/HOLD",
|
"decision": "HOLD",
|
||||||
"reason": "재무와 뉴스를 대조한 분석 결과 (한국어)",
|
"reason": "적자 지속 및 네수파립 임상 대기 중으로 관망 필요",
|
||||||
"confidence": 0~100
|
"confidence": 50
|
||||||
}
|
}
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
|
|
||||||
val response = chatModel.chat(UserMessage.from(prompt))
|
|
||||||
val rawResponse = response.aiMessage().text()
|
|
||||||
val jsonResponse = JsonSanitizer.formatJson(rawResponse)
|
|
||||||
// println("📥 [AI Raw JSON]:\n$jsonResponse")
|
|
||||||
|
|
||||||
// 2. 유연한 파서 설정 (소수점 및 예외 상황 대응)
|
return try {
|
||||||
|
val rawResponse = callLlamaWithSchema(prompt)
|
||||||
|
println("📥 [AI Strict JSON]:\n$rawResponse")
|
||||||
|
|
||||||
|
// 엄격한 스키마가 적용되었으므로 JsonSanitizer 없이 바로 파싱 가능
|
||||||
val lenientJson = Json {
|
val lenientJson = Json {
|
||||||
ignoreUnknownKeys = true
|
ignoreUnknownKeys = true
|
||||||
isLenient = true
|
isLenient = true
|
||||||
coerceInputValues = true
|
coerceInputValues = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val decision = lenientJson.decodeFromString<TradingDecision>(rawResponse)
|
||||||
|
|
||||||
|
// 데이터 매핑
|
||||||
|
decision.apply {
|
||||||
|
financialData = tempDecision.financialData
|
||||||
|
newsContext = tempDecision.newsContext
|
||||||
|
techSummary = tempDecision.techSummary
|
||||||
|
stockCode = tempDecision.stockCode
|
||||||
|
corpName = tempDecision.corpName
|
||||||
|
this.stockName = tempDecision.stockName
|
||||||
|
}
|
||||||
|
|
||||||
// JSON 파싱 (Kotlinx Serialization 활용)
|
|
||||||
return try {
|
|
||||||
// println(jsonResponse)
|
|
||||||
val decision = lenientJson.decodeFromString<TradingDecision>(jsonResponse)
|
|
||||||
decision.financialData = tempDecision.financialData
|
|
||||||
decision.newsContext = tempDecision.newsContext
|
|
||||||
decision.techSummary = tempDecision.techSummary
|
|
||||||
decision.stockCode = tempDecision.stockCode
|
|
||||||
decision.corpName = tempDecision.corpName
|
|
||||||
decision.stockName = tempDecision.stockName
|
|
||||||
decision
|
decision
|
||||||
} catch (e: dev.langchain4j.exception.InternalServerException) {
|
} catch (e: InternalServerException) {
|
||||||
// 서버 에러 (컨텍스트 초과 등) 발생 시 로그 남기고 null 반환 혹은 커스텀 에러 처리
|
// 서버 에러 (컨텍스트 초과 등) 발생 시 로그 남기고 null 반환 혹은 커스텀 에러 처리
|
||||||
println("🚨 [AI Server Error] ${e.message}")
|
println("🚨 [AI Server Error] ${e.message}")
|
||||||
if (e.message?.contains("Context size") == true) {
|
if (e.message?.contains("Context size") == true) {
|
||||||
|
|||||||
@ -1,9 +1,8 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import AutoTradeItem
|
import AutoTradeItem
|
||||||
import TradingDecision
|
import network.TradingDecision
|
||||||
import TradingLogStore
|
import TradingLogStore
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import getLlamaBinPath
|
import getLlamaBinPath
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@ -27,11 +26,11 @@ import model.RankingStock
|
|||||||
import model.RankingType
|
import model.RankingType
|
||||||
import model.UnifiedBalance
|
import model.UnifiedBalance
|
||||||
import network.DartCodeManager
|
import network.DartCodeManager
|
||||||
import network.FinancialMapper
|
|
||||||
import network.FinancialStatement
|
import network.FinancialStatement
|
||||||
import network.KisAuthService
|
import network.KisAuthService
|
||||||
import network.KisTradeService
|
import network.KisTradeService
|
||||||
import network.KisWebSocketManager
|
import network.KisWebSocketManager
|
||||||
|
import network.RagService
|
||||||
import network.StockUniverseLoader
|
import network.StockUniverseLoader
|
||||||
import util.MarketUtil
|
import util.MarketUtil
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
|
|||||||
@ -2,13 +2,9 @@ package service
|
|||||||
|
|
||||||
import com.microsoft.playwright.Browser
|
import com.microsoft.playwright.Browser
|
||||||
import com.microsoft.playwright.Playwright
|
import com.microsoft.playwright.Playwright
|
||||||
import com.microsoft.playwright.BrowserType
|
|
||||||
import com.microsoft.playwright.Page
|
import com.microsoft.playwright.Page
|
||||||
import com.microsoft.playwright.options.LoadState
|
import com.microsoft.playwright.options.LoadState
|
||||||
import com.microsoft.playwright.options.WaitUntilState
|
import com.microsoft.playwright.options.WaitUntilState
|
||||||
import kotlinx.coroutines.TimeoutCancellationException
|
|
||||||
import kotlinx.coroutines.async
|
|
||||||
import kotlinx.coroutines.awaitAll
|
|
||||||
import kotlinx.coroutines.coroutineScope
|
import kotlinx.coroutines.coroutineScope
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
@ -18,6 +14,7 @@ import kotlinx.coroutines.sync.withPermit
|
|||||||
import kotlinx.coroutines.withTimeout
|
import kotlinx.coroutines.withTimeout
|
||||||
import model.NewsItem
|
import model.NewsItem
|
||||||
import network.CorpInfo
|
import network.CorpInfo
|
||||||
|
import network.RagService
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
|
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import kotlinx.coroutines.CoroutineScope
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import network.RagService
|
||||||
import java.io.BufferedReader
|
import java.io.BufferedReader
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.InputStreamReader
|
import java.io.InputStreamReader
|
||||||
@ -31,7 +32,7 @@ object LlamaServerManager {
|
|||||||
else -> 0 to 4 // 인텔 맥 2017 등
|
else -> 0 to 4 // 인텔 맥 2017 등
|
||||||
}
|
}
|
||||||
|
|
||||||
val command = listOf(
|
val command = mutableListOf(
|
||||||
binPath,
|
binPath,
|
||||||
"-m", modelPath,
|
"-m", modelPath,
|
||||||
"--port", port.toString(),
|
"--port", port.toString(),
|
||||||
@ -40,7 +41,13 @@ object LlamaServerManager {
|
|||||||
"-t", threads.toString(),
|
"-t", threads.toString(),
|
||||||
"--embedding"
|
"--embedding"
|
||||||
)
|
)
|
||||||
|
if (port != 8081) { // 텍스트 생성용 모델에만 적용
|
||||||
|
command.addAll(listOf(
|
||||||
|
"-b", "512", // Batch size (토큰 병렬 처리량 제한으로 연산 안정화)
|
||||||
|
"--threads-batch", threads.toString(),
|
||||||
|
"-fa","on" // Flash Attention 활성화 (메모리 절약 및 긴 컨텍스트 연산 안정성 증가)
|
||||||
|
))
|
||||||
|
}
|
||||||
scope.launch {
|
scope.launch {
|
||||||
try {
|
try {
|
||||||
val pb = ProcessBuilder(command)
|
val pb = ProcessBuilder(command)
|
||||||
|
|||||||
@ -59,11 +59,17 @@ object SystemSleepPreventer {
|
|||||||
* 맥의 절전 모드 및 디스플레이 취침을 방지하는 명령 실행
|
* 맥의 절전 모드 및 디스플레이 취침을 방지하는 명령 실행
|
||||||
*/
|
*/
|
||||||
fun start() {
|
fun start() {
|
||||||
|
val os = System.getProperty("os.name").lowercase()
|
||||||
|
val arch = System.getProperty("os.arch").lowercase()
|
||||||
|
val isWin = os.contains("win")
|
||||||
val root = LoggerFactory.getLogger("Exposed") as Logger
|
val root = LoggerFactory.getLogger("Exposed") as Logger
|
||||||
root.level = Level.ERROR
|
root.level = Level.ERROR
|
||||||
|
if (!isWin) {
|
||||||
checkAndRequestAccessibility()
|
checkAndRequestAccessibility()
|
||||||
if (process?.isAlive == true) return
|
}
|
||||||
|
|
||||||
|
if (process?.isAlive == true) return
|
||||||
|
if (!isWin) {
|
||||||
try {
|
try {
|
||||||
// -i: 시스템 절전 방지, -d: 디스플레이 취침 방지, -m: 디스크 유휴 상태 방지
|
// -i: 시스템 절전 방지, -d: 디스플레이 취침 방지, -m: 디스크 유휴 상태 방지
|
||||||
val command = listOf("caffeinate", "-i", "-d", "-m")
|
val command = listOf("caffeinate", "-i", "-d", "-m")
|
||||||
@ -73,6 +79,7 @@ object SystemSleepPreventer {
|
|||||||
println("⚠️ [System] caffeinate 실행 실패: ${e.message}")
|
println("⚠️ [System] caffeinate 실행 실패: ${e.message}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 모니터를 즉시 잠자기 모드로 전환
|
* 모니터를 즉시 잠자기 모드로 전환
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
package ui
|
package ui
|
||||||
|
|
||||||
import AutoTradeItem
|
import AutoTradeItem
|
||||||
import TradingDecision
|
import network.TradingDecision
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.lazy.grid.GridCells
|
import androidx.compose.foundation.lazy.grid.GridCells
|
||||||
@ -19,10 +19,6 @@ import androidx.compose.ui.graphics.Color
|
|||||||
import androidx.compose.ui.text.input.ImeAction
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
import androidx.compose.ui.text.input.KeyboardType
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.CoroutineStart
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import model.CandleData
|
import model.CandleData
|
||||||
import model.ConfigIndex
|
import model.ConfigIndex
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
package ui
|
package ui
|
||||||
|
|
||||||
import AutoTradeItem
|
import AutoTradeItem
|
||||||
import TradingDecision
|
import network.TradingDecision
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
@ -18,7 +18,6 @@ import androidx.compose.ui.text.input.VisualTransformation
|
|||||||
import androidx.compose.ui.text.rememberTextMeasurer
|
import androidx.compose.ui.text.rememberTextMeasurer
|
||||||
import androidx.compose.ui.unit.TextUnit
|
import androidx.compose.ui.unit.TextUnit
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.min
|
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import model.ConfigIndex
|
import model.ConfigIndex
|
||||||
|
|||||||
@ -128,32 +128,60 @@ fun SettingsScreen(onAuthSuccess: () -> Unit) {
|
|||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(), // 필요에 따라 너비 설정
|
modifier = Modifier.fillMaxWidth(), // 필요에 따라 너비 설정
|
||||||
verticalAlignment = Alignment.CenterVertically // 상하 중앙 정렬 추가
|
verticalAlignment = Alignment.CenterVertically // 상하 중앙 정렬 추가
|
||||||
){
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier.weight(0.5f).height(60.dp).border(1.dp, Color.Gray, RoundedCornerShape(8.dp))
|
modifier = Modifier.weight(0.5f).height(60.dp).border(1.dp, Color.Gray, RoundedCornerShape(8.dp))
|
||||||
.onExternalDrag(onDrop = { state ->
|
.onExternalDrag(onDrop = { state ->
|
||||||
val data = state.dragData
|
val data = state.dragData
|
||||||
if (data is DragData.FilesList) {
|
if (data is DragData.FilesList) {
|
||||||
val path = data.readFiles().firstOrNull()?.removePrefix("file:")
|
val rawUri = data.readFiles().firstOrNull()
|
||||||
if (path?.endsWith(".gguf") == true) config = config.copy(modelPath = path,)
|
if (rawUri != null) {
|
||||||
|
// 1. file:// 또는 file: 접두사 제거
|
||||||
|
var path = rawUri.removePrefix("file://").removePrefix("file:")
|
||||||
|
|
||||||
|
// 2. 윈도우 환경의 드라이브 문자(예: /C:/) 앞의 슬래시 제거
|
||||||
|
if (path.startsWith("/") && path.getOrNull(2) == ':') {
|
||||||
|
path = path.drop(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path.endsWith(".gguf")) config = config.copy(modelPath = path)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
Text(if(config.modelPath.isEmpty()) "GGUF 모델 파일을 여기로 드래그하세요" else config.modelPath, fontSize = 12.sp)
|
Text(
|
||||||
|
if (config.modelPath.isEmpty()) "GGUF 모델 파일을 여기로 드래그하세요" else config.modelPath,
|
||||||
|
fontSize = 12.sp
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier.weight(0.5f).height(60.dp).border(1.dp, Color.Gray, RoundedCornerShape(8.dp))
|
modifier = Modifier.weight(0.5f).height(60.dp).border(1.dp, Color.Gray, RoundedCornerShape(8.dp))
|
||||||
.onExternalDrag(onDrop = { state ->
|
.onExternalDrag(onDrop = { state ->
|
||||||
val data = state.dragData
|
val data = state.dragData
|
||||||
if (data is DragData.FilesList) {
|
if (data is DragData.FilesList) {
|
||||||
val embedModelPath = data.readFiles().firstOrNull()?.removePrefix("file:")
|
val rawUri = data.readFiles().firstOrNull()
|
||||||
if (embedModelPath?.endsWith(".gguf") == true) config = config.copy(embedModelPath = embedModelPath,)
|
if (rawUri != null) {
|
||||||
|
// 1. file:// 또는 file: 접두사 제거
|
||||||
|
var embedModelPath = rawUri.removePrefix("file://").removePrefix("file:")
|
||||||
|
|
||||||
|
// 2. 윈도우 환경의 드라이브 문자(예: /C:/) 앞의 슬래시 제거
|
||||||
|
if (embedModelPath.startsWith("/") && embedModelPath.getOrNull(2) == ':') {
|
||||||
|
embedModelPath = embedModelPath.drop(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (embedModelPath.endsWith(".gguf")) config =
|
||||||
|
config.copy(embedModelPath = embedModelPath)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
Text(if(config.embedModelPath.isEmpty()) "임베드용 GGUF 모델 파일을 여기로 드래그하세요" else config.embedModelPath, fontSize = 12.sp)
|
Text(
|
||||||
|
if (config.embedModelPath.isEmpty()) "임베드용 GGUF 모델 파일을 여기로 드래그하세요" else config.embedModelPath,
|
||||||
|
fontSize = 12.sp
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Spacer(Modifier.height(10.dp))
|
Spacer(Modifier.height(10.dp))
|
||||||
|
|||||||
@ -2,7 +2,7 @@ package ui
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
import TradingDecision
|
import network.TradingDecision
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.material.*
|
import androidx.compose.material.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user