feat(UI/Core): DB 디버거 팝업 리스트화, Optuna 적용 연동 및 Rust tail 코어 안정화
This commit is contained in:
@@ -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"] }
|
||||
|
||||
52
kis_rust_core/src/db_loader.rs
Normal file
52
kis_rust_core/src/db_loader.rs
Normal 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)
|
||||
})
|
||||
}
|
||||
@@ -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
97
kis_rust_core/src/tail.rs
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user