Cycles/IncrementalTrader/docs/utils/timeframe-aggregation.md

636 lines
20 KiB
Markdown
Raw Normal View History

2025-05-28 18:26:51 +08:00
# Timeframe Aggregation Usage Guide
## Overview
This guide covers how to use the new timeframe aggregation utilities in the IncrementalTrader framework. The new system provides mathematically correct aggregation with proper timestamp handling to prevent future data leakage.
## Key Features
### ✅ **Fixed Critical Issues**
- **No Future Data Leakage**: Bar timestamps represent END of period
- **Mathematical Correctness**: Results match pandas resampling exactly
- **Trading Industry Standard**: Uses standard bar grouping conventions
- **Proper OHLCV Aggregation**: Correct first/max/min/last/sum rules
### 🚀 **New Capabilities**
- **MinuteDataBuffer**: Efficient real-time data management
- **Flexible Timestamp Modes**: Support for both bar start and end timestamps
- **Memory Bounded**: Automatic buffer size management
- **Performance Optimized**: Fast aggregation for real-time use
## Quick Start
### Basic Usage
```python
from IncrementalTrader.utils.timeframe_utils import aggregate_minute_data_to_timeframe
# Sample minute data
minute_data = [
{
'timestamp': pd.Timestamp('2024-01-01 09:00:00'),
'open': 50000.0, 'high': 50050.0, 'low': 49950.0, 'close': 50025.0, 'volume': 1000
},
{
'timestamp': pd.Timestamp('2024-01-01 09:01:00'),
'open': 50025.0, 'high': 50075.0, 'low': 50000.0, 'close': 50050.0, 'volume': 1200
},
# ... more minute data
]
# Aggregate to 15-minute bars
bars_15m = aggregate_minute_data_to_timeframe(minute_data, "15min")
# Result: bars with END timestamps (no future data leakage)
for bar in bars_15m:
print(f"Bar ending at {bar['timestamp']}: OHLCV = {bar['open']}, {bar['high']}, {bar['low']}, {bar['close']}, {bar['volume']}")
```
### Using MinuteDataBuffer for Real-Time Strategies
```python
from IncrementalTrader.utils.timeframe_utils import MinuteDataBuffer
class MyStrategy(IncStrategyBase):
def __init__(self, name: str = "my_strategy", weight: float = 1.0, params: Optional[Dict] = None):
super().__init__(name, weight, params)
self.timeframe = self.params.get("timeframe", "15min")
self.minute_buffer = MinuteDataBuffer(max_size=1440) # 24 hours
self.last_processed_bar_timestamp = None
def calculate_on_data(self, new_data_point: Dict[str, float], timestamp: pd.Timestamp) -> None:
# Add to buffer
self.minute_buffer.add(timestamp, new_data_point)
# Get latest complete bar
latest_bar = self.minute_buffer.get_latest_complete_bar(self.timeframe)
if latest_bar and latest_bar['timestamp'] != self.last_processed_bar_timestamp:
# Process new complete bar
self.last_processed_bar_timestamp = latest_bar['timestamp']
self._process_complete_bar(latest_bar)
def _process_complete_bar(self, bar: Dict[str, float]) -> None:
# Your strategy logic here
# bar['timestamp'] is the END of the bar period (no future data)
pass
```
## Core Functions
### aggregate_minute_data_to_timeframe()
**Purpose**: Aggregate minute-level OHLCV data to higher timeframes
**Signature**:
```python
def aggregate_minute_data_to_timeframe(
minute_data: List[Dict[str, Union[float, pd.Timestamp]]],
timeframe: str,
timestamp_mode: str = "end"
) -> List[Dict[str, Union[float, pd.Timestamp]]]
```
**Parameters**:
- `minute_data`: List of minute OHLCV dictionaries with 'timestamp' field
- `timeframe`: Target timeframe ("1min", "5min", "15min", "1h", "4h", "1d")
- `timestamp_mode`: "end" (default) for bar end timestamps, "start" for bar start
**Returns**: List of aggregated OHLCV dictionaries with proper timestamps
**Example**:
```python
# Aggregate to 5-minute bars with end timestamps
bars_5m = aggregate_minute_data_to_timeframe(minute_data, "5min", "end")
# Aggregate to 1-hour bars with start timestamps
bars_1h = aggregate_minute_data_to_timeframe(minute_data, "1h", "start")
```
### get_latest_complete_bar()
**Purpose**: Get the latest complete bar for real-time processing
**Signature**:
```python
def get_latest_complete_bar(
minute_data: List[Dict[str, Union[float, pd.Timestamp]]],
timeframe: str,
timestamp_mode: str = "end"
) -> Optional[Dict[str, Union[float, pd.Timestamp]]]
```
**Example**:
```python
# Get latest complete 15-minute bar
latest_15m = get_latest_complete_bar(minute_data, "15min")
if latest_15m:
print(f"Latest complete bar: {latest_15m['timestamp']}")
```
### parse_timeframe_to_minutes()
**Purpose**: Parse timeframe strings to minutes
**Signature**:
```python
def parse_timeframe_to_minutes(timeframe: str) -> int
```
**Supported Formats**:
- Minutes: "1min", "5min", "15min", "30min"
- Hours: "1h", "2h", "4h", "6h", "12h"
- Days: "1d", "7d"
- Weeks: "1w", "2w"
**Example**:
```python
minutes = parse_timeframe_to_minutes("15min") # Returns 15
minutes = parse_timeframe_to_minutes("1h") # Returns 60
minutes = parse_timeframe_to_minutes("1d") # Returns 1440
```
## MinuteDataBuffer Class
### Overview
The `MinuteDataBuffer` class provides efficient buffer management for minute-level data with automatic aggregation capabilities.
### Key Features
- **Memory Bounded**: Configurable maximum size (default: 1440 minutes = 24 hours)
- **Automatic Cleanup**: Old data automatically removed when buffer is full
- **Thread Safe**: Safe for use in multi-threaded environments
- **Efficient Access**: Fast data retrieval and aggregation methods
### Basic Usage
```python
from IncrementalTrader.utils.timeframe_utils import MinuteDataBuffer
# Create buffer for 24 hours of data
buffer = MinuteDataBuffer(max_size=1440)
# Add minute data
buffer.add(timestamp, {
'open': 50000.0,
'high': 50050.0,
'low': 49950.0,
'close': 50025.0,
'volume': 1000
})
# Get aggregated data
bars_15m = buffer.aggregate_to_timeframe("15min", lookback_bars=4)
latest_bar = buffer.get_latest_complete_bar("15min")
# Buffer management
print(f"Buffer size: {buffer.size()}")
print(f"Is full: {buffer.is_full()}")
print(f"Time range: {buffer.get_time_range()}")
```
### Methods
#### add(timestamp, ohlcv_data)
Add new minute data point to the buffer.
```python
buffer.add(pd.Timestamp('2024-01-01 09:00:00'), {
'open': 50000.0, 'high': 50050.0, 'low': 49950.0, 'close': 50025.0, 'volume': 1000
})
```
#### get_data(lookback_minutes=None)
Get data from buffer.
```python
# Get all data
all_data = buffer.get_data()
# Get last 60 minutes
recent_data = buffer.get_data(lookback_minutes=60)
```
#### aggregate_to_timeframe(timeframe, lookback_bars=None, timestamp_mode="end")
Aggregate buffer data to specified timeframe.
```python
# Get last 4 bars of 15-minute data
bars = buffer.aggregate_to_timeframe("15min", lookback_bars=4)
# Get all available 1-hour bars
bars = buffer.aggregate_to_timeframe("1h")
```
#### get_latest_complete_bar(timeframe, timestamp_mode="end")
Get the latest complete bar for the specified timeframe.
```python
latest_bar = buffer.get_latest_complete_bar("15min")
if latest_bar:
print(f"Latest complete bar ends at: {latest_bar['timestamp']}")
```
## Timestamp Modes
### "end" Mode (Default - Recommended)
- **Bar timestamps represent the END of the bar period**
- **Prevents future data leakage**
- **Safe for real-time trading**
```python
# 5-minute bar from 09:00-09:04 is timestamped 09:05
bars = aggregate_minute_data_to_timeframe(data, "5min", "end")
```
### "start" Mode
- **Bar timestamps represent the START of the bar period**
- **Matches some external data sources**
- **Use with caution in real-time systems**
```python
# 5-minute bar from 09:00-09:04 is timestamped 09:00
bars = aggregate_minute_data_to_timeframe(data, "5min", "start")
```
## Best Practices
### 1. Always Use "end" Mode for Real-Time Trading
```python
# ✅ GOOD: Prevents future data leakage
bars = aggregate_minute_data_to_timeframe(data, "15min", "end")
# ❌ RISKY: Could lead to future data leakage
bars = aggregate_minute_data_to_timeframe(data, "15min", "start")
```
### 2. Use MinuteDataBuffer for Strategies
```python
# ✅ GOOD: Efficient memory management
class MyStrategy(IncStrategyBase):
def __init__(self, ...):
self.buffer = MinuteDataBuffer(max_size=1440) # 24 hours
def calculate_on_data(self, data, timestamp):
self.buffer.add(timestamp, data)
latest_bar = self.buffer.get_latest_complete_bar(self.timeframe)
# Process latest_bar...
# ❌ INEFFICIENT: Keeping all data in memory
class BadStrategy(IncStrategyBase):
def __init__(self, ...):
self.all_data = [] # Grows indefinitely
```
### 3. Check for Complete Bars
```python
# ✅ GOOD: Only process complete bars
latest_bar = buffer.get_latest_complete_bar("15min")
if latest_bar and latest_bar['timestamp'] != self.last_processed:
self.process_bar(latest_bar)
self.last_processed = latest_bar['timestamp']
# ❌ BAD: Processing incomplete bars
bars = buffer.aggregate_to_timeframe("15min")
if bars:
self.process_bar(bars[-1]) # Might be incomplete!
```
### 4. Handle Edge Cases
```python
# ✅ GOOD: Robust error handling
try:
bars = aggregate_minute_data_to_timeframe(data, timeframe)
if bars:
# Process bars...
else:
logger.warning("No complete bars available")
except TimeframeError as e:
logger.error(f"Invalid timeframe: {e}")
except ValueError as e:
logger.error(f"Invalid data: {e}")
# ❌ BAD: No error handling
bars = aggregate_minute_data_to_timeframe(data, timeframe)
latest_bar = bars[-1] # Could crash if bars is empty!
```
### 5. Optimize Buffer Size
```python
# ✅ GOOD: Size buffer based on strategy needs
# For 15min strategy needing 20 bars lookback: 20 * 15 = 300 minutes
buffer = MinuteDataBuffer(max_size=300)
# For daily strategy: 24 * 60 = 1440 minutes
buffer = MinuteDataBuffer(max_size=1440)
# ❌ WASTEFUL: Oversized buffer
buffer = MinuteDataBuffer(max_size=10080) # 1 week for 15min strategy
```
## Performance Considerations
### Memory Usage
- **MinuteDataBuffer**: ~1KB per minute of data
- **1440 minutes (24h)**: ~1.4MB memory usage
- **Automatic cleanup**: Old data removed when buffer is full
### Processing Speed
- **Small datasets (< 500 minutes)**: < 5ms aggregation time
- **Large datasets (2000+ minutes)**: < 15ms aggregation time
- **Real-time processing**: < 2ms per minute update
### Optimization Tips
1. **Use appropriate buffer sizes** - don't keep more data than needed
2. **Process complete bars only** - avoid reprocessing incomplete bars
3. **Cache aggregated results** - don't re-aggregate the same data
4. **Use lookback_bars parameter** - limit returned data to what you need
```python
# ✅ OPTIMIZED: Only get what you need
recent_bars = buffer.aggregate_to_timeframe("15min", lookback_bars=20)
# ❌ INEFFICIENT: Getting all data every time
all_bars = buffer.aggregate_to_timeframe("15min")
recent_bars = all_bars[-20:] # Wasteful
```
## Common Patterns
### Pattern 1: Simple Strategy with Buffer
```python
class TrendStrategy(IncStrategyBase):
def __init__(self, name: str = "trend", weight: float = 1.0, params: Optional[Dict] = None):
super().__init__(name, weight, params)
self.timeframe = self.params.get("timeframe", "15min")
self.lookback_period = self.params.get("lookback_period", 20)
# Calculate buffer size: lookback_period * timeframe_minutes
timeframe_minutes = parse_timeframe_to_minutes(self.timeframe)
buffer_size = self.lookback_period * timeframe_minutes
self.buffer = MinuteDataBuffer(max_size=buffer_size)
self.last_processed_timestamp = None
def calculate_on_data(self, new_data_point: Dict[str, float], timestamp: pd.Timestamp) -> None:
# Add to buffer
self.buffer.add(timestamp, new_data_point)
# Get latest complete bar
latest_bar = self.buffer.get_latest_complete_bar(self.timeframe)
if latest_bar and latest_bar['timestamp'] != self.last_processed_timestamp:
# Get historical bars for analysis
historical_bars = self.buffer.aggregate_to_timeframe(
self.timeframe,
lookback_bars=self.lookback_period
)
if len(historical_bars) >= self.lookback_period:
signal = self._analyze_trend(historical_bars)
if signal:
self._generate_signal(signal, latest_bar['timestamp'])
self.last_processed_timestamp = latest_bar['timestamp']
def _analyze_trend(self, bars: List[Dict]) -> Optional[str]:
# Your trend analysis logic here
closes = [bar['close'] for bar in bars]
# ... analysis ...
return "BUY" if trend_up else "SELL" if trend_down else None
```
### Pattern 2: Multi-Timeframe Strategy
```python
class MultiTimeframeStrategy(IncStrategyBase):
def __init__(self, name: str = "multi_tf", weight: float = 1.0, params: Optional[Dict] = None):
super().__init__(name, weight, params)
self.primary_timeframe = self.params.get("primary_timeframe", "15min")
self.secondary_timeframe = self.params.get("secondary_timeframe", "1h")
# Buffer size for the largest timeframe needed
max_timeframe_minutes = max(
parse_timeframe_to_minutes(self.primary_timeframe),
parse_timeframe_to_minutes(self.secondary_timeframe)
)
buffer_size = 50 * max_timeframe_minutes # 50 bars of largest timeframe
self.buffer = MinuteDataBuffer(max_size=buffer_size)
self.last_processed = {
self.primary_timeframe: None,
self.secondary_timeframe: None
}
def calculate_on_data(self, new_data_point: Dict[str, float], timestamp: pd.Timestamp) -> None:
self.buffer.add(timestamp, new_data_point)
# Check both timeframes
for timeframe in [self.primary_timeframe, self.secondary_timeframe]:
latest_bar = self.buffer.get_latest_complete_bar(timeframe)
if latest_bar and latest_bar['timestamp'] != self.last_processed[timeframe]:
self._process_timeframe(timeframe, latest_bar)
self.last_processed[timeframe] = latest_bar['timestamp']
def _process_timeframe(self, timeframe: str, latest_bar: Dict) -> None:
if timeframe == self.primary_timeframe:
# Primary timeframe logic
pass
elif timeframe == self.secondary_timeframe:
# Secondary timeframe logic
pass
```
### Pattern 3: Backtesting with Historical Data
```python
def backtest_strategy(strategy_class, historical_data: List[Dict], params: Dict):
"""Run backtest with historical minute data."""
strategy = strategy_class("backtest", params=params)
signals = []
# Process data chronologically
for data_point in historical_data:
timestamp = data_point['timestamp']
ohlcv = {k: v for k, v in data_point.items() if k != 'timestamp'}
# Process data point
signal = strategy.process_data_point(timestamp, ohlcv)
if signal and signal.signal_type != "HOLD":
signals.append({
'timestamp': timestamp,
'signal_type': signal.signal_type,
'confidence': signal.confidence
})
return signals
# Usage
historical_data = load_historical_data("BTCUSD", "2024-01-01", "2024-01-31")
signals = backtest_strategy(TrendStrategy, historical_data, {"timeframe": "15min"})
```
## Error Handling
### Common Errors and Solutions
#### TimeframeError
```python
try:
bars = aggregate_minute_data_to_timeframe(data, "invalid_timeframe")
except TimeframeError as e:
logger.error(f"Invalid timeframe: {e}")
# Use default timeframe
bars = aggregate_minute_data_to_timeframe(data, "15min")
```
#### ValueError (Invalid Data)
```python
try:
buffer.add(timestamp, ohlcv_data)
except ValueError as e:
logger.error(f"Invalid data: {e}")
# Skip this data point
continue
```
#### Empty Data
```python
bars = aggregate_minute_data_to_timeframe(minute_data, "15min")
if not bars:
logger.warning("No complete bars available")
return
latest_bar = get_latest_complete_bar(minute_data, "15min")
if latest_bar is None:
logger.warning("No complete bar available")
return
```
## Migration from Old System
### Before (Old TimeframeAggregator)
```python
# Old approach - potential future data leakage
class OldStrategy(IncStrategyBase):
def __init__(self, ...):
self.aggregator = TimeframeAggregator(timeframe="15min")
def calculate_on_data(self, data, timestamp):
# Potential issues:
# - Bar timestamps might represent start (future data leakage)
# - Inconsistent aggregation logic
# - Memory not bounded
pass
```
### After (New Utilities)
```python
# New approach - safe and efficient
class NewStrategy(IncStrategyBase):
def __init__(self, ...):
self.buffer = MinuteDataBuffer(max_size=1440)
self.timeframe = "15min"
self.last_processed = None
def calculate_on_data(self, data, timestamp):
self.buffer.add(timestamp, data)
latest_bar = self.buffer.get_latest_complete_bar(self.timeframe)
if latest_bar and latest_bar['timestamp'] != self.last_processed:
# Safe: bar timestamp is END of period (no future data)
# Efficient: bounded memory usage
# Correct: matches pandas resampling
self.process_bar(latest_bar)
self.last_processed = latest_bar['timestamp']
```
### Migration Checklist
- [ ] Replace `TimeframeAggregator` with `MinuteDataBuffer`
- [ ] Update timestamp handling to use "end" mode
- [ ] Add checks for complete bars only
- [ ] Set appropriate buffer sizes
- [ ] Update error handling
- [ ] Test with historical data
- [ ] Verify no future data leakage
## Troubleshooting
### Issue: No bars returned
**Cause**: Not enough data for complete bars
**Solution**: Check data length vs timeframe requirements
```python
timeframe_minutes = parse_timeframe_to_minutes("15min") # 15
if len(minute_data) < timeframe_minutes:
logger.warning(f"Need at least {timeframe_minutes} minutes for {timeframe} bars")
```
### Issue: Memory usage growing
**Cause**: Buffer size too large or not using buffer
**Solution**: Optimize buffer size
```python
# Calculate optimal buffer size
lookback_bars = 20
timeframe_minutes = parse_timeframe_to_minutes("15min")
optimal_size = lookback_bars * timeframe_minutes # 300 minutes
buffer = MinuteDataBuffer(max_size=optimal_size)
```
### Issue: Signals generated too frequently
**Cause**: Processing incomplete bars
**Solution**: Only process complete bars
```python
# ✅ CORRECT: Only process new complete bars
if latest_bar and latest_bar['timestamp'] != self.last_processed:
self.process_bar(latest_bar)
self.last_processed = latest_bar['timestamp']
# ❌ WRONG: Processing every minute
self.process_bar(latest_bar) # Processes same bar multiple times
```
### Issue: Inconsistent results
**Cause**: Using "start" mode or wrong pandas comparison
**Solution**: Use "end" mode and trading standard comparison
```python
# ✅ CORRECT: Trading standard with end timestamps
bars = aggregate_minute_data_to_timeframe(data, "15min", "end")
# ❌ INCONSISTENT: Start mode can cause confusion
bars = aggregate_minute_data_to_timeframe(data, "15min", "start")
```
---
## Summary
The new timeframe aggregation system provides:
- **✅ Mathematical Correctness**: Matches pandas resampling exactly
- **✅ No Future Data Leakage**: Bar end timestamps prevent future data usage
- **✅ Trading Industry Standard**: Compatible with major trading platforms
- **✅ Memory Efficient**: Bounded buffer management
- **✅ Performance Optimized**: Fast real-time processing
- **✅ Easy to Use**: Simple, intuitive API
Use this guide to implement robust, efficient timeframe aggregation in your trading strategies!