atrade/src/main/kotlin/database/DatabaseFactory.kt

284 lines
11 KiB
Kotlin
Raw Normal View History

2026-01-19 17:09:37 +09:00
import AutoTradeTable.orderNo
import kotlinx.serialization.Serializable
2026-01-13 16:04:25 +09:00
import model.AppConfig
2026-01-10 18:16:50 +09:00
import org.jetbrains.exposed.sql.*
2026-01-14 15:42:26 +09:00
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
2026-01-10 18:16:50 +09:00
import org.jetbrains.exposed.sql.javatime.datetime
import org.jetbrains.exposed.sql.transactions.transaction
import java.io.File
import java.time.LocalDateTime
2026-01-19 17:09:37 +09:00
object TradeStatus {
const val PENDING_BUY = "PENDING_BUY" // 매수 주문 중
const val MONITORING = "MONITORING" // 매수 체결 후 감시 중
const val SELLING = "SELLING" // 손절/익절 매도 주문 중
const val EXPIRED = "EXPIRED" // 서버와 불일치 (유저 판단 대기)
const val COMPLETED = "COMPLETED" // 거래 종료
}
2026-01-10 18:16:50 +09:00
// 1. 앱 설정 테이블
object ConfigTable : Table("app_config") {
val id = integer("id").autoIncrement()
2026-01-13 16:04:25 +09:00
val realAppKey = varchar("real_app_key", 255).default("")
val realSecretKey = varchar("real_secret_key", 255).default("")
val realAccountNo = varchar("real_account_no", 20).default("")
val vtsAppKey = varchar("vts_app_key", 255).default("")
val vtsSecretKey = varchar("vts_secret_key", 255).default("")
val vtsAccountNo = varchar("vts_account_no", 20).default("")
val isSimulation = bool("is_simulation").default(true)
val modelPath = varchar("model_path", 512).default("")
2026-01-14 15:42:26 +09:00
val htsId = varchar("hts_id", 50).default("") // HTS ID 컬럼 추가
2026-01-10 18:16:50 +09:00
override val primaryKey = PrimaryKey(id)
}
2026-01-13 16:04:25 +09:00
2026-01-14 15:42:26 +09:00
// 2. 자동매매 감시 테이블
object AutoTradeTable : Table("auto_trades") {
val id = integer("id").autoIncrement()
val stockCode = varchar("stock_code", 20)
val stockName = varchar("stock_name", 100)
2026-01-19 17:09:37 +09:00
val quantity = integer("quantity").default(0)
val profitRate = double("profit_rate").default(0.0)
val stopLossRate = double("stop_loss_rate").default(0.0)
val targetPrice = double("target_price").default(0.0)
val stopLossPrice = double("stop_loss_price").default(0.0)
val orderNo = varchar("order_no", 50).uniqueIndex()
val status = varchar("status", 20).default("PENDING_BUY")
2026-01-14 15:42:26 +09:00
val isDomestic = bool("is_domestic").default(true)
override val primaryKey = PrimaryKey(id)
}
2026-01-19 17:09:37 +09:00
2026-01-14 15:42:26 +09:00
// 3. 거래 내역 테이블
2026-01-10 18:16:50 +09:00
object TradeLogTable : Table("trade_logs") {
val id = long("id").autoIncrement()
2026-01-14 15:42:26 +09:00
val stockCode = varchar("stock_code", 20)
val stockName = varchar("stock_name", 50)
val tradeType = varchar("trade_type", 10)
val price = double("price")
val quantity = integer("quantity")
val timestamp = datetime("timestamp")
val logMessage = text("log_message")
2026-01-10 18:16:50 +09:00
override val primaryKey = PrimaryKey(id)
}
object DatabaseFactory {
fun init() {
2026-01-14 15:42:26 +09:00
val dbPath = File("db/autotrade_db").absolutePath
2026-01-10 18:16:50 +09:00
Database.connect(
"jdbc:h2:$dbPath;DB_CLOSE_DELAY=-1;",
driver = "org.h2.Driver"
)
transaction {
2026-01-14 15:42:26 +09:00
// 테이블 생성 (AutoTradeTable 포함)
SchemaUtils.create(ConfigTable, TradeLogTable, AutoTradeTable)
2026-01-10 18:16:50 +09:00
}
}
2026-01-14 15:42:26 +09:00
// --- 자동매매(감시) 관련 함수 ---
2026-01-19 17:09:37 +09:00
2026-01-14 15:42:26 +09:00
/**
2026-01-19 17:09:37 +09:00
* 새로운 자동매매 등록 (주로 PENDING_BUY 상태로 시작)
2026-01-14 15:42:26 +09:00
*/
2026-01-19 17:09:37 +09:00
fun saveAutoTrade(item: AutoTradeItem) = transaction {
AutoTradeTable.insert {
it[stockCode] = item.code
it[stockName] = item.name
it[quantity] = item.quantity
it[profitRate] = item.profitRate
it[stopLossRate] = item.stopLossRate
it[targetPrice] = item.targetPrice
it[stopLossPrice] = item.stopLossPrice
it[orderNo] = item.orderNo
it[status] = item.status
it[isDomestic] = item.isDomestic
2026-01-14 15:42:26 +09:00
}
}
/**
2026-01-19 17:09:37 +09:00
* 상태 변경 가격 업데이트 (: PENDING_BUY -> MONITORING)
2026-01-14 15:42:26 +09:00
*/
2026-01-19 17:09:37 +09:00
fun updateAutoTrade(item: AutoTradeItem) = transaction {
val id = item.id ?: return@transaction
AutoTradeTable.update({ AutoTradeTable.id eq id }) {
it[targetPrice] = item.targetPrice
it[stopLossPrice] = item.stopLossPrice
it[orderNo] = item.orderNo
it[status] = item.status
2026-01-14 15:42:26 +09:00
}
}
/**
2026-01-19 17:09:37 +09:00
* 감시 중인 모든 종목 리스트 반환 (ActiveTradeSection UI용)
2026-01-14 15:42:26 +09:00
*/
2026-01-19 17:09:37 +09:00
fun getActiveAutoTrades(): List<AutoTradeItem> = transaction {
AutoTradeTable.select {
AutoTradeTable.status inList listOf("MONITORING", "SELLING", "PENDING_BUY")
}.map { mapToAutoTradeItem(it) }
2026-01-14 15:42:26 +09:00
}
/**
2026-01-19 17:09:37 +09:00
* 종목코드로 현재 감시 중인 설정이 있는지 확인 (UI 체크박스 상태용)
2026-01-14 15:42:26 +09:00
*/
2026-01-19 17:09:37 +09:00
fun findConfigByCode(code: String): AutoTradeItem? = transaction {
AutoTradeTable.select {
(AutoTradeTable.stockCode eq code) and (AutoTradeTable.status eq "MONITORING")
}.lastOrNull()?.let { mapToAutoTradeItem(it) }
}
fun deleteAutoTrade(id: Int) = transaction {
AutoTradeTable.deleteWhere { AutoTradeTable.id eq id }
2026-01-14 15:42:26 +09:00
}
private fun mapToAutoTradeItem(it: ResultRow) = AutoTradeItem(
2026-01-19 17:09:37 +09:00
id = it[AutoTradeTable.id],
2026-01-14 15:42:26 +09:00
code = it[AutoTradeTable.stockCode],
name = it[AutoTradeTable.stockName],
2026-01-19 17:09:37 +09:00
quantity = it[AutoTradeTable.quantity],
profitRate = it[AutoTradeTable.profitRate],
stopLossRate = it[AutoTradeTable.stopLossRate],
2026-01-14 15:42:26 +09:00
targetPrice = it[AutoTradeTable.targetPrice],
stopLossPrice = it[AutoTradeTable.stopLossPrice],
2026-01-19 17:09:37 +09:00
orderNo = it[AutoTradeTable.orderNo],
2026-01-14 15:42:26 +09:00
status = it[AutoTradeTable.status],
isDomestic = it[AutoTradeTable.isDomestic]
)
// --- 기존 설정 및 로그 관련 함수 ---
2026-01-10 18:16:50 +09:00
fun saveTradeLog(code: String, name: String, type: String, price: Double, qty: Int, msg: String) {
transaction {
TradeLogTable.insert {
it[stockCode] = code
it[stockName] = name
it[tradeType] = type
it[TradeLogTable.price] = price
it[quantity] = qty
it[timestamp] = LocalDateTime.now()
it[logMessage] = msg
}
}
}
2026-01-14 15:42:26 +09:00
fun findConfigByAccount(accountNo: String): AppConfig? = transaction {
ConfigTable.select {
(ConfigTable.realAccountNo eq accountNo) or (ConfigTable.vtsAccountNo eq accountNo)
}.lastOrNull()?.let {
AppConfig(
realAppKey = it[ConfigTable.realAppKey],
realSecretKey = it[ConfigTable.realSecretKey],
realAccountNo = it[ConfigTable.realAccountNo],
vtsAppKey = it[ConfigTable.vtsAppKey],
vtsSecretKey = it[ConfigTable.vtsSecretKey],
vtsAccountNo = it[ConfigTable.vtsAccountNo],
isSimulation = it[ConfigTable.isSimulation],
htsId = it[ConfigTable.htsId], // htsId 로드
modelPath = it[ConfigTable.modelPath]
)
2026-01-13 16:04:25 +09:00
}
}
fun saveConfig(config: AppConfig) {
transaction {
ConfigTable.deleteAll()
ConfigTable.insert {
it[realAppKey] = config.realAppKey
it[realSecretKey] = config.realSecretKey
it[vtsAppKey] = config.vtsAppKey
it[vtsSecretKey] = config.vtsSecretKey
it[realAccountNo] = config.realAccountNo
it[vtsAccountNo] = config.vtsAccountNo
it[isSimulation] = config.isSimulation
2026-01-14 15:42:26 +09:00
it[htsId] = config.htsId
2026-01-13 16:04:25 +09:00
it[modelPath] = config.modelPath
}
}
}
2026-01-19 17:09:37 +09:00
fun saveOrUpdate(item: AutoTradeItem) = transaction {
val existing = AutoTradeTable.select { AutoTradeTable.orderNo eq item.orderNo }.firstOrNull()
if (existing == null) {
AutoTradeTable.insert {
it[orderNo] = item.orderNo
it[stockCode] = item.code
it[stockName] = item.name
it[status] = item.status
it[targetPrice] = item.targetPrice
it[stopLossPrice] = item.stopLossPrice
it[quantity] = item.quantity
it[isDomestic] = item.isDomestic
}
} else {
AutoTradeTable.update({ AutoTradeTable.orderNo eq item.orderNo }) {
it[status] = item.status
it[targetPrice] = item.targetPrice
it[stopLossPrice] = item.stopLossPrice
}
}
}
/**
* 주문번호로 항목 조회 (가장 핵심적인 식별자)
*/
fun findByOrderNo(orderNo: String): AutoTradeItem? = transaction {
AutoTradeTable.select { AutoTradeTable.orderNo eq orderNo }
.map { mapToAutoTradeItem(it) }
.singleOrNull()
}
/**
* 서버 동기화: DB에는 PENDING_BUY/MONITORING인데 서버 미체결 내역에 없는 경우 EXPIRED로 변경
*/
fun syncWithServer(serverOrderNos: List<String>) = transaction {
AutoTradeTable.update({
(AutoTradeTable.status inList listOf(TradeStatus.PENDING_BUY, TradeStatus.MONITORING)) and
(AutoTradeTable.orderNo notInList serverOrderNos)
}) {
it[status] = TradeStatus.EXPIRED
}
}
/**
* 상태 업데이트 주문번호 갱신 (: 매수체결 신규 익절 주문번호로 교체)
*/
fun updateStatusAndOrderNo(id: Int, newStatus: String, newOrderNo: String? = null) = transaction {
AutoTradeTable.update({ AutoTradeTable.id eq id }) {
it[status] = newStatus
if (newOrderNo != null) it[orderNo] = newOrderNo
}
}
/**
* 감시 중인 모든 종목 리스트 (Status별 필터링 용이하게 수정)
*/
fun getAutoTradesByStatus(statusList: List<String>): List<AutoTradeItem> = transaction {
AutoTradeTable.select { AutoTradeTable.status inList statusList }
.map { mapToAutoTradeItem(it) }
}
2026-01-14 15:42:26 +09:00
}
2026-01-13 16:04:25 +09:00
2026-01-19 17:09:37 +09:00
@Serializable
2026-01-14 15:42:26 +09:00
data class AutoTradeItem(
2026-01-19 17:09:37 +09:00
val id: Int? = null, // DB 식별자
val orderNo: String, // 핵심 키: KIS 주문번호 (odno)
val code: String, // 종목 코드
val name: String, // 종목 명
// 상태 머신 (PENDING_BUY, MONITORING, SELLING, EXPIRED, COMPLETED)
var status: String = "PENDING_BUY",
// 가격 정보
val orderedPrice: Double = 0.0, // 주문 단가
var targetPrice: Double = 0.0, // 익절 목표가
var stopLossPrice: Double = 0.0, // 손절 목표가
// 수량 정보
val quantity: Int = 0, // 총 주문 수량
var remainedQuantity: Int = 0, // 미체결 잔량 (서버 동기화용)
val isDomestic: Boolean = true,
val profitRate: Double = 0.0, // 설정 시 사용한 목표 비율
val stopLossRate: Double = 0.0
2026-01-14 15:42:26 +09:00
)