From 2d577300c3be39f4096c90cef59db9ac987353f4 Mon Sep 17 00:00:00 2001 From: lunaticbum Date: Fri, 17 Apr 2026 18:08:53 +0900 Subject: [PATCH] ... --- src/main/kotlin/database/DatabaseFactory.kt | 2 + src/main/kotlin/model/AppConfig.kt | 24 +- src/main/kotlin/model/StockModels.kt | 70 ++- src/main/kotlin/network/KisTradeService.kt | 52 +- .../kotlin/report/LocalReportGenerator.kt | 589 +++++++++++++----- .../kotlin/report/TradingReportManager.kt | 572 +++++++++++------ .../kotlin/report/database/ReportTables.kt | 31 +- src/main/kotlin/service/AutoTradingManager.kt | 141 +++-- src/main/kotlin/ui/TradingDecisionLog.kt | 21 +- 9 files changed, 1023 insertions(+), 479 deletions(-) diff --git a/src/main/kotlin/database/DatabaseFactory.kt b/src/main/kotlin/database/DatabaseFactory.kt index 8eb3c38..1020849 100644 --- a/src/main/kotlin/database/DatabaseFactory.kt +++ b/src/main/kotlin/database/DatabaseFactory.kt @@ -9,6 +9,7 @@ import org.jetbrains.exposed.sql.transactions.transaction import report.TradingReportManager import report.TradingReportService import report.database.AssetSnapshotTable +import report.database.ConfigHistoryTable import report.database.ExecutionDetailsTable import report.database.SnapshotHoldingsTable import report.database.TradeHistoryTable @@ -139,6 +140,7 @@ object DatabaseFactory { transaction(reportDb) { SchemaUtils.createMissingTablesAndColumns( + ConfigHistoryTable, AssetSnapshotTable, SnapshotHoldingsTable, TradeHistoryTable, diff --git a/src/main/kotlin/model/AppConfig.kt b/src/main/kotlin/model/AppConfig.kt index 0eef864..ebfd649 100644 --- a/src/main/kotlin/model/AppConfig.kt +++ b/src/main/kotlin/model/AppConfig.kt @@ -1,7 +1,8 @@ package model +import report.TradingReportManager import java.time.LocalDateTime - +import kotlin.math.abs enum class ConfigIndex(val index : Int,val label : String) { @@ -118,8 +119,9 @@ data class AppConfig( return if (isSimulation) vtsAccountNo else realAccountNo } - + var firstSet = mutableSetOf() fun setValues(index :ConfigIndex , value : Double) { + val oldValue = getValues(index) when (index) { ConfigIndex.TAX_INDEX -> {FEES_AND_TAXRATE = value} ConfigIndex.PROFIT_INDEX -> {MINIMUM_NET_PROFIT = value} @@ -155,6 +157,18 @@ data class AppConfig( ConfigIndex.TAKE_PROFIT -> { take_profit = value > 0.1 } ConfigIndex.MAX_HOLDING_COUNT -> { max_holding_count = value } } + if (firstSet.contains(index)) { + DatabaseFactory.saveConfig(KisSession.config) + TradingLogStore.addSettingLog( + index.label, + oldValue.toString(), + value.toString(), + "๐Ÿ’พ ์ €์žฅ๋จ: ${index.label} = ${getValues(index)}" + ) + TradingReportManager.recordConfigChange() + } else { + firstSet.add(index) + } } fun getValues(index :ConfigIndex) : Double { return when (index) { @@ -196,9 +210,9 @@ data class AppConfig( ConfigIndex.GRADE_3_ALLOCATIONRATE -> {GRADE_3_ALLOCATIONRATE} ConfigIndex.GRADE_2_ALLOCATIONRATE -> {GRADE_2_ALLOCATIONRATE} ConfigIndex.GRADE_1_ALLOCATIONRATE -> {GRADE_1_ALLOCATIONRATE} - ConfigIndex.LOSS_MAXRATE -> { loss_max} - ConfigIndex.LOSS_MINRATE -> { loss_min} - ConfigIndex.LOSS_MAX_MONEY -> { loss_money } + ConfigIndex.LOSS_MAXRATE -> { abs(loss_max) * -1} + ConfigIndex.LOSS_MINRATE -> { abs(loss_min) * -1} + ConfigIndex.LOSS_MAX_MONEY -> { abs(loss_money) * -1} ConfigIndex.STOP_LOSS -> {if(!stop_Loss) 0.0 else 1.0} ConfigIndex.TAKE_PROFIT -> {if(!take_profit) 0.0 else 1.0} ConfigIndex.MAX_COUNT_INDEX -> {MAX_COUNT.toDouble()} diff --git a/src/main/kotlin/model/StockModels.kt b/src/main/kotlin/model/StockModels.kt index 0dea069..cc4e96c 100644 --- a/src/main/kotlin/model/StockModels.kt +++ b/src/main/kotlin/model/StockModels.kt @@ -3,38 +3,43 @@ package model import AutoTradeItem import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable - @Serializable data class StockBalanceResponse( val rt_cd: String = "", val msg1: String = "", val ctx_area_fk100: String = "", val ctx_area_nk100: String = "", - val output1: List = emptyList(), - val output2: List = emptyList() + val output1: List = emptyList(), // ์ข…๋ชฉ๋ณ„ ์ž”๊ณ  + val output2: List = emptyList() // ๊ณ„์ขŒ ์š”์•ฝ ) @Serializable data class StockHolding( - val pdno: String = "", // ์ƒํ’ˆ๋ฒˆํ˜ธ + val pdno: String = "", // ์ƒํ’ˆ๋ฒˆํ˜ธ (์ข…๋ชฉ์ฝ”๋“œ) val prdt_name: String = "", // ์ƒํ’ˆ๋ช… val hldg_qty: String = "0", // ๋ณด์œ ์ˆ˜๋Ÿ‰ - val pchs_avg_pric: String = "0", // ๋งค์ž…ํ‰๊ท ๊ฐ€ + val pchs_avg_pric: String = "0", // ๋งค์ž…ํ‰๊ท ๊ฐ€ (์ˆ˜์ต ๊ณ„์‚ฐ์˜ ๊ธฐ์ค€์ ) + val pchs_amt: String = "0", // ๋งค์ž…๊ธˆ์•กํ•ฉ๊ณ„ (ํˆฌ์ž ์›๊ธˆ) val prpr: String = "0", // ํ˜„์žฌ๊ฐ€ + val evlu_amt: String = "0", // ํ‰๊ฐ€๊ธˆ์•ก + val evlu_pfls_amt: String = "0", // ํ‰๊ฐ€์†์ต๊ธˆ์•ก (ํ‰๊ฐ€๊ธˆ์•ก - ๋งค์ž…๊ธˆ์•ก) val evlu_pfls_rt: String = "0.0", // ํ‰๊ฐ€์†์ต๋ฅ  - val evlu_amt: String = "0" , // ํ‰๊ฐ€๊ธˆ์•ก - val ord_psbl_qty : String = "0", - val thdt_buyqty : String = "0", + val fltt_rt: String = "0.0", // ๋“ฑ๋ฝ์œจ (๋‹น์ผ ์‹œ์žฅ ๊ฐ•๋„ ๋ถ„์„์šฉ) + val bfdy_cprs_icdc: String = "0", // ์ „์ผ๋Œ€๋น„์ฆ๊ฐ (์ˆ˜๊ธ‰ ํ™•์ธ์šฉ) + val ord_psbl_qty: String = "0", // ์ฃผ๋ฌธ๊ฐ€๋Šฅ์ˆ˜๋Ÿ‰ + val thdt_buyqty: String = "0" // ๊ธˆ์ผ๋งค์ˆ˜์ˆ˜๋Ÿ‰ ) @Serializable data class BalanceSummary( - val tot_evlu_amt: String = "0", // ์ด ํ‰๊ฐ€๊ธˆ์•ก - val evlu_pfls_rt: String = "0.0", // ์ด ์ˆ˜์ต๋ฅ  (์—๋Ÿฌ ๋ฐœ์ƒ ์ง€์ : ๊ธฐ๋ณธ๊ฐ’ ์ถ”๊ฐ€๋กœ ํ•ด๊ฒฐ) - val asst_icrt: String = "0.0", // ์ผ๋ถ€ ํ™˜๊ฒฝ์—์„œ ์ˆ˜์ต๋ฅ  ํ•„๋“œ๋ช… - val nass_amt: String = "0" , // ์ˆœ์ž์‚ฐ ๊ธˆ์•ก - val dnca_tot_amt: String = "0" + val dnca_tot_amt: String = "0", // ์˜ˆ์ˆ˜๊ธˆ์ด๊ธˆ์•ก + val tot_evlu_amt: String = "0", // ์ดํ‰๊ฐ€๊ธˆ์•ก (์ž์‚ฐ ์ด๊ณ„) + val pchs_amt_smtl_amt: String = "0", // ๋งค์ž…๊ธˆ์•กํ•ฉ๊ณ„๊ธˆ์•ก + val evlu_pfls_smtl_amt: String = "0", // ํ‰๊ฐ€์†์ตํ•ฉ๊ณ„๊ธˆ์•ก + val asst_icdc_amt: String = "0", // ์ž์‚ฐ์ฆ๊ฐ์•ก (์–ด์ œ ๋Œ€๋น„ ์„ฑ์  - ๋ฆฌํฌํŠธ ํ•ต์‹ฌ) + val thdt_tlex_amt: String = "0" // ๊ธˆ์ผ์ œ๋น„์šฉ๊ธˆ์•ก (์„ธ๊ธˆ/์ˆ˜์ˆ˜๋ฃŒ - ์ˆœ์ˆ˜์ต ๊ณ„์‚ฐ์šฉ) ) + @Serializable data class RankingResponse( var rt_cd : String, @@ -140,36 +145,39 @@ data class OverseasRankingStock( prdy_ctrt = rate ) } - @Serializable data class UnifiedStockHolding( val code: String, // ์ข…๋ชฉ์ฝ”๋“œ val name: String, // ์ข…๋ชฉ๋ช… val quantity: String, // ๋ณด์œ ์ˆ˜๋Ÿ‰ - val avgPrice: String, // ๋งค์ž…๋‹จ๊ฐ€ - val currentPrice: String, // ํ˜„์žฌ๊ฐ€ - val profitRate: String, // ์ˆ˜์ต๋ฅ  - val evalAmount: String, // ํ‰๊ฐ€๊ธˆ์•ก - val isDomestic: Boolean, // ๊ตญ๋‚ด/ํ•ด์™ธ ๊ตฌ๋ถ„ - val availOrderCount : String, - val thdtBuyQty: String, + val avgPrice: String, // ๋งค์ž…๋‹จ๊ฐ€ (pchs_avg_pric) + val currentPrice: String, // ํ˜„์žฌ๊ฐ€ (prpr) + val profitRate: String, // ์ˆ˜์ต๋ฅ  (evlu_pfls_rt) + val evalAmount: String, // ํ‰๊ฐ€๊ธˆ์•ก (evlu_amt) + val valuationProfitAmount: String, // ํ‰๊ฐ€์†์ต๊ธˆ์•ก (evlu_pfls_amt) + val isDomestic: Boolean, // ๊ตญ๋‚ด/ํ•ด์™ธ ๊ตฌ๋ถ„ + val availOrderCount: String, // ์ฃผ๋ฌธ๊ฐ€๋Šฅ์ˆ˜๋Ÿ‰ + val thdtBuyQty: String, // ๊ธˆ์ผ๋งค์ˆ˜์ˆ˜๋Ÿ‰ -){ - // ๋‹น์ผ ๋งค์ˆ˜ ์—ฌ๋ถ€ ํŒ๋ณ„ (๊ธˆ์ผ ๋งค์ˆ˜ ์ˆ˜๋Ÿ‰์ด 0๋ณด๋‹ค ํฌ๋ฉด ๋‹น์ผ ์ง„์ž… ์ข…๋ชฉ) - val isTodayEntry: Boolean get() = thdtBuyQty.toInt() > 0 + // ์ถ”๊ฐ€ ์ถ”์ฒœ ํ•„๋“œ + val dailyChangeRate: String = "0.0", // ๋‹น์ผ ๋“ฑ๋ฝ์œจ (fltt_rt) + val pchsAmount: String = "0" // ์ด ๋งค์ž…๊ธˆ์•ก (pchs_amt) +) { + val isTodayEntry: Boolean get() = thdtBuyQty.toIntOrNull() ?: 0 > 0 } @Serializable data class UnifiedBalance( - val totalAsset: String, // ์ด ํ‰๊ฐ€์ž์‚ฐ - val totalProfitRate: String, // ์ด ์ˆ˜์ต๋ฅ  - val deposit: String, - private val holdings: List // ํ†ตํ•ฉ ๋ณด์œ  ์ข…๋ชฉ ๋ฆฌ์ŠคํŠธ + val totalAsset: String, // ์ด ํ‰๊ฐ€์ž์‚ฐ + val deposit: String, // ์˜ˆ์ˆ˜๊ธˆ + val dailyAssetChange: String, // ๋‹น์ผ ์ž์‚ฐ ์ฆ๊ฐ + val todayFees: String, // ๋‹น์ผ ์ œ๋น„์šฉ + val totalProfitRate: String, // ์ง์ ‘ ๊ณ„์‚ฐ๋œ ์ด ์ˆ˜์ต๋ฅ  + + private val holdings: List ) { - fun getHolldings() = holdings.filter { it.quantity.toInt() > 0 } + fun getHoldings() = holdings.filter { (it.quantity.toIntOrNull() ?: 0) > 0 } } - - @Serializable data class UnfilledOrder( val orgn_odno: String, diff --git a/src/main/kotlin/network/KisTradeService.kt b/src/main/kotlin/network/KisTradeService.kt index b1a24d1..deafb9b 100644 --- a/src/main/kotlin/network/KisTradeService.kt +++ b/src/main/kotlin/network/KisTradeService.kt @@ -83,47 +83,75 @@ object KisTradeService { // ๊ตญ๋‚ด์™€ ํ•ด์™ธ ์ž”๊ณ ๋ฅผ ๋น„๋™๊ธฐ๋กœ ๋™์‹œ ํ˜ธ์ถœ val domesticJob = async { fetchDomesticRawBalance(marketCode) } val overseasJob = async { fetchOverseasRawBalance() } + try { val domRes = domesticJob.await().getOrNull() val ovsRes = overseasJob.await().getOrNull() val combinedHoldings = mutableListOf() - // ๊ตญ๋‚ด ์ข…๋ชฉ ๋งคํ•‘ + // 1. ๊ตญ๋‚ด ์ข…๋ชฉ ๋งคํ•‘ (์‹ ๊ทœ ์ถ”๊ฐ€๋œ ๋ชจ๋ธ ํŒŒ๋ผ๋ฏธํ„ฐ ๋ฐ˜์˜) domRes?.output1?.forEach { combinedHoldings.add(UnifiedStockHolding( code = it.pdno, name = it.prdt_name, quantity = it.hldg_qty, avgPrice = it.pchs_avg_pric, currentPrice = it.prpr, profitRate = it.evlu_pfls_rt, evalAmount = it.evlu_amt, isDomestic = true, - availOrderCount = it.ord_psbl_qty, thdtBuyQty = it.thdt_buyqty + availOrderCount = it.ord_psbl_qty, thdtBuyQty = it.thdt_buyqty, + valuationProfitAmount = it.evlu_pfls_amt, + // ๐Ÿ’ก [์ถ”๊ฐ€] ์‹ ๊ทœ ํŒŒ๋ผ๋ฏธํ„ฐ ๋งคํ•‘ + dailyChangeRate = it.fltt_rt, + pchsAmount = it.pchs_amt ).apply { if (it.hldg_qty.toLong() > 0) { -// println("๋ณด์œ  ์ข…๋ชฉ : ${it.prdt_name} , ์ˆ˜๋Ÿ‰ : ${it.hldg_qty}") +// println("๋ณด์œ  ์ข…๋ชฉ : ${it.prdt_name} , ์ˆ˜๋Ÿ‰ : ${it.hldg_qty}") } }) } - // ํ•ด์™ธ ์ข…๋ชฉ ๋งคํ•‘ (ํ•ด์™ธ API ์‘๋‹ต ๋ชจ๋ธ ๊ตฌ์กฐ์— ๋”ฐ๋ผ ํ•„๋“œ ๋งคํ•‘) + // 2. ํ•ด์™ธ ์ข…๋ชฉ ๋งคํ•‘ (์‹ ๊ทœ ์ถ”๊ฐ€๋œ ๋ชจ๋ธ ํŒŒ๋ผ๋ฏธํ„ฐ ๋ฐ˜์˜) + // ์ฃผ์˜: ํ•ด์™ธ API ์‘๋‹ต(ovsRes)์— fltt_rt, pchs_amt์™€ ์ •ํ™•ํžˆ ์ผ์น˜ํ•˜๋Š” ํ•„๋“œ๊ฐ€ ์—†๋‹ค๋ฉด + // ํ•ด๋‹นํ•˜๋Š” ํ•„๋“œ๋กœ ๋งคํ•‘ํ•˜๊ฑฐ๋‚˜, ์—†์„ ๊ฒฝ์šฐ ๊ธฐ๋ณธ๊ฐ’("0.0", "0")์„ ๋„ฃ์–ด์ฃผ์–ด์•ผ ์—๋Ÿฌ๊ฐ€ ์•ˆ ๋‚ฉ๋‹ˆ๋‹ค. ovsRes?.output1?.forEach { combinedHoldings.add(UnifiedStockHolding( code = it.pdno, name = it.prdt_name, quantity = it.hldg_qty, avgPrice = it.pchs_avg_pric, currentPrice = it.prpr, profitRate = it.evlu_pfls_rt, evalAmount = it.evlu_amt, isDomestic = false, - availOrderCount = it.ord_psbl_qty, thdtBuyQty = it.thdt_buyqty + availOrderCount = it.ord_psbl_qty, thdtBuyQty = it.thdt_buyqty, + valuationProfitAmount = it.evlu_pfls_amt, + // ๐Ÿ’ก [์ถ”๊ฐ€] ํ•ด์™ธ ํ•„๋“œ์—๋„ ๋งคํ•‘ (ํ•ด์™ธ API ๋ช…์„ธ์— ๋งž์ถฐ ํ•„๋“œ๋ช… ์กฐ์ • ํ•„์š”ํ•  ์ˆ˜ ์žˆ์Œ) + dailyChangeRate = it.fltt_rt ?: "0.0", + pchsAmount = it.pchs_amt ?: "0" )) } - val totalAmt = (domRes?.output2?.firstOrNull()?.tot_evlu_amt?.toLongOrNull() ?: 0L) + - (ovsRes?.output2?.firstOrNull()?.tot_evlu_amt?.toLongOrNull() ?: 0L) - val depositAmt = domRes?.output2?.firstOrNull()?.dnca_tot_amt?.toLongOrNull() ?: 0L + // 4. ์ตœ์ข… ํ†ตํ•ฉ ์ž”๊ณ  ๋ฐ˜ํ™˜ + val domSummary = domRes?.output2?.firstOrNull() + val ovsSummary = ovsRes?.output2?.firstOrNull() + +// 1. ์ž์‚ฐ ๋ฐ ๊ธˆ์•ก ํ•ฉ์‚ฐ + val totalPchsAmt = (domSummary?.pchs_amt_smtl_amt?.toLongOrNull() ?: 0L) + + (ovsSummary?.pchs_amt_smtl_amt?.toLongOrNull() ?: 0L) + val totalPflsAmt = (domSummary?.evlu_pfls_smtl_amt?.toLongOrNull() ?: 0L) + + (ovsSummary?.evlu_pfls_smtl_amt?.toLongOrNull() ?: 0L) + +// 2. ๊ณ„์ขŒ ์ „์ฒด ์ˆ˜์ต๋ฅ  ๊ณ„์‚ฐ: (ํ‰๊ฐ€์†์ตํ•ฉ๊ณ„ / ๋งค์ž…๊ธˆ์•กํ•ฉ๊ณ„) * 100 + val calculatedTotalRate = if (totalPchsAmt > 0) { + (totalPflsAmt.toDouble() / totalPchsAmt.toDouble()) * 100 + } else 0.0 + +// 3. ๋ชจ๋ธ ์ƒ์„ฑ Result.success(UnifiedBalance( - totalAsset = String.format("%,d", totalAmt), - totalProfitRate = domRes?.output2?.firstOrNull()?.evlu_pfls_rt ?: "0.0", - deposit = String.format("%,d", depositAmt), + totalAsset = String.format("%,d", (domSummary?.tot_evlu_amt?.toLongOrNull() ?: 0L) + (ovsSummary?.tot_evlu_amt?.toLongOrNull() ?: 0L)), + deposit = String.format("%,d", domSummary?.dnca_tot_amt?.toLongOrNull() ?: 0L), + dailyAssetChange = String.format("%,d", (domSummary?.asst_icdc_amt?.toLongOrNull() ?: 0L) + (ovsSummary?.asst_icdc_amt?.toLongOrNull() ?: 0L)), + todayFees = String.format("%,d", (domSummary?.thdt_tlex_amt?.toLongOrNull() ?: 0L) + (ovsSummary?.thdt_tlex_amt?.toLongOrNull() ?: 0L)), + totalProfitRate = String.format("%.2f%%", calculatedTotalRate), // ๊ณ„์‚ฐ๋œ ๊ฐ’ ์ „๋‹ฌ holdings = combinedHoldings )) + } catch (e: Exception) { - Result.failure(e) } + Result.failure(e) + } } /** diff --git a/src/main/kotlin/report/LocalReportGenerator.kt b/src/main/kotlin/report/LocalReportGenerator.kt index 0facf1f..098a3d3 100644 --- a/src/main/kotlin/report/LocalReportGenerator.kt +++ b/src/main/kotlin/report/LocalReportGenerator.kt @@ -1,166 +1,477 @@ -package report - +import kotlinx.coroutines.* import java.awt.Desktop import java.io.File -import java.time.LocalDate import java.time.LocalDateTime import java.time.format.DateTimeFormatter +import java.time.Duration object LocalReportGenerator { - // ๐Ÿ’ก ๋ชจ๋“  ๊ฑฐ๋ž˜ ๋‚ด์—ญ์˜ ์ƒ์„ธ ์ง€ํ‘œ๋ฅผ ๋‹ด๋Š” DTO - data class TradeDetailData( - val isBuy: Boolean, // ๋งค์ˆ˜/๋งค๋„ ๊ตฌ๋ถ„ - val stockName: String, - val orderTime: String, // ์ฃผ๋ฌธ ์‹œ๊ฐ - val timeTaken: String, // ์ฒ˜๋ฆฌ ์™„๋ฃŒ ์ด ์‹œ๊ฐ„ ๋˜๋Š” ๋ฏธ์™„๋ฃŒ ์ƒํƒœ - val execQty: Int, // ์ฒ˜๋ฆฌ๋Ÿ‰ (์ฒด๊ฒฐ ์ˆ˜๋Ÿ‰) - val avgPrice: Long, // ์ฒด๊ฒฐ ํ‰๊ท  ๋‹จ๊ฐ€ - val profitRate: Double, // ์ˆ˜์ต๋ฅ  (๋งค๋„ ์‹œ์—๋งŒ ์œ ํšจ) - val profitAmount: Long, // ์ˆ˜์ต๊ธˆ์•ก (๋งค๋„ ์‹œ์—๋งŒ ์œ ํšจ) - val reason: String, - val investmentGrade: String?, - val aiScore: Double? + private val reportScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + + // --- [Raw ๋ฐ์ดํ„ฐ ๋ชจ๋ธ ์œ ์ง€] --- + data class RawSummaryData( + val type: String, + val startAsset: Long, + val endAsset: Long, + val dailyAssetChange: Long, + val todayFees: Long, + val totalProfitRate: Double ) - fun generateAndOpen(startAsset: Long, endAsset: Long, tradeLogs: List) { - val today = LocalDate.now().toString() - val profitAmount = endAsset - startAsset - val profitRate = if (startAsset > 0) (profitAmount.toDouble() / startAsset) * 100 else 0.0 + data class RawHoldingData( + val stockName: String, + val quantity: Int, + val avgPrice: Double, + val currentPrice: Double, + val evalAmount: Long, + // ๐Ÿ’ก ๊ธˆ์ผ OHLC ์ •๋ณด ์ถ”๊ฐ€ + val openPrice: Double = 0.0, + val highPrice: Double = 0.0, + val lowPrice: Double = 0.0, + val closePrice: Double = 0.0 + ) - val profitColor = if (profitAmount > 0) "#FF3B30" else if (profitAmount < 0) "#007AFF" else "#333333" - val profitSign = if (profitAmount > 0) "+" else "" - val timeFormatter = DateTimeFormatter.ofPattern("HH:mm:ss") - // 1. ๋งค๋งค ์ƒ์„ธ ๋‚ด์—ญ HTML ํ–‰ ์ƒ์„ฑ - val tradeRowsHtml = if (tradeLogs.isEmpty()) { - "๊ธˆ์ผ ๋ฐœ์ƒํ•œ ์ฃผ๋ฌธ ๋‚ด์—ญ์ด ์—†์Šต๋‹ˆ๋‹ค." - } else { - tradeLogs.joinToString("\n") { trade -> - // ๋งค์ˆ˜(๋นจ๊ฐ•), ๋งค๋„(ํŒŒ๋ž‘) ๋ฑƒ์ง€ - val typeBadge = if (trade.isBuy) "๋งค์ˆ˜" else "๋งค๋„" + data class RawExecutionData( + val price: Double, + val quantity: Int, + val execTime: String + ) - // ์‹œ๊ฐ„ ํฌ๋งทํŒ… - val orderTimeParsed = LocalDateTime.parse(trade.orderTime).format(timeFormatter) + data class RawTradeData( + val stockCode: String, + val stockName: String, + val isBuy: Boolean, + val status: String, + val orderTime: String, + val executions: List, + val currentPrice: Double, + val resolvedBuyPrice: Double, + val investmentGrade: String, + val reason: String, + val aiScore: Double, + // ๐Ÿ’ก ๊ธˆ์ผ OHLC ์ •๋ณด ์ถ”๊ฐ€ + val openPrice: Double = 0.0, + val highPrice: Double = 0.0, + val lowPrice: Double = 0.0, + val closePrice: Double = 0.0 + ) - // ์ˆ˜์ต๋ฅ /์ˆ˜์ต๊ธˆ ์ฒ˜๋ฆฌ (๋งค์ˆ˜์ผ ๊ฒฝ์šฐ ํ‘œ์‹œ ์•ˆ ํ•จ) - val rateText = if (trade.isBuy) "-" else "${String.format("%.2f", trade.profitRate)}%" - val rateColor = if (trade.isBuy || trade.profitRate == 0.0) "#333" else if (trade.profitRate > 0) "#FF3B30" else "#007AFF" - val amountText = if (trade.isBuy) "-" else "${String.format("%,d", trade.profitAmount)}์›" + // ๐Ÿ’ก ๋‚ด๋ถ€ ๋Œ€์‹œ๋ณด๋“œ ํ†ต๊ณ„์šฉ ๋ฐ์ดํ„ฐ ํด๋ž˜์Šค + private data class DashboardStats( + val buyOrderCount: Int, // ๊ธˆ์ผ ์ด ๋งค์ˆ˜ ์ฃผ๋ฌธ ํšŸ์ˆ˜ + val sellOrderCount: Int, // ๊ธˆ์ผ ์ด ๋งค๋„ ์ฃผ๋ฌธ ํšŸ์ˆ˜ + val completedCycles: Int, // ์ง„์ž… ํ›„ ์ฒญ์‚ฐ ์™„๋ฃŒ ๊ฑด์ˆ˜ + val pendingCycles: Int, // ์ง„์ž… ํ›„ ๋ฏธ์™„๋ฃŒ(๋ณด์œ ) ๊ฑด์ˆ˜ + val totalRealizedProfit: Long, // ์ด ์‹คํ˜„ ์ˆ˜์ต๊ธˆ + val avgRealizedProfit: Long, // ๊ฑฐ๋ž˜๋‹น ํ‰๊ท  ์ˆ˜์ต๊ธˆ + val avgRealizedRate: Double, // ๊ฑฐ๋ž˜๋‹น ํ‰๊ท  ์ˆ˜์ต๋ฅ  + val winRate: Double, // ์Šน๋ฅ  + val bestTradeName: String, + val bestTradeProfit: Long + ) - // ์ƒํƒœ/์‹œ๊ฐ„ ํ‘œ์‹œ ์ฒ˜๋ฆฌ - val timeStatusHtml = if (trade.timeTaken.contains("๋ฏธ์™„๋ฃŒ")) { - "${trade.timeTaken}" - } else { - "${trade.timeTaken}" + var lastSavedTime = -1L + val reportOpenTime = 60 * 1000 * 60 + fun generateAndOpenAsync( + summary: RawSummaryData, + rawHoldings: List, + rawTrades: List + ) { + reportScope.launch { + try { + // 1. [ํ•ต์‹ฌ] ๋Œ€์‹œ๋ณด๋“œ ํ†ต๊ณ„ ์ง€ํ‘œ ์ถ”์ถœ (Generator๊ฐ€ ์ง์ ‘ ๊ณ„์‚ฐ) + val stats = calculateDashboardStats(rawHoldings, rawTrades) + + // 2. ํƒญ 2 & 3 HTML ๊ฐ€๊ณต + val holdingsHtml = processHoldings(rawHoldings) + val tradesHtml = processTrades(rawTrades) + + // 3. ์ „์ฒด HTML ์กฐ๋ฆฝ + val htmlContent = buildHtml(summary, stats, holdingsHtml, tradesHtml) + var currentTime = System.currentTimeMillis() + if (summary.type.equals("END", true) || currentTime >= (lastSavedTime + reportOpenTime)) { + saveAndOpen(summary.type, htmlContent) + lastSavedTime = currentTime } + } catch (e: Exception) { + println("โŒ [Report] ๋ฆฌํฌํŠธ ๋น„๋™๊ธฐ ์ƒ์„ฑ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: ${e.message}") + e.printStackTrace() + } + } + } - val gradeHtml = trade.investmentGrade?.let { "$it" } ?: "" + // --- [์ƒˆ๋กœ์šด ํ†ต๊ณ„ ๊ณ„์‚ฐ ๋กœ์ง] --- + private fun calculateDashboardStats(holdings: List, trades: List): DashboardStats { + val tradesByStock = trades.groupBy { it.stockCode } - """ - - $typeBadge - ${trade.stockName} $gradeHtml - $orderTimeParsed - $timeStatusHtml - ${String.format("%,d", trade.execQty)}์ฃผ - ${String.format("%,d", trade.avgPrice)}์› - $rateText - $amountText - - - -
- ๐Ÿ’ก - AI (${String.format("%.1f", trade.aiScore ?: 0.0)}์ ): ${trade.reason.replace("\n", " ")} -
- - - """.trimIndent() + var buyCount = 0 + var sellCount = 0 + var completed = 0 + var pending = 0 + var totalProfit = 0L + var totalRateSum = 0.0 + var closedTradeCount = 0 + + var bestName = "-" + var bestProfit = Long.MIN_VALUE + + tradesByStock.forEach { (_, stockTrades) -> + val buys = stockTrades.filter { it.isBuy } + val sells = stockTrades.filter { !it.isBuy } + + buyCount += buys.size + sellCount += sells.size + + // 1. ์ฒญ์‚ฐ ์™„๋ฃŒ ์—ฌ๋ถ€ ํŒ๋ณ„ + if (buys.isNotEmpty() && sells.isNotEmpty()) { + completed++ + val avgBuy = calculateAvgPrice(buys) + val avgSell = calculateAvgPrice(sells) + val totalQty = sells.flatMap { it.executions }.sumOf { it.quantity } + + val profit = ((avgSell - avgBuy) * totalQty).toLong() + val rate = if (avgBuy > 0) ((avgSell - avgBuy) / avgBuy) * 100 else 0.0 + + totalProfit += profit + totalRateSum += rate + closedTradeCount++ + + if (profit > bestProfit) { bestProfit = profit; bestName = stockTrades.first().stockName } + } + else if (buys.isNotEmpty() && sells.isEmpty()) { + pending++ // ์ง„์ž…์€ ํ–ˆ์œผ๋‚˜ ์•„์ง ์•ˆ ํŒ ์ข…๋ชฉ + } + else if (sells.isNotEmpty() && buys.isEmpty()) { + // ์Šค์œ™ ์ข…๋ชฉ ๋งค๋„ ์ฒ˜๋ฆฌ + val avgSell = calculateAvgPrice(sells) + val totalQty = sells.flatMap { it.executions }.sumOf { it.quantity } + val buyPrice = sells.first().resolvedBuyPrice + + if (buyPrice > 0) { + val profit = ((avgSell - buyPrice) * totalQty).toLong() + val rate = ((avgSell - buyPrice) / buyPrice) * 100 + totalProfit += profit + totalRateSum += rate + closedTradeCount++ + if (profit > bestProfit) { bestProfit = profit; bestName = stockTrades.first().stockName } + } } } - // 2. ์ „์ฒด HTML ํ…œํ”Œ๋ฆฟ - val htmlTemplate = """ - - - - - ATRADE ์ผ๊ฐ„ ๋ฆฌํฌํŠธ - $today - - - -
-
-

ATRADE ์ผ๊ฐ„ ๋งค๋งค ์ƒ์„ธ ๋ฆฌํฌํŠธ

-
-
-

์˜ค๋Š˜์˜ ์‹คํ˜„ ์†์ต (END - START)

-

$profitSign${String.format("%,d", profitAmount)}์› ($profitSign${String.format("%.2f", profitRate)}%)

-
-
-

๐Ÿ“Š ๊ธˆ์ผ ์ฃผ๋ฌธ ๋ฐ ์ฒด๊ฒฐ ๋‚ด์—ญ ์ „์ฒด

- - - - - - - - - - - - - - - $tradeRowsHtml - -
๊ตฌ๋ถ„์ข…๋ชฉ๋ช…์ฃผ๋ฌธ์‹œ๊ฐ์†Œ์š”์‹œ๊ฐ„/์ƒํƒœ์ฒด๊ฒฐ๋Ÿ‰ํ‰๊ท ๋‹จ๊ฐ€์ˆ˜์ต๋ฅ ์ˆ˜์ต๊ธˆ
-
-
- - - """.trimIndent() + return DashboardStats( + buyOrderCount = buyCount, + sellOrderCount = sellCount, + completedCycles = completed, + pendingCycles = pending, + totalRealizedProfit = totalProfit, + avgRealizedProfit = if (closedTradeCount > 0) totalProfit / closedTradeCount else 0L, + avgRealizedRate = if (closedTradeCount > 0) totalRateSum / closedTradeCount else 0.0, + winRate = if (closedTradeCount > 0) (tradesByStock.filter { /* ์Šน๋ฆฌํŒ๋ณ„๋กœ์ง */ true }.size.toDouble()) /* ์‹ค์ œ ์Šน๋ฅ  ๊ณ„์‚ฐ ํ•„์š” ์‹œ ์ถ”๊ฐ€ */ else 0.0, + bestTradeName = bestName, + bestTradeProfit = if (bestProfit == Long.MIN_VALUE) 0L else bestProfit + ) + } - val directory = File("reports") - if (!directory.exists()) directory.mkdirs() - val reportFile = File(directory, "ATRADE_Report_$today.html") + // --- [ํƒญ 2, 3 ์ฒ˜๋ฆฌ ๋กœ์ง] (์ด์ „๊ณผ ๊ฑฐ์˜ ๋™์ผ) --- + private fun processHoldings(rawHoldings: List): String { + if (rawHoldings.isEmpty()) return "ํ˜„์žฌ ๋ณด์œ  ์ค‘์ธ ์ข…๋ชฉ์ด ์—†์Šต๋‹ˆ๋‹ค." - try { - reportFile.writeText(htmlTemplate, Charsets.UTF_8) - if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) { - Desktop.getDesktop().browse(reportFile.toURI()) + return rawHoldings.joinToString("\n") { h -> + val valuationProfit = ((h.currentPrice - h.avgPrice) * h.quantity).toLong() + val profitRate = if (h.avgPrice > 0) ((h.currentPrice - h.avgPrice) / h.avgPrice) * 100 else 0.0 + val profitClass = if (valuationProfit > 0) "plus" else if (valuationProfit < 0) "minus" else "" + + // ๐Ÿ’ก 4๊ฐœ ๊ฐ’์ด ๋ชจ๋‘ 0๋ณด๋‹ค ํด ๋•Œ๋งŒ HTML ๋ฌธ์ž์—ด ์ƒ์„ฑ + val ohlcHtml = if (h.openPrice > 0 && h.highPrice > 0 && h.lowPrice > 0 && h.currentPrice > 0) { + """ + + ์‹œ: ${String.format("%,d", h.openPrice.toLong())}
+ ๊ณ : ${String.format("%,d", h.highPrice.toLong())}
+ ์ €: ${String.format("%,d", h.lowPrice.toLong())}
+ ์ข…: ${String.format("%,d", h.currentPrice.toLong())} +
+ """.trimIndent() + } else { + "-" // ์‹œ์„ธ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์„ ๊ฒฝ์šฐ ํ‘œ์‹œํ•  ํ…์ŠคํŠธ (๋˜๋Š” "") } - } catch (e: Exception) { - e.printStackTrace() + + """ + + ${h.stockName} + ${h.quantity}์ฃผ + ${String.format("%,d", h.avgPrice.toLong())}์› + ${String.format("%,d", h.currentPrice.toLong())}์› + ${String.format("%,d", h.evalAmount)}์› + ${String.format("%.2f%%", profitRate)} + ${String.format("%,d", valuationProfit)}์› + $ohlcHtml + + """ + } + } + + private fun processTrades(rawTrades: List): String { + if (rawTrades.isEmpty()) return "๋‹น์ผ ๊ฑฐ๋ž˜ ๋‚ด์—ญ์ด ์—†์Šต๋‹ˆ๋‹ค." + val htmlBuilder = StringBuilder() + val tradesByStock = rawTrades.groupBy { it.stockCode } + + for ((_, trades) in tradesByStock) { + val buyTrades = trades.filter { it.isBuy } + val sellTrades = trades.filter { !it.isBuy } + + val stockName = trades.first().stockName + val reason = trades.first().reason + val aiScore = trades.first().aiScore + val investmentGrade = trades.first().investmentGrade + val h = trades.first() + val ohlcHtml = if (h.openPrice > 0 && h.highPrice > 0 && h.lowPrice > 0 && h.currentPrice > 0) { + """ + + ์‹œ: ${String.format("%,d", h.openPrice.toLong())}
+ ๊ณ : ${String.format("%,d", h.highPrice.toLong())}
+ ์ €: ${String.format("%,d", h.lowPrice.toLong())}
+ ์ข…: ${String.format("%,d", h.currentPrice.toLong())} +
+ """.trimIndent() + } else { + "-" // ์‹œ์„ธ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์„ ๊ฒฝ์šฐ ํ‘œ์‹œํ•  ํ…์ŠคํŠธ (๋˜๋Š” "") + } + + if (buyTrades.isNotEmpty() && sellTrades.isNotEmpty()) { + val totalSellQty = sellTrades.flatMap { it.executions }.sumOf { it.quantity } + val avgBuyPrice = calculateAvgPrice(buyTrades) + val avgSellPrice = calculateAvgPrice(sellTrades) + val profitAmount = ((avgSellPrice - avgBuyPrice) * totalSellQty).toLong() + val profitRate = if (avgBuyPrice > 0) ((avgSellPrice - avgBuyPrice) / avgBuyPrice) * 100 else 0.0 + val profitClass = if (profitAmount > 0) "plus" else if (profitAmount < 0) "minus" else "" + val buyTime = buyTrades.first().orderTime.substringAfter("T").substringBefore(".") + val sellTime = sellTrades.last().orderTime.substringAfter("T").substringBefore(".") + + htmlBuilder.append(""" + + ๋‹น์ผ ์ฒญ์‚ฐ + $stockName
$investmentGrade + ์ง„์ž…: $buyTime
์ฒญ์‚ฐ: $sellTime + - + ${totalSellQty}์ฃผ + ๋งค์ˆ˜: ${String.format("%,d", avgBuyPrice.toLong())}์›
๋งค๋„: ${String.format("%,d", avgSellPrice.toLong())}์› + ${String.format("%.2f%%", profitRate)} + ์‹คํ˜„: ${String.format("%,d", profitAmount)}์› + $ohlcHtml + + ๐Ÿ’ก AI (${String.format("%.1f", aiScore)}์ ): $reason + """) + } else if (sellTrades.isNotEmpty() && buyTrades.isEmpty()) { + sellTrades.forEach { sell -> + val sellQty = sell.executions.sumOf { it.quantity } + val avgSellPrice = calculateAvgPrice(listOf(sell)) + val buyPrice = sell.resolvedBuyPrice + val profitAmount = if (buyPrice > 0) ((avgSellPrice - buyPrice) * sellQty).toLong() else 0L + val profitRate = if (buyPrice > 0) ((avgSellPrice - buyPrice) / buyPrice) * 100 else 0.0 + val profitClass = if (profitAmount > 0) "plus" else if (profitAmount < 0) "minus" else "" + + htmlBuilder.append(""" + + ์Šค์œ™ ์ฒญ์‚ฐ + $stockName + ${sell.orderTime.substringAfter("T").substringBefore(".")} + - + ${sellQty}์ฃผ + ๋งค๋„๋‹จ๊ฐ€: ${String.format("%,d", avgSellPrice.toLong())}์› + ${String.format("%.2f%%", profitRate)} + ์‹คํ˜„: ${String.format("%,d", profitAmount)}์› + $ohlcHtml + + """) + } + } else if (buyTrades.isNotEmpty() && sellTrades.isEmpty()) { + buyTrades.forEach { buy -> + val buyQty = buy.executions.sumOf { it.quantity } + val avgBuyPrice = calculateAvgPrice(listOf(buy)) + val currentPrice = buy.currentPrice + val valuationAmount = if (avgBuyPrice > 0) ((currentPrice - avgBuyPrice) * buyQty).toLong() else 0L + val profitClass = if (valuationAmount > 0) "plus" else if (valuationAmount < 0) "minus" else "" + + htmlBuilder.append(""" + + ์‹ ๊ทœ ์ง„์ž… + $stockName
$investmentGrade + ${buy.orderTime.substringAfter("T").substringBefore(".")} + - + ${buyQty}์ฃผ + ๋งค์ˆ˜๋‹จ๊ฐ€: ${String.format("%,d", avgBuyPrice.toLong())}์› + - + ํ‰๊ฐ€: ${String.format("%,d", valuationAmount)}์› + $ohlcHtml + + ๐Ÿ’ก AI (${String.format("%.1f", aiScore)}์ ): $reason + """) + } + } + } + return htmlBuilder.toString() + } + + private fun calculateAvgPrice(trades: List): Double { + val allExecutions = trades.flatMap { it.executions } + val totalQty = allExecutions.sumOf { it.quantity } + return if (totalQty > 0) allExecutions.sumOf { it.price * it.quantity } / totalQty else trades.firstOrNull()?.currentPrice ?: 0.0 + } + + // --- [HTML ๋นŒ๋”] --- + private fun buildHtml(summary: RawSummaryData, stats: DashboardStats, holdingsHtml: String, tradesHtml: String): String { + val now = LocalDateTime.now() + val dateStr = now.format(DateTimeFormatter.ofPattern("yyyyMMdd")) + val timeStr = now.format(DateTimeFormatter.ofPattern("HHmmss")) + + return """ + + + + + ATRADE ๋ฐ์ผ๋ฆฌ ๋ฆฌํฌํŠธ (${summary.type}) + + + +

๐Ÿ“Š ATRADE ์ผ์ผ ์šด์šฉ ๋ฆฌํฌํŠธ (${summary.type} : $dateStr $timeStr)

+ +
+ + + +
+ +
+
+
+
์ตœ์ข… ์šด์šฉ ์ž์‚ฐ
+
${String.format("%,d", summary.endAsset)} ์›
+
+
+ ์˜ค๋Š˜ ์ˆ˜์ต: ${if (summary.dailyAssetChange > 0) "+" else ""}${String.format("%,d", summary.dailyAssetChange)} ์› +
+
+ +
+
+

๐Ÿ”„ ๊ธˆ์ผ ์ฃผ๋ฌธ ํ™œ๋™

+

๋งค์ˆ˜ ${stats.buyOrderCount}ํšŒ / ๋งค๋„ ${stats.sellOrderCount}ํšŒ

+
+
+

๐ŸŽฏ ์ฒญ์‚ฐ ์ƒํƒœ (์ง„์ž… ๊ธฐ์ค€)

+

์™„๋ฃŒ ${stats.completedCycles}๊ฑด / ๋ฏธ์™„๋ฃŒ ${stats.pendingCycles}๊ฑด

+
+
+

๐Ÿ’ธ ๊ฑฐ๋ž˜๋‹น ํ‰๊ท  ์ˆ˜์ต

+

+ ${String.format("%,d", stats.avgRealizedProfit)} ์› (${String.format("%.2f", stats.avgRealizedRate)}%) +

+
+
+ +
+
+

๐Ÿ’ฐ ์ด ์‹คํ˜„ ์†์ต (ํ™•์ •)

+

+ ${if (stats.totalRealizedProfit > 0) "+" else ""}${String.format("%,d", stats.totalRealizedProfit)} ์› +

+
+
+

๐Ÿ† ์˜ค๋Š˜์˜ BEST

+

${stats.bestTradeName} (+${String.format("%,d", stats.bestTradeProfit)})

+
+
+

๐Ÿฆ ๋ฐœ์ƒ ์ œ๋น„์šฉ

+

${String.format("%,d", summary.todayFees)} ์›

+
+
+
+ +
+ + + $holdingsHtml +
์ข…๋ชฉ๋ช…๋ณด์œ ์ˆ˜๋Ÿ‰๋งค์ž…๋‹จ๊ฐ€ํ˜„์žฌ๊ฐ€ํ‰๊ฐ€๊ธˆ์•ก์ˆ˜์ต๋ฅ ํ‰๊ฐ€์†์ต
OHLC
+
+ +
+ + + $tradesHtml +
๊ตฌ๋ถ„์ข…๋ชฉ๋ช…์ฒด๊ฒฐ์‹œ๊ฐ์†Œ์š”์‹œ๊ฐ„/์ƒํƒœ์ฒด๊ฒฐ๋Ÿ‰์ฒด๊ฒฐ๋‹จ๊ฐ€์ˆ˜์ต๋ฅ ์ˆ˜์ต๊ธˆ(์‹คํ˜„/ํ‰๊ฐ€)OHLC
+
+ + + + + """.trimIndent() + } + + private fun saveAndOpen(type: String, htmlContent: String) { + val now = LocalDateTime.now() + val year = now.year.toString() + val month = String.format("%02d", now.monthValue) + val dateStr = now.format(DateTimeFormatter.ofPattern("yyyyMMdd")) + val timeStr = now.format(DateTimeFormatter.ofPattern("HHmmss")) + + val directory = File("reports/$year/$month") + if (!directory.exists()) directory.mkdirs() + + val fileName = "ATRADE_REPORT_${dateStr}_${timeStr}_${type}.html" + val reportFile = File(directory, fileName) + + reportFile.writeText(htmlContent, Charsets.UTF_8) + if (Desktop.isDesktopSupported()) { + Desktop.getDesktop().browse(reportFile.toURI()) } } } \ No newline at end of file diff --git a/src/main/kotlin/report/TradingReportManager.kt b/src/main/kotlin/report/TradingReportManager.kt index 9970f71..7d87826 100644 --- a/src/main/kotlin/report/TradingReportManager.kt +++ b/src/main/kotlin/report/TradingReportManager.kt @@ -1,6 +1,13 @@ package report import io.ktor.utils.io.core.String +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import kotlinx.serialization.Serializable import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json @@ -8,11 +15,12 @@ import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.transactions.transaction import report.database.* import model.* +import network.KisTradeService import service.InvestmentGrade import java.time.Duration import java.time.LocalDate import java.time.LocalDateTime - +import org.jetbrains.exposed.dao.id.IntIdTable enum class SnapshotType { START, END, MIDDLE } @@ -50,6 +58,182 @@ object TradingReportManager : TradingReportService { // Key: ์ข…๋ชฉ์ฝ”๋“œ, Value: ํ˜„์žฌ ํ™œ์„ฑํ™”๋œ Position ID private val activePositions = mutableMapOf() + override fun recordAssetSnapshot(type: SnapshotType, balance: UnifiedBalance, remark: String?) { + CoroutineScope(Dispatchers.IO).launch { + val todayDate = LocalDate.now().toString() + + // 1. ์ค‘๋ณต ์—†๋Š” ์ „์ฒด ์ข…๋ชฉ ์ฝ”๋“œ ๋ฆฌ์ŠคํŠธ ์ถ”์ถœ + val allCodes = mutableSetOf() + val holdings = balance.getHoldings() + allCodes.addAll(holdings.map { it.code }) + + // ๊ฑฐ๋ž˜ ๋‚ด์—ญ ํ…Œ์ด๋ธ”์—์„œ ๋‹น์ผ ๊ฑฐ๋ž˜๋œ ์ข…๋ชฉ ์ฝ”๋“œ ์ถ”๊ฐ€ + val tradedCodes = transaction(DatabaseFactory.reportDb) { + TradeHistoryTable.select { TradeHistoryTable.orderTime like "$todayDate%" } + .map { it[TradeHistoryTable.stockCode] } + } + allCodes.addAll(tradedCodes) + + // 2. [ํ•ต์‹ฌ] ์‹œ์„ธ ๋ฐ์ดํ„ฐ ์ผ๊ด„ ๋ณ‘๋ ฌ ์กฐํšŒ (Cache ๊ตฌ์„ฑ) + val priceCache = mutableMapOf() + if (type == SnapshotType.END) { + allCodes.chunked(10).forEach { batch -> // API ๋ถ€ํ•˜ ์กฐ์ ˆ์„ ์œ„ํ•ด 10๊ฐœ์”ฉ ๋ฌถ์Œ ์ฒ˜๋ฆฌ ๊ฐ€๋Šฅ + batch.map { code -> + async { + code to KisTradeService.fetchPeriodChartData(code, "D", true).getOrNull()?.lastOrNull() + } + }.awaitAll().forEach { (code, data) -> + priceCache[code] = data + } + delay(200) // API ์ดˆ๋‹น ํ˜ธ์ถœ ์ œํ•œ(TPS) ์ค€์ˆ˜ + } + } + transaction(DatabaseFactory.reportDb) { + // 1. ์ž์‚ฐ ์Šค๋ƒ…์ƒท ์ €์žฅ + val snapshotId = AssetSnapshotTable.insertAndGetId { + it[AssetSnapshotTable.date] = todayDate + it[AssetSnapshotTable.snapshotType] = type.name + + // ๊ธฐ์กด ๋ฐ์ดํ„ฐ ์œ ์ง€ ๋ถ€๋ถ„ + it[AssetSnapshotTable.totalAsset] = balance.totalAsset.replace(",", "").toLongOrNull() ?: 0L + it[AssetSnapshotTable.totalProfitRate] = + balance.totalProfitRate.replace("%", "").toDoubleOrNull() ?: 0.0 + it[AssetSnapshotTable.deposit] = + balance.deposit.replace(",", "").toLongOrNull() ?: 0L // ๐Ÿ‘ˆ [์ถ”๊ฐ€] ์—๋Ÿฌ ์›์ธ ํ•ด๊ฒฐ! + + // ์‹ ๊ทœ ๋ฆฌํฌํŠธ์šฉ ๋ฐ์ดํ„ฐ ๋ถ€๋ถ„ + it[AssetSnapshotTable.dailyAssetChange] = + balance.dailyAssetChange.replace(",", "").toLongOrNull() ?: 0L + it[AssetSnapshotTable.todayFees] = balance.todayFees.replace(",", "").toLongOrNull() ?: 0L + +// it[AssetSnapshotTable.appliedConfigJson] = "{}" // ๐Ÿ‘ˆ [์ถ”๊ฐ€] ์Šคํ‚ค๋งˆ์ƒ ํ•„์ˆ˜๋กœ ์ง€์ •๋˜์–ด ์žˆ์–ด ๋นˆ ๊ฐ’์ด๋ผ๋„ ๋„ฃ์–ด์ค๋‹ˆ๋‹ค. + it[AssetSnapshotTable.remark] = remark + }.value + + // 2. ๋ณด์œ  ์ข…๋ชฉ ์Šค๋ƒ…์ƒท ์ €์žฅ + if (balance.getHoldings().isNotEmpty()) { + SnapshotHoldingsTable.batchInsert(balance.getHoldings()) { stock -> + this[SnapshotHoldingsTable.snapshotId] = snapshotId + this[SnapshotHoldingsTable.code] = stock.code + this[SnapshotHoldingsTable.name] = stock.name + this[SnapshotHoldingsTable.quantity] = stock.quantity.toIntOrNull() ?: 0 + this[SnapshotHoldingsTable.avgPrice] = stock.avgPrice.toDoubleOrNull() ?: 0.0 + this[SnapshotHoldingsTable.currentPrice] = stock.currentPrice.toDoubleOrNull() ?: 0.0 + this[SnapshotHoldingsTable.profitRate] = stock.profitRate.toDoubleOrNull() ?: 0.0 + this[SnapshotHoldingsTable.evalAmount] = stock.evalAmount.replace(",", "").toLongOrNull() ?: 0L + this[SnapshotHoldingsTable.isDomestic] = stock.isDomestic + this[SnapshotHoldingsTable.isTodayEntry] = stock.isTodayEntry + } + } + + val startAsset = AssetSnapshotTable.select { + (AssetSnapshotTable.date eq todayDate) and (AssetSnapshotTable.snapshotType eq SnapshotType.START.name) + }.orderBy(AssetSnapshotTable.id to SortOrder.ASC).limit(1).singleOrNull() + ?.get(AssetSnapshotTable.totalAsset) + ?: balance.totalAsset.replace(",", "").toLongOrNull() ?: 0L + + // 3. ๐Ÿš€ ์š”์•ฝ ๋ฐ์ดํ„ฐ Raw ํŒจํ‚ค์ง• + val summaryData = LocalReportGenerator.RawSummaryData( + type = type.name, + startAsset = startAsset, + endAsset = balance.totalAsset.replace(",", "").toLongOrNull() ?: 0L, + dailyAssetChange = balance.dailyAssetChange.replace(",", "").toLongOrNull() ?: 0L, + todayFees = balance.todayFees.replace(",", "").toLongOrNull() ?: 0L, + totalProfitRate = balance.totalProfitRate.replace("%", "").toDoubleOrNull() ?: 0.0 + ) + + // 4. ๐Ÿš€ ๋ณด์œ  ์ž”๊ณ  ๋ฐ์ดํ„ฐ Raw ํŒจํ‚ค์ง• (์ˆ˜์ต๋ฅ  ๊ณ„์‚ฐ์€ Generator์— ์œ„์ž„) + val holdingLogs = balance.getHoldings().map { stock -> + var o = 0.0; + var h = 0.0; + var l = 0.0 + var c = 0.0 + + // ๐Ÿ’ก ์žฅ ๋งˆ๊ฐ ๋ฆฌํฌํŠธ์ผ ๋•Œ๋งŒ ์‹œ์„ธ API ํ˜ธ์ถœ + if (type == SnapshotType.END) { + runBlocking { + priceCache[stock.code]?.let { + o = it.stck_oprc.toDoubleOrNull() ?: 0.0 + h = it.stck_hgpr.toDoubleOrNull() ?: 0.0 + l = it.stck_lwpr.toDoubleOrNull() ?: 0.0 + c = it.stck_prpr.toDoubleOrNull() ?: 0.0 + } + } + } + + LocalReportGenerator.RawHoldingData( + stockName = stock.name, + quantity = stock.quantity.toIntOrNull() ?: 0, + avgPrice = stock.avgPrice.toDoubleOrNull() ?: 0.0, + currentPrice = stock.currentPrice.toDoubleOrNull() ?: 0.0, + evalAmount = stock.evalAmount.replace(",", "").toLongOrNull() ?: 0L, + openPrice = o, highPrice = h, lowPrice = l, closePrice = c, + ) + } + + // 5. ๐Ÿš€ ๋‹น์ผ ๊ฑฐ๋ž˜ ๋‚ด์—ญ Raw ํŒจํ‚ค์ง• + val tradeLogs = TradeHistoryTable.select { + TradeHistoryTable.orderTime like "$todayDate%" + }.orderBy(TradeHistoryTable.orderTime to SortOrder.ASC).map { row -> + val code = row[TradeHistoryTable.stockCode] + var o = 0.0; + var h = 0.0; + var l = 0.0; + var c = 0.0 + + if (type == SnapshotType.END) { + runBlocking { + priceCache[code]?.let { + o = it.stck_oprc.toDoubleOrNull() ?: 0.0 + h = it.stck_hgpr.toDoubleOrNull() ?: 0.0 + l = it.stck_lwpr.toDoubleOrNull() ?: 0.0 + c = it.stck_prpr.toDoubleOrNull() ?: 0.0 + } + } + } + // ์ฃผ์˜: ์‹œ๊ฐ„์ˆœ(ASC)์œผ๋กœ ์ •๋ ฌํ•ด์„œ ๋„˜๊ฒจ์•ผ ์ œ๋„ˆ๋ ˆ์ดํ„ฐ๊ฐ€ ์‹œ๊ฐ„ ํ๋ฆ„๋Œ€๋กœ ๋ฌถ๊ธฐ ํŽธํ•ฉ๋‹ˆ๋‹ค. + + val tradeId = row[TradeHistoryTable.id].value + + val rawExecutions = + ExecutionDetailsTable.select { ExecutionDetailsTable.tradeId eq tradeId }.map { exec -> + LocalReportGenerator.RawExecutionData( + price = exec[ExecutionDetailsTable.price], + quantity = exec[ExecutionDetailsTable.quantity], + execTime = exec[ExecutionDetailsTable.execTime] + ) + } + + // ๋งค๋„์ผ ๊ฒฝ์šฐ์—๋งŒ ๋งค์ˆ˜ ๋‹จ๊ฐ€ ์ถ”์  (DB ์กฐํšŒ๊ฐ€ ํ•„์š”ํ•˜๋ฏ€๋กœ ์ด ์ž‘์—…๋งŒ ๋งค๋‹ˆ์ €๊ฐ€ ์ˆ˜ํ–‰) + val isBuy = row[TradeHistoryTable.isBuy] + val resolvedBuyPrice = + if (!isBuy) findBuyPrice(row[TradeHistoryTable.stockCode], tradeId, todayDate) else 0.0 + + LocalReportGenerator.RawTradeData( + stockCode = row[TradeHistoryTable.stockCode], // ๐Ÿ’ก [์ถ”๊ฐ€] ์ œ๋„ˆ๋ ˆ์ดํ„ฐ๊ฐ€ ์ข…๋ชฉ๋ณ„๋กœ ๋ฌถ์„ ์ˆ˜ ์žˆ๋„๋ก ์ฝ”๋“œ๊ฐ’ ์ „๋‹ฌ + stockName = row[TradeHistoryTable.stockName], + isBuy = isBuy, + status = row[TradeHistoryTable.status], + orderTime = row[TradeHistoryTable.orderTime], + executions = rawExecutions, + currentPrice = row[TradeHistoryTable.currentPrice] ?: 0.0, + resolvedBuyPrice = resolvedBuyPrice, + investmentGrade = row[TradeHistoryTable.investmentGrade] ?: "-", + reason = row[TradeHistoryTable.reason] ?: "", + aiScore = row[TradeHistoryTable.aiScore] ?: 0.0, + openPrice = o, + highPrice = h, + lowPrice = l, + closePrice = c, + ) + } + + // 6. ์ฝ”๋ฃจํ‹ด ๊ธฐ๋ฐ˜ ์ œ๋„ˆ๋ ˆ์ดํ„ฐ ํ˜ธ์ถœ + LocalReportGenerator.generateAndOpenAsync(summaryData, holdingLogs, tradeLogs) + } + } + } + override fun recordConfigChange() { transaction(DatabaseFactory.reportDb) { val now = LocalDateTime.now() @@ -108,98 +292,72 @@ object TradingReportManager : TradingReportService { } } - /** - * [1] ์ž์‚ฐ ํ˜„ํ™ฉ ๋ฐ ๋ณด์œ  ์ข…๋ชฉ ์Šค๋ƒ…์ƒท ์ €์žฅ - * isClose ํŒŒ๋ผ๋ฏธํ„ฐ๊ฐ€ true๋กœ ๋“ค์–ด์˜ค๋ฉด ๋ชจ๋“  ๊ธฐ๋ก์„ ๋งˆ์น˜๊ณ  ์ž๋™์œผ๋กœ ์ผ๊ฐ„ ๋ฆฌํฌํŠธ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. - */ - override fun recordAssetSnapshot( - type: SnapshotType, - balance: UnifiedBalance, - remark: String? - ) { - transaction(DatabaseFactory.reportDb) { - val todayDate = LocalDate.now().toString() + // ========================================== + // 3. ๋‚ด๋ถ€ ์œ ํ‹ธ๋ฆฌํ‹ฐ ํ•จ์ˆ˜ + // ========================================== - // ๐Ÿ’ก [ํ•ต์‹ฌ ๋กœ์ง] ์Šค๋ƒ…์ƒท ํƒ€์ž… ์ž๋™ ๊ฒฐ์ • - val actualSnapshotType = if (type == SnapshotType.END) { - SnapshotType.END + private fun calculateTimeTaken(orderTimeStr: String, lastExecTimeStr: String?, status: String): String { + if (lastExecTimeStr == null) return "๋ฏธ์ฒด๊ฒฐ (์ง„ํ–‰์ค‘)" + return try { + val orderTime = LocalDateTime.parse(orderTimeStr) + val lastExecTime = LocalDateTime.parse(lastExecTimeStr) + val duration = java.time.Duration.between(orderTime, lastExecTime) + + val timeString = if (duration.toMinutes() > 0) { + "${duration.toMinutes()}๋ถ„ ${duration.seconds % 60}์ดˆ" } else { - // ์˜ค๋Š˜ ๋‚ ์งœ๋กœ ๊ธฐ๋ก๋œ START ์Šค๋ƒ…์ƒท์ด ์žˆ๋Š”์ง€ ๊ฒ€์‚ฌ - val hasStartToday = AssetSnapshotTable.select { - (AssetSnapshotTable.date eq todayDate) and - (AssetSnapshotTable.snapshotType eq SnapshotType.START.name) - }.limit(1).count() > 0 - - // ์—†์œผ๋ฉด START, ์ด๋ฏธ ์žˆ์œผ๋ฉด MIDDLE๋กœ ์ง€์ • - if (hasStartToday) SnapshotType.MIDDLE else SnapshotType.START - } - - // 1. ๋‹น์‹œ์˜ ์ฃผ์š” ์„ค์ •๊ฐ’ ๋ฐฑ์—… (๊ธฐ์กด ๋Œ€ํ‘œ๋‹˜ ์ฝ”๋“œ ์œ ์ง€) - var buffer = StringBuffer() - arrayOf( - InvestmentGrade.LEVEL_5_STRONG_RECOMMEND, InvestmentGrade.LEVEL_4_BALANCED_RECOMMEND, - InvestmentGrade.LEVEL_3_CAUTIOUS_RECOMMEND, InvestmentGrade.LEVEL_2_HIGH_RISK, InvestmentGrade.LEVEL_1_SPECULATIVE - ).forEach { grade -> - buffer.appendLine("${grade.name}") - .appendLine(" ๋งค์ˆ˜ ๋ชฉํ‘œ : -${KisSession.config.getValues(grade.buyGuide)}ํ˜ธ๊ฐ€, ๋ชฉํ‘œ ์ˆ˜์ต:${KisSession.config.getValues(ConfigIndex.TAX_INDEX) + (KisSession.config.getValues(ConfigIndex.PROFIT_INDEX) * KisSession.config.getValues(grade.profitGuide))}, ์ตœ๋Œ€ ํˆฌ์ž:${KisSession.config.getValues(ConfigIndex.MAX_BUDGET_INDEX) * KisSession.config.getValues(grade.allocationRate)}") - } - - val configJson = Json.encodeToString( - ConfigSnapshot( - targetProfit = KisSession.config.getValues(ConfigIndex.PROFIT_INDEX) ?: 0.0, - stopLoss = KisSession.config.getValues(ConfigIndex.LOSS_MAXRATE) ?: 0.0, - maxBudget = KisSession.config.getValues(ConfigIndex.MAX_BUDGET_INDEX) ?: 0.0, - guideScore = KisSession.config.getValues(ConfigIndex.MIN_PURCHASE_SCORE_INDEX) ?: 0.0, - descMinMax = "์ข…๋ชฉ ๊ธˆ์•ก ํ•„ํ„ฐ MIN:${KisSession.config.getValues(ConfigIndex.MIN_PRICE_INDEX) ?: 0.0} ~ MAX:${KisSession.config.getValues(ConfigIndex.MAX_PRICE_INDEX) ?: 0.0}", - grades = buffer.toString() - ) - ) - - // 2. ์ž์‚ฐ ๋งˆ์Šคํ„ฐ ๋ฐ์ดํ„ฐ ์ €์žฅ (์‹ค์ œ ๊ฒฐ์ •๋œ actualSnapshotType ์‚ฌ์šฉ) - val snapshotId = AssetSnapshotTable.insert { - it[date] = todayDate - it[snapshotType] = actualSnapshotType.name - it[totalAsset] = balance.totalAsset.toLongOrNull() ?: 0L - it[totalProfitRate] = balance.totalProfitRate.toDoubleOrNull() ?: 0.0 - it[deposit] = balance.deposit.toLongOrNull() ?: 0L - it[appliedConfigJson] = configJson - it[this.remark] = remark ?: if (actualSnapshotType == SnapshotType.MIDDLE) "์žฅ์ค‘ ์ƒํƒœ ๊ธฐ๋ก" else "" - }[AssetSnapshotTable.id] - - // 3. ๋ณด์œ  ์ข…๋ชฉ ๋ฆฌ์ŠคํŠธ ์ผ๊ด„ ์ €์žฅ (Batch Insert) - if (balance.getHolldings().isNotEmpty()) { - SnapshotHoldingsTable.batchInsert(balance.getHolldings()) { stock -> - this[SnapshotHoldingsTable.snapshotId] = snapshotId - this[SnapshotHoldingsTable.code] = stock.code - this[SnapshotHoldingsTable.name] = stock.name - - // ํฌ์ง€์…˜ ID ๋งคํ•‘: ๋ฉ”๋ชจ๋ฆฌ ๋งต์— ์—†์œผ๋ฉด ์‹ ๊ทœ ๋ฐœ๊ธ‰ - val posId = activePositions.getOrPut(stock.code) { - "${stock.code}_${System.currentTimeMillis()}" - } - this[SnapshotHoldingsTable.positionId] = posId - - this[SnapshotHoldingsTable.quantity] = stock.quantity.toIntOrNull() ?: 0 - this[SnapshotHoldingsTable.avgPrice] = stock.avgPrice.toDoubleOrNull() ?: 0.0 - this[SnapshotHoldingsTable.currentPrice] = stock.currentPrice.toDoubleOrNull() ?: 0.0 - this[SnapshotHoldingsTable.profitRate] = stock.profitRate.toDoubleOrNull() ?: 0.0 - this[SnapshotHoldingsTable.evalAmount] = stock.evalAmount.toLongOrNull() ?: 0L - this[SnapshotHoldingsTable.isDomestic] = stock.isDomestic - this[SnapshotHoldingsTable.isTodayEntry] = stock.isTodayEntry - } - } - - println("๐Ÿ“Š [Report] ${actualSnapshotType.name} ์ž์‚ฐ ์Šค๋ƒ…์ƒท ์ €์žฅ ์™„๋ฃŒ (๋ณด์œ ์ข…๋ชฉ: ${balance.getHolldings().size}๊ฐœ)") - - // ๐Ÿ’ก 4. ๋งˆ๊ฐ ํ”Œ๋ž˜๊ทธ: END์ผ ๋•Œ๋งŒ ๋ฆฌํฌํŠธ ์ƒ์„ฑ (MIDDLE์€ ๋ฐ์ดํ„ฐ๋งŒ ์ ์žฌํ•˜๊ณ  ํŒจ์Šค) - if (actualSnapshotType == SnapshotType.END) { - println("๐Ÿ”” [Report] ์žฅ ๋งˆ๊ฐ(END) ํ™•์ธ. ์ผ๊ฐ„ ๋งค๋งค ๋ฆฌํฌํŠธ ์ƒ์„ฑ์„ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค.") - generateDailyLocalReport() + "${duration.toMillis() / 1000.0}์ดˆ" } + if (status == "COMPLETED") "์™„๋ฃŒ ($timeString)" else "๋ฏธ์™„๋ฃŒ ($timeString)" + } catch (e: Exception) { + "๊ณ„์‚ฐ ๋ถˆ๊ฐ€" } } + private fun findBuyPrice(stockCode: String, currentTradeId: Int, todayDate: String): Double { + // 1์ฐจ ๋ฐฉ์–ด์„ : ๋‹น์ผ ์ฒด๊ฒฐ๋œ ๋งค์ˆ˜ ๋‚ด์—ญ ์ค‘ ํ™•์ •๋œ purchasePrice๊ฐ€ ์žˆ๋Š”์ง€ ํ™•์ธ + val recentBuyTrade = TradeHistoryTable.select { + (TradeHistoryTable.stockCode eq stockCode) and + (TradeHistoryTable.isBuy eq true) and + (TradeHistoryTable.id less currentTradeId) + }.orderBy(TradeHistoryTable.id to SortOrder.DESC).limit(1).singleOrNull() + + if (recentBuyTrade != null) { + val pPrice = recentBuyTrade[TradeHistoryTable.purchasePrice] + if (pPrice > 0.0) return pPrice // ๐Ÿ’ก ์ƒˆ๋กœ ์ถ”๊ฐ€๋œ ํ™•์ • ๋‹จ๊ฐ€ ์ตœ์šฐ์„  ์‚ฌ์šฉ + + // ๋งŒ์•ฝ ์—†๋‹ค๋ฉด ์ฒด๊ฒฐ ๋‚ด์—ญ์—์„œ ๊ณ„์‚ฐ (๊ธฐ์กด ๋ฐฉ์‹) + val buyExecutions = ExecutionDetailsTable.select { + ExecutionDetailsTable.tradeId eq recentBuyTrade[TradeHistoryTable.id] + }.toList() + val buyQty = buyExecutions.sumOf { it[ExecutionDetailsTable.quantity] } + if (buyQty > 0) { + return buyExecutions.sumOf { it[ExecutionDetailsTable.price] * it[ExecutionDetailsTable.quantity] } / buyQty + } + } + + // 2์ฐจ ๋ฐฉ์–ด์„ : ๋‹น์ผ ์•„์นจ START ์Šค๋ƒ…์ƒท + val startSnapshotId = AssetSnapshotTable.select { + (AssetSnapshotTable.date eq todayDate) and + (AssetSnapshotTable.snapshotType eq SnapshotType.START.name) + }.limit(1).singleOrNull()?.get(AssetSnapshotTable.id) + + if (startSnapshotId != null) { + val startAvgPrice = SnapshotHoldingsTable.select { + (SnapshotHoldingsTable.snapshotId eq startSnapshotId) and + (SnapshotHoldingsTable.code eq stockCode) + }.singleOrNull()?.get(SnapshotHoldingsTable.avgPrice) + + if (startAvgPrice != null && startAvgPrice > 0) return startAvgPrice + } + + // 3์ฐจ ๋ฐฉ์–ด์„ : ๋ ˆ๊ฑฐ์‹œ ๋ณด์œ  ํ‰๋‹จ๊ฐ€ + return TradeHistoryTable.select { TradeHistoryTable.id eq currentTradeId } + .singleOrNull()?.get(TradeHistoryTable.holdingAvgPrice) ?: 0.0 + } + + override fun recordTradeDecision(orderNo: String, stockCode: String, stockName: String, @@ -330,126 +488,126 @@ object TradingReportManager : TradingReportService { * [4] ์žฅ ๋งˆ๊ฐ ๋กœ์ปฌ ๋ฆฌํฌํŠธ(HTML) ์ž๋™ ์ƒ์„ฑ */ override fun generateDailyLocalReport() { - transaction(DatabaseFactory.reportDb) { - val today = LocalDate.now().toString() - - val startAsset = AssetSnapshotTable.select { - (AssetSnapshotTable.date eq today) and (AssetSnapshotTable.snapshotType eq SnapshotType.START.name) - }.orderBy(AssetSnapshotTable.id to SortOrder.ASC).limit(1).singleOrNull()?.get(AssetSnapshotTable.totalAsset) ?: 0L - - val endAsset = AssetSnapshotTable.select { - (AssetSnapshotTable.date eq today) and (AssetSnapshotTable.snapshotType eq SnapshotType.END.name) - }.orderBy(AssetSnapshotTable.id to SortOrder.DESC).limit(1).singleOrNull()?.get(AssetSnapshotTable.totalAsset) ?: startAsset - - // ๐Ÿ’ก [๋ณ€๊ฒฝ] isBuy == false ํ•„ํ„ฐ๋ฅผ ์ œ๊ฑฐํ•˜์—ฌ ๊ธˆ์ผ ๋ฐœ์ƒํ•œ '๋ชจ๋“ ' ์ฃผ๋ฌธ ๋‚ด์—ญ์„ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค. - val allTrades = TradeHistoryTable.select { - (TradeHistoryTable.orderTime like "$today%") - }.orderBy(TradeHistoryTable.orderTime to SortOrder.DESC).toList() - - val tradeLogs = allTrades.map { row -> - val tradeId = row[TradeHistoryTable.id] - val stockCode = row[TradeHistoryTable.stockCode] - val isBuy = row[TradeHistoryTable.isBuy] - val status = row[TradeHistoryTable.status] - val orderTimeStr = row[TradeHistoryTable.orderTime] // ์˜ˆ: "2024-05-20T09:10:00.123" - - // 1. ์ฒด๊ฒฐ ์ƒ์„ธ ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ - val executions = ExecutionDetailsTable.select { ExecutionDetailsTable.tradeId eq tradeId }.toList() - val execQty = executions.sumOf { it[ExecutionDetailsTable.quantity] } - - // VWAP ํ‰๊ท  ์ฒด๊ฒฐ๊ฐ€ ์‚ฐ์ถœ - val avgPrice = if (execQty > 0) { - executions.sumOf { it[ExecutionDetailsTable.price] * it[ExecutionDetailsTable.quantity] } / execQty - } else { - row[TradeHistoryTable.currentPrice] ?: 0.0 - } - - // ๐Ÿ’ก 2. ์ฃผ๋ฌธ๋ถ€ํ„ฐ ๋งˆ์ง€๋ง‰ ์ฒด๊ฒฐ๊นŒ์ง€ ์†Œ์š”๋œ ์‹œ๊ฐ„ ๊ณ„์‚ฐ ๋กœ์ง - val lastExecTimeStr = executions.maxByOrNull { it[ExecutionDetailsTable.execTime] }?.get(ExecutionDetailsTable.execTime) - - val timeTakenStr = if (lastExecTimeStr != null) { - try { - val orderTime = LocalDateTime.parse(orderTimeStr) - val lastExecTime = LocalDateTime.parse(lastExecTimeStr) - val duration = Duration.between(orderTime, lastExecTime) - - val timeString = if (duration.toMinutes() > 0) { - "${duration.toMinutes()}๋ถ„ ${duration.seconds % 60}์ดˆ" - } else { - "${duration.toMillis() / 1000.0}์ดˆ" - } - - // ์•„์ง ์ƒํƒœ๊ฐ€ COMPLETED๊ฐ€ ์•„๋‹ˆ๋ฉด ๋ฏธ์™„๋ฃŒ ํ‘œ์‹œ (ํ˜„์žฌ ์ƒํƒœ๊ฐ’์ด ORDERED, PARTIAL ๋“ฑ์ผ ๊ฒฝ์šฐ) - if (status == "COMPLETED") "์™„๋ฃŒ ($timeString)" else "๋ฏธ์™„๋ฃŒ ($timeString)" - } catch (e: Exception) { - "๊ณ„์‚ฐ ๋ถˆ๊ฐ€" - } - } else { - "๋ฏธ์ฒด๊ฒฐ (์ง„ํ–‰์ค‘)" - } - - // 3. ์ˆ˜์ต๋ฅ  ๋ฐ ์ˆ˜์ต๊ธˆ ๊ณ„์‚ฐ (๋งค๋„์ผ ๋•Œ๋งŒ) - var calculatedProfitRate = 0.0 - var calculatedProfitAmount = 0L - - if (!isBuy) { - // ๊ณผ๊ฑฐ ๋งค์ˆ˜ ๋‹จ๊ฐ€ ์ฐพ๊ธฐ (1์ˆœ์œ„: ์ด์ „ ๋งค์ˆ˜ ๊ธฐ๋ก, 2์ˆœ์œ„: ์•„์นจ ์Šค๋ƒ…์ƒท) - val recentBuyTrade = TradeHistoryTable.select { - (TradeHistoryTable.stockCode eq stockCode) and - (TradeHistoryTable.isBuy eq true) and - (TradeHistoryTable.id less tradeId) - }.orderBy(TradeHistoryTable.id to SortOrder.DESC).limit(1).singleOrNull() - - var avgBuyPrice = 0.0 - if (recentBuyTrade != null) { - val buyExecutions = ExecutionDetailsTable.select { ExecutionDetailsTable.tradeId eq recentBuyTrade[TradeHistoryTable.id] }.toList() - val buyQty = buyExecutions.sumOf { it[ExecutionDetailsTable.quantity] } - if (buyQty > 0) avgBuyPrice = buyExecutions.sumOf { it[ExecutionDetailsTable.price] * it[ExecutionDetailsTable.quantity] } / buyQty - } - - // 2์ฐจ ์‹œ๋„: ์˜ค๋Š˜ ์•„์นจ START ์Šค๋ƒ…์ƒท์— ๊ธฐ๋ก๋œ ๋ณด์œ  ํ‰๋‹จ๊ฐ€ - if (avgBuyPrice == 0.0) { - val startHoldings = SnapshotHoldingsTable.innerJoin(AssetSnapshotTable).select { - (SnapshotHoldingsTable.code eq stockCode) and - (AssetSnapshotTable.date eq today) and - (AssetSnapshotTable.snapshotType eq SnapshotType.START.name) - }.singleOrNull() - avgBuyPrice = startHoldings?.get(SnapshotHoldingsTable.avgPrice) ?: 0.0 - } - - // ๐Ÿ’ก 3์ฐจ ์‹œ๋„ (๊ถ๊ทน์˜ ๋ฐฉ์–ด๋ง‰): ๋ฆฌํฌํŒ… ๋„์ž… ์ด์ „๋ถ€ํ„ฐ ๋“ค๊ณ  ์žˆ๋˜ ๋ ˆ๊ฑฐ์‹œ ์ข…๋ชฉ์ผ ๊ฒฝ์šฐ - // ๋งค๋„ ์ฃผ๋ฌธ ๋‹น์‹œ์— DB์— ๋ฐฑ์—…ํ•ด๋‘์—ˆ๋˜ ์ฆ๊ถŒ์‚ฌ ๋ณด์œ  ํ‰๋‹จ๊ฐ€๋ฅผ ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. - if (avgBuyPrice == 0.0) { - avgBuyPrice = row[TradeHistoryTable.holdingAvgPrice] - } - - // --- [C] ์ตœ์ข… ์ˆ˜์ต๋ฅ  ๊ณ„์‚ฐ --- - if (avgBuyPrice > 0.0) { - calculatedProfitRate = ((avgPrice - avgBuyPrice) / avgBuyPrice) * 100 - calculatedProfitAmount = ((avgPrice - avgBuyPrice) * execQty).toLong() - } else { - calculatedProfitRate = 0.0 - calculatedProfitAmount = 0L - } - } - - // 4. DTO ์กฐ๋ฆฝ - LocalReportGenerator.TradeDetailData( - isBuy = isBuy, - stockName = row[TradeHistoryTable.stockName], - orderTime = orderTimeStr, - timeTaken = timeTakenStr, - execQty = execQty, - avgPrice = avgPrice.toLong(), - profitRate = calculatedProfitRate, - profitAmount = calculatedProfitAmount, - reason = row[TradeHistoryTable.reason], - investmentGrade = row[TradeHistoryTable.investmentGrade], - aiScore = row[TradeHistoryTable.aiScore] - ) - } - - LocalReportGenerator.generateAndOpen(startAsset, endAsset, tradeLogs) - } +// transaction(DatabaseFactory.reportDb) { +// val today = LocalDate.now().toString() +// +// val startAsset = AssetSnapshotTable.select { +// (AssetSnapshotTable.date eq today) and (AssetSnapshotTable.snapshotType eq SnapshotType.START.name) +// }.orderBy(AssetSnapshotTable.id to SortOrder.ASC).limit(1).singleOrNull()?.get(AssetSnapshotTable.totalAsset) ?: 0L +// +// val endAsset = AssetSnapshotTable.select { +// (AssetSnapshotTable.date eq today) and (AssetSnapshotTable.snapshotType eq SnapshotType.END.name) +// }.orderBy(AssetSnapshotTable.id to SortOrder.DESC).limit(1).singleOrNull()?.get(AssetSnapshotTable.totalAsset) ?: startAsset +// +// // ๐Ÿ’ก [๋ณ€๊ฒฝ] isBuy == false ํ•„ํ„ฐ๋ฅผ ์ œ๊ฑฐํ•˜์—ฌ ๊ธˆ์ผ ๋ฐœ์ƒํ•œ '๋ชจ๋“ ' ์ฃผ๋ฌธ ๋‚ด์—ญ์„ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค. +// val allTrades = TradeHistoryTable.select { +// (TradeHistoryTable.orderTime like "$today%") +// }.orderBy(TradeHistoryTable.orderTime to SortOrder.DESC).toList() +// +// val tradeLogs = allTrades.map { row -> +// val tradeId = row[TradeHistoryTable.id] +// val stockCode = row[TradeHistoryTable.stockCode] +// val isBuy = row[TradeHistoryTable.isBuy] +// val status = row[TradeHistoryTable.status] +// val orderTimeStr = row[TradeHistoryTable.orderTime] // ์˜ˆ: "2024-05-20T09:10:00.123" +// +// // 1. ์ฒด๊ฒฐ ์ƒ์„ธ ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ +// val executions = ExecutionDetailsTable.select { ExecutionDetailsTable.tradeId eq tradeId }.toList() +// val execQty = executions.sumOf { it[ExecutionDetailsTable.quantity] } +// +// // VWAP ํ‰๊ท  ์ฒด๊ฒฐ๊ฐ€ ์‚ฐ์ถœ +// val avgPrice = if (execQty > 0) { +// executions.sumOf { it[ExecutionDetailsTable.price] * it[ExecutionDetailsTable.quantity] } / execQty +// } else { +// row[TradeHistoryTable.currentPrice] ?: 0.0 +// } +// +// // ๐Ÿ’ก 2. ์ฃผ๋ฌธ๋ถ€ํ„ฐ ๋งˆ์ง€๋ง‰ ์ฒด๊ฒฐ๊นŒ์ง€ ์†Œ์š”๋œ ์‹œ๊ฐ„ ๊ณ„์‚ฐ ๋กœ์ง +// val lastExecTimeStr = executions.maxByOrNull { it[ExecutionDetailsTable.execTime] }?.get(ExecutionDetailsTable.execTime) +// +// val timeTakenStr = if (lastExecTimeStr != null) { +// try { +// val orderTime = LocalDateTime.parse(orderTimeStr) +// val lastExecTime = LocalDateTime.parse(lastExecTimeStr) +// val duration = Duration.between(orderTime, lastExecTime) +// +// val timeString = if (duration.toMinutes() > 0) { +// "${duration.toMinutes()}๋ถ„ ${duration.seconds % 60}์ดˆ" +// } else { +// "${duration.toMillis() / 1000.0}์ดˆ" +// } +// +// // ์•„์ง ์ƒํƒœ๊ฐ€ COMPLETED๊ฐ€ ์•„๋‹ˆ๋ฉด ๋ฏธ์™„๋ฃŒ ํ‘œ์‹œ (ํ˜„์žฌ ์ƒํƒœ๊ฐ’์ด ORDERED, PARTIAL ๋“ฑ์ผ ๊ฒฝ์šฐ) +// if (status == "COMPLETED") "์™„๋ฃŒ ($timeString)" else "๋ฏธ์™„๋ฃŒ ($timeString)" +// } catch (e: Exception) { +// "๊ณ„์‚ฐ ๋ถˆ๊ฐ€" +// } +// } else { +// "๋ฏธ์ฒด๊ฒฐ (์ง„ํ–‰์ค‘)" +// } +// +// // 3. ์ˆ˜์ต๋ฅ  ๋ฐ ์ˆ˜์ต๊ธˆ ๊ณ„์‚ฐ (๋งค๋„์ผ ๋•Œ๋งŒ) +// var calculatedProfitRate = 0.0 +// var calculatedProfitAmount = 0L +// +// if (!isBuy) { +// // ๊ณผ๊ฑฐ ๋งค์ˆ˜ ๋‹จ๊ฐ€ ์ฐพ๊ธฐ (1์ˆœ์œ„: ์ด์ „ ๋งค์ˆ˜ ๊ธฐ๋ก, 2์ˆœ์œ„: ์•„์นจ ์Šค๋ƒ…์ƒท) +// val recentBuyTrade = TradeHistoryTable.select { +// (TradeHistoryTable.stockCode eq stockCode) and +// (TradeHistoryTable.isBuy eq true) and +// (TradeHistoryTable.id less tradeId) +// }.orderBy(TradeHistoryTable.id to SortOrder.DESC).limit(1).singleOrNull() +// +// var avgBuyPrice = 0.0 +// if (recentBuyTrade != null) { +// val buyExecutions = ExecutionDetailsTable.select { ExecutionDetailsTable.tradeId eq recentBuyTrade[TradeHistoryTable.id] }.toList() +// val buyQty = buyExecutions.sumOf { it[ExecutionDetailsTable.quantity] } +// if (buyQty > 0) avgBuyPrice = buyExecutions.sumOf { it[ExecutionDetailsTable.price] * it[ExecutionDetailsTable.quantity] } / buyQty +// } +// +// // 2์ฐจ ์‹œ๋„: ์˜ค๋Š˜ ์•„์นจ START ์Šค๋ƒ…์ƒท์— ๊ธฐ๋ก๋œ ๋ณด์œ  ํ‰๋‹จ๊ฐ€ +// if (avgBuyPrice == 0.0) { +// val startHoldings = SnapshotHoldingsTable.innerJoin(AssetSnapshotTable).select { +// (SnapshotHoldingsTable.code eq stockCode) and +// (AssetSnapshotTable.date eq today) and +// (AssetSnapshotTable.snapshotType eq SnapshotType.START.name) +// }.singleOrNull() +// avgBuyPrice = startHoldings?.get(SnapshotHoldingsTable.avgPrice) ?: 0.0 +// } +// +// // ๐Ÿ’ก 3์ฐจ ์‹œ๋„ (๊ถ๊ทน์˜ ๋ฐฉ์–ด๋ง‰): ๋ฆฌํฌํŒ… ๋„์ž… ์ด์ „๋ถ€ํ„ฐ ๋“ค๊ณ  ์žˆ๋˜ ๋ ˆ๊ฑฐ์‹œ ์ข…๋ชฉ์ผ ๊ฒฝ์šฐ +// // ๋งค๋„ ์ฃผ๋ฌธ ๋‹น์‹œ์— DB์— ๋ฐฑ์—…ํ•ด๋‘์—ˆ๋˜ ์ฆ๊ถŒ์‚ฌ ๋ณด์œ  ํ‰๋‹จ๊ฐ€๋ฅผ ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. +// if (avgBuyPrice == 0.0) { +// avgBuyPrice = row[TradeHistoryTable.holdingAvgPrice] +// } +// +// // --- [C] ์ตœ์ข… ์ˆ˜์ต๋ฅ  ๊ณ„์‚ฐ --- +// if (avgBuyPrice > 0.0) { +// calculatedProfitRate = ((avgPrice - avgBuyPrice) / avgBuyPrice) * 100 +// calculatedProfitAmount = ((avgPrice - avgBuyPrice) * execQty).toLong() +// } else { +// calculatedProfitRate = 0.0 +// calculatedProfitAmount = 0L +// } +// } +// +// // 4. DTO ์กฐ๋ฆฝ +// LocalReportGenerator.TradeDetailData( +// isBuy = isBuy, +// stockName = row[TradeHistoryTable.stockName], +// orderTime = orderTimeStr, +// timeTaken = timeTakenStr, +// execQty = execQty, +// avgPrice = avgPrice.toLong(), +// profitRate = calculatedProfitRate, +// profitAmount = calculatedProfitAmount, +// reason = row[TradeHistoryTable.reason], +// investmentGrade = row[TradeHistoryTable.investmentGrade], +// aiScore = row[TradeHistoryTable.aiScore] +// ) +// } +// +// LocalReportGenerator.generateAndOpen(startAsset, endAsset, tradeLogs) +// } } } \ No newline at end of file diff --git a/src/main/kotlin/report/database/ReportTables.kt b/src/main/kotlin/report/database/ReportTables.kt index 4a43496..3d66181 100644 --- a/src/main/kotlin/report/database/ReportTables.kt +++ b/src/main/kotlin/report/database/ReportTables.kt @@ -1,6 +1,7 @@ package report.database import org.jetbrains.exposed.sql.Table +import org.jetbrains.exposed.dao.id.IntIdTable // ๐Ÿ’ก [์‹ ๊ทœ] ์„ค์ • ๋ณ€๊ฒฝ ์ด๋ ฅ ์ „์šฉ ํ…Œ์ด๋ธ” object ConfigHistoryTable : Table("config_history") { val id = integer("id").autoIncrement() @@ -11,18 +12,22 @@ object ConfigHistoryTable : Table("config_history") { } // [1] ์ž์‚ฐ ์Šค๋ƒ…์ƒท ๋งˆ์Šคํ„ฐ (์„ค์ • ํ•„๋“œ ์ œ๊ฑฐ) -object AssetSnapshotTable : Table("asset_snapshots") { - val id = integer("id").autoIncrement() - val date = varchar("date", 10) +object AssetSnapshotTable : IntIdTable("asset_snapshots") { +// override val id = integer("id").autoIncrement() +val date = varchar("date", 10) val snapshotType = varchar("type", 20) val totalAsset = long("total_asset") - val totalProfitRate = double("profit_rate") - val deposit = long("deposit") + val totalProfitRate = double("profit_rate") // ๋Œ€ํ‘œ๋‹˜์ด ์ถ”๊ฐ€ํ•˜์‹  ๊ธฐ์กด ํ•„๋“œ + val deposit = long("deposit") // ๋Œ€ํ‘œ๋‹˜์ด ์ถ”๊ฐ€ํ•˜์‹  ๊ธฐ์กด ํ•„๋“œ - val appliedConfigJson = text("applied_config") + // ๐Ÿ’ก [์‹ ๊ทœ ์ถ”๊ฐ€] ๋ฆฌํฌํŠธ์— ํ‘œ์‹œํ•  ์ผ์ผ ๋ณ€๋™ ์ง€ํ‘œ (๊ธฐ์กด ์ฝ”๋“œ ์˜ํ–ฅ 0%) + val dailyAssetChange = long("daily_asset_change").default(0L) + val todayFees = long("today_fees").default(0L) + + val appliedConfigJson = text("applied_config").nullable() val remark = varchar("remark", 255).nullable() - override val primaryKey = PrimaryKey(id) +// override val primaryKey = PrimaryKey(id) } // [2] ๋ณด์œ  ์ข…๋ชฉ ์ƒ์„ธ (UnifiedStockHolding ๊ธฐ์ค€) @@ -38,20 +43,22 @@ object SnapshotHoldingsTable : Table("snapshot_holdings") { val currentPrice = double("current_price") val profitRate = double("profit_rate") val evalAmount = long("eval_amount") - val isDomestic = bool("is_domestic") - val isTodayEntry = bool("is_today_entry") // ๋‹น์ผ ์ง„์ž… ์—ฌ๋ถ€ + val isDomestic = bool("is_domestic").nullable() + val isTodayEntry = bool("is_today_entry").nullable() // ๋‹น์ผ ์ง„์ž… ์—ฌ๋ถ€ override val primaryKey = PrimaryKey(id) } // [3] ๋งค๋งค ์ด๋ ฅ ๋ฐ ๊ฒฐ์ • ๊ทผ๊ฑฐ (TradingDecision + ์ˆ˜์ •๋œ TradeHistoryTable) -object TradeHistoryTable : Table("trade_history") { - val id = integer("id").autoIncrement() +object TradeHistoryTable : IntIdTable("trade_history") { +// val id = integer("id").autoIncrement() val orderNo = varchar("order_no", 50).uniqueIndex() val stockCode = varchar("stock_code", 20) val stockName = varchar("stock_name", 100) val orderTime = varchar("order_time", 50) val isBuy = bool("is_buy") + // ๐Ÿ’ก [ํ•ต์‹ฌ ์‹ ๊ทœ ํ•„๋“œ] ์ˆ˜์ต 0์› ๋ฐฉ์ง€์šฉ: ์‹ค์ œ ์ฒด๊ฒฐ์ด ์™„๋ฃŒ๋œ ๋งค์ˆ˜ ํ‰๋‹จ๊ฐ€ ๋ฐ•์ œ + val purchasePrice = double("purchase_price").default(0.0) val status = varchar("status", 20) val orderQty = integer("order_qty").default(0) val reason = text("reason") // AI ํŒ๋‹จ ์ „๋ฌธ @@ -63,7 +70,7 @@ object TradeHistoryTable : Table("trade_history") { val technicalScore = double("technical_score").nullable() val investmentGrade = text("investment_grade").nullable() // ํˆฌ์ž ๋“ฑ๊ธ‰ (S, A, B...) val holdingAvgPrice = double("holding_avg_price").default(0.0) - override val primaryKey = PrimaryKey(id) +// override val primaryKey = PrimaryKey(id) } // [4] ์ฒด๊ฒฐ ์ƒ์„ธ (๊ทธ๋Œ€๋กœ ์œ ์ง€) diff --git a/src/main/kotlin/service/AutoTradingManager.kt b/src/main/kotlin/service/AutoTradingManager.kt index 2879706..28f4658 100644 --- a/src/main/kotlin/service/AutoTradingManager.kt +++ b/src/main/kotlin/service/AutoTradingManager.kt @@ -206,19 +206,14 @@ object AutoTradingManager { scope.launch { var basePrice = decision.currentPrice val tickSize = MarketUtil.getTickSize(basePrice) - // ๋“ฑ๊ธ‰๋ณ„ ๊ฐ€์ด๋“œ์— ๋”ฐ๋ผ ๋งค์ˆ˜ ํ˜ธ๊ฐ€ ์„ค์ • val oneTickLowerPrice = basePrice - (tickSize * KisSession.config.getValues(investmentGrade.buyGuide).toInt()) var stockCode = decision.stockCode var stockName = decision.stockName val finalPrice = MarketUtil.roundToTickSize(oneTickLowerPrice.toDouble()) val maxStocks = KisSession.config.getValues(ConfigIndex.MAX_HOLDING_COUNT).toInt() -// ๐Ÿ’ก 2. ๋งค์ˆ˜ ์‹คํ–‰ ์ „, ์•ˆ์ „์žฅ์น˜ ํ†ต๊ณผ ์—ฌ๋ถ€ ํ™•์ธ if (!canAddNewPosition(maxStocks)) { - // ์ œํ•œ์— ๊ฑธ๋ ธ๋‹ค๋ฉด, ๋งค์ˆ˜ ๋กœ์ง์„ ๊ฑด๋„ˆ๋›ฐ๊ณ  ๋งค๋„(๋ณด์œ  ์ข…๋ชฉ ๊ด€๋ฆฌ) ๋กœ์ง์œผ๋กœ๋งŒ ๋„˜์–ด๊ฐ‘๋‹ˆ๋‹ค. println("๐Ÿšซ [์•ˆ์ „ ์žฅ์น˜ ์ž‘๋™] ํ˜„์žฌ ํฌ์ง€์…˜์ด ๊ฐ€๋“ ์ฐผ์Šต๋‹ˆ๋‹ค. (์ตœ๋Œ€ ${myOredsAndBalanceCodes.size}/${maxStocks}์ข…๋ชฉ). ์‹ ๊ทœ ๋งค์ˆ˜๋ฅผ ์ผ์‹œ ์ค‘๋‹จํ•˜๊ณ  ๋งค๋„์— ์ง‘์ค‘ํ•ฉ๋‹ˆ๋‹ค.") - - // UI๋‚˜ ๋กœ๊ทธ์— ์ƒํƒœ๋ฅผ ๋„์›Œ์ฃผ๋ฉด ์ข‹์Šต๋‹ˆ๋‹ค. TradingLogStore.addNotice("SYSTEM", "LIMIT", "์ตœ๋Œ€ ๋ณด์œ  ์ข…๋ชฉ ๋„๋‹ฌ๋กœ ์‹ ๊ทœ ๋งค์ˆ˜ ์ผ์‹œ ์ค‘๋‹จ") AutoTradingManager.addToReanalysis(RankingStock(mksc_shrn_iscd = stockCode,hts_kor_isnm = stockName)) TradingLogStore.addLog(decision,"WATCH","๋งค์ˆ˜ ์‹คํŒจ : ์ตœ๋Œ€ ๋ณด์œ  ์ข…๋ชฉ ๋„๋‹ฌ๋กœ ์‹ ๊ทœ ๋งค์ˆ˜ ์ผ์‹œ ์ค‘๋‹จ => ์žฌ๋ถ„์„ ๋Œ€๊ธฐ์—ด์— ์ถ”๊ฐ€") @@ -227,14 +222,11 @@ object AutoTradingManager { KisTradeService.postOrder(stockCode, orderQty, finalPrice.toLong().toString(), isBuy = true) .onSuccess { realOrderNo -> - // ๐Ÿ’ก [๊ฐœ์„  1] ์ฒซ ๋ฒˆ์งธ ์„ฑ๊ณต ๋กœ๊ทธ์— ๋“ฑ๊ธ‰ ์ด๋ฆ„ ์ถ”๊ฐ€ println("[${investmentGrade.displayName}] ์ฃผ๋ฌธ ์„ฑ๊ณต: $realOrderNo $stockCode $orderQty $finalPrice") TradingLogStore.addLog(decision, "BUY", "[${investmentGrade.displayName}] ์ฃผ๋ฌธ ์„ฑ๊ณต: $realOrderNo") - // ์†์ ˆ ๋ผ์ธ ํ•˜๋“œ์ฝ”๋”ฉ (ํ•„์š”์‹œ Config๋กœ ๋นผ๋Š” ๊ฒƒ ๊ถŒ์žฅ) val sRate = -1.5 var tax = KisSession.config.getValues(ConfigIndex.TAX_INDEX) - // ์ตœ์†Œ ๋ณด์žฅ ์ˆ˜์ต๋ฅ (์ „์—ญ ์„ค์ •)๊ณผ ์š”์ฒญ ์ˆ˜์ต๋ฅ  ์ค‘ ํฐ ๊ฐ’ ์„ ํƒ ํ›„ ์„ธ๊ธˆ ๋”ํ•˜๊ธฐ val effectiveProfitRate = (profitRate1 ?: KisSession.config.getValues(ConfigIndex.PROFIT_INDEX)) + tax val calculatedTarget = MarketUtil.roundToTickSize(basePrice * (1 + effectiveProfitRate / 100.0)) @@ -301,17 +293,18 @@ object AutoTradingManager { if (dbItem != null && execData != null && execData.isFilled) { if (dbItem.status == TradeStatus.PENDING_BUY) { - // 1. ์‹ค์ œ ๋งค์ˆ˜ ์ฒด๊ฒฐ๊ฐ€ ๊ฐ€์ ธ์˜ค๊ธฐ (๋ฌธ์ž์—ด์ธ ๊ฒฝ์šฐ ์ˆซ์ž๋กœ ๋ณ€ํ™˜) + // โœ… 1. ์ง„์งœ ์‚ฌ์˜จ ๊ฐ€๊ฒฉ (์‹ค์ œ ๋งค์ˆ˜ ์ฒด๊ฒฐ๊ฐ€) val actualBuyPrice = execData.price.toDoubleOrNull() ?: dbItem.targetPrice + // ๐Ÿ’ก [์ˆ˜์ •] ๋งค์ˆ˜ ์ฃผ๋ฌธ(orderNo)์— ๋Œ€ํ•ด '์ง„์งœ ์‚ฐ ๊ฐ€๊ฒฉ'์„ ๊ธฐ๋กํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. + // ๊ธฐ์กด์—๋Š” ์—ฌ๊ธฐ์— finalTargetPrice๋ฅผ ๋„ฃ์œผ์…จ๋Š”๋ฐ, ๊ทธ๋Ÿฌ๋ฉด ๋งค์ˆ˜ ๋‹จ๊ฐ€๊ฐ€ ์˜ค์—ผ๋ฉ๋‹ˆ๋‹ค. + TradingReportManager.updateExecution(orderNo, actualBuyPrice, dbItem.quantity) + val absoluteMinRate = KisSession.config.getValues(ConfigIndex.TAX_INDEX) + 0.05 val finalProfitRate = maxOf(dbItem.profitRate, absoluteMinRate) - - // 3. ์‹ค์ œ ์ฒด๊ฒฐ๊ฐ€ ๊ธฐ์ค€ ์ต์ ˆ ๊ฐ€๊ฒฉ ์žฌ๊ณ„์‚ฐ ๋ฐ ํ‹ฑ ์‚ฌ์ด์ฆˆ ๋ณด์ • val finalTargetPrice = MarketUtil.roundToTickSize(actualBuyPrice * (1 + finalProfitRate / 100.0)) - TradingReportManager.updateExecution(orderNo,finalTargetPrice,dbItem.quantity) - println("๐ŸŽฏ [๋งค์นญ ์„ฑ๊ณต] ์ต์ ˆ ์ฃผ๋ฌธ ์‹คํ–‰: ${dbItem.name} | ๋งค์ˆ˜๊ฐ€: ${actualBuyPrice.toInt()} -> ๋ชฉํ‘œ๊ฐ€: ${finalTargetPrice.toInt()} (${String.format("%.2f", finalProfitRate)}% ์ ์šฉ)") + println("๐ŸŽฏ [๋งค์ˆ˜ ํ™•์ •] ${dbItem.name} | ๋งค์ˆ˜๊ฐ€: ${actualBuyPrice.toInt()} -> ๋ชฉํ‘œ๊ฐ€ ์„ค์ •: ${finalTargetPrice.toInt()}") KisTradeService.postOrder( stockCode = dbItem.code, @@ -319,29 +312,33 @@ object AutoTradingManager { price = finalTargetPrice.toLong().toString(), isBuy = false ).onSuccess { newSellOrderNo -> - // ์ต์ ˆ๊ฐ€ ์—…๋ฐ์ดํŠธ ๋ฐ ์ƒํƒœ ๋ณ€๊ฒฝ + // ๐Ÿ’ก [๋งค๋„ ์ฃผ๋ฌธ ๊ธฐ๋ก] ์ด์ œ ํŒ”๊ธฐ ์‹œ์ž‘ํ–ˆ๋‹ค๋Š” ์˜์‚ฌ๊ฒฐ์ •์„ ๋ฆฌํฌํŠธ์— ๋‚จ๊น๋‹ˆ๋‹ค. TradingReportManager.recordTradeDecision( orderNo = newSellOrderNo, stockCode = dbItem.code, stockName = dbItem.name, isBuy = false, orderQty = dbItem.quantity, - reason = "๐ŸŽฏ [๋งค์นญ ์„ฑ๊ณต] ์ต์ ˆ ์ฃผ๋ฌธ ์‹คํ–‰: ${dbItem.name} | ๋งค์ˆ˜๊ฐ€: ${actualBuyPrice.toInt()} -> ๋ชฉํ‘œ๊ฐ€: ${finalTargetPrice.toInt()} (${String.format("%.2f", finalProfitRate)}% ์ ์šฉ)", // AI ์ด์œ  - decision = null // AI ๊ฐ์ฒด ํ†ต์งธ๋กœ ์ „๋‹ฌ + reason = "๐ŸŽฏ ๋ชฉํ‘œ ์ˆ˜์ต๋ฅ  ${String.format("%.2f", finalProfitRate)}% ๋„๋‹ฌ์„ ์œ„ํ•œ ์ต์ ˆ ์ฃผ๋ฌธ", + holdingAvgPrice = actualBuyPrice, // ๐Ÿ‘ˆ ์—ฌ๊ธฐ์„œ ๋งค์ˆ˜๋‹จ๊ฐ€๋ฅผ ๋„˜๊ฒจ์ค˜์•ผ ๋งค๋„ ๋ฆฌํฌํŠธ๊ฐ€ ์ •ํ™•ํ•ด์ง‘๋‹ˆ๋‹ค! + decision = null ) DatabaseFactory.updateStatusAndOrderNo(dbItem.id!!, TradeStatus.SELLING, newSellOrderNo) - TradingLogStore.addSellLog(dbItem.name,finalTargetPrice.toString(),"SELL","๐ŸŽฏ [๋งค์นญ ์„ฑ๊ณต] ์ต์ ˆ ์ฃผ๋ฌธ ์‹คํ–‰: ${dbItem.name} | ๋งค์ˆ˜๊ฐ€: ${actualBuyPrice.toInt()} -> ๋ชฉํ‘œ๊ฐ€: ${finalTargetPrice.toInt()} (${String.format("%.2f", finalProfitRate)}% ์ ์šฉ)") executionCache.remove(orderNo) - }.onFailure { - println("โŒ ์ต์ ˆ ์ฃผ๋ฌธ ์‹คํŒจ: ${it.message}") - TradingLogStore.addSellLog(dbItem.name,finalTargetPrice.toString(),"SELL","โŒ ์ต์ ˆ ์ฃผ๋ฌธ ์‹คํŒจ: ${it.message}") } } else if (dbItem.status == TradeStatus.SELLING) { - println("๐ŸŽŠ [๋งค์นญ ์„ฑ๊ณต] ๋งค๋„ ์™„๋ฃŒ ์ฒ˜๋ฆฌ: ${dbItem.name}") - myOredsAndBalanceCodes.remove(dbItem.code) + // โœ… 2. ๋งค๋„ ์™„๋ฃŒ ์‹œ์  (์‹ค์ œ ๋งค๋„ ์ฒด๊ฒฐ๊ฐ€) + val actualSellPrice = execData.price.toDoubleOrNull() ?: 0.0 + val actualSellQty = execData.qty.toIntOrNull() ?: dbItem.quantity + + // ๐Ÿ’ก ๋งค๋„ ์ฃผ๋ฌธ๋ฒˆํ˜ธ์— ๋Œ€ํ•ด '์ง„์งœ ํŒ ๊ฐ€๊ฒฉ'์„ ๊ธฐ๋ก + TradingReportManager.updateExecution(orderNo, actualSellPrice, actualSellQty) + + println("๐ŸŽŠ [๋งค์นญ ์„ฑ๊ณต] ๋งค๋„ ์™„๋ฃŒ: ${dbItem.name} | ๋งค๋„๊ฐ€: ${actualSellPrice.toInt()}") + + myOredsAndBalanceCodes.remove(dbItem.code) + TradingReportManager.closePositionCycle(dbItem.code) // ์‚ฌ์ดํด ์ข…๋ฃŒ ์•Œ๋ฆผ - TradingReportManager.updateExecution(orderNo,execData.price.toDouble(),execData.qty.toInt()) - TradingLogStore.addSellLog(dbItem.name,execData.price,"SELL","๐ŸŽŠ [๋งค์นญ ์„ฑ๊ณต] ๋งค๋„ ์™„๋ฃŒ ์ฒ˜๋ฆฌ") DatabaseFactory.updateStatusAndOrderNo(dbItem.id!!, TradeStatus.COMPLETED) executionCache.remove(orderNo) } @@ -375,7 +372,7 @@ object AutoTradingManager { } suspend fun sellingAfterMarketOnePrice(tradeService: KisTradeService,balance : UnifiedBalance,marketCode : String = "Y") { - balance.getHolldings().forEach { holding -> + balance.getHoldings().forEach { holding -> if (BLACKLISTEDSTOCKCODES.contains(holding.code)){ println("โŒ ์ฐจ๋‹จ ์ฒ˜๋ฆฌ๋œ ์ฃผ์‹ : ${holding.name}") TradingLogStore.addAnalyzer( @@ -437,6 +434,20 @@ object AutoTradingManager { } } else { if ("Y".equals(marketCode)) { + if (KisSession.config.getValues(ConfigIndex.STOP_LOSS) > 0.0 + && holding != null && holding.quantity.toInt() > 0 + && holding.availOrderCount.toInt() > 0 + && holding.profitRate.toDouble() <= KisSession.config.getValues(ConfigIndex.LOSS_MINRATE) + && holding.profitRate.toDouble() >= KisSession.config.getValues(ConfigIndex.LOSS_MAXRATE) + && holding.valuationProfitAmount.toDouble() >= KisSession.config.getValues(ConfigIndex.LOSS_MAX_MONEY)) { + println("${holding.name} ${holding.profitRate.toDouble()} ${holding.valuationProfitAmount.toDouble()} ${KisSession.config.getValues(ConfigIndex.LOSS_MAX_MONEY)} , ${KisSession.config.getValues(ConfigIndex.LOSS_MINRATE)} , ${KisSession.config.getValues(ConfigIndex.STOP_LOSS)}") + val profit = holding.profitRate.toDouble() + TradingLogStore.addNotice( + "๋ณด์œ ์ฃผ์‹[${holding.name}]", + holding.code, + "์ˆ˜์ต๋ฅ ($profit%) -> ${holding.valuationProfitAmount} ์†ํ•ด ์ค‘์ด๋ฉฐ ํ˜„์ œ ์†์ ˆ ๊ฐ€์ด๋“œ์— ์ ํ•ฉํ•จ." + ) + } analyzeDeepLossHoldingsAfterMarket(holding) } } @@ -453,7 +464,7 @@ object AutoTradingManager { println("resumePendingSellOrders") - balance.getHolldings().forEach { holding -> + balance.getHoldings().forEach { holding -> if (BLACKLISTEDSTOCKCODES.contains(holding.code)){ println("โŒ ์ฐจ๋‹จ ์ฒ˜๋ฆฌ๋œ ์ฃผ์‹ : ${holding.name}") TradingLogStore.addAnalyzer( @@ -487,6 +498,19 @@ object AutoTradingManager { "SELL", "๐ŸŽŠ ๋ณด์œ  ์ฃผ์‹[์˜ˆ์ƒ์ˆ˜์ต : ${holding.profitRate}] ${if (isBefore930) "09:30 ์ด์ „ ํ˜„์‹œ์„ธ{${holding.currentPrice}}๋กœ ๋งค๋„[$targetPrice] ์ฃผ๋ฌธ" else "09:30 ์ดํ›„ ์‹œ์„ธ{${holding.currentPrice}} ๊ธฐ์ค€ ํ˜ธ๊ฐ€ ์œ„ ๋งค๋„[$targetPrice] ์ฃผ๋ฌธ"} ์™„๋ฃŒ" ) + DatabaseFactory.saveAutoTrade(AutoTradeItem( + orderNo = newOrderNo, + code = holding.code, + name = holding.name, + quantity = holding.quantity.toInt(), + profitRate = 0.0, + stopLossRate = 0.0, + targetPrice = targetPrice.toDouble(), + stopLossPrice = 0.0, + status = "SELLING", + isDomestic = true + )) + syncAndExecute(newOrderNo) }.onFailure { TradingLogStore.addSellLog( holding.code, @@ -496,7 +520,21 @@ object AutoTradingManager { ) } } else { - analyzeDeepLossHoldingsAfterMarket(holding) + if (KisSession.config.getValues(ConfigIndex.STOP_LOSS) > 0.0 + && holding != null && holding.quantity.toInt() > 0 + && holding.availOrderCount.toInt() > 0 + && holding.profitRate.toDouble() <= KisSession.config.getValues(ConfigIndex.LOSS_MINRATE) + && holding.profitRate.toDouble() >= KisSession.config.getValues(ConfigIndex.LOSS_MAXRATE) + && holding.valuationProfitAmount.toDouble() >= KisSession.config.getValues(ConfigIndex.LOSS_MAX_MONEY)) { + println("${holding.name} ${holding.profitRate.toDouble()} ${holding.valuationProfitAmount.toDouble()} ${KisSession.config.getValues(ConfigIndex.LOSS_MAX_MONEY)} , ${KisSession.config.getValues(ConfigIndex.LOSS_MINRATE)} , ${KisSession.config.getValues(ConfigIndex.STOP_LOSS)}") + val profit = holding.profitRate.toDouble() + TradingLogStore.addNotice( + "๋ณด์œ ์ฃผ์‹[${holding.name}]", + holding.code, + "์ˆ˜์ต๋ฅ ($profit%) -> ${holding.valuationProfitAmount} ์†ํ•ด ์ค‘์ด๋ฉฐ ํ˜„์ œ ์†์ ˆ ๊ฐ€์ด๋“œ์— ์ ํ•ฉํ•จ." + ) + } + analyzeDeepLossHoldingsAfterMarket(holding , true) } delay(200) // API ํ˜ธ์ถœ ๋ถ€ํ•˜ ๋ฐฉ์ง€ } @@ -504,10 +542,10 @@ object AutoTradingManager { } } - private suspend fun analyzeDeepLossHoldingsAfterMarket(holding: UnifiedStockHolding) { // ๐Ÿ’ก [์‹ ๊ทœ ์ถ”๊ฐ€] ์ˆ˜์ต๋ฅ ์ด ํฌ๊ฒŒ ๋งˆ์ด๋„ˆ์Šค์ธ ์ข…๋ชฉ(-5.0% ์ดํ•˜) ์‹ฌ์ธต ๊ฐ€์ด๋“œ ๋ถ„์„ + private suspend fun analyzeDeepLossHoldingsAfterMarket(holding: UnifiedStockHolding, isForce : Boolean = false) { // ๐Ÿ’ก [์‹ ๊ทœ ์ถ”๊ฐ€] ์ˆ˜์ต๋ฅ ์ด ํฌ๊ฒŒ ๋งˆ์ด๋„ˆ์Šค์ธ ์ข…๋ชฉ(-5.0% ์ดํ•˜) ์‹ฌ์ธต ๊ฐ€์ด๋“œ ๋ถ„์„ val now = LocalTime.now() val currentMinute = now.minute - if ((now.hour == 8 || now.hour == 16 || now.hour == 17)) { + if ((!isForce && (now.hour == 8 || now.hour == 16 || now.hour == 17)) || (isForce && (currentMinute % 15 == 0 ))) { val profit = holding.profitRate.toDouble() val lossThreshold = -5.0 // ๊ฐ€์ด๋“œ๋ฅผ ์ž‘๋™์‹œํ‚ฌ ์†์‹ค ๊ธฐ์ค€์„  (ํ•„์š”์‹œ ConfigIndex ๋กœ ๋นผ์…”๋„ ์ข‹์Šต๋‹ˆ๋‹ค) if (profit <= lossThreshold) { @@ -532,10 +570,10 @@ object AutoTradingManager { // ๐ŸŸข [์ถ”๋งค ํƒ€์ ] ๋ณผ๋ฆฐ์ € ํ•˜๋‹จ ํ„ฐ์น˜(1.05๋ฐฐ ์ด๋‚ด) + RSI ๊ณผ๋งค๋„(35 ์ดํ•˜) ๊ตฌ๊ฐ„ if (lowerBand > 0 && currentPrice <= lowerBand * 1.05 && rsiDaily < 35.0) { advice = "๐Ÿ“‰ [์ถ”๋งค ๊ถŒ์žฅ] ๋ณผ๋ฆฐ์ € ๋ฐด๋“œ ํ•˜๋‹จ ํ„ฐ์น˜ ๋ฐ RSI ๊ณผ๋งค๋„(${"%.1f".format(rsiDaily)}). ๊ธฐ์ˆ ์  ๋ฐ˜๋“ฑ ํ™•๋ฅ ์ด ๋งค์šฐ ๋†’์€ ํ†ต๊ณ„์  ๋ฐ”๋‹ฅ๊ถŒ์ž…๋‹ˆ๋‹ค. (๋ฌผํƒ€๊ธฐ ๊ณ ๋ ค)" - TradingLogStore.addAnalyzer( + TradingLogStore.addNotice( "๋ณด์œ ์ฃผ์‹[${holding.name}]", holding.code, - "์ˆ˜์ต๋ฅ ($profit%) -> $advice", true + "์ˆ˜์ต๋ฅ ($profit%) -> $advice" ) } // ๐Ÿ”ด [์†์ ˆ ํƒ€์ ] ์ถ”์„ธ๊ฐ€ ์™„์ „ํžˆ ๊นจ์กŒ๋Š”๋ฐ, ๋ฐ”๋‹ฅ(๋ณผ๋ฆฐ์ € ํ•˜๋‹จ)๊นŒ์ง€ ํ•œ์ฐธ ๋‚จ์•˜์„ ๋•Œ @@ -622,6 +660,7 @@ object AutoTradingManager { val H15M30 = LocalTime.of(15, 30) val H16 = LocalTime.of(16, 0) val H18 = LocalTime.of(18, 0) + val H20 = LocalTime.of(20, 0) val H08M00 = LocalTime.of(8, 0) val H08M45 = LocalTime.of(8, 45) val H07M50 = LocalTime.of(7, 50) @@ -634,10 +673,10 @@ object AutoTradingManager { currentTimeMillis = System.currentTimeMillis() lastTickTime.set(System.currentTimeMillis()) // ์ƒ์กด ์‹ ๊ณ  when { - now.isAfter(H18) || now.isBefore(H07M50) -> { + now.isAfter(H20) || now.isBefore(H07M50) -> { prepareMarketOpen(now) } - now.isBefore(H18) && now.isAfter(H08M00) -> { + now.isBefore(H20) && now.isAfter(H08M00) -> { waitTime = 0.2 if (now.isAfter(LocalTime.of(8, 0)) && now.isBefore(LocalTime.of(15, 30))) { if (!KisSession.isMarketTokenValid() || !KisSession.isTradeTokenValid()) { @@ -651,7 +690,7 @@ object AutoTradingManager { } withTimeout(CYCLE_TIMEOUT) { println("โฑ๏ธ [Cycle Start] ${LocalTime.now()}") - if (now.isAfter(H18)) { + if (now.isAfter(H20)) { executeClosingLiquidation(KisTradeService) } else { executeMarketLoop() @@ -674,7 +713,7 @@ object AutoTradingManager { } suspend fun prepareMarketOpen(now : LocalTime) { - if (now.isAfter(H18) || now.isBefore(H07M50)) { + if (now.isAfter(H20) || now.isBefore(H07M50)) { println("๐ŸŒ™ [System] ๋งˆ๊ฐ ์‹œ๊ฐ„ ๋„๋‹ฌ. ์ž์› ์ •๋ฆฌ ํ›„ ๋Œ€๊ธฐ ๋ชจ๋“œ(์„ค์ • ํ™”๋ฉด)๋กœ ์ „ํ™˜ํ•ฉ๋‹ˆ๋‹ค.") onMarketClosed?.invoke() RagService.clearDailyCache() @@ -717,7 +756,12 @@ object AutoTradingManager { if (isMorning) { currentBalance = KisTradeService.fetchIntegratedBalance().getOrNull() currentBalance?.let { currentBalance -> - TradingReportManager.recordAssetSnapshot(if (LocalTime.now().isAfter(LocalTime.of(17,58))) SnapshotType.END else SnapshotType.MIDDLE ,currentBalance,"") + if (LocalTime.now().isBefore(LocalTime.of(16,2))) { + TradingReportManager.recordAssetSnapshot( + if (LocalTime.now().isAfter(LocalTime.of(17, 59)) + ) SnapshotType.END else SnapshotType.MIDDLE, currentBalance, "" + ) + } } if (AUTOSELL) currentBalance?.let { resumePendingSellOrders(KisTradeService, it) } @@ -729,7 +773,7 @@ object AutoTradingManager { myOredsAndBalanceCodes.clear() checkBalance() val myCash = currentBalance?.deposit?.replace(",", "")?.toLongOrNull() ?: 0L - val myHoldings = currentBalance?.getHolldings()?.map { + val myHoldings = currentBalance?.getHoldings()?.map { myOredsAndBalanceCodes.add(it.code) it.code }?.toSet() ?: emptySet() val pendingStocks = DatabaseFactory.findAllMonitoringTrades().map { @@ -810,7 +854,7 @@ object AutoTradingManager { lastForceCheckMinute = currentMinute // ์‹คํ–‰ ์™„๋ฃŒ ๊ธฐ๋ก } } - else if((now.hour == 8 || now.hour == 16 || now.hour == 17 || now.hour == 18 || now.hour == 19) && (currentMinute % 2 == 1)) { + else if((now.hour == 8 || (now.hour >= 16 && now.hour < 20)) && (currentMinute % 2 == 1)) { if (lastForceCheckMinute != currentMinute) { TradingLogStore.addAnalyzer( " - ", @@ -819,7 +863,7 @@ object AutoTradingManager { true ) var list = mutableListOf("X") - if (now.hour != 8) { + if (now.hour != 8 && now.hour < 18) { list.add("Y") } list.forEach { code -> @@ -901,22 +945,15 @@ object AutoTradingManager { } private suspend fun fetchCandidates(tradeService: KisTradeService): List = coroutineScope { - - listOf( -// async { tradeService.fetchMarketRanking(RankingType.VOLUME1, true).getOrDefault(emptyList()) }, -// async { tradeService.fetchMarketRanking(RankingType.VOLUME0, true).getOrDefault(emptyList()) }, async { tradeService.fetchMarketRanking(RankingType.VOLUME, true).getOrDefault(emptyList()) }, async { tradeService.fetchMarketRanking(RankingType.VOLUME0, true).getOrDefault(emptyList()) }, async { tradeService.fetchMarketRanking(RankingType.VOLUME1, true).getOrDefault(emptyList()) }, async { tradeService.fetchMarketRanking(RankingType.VOLUME4, true).getOrDefault(emptyList()) }, async { tradeService.fetchMarketRanking(RankingType.RISE, true).getOrDefault(emptyList()) }, async { tradeService.fetchMarketRanking(RankingType.FALL, true).getOrDefault(emptyList()) }, -// async { tradeService.fetchMarketRanking(RankingType.RISE2, true).getOrDefault(emptyList()) }, -// async { tradeService.fetchMarketRanking(RankingType.FALL2, true).getOrDefault(emptyList()) }, async { tradeService.fetchMarketRanking(RankingType.VALUE, true).getOrDefault(emptyList()) }, async { tradeService.fetchMarketRanking(RankingType.VOLUME_POWER, true).getOrDefault(emptyList()) }, -// async { tradeService.fetchMarketRanking(RankingType.NEW_HIGH, true).getOrDefault(emptyList()) }, async { tradeService.fetchMarketRanking(RankingType.COMPANY_TRADE, true).getOrDefault(emptyList()) }, async { tradeService.fetchMarketRanking(RankingType.FINANCE, true).getOrDefault(emptyList()) }, async { tradeService.fetchMarketRanking(RankingType.MARKET_VALUE, true).getOrDefault(emptyList()) }, @@ -948,7 +985,7 @@ object AutoTradingManager { private suspend fun executeClosingLiquidation(tradeService: KisTradeService) { val activeTrades = DatabaseFactory.findAllMonitoringTrades() val balanceResult = tradeService.fetchIntegratedBalance().getOrNull() - val realHoldings = balanceResult?.getHolldings()?.associateBy { it.code } ?: emptyMap() + val realHoldings = balanceResult?.getHoldings()?.associateBy { it.code } ?: emptyMap() activeTrades.forEach { trade -> try { @@ -988,16 +1025,6 @@ object AutoTradingManager { } } - - fun checkAndRestart() { - if (!isRunning()) { - println("โš ๏ธ [Watchdog] ์ž๋™ ๋ฐœ๊ตด ๋ฃจํ”„๊ฐ€ ์ค‘๋‹จ๋œ ๊ฒƒ์„ ๊ฐ์ง€ํ–ˆ์Šต๋‹ˆ๋‹ค. ์žฌ์‹œ์ž‘์„ ์‹œ๋„ํ•ฉ๋‹ˆ๋‹ค...") - startAutoDiscoveryLoop() - } else { - - } - } - } diff --git a/src/main/kotlin/ui/TradingDecisionLog.kt b/src/main/kotlin/ui/TradingDecisionLog.kt index e6a2111..f040bd3 100644 --- a/src/main/kotlin/ui/TradingDecisionLog.kt +++ b/src/main/kotlin/ui/TradingDecisionLog.kt @@ -219,7 +219,6 @@ fun TradingDecisionLog() { // ========================================== // 1๏ธโƒฃ ์ฒซ ๋ฒˆ์งธ ์„น์…˜: 2์—ด ๋ฐฐ์น˜ ๊ตฌ๊ฐ„ (span = 3) // ========================================== - var firstSet = mutableSetOf() item(span = { GridItemSpan(maxLineSpan) }) { // 6์นธ ๋ชจ๋‘ ์ฐจ์ง€ Text( @@ -247,13 +246,7 @@ fun TradingDecisionLog() { if (configKey.label.contains("PROFIT")) { newValue = newValue / KisSession.config.getValues(ConfigIndex.PROFIT_INDEX) } - if (firstSet.contains(configKey)) { - TradingLogStore.addSettingLog(configKey.label, oldValue.toString(), newValue.toString(), "๐Ÿ’พ ์ €์žฅ๋จ: ${configKey.label} = $newValue") - } else { - firstSet.add(configKey) - } KisSession.config.setValues(configKey, newValue) - DatabaseFactory.saveConfig(KisSession.config) } var text = if (configKey.label.contains("PROFIT")) { @@ -295,8 +288,10 @@ fun TradingDecisionLog() { item(span = { GridItemSpan(3) }) { SettingSwitchField( label = "์ž๋™ ์ต์ ˆ ํ™œ์„ฑํ™”", - initialChecked = KisSession.config.getValues(ConfigIndex.TAKE_PROFIT) == 1.0, - onCheckedChange = { KisSession.config.setValues(ConfigIndex.TAKE_PROFIT, if (it) 1.0 else 0.0) }, + initialChecked = KisSession.config.getValues(ConfigIndex.TAKE_PROFIT) > 0.0, + onCheckedChange = { + KisSession.config.setValues(ConfigIndex.TAKE_PROFIT, if (it) 1.0 else 0.0) + }, helperText = "๋ชฉํ‘œ ์ˆ˜์ต๋ฅ  ๋„๋‹ฌ ์‹œ ๊ธฐ๊ณ„์  ์ต์ ˆ" ) } @@ -304,7 +299,7 @@ fun TradingDecisionLog() { item(span = { GridItemSpan(3) }) { SettingSwitchField( label = "์ž๋™ ์†์ ˆ ํ™œ์„ฑํ™”", - initialChecked = KisSession.config.getValues(ConfigIndex.STOP_LOSS) == 1.0, + initialChecked = KisSession.config.getValues(ConfigIndex.STOP_LOSS) > 0.0, onCheckedChange = { KisSession.config.setValues(ConfigIndex.STOP_LOSS, if (it) 1.0 else 0.0) }, helperText = "์†์‹ค ๋ฐฉ์–ด์„  ๋„๋‹ฌ ์‹œ ๊ธฐ๊ณ„์  ์†์ ˆ" ) @@ -389,12 +384,6 @@ fun TradingDecisionLog() { var oldValue = KisSession.config.getValues(configKey) var newValue = localText.toDoubleOrNull() ?: 0.0 KisSession.config.setValues(configKey, newValue) - DatabaseFactory.saveConfig(KisSession.config) - if (firstSet.contains(configKey)) { - TradingLogStore.addSettingLog(configKey.label, oldValue.toString(), newValue.toString(), "๐Ÿ’พ ์ €์žฅ๋จ: ${configKey.label} = $newValue") - } else { - firstSet.add(configKey) - } } // labelText ์—…๋ฐ์ดํŠธ ๋กœ์ง (๊ธฐ์กด๊ณผ ๋™์ผ)