378 lines
15 KiB
Python
378 lines
15 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Chart Data Provider Core Module
|
|
|
|
This module handles all chart data preparation and market data simulation,
|
|
separated from the web UI layer.
|
|
"""
|
|
|
|
import logging
|
|
import numpy as np
|
|
import pandas as pd
|
|
from datetime import datetime, timedelta
|
|
from typing import Dict, List, Any, Optional, Tuple
|
|
import plotly.graph_objects as go
|
|
from plotly.subplots import make_subplots
|
|
|
|
from .cnn_pivot_predictor import CNNPivotPredictor, PivotPrediction
|
|
from .pivot_detector import WilliamsPivotDetector, DetectedPivot
|
|
|
|
# Setup logging with ASCII-only output
|
|
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
|
logger = logging.getLogger(__name__)
|
|
|
|
class ChartDataProvider:
|
|
"""Core chart data provider with market simulation and chart preparation"""
|
|
|
|
def __init__(self, config: Optional[Dict] = None):
|
|
self.config = config or self._default_config()
|
|
|
|
# Initialize core components
|
|
self.cnn_predictor = CNNPivotPredictor()
|
|
self.pivot_detector = WilliamsPivotDetector()
|
|
|
|
# Market data
|
|
self.current_price = 3500.0 # Starting ETH price
|
|
self.price_history: List[Dict] = []
|
|
|
|
# Initialize with sample data
|
|
self._generate_initial_data()
|
|
|
|
logger.info("Chart Data Provider initialized")
|
|
|
|
def _default_config(self) -> Dict:
|
|
"""Default configuration"""
|
|
return {
|
|
'initial_history_hours': 2,
|
|
'price_volatility': 5.0,
|
|
'volume_range': (100, 1000),
|
|
'chart_height': 600,
|
|
'subplots': True
|
|
}
|
|
|
|
def _generate_initial_data(self) -> None:
|
|
"""Generate initial price history for demonstration"""
|
|
base_time = datetime.now() - timedelta(hours=self.config['initial_history_hours'])
|
|
|
|
for i in range(120): # 2 hours of minute data
|
|
# Simulate realistic price movement
|
|
change = np.random.normal(0, self.config['price_volatility'])
|
|
self.current_price += change
|
|
|
|
# Ensure price doesn't go negative
|
|
self.current_price = max(self.current_price, 100.0)
|
|
|
|
timestamp = base_time + timedelta(minutes=i)
|
|
|
|
# Generate OHLC data
|
|
open_price = self.current_price - np.random.uniform(-2, 2)
|
|
high_price = max(open_price, self.current_price) + np.random.uniform(0, 8)
|
|
low_price = min(open_price, self.current_price) - np.random.uniform(0, 8)
|
|
close_price = self.current_price
|
|
volume = np.random.uniform(*self.config['volume_range'])
|
|
|
|
candle = {
|
|
'timestamp': timestamp,
|
|
'open': open_price,
|
|
'high': high_price,
|
|
'low': low_price,
|
|
'close': close_price,
|
|
'volume': volume
|
|
}
|
|
|
|
self.price_history.append(candle)
|
|
|
|
logger.info(f"Generated {len(self.price_history)} initial price candles")
|
|
|
|
def simulate_price_update(self) -> Dict:
|
|
"""Simulate real-time price update"""
|
|
try:
|
|
# Generate new price movement
|
|
change = np.random.normal(0, self.config['price_volatility'])
|
|
self.current_price += change
|
|
self.current_price = max(self.current_price, 100.0)
|
|
|
|
# Create new candle
|
|
timestamp = datetime.now()
|
|
open_price = self.price_history[-1]['close'] if self.price_history else self.current_price
|
|
high_price = max(open_price, self.current_price) + np.random.uniform(0, 5)
|
|
low_price = min(open_price, self.current_price) - np.random.uniform(0, 5)
|
|
close_price = self.current_price
|
|
volume = np.random.uniform(*self.config['volume_range'])
|
|
|
|
new_candle = {
|
|
'timestamp': timestamp,
|
|
'open': open_price,
|
|
'high': high_price,
|
|
'low': low_price,
|
|
'close': close_price,
|
|
'volume': volume
|
|
}
|
|
|
|
self.price_history.append(new_candle)
|
|
|
|
# Keep only last 200 candles to prevent memory growth
|
|
if len(self.price_history) > 200:
|
|
self.price_history = self.price_history[-200:]
|
|
|
|
return new_candle
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error simulating price update: {e}")
|
|
return {}
|
|
|
|
def get_market_data_df(self) -> pd.DataFrame:
|
|
"""Convert price history to pandas DataFrame"""
|
|
try:
|
|
if not self.price_history:
|
|
return pd.DataFrame()
|
|
|
|
df = pd.DataFrame(self.price_history)
|
|
df['timestamp'] = pd.to_datetime(df['timestamp'])
|
|
return df
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error creating DataFrame: {e}")
|
|
return pd.DataFrame()
|
|
|
|
def update_predictions_and_pivots(self) -> Tuple[List[PivotPrediction], List[DetectedPivot]]:
|
|
"""Update CNN predictions and detect new pivots"""
|
|
try:
|
|
market_df = self.get_market_data_df()
|
|
|
|
if market_df.empty:
|
|
return [], []
|
|
|
|
# Update CNN predictions
|
|
predictions = self.cnn_predictor.update_predictions(market_df, self.current_price)
|
|
|
|
# Detect pivots
|
|
detected_pivots = self.pivot_detector.detect_pivots(market_df)
|
|
|
|
# Capture training data if new pivots are found
|
|
for pivot in detected_pivots:
|
|
if pivot.confirmed:
|
|
actual_pivot = type('ActualPivot', (), {
|
|
'type': pivot.type,
|
|
'price': pivot.price,
|
|
'timestamp': pivot.timestamp,
|
|
'strength': pivot.strength
|
|
})()
|
|
self.cnn_predictor.capture_training_data(actual_pivot)
|
|
|
|
return predictions, detected_pivots
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error updating predictions and pivots: {e}")
|
|
return [], []
|
|
|
|
def create_price_chart(self) -> go.Figure:
|
|
"""Create main price chart with candlesticks and volume"""
|
|
try:
|
|
market_df = self.get_market_data_df()
|
|
|
|
if market_df.empty:
|
|
return go.Figure()
|
|
|
|
# Create subplots
|
|
if self.config['subplots']:
|
|
fig = make_subplots(
|
|
rows=2, cols=1,
|
|
shared_xaxes=True,
|
|
vertical_spacing=0.05,
|
|
subplot_titles=('Price', 'Volume'),
|
|
row_width=[0.7, 0.3]
|
|
)
|
|
else:
|
|
fig = go.Figure()
|
|
|
|
# Add candlestick chart
|
|
candlestick = go.Candlestick(
|
|
x=market_df['timestamp'],
|
|
open=market_df['open'],
|
|
high=market_df['high'],
|
|
low=market_df['low'],
|
|
close=market_df['close'],
|
|
name='ETH/USDT',
|
|
increasing_line_color='#00ff88',
|
|
decreasing_line_color='#ff4444'
|
|
)
|
|
|
|
if self.config['subplots']:
|
|
fig.add_trace(candlestick, row=1, col=1)
|
|
else:
|
|
fig.add_trace(candlestick)
|
|
|
|
# Add volume bars if subplots enabled
|
|
if self.config['subplots']:
|
|
volume_colors = ['#00ff88' if close >= open else '#ff4444'
|
|
for close, open in zip(market_df['close'], market_df['open'])]
|
|
|
|
volume_bar = go.Bar(
|
|
x=market_df['timestamp'],
|
|
y=market_df['volume'],
|
|
name='Volume',
|
|
marker_color=volume_colors,
|
|
opacity=0.7
|
|
)
|
|
fig.add_trace(volume_bar, row=2, col=1)
|
|
|
|
# Update layout
|
|
fig.update_layout(
|
|
title='ETH/USDT Price Chart with CNN Predictions',
|
|
xaxis_title='Time',
|
|
yaxis_title='Price (USDT)',
|
|
height=self.config['chart_height'],
|
|
showlegend=True,
|
|
xaxis_rangeslider_visible=False
|
|
)
|
|
|
|
return fig
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error creating price chart: {e}")
|
|
return go.Figure()
|
|
|
|
def add_cnn_predictions_to_chart(self, fig: go.Figure, predictions: List[PivotPrediction]) -> go.Figure:
|
|
"""Add CNN predictions as hollow circles to the chart"""
|
|
try:
|
|
if not predictions:
|
|
return fig
|
|
|
|
# Separate HIGH and LOW predictions
|
|
high_predictions = [p for p in predictions if p.type == 'HIGH']
|
|
low_predictions = [p for p in predictions if p.type == 'LOW']
|
|
|
|
# Add HIGH predictions (red hollow circles)
|
|
if high_predictions:
|
|
high_x = [p.timestamp for p in high_predictions]
|
|
high_y = [p.predicted_price for p in high_predictions]
|
|
high_sizes = [max(8, min(20, p.confidence * 25)) for p in high_predictions]
|
|
high_text = [f"HIGH Prediction<br>Price: ${p.predicted_price:.2f}<br>Confidence: {p.confidence:.1%}<br>Level: {p.level}"
|
|
for p in high_predictions]
|
|
|
|
fig.add_trace(go.Scatter(
|
|
x=high_x,
|
|
y=high_y,
|
|
mode='markers',
|
|
marker=dict(
|
|
symbol='circle-open',
|
|
size=high_sizes,
|
|
color='red',
|
|
line=dict(width=2)
|
|
),
|
|
name='CNN HIGH Predictions',
|
|
text=high_text,
|
|
hovertemplate='%{text}<extra></extra>'
|
|
))
|
|
|
|
# Add LOW predictions (green hollow circles)
|
|
if low_predictions:
|
|
low_x = [p.timestamp for p in low_predictions]
|
|
low_y = [p.predicted_price for p in low_predictions]
|
|
low_sizes = [max(8, min(20, p.confidence * 25)) for p in low_predictions]
|
|
low_text = [f"LOW Prediction<br>Price: ${p.predicted_price:.2f}<br>Confidence: {p.confidence:.1%}<br>Level: {p.level}"
|
|
for p in low_predictions]
|
|
|
|
fig.add_trace(go.Scatter(
|
|
x=low_x,
|
|
y=low_y,
|
|
mode='markers',
|
|
marker=dict(
|
|
symbol='circle-open',
|
|
size=low_sizes,
|
|
color='green',
|
|
line=dict(width=2)
|
|
),
|
|
name='CNN LOW Predictions',
|
|
text=low_text,
|
|
hovertemplate='%{text}<extra></extra>'
|
|
))
|
|
|
|
return fig
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error adding CNN predictions to chart: {e}")
|
|
return fig
|
|
|
|
def add_actual_pivots_to_chart(self, fig: go.Figure, pivots: List[DetectedPivot]) -> go.Figure:
|
|
"""Add actual detected pivots as solid triangles to the chart"""
|
|
try:
|
|
if not pivots:
|
|
return fig
|
|
|
|
# Separate HIGH and LOW pivots
|
|
high_pivots = [p for p in pivots if p.type == 'HIGH']
|
|
low_pivots = [p for p in pivots if p.type == 'LOW']
|
|
|
|
# Add HIGH pivots (red triangles pointing down)
|
|
if high_pivots:
|
|
high_x = [p.timestamp for p in high_pivots]
|
|
high_y = [p.price for p in high_pivots]
|
|
high_sizes = [max(10, min(25, p.strength * 5)) for p in high_pivots]
|
|
high_text = [f"HIGH Pivot<br>Price: ${p.price:.2f}<br>Strength: {p.strength}<br>Confirmed: {p.confirmed}"
|
|
for p in high_pivots]
|
|
|
|
fig.add_trace(go.Scatter(
|
|
x=high_x,
|
|
y=high_y,
|
|
mode='markers',
|
|
marker=dict(
|
|
symbol='triangle-down',
|
|
size=high_sizes,
|
|
color='darkred',
|
|
line=dict(width=1, color='white')
|
|
),
|
|
name='Actual HIGH Pivots',
|
|
text=high_text,
|
|
hovertemplate='%{text}<extra></extra>'
|
|
))
|
|
|
|
# Add LOW pivots (green triangles pointing up)
|
|
if low_pivots:
|
|
low_x = [p.timestamp for p in low_pivots]
|
|
low_y = [p.price for p in low_pivots]
|
|
low_sizes = [max(10, min(25, p.strength * 5)) for p in low_pivots]
|
|
low_text = [f"LOW Pivot<br>Price: ${p.price:.2f}<br>Strength: {p.strength}<br>Confirmed: {p.confirmed}"
|
|
for p in low_pivots]
|
|
|
|
fig.add_trace(go.Scatter(
|
|
x=low_x,
|
|
y=low_y,
|
|
mode='markers',
|
|
marker=dict(
|
|
symbol='triangle-up',
|
|
size=low_sizes,
|
|
color='darkgreen',
|
|
line=dict(width=1, color='white')
|
|
),
|
|
name='Actual LOW Pivots',
|
|
text=low_text,
|
|
hovertemplate='%{text}<extra></extra>'
|
|
))
|
|
|
|
return fig
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error adding actual pivots to chart: {e}")
|
|
return fig
|
|
|
|
def get_current_status(self) -> Dict:
|
|
"""Get current system status for dashboard display"""
|
|
try:
|
|
prediction_stats = self.cnn_predictor.get_prediction_stats()
|
|
pivot_stats = self.pivot_detector.get_statistics()
|
|
training_stats = self.cnn_predictor.get_training_stats()
|
|
|
|
return {
|
|
'current_price': self.current_price,
|
|
'total_candles': len(self.price_history),
|
|
'last_update': datetime.now().strftime('%H:%M:%S'),
|
|
'predictions': prediction_stats,
|
|
'pivots': pivot_stats,
|
|
'training': training_stats
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error getting current status: {e}")
|
|
return {} |