feat(UI/Core): DB 디버거 팝업 리스트화, Optuna 적용 연동 및 Rust tail 코어 안정화

This commit is contained in:
Your Name
2026-09-03 02:29:30 +09:00
parent 93c6f711fa
commit ac1190f1fc
13 changed files with 529 additions and 34 deletions

View File

@@ -9,3 +9,6 @@ crate-type = ["cdylib"]
[dependencies]
pyo3 = { version = "0.20.0", features = ["extension-module"] }
serde = { version = "1.0", features = ["derive"] }
sqlx = { version = "0.7", features = ["mysql", "runtime-tokio-rustls"] }
tokio = { version = "1", features = ["full"] }

View File

@@ -0,0 +1,52 @@
use pyo3::prelude::*;
use sqlx::mysql::MySqlPoolOptions;
use sqlx::Row;
use tokio::runtime::Runtime;
use crate::tail::CandleData;
/// 파이썬에서 DB URL(DSN)을 받아 MariaDB에서 데이터를 직접 조회하여 반환
#[pyfunction]
pub fn load_candles_from_db(dsn: &str, target_code: &str, limit: usize) -> PyResult<Vec<CandleData>> {
// pyo3에서 비동기 코드를 실행하기 위해 tokio 런타임 생성
let rt = Runtime::new().map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("런타임 에러: {}", e)))?;
rt.block_on(async {
let pool = MySqlPoolOptions::new()
.max_connections(2)
.connect(dsn)
.await
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("DB 풀 생성 실패: {}", e)))?;
// ls_ws_candles 예시 테이블에서 데이터 조회
let query = format!(
"SELECT candle_time, open_price, high_price, low_price, close_price, volume, rsi
FROM ls_ws_candles
WHERE code = '{}'
ORDER BY candle_time DESC LIMIT {}",
target_code, limit
);
let rows = sqlx::query(&query)
.fetch_all(&pool)
.await
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("쿼리 실패: {}", e)))?;
let mut candles = Vec::new();
for row in rows {
let rsi: Option<f64> = row.try_get("rsi").unwrap_or(Some(50.0));
candles.push(CandleData {
time_str: row.try_get("candle_time").unwrap_or_default(),
open: row.try_get("open_price").unwrap_or_default(),
high: row.try_get("high_price").unwrap_or_default(),
low: row.try_get("low_price").unwrap_or_default(),
close: row.try_get("close_price").unwrap_or_default(),
volume: row.try_get("volume").unwrap_or_default(),
rsi: rsi.unwrap_or(50.0),
});
}
// 시간순(오름차순) 정렬
candles.reverse();
Ok(candles)
})
}

View File

@@ -1,5 +1,8 @@
use pyo3::prelude::*;
pub mod tail;
pub mod db_loader;
/// 파이썬에서 호출할 백테스트 벤치마크 함수
#[pyfunction]
fn run_dummy_backtest(params: &str) -> PyResult<f64> {
@@ -13,5 +16,9 @@ fn run_dummy_backtest(params: &str) -> PyResult<f64> {
#[pymodule]
fn kis_rust_core(_py: Python, m: &PyModule) -> PyResult<()> {
m.add_function(wrap_pyfunction!(run_dummy_backtest, m)?)?;
m.add_function(wrap_pyfunction!(tail::run_tail_backtest_fast, m)?)?;
m.add_function(wrap_pyfunction!(db_loader::load_candles_from_db, m)?)?;
m.add_class::<tail::TailParams>()?;
m.add_class::<tail::CandleData>()?;
Ok(())
}

97
kis_rust_core/src/tail.rs Normal file
View File

@@ -0,0 +1,97 @@
use pyo3::prelude::*;
use serde::{Deserialize, Serialize};
#[pyclass]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TailParams {
#[pyo3(get, set)]
pub rsi_limit: f64,
#[pyo3(get, set)]
pub drop_pct_min: f64,
#[pyo3(get, set)]
pub tail_recovery_min: f64,
#[pyo3(get, set)]
pub target_pct: f64,
#[pyo3(get, set)]
pub stop_loss_pct: f64,
}
#[pymethods]
impl TailParams {
#[new]
pub fn new(rsi_limit: f64, drop_pct_min: f64, tail_recovery_min: f64, target_pct: f64, stop_loss_pct: f64) -> Self {
Self {
rsi_limit,
drop_pct_min,
tail_recovery_min,
target_pct,
stop_loss_pct,
}
}
}
/// 꼬리잡기용 캔들 정보 (간소화)
#[pyclass]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CandleData {
#[pyo3(get, set)]
pub time_str: String,
#[pyo3(get, set)]
pub open: f64,
#[pyo3(get, set)]
pub high: f64,
#[pyo3(get, set)]
pub low: f64,
#[pyo3(get, set)]
pub close: f64,
#[pyo3(get, set)]
pub volume: f64,
#[pyo3(get, set)]
pub rsi: f64,
}
#[pymethods]
impl CandleData {
#[new]
pub fn new(time_str: String, open: f64, high: f64, low: f64, close: f64, volume: f64, rsi: f64) -> Self {
Self { time_str, open, high, low, close, volume, rsi }
}
}
/// 단일 종목 백테스트 시뮬레이터 (꼬리잡기)
#[pyfunction]
pub fn run_tail_backtest_fast(params: &TailParams, candles: Vec<CandleData>) -> PyResult<f64> {
let mut pnl = 0.0;
let mut in_position = false;
let mut entry_price = 0.0;
for candle in candles {
if !in_position {
// 진입 로직: 하락 후 꼬리 회복 (단순화된 휩쏘 흉내)
let drop = (candle.low - candle.open) / candle.open * 100.0;
let recovery = (candle.close - candle.low) / candle.open * 100.0;
if candle.rsi < params.rsi_limit
&& drop <= -params.drop_pct_min
&& recovery >= params.tail_recovery_min
{
in_position = true;
entry_price = candle.close;
}
} else {
// 청산 로직
let profit_pct = (candle.high - entry_price) / entry_price * 100.0;
let loss_pct = (candle.low - entry_price) / entry_price * 100.0;
if profit_pct >= params.target_pct {
pnl += entry_price * (params.target_pct / 100.0);
in_position = false;
} else if loss_pct <= -params.stop_loss_pct {
pnl += entry_price * (-params.stop_loss_pct / 100.0);
in_position = false;
}
}
}
Ok(pnl)
}