feat: OI nan 마스킹 / epsilon 통일 / 정밀도 우선 임계값 #1
@@ -116,12 +116,11 @@ def _calc_signals(d: pd.DataFrame) -> np.ndarray:
|
|||||||
|
|
||||||
|
|
||||||
def _rolling_zscore(arr: np.ndarray, window: int = 288) -> np.ndarray:
|
def _rolling_zscore(arr: np.ndarray, window: int = 288) -> np.ndarray:
|
||||||
"""rolling window z-score 정규화. 15분봉 기준 3일(288캔들) 윈도우.
|
"""rolling window z-score 정규화. nan은 전파된다(nan-safe).
|
||||||
절대값 피처(ATR, 수익률 등)를 레짐 변화에 무관하게 만든다.
|
15분봉 기준 3일(288캔들) 윈도우. min_periods=1로 초반 데이터도 활용."""
|
||||||
min_periods=1로 초반 데이터도 활용하며, ddof=0(모표준편차)으로 계산한다."""
|
|
||||||
s = pd.Series(arr.astype(np.float64))
|
s = pd.Series(arr.astype(np.float64))
|
||||||
r = s.rolling(window=window, min_periods=1)
|
r = s.rolling(window=window, min_periods=1)
|
||||||
mean = r.mean()
|
mean = r.mean() # pandas rolling은 nan을 자동으로 건너뜀
|
||||||
std = r.std(ddof=0)
|
std = r.std(ddof=0)
|
||||||
std = std.where(std >= 1e-8, other=1e-8)
|
std = std.where(std >= 1e-8, other=1e-8)
|
||||||
z = (s - mean) / std
|
z = (s - mean) / std
|
||||||
@@ -258,10 +257,18 @@ def _calc_features_vectorized(
|
|||||||
}, index=d.index)
|
}, index=d.index)
|
||||||
result = pd.concat([result, extra], axis=1)
|
result = pd.concat([result, extra], axis=1)
|
||||||
|
|
||||||
# OI 변화율 / 펀딩비 피처 (parquet에 컬럼이 있으면 z-score, 없으면 0)
|
# OI 변화율 / 펀딩비 피처
|
||||||
# OI는 최근 30일치만 제공되므로 이전 구간은 0으로 채워진 채로 들어옴
|
# 컬럼 없으면 전체 nan, 있으면 0.0 구간(데이터 미제공 구간)을 nan으로 마스킹
|
||||||
oi_raw = d["oi_change"].values if "oi_change" in d.columns else np.zeros(len(d))
|
# LightGBM은 nan을 자체 처리; MLX는 fit()에서 nanmean/nanstd + nan_to_num 처리
|
||||||
fr_raw = d["funding_rate"].values if "funding_rate" in d.columns else np.zeros(len(d))
|
if "oi_change" in d.columns:
|
||||||
|
oi_raw = np.where(d["oi_change"].values == 0.0, np.nan, d["oi_change"].values)
|
||||||
|
else:
|
||||||
|
oi_raw = np.full(len(d), np.nan)
|
||||||
|
|
||||||
|
if "funding_rate" in d.columns:
|
||||||
|
fr_raw = np.where(d["funding_rate"].values == 0.0, np.nan, d["funding_rate"].values)
|
||||||
|
else:
|
||||||
|
fr_raw = np.full(len(d), np.nan)
|
||||||
|
|
||||||
result["oi_change"] = _rolling_zscore(oi_raw.astype(np.float64))
|
result["oi_change"] = _rolling_zscore(oi_raw.astype(np.float64))
|
||||||
result["funding_rate"] = _rolling_zscore(fr_raw.astype(np.float64))
|
result["funding_rate"] = _rolling_zscore(fr_raw.astype(np.float64))
|
||||||
@@ -356,7 +363,12 @@ def generate_dataset_vectorized(
|
|||||||
feat_all = _calc_features_vectorized(d, signal_arr, btc_df=btc_df, eth_df=eth_df)
|
feat_all = _calc_features_vectorized(d, signal_arr, btc_df=btc_df, eth_df=eth_df)
|
||||||
|
|
||||||
# 신호 발생 + NaN 없음 + 미래 데이터 충분한 인덱스만
|
# 신호 발생 + NaN 없음 + 미래 데이터 충분한 인덱스만
|
||||||
available_cols_for_nan_check = [c for c in FEATURE_COLS if c in feat_all.columns]
|
# oi_change/funding_rate는 선택적 피처(컬럼 없으면 전체 nan)이므로 NaN 체크에서 제외
|
||||||
|
OPTIONAL_COLS = {"oi_change", "funding_rate"}
|
||||||
|
available_cols_for_nan_check = [
|
||||||
|
c for c in FEATURE_COLS
|
||||||
|
if c in feat_all.columns and c not in OPTIONAL_COLS
|
||||||
|
]
|
||||||
valid_rows = (
|
valid_rows = (
|
||||||
(signal_arr != "HOLD") &
|
(signal_arr != "HOLD") &
|
||||||
(~feat_all[available_cols_for_nan_check].isna().any(axis=1).values) &
|
(~feat_all[available_cols_for_nan_check].isna().any(axis=1).values) &
|
||||||
|
|||||||
@@ -91,3 +91,48 @@ def test_matches_original_generate_dataset(sample_df):
|
|||||||
assert 0.5 <= ratio <= 2.0, (
|
assert 0.5 <= ratio <= 2.0, (
|
||||||
f"샘플 수 차이가 너무 큼: 벡터화={len(vec)}, 기존={len(orig)}, 비율={ratio:.2f}"
|
f"샘플 수 차이가 너무 큼: 벡터화={len(vec)}, 기존={len(orig)}, 비율={ratio:.2f}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_oi_nan_masking_no_column():
|
||||||
|
"""oi_change 컬럼이 없으면 전체가 nan이어야 한다."""
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
from src.dataset_builder import _calc_features_vectorized, _calc_signals, _calc_indicators
|
||||||
|
|
||||||
|
n = 100
|
||||||
|
np.random.seed(0)
|
||||||
|
df = pd.DataFrame({
|
||||||
|
"open": np.random.uniform(1, 2, n),
|
||||||
|
"high": np.random.uniform(2, 3, n),
|
||||||
|
"low": np.random.uniform(0.5, 1, n),
|
||||||
|
"close": np.random.uniform(1, 2, n),
|
||||||
|
"volume": np.random.uniform(1000, 5000, n),
|
||||||
|
})
|
||||||
|
d = _calc_indicators(df)
|
||||||
|
sig = _calc_signals(d)
|
||||||
|
feat = _calc_features_vectorized(d, sig)
|
||||||
|
|
||||||
|
assert feat["oi_change"].isna().all(), "oi_change 컬럼 없을 때 전부 nan이어야 함"
|
||||||
|
|
||||||
|
|
||||||
|
def test_oi_nan_masking_with_zeros():
|
||||||
|
"""oi_change 컬럼이 있어도 0.0 구간은 nan으로 마스킹되어야 한다."""
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
from src.dataset_builder import _calc_features_vectorized, _calc_signals, _calc_indicators
|
||||||
|
|
||||||
|
n = 100
|
||||||
|
np.random.seed(0)
|
||||||
|
df = pd.DataFrame({
|
||||||
|
"open": np.random.uniform(1, 2, n),
|
||||||
|
"high": np.random.uniform(2, 3, n),
|
||||||
|
"low": np.random.uniform(0.5, 1, n),
|
||||||
|
"close": np.random.uniform(1, 2, n),
|
||||||
|
"volume": np.random.uniform(1000, 5000, n),
|
||||||
|
"oi_change": np.concatenate([np.zeros(50), np.random.uniform(-0.1, 0.1, 50)]),
|
||||||
|
})
|
||||||
|
d = _calc_indicators(df)
|
||||||
|
sig = _calc_signals(d)
|
||||||
|
feat = _calc_features_vectorized(d, sig)
|
||||||
|
|
||||||
|
assert feat["oi_change"].iloc[50:].notna().any(), "실제 OI 값 구간에 유한값이 있어야 함"
|
||||||
|
|||||||
Reference in New Issue
Block a user