File size: 13,276 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
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
"""
Multi-Asset Feature Engine

Extends UltimateFeatureEngine to support multiple assets with:
- Asset embeddings (unique features per asset)
- Cross-asset correlation features (BTC dominance effect)
- Asset-specific risk scaling
"""

import numpy as np
import pandas as pd
from typing import Dict, List, Optional, Tuple
import logging

from src.features.ultimate_features import UltimateFeatureEngine
from src.data.multi_asset_fetcher import SUPPORTED_ASSETS, get_asset_embedding

logger = logging.getLogger(__name__)


class MultiAssetFeatureEngine:
    """
    Feature engine for multi-asset trading.
    
    Extends base features with:
    - Asset ID embedding (4 features)
    - Cross-asset features (BTC influence, correlation regimes)
    - Asset-specific volatility scaling
    
    Total features: 94 (base) + 4 (asset) + 6 (cross-asset) = 104
    """
    
    # Asset embedding features
    ASSET_FEATURES = [
        'asset_id_norm',       # Normalized asset ID
        'base_volatility',     # Expected volatility vs BTC
        'liquidity_score',     # Relative liquidity (1.0 = BTC)
        'btc_correlation',     # Typical correlation with BTC
    ]
    
    # Cross-asset features (when BTC data is available)
    CROSS_ASSET_FEATURES = [
        'btc_return_1h',       # BTC return last 1h (leader signal)
        'btc_return_4h',       # BTC return last 4h
        'btc_momentum',        # BTC momentum (EMA slope)
        'btc_volatility',      # BTC current volatility
        'relative_strength',   # Asset return vs BTC return (RS)
        'correlation_regime',  # Current correlation regime
    ]
    
    # Alternative Data Features (Phase 11.4)
    ALT_DATA_FEATURES = [
        'fear_greed_value',
        'fear_greed_class',
        'btc_dominance',
        'altcoin_season_index',
    ]
    
    def __init__(
        self,
        include_cross_asset: bool = True,
        btc_data: Optional[pd.DataFrame] = None,
    ):
        """
        Initialize multi-asset feature engine.
        
        Args:
            include_cross_asset: Whether to include BTC cross-asset features
            btc_data: BTC price data for cross-asset calculations
        """
        self.base_engine = UltimateFeatureEngine()
        self.include_cross_asset = include_cross_asset
        self.btc_data = btc_data
        
        from src.features.alternative_data import AlternativeDataCollector
        self.alt_collector = AlternativeDataCollector()
        self.current_alt_features = None
        
        # Calculate feature count
        self.n_asset_features = len(self.ASSET_FEATURES)
        self.n_cross_features = len(self.CROSS_ASSET_FEATURES) if include_cross_asset else 0
        self.n_alt_features = len(self.ALT_DATA_FEATURES)
        self.n_base_features = 94  # From UltimateFeatureEngine
        self.n_total_features = self.n_base_features + self.n_asset_features + self.n_cross_features + self.n_alt_features
        
        logger.info(
            f"๐Ÿ“Š MultiAssetFeatureEngine: {self.n_total_features} features "
            f"(base={self.n_base_features}, asset={self.n_asset_features}, cross={self.n_cross_features}, alt={self.n_alt_features})"
        )
    
    def set_btc_data(self, btc_data: pd.DataFrame):
        """Set BTC data for cross-asset feature calculation."""
        self.btc_data = btc_data.copy()
        self.btc_data['btc_timestamp'] = self.btc_data['timestamp']
        logger.info(f"๐Ÿ“Š BTC data set: {len(btc_data)} candles")
    
    def compute_asset_features(self, symbol: str) -> np.ndarray:
        """
        Compute asset embedding features.
        
        Returns:
            Array of 4 asset-specific features
        """
        if symbol not in SUPPORTED_ASSETS:
            # Default features for unknown assets
            return np.array([0.5, 1.5, 0.5, 0.7])
        
        return get_asset_embedding(symbol)
    
    def compute_cross_asset_features(
        self,
        df: pd.DataFrame,
        symbol: str,
        idx: int,
    ) -> np.ndarray:
        """
        Compute cross-asset features (BTC influence on alts).
        
        Returns:
            Array of 6 cross-asset features
        """
        if not self.include_cross_asset or self.btc_data is None or symbol == "BTCUSDT":
            return np.zeros(len(self.CROSS_ASSET_FEATURES))
        
        try:
            # Find corresponding BTC timestamp
            current_time = df['timestamp'].iloc[idx]
            
            # Find BTC data at same timestamp
            btc_subset = self.btc_data[self.btc_data['timestamp'] <= current_time].tail(24)
            
            if len(btc_subset) < 4:
                return np.zeros(len(self.CROSS_ASSET_FEATURES))
            
            # BTC returns
            btc_close = btc_subset['close'].values
            btc_return_1h = (btc_close[-1] / btc_close[-2] - 1) if len(btc_close) >= 2 else 0
            btc_return_4h = (btc_close[-1] / btc_close[-4] - 1) if len(btc_close) >= 4 else 0
            
            # BTC momentum (EMA slope)
            if len(btc_close) >= 8:
                ema_fast = btc_close[-4:].mean()
                ema_slow = btc_close[-8:].mean()
                btc_momentum = (ema_fast / ema_slow - 1) * 10
            else:
                btc_momentum = 0
            
            # BTC volatility
            btc_returns = np.diff(btc_close) / btc_close[:-1]
            btc_volatility = np.std(btc_returns) * np.sqrt(24) if len(btc_returns) > 1 else 0
            
            # Asset relative strength
            asset_close = df['close'].values
            if idx >= 1:
                asset_return = asset_close[idx] / asset_close[idx-1] - 1
                relative_strength = asset_return - btc_return_1h
            else:
                relative_strength = 0
            
            # Correlation regime (rolling correlation with BTC)
            if idx >= 20:
                asset_returns = np.diff(asset_close[idx-20:idx+1]) / asset_close[idx-20:idx]
                btc_returns_align = np.diff(btc_close[-21:]) / btc_close[-21:-1] if len(btc_close) >= 21 else np.zeros(20)
                
                if len(asset_returns) == len(btc_returns_align) == 20:
                    correlation_regime = np.corrcoef(asset_returns, btc_returns_align)[0, 1]
                else:
                    correlation_regime = SUPPORTED_ASSETS.get(symbol, SUPPORTED_ASSETS["BTCUSDT"]).btc_correlation
            else:
                correlation_regime = SUPPORTED_ASSETS.get(symbol, SUPPORTED_ASSETS["BTCUSDT"]).btc_correlation
            
            features = np.array([
                np.clip(btc_return_1h * 100, -10, 10),  # Scale to reasonable range
                np.clip(btc_return_4h * 100, -20, 20),
                np.clip(btc_momentum, -5, 5),
                np.clip(btc_volatility, 0, 1),
                np.clip(relative_strength * 100, -10, 10),
                np.clip(correlation_regime, -1, 1),
            ])
            
            return np.nan_to_num(features, nan=0.0)
            
        except Exception as e:
            logger.warning(f"Cross-asset feature error: {e}")
            return np.zeros(len(self.CROSS_ASSET_FEATURES))
    
    def compute_features(
        self,
        df: pd.DataFrame,
        symbol: str,
        idx: int = -1,
    ) -> np.ndarray:
        """
        Compute full feature vector for multi-asset trading.
        
        Args:
            df: OHLCV DataFrame (must have timestamp, open, high, low, close, volume)
            symbol: Trading pair (e.g., "BTCUSDT")
            idx: Row index to compute features for (-1 for last row)
            
        Returns:
            Feature vector of length n_total_features
        """
        if idx == -1:
            idx = len(df) - 1
        
        # 1. Base features (94)
    def compute_features(
        self,
        df: pd.DataFrame,
        symbol: str,
        idx: int = -1,
    ) -> np.ndarray:
        """
        Compute full feature vector for multi-asset trading.
        """
        if idx == -1:
            idx = len(df) - 1
            
        # 1. Base features (94)
        # Optimization: UltimateFeatureEngine computes for whole DF. 
        # If we only need one row, this is expensive but necessary for indicators.
        # Ideally we cache this or use a stateful engine.
        all_base_features = self.base_engine.compute_features(df)
        base_features = all_base_features[idx]
        
        # 2. Asset embedding features (4)
        asset_features = self.compute_asset_features(symbol)
        
        # 3. Cross-asset features (6, optional)
        if self.include_cross_asset:
            cross_features = self.compute_cross_asset_features(df, symbol, idx)
        else:
            cross_features = np.array([])
            
        # 4. Alternative Data features (4)
        if self.current_alt_features is None:
            raw_alt = self.alt_collector.get_current_features()
            self.current_alt_features = np.array([
                raw_alt['fear_greed_value'],
                raw_alt['fear_greed_class'],
                raw_alt['btc_dominance'],
                raw_alt['altcoin_season_index']
            ], dtype=np.float32)
        alt_features = self.current_alt_features
        
        # Combine all features
        all_features = np.concatenate([base_features, asset_features, cross_features, alt_features])
        
        return all_features.astype(np.float32)
    
    def compute_features_batch(
        self,
        df: pd.DataFrame,
        symbol: str,
        start_idx: int = 0,
    ) -> np.ndarray:
        """
        Compute features for all rows from start_idx to end.
        Vectorized implementation for speed.
        """
        # 1. Base features (N x 94)
        all_base_features = self.base_engine.compute_features(df)
        base_batch = all_base_features[start_idx:]
        n_rows = len(base_batch)
        
        # 2. Asset features (N x 4)
        asset_feat = self.compute_asset_features(symbol)
        asset_batch = np.tile(asset_feat, (n_rows, 1))
        
        # 3. Cross-asset features (N x 6)
        if self.include_cross_asset:
            # We still compute these in a loop for now as they depend on specific logic
            # or we could optimize later. For now, loop is okay as it's just 6 features.
            cross_batch = np.zeros((n_rows, self.n_cross_features))
            for i in range(n_rows):
                cross_batch[i] = self.compute_cross_asset_features(df, symbol, start_idx + i)
        else:
            cross_batch = np.zeros((n_rows, 0))
            
        # 4. Alternative Data features (N x 4)
        if self.current_alt_features is None:
            raw_alt = self.alt_collector.get_current_features()
            self.current_alt_features = np.array([
                raw_alt['fear_greed_value'],
                raw_alt['fear_greed_class'],
                raw_alt['btc_dominance'],
                raw_alt['altcoin_season_index']
            ], dtype=np.float32)
            
        alt_batch = np.tile(self.current_alt_features, (n_rows, 1))
            
        # Combine
        combined = np.hstack([base_batch, asset_batch, cross_batch, alt_batch])
        
        return combined.astype(np.float32)
    
    def get_feature_names(self) -> List[str]:
        """Get names of all features."""
        base_names = self.base_engine.get_feature_names()
        all_names = base_names + self.ASSET_FEATURES
        
        if self.include_cross_asset:
            all_names += self.CROSS_ASSET_FEATURES
        
        return all_names


