File size: 7,234 Bytes
fc115d5 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 | #!/usr/bin/env python3
"""
Regime Specialist Trainer
Trains a specialized PPO agent on a specific market regime:
BULL_TREND, BEAR_TREND, RANGE_CHOP, or HIGH_VOL_BREAKOUT.
The HMM Regime Classifier is used to filter the historical data
so the agent only sees state-transitions relevant to its specialty.
Usage:
python -m src.models.train_specialist --asset BTCUSDT --regime BULL_TREND --timesteps 500000
"""
import os
import sys
import logging
import argparse
from pathlib import Path
import numpy as np
import pandas as pd
from stable_baselines3 import PPO
from stable_baselines3.common.callbacks import EvalCallback
from stable_baselines3.common.monitor import Monitor
from stable_baselines3.common.vec_env import DummyVecEnv
# Add project root
PROJECT_ROOT = Path(__file__).parent.parent.parent
sys.path.insert(0, str(PROJECT_ROOT))
from src.env.mtf_env import create_mtf_env
from src.models.regime_classifier import RegimeClassifier, REGIME_NAMES
from src.backtest.data_loader import download_binance_data
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
def create_regime_dataset(
df: pd.DataFrame,
regime: str,
classifier: RegimeClassifier,
context_window: int = 48,
) -> pd.DataFrame:
"""
Filter dataframe to only include periods where the requested regime is active.
CRITICAL: We can't just drop rows, because the environment needs contiguous
time-series data to compute features (like RSI, MACD, etc.) properly.
Instead, we find continuous 'chunks' of the regime. If a chunk is shorter
than the context window, we discard it.
"""
logger.info(f"Filtering dataset for {regime} regime...")
indices = classifier.get_regime_filtered_indices(df, regime)
if len(indices) == 0:
logger.warning(f"No data found for regime {regime}")
return pd.DataFrame()
# Find contiguous chunks
chunks = []
current_chunk = [indices[0]]
min_chunk_length = 6 # Require at least 6 consecutive hours in the regime
for i in range(1, len(indices)):
if indices[i] == indices[i-1] + 1:
current_chunk.append(indices[i])
else:
if len(current_chunk) >= min_chunk_length:
chunks.append(current_chunk)
current_chunk = [indices[i]]
if len(current_chunk) >= min_chunk_length:
chunks.append(current_chunk)
# Reassemble safe dataframe chunks
# We pad the beginning of each chunk with context_window bars from BEFORE the regime started
# so the environment can compute features right when the regime begins.
safe_indices = []
for chunk in chunks:
start_idx = max(0, chunk[0] - context_window)
end_idx = chunk[-1]
safe_indices.extend(list(range(start_idx, end_idx + 1)))
# Remove duplicates but keep order
safe_indices = sorted(list(set(safe_indices)))
regime_df = df.iloc[safe_indices].copy()
logger.info(
f"β
Filtered to {len(regime_df)} bars "
f"({len(regime_df)/len(df)*100:.1f}%), {len(chunks)} chunks"
)
return regime_df
def train_specialist(
symbol: str = 'BTCUSDT',
regime: str = 'BULL_TREND',
timesteps: int = 500000,
days: int = 730,
):
"""Train a regime specialist agent."""
# ββ Verify Regime ββ
valid_regimes = list(REGIME_NAMES.values())
if regime not in valid_regimes:
raise ValueError(f"Invalid regime {regime}. Must be one of {valid_regimes}")
# ββ Fetch Data ββ
clean_symbol = symbol.replace('USDT', '/USDT')
logger.info(f"π₯ Fetching {days} days of data for {clean_symbol}...")
df = download_binance_data(symbol=clean_symbol, timeframe='1h', days=days)
# ββ Load Regime Classifier ββ
classifier = RegimeClassifier()
if not classifier.load(symbol):
logger.info(f"No existing classifier found. Training new HMM for {symbol}...")
classifier.fit(df)
classifier.save(symbol)
# ββ Filter Data ββ
regime_df = create_regime_dataset(df, regime, classifier, context_window=48)
if len(regime_df) < 1000:
logger.error(f"Not enough data to train {regime} specialist (only {len(regime_df)} bars)")
return
# ββ Train/Val Split ββ
split_idx = int(len(regime_df) * 0.8)
train_df = regime_df.iloc[:split_idx]
val_df = regime_df.iloc[split_idx:]
# ββ Environments ββ
# Using MTF Env from Phase 11.2
logger.info("Setting up Multi-Timeframe Environments...")
train_env = create_mtf_env(train_df)
train_env = Monitor(train_env)
train_env = DummyVecEnv([lambda: train_env])
val_env = create_mtf_env(val_df)
val_env = Monitor(val_env)
val_env = DummyVecEnv([lambda: val_env])
# ββ PPO Setup ββ
# We use a slightly smaller network since it's a specialist
policy_kwargs = dict(net_arch=[dict(pi=[256, 128], vf=[256, 128])])
model = PPO(
"MlpPolicy",
train_env,
learning_rate=3e-4,
n_steps=2048,
batch_size=64,
n_epochs=10,
gamma=0.99,
gae_lambda=0.95,
clip_range=0.2,
ent_coef=0.01,
policy_kwargs=policy_kwargs,
verbose=1,
device="auto"
)
# ββ Training ββ
logger.info(f"\n{'='*60}")
logger.info(f"π Training {regime} Specialist for {symbol}")
logger.info(f"{'='*60}\n")
model_dir = Path('./data/models/specialists')
model_dir.mkdir(parents=True, exist_ok=True)
model_path = model_dir / f"ppo_{symbol.lower()}_{regime.lower()}"
best_model_dir = model_dir / f"best_{symbol.lower()}_{regime.lower()}"
eval_callback = EvalCallback(
val_env,
best_model_save_path=str(best_model_dir),
log_path=str(model_dir / "logs"),
eval_freq=10000,
deterministic=True,
render=False,
)
model.learn(total_timesteps=timesteps, callback=eval_callback)
# Save final model
model.save(model_path)
logger.info(f"β
Final model saved to {model_path}.zip")
logger.info(f"β
Best model saved to {best_model_dir}/best_model.zip")
# βββ CLI ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='Train Regime Specialist')
parser.add_argument('--asset', type=str, default='BTCUSDT')
parser.add_argument('--regime', type=str, required=True,
help='BULL_TREND, BEAR_TREND, RANGE_CHOP, or HIGH_VOL_BREAKOUT')
parser.add_argument('--timesteps', type=int, default=500000)
parser.add_argument('--days', type=int, default=730)
args = parser.parse_args()
train_specialist(
symbol=args.asset,
regime=args.regime.upper(),
timesteps=args.timesteps,
days=args.days,
)
|