548 lines
14 KiB
Markdown
548 lines
14 KiB
Markdown
# Candle TA Features Quick Reference
|
||
|
||
## Overview
|
||
|
||
Enhanced technical analysis features for `OHLCVBar` class providing comprehensive candle pattern recognition, relative sizing, and body/wick analysis.
|
||
|
||
**Location**: `core/data_models.py` - `OHLCVBar` class
|
||
|
||
---
|
||
|
||
## Quick Start
|
||
|
||
```python
|
||
from core.data_models import OHLCVBar, BaseDataInput
|
||
from datetime import datetime
|
||
|
||
# Create a candle
|
||
bar = OHLCVBar(
|
||
symbol='ETH/USDT',
|
||
timestamp=datetime.now(),
|
||
open=2000.0,
|
||
high=2050.0,
|
||
low=1990.0,
|
||
close=2040.0,
|
||
volume=1000.0,
|
||
timeframe='1m'
|
||
)
|
||
|
||
# Check basic properties
|
||
print(f"Bullish: {bar.is_bullish}") # True
|
||
print(f"Body size: {bar.body_size}") # 40.0
|
||
print(f"Pattern: {bar.get_candle_pattern()}") # 'standard'
|
||
|
||
# Get all TA features
|
||
reference_bars = [...] # Previous 10 candles
|
||
ta_features = bar.get_ta_features(reference_bars)
|
||
print(f"Features: {len(ta_features)}") # 22 features
|
||
```
|
||
|
||
---
|
||
|
||
## Properties (Computed On-Demand)
|
||
|
||
### Basic Measurements
|
||
|
||
| Property | Type | Description | Example |
|
||
|----------|------|-------------|---------|
|
||
| `body_size` | float | Absolute size of candle body | `abs(close - open)` |
|
||
| `upper_wick` | float | Size of upper shadow | `high - max(open, close)` |
|
||
| `lower_wick` | float | Size of lower shadow | `min(open, close) - low` |
|
||
| `total_range` | float | Total high-low range | `high - low` |
|
||
|
||
### Candle Type
|
||
|
||
| Property | Type | Description |
|
||
|----------|------|-------------|
|
||
| `is_bullish` | bool | True if close > open (hollow/green) |
|
||
| `is_bearish` | bool | True if close < open (solid/red) |
|
||
| `is_doji` | bool | True if body < 10% of total range |
|
||
|
||
---
|
||
|
||
## Methods
|
||
|
||
### 1. Ratio Calculations
|
||
|
||
#### `get_body_to_range_ratio() -> float`
|
||
Returns body size as percentage of total range (0.0 to 1.0)
|
||
|
||
```python
|
||
ratio = bar.get_body_to_range_ratio()
|
||
# 0.0 = doji (no body)
|
||
# 0.5 = body is half the range
|
||
# 1.0 = marubozu (all body, no wicks)
|
||
```
|
||
|
||
#### `get_upper_wick_ratio() -> float`
|
||
Returns upper wick as percentage of total range (0.0 to 1.0)
|
||
|
||
```python
|
||
ratio = bar.get_upper_wick_ratio()
|
||
# 0.0 = no upper wick
|
||
# 0.5 = upper wick is half the range
|
||
# 1.0 = all upper wick (impossible in practice)
|
||
```
|
||
|
||
#### `get_lower_wick_ratio() -> float`
|
||
Returns lower wick as percentage of total range (0.0 to 1.0)
|
||
|
||
```python
|
||
ratio = bar.get_lower_wick_ratio()
|
||
# 0.0 = no lower wick
|
||
# 0.5 = lower wick is half the range
|
||
```
|
||
|
||
---
|
||
|
||
### 2. Relative Sizing
|
||
|
||
#### `get_relative_size(reference_bars, method='avg') -> float`
|
||
|
||
Compare current candle size to reference candles.
|
||
|
||
**Parameters:**
|
||
- `reference_bars`: List of previous OHLCVBar objects
|
||
- `method`: Comparison method
|
||
- `'avg'`: Compare to average (default)
|
||
- `'max'`: Compare to maximum
|
||
- `'median'`: Compare to median
|
||
|
||
**Returns:**
|
||
- `1.0` = Same size as reference
|
||
- `> 1.0` = Larger than reference
|
||
- `< 1.0` = Smaller than reference
|
||
|
||
**Example:**
|
||
```python
|
||
# Get last 10 candles
|
||
recent = ohlcv_list[-10:]
|
||
current = ohlcv_list[-1]
|
||
|
||
# Compare to average
|
||
avg_ratio = current.get_relative_size(recent[:-1], 'avg')
|
||
if avg_ratio > 2.0:
|
||
print("Current candle is 2x larger than average!")
|
||
|
||
# Compare to maximum
|
||
max_ratio = current.get_relative_size(recent[:-1], 'max')
|
||
if max_ratio > 1.0:
|
||
print("Current candle is the largest!")
|
||
```
|
||
|
||
---
|
||
|
||
### 3. Pattern Recognition
|
||
|
||
#### `get_candle_pattern() -> str`
|
||
|
||
Identify basic candle pattern.
|
||
|
||
**Patterns Detected:**
|
||
|
||
| Pattern | Criteria | Interpretation |
|
||
|---------|----------|----------------|
|
||
| `'doji'` | Body < 10% of range | Indecision, potential reversal |
|
||
| `'hammer'` | Small body at top, long lower wick | Bullish reversal signal |
|
||
| `'shooting_star'` | Small body at bottom, long upper wick | Bearish reversal signal |
|
||
| `'spinning_top'` | Small body, both wicks present | Indecision |
|
||
| `'marubozu_bullish'` | Large bullish body (>90% of range) | Strong bullish momentum |
|
||
| `'marubozu_bearish'` | Large bearish body (>90% of range) | Strong bearish momentum |
|
||
| `'standard'` | Regular candle | Normal price action |
|
||
|
||
**Example:**
|
||
```python
|
||
pattern = bar.get_candle_pattern()
|
||
|
||
if pattern == 'hammer':
|
||
print("Potential bullish reversal!")
|
||
elif pattern == 'shooting_star':
|
||
print("Potential bearish reversal!")
|
||
elif pattern == 'doji':
|
||
print("Market indecision")
|
||
```
|
||
|
||
**Pattern Criteria Details:**
|
||
|
||
```python
|
||
# Doji
|
||
body_ratio < 0.1
|
||
|
||
# Marubozu
|
||
body_ratio > 0.9
|
||
|
||
# Hammer
|
||
body_ratio < 0.3 and lower_ratio > 0.6 and upper_ratio < 0.1
|
||
|
||
# Shooting Star
|
||
body_ratio < 0.3 and upper_ratio > 0.6 and lower_ratio < 0.1
|
||
|
||
# Spinning Top
|
||
body_ratio < 0.3 and (upper_ratio + lower_ratio) > 0.6
|
||
```
|
||
|
||
---
|
||
|
||
### 4. Complete TA Feature Set
|
||
|
||
#### `get_ta_features(reference_bars=None) -> Dict[str, float]`
|
||
|
||
Get all technical analysis features as a dictionary.
|
||
|
||
**Parameters:**
|
||
- `reference_bars`: Optional list of previous bars for relative sizing
|
||
|
||
**Returns:** Dictionary with 22 features (or 12 without reference_bars)
|
||
|
||
**Feature Categories:**
|
||
|
||
#### Basic Properties (3 features)
|
||
```python
|
||
{
|
||
'is_bullish': 1.0 or 0.0,
|
||
'is_bearish': 1.0 or 0.0,
|
||
'is_doji': 1.0 or 0.0,
|
||
}
|
||
```
|
||
|
||
#### Size Ratios (3 features)
|
||
```python
|
||
{
|
||
'body_to_range_ratio': 0.0 to 1.0,
|
||
'upper_wick_ratio': 0.0 to 1.0,
|
||
'lower_wick_ratio': 0.0 to 1.0,
|
||
}
|
||
```
|
||
|
||
#### Normalized Sizes (4 features)
|
||
```python
|
||
{
|
||
'body_size_pct': body_size / close,
|
||
'upper_wick_pct': upper_wick / close,
|
||
'lower_wick_pct': lower_wick / close,
|
||
'total_range_pct': total_range / close,
|
||
}
|
||
```
|
||
|
||
#### Volume Analysis (1 feature)
|
||
```python
|
||
{
|
||
'volume_per_range': volume / total_range,
|
||
}
|
||
```
|
||
|
||
#### Relative Sizing (3 features - if reference_bars provided)
|
||
```python
|
||
{
|
||
'relative_size_avg': ratio vs average,
|
||
'relative_size_max': ratio vs maximum,
|
||
'relative_size_median': ratio vs median,
|
||
}
|
||
```
|
||
|
||
#### Pattern Encoding (7 features - one-hot)
|
||
```python
|
||
{
|
||
'pattern_doji': 1.0 or 0.0,
|
||
'pattern_hammer': 1.0 or 0.0,
|
||
'pattern_shooting_star': 1.0 or 0.0,
|
||
'pattern_spinning_top': 1.0 or 0.0,
|
||
'pattern_marubozu_bullish': 1.0 or 0.0,
|
||
'pattern_marubozu_bearish': 1.0 or 0.0,
|
||
'pattern_standard': 1.0 or 0.0,
|
||
}
|
||
```
|
||
|
||
**Example:**
|
||
```python
|
||
# Get complete feature set
|
||
reference_bars = ohlcv_list[-10:-1]
|
||
current_bar = ohlcv_list[-1]
|
||
|
||
ta_features = current_bar.get_ta_features(reference_bars)
|
||
|
||
# Access specific features
|
||
if ta_features['pattern_hammer'] == 1.0:
|
||
print("Hammer pattern detected!")
|
||
|
||
if ta_features['relative_size_avg'] > 2.0:
|
||
print("Unusually large candle!")
|
||
|
||
if ta_features['body_to_range_ratio'] < 0.1:
|
||
print("Doji-like candle (small body)")
|
||
```
|
||
|
||
---
|
||
|
||
## Integration with BaseDataInput
|
||
|
||
### Standard Mode (7,850 features)
|
||
|
||
```python
|
||
base_data = data_provider.build_base_data_input('ETH/USDT')
|
||
features = base_data.get_feature_vector(include_candle_ta=False)
|
||
# Returns: 7,850 features (backward compatible)
|
||
```
|
||
|
||
### Enhanced Mode (22,850 features)
|
||
|
||
```python
|
||
base_data = data_provider.build_base_data_input('ETH/USDT')
|
||
features = base_data.get_feature_vector(include_candle_ta=True)
|
||
# Returns: 22,850 features (includes 10 TA features per candle)
|
||
```
|
||
|
||
**10 TA Features Per Candle:**
|
||
1. `is_bullish`
|
||
2. `body_to_range_ratio`
|
||
3. `upper_wick_ratio`
|
||
4. `lower_wick_ratio`
|
||
5. `body_size_pct`
|
||
6. `total_range_pct`
|
||
7. `relative_size_avg`
|
||
8. `pattern_doji`
|
||
9. `pattern_hammer`
|
||
10. `pattern_shooting_star`
|
||
|
||
**Total Addition:**
|
||
- ETH: 300 frames × 4 timeframes × 10 features = 12,000 features
|
||
- BTC: 300 frames × 10 features = 3,000 features
|
||
- **Total**: 15,000 additional features
|
||
|
||
---
|
||
|
||
## Common Use Cases
|
||
|
||
### 1. Detect Reversal Patterns
|
||
|
||
```python
|
||
def scan_for_reversals(ohlcv_list: List[OHLCVBar]) -> List[tuple]:
|
||
"""Scan for potential reversal patterns"""
|
||
reversals = []
|
||
|
||
for i, bar in enumerate(ohlcv_list[-50:]):
|
||
pattern = bar.get_candle_pattern()
|
||
|
||
if pattern in ['hammer', 'shooting_star']:
|
||
reversals.append((i, bar.timestamp, pattern, bar.close))
|
||
|
||
return reversals
|
||
|
||
# Usage
|
||
reversals = scan_for_reversals(base_data.ohlcv_1m)
|
||
for idx, timestamp, pattern, price in reversals:
|
||
print(f"{timestamp}: {pattern} at ${price:.2f}")
|
||
```
|
||
|
||
### 2. Identify Momentum Candles
|
||
|
||
```python
|
||
def find_momentum_candles(ohlcv_list: List[OHLCVBar],
|
||
threshold: float = 2.0) -> List[OHLCVBar]:
|
||
"""Find unusually large candles indicating momentum"""
|
||
momentum_candles = []
|
||
|
||
for i in range(10, len(ohlcv_list)):
|
||
current = ohlcv_list[i]
|
||
reference = ohlcv_list[i-10:i]
|
||
|
||
relative_size = current.get_relative_size(reference, 'avg')
|
||
|
||
if relative_size > threshold:
|
||
momentum_candles.append(current)
|
||
|
||
return momentum_candles
|
||
|
||
# Usage
|
||
momentum = find_momentum_candles(base_data.ohlcv_1m, threshold=2.5)
|
||
print(f"Found {len(momentum)} momentum candles")
|
||
```
|
||
|
||
### 3. Analyze Candle Structure
|
||
|
||
```python
|
||
def analyze_candle_structure(bar: OHLCVBar) -> Dict[str, Any]:
|
||
"""Comprehensive candle analysis"""
|
||
return {
|
||
'direction': 'bullish' if bar.is_bullish else 'bearish',
|
||
'pattern': bar.get_candle_pattern(),
|
||
'body_dominance': bar.get_body_to_range_ratio(),
|
||
'upper_wick_dominance': bar.get_upper_wick_ratio(),
|
||
'lower_wick_dominance': bar.get_lower_wick_ratio(),
|
||
'interpretation': _interpret_structure(bar)
|
||
}
|
||
|
||
def _interpret_structure(bar: OHLCVBar) -> str:
|
||
"""Interpret candle structure"""
|
||
body_ratio = bar.get_body_to_range_ratio()
|
||
|
||
if body_ratio > 0.8:
|
||
return "Strong momentum"
|
||
elif body_ratio < 0.2:
|
||
return "Indecision/consolidation"
|
||
elif bar.get_upper_wick_ratio() > 0.5:
|
||
return "Rejection at highs"
|
||
elif bar.get_lower_wick_ratio() > 0.5:
|
||
return "Support at lows"
|
||
else:
|
||
return "Normal price action"
|
||
|
||
# Usage
|
||
current_bar = base_data.ohlcv_1m[-1]
|
||
analysis = analyze_candle_structure(current_bar)
|
||
print(f"Pattern: {analysis['pattern']}")
|
||
print(f"Interpretation: {analysis['interpretation']}")
|
||
```
|
||
|
||
### 4. Build Custom Features
|
||
|
||
```python
|
||
def extract_custom_candle_features(ohlcv_list: List[OHLCVBar],
|
||
window: int = 10) -> np.ndarray:
|
||
"""Extract custom candle features for ML model"""
|
||
features = []
|
||
|
||
for i in range(window, len(ohlcv_list)):
|
||
current = ohlcv_list[i]
|
||
reference = ohlcv_list[i-window:i]
|
||
|
||
# Get TA features
|
||
ta = current.get_ta_features(reference)
|
||
|
||
# Custom feature engineering
|
||
features.append([
|
||
ta['is_bullish'],
|
||
ta['body_to_range_ratio'],
|
||
ta['relative_size_avg'],
|
||
ta['pattern_doji'],
|
||
ta['pattern_hammer'],
|
||
ta['pattern_shooting_star'],
|
||
# Add more as needed
|
||
])
|
||
|
||
return np.array(features)
|
||
|
||
# Usage
|
||
custom_features = extract_custom_candle_features(base_data.ohlcv_1m)
|
||
print(f"Custom features shape: {custom_features.shape}")
|
||
```
|
||
|
||
---
|
||
|
||
## Performance Considerations
|
||
|
||
### Computation Time
|
||
|
||
| Operation | Time | Notes |
|
||
|-----------|------|-------|
|
||
| Property access (cached) | ~0.001 ms | Very fast |
|
||
| `get_candle_pattern()` | ~0.01 ms | Fast |
|
||
| `get_ta_features()` | ~0.1 ms | Moderate |
|
||
| Full feature vector (1500 candles) | ~150 ms | Can be optimized |
|
||
|
||
### Optimization Tips
|
||
|
||
#### 1. Cache TA Features in OHLCVBar
|
||
|
||
```python
|
||
# When creating OHLCVBar, pre-compute TA features
|
||
bar = OHLCVBar(...)
|
||
ta_features = bar.get_ta_features(reference_bars)
|
||
bar.indicators.update(ta_features) # Cache in indicators dict
|
||
```
|
||
|
||
#### 2. Batch Processing
|
||
|
||
```python
|
||
# Process all candles at once
|
||
def precompute_ta_features(ohlcv_list: List[OHLCVBar]):
|
||
"""Pre-compute TA features for all candles"""
|
||
for i in range(10, len(ohlcv_list)):
|
||
current = ohlcv_list[i]
|
||
reference = ohlcv_list[i-10:i]
|
||
ta = current.get_ta_features(reference)
|
||
current.indicators.update(ta)
|
||
```
|
||
|
||
#### 3. Lazy Evaluation
|
||
|
||
```python
|
||
# Only compute when needed
|
||
if model.requires_candle_ta:
|
||
features = base_data.get_feature_vector(include_candle_ta=True)
|
||
else:
|
||
features = base_data.get_feature_vector(include_candle_ta=False)
|
||
```
|
||
|
||
---
|
||
|
||
## Testing
|
||
|
||
### Unit Tests
|
||
|
||
```python
|
||
def test_candle_properties():
|
||
bar = OHLCVBar('ETH/USDT', datetime.now(), 2000, 2050, 1990, 2040, 1000, '1m')
|
||
assert bar.is_bullish == True
|
||
assert bar.body_size == 40.0
|
||
assert bar.total_range == 60.0
|
||
|
||
def test_pattern_recognition():
|
||
doji = OHLCVBar('ETH/USDT', datetime.now(), 2000, 2005, 1995, 2001, 100, '1m')
|
||
assert doji.get_candle_pattern() == 'doji'
|
||
|
||
def test_relative_sizing():
|
||
bars = [OHLCVBar('ETH/USDT', datetime.now(), 2000, 2010, 1990, 2005, 100, '1m') for _ in range(10)]
|
||
large = OHLCVBar('ETH/USDT', datetime.now(), 2000, 2060, 1980, 2055, 100, '1m')
|
||
assert large.get_relative_size(bars, 'avg') > 2.0
|
||
```
|
||
|
||
---
|
||
|
||
## Troubleshooting
|
||
|
||
### Issue: TA features all zeros
|
||
|
||
**Cause**: No reference bars provided to `get_ta_features()`
|
||
|
||
**Solution**:
|
||
```python
|
||
# Provide reference bars
|
||
reference_bars = ohlcv_list[-10:-1]
|
||
ta_features = current_bar.get_ta_features(reference_bars)
|
||
```
|
||
|
||
### Issue: Pattern always 'standard'
|
||
|
||
**Cause**: Candle doesn't meet specific pattern criteria
|
||
|
||
**Solution**: Check ratios manually
|
||
```python
|
||
print(f"Body ratio: {bar.get_body_to_range_ratio()}")
|
||
print(f"Upper wick: {bar.get_upper_wick_ratio()}")
|
||
print(f"Lower wick: {bar.get_lower_wick_ratio()}")
|
||
```
|
||
|
||
### Issue: Slow feature extraction
|
||
|
||
**Cause**: Computing TA features for many candles
|
||
|
||
**Solution**: Pre-compute and cache
|
||
```python
|
||
# Cache in data provider
|
||
for bar in ohlcv_list:
|
||
if 'ta_cached' not in bar.indicators:
|
||
ta = bar.get_ta_features(reference_bars)
|
||
bar.indicators.update(ta)
|
||
bar.indicators['ta_cached'] = True
|
||
```
|
||
|
||
---
|
||
|
||
## References
|
||
|
||
- **Implementation**: `core/data_models.py` - `OHLCVBar` class
|
||
- **Usage Guide**: `docs/BASE_DATA_INPUT_USAGE_AUDIT.md`
|
||
- **Specification**: `docs/BASE_DATA_INPUT_SPECIFICATION.md`
|
||
- **Integration**: `core/standardized_data_provider.py`
|