Download src/env/mtf_env.py from chen470/drl-trading-bot-dev2: direct link, hf CLI and curl.
- Browser
- Download file 8.86 kB
-
https://huggingface.co/spaces/chen470/drl-trading-bot-dev2/resolve/main/src/env/mtf_env.py
- Command line
-
hf download hf://spaces/chen470/drl-trading-bot-dev2/src/env/mtf_env.py
-
curl -L -o mtf_env.py https://huggingface.co/spaces/chen470/drl-trading-bot-dev2/resolve/main/src/env/mtf_env.py
8.86 kB
| """ | |
| Multi-Timeframe Trading Environment | |
| Extends AdvancedTradingEnv with real multi-timeframe observations: | |
| - 15-minute data: micro order flow, entry timing, short-term momentum | |
| - 1-hour data: main trading timeframe (existing) | |
| - 4-hour data: macro bias, trend direction, support/resistance | |
| The observation is extended with compact MTF summary features | |
| (not full bars), keeping the observation space manageable. | |
| """ | |
| import gymnasium as gym | |
| from gymnasium import spaces | |
| import numpy as np | |
| import pandas as pd | |
| from typing import Optional, Dict, Tuple, List | |
| import logging | |
| from .advanced_env import AdvancedTradingEnv, Actions, Positions | |
| from .advanced_features import AdvancedFeatureEngine | |
| from .advanced_rewards import AdvancedRewardCalculator | |
| logger = logging.getLogger(__name__) | |
| def resample_to_higher_tf(df_1h: pd.DataFrame, target_tf: str = '4h') -> pd.DataFrame: | |
| """ | |
| Resample 1h OHLCV data to a higher timeframe. | |
| Args: | |
| df_1h: DataFrame with 1h OHLCV data (DatetimeIndex) | |
| target_tf: Target timeframe ('4h', '1d', etc.) | |
| Returns: | |
| Resampled DataFrame | |
| """ | |
| # Map our labels to pandas resample rules | |
| tf_map = {'4h': '4h', '1d': '1D', '1w': '1W'} | |
| rule = tf_map.get(target_tf, target_tf) | |
| resampled = df_1h.resample(rule).agg({ | |
| 'open': 'first', | |
| 'high': 'max', | |
| 'low': 'min', | |
| 'close': 'last', | |
| 'volume': 'sum', | |
| }).dropna() | |
| return resampled | |
| def compute_mtf_features_at( | |
| df_4h: pd.DataFrame, | |
| timestamp: pd.Timestamp, | |
| lookback: int = 30, | |
| ) -> np.ndarray: | |
| """ | |
| Compute compact 4h timeframe features at a given 1h timestamp. | |
| Returns 12 features: | |
| - 4h trend (EMA 7 vs 14 slope) | |
| - 4h RSI | |
| - 4h momentum (return over 5, 10, 20 bars) | |
| - 4h volatility (ATR ratio) | |
| - 4h volume trend | |
| - 4h MACD state | |
| - 4h Bollinger position | |
| - 4h support/resistance distance | |
| - 4h candle body ratio | |
| - 4h trend strength (ADX proxy) | |
| """ | |
| # Find the most recent 4h bar at or before this timestamp | |
| mask = df_4h.index <= timestamp | |
| available = df_4h[mask] | |
| if len(available) < lookback: | |
| return np.zeros(12, dtype=np.float32) | |
| window = available.tail(lookback) | |
| close = window['close'].values | |
| high = window['high'].values | |
| low = window['low'].values | |
| volume = window['volume'].values | |
| opn = window['open'].values | |
| features = np.zeros(12, dtype=np.float32) | |
| try: | |
| # 1. EMA trend: EMA7 vs EMA14 normalized | |
| ema7 = pd.Series(close).ewm(span=7).mean().values | |
| ema14 = pd.Series(close).ewm(span=14).mean().values | |
| features[0] = np.clip((ema7[-1] - ema14[-1]) / (close[-1] * 0.01 + 1e-10), -3, 3) | |
| # 2. RSI 14 | |
| delta = np.diff(close) | |
| gain = np.where(delta > 0, delta, 0) | |
| loss = np.where(delta < 0, -delta, 0) | |
| avg_gain = pd.Series(gain).rolling(14).mean().values[-1] | |
| avg_loss = pd.Series(loss).rolling(14).mean().values[-1] | |
| if avg_loss > 0: | |
| rs = avg_gain / avg_loss | |
| features[1] = (100 - 100 / (1 + rs)) / 100 # Normalized 0-1 | |
| else: | |
| features[1] = 0.5 | |
| # 3-5. Momentum (returns over 5, 10, 20 bars) | |
| for i, period in enumerate([5, 10, 20]): | |
| if len(close) > period: | |
| features[2 + i] = np.clip((close[-1] / close[-period] - 1) * 10, -3, 3) | |
| # 6. ATR ratio (current vs average) | |
| tr = np.maximum(high[1:] - low[1:], | |
| np.maximum(np.abs(high[1:] - close[:-1]), | |
| np.abs(low[1:] - close[:-1]))) | |
| atr_14 = pd.Series(tr).rolling(14).mean().values[-1] | |
| atr_avg = pd.Series(tr).rolling(min(len(tr), 50)).mean().values[-1] | |
| features[5] = np.clip(atr_14 / (atr_avg + 1e-10) - 1, -2, 2) | |
| # 7. Volume trend (current vs 20-bar average) | |
| vol_avg = np.mean(volume[-20:]) if len(volume) >= 20 else np.mean(volume) | |
| features[6] = np.clip(volume[-1] / (vol_avg + 1e-10) - 1, -3, 3) | |
| # 8. MACD state (sign of MACD histogram) | |
| ema12 = pd.Series(close).ewm(span=12).mean().values | |
| ema26 = pd.Series(close).ewm(span=26).mean().values | |
| macd = ema12 - ema26 | |
| signal = pd.Series(macd).ewm(span=9).mean().values | |
| hist = macd[-1] - signal[-1] | |
| features[7] = np.clip(hist / (close[-1] * 0.001 + 1e-10), -3, 3) | |
| # 9. Bollinger position | |
| bb_ma = np.mean(close[-20:]) | |
| bb_std = np.std(close[-20:]) | |
| if bb_std > 0: | |
| features[8] = np.clip((close[-1] - bb_ma) / (2 * bb_std + 1e-10), -1.5, 1.5) | |
| # 10. Support/resistance distance (distance to recent high/low) | |
| recent_high = np.max(high[-20:]) | |
| recent_low = np.min(low[-20:]) | |
| price_range = recent_high - recent_low | |
| if price_range > 0: | |
| features[9] = (close[-1] - recent_low) / price_range # 0-1 position | |
| # 11. Candle body ratio (last bar) | |
| hl_range = high[-1] - low[-1] | |
| if hl_range > 0: | |
| features[10] = (close[-1] - opn[-1]) / hl_range # -1 to 1 | |
| # 12. Trend strength (directional movement proxy) | |
| if len(close) >= 14: | |
| up_moves = np.diff(high[-15:]) | |
| down_moves = -np.diff(low[-15:]) | |
| up_moves = np.where(up_moves > 0, up_moves, 0) | |
| down_moves = np.where(down_moves > 0, down_moves, 0) | |
| di_plus = np.mean(up_moves[-14:]) | |
| di_minus = np.mean(down_moves[-14:]) | |
| if (di_plus + di_minus) > 0: | |
| features[11] = np.clip((di_plus - di_minus) / (di_plus + di_minus), -1, 1) | |
| except Exception as e: | |
| logger.debug(f"MTF feature error: {e}") | |
| return np.nan_to_num(features, nan=0.0, posinf=1.0, neginf=-1.0).astype(np.float32) | |
| N_MTF_FEATURES = 12 # per higher timeframe | |
| N_MTF_TOTAL = N_MTF_FEATURES # just 4h for now (15m would need separate data) | |
| class MultiTimeframeTradingEnv(AdvancedTradingEnv): | |
| """ | |
| Multi-Timeframe Trading Environment. | |
| Extends AdvancedTradingEnv by appending compact 4h timeframe features | |
| to the observation space. The agent sees: | |
| [flattened 1h OHLCV+features | agent state | 4h MTF features] | |
| This is backward-compatible — the original observation is preserved | |
| and the MTF features are appended at the end. | |
| """ | |
| def __init__( | |
| self, | |
| df: pd.DataFrame, | |
| df_4h: pd.DataFrame = None, | |
| initial_balance: float = 10000.0, | |
| lookback_window: int = 48, | |
| trading_fee: float = 0.0004, | |
| position_size: float = 0.25, | |
| max_position: int = 1, | |
| render_mode: Optional[str] = None, | |
| ): | |
| # Store 4h data before parent init | |
| if df_4h is not None: | |
| self._df_4h = df_4h.copy() | |
| else: | |
| # Auto-resample from 1h if not provided | |
| self._df_4h = resample_to_higher_tf(df, '4h') | |
| # Parent init (computes features, sets observation space) | |
| super().__init__( | |
| df=df, | |
| initial_balance=initial_balance, | |
| lookback_window=lookback_window, | |
| trading_fee=trading_fee, | |
| position_size=position_size, | |
| max_position=max_position, | |
| render_mode=render_mode, | |
| ) | |
| # Extend observation space with MTF features | |
| base_obs_dim = self.observation_space.shape[0] | |
| new_obs_dim = base_obs_dim + N_MTF_TOTAL | |
| self.observation_space = spaces.Box( | |
| low=-np.inf, high=np.inf, shape=(new_obs_dim,), dtype=np.float32 | |
| ) | |
| logger.info( | |
| f"🔀 MTF env initialized: base={base_obs_dim}, " | |
| f"mtf={N_MTF_TOTAL}, total={new_obs_dim}, " | |
| f"4h bars={len(self._df_4h)}" | |
| ) | |
| def _get_observation(self) -> np.ndarray: | |
| """Construct observation with base features + MTF features.""" | |
| # Get base observation from parent | |
| base_obs = super()._get_observation() | |
| # Compute 4h MTF features at current timestamp | |
| current_idx = min(self.current_step, len(self.df) - 1) | |
| current_ts = self.df.index[current_idx] | |
| mtf_4h = compute_mtf_features_at(self._df_4h, current_ts) | |
| # Concatenate | |
| observation = np.concatenate([base_obs, mtf_4h]).astype(np.float32) | |
| return observation | |
| def create_mtf_env( | |
| df_1h: pd.DataFrame, | |
| df_4h: pd.DataFrame = None, | |
| config: Optional[Dict] = None, | |
| ) -> MultiTimeframeTradingEnv: | |
| """Factory function to create MTF environment.""" | |
| config = config or {} | |
| return MultiTimeframeTradingEnv( | |
| df=df_1h, | |
| df_4h=df_4h, | |
| initial_balance=config.get('initial_balance', 10000.0), | |
| lookback_window=config.get('lookback_window', 48), | |
| trading_fee=config.get('trading_fee', 0.0004), | |
| position_size=config.get('position_size', 0.25), | |
| ) | |