def create_multi_asset_env_features(
    asset_data: Dict[str, pd.DataFrame],
    btc_df: pd.DataFrame,
) -> Dict[str, np.ndarray]:
    """
    Create feature arrays for multiple assets.
    
    Args:
        asset_data: Dict mapping symbol to OHLCV DataFrame
        btc_df: BTC data for cross-asset features
        
    Returns:
        Dict mapping symbol to feature array
    """
    engine = MultiAssetFeatureEngine(include_cross_asset=True)
    engine.set_btc_data(btc_df)
    
    result = {}
    
    for symbol, df in asset_data.items():
        if df.empty:
            continue
        
        # Compute features for all rows (skip first 50 for warmup)
        start_idx = min(50, len(df) - 1)
        features = engine.compute_features_batch(df, symbol, start_idx)
        result[symbol] = features
        
        logger.info(f"๐Ÿ“Š {symbol}: {features.shape[0]} samples, {features.shape[1]} features")
    
    return result


if __name__ == "__main__":
    logging.basicConfig(level=logging.INFO)
    
    from src.data.multi_asset_fetcher import MultiAssetDataFetcher
    
    # Fetch data
    fetcher = MultiAssetDataFetcher()
    btc_df = fetcher.fetch_asset("BTCUSDT", "1h", days=7)
    eth_df = fetcher.fetch_asset("ETHUSDT", "1h", days=7)
    
    # Create feature engine
    engine = MultiAssetFeatureEngine(include_cross_asset=True)
    engine.set_btc_data(btc_df)
    
    # Compute features
    features = engine.compute_features(eth_df, "ETHUSDT", -1)
    print(f"Feature vector shape: {features.shape}")
    print(f"Feature names: {engine.get_feature_names()[-10:]}")  # Last 10