275 lines
8.9 KiB
Python
275 lines
8.9 KiB
Python
"""
|
|
Main Market Analysis Engine - Orchestrates all analysis components
|
|
Pure API mode - no Redis dependency
|
|
"""
|
|
import logging
|
|
from typing import Dict, Any
|
|
import pandas as pd
|
|
|
|
from .data_reader import MarketDataReader
|
|
from .indicators import TechnicalIndicators
|
|
from .market_structure import MarketStructureAnalyzer
|
|
from .orderflow import OrderFlowAnalyzer
|
|
from .llm_context import LLMContextBuilder
|
|
from .config import config
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class MarketAnalysisEngine:
|
|
"""
|
|
Main analysis engine that orchestrates all market analysis components
|
|
"""
|
|
|
|
def __init__(self, symbol: str = None):
|
|
"""
|
|
Initialize analysis engine
|
|
|
|
Args:
|
|
symbol: Trading symbol (default from config)
|
|
"""
|
|
self.symbol = symbol or config.SYMBOL
|
|
self.data_reader = MarketDataReader(symbol=self.symbol)
|
|
self.llm_builder = LLMContextBuilder()
|
|
|
|
def analyze_current_market(
|
|
self, timeframe: str = '5m', symbol: str = None
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
Perform complete market analysis for current state
|
|
|
|
Args:
|
|
timeframe: Primary timeframe for analysis (5m, 15m, 1h, 4h)
|
|
symbol: Trading symbol (uses default if None)
|
|
|
|
Returns:
|
|
Complete analysis dictionary
|
|
"""
|
|
if symbol is None:
|
|
symbol = self.symbol
|
|
|
|
try:
|
|
logger.info(f"Starting market analysis for {symbol} on {timeframe}")
|
|
|
|
# Fetch data
|
|
df = self._fetch_and_prepare_data(timeframe)
|
|
|
|
if df.empty:
|
|
logger.error(f"No data available for {timeframe}")
|
|
return {'error': 'No data available'}
|
|
|
|
# Get current price
|
|
current_price = float(df.iloc[-1]['close'])
|
|
|
|
# Fetch order book
|
|
depth_data = self.data_reader.read_latest_depth()
|
|
|
|
# Perform analysis components
|
|
analysis = {
|
|
'symbol': symbol,
|
|
'timeframe': timeframe,
|
|
'current_price': round(current_price, 2),
|
|
'timestamp': df.index[-1].isoformat(),
|
|
'trend_analysis': MarketStructureAnalyzer.identify_trend(df),
|
|
'support_resistance': MarketStructureAnalyzer.find_support_resistance(
|
|
df, current_price
|
|
),
|
|
'momentum': MarketStructureAnalyzer.calculate_momentum(df),
|
|
'indicators': TechnicalIndicators.get_latest_indicators(df),
|
|
'price_changes': TechnicalIndicators.calculate_price_changes(df),
|
|
}
|
|
|
|
# Add order flow if depth data available
|
|
if depth_data:
|
|
analysis['orderflow'] = {
|
|
'imbalance': OrderFlowAnalyzer.analyze_orderbook_imbalance(depth_data),
|
|
'liquidity': OrderFlowAnalyzer.analyze_liquidity_depth(
|
|
depth_data, current_price
|
|
),
|
|
'large_orders': OrderFlowAnalyzer.detect_large_orders(depth_data),
|
|
}
|
|
|
|
# Calculate order flow strength
|
|
analysis['orderflow']['strength'] = OrderFlowAnalyzer.calculate_orderflow_strength(
|
|
analysis['orderflow']['imbalance'],
|
|
analysis['orderflow']['large_orders'],
|
|
analysis['orderflow']['liquidity'],
|
|
)
|
|
|
|
# Add breakout detection
|
|
analysis['breakout'] = MarketStructureAnalyzer.detect_breakout(
|
|
df, analysis['support_resistance']
|
|
)
|
|
|
|
# Add volatility and volume analysis for LLM gate
|
|
analysis['volatility_analysis'] = {
|
|
'atr': analysis['indicators'].get('atr', 0),
|
|
'atr_pct': (analysis['indicators'].get('atr', 0) / current_price * 100) if current_price > 0 else 0,
|
|
'bb_status': self._get_bb_status(df),
|
|
}
|
|
|
|
analysis['volume_analysis'] = {
|
|
'current_volume': float(df.iloc[-1]['volume']),
|
|
'avg_volume': float(df['volume'].tail(20).mean()),
|
|
'volume_status': self._get_volume_status(df),
|
|
'obv_trend': self._get_obv_trend(df),
|
|
}
|
|
|
|
# Add metadata for LLM gate
|
|
analysis['metadata'] = {
|
|
'candle_count': len(df),
|
|
'timeframe': timeframe,
|
|
'analysis_timestamp': df.index[-1].isoformat(),
|
|
}
|
|
|
|
logger.info(
|
|
f"Analysis complete: trend={analysis['trend_analysis']['direction']}, "
|
|
f"rsi={analysis['momentum']['rsi']}, "
|
|
f"candles={len(df)}"
|
|
)
|
|
|
|
return analysis
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error in market analysis: {e}", exc_info=True)
|
|
return {'error': str(e)}
|
|
|
|
def get_llm_context(self, format: str = 'full') -> Dict[str, Any]:
|
|
"""
|
|
Get market context formatted for LLM consumption
|
|
|
|
Args:
|
|
format: 'full' or 'simplified'
|
|
|
|
Returns:
|
|
LLM-ready context dictionary
|
|
"""
|
|
if format == 'simplified':
|
|
return self.llm_builder.get_simplified_context()
|
|
else:
|
|
return self.llm_builder.build_full_context()
|
|
|
|
def get_multi_timeframe_analysis(self) -> Dict[str, Any]:
|
|
"""
|
|
Get analysis across all timeframes
|
|
|
|
Returns:
|
|
Dict mapping timeframe to analysis
|
|
"""
|
|
timeframes = ['5m', '15m', '1h', '4h']
|
|
results = {}
|
|
|
|
for tf in timeframes:
|
|
analysis = self.analyze_current_market(timeframe=tf)
|
|
if 'error' not in analysis:
|
|
results[tf] = {
|
|
'trend': analysis['trend_analysis'].get('direction', 'unknown'),
|
|
'strength': analysis['trend_analysis'].get('strength', 'weak'),
|
|
'rsi': analysis['momentum'].get('rsi', 50),
|
|
'adx': analysis['trend_analysis'].get('adx', 0),
|
|
}
|
|
|
|
return results
|
|
|
|
def _fetch_and_prepare_data(self, timeframe: str) -> pd.DataFrame:
|
|
"""
|
|
Fetch data and add all technical indicators
|
|
|
|
Args:
|
|
timeframe: Timeframe to fetch (5m, 15m, 1h, 4h)
|
|
|
|
Returns:
|
|
DataFrame with OHLCV and indicators
|
|
"""
|
|
# Fetch data directly from API
|
|
df = self.data_reader.fetch_klines(interval=timeframe, limit=config.LOOKBACK_PERIODS)
|
|
|
|
if df.empty:
|
|
return df
|
|
|
|
# Add all technical indicators
|
|
df = TechnicalIndicators.add_all_indicators(df)
|
|
|
|
return df
|
|
|
|
def check_data_availability(self) -> Dict[str, Any]:
|
|
"""
|
|
Check what data is available from API
|
|
|
|
Returns:
|
|
Dict with data availability status
|
|
"""
|
|
status = {
|
|
'klines': {},
|
|
'depth': False,
|
|
'trades': False,
|
|
}
|
|
|
|
# Check kline data for each timeframe
|
|
for tf in ['5m', '15m', '1h', '4h']:
|
|
df = self.data_reader.fetch_klines(interval=tf, limit=1)
|
|
status['klines'][tf] = {
|
|
'available': not df.empty,
|
|
'latest': df.index[-1].isoformat() if not df.empty else None,
|
|
}
|
|
|
|
# Check depth
|
|
depth = self.data_reader.read_latest_depth()
|
|
status['depth'] = depth is not None
|
|
|
|
# Check trades
|
|
trades = self.data_reader.read_recent_trades(count=1)
|
|
status['trades'] = len(trades) > 0
|
|
|
|
return status
|
|
|
|
def _get_bb_status(self, df: pd.DataFrame) -> str:
|
|
"""Get Bollinger Bands status"""
|
|
if 'bb_upper' not in df.columns or 'bb_lower' not in df.columns:
|
|
return 'unknown'
|
|
|
|
last_close = df.iloc[-1]['close']
|
|
bb_upper = df.iloc[-1]['bb_upper']
|
|
bb_lower = df.iloc[-1]['bb_lower']
|
|
bb_middle = df.iloc[-1].get('bb_middle', (bb_upper + bb_lower) / 2)
|
|
|
|
if last_close > bb_upper:
|
|
return 'overbought'
|
|
elif last_close < bb_lower:
|
|
return 'oversold'
|
|
elif last_close > bb_middle:
|
|
return 'upper_half'
|
|
else:
|
|
return 'lower_half'
|
|
|
|
def _get_volume_status(self, df: pd.DataFrame) -> str:
|
|
"""Get volume status compared to average"""
|
|
if len(df) < 20:
|
|
return 'unknown'
|
|
|
|
current_volume = df.iloc[-1]['volume']
|
|
avg_volume = df['volume'].tail(20).mean()
|
|
|
|
if current_volume > avg_volume * 1.5:
|
|
return 'high'
|
|
elif current_volume > avg_volume * 0.8:
|
|
return 'normal'
|
|
else:
|
|
return 'low'
|
|
|
|
def _get_obv_trend(self, df: pd.DataFrame) -> str:
|
|
"""Get OBV (On-Balance Volume) trend"""
|
|
if 'obv' not in df.columns or len(df) < 20:
|
|
return 'unknown'
|
|
|
|
obv_current = df.iloc[-1]['obv']
|
|
obv_sma = df['obv'].tail(20).mean()
|
|
|
|
if obv_current > obv_sma * 1.05:
|
|
return 'bullish'
|
|
elif obv_current < obv_sma * 0.95:
|
|
return 'bearish'
|
|
else:
|
|
return 'neutral'
|