3.9 Enhance chart functionality with indicator management and data export features
- Updated `ChartBuilder` to support dynamic indicator integration, allowing users to specify overlay and subplot indicators for enhanced chart analysis. - Implemented a new `get_indicator_data` method in `MarketDataIntegrator` for fetching indicator data based on user configurations. - Added `create_export_controls` in `chart_controls.py` to facilitate data export options (CSV/JSON) for user analysis. - Enhanced error handling and logging throughout the chart and data analysis processes to improve reliability and user feedback. - Updated documentation to reflect new features and usage guidelines for indicator management and data export functionalities.
This commit is contained in:
@@ -181,4 +181,45 @@ def create_time_range_controls():
|
||||
'padding': '15px',
|
||||
'background-color': '#f0f8ff',
|
||||
'margin-bottom': '20px'
|
||||
})
|
||||
|
||||
|
||||
def create_export_controls():
|
||||
"""Create the data export control panel."""
|
||||
return html.Div([
|
||||
html.H5("💾 Data Export", style={'color': '#2c3e50', 'margin-bottom': '15px'}),
|
||||
html.Button(
|
||||
"Export to CSV",
|
||||
id="export-csv-btn",
|
||||
className="btn btn-primary",
|
||||
style={
|
||||
'background-color': '#28a745',
|
||||
'color': 'white',
|
||||
'border': 'none',
|
||||
'padding': '8px 16px',
|
||||
'border-radius': '4px',
|
||||
'cursor': 'pointer',
|
||||
'margin-right': '10px'
|
||||
}
|
||||
),
|
||||
html.Button(
|
||||
"Export to JSON",
|
||||
id="export-json-btn",
|
||||
className="btn btn-primary",
|
||||
style={
|
||||
'background-color': '#17a2b8',
|
||||
'color': 'white',
|
||||
'border': 'none',
|
||||
'padding': '8px 16px',
|
||||
'border-radius': '4px',
|
||||
'cursor': 'pointer'
|
||||
}
|
||||
),
|
||||
dcc.Download(id="download-chart-data")
|
||||
], style={
|
||||
'border': '1px solid #bdc3c7',
|
||||
'border-radius': '8px',
|
||||
'padding': '15px',
|
||||
'background-color': '#f8f9fa',
|
||||
'margin-bottom': '20px'
|
||||
})
|
||||
@@ -26,89 +26,54 @@ class VolumeAnalyzer:
|
||||
self.db_manager = DatabaseManager()
|
||||
self.db_manager.initialize()
|
||||
|
||||
def get_volume_statistics(self, symbol: str, timeframe: str = "1h", days_back: int = 7) -> Dict[str, Any]:
|
||||
"""Calculate comprehensive volume statistics."""
|
||||
def get_volume_statistics(self, df: pd.DataFrame) -> Dict[str, Any]:
|
||||
"""Calculate comprehensive volume statistics from a DataFrame."""
|
||||
try:
|
||||
# Fetch recent market data
|
||||
end_time = datetime.now(timezone.utc)
|
||||
start_time = end_time - timedelta(days=days_back)
|
||||
if df.empty or 'volume' not in df.columns:
|
||||
return {'error': 'DataFrame is empty or missing volume column'}
|
||||
|
||||
# Calculate volume statistics
|
||||
total_volume = df['volume'].sum()
|
||||
avg_volume = df['volume'].mean()
|
||||
volume_std = df['volume'].std()
|
||||
|
||||
with self.db_manager.get_session() as session:
|
||||
from sqlalchemy import text
|
||||
|
||||
query = text("""
|
||||
SELECT timestamp, open, high, low, close, volume, trades_count
|
||||
FROM market_data
|
||||
WHERE symbol = :symbol
|
||||
AND timeframe = :timeframe
|
||||
AND timestamp >= :start_time
|
||||
AND timestamp <= :end_time
|
||||
ORDER BY timestamp ASC
|
||||
""")
|
||||
|
||||
result = session.execute(query, {
|
||||
'symbol': symbol,
|
||||
'timeframe': timeframe,
|
||||
'start_time': start_time,
|
||||
'end_time': end_time
|
||||
})
|
||||
|
||||
candles = []
|
||||
for row in result:
|
||||
candles.append({
|
||||
'timestamp': row.timestamp,
|
||||
'open': float(row.open),
|
||||
'high': float(row.high),
|
||||
'low': float(row.low),
|
||||
'close': float(row.close),
|
||||
'volume': float(row.volume),
|
||||
'trades_count': int(row.trades_count) if row.trades_count else 0
|
||||
})
|
||||
|
||||
if not candles:
|
||||
return {'error': 'No data available'}
|
||||
|
||||
df = pd.DataFrame(candles)
|
||||
|
||||
# Calculate volume statistics
|
||||
total_volume = df['volume'].sum()
|
||||
avg_volume = df['volume'].mean()
|
||||
volume_std = df['volume'].std()
|
||||
|
||||
# Volume trend analysis
|
||||
recent_volume = df['volume'].tail(10).mean() # Last 10 periods
|
||||
older_volume = df['volume'].head(10).mean() # First 10 periods
|
||||
volume_trend = "Increasing" if recent_volume > older_volume else "Decreasing"
|
||||
|
||||
# High volume periods (above 2 standard deviations)
|
||||
high_volume_threshold = avg_volume + (2 * volume_std)
|
||||
high_volume_periods = len(df[df['volume'] > high_volume_threshold])
|
||||
|
||||
# Volume-Price correlation
|
||||
price_change = df['close'] - df['open']
|
||||
volume_price_corr = df['volume'].corr(price_change.abs())
|
||||
|
||||
# Average trade size (volume per trade)
|
||||
# Volume trend analysis
|
||||
recent_volume = df['volume'].tail(10).mean() # Last 10 periods
|
||||
older_volume = df['volume'].head(10).mean() # First 10 periods
|
||||
volume_trend = "Increasing" if recent_volume > older_volume else "Decreasing"
|
||||
|
||||
# High volume periods (above 2 standard deviations)
|
||||
high_volume_threshold = avg_volume + (2 * volume_std)
|
||||
high_volume_periods = len(df[df['volume'] > high_volume_threshold])
|
||||
|
||||
# Volume-Price correlation
|
||||
price_change = df['close'] - df['open']
|
||||
volume_price_corr = df['volume'].corr(price_change.abs())
|
||||
|
||||
# Average trade size (volume per trade)
|
||||
if 'trades_count' in df.columns:
|
||||
df['avg_trade_size'] = df['volume'] / df['trades_count'].replace(0, 1)
|
||||
avg_trade_size = df['avg_trade_size'].mean()
|
||||
|
||||
return {
|
||||
'total_volume': total_volume,
|
||||
'avg_volume': avg_volume,
|
||||
'volume_std': volume_std,
|
||||
'volume_trend': volume_trend,
|
||||
'high_volume_periods': high_volume_periods,
|
||||
'volume_price_correlation': volume_price_corr,
|
||||
'avg_trade_size': avg_trade_size,
|
||||
'max_volume': df['volume'].max(),
|
||||
'min_volume': df['volume'].min(),
|
||||
'volume_percentiles': {
|
||||
'25th': df['volume'].quantile(0.25),
|
||||
'50th': df['volume'].quantile(0.50),
|
||||
'75th': df['volume'].quantile(0.75),
|
||||
'95th': df['volume'].quantile(0.95)
|
||||
}
|
||||
else:
|
||||
avg_trade_size = None # Not available
|
||||
|
||||
return {
|
||||
'total_volume': total_volume,
|
||||
'avg_volume': avg_volume,
|
||||
'volume_std': volume_std,
|
||||
'volume_trend': volume_trend,
|
||||
'high_volume_periods': high_volume_periods,
|
||||
'volume_price_correlation': volume_price_corr,
|
||||
'avg_trade_size': avg_trade_size,
|
||||
'max_volume': df['volume'].max(),
|
||||
'min_volume': df['volume'].min(),
|
||||
'volume_percentiles': {
|
||||
'25th': df['volume'].quantile(0.25),
|
||||
'50th': df['volume'].quantile(0.50),
|
||||
'75th': df['volume'].quantile(0.75),
|
||||
'95th': df['volume'].quantile(0.95)
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Volume analysis error: {e}")
|
||||
@@ -122,120 +87,83 @@ class PriceMovementAnalyzer:
|
||||
self.db_manager = DatabaseManager()
|
||||
self.db_manager.initialize()
|
||||
|
||||
def get_price_movement_statistics(self, symbol: str, timeframe: str = "1h", days_back: int = 7) -> Dict[str, Any]:
|
||||
"""Calculate comprehensive price movement statistics."""
|
||||
def get_price_movement_statistics(self, df: pd.DataFrame) -> Dict[str, Any]:
|
||||
"""Calculate comprehensive price movement statistics from a DataFrame."""
|
||||
try:
|
||||
# Fetch recent market data
|
||||
end_time = datetime.now(timezone.utc)
|
||||
start_time = end_time - timedelta(days=days_back)
|
||||
if df.empty or not all(col in df.columns for col in ['open', 'high', 'low', 'close']):
|
||||
return {'error': 'DataFrame is empty or missing required price columns'}
|
||||
|
||||
# Basic price statistics
|
||||
current_price = df['close'].iloc[-1]
|
||||
period_start_price = df['open'].iloc[0]
|
||||
period_return = ((current_price - period_start_price) / period_start_price) * 100
|
||||
|
||||
with self.db_manager.get_session() as session:
|
||||
from sqlalchemy import text
|
||||
|
||||
query = text("""
|
||||
SELECT timestamp, open, high, low, close, volume
|
||||
FROM market_data
|
||||
WHERE symbol = :symbol
|
||||
AND timeframe = :timeframe
|
||||
AND timestamp >= :start_time
|
||||
AND timestamp <= :end_time
|
||||
ORDER BY timestamp ASC
|
||||
""")
|
||||
|
||||
result = session.execute(query, {
|
||||
'symbol': symbol,
|
||||
'timeframe': timeframe,
|
||||
'start_time': start_time,
|
||||
'end_time': end_time
|
||||
})
|
||||
|
||||
candles = []
|
||||
for row in result:
|
||||
candles.append({
|
||||
'timestamp': row.timestamp,
|
||||
'open': float(row.open),
|
||||
'high': float(row.high),
|
||||
'low': float(row.low),
|
||||
'close': float(row.close),
|
||||
'volume': float(row.volume)
|
||||
})
|
||||
|
||||
if not candles:
|
||||
return {'error': 'No data available'}
|
||||
|
||||
df = pd.DataFrame(candles)
|
||||
|
||||
# Basic price statistics
|
||||
current_price = df['close'].iloc[-1]
|
||||
period_start_price = df['open'].iloc[0]
|
||||
period_return = ((current_price - period_start_price) / period_start_price) * 100
|
||||
|
||||
# Daily returns (percentage changes)
|
||||
df['returns'] = df['close'].pct_change() * 100
|
||||
df['returns'] = df['returns'].fillna(0)
|
||||
|
||||
# Volatility metrics
|
||||
volatility = df['returns'].std()
|
||||
avg_return = df['returns'].mean()
|
||||
|
||||
# Price range analysis
|
||||
df['range'] = df['high'] - df['low']
|
||||
df['range_pct'] = (df['range'] / df['open']) * 100
|
||||
avg_range_pct = df['range_pct'].mean()
|
||||
|
||||
# Directional analysis
|
||||
bullish_periods = len(df[df['close'] > df['open']])
|
||||
bearish_periods = len(df[df['close'] < df['open']])
|
||||
neutral_periods = len(df[df['close'] == df['open']])
|
||||
|
||||
total_periods = len(df)
|
||||
bullish_ratio = (bullish_periods / total_periods) * 100 if total_periods > 0 else 0
|
||||
|
||||
# Price extremes
|
||||
period_high = df['high'].max()
|
||||
period_low = df['low'].min()
|
||||
|
||||
# Momentum indicators
|
||||
# Simple momentum (current vs N periods ago)
|
||||
momentum_periods = min(10, len(df) - 1)
|
||||
if momentum_periods > 0:
|
||||
momentum = ((current_price - df['close'].iloc[-momentum_periods-1]) / df['close'].iloc[-momentum_periods-1]) * 100
|
||||
else:
|
||||
momentum = 0
|
||||
|
||||
# Trend strength (linear regression slope)
|
||||
if len(df) > 2:
|
||||
x = np.arange(len(df))
|
||||
slope, _ = np.polyfit(x, df['close'], 1)
|
||||
trend_strength = slope / df['close'].mean() * 100 # Normalize by average price
|
||||
else:
|
||||
trend_strength = 0
|
||||
|
||||
return {
|
||||
'current_price': current_price,
|
||||
'period_return': period_return,
|
||||
'volatility': volatility,
|
||||
'avg_return': avg_return,
|
||||
'avg_range_pct': avg_range_pct,
|
||||
'bullish_periods': bullish_periods,
|
||||
'bearish_periods': bearish_periods,
|
||||
'neutral_periods': neutral_periods,
|
||||
'bullish_ratio': bullish_ratio,
|
||||
'period_high': period_high,
|
||||
'period_low': period_low,
|
||||
'momentum': momentum,
|
||||
'trend_strength': trend_strength,
|
||||
'return_percentiles': {
|
||||
'5th': df['returns'].quantile(0.05),
|
||||
'25th': df['returns'].quantile(0.25),
|
||||
'75th': df['returns'].quantile(0.75),
|
||||
'95th': df['returns'].quantile(0.95)
|
||||
},
|
||||
'max_gain': df['returns'].max(),
|
||||
'max_loss': df['returns'].min(),
|
||||
'positive_returns': len(df[df['returns'] > 0]),
|
||||
'negative_returns': len(df[df['returns'] < 0])
|
||||
}
|
||||
# Daily returns (percentage changes)
|
||||
df['returns'] = df['close'].pct_change() * 100
|
||||
df['returns'] = df['returns'].fillna(0)
|
||||
|
||||
# Volatility metrics
|
||||
volatility = df['returns'].std()
|
||||
avg_return = df['returns'].mean()
|
||||
|
||||
# Price range analysis
|
||||
df['range'] = df['high'] - df['low']
|
||||
df['range_pct'] = (df['range'] / df['open']) * 100
|
||||
avg_range_pct = df['range_pct'].mean()
|
||||
|
||||
# Directional analysis
|
||||
bullish_periods = len(df[df['close'] > df['open']])
|
||||
bearish_periods = len(df[df['close'] < df['open']])
|
||||
neutral_periods = len(df[df['close'] == df['open']])
|
||||
|
||||
total_periods = len(df)
|
||||
bullish_ratio = (bullish_periods / total_periods) * 100 if total_periods > 0 else 0
|
||||
|
||||
# Price extremes
|
||||
period_high = df['high'].max()
|
||||
period_low = df['low'].min()
|
||||
|
||||
# Momentum indicators
|
||||
# Simple momentum (current vs N periods ago)
|
||||
momentum_periods = min(10, len(df) - 1)
|
||||
if momentum_periods > 0:
|
||||
momentum = ((current_price - df['close'].iloc[-momentum_periods-1]) / df['close'].iloc[-momentum_periods-1]) * 100
|
||||
else:
|
||||
momentum = 0
|
||||
|
||||
# Trend strength (linear regression slope)
|
||||
if len(df) > 2:
|
||||
x = np.arange(len(df))
|
||||
slope, _ = np.polyfit(x, df['close'], 1)
|
||||
trend_strength = slope / df['close'].mean() * 100 # Normalize by average price
|
||||
else:
|
||||
trend_strength = 0
|
||||
|
||||
return {
|
||||
'current_price': current_price,
|
||||
'period_return': period_return,
|
||||
'volatility': volatility,
|
||||
'avg_return': avg_return,
|
||||
'avg_range_pct': avg_range_pct,
|
||||
'bullish_periods': bullish_periods,
|
||||
'bearish_periods': bearish_periods,
|
||||
'neutral_periods': neutral_periods,
|
||||
'bullish_ratio': bullish_ratio,
|
||||
'period_high': period_high,
|
||||
'period_low': period_low,
|
||||
'momentum': momentum,
|
||||
'trend_strength': trend_strength,
|
||||
'return_percentiles': {
|
||||
'5th': df['returns'].quantile(0.05),
|
||||
'25th': df['returns'].quantile(0.25),
|
||||
'75th': df['returns'].quantile(0.75),
|
||||
'95th': df['returns'].quantile(0.95)
|
||||
},
|
||||
'max_gain': df['returns'].max(),
|
||||
'max_loss': df['returns'].min(),
|
||||
'positive_returns': len(df[df['returns'] > 0]),
|
||||
'negative_returns': len(df[df['returns'] < 0])
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Price movement analysis error: {e}")
|
||||
@@ -718,4 +646,39 @@ def create_price_stats_display(stats: Dict[str, Any]) -> html.Div:
|
||||
])
|
||||
], p="md", shadow="sm")
|
||||
|
||||
], cols=3, spacing="md", style={'margin-top': '20px'})
|
||||
], cols=3, spacing="md", style={'margin-top': '20px'})
|
||||
|
||||
|
||||
def get_market_statistics(df: pd.DataFrame, symbol: str, timeframe: str) -> html.Div:
|
||||
"""
|
||||
Generate a comprehensive market statistics component from a DataFrame.
|
||||
"""
|
||||
try:
|
||||
volume_analyzer = VolumeAnalyzer()
|
||||
price_analyzer = PriceMovementAnalyzer()
|
||||
|
||||
volume_stats = volume_analyzer.get_volume_statistics(df)
|
||||
price_stats = price_analyzer.get_price_movement_statistics(df)
|
||||
|
||||
if 'error' in volume_stats or 'error' in price_stats:
|
||||
error_msg = volume_stats.get('error') or price_stats.get('error')
|
||||
return html.Div(f"Error generating statistics: {error_msg}", style={'color': 'red'})
|
||||
|
||||
# Time range for display
|
||||
start_date = df['timestamp'].min().strftime('%Y-%m-%d %H:%M')
|
||||
end_date = df['timestamp'].max().strftime('%Y-%m-%d %H:%M')
|
||||
days_back = (df['timestamp'].max() - df['timestamp'].min()).days
|
||||
time_status = f"📅 Analysis Range: {start_date} to {end_date} (~{days_back} days)"
|
||||
|
||||
return html.Div([
|
||||
html.H3("📊 Enhanced Market Statistics"),
|
||||
html.P(
|
||||
time_status,
|
||||
style={'font-weight': 'bold', 'margin-bottom': '15px', 'color': '#4A4A4A', 'text-align': 'center', 'font-size': '1.1em'}
|
||||
),
|
||||
create_price_stats_display(price_stats),
|
||||
create_volume_stats_display(volume_stats)
|
||||
])
|
||||
except Exception as e:
|
||||
logger.error(f"Error in get_market_statistics: {e}", exc_info=True)
|
||||
return html.Div(f"Error generating statistics display: {e}", style={'color': 'red'})
|
||||
Reference in New Issue
Block a user