Skip to content

Commit 41abbe4

Browse files
KIMSE0NG1Ltwq110
andauthored
[AI][FEAT] TCN 모델 뼈대 및 학습/추론 어댑터 구현 (#291)
* [AI][FEAT]: TCN 모델 뼈대 및 학습/추론 어댑터 구현 * [AI][FEAT]: TCN 모델 뼈대 및 학습/추론 어댑터 구현 * [AI] SISC-290 [FIX] TCN wrapper 최적화(중복 루프 제거) * [AI] SISC-290 [FIX] TCN wrapper 기술적 지표 레거시 버전 사용 픽스 * [AI] SISC-290 [FEAT] TCN 모델 래퍼 텐서 학습 코드 추가 * [AI] SISC-290 [FEAT] TCN train 데이터셋 인덱스 슬라이싱을 날짜단위로 처리하도록 변경 --------- Co-authored-by: twq110 <aaa11093@gmail.com>
1 parent f0a7db9 commit 41abbe4

6 files changed

Lines changed: 654 additions & 68 deletions

File tree

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,11 @@ __pycache__/
2828
/env
2929
/.vs
3030
/.venv/
31+
AI/.venv/
32+
AI/data/weights/tcn/
3133

3234
# ===== Backend =====
3335
backend/src/main/java/org/sejongisc/backend/stock/TestController.java
3436

3537
# ===== windows =====
36-
.desktop.ini
38+
.desktop.ini

AI/modules/features/market_derived.py

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,21 @@
44
from .technical import compute_rsi, compute_atr, compute_macd, compute_bollinger_bands
55

66
def add_market_changes(df: pd.DataFrame) -> pd.DataFrame:
7-
"""가격 및 거래량 기반 변화율 계산 [명세서 준수]"""
7+
"""가격 및 거래량 기반 변화율 계산 [명세서 및 레거시 하위 호환]"""
8+
epsilon = 1e-9
9+
10+
# --- [레거시 호환] 기존 모델 학습에 사용되었던 캔들 모양 및 거래량 피처 복구 ---
11+
prev_close = df['close'].shift(1)
12+
df['open_ratio'] = (df['open'] - prev_close) / (prev_close + epsilon)
13+
df['high_ratio'] = (df['high'] - prev_close) / (prev_close + epsilon)
14+
df['low_ratio'] = (df['low'] - prev_close) / (prev_close + epsilon)
15+
df['vol_change'] = df['volume'].pct_change()
16+
17+
# --- [신규 명세서] 일간 수익률 및 일중 변동성 ---
818
df['ret_1d'] = df['close'].pct_change()
919
df['log_return'] = np.log(df['close'] / df['close'].shift(1))
10-
# 일중변동성비율 : (High - Low) / Close
11-
df['intraday_vol'] = (df['high'] - df['low']) / (df['close'] + 1e-9)
20+
df['intraday_vol'] = (df['high'] - df['low']) / (df['close'] + epsilon)
21+
1222
return df
1323

1424
def add_macro_changes(df: pd.DataFrame) -> pd.DataFrame:
@@ -32,8 +42,10 @@ def add_standard_technical_features(df: pd.DataFrame) -> pd.DataFrame:
3242
"""레거시 로직 + 명세서 신규 지표 통합"""
3343
epsilon = 1e-9
3444

35-
# 1. RSI (rsi_14)
45+
# 1. RSI
46+
# 명세서 기준(rsi_14)과 레거시 모델 호환용(rsi) 컬럼을 모두 생성합니다.
3647
df['rsi_14'] = compute_rsi(df['close'], 14) / 100.0
48+
df['rsi'] = df['rsi_14']
3749

3850
# 2. MACD (macd, macd_signal)
3951
df['macd'], df['macd_signal'] = compute_macd(df['close'])
@@ -48,11 +60,12 @@ def add_standard_technical_features(df: pd.DataFrame) -> pd.DataFrame:
4860
# 모델 입력용 포지션
4961
df['bb_position'] = (df['close'] - df['bollinger_lb']) / ( (df['bollinger_ub'] - df['bollinger_lb']).replace(0, epsilon) )
5062

51-
# 4. ATR (atr_14)
63+
# 4. ATR (atr_14) - [신규 명세서 지표]
5264
df['atr_14'] = compute_atr(df['high'], df['low'], df['close'], 14)
5365

54-
# 5. Moving Averages (ma_20, ma_60)
55-
for w in [20, 60]:
66+
# 5. Moving Averages (ma_5, ma_20, ma_60)
67+
# [레거시 호환] 기존에 사용하던 5일 이평선(ma_5)을 복구하여 배열에 추가했습니다.
68+
for w in [5, 20, 60]:
5669
df[f'ma_{w}'] = df['close'].rolling(window=w).mean()
5770
# 모델 입력용 이격도 (Standard Key: ma_trend_score 등에 활용)
5871
df[f'ma{w}_ratio'] = (df['close'] - df[f'ma_{w}']) / (df[f'ma_{w}'] + epsilon)
Lines changed: 42 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,53 @@
11
# AI/modules/signal/core/dataset_builder.py
2+
import pandas as pd
3+
from typing import Union, Optional
24

35
from AI.modules.signal.core.data_loader import DataLoader
4-
from AI.modules.features.technical import add_technical_indicators
5-
from AI.modules.features.market_derived import add_macro_features
6+
from AI.modules.features.processor import FeatureProcessor
7+
from AI.modules.features.market_derived import add_market_changes, add_macro_changes
68

7-
def get_standard_training_data(start_date: str, end_date: str) -> pd.DataFrame:
9+
def apply_strict_nan_rules(df: pd.DataFrame) -> pd.DataFrame:
810
"""
9-
데이터 수집/전처리 코드를 짤 필요 없이 이 함수만 호출하면 됩니다.
10-
명세서에 정의된 모든 원천/파생 피처가 포함된 DataFrame을 반환합니다.
11+
SISC 데이터 명세서에 따른 엄격한 결측치 처리 규칙을 적용합니다.
1112
"""
12-
# 1. DB에서 원천 데이터(Raw) 로드
13-
loader = DataLoader()
14-
raw_df = loader.load_data_from_db(start_date, end_date)
13+
df_clean = df.copy()
14+
15+
macro_cols = ['vix_close', 'vix_change_rate', 'us10y', 'us10y_chg', 'dxy_close', 'dxy_chg']
16+
available_macro = [col for col in macro_cols if col in df_clean.columns]
17+
18+
if available_macro:
19+
df_clean[available_macro] = df_clean[available_macro].ffill()
20+
21+
df_clean = df_clean.dropna().reset_index(drop=True)
22+
return df_clean
23+
24+
def get_standard_training_data(
25+
start_date_or_df: Union[str, pd.DataFrame],
26+
end_date: Optional[str] = None
27+
) -> pd.DataFrame:
28+
"""
29+
SISC 파이프라인 표준 학습 데이터셋 생성기. (학습/추론 겸용)
30+
- 사용법 1 (학습용): get_standard_training_data('2020-01-01', '2024-01-01') -> DB 조회
31+
- 사용법 2 (추론용): get_standard_training_data(df) -> DB 생략하고 전처리만 수행
32+
"""
33+
# 1. 입력 타입에 따른 분기 처리 (DB 로드 vs 직접 주입)
34+
if isinstance(start_date_or_df, pd.DataFrame):
35+
df = start_date_or_df.copy()
36+
else:
37+
loader = DataLoader()
38+
df = loader.load_data_from_db(start_date_or_df, end_date)
39+
if df is None or df.empty:
40+
raise ValueError(f"지정된 기간({start_date_or_df} ~ {end_date})의 데이터를 불러오지 못했습니다.")
41+
42+
# 2. 파생 피처 레이어 계산 (1차: 기초 변화율 연산)
43+
df = add_market_changes(df)
44+
df = add_macro_changes(df)
1545

16-
# 2. 파생 피처 레이어 계산
17-
# (팀장님이 기존 features 모듈을 활용해 모든 지표를 미리 계산해서 붙여줌)
18-
df = add_technical_indicators(raw_df) # rsi_14, macd 등 추가
19-
df = add_macro_features(df) # vix_z_score, us10y_chg 등 추가
46+
# 3. 파생 피처 레이어 계산 (2차: FeatureProcessor를 통한 심화 지표 일괄 연산)
47+
processor = FeatureProcessor(df)
48+
df = processor.execute_pipeline()
2049

21-
# 3. 결측치(NaN) 처리
22-
# Market은 Drop, Macro는 ffill 등...
50+
# 4. 결측치(NaN) 처리 규칙 적용
2351
df = apply_strict_nan_rules(df)
2452

2553
return df
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import torch
2+
import torch.nn as nn
3+
from typing import List
4+
5+
6+
class Chomp1d(nn.Module):
7+
# Causal padding 뒤에 생기는 미래 시점 누수를 잘라냅니다.
8+
def __init__(self, chomp_size: int):
9+
super().__init__()
10+
self.chomp_size = chomp_size
11+
12+
def forward(self, x: torch.Tensor) -> torch.Tensor:
13+
if self.chomp_size == 0:
14+
return x
15+
return x[:, :, :-self.chomp_size].contiguous()
16+
17+
18+
class TemporalBlock(nn.Module):
19+
# 두 개의 dilated Conv1d와 residual connection으로 TCN의 기본 블록을 구성합니다.
20+
def __init__(
21+
self,
22+
in_channels: int,
23+
out_channels: int,
24+
kernel_size: int,
25+
dilation: int,
26+
dropout: float,
27+
):
28+
super().__init__()
29+
padding = (kernel_size - 1) * dilation
30+
31+
self.net = nn.Sequential(
32+
nn.Conv1d(
33+
in_channels,
34+
out_channels,
35+
kernel_size,
36+
padding=padding,
37+
dilation=dilation,
38+
),
39+
Chomp1d(padding),
40+
nn.ReLU(),
41+
nn.Dropout(dropout),
42+
nn.Conv1d(
43+
out_channels,
44+
out_channels,
45+
kernel_size,
46+
padding=padding,
47+
dilation=dilation,
48+
),
49+
Chomp1d(padding),
50+
nn.ReLU(),
51+
nn.Dropout(dropout),
52+
)
53+
self.downsample = (
54+
nn.Conv1d(in_channels, out_channels, kernel_size=1)
55+
if in_channels != out_channels
56+
else None
57+
)
58+
self.activation = nn.ReLU()
59+
60+
def forward(self, x: torch.Tensor) -> torch.Tensor:
61+
residual = x if self.downsample is None else self.downsample(x)
62+
return self.activation(self.net(x) + residual)
63+
64+
65+
class TCNClassifier(nn.Module):
66+
# 여러 개의 TemporalBlock을 쌓아 멀티 호라이즌 이진 분류 logits를 출력합니다.
67+
def __init__(
68+
self,
69+
input_size: int,
70+
output_size: int,
71+
num_channels: List[int],
72+
kernel_size: int = 3,
73+
dropout: float = 0.2,
74+
):
75+
super().__init__()
76+
77+
layers = []
78+
for i, out_channels in enumerate(num_channels):
79+
in_channels = input_size if i == 0 else num_channels[i - 1]
80+
dilation = 2 ** i
81+
layers.append(
82+
TemporalBlock(
83+
in_channels=in_channels,
84+
out_channels=out_channels,
85+
kernel_size=kernel_size,
86+
dilation=dilation,
87+
dropout=dropout,
88+
)
89+
)
90+
91+
self.backbone = nn.Sequential(*layers)
92+
self.head = nn.Sequential(
93+
nn.AdaptiveAvgPool1d(1),
94+
nn.Flatten(),
95+
nn.Linear(num_channels[-1], output_size),
96+
)
97+
98+
def forward(self, x: torch.Tensor) -> torch.Tensor:
99+
# 입력은 [Batch, Seq, Features]이며 Conv1d에 맞게 [Batch, Features, Seq]로 바꿉니다.
100+
x = x.permute(0, 2, 1)
101+
x = self.backbone(x)
102+
return self.head(x)

0 commit comments

Comments
 (0)