fix 1s 1m chart less candles ;
fix vertical zoom
This commit is contained in:
@@ -2430,13 +2430,14 @@ class RealTrainingAdapter:
|
|||||||
if not hasattr(self, 'inference_sessions'):
|
if not hasattr(self, 'inference_sessions'):
|
||||||
self.inference_sessions = {}
|
self.inference_sessions = {}
|
||||||
|
|
||||||
# Create inference session
|
# Create inference session with position tracking
|
||||||
self.inference_sessions[inference_id] = {
|
self.inference_sessions[inference_id] = {
|
||||||
'model_name': model_name,
|
'model_name': model_name,
|
||||||
'symbol': symbol,
|
'symbol': symbol,
|
||||||
'status': 'running',
|
'status': 'running',
|
||||||
'start_time': time.time(),
|
'start_time': time.time(),
|
||||||
'signals': [],
|
'signals': [], # All signals (including rejected ones)
|
||||||
|
'executed_trades': [], # Only executed trades (open/close positions)
|
||||||
'stop_flag': False,
|
'stop_flag': False,
|
||||||
'live_training_enabled': enable_live_training,
|
'live_training_enabled': enable_live_training,
|
||||||
'train_every_candle': train_every_candle,
|
'train_every_candle': train_every_candle,
|
||||||
@@ -2447,7 +2448,13 @@ class RealTrainingAdapter:
|
|||||||
'loss': 0.0,
|
'loss': 0.0,
|
||||||
'steps': 0
|
'steps': 0
|
||||||
},
|
},
|
||||||
'last_candle_time': None
|
'last_candle_time': None,
|
||||||
|
# Position tracking
|
||||||
|
'position': None, # {'type': 'long/short', 'entry_price': float, 'entry_time': str, 'entry_id': str}
|
||||||
|
'total_pnl': 0.0,
|
||||||
|
'win_count': 0,
|
||||||
|
'loss_count': 0,
|
||||||
|
'total_trades': 0
|
||||||
}
|
}
|
||||||
|
|
||||||
training_mode = "per-candle" if train_every_candle else ("pivot-based" if enable_live_training else "inference-only")
|
training_mode = "per-candle" if train_every_candle else ("pivot-based" if enable_live_training else "inference-only")
|
||||||
@@ -3211,13 +3218,39 @@ class RealTrainingAdapter:
|
|||||||
'predicted_candle': prediction.get('predicted_candle')
|
'predicted_candle': prediction.get('predicted_candle')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Store signal (all signals, including rejected ones)
|
||||||
session['signals'].append(signal)
|
session['signals'].append(signal)
|
||||||
|
|
||||||
# Keep only last 100 signals
|
# Keep only last 100 signals
|
||||||
if len(session['signals']) > 100:
|
if len(session['signals']) > 100:
|
||||||
session['signals'] = session['signals'][-100:]
|
session['signals'] = session['signals'][-100:]
|
||||||
|
|
||||||
logger.info(f"Live Signal: {signal['action']} @ {signal['price']:.2f} (conf: {signal['confidence']:.2f})")
|
# Execute trade logic (only if confidence is high enough and position logic allows)
|
||||||
|
executed_trade = self._execute_realtime_trade(session, signal, current_price)
|
||||||
|
|
||||||
|
if executed_trade:
|
||||||
|
logger.info(f"Live Trade EXECUTED: {executed_trade['action']} @ {executed_trade['price']:.2f} (conf: {signal['confidence']:.2f})")
|
||||||
|
|
||||||
|
# Send executed trade to frontend via WebSocket
|
||||||
|
if hasattr(self, 'socketio') and self.socketio:
|
||||||
|
self.socketio.emit('executed_trade', {
|
||||||
|
'trade': executed_trade,
|
||||||
|
'position_state': {
|
||||||
|
'has_position': session['position'] is not None,
|
||||||
|
'position_type': session['position']['type'] if session['position'] else None,
|
||||||
|
'entry_price': session['position']['entry_price'] if session['position'] else None,
|
||||||
|
'unrealized_pnl': self._calculate_unrealized_pnl(session, current_price) if session['position'] else 0.0
|
||||||
|
},
|
||||||
|
'session_metrics': {
|
||||||
|
'total_pnl': session['total_pnl'],
|
||||||
|
'total_trades': session['total_trades'],
|
||||||
|
'win_count': session['win_count'],
|
||||||
|
'loss_count': session['loss_count'],
|
||||||
|
'win_rate': (session['win_count'] / session['total_trades'] * 100) if session['total_trades'] > 0 else 0
|
||||||
|
}
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
logger.info(f"Live Signal (NOT executed): {signal['action']} @ {signal['price']:.2f} (conf: {signal['confidence']:.2f}) - {self._get_rejection_reason(session, signal)}")
|
||||||
|
|
||||||
# Store prediction for visualization
|
# Store prediction for visualization
|
||||||
if self.orchestrator and hasattr(self.orchestrator, 'store_transformer_prediction'):
|
if self.orchestrator and hasattr(self.orchestrator, 'store_transformer_prediction'):
|
||||||
@@ -3250,3 +3283,173 @@ class RealTrainingAdapter:
|
|||||||
logger.error(f"Fatal error in inference loop: {e}")
|
logger.error(f"Fatal error in inference loop: {e}")
|
||||||
session['status'] = 'error'
|
session['status'] = 'error'
|
||||||
session['error'] = str(e)
|
session['error'] = str(e)
|
||||||
|
|
||||||
|
def _execute_realtime_trade(self, session: Dict, signal: Dict, current_price: float) -> Optional[Dict]:
|
||||||
|
"""
|
||||||
|
Execute trade based on signal, respecting position management rules
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
1. Only execute if confidence >= 0.6
|
||||||
|
2. Only open new position if no position is currently open
|
||||||
|
3. Close position on opposite signal
|
||||||
|
4. Track all executed trades for visualization
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict with executed trade info, or None if signal was rejected
|
||||||
|
"""
|
||||||
|
action = signal['action']
|
||||||
|
confidence = signal['confidence']
|
||||||
|
timestamp = signal['timestamp']
|
||||||
|
|
||||||
|
# Rule 1: Confidence threshold
|
||||||
|
if confidence < 0.6:
|
||||||
|
return None # Rejected: low confidence
|
||||||
|
|
||||||
|
# Rule 2 & 3: Position management
|
||||||
|
position = session.get('position')
|
||||||
|
|
||||||
|
if action == 'BUY':
|
||||||
|
if position is None:
|
||||||
|
# Open long position
|
||||||
|
trade_id = str(uuid.uuid4())[:8]
|
||||||
|
session['position'] = {
|
||||||
|
'type': 'long',
|
||||||
|
'entry_price': current_price,
|
||||||
|
'entry_time': timestamp,
|
||||||
|
'entry_id': trade_id,
|
||||||
|
'signal_confidence': confidence
|
||||||
|
}
|
||||||
|
|
||||||
|
executed_trade = {
|
||||||
|
'trade_id': trade_id,
|
||||||
|
'action': 'OPEN_LONG',
|
||||||
|
'price': current_price,
|
||||||
|
'timestamp': timestamp,
|
||||||
|
'confidence': confidence
|
||||||
|
}
|
||||||
|
|
||||||
|
session['executed_trades'].append(executed_trade)
|
||||||
|
return executed_trade
|
||||||
|
|
||||||
|
elif position['type'] == 'short':
|
||||||
|
# Close short position
|
||||||
|
entry_price = position['entry_price']
|
||||||
|
pnl = entry_price - current_price # Short profit
|
||||||
|
pnl_pct = (pnl / entry_price) * 100
|
||||||
|
|
||||||
|
executed_trade = {
|
||||||
|
'trade_id': position['entry_id'],
|
||||||
|
'action': 'CLOSE_SHORT',
|
||||||
|
'price': current_price,
|
||||||
|
'timestamp': timestamp,
|
||||||
|
'confidence': confidence,
|
||||||
|
'entry_price': entry_price,
|
||||||
|
'entry_time': position['entry_time'],
|
||||||
|
'pnl': pnl,
|
||||||
|
'pnl_pct': pnl_pct
|
||||||
|
}
|
||||||
|
|
||||||
|
# Update session metrics
|
||||||
|
session['total_pnl'] += pnl
|
||||||
|
session['total_trades'] += 1
|
||||||
|
if pnl > 0:
|
||||||
|
session['win_count'] += 1
|
||||||
|
else:
|
||||||
|
session['loss_count'] += 1
|
||||||
|
|
||||||
|
session['position'] = None
|
||||||
|
session['executed_trades'].append(executed_trade)
|
||||||
|
|
||||||
|
logger.info(f"Position CLOSED: SHORT @ {current_price:.2f}, PnL=${pnl:.2f} ({pnl_pct:+.2f}%)")
|
||||||
|
return executed_trade
|
||||||
|
|
||||||
|
elif action == 'SELL':
|
||||||
|
if position is None:
|
||||||
|
# Open short position
|
||||||
|
trade_id = str(uuid.uuid4())[:8]
|
||||||
|
session['position'] = {
|
||||||
|
'type': 'short',
|
||||||
|
'entry_price': current_price,
|
||||||
|
'entry_time': timestamp,
|
||||||
|
'entry_id': trade_id,
|
||||||
|
'signal_confidence': confidence
|
||||||
|
}
|
||||||
|
|
||||||
|
executed_trade = {
|
||||||
|
'trade_id': trade_id,
|
||||||
|
'action': 'OPEN_SHORT',
|
||||||
|
'price': current_price,
|
||||||
|
'timestamp': timestamp,
|
||||||
|
'confidence': confidence
|
||||||
|
}
|
||||||
|
|
||||||
|
session['executed_trades'].append(executed_trade)
|
||||||
|
return executed_trade
|
||||||
|
|
||||||
|
elif position['type'] == 'long':
|
||||||
|
# Close long position
|
||||||
|
entry_price = position['entry_price']
|
||||||
|
pnl = current_price - entry_price # Long profit
|
||||||
|
pnl_pct = (pnl / entry_price) * 100
|
||||||
|
|
||||||
|
executed_trade = {
|
||||||
|
'trade_id': position['entry_id'],
|
||||||
|
'action': 'CLOSE_LONG',
|
||||||
|
'price': current_price,
|
||||||
|
'timestamp': timestamp,
|
||||||
|
'confidence': confidence,
|
||||||
|
'entry_price': entry_price,
|
||||||
|
'entry_time': position['entry_time'],
|
||||||
|
'pnl': pnl,
|
||||||
|
'pnl_pct': pnl_pct
|
||||||
|
}
|
||||||
|
|
||||||
|
# Update session metrics
|
||||||
|
session['total_pnl'] += pnl
|
||||||
|
session['total_trades'] += 1
|
||||||
|
if pnl > 0:
|
||||||
|
session['win_count'] += 1
|
||||||
|
else:
|
||||||
|
session['loss_count'] += 1
|
||||||
|
|
||||||
|
session['position'] = None
|
||||||
|
session['executed_trades'].append(executed_trade)
|
||||||
|
|
||||||
|
logger.info(f"Position CLOSED: LONG @ {current_price:.2f}, PnL=${pnl:.2f} ({pnl_pct:+.2f}%)")
|
||||||
|
return executed_trade
|
||||||
|
|
||||||
|
# HOLD or position already open in same direction
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _get_rejection_reason(self, session: Dict, signal: Dict) -> str:
|
||||||
|
"""Get reason why a signal was not executed"""
|
||||||
|
action = signal['action']
|
||||||
|
confidence = signal['confidence']
|
||||||
|
position = session.get('position')
|
||||||
|
|
||||||
|
if confidence < 0.6:
|
||||||
|
return f"Low confidence ({confidence:.2f} < 0.6)"
|
||||||
|
|
||||||
|
if action == 'HOLD':
|
||||||
|
return "HOLD signal (no trade)"
|
||||||
|
|
||||||
|
if position:
|
||||||
|
if action == 'BUY' and position['type'] == 'long':
|
||||||
|
return "Already in LONG position"
|
||||||
|
elif action == 'SELL' and position['type'] == 'short':
|
||||||
|
return "Already in SHORT position"
|
||||||
|
|
||||||
|
return "Unknown reason"
|
||||||
|
|
||||||
|
def _calculate_unrealized_pnl(self, session: Dict, current_price: float) -> float:
|
||||||
|
"""Calculate unrealized PnL for open position"""
|
||||||
|
position = session.get('position')
|
||||||
|
if not position or not current_price:
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
entry_price = position['entry_price']
|
||||||
|
|
||||||
|
if position['type'] == 'long':
|
||||||
|
return ((current_price - entry_price) / entry_price) * 100 # Percentage
|
||||||
|
else: # short
|
||||||
|
return ((entry_price - current_price) / entry_price) * 100 # Percentage
|
||||||
|
|||||||
@@ -538,6 +538,9 @@ class AnnotationDashboard:
|
|||||||
engineio_logger=False
|
engineio_logger=False
|
||||||
)
|
)
|
||||||
self.has_socketio = True
|
self.has_socketio = True
|
||||||
|
# Pass socketio to training adapter for live trade updates
|
||||||
|
if self.training_adapter:
|
||||||
|
self.training_adapter.socketio = self.socketio
|
||||||
logger.info("SocketIO initialized for real-time updates")
|
logger.info("SocketIO initialized for real-time updates")
|
||||||
except ImportError:
|
except ImportError:
|
||||||
self.socketio = None
|
self.socketio = None
|
||||||
@@ -586,6 +589,8 @@ class AnnotationDashboard:
|
|||||||
self.annotation_manager = AnnotationManager()
|
self.annotation_manager = AnnotationManager()
|
||||||
# Use REAL training adapter - NO SIMULATION!
|
# Use REAL training adapter - NO SIMULATION!
|
||||||
self.training_adapter = RealTrainingAdapter(None, self.data_provider)
|
self.training_adapter = RealTrainingAdapter(None, self.data_provider)
|
||||||
|
# Pass socketio to training adapter for live trade updates
|
||||||
|
self.training_adapter.socketio = None # Will be set after socketio initialization
|
||||||
# Backtest runner for replaying visible chart with predictions
|
# Backtest runner for replaying visible chart with predictions
|
||||||
self.backtest_runner = BacktestRunner()
|
self.backtest_runner = BacktestRunner()
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ class ChartManager {
|
|||||||
this.lastPredictionHash = null; // Track if predictions actually changed
|
this.lastPredictionHash = null; // Track if predictions actually changed
|
||||||
this.ghostCandleHistory = {}; // Store ghost candles per timeframe (max 50 each)
|
this.ghostCandleHistory = {}; // Store ghost candles per timeframe (max 50 each)
|
||||||
this.maxGhostCandles = 150; // Maximum number of ghost candles to keep
|
this.maxGhostCandles = 150; // Maximum number of ghost candles to keep
|
||||||
|
this.modelAccuracyMetrics = {}; // Track overall model accuracy per timeframe
|
||||||
|
|
||||||
// Helper to ensure all timestamps are in UTC
|
// Helper to ensure all timestamps are in UTC
|
||||||
this.normalizeTimestamp = (timestamp) => {
|
this.normalizeTimestamp = (timestamp) => {
|
||||||
@@ -81,7 +82,8 @@ class ChartManager {
|
|||||||
*/
|
*/
|
||||||
async updateChart(timeframe) {
|
async updateChart(timeframe) {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/chart-data?timeframe=${timeframe}&limit=1000`);
|
// Use consistent candle count across all timeframes (2500 for sufficient training context)
|
||||||
|
const response = await fetch(`/api/chart-data?timeframe=${timeframe}&limit=2500`);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`HTTP ${response.status}`);
|
throw new Error(`HTTP ${response.status}`);
|
||||||
}
|
}
|
||||||
@@ -109,7 +111,7 @@ class ChartManager {
|
|||||||
Plotly.restyle(plotId, candlestickUpdate, [0]);
|
Plotly.restyle(plotId, candlestickUpdate, [0]);
|
||||||
Plotly.restyle(plotId, volumeUpdate, [1]);
|
Plotly.restyle(plotId, volumeUpdate, [1]);
|
||||||
|
|
||||||
console.log(`Updated ${timeframe} chart at ${new Date().toLocaleTimeString()}`);
|
console.log(`Updated ${timeframe} chart with ${chartData.timestamps.length} candles at ${new Date().toLocaleTimeString()}`);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error updating ${timeframe} chart:`, error);
|
console.error(`Error updating ${timeframe} chart:`, error);
|
||||||
@@ -546,9 +548,9 @@ class ChartManager {
|
|||||||
plot_bgcolor: '#1f2937',
|
plot_bgcolor: '#1f2937',
|
||||||
paper_bgcolor: '#1f2937',
|
paper_bgcolor: '#1f2937',
|
||||||
font: { color: '#f8f9fa', size: 11 },
|
font: { color: '#f8f9fa', size: 11 },
|
||||||
margin: { l: 60, r: 20, t: 10, b: 40 },
|
margin: { l: 80, r: 20, t: 10, b: 40 }, // Increased left margin for better Y-axis drag area
|
||||||
hovermode: 'x unified',
|
hovermode: 'x unified',
|
||||||
dragmode: 'pan',
|
dragmode: 'pan', // Pan mode for main chart area (horizontal panning)
|
||||||
// Performance optimizations
|
// Performance optimizations
|
||||||
autosize: true,
|
autosize: true,
|
||||||
staticPlot: false
|
staticPlot: false
|
||||||
@@ -562,7 +564,7 @@ class ChartManager {
|
|||||||
scrollZoom: true,
|
scrollZoom: true,
|
||||||
// Performance optimizations
|
// Performance optimizations
|
||||||
doubleClick: 'reset', // Enable double-click reset
|
doubleClick: 'reset', // Enable double-click reset
|
||||||
showAxisDragHandles: true, // Enable axis dragging
|
showAxisDragHandles: true, // Enable axis dragging - allows Y-axis vertical zoom when dragging on Y-axis area
|
||||||
showAxisRangeEntryBoxes: false
|
showAxisRangeEntryBoxes: false
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -711,6 +713,10 @@ class ChartManager {
|
|||||||
Plotly.newPlot(plotId, chartData, layout, config).then(() => {
|
Plotly.newPlot(plotId, chartData, layout, config).then(() => {
|
||||||
// Optimize rendering after initial plot
|
// Optimize rendering after initial plot
|
||||||
plotElement._fullLayout._replotting = false;
|
plotElement._fullLayout._replotting = false;
|
||||||
|
|
||||||
|
// Add custom handler for Y-axis vertical zoom
|
||||||
|
// When user drags on Y-axis area (left side), enable vertical zoom
|
||||||
|
this._setupYAxisZoom(plotElement, plotId, timeframe);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Store chart reference
|
// Store chart reference
|
||||||
@@ -777,6 +783,134 @@ class ChartManager {
|
|||||||
|
|
||||||
console.log(`Chart created for ${timeframe} with ${data.timestamps.length} candles`);
|
console.log(`Chart created for ${timeframe} with ${data.timestamps.length} candles`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup Y-axis vertical zoom handler
|
||||||
|
* Allows vertical zoom when dragging on the Y-axis area (left side of chart)
|
||||||
|
*/
|
||||||
|
_setupYAxisZoom(plotElement, plotId, timeframe) {
|
||||||
|
let isDraggingYAxis = false;
|
||||||
|
let dragStartY = null;
|
||||||
|
let dragStartRange = null;
|
||||||
|
const Y_AXIS_MARGIN = 80; // Left margin width in pixels
|
||||||
|
|
||||||
|
// Mouse down handler - check if on Y-axis area
|
||||||
|
const handleMouseDown = (event) => {
|
||||||
|
const rect = plotElement.getBoundingClientRect();
|
||||||
|
const x = event.clientX - rect.left;
|
||||||
|
|
||||||
|
// Check if click is in Y-axis area (left margin)
|
||||||
|
if (x < Y_AXIS_MARGIN) {
|
||||||
|
isDraggingYAxis = true;
|
||||||
|
dragStartY = event.clientY;
|
||||||
|
|
||||||
|
// Get current Y-axis range
|
||||||
|
const layout = plotElement._fullLayout;
|
||||||
|
if (layout && layout.yaxis && layout.yaxis.range) {
|
||||||
|
dragStartRange = {
|
||||||
|
min: layout.yaxis.range[0],
|
||||||
|
max: layout.yaxis.range[1],
|
||||||
|
range: layout.yaxis.range[1] - layout.yaxis.range[0]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Change cursor to indicate vertical zoom
|
||||||
|
plotElement.style.cursor = 'ns-resize';
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mouse move handler - handle vertical zoom and cursor update
|
||||||
|
const handleMouseMove = (event) => {
|
||||||
|
const rect = plotElement.getBoundingClientRect();
|
||||||
|
const x = event.clientX - rect.left;
|
||||||
|
|
||||||
|
// Update cursor when hovering over Y-axis area (only if not dragging)
|
||||||
|
if (!isDraggingYAxis) {
|
||||||
|
if (x < Y_AXIS_MARGIN) {
|
||||||
|
plotElement.style.cursor = 'ns-resize';
|
||||||
|
} else {
|
||||||
|
plotElement.style.cursor = 'default';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle vertical zoom drag
|
||||||
|
if (isDraggingYAxis && dragStartY !== null && dragStartRange !== null) {
|
||||||
|
const deltaY = dragStartY - event.clientY; // Negative = zoom in (drag up), Positive = zoom out (drag down)
|
||||||
|
const zoomFactor = 1 + (deltaY / 200); // Adjust sensitivity (200px = 2x zoom)
|
||||||
|
|
||||||
|
// Clamp zoom factor to reasonable limits
|
||||||
|
const clampedZoom = Math.max(0.1, Math.min(10, zoomFactor));
|
||||||
|
|
||||||
|
// Calculate new range centered on current view
|
||||||
|
const center = (dragStartRange.min + dragStartRange.max) / 2;
|
||||||
|
const newRange = dragStartRange.range * clampedZoom;
|
||||||
|
const newMin = center - newRange / 2;
|
||||||
|
const newMax = center + newRange / 2;
|
||||||
|
|
||||||
|
// Update Y-axis range
|
||||||
|
Plotly.relayout(plotId, {
|
||||||
|
'yaxis.range': [newMin, newMax]
|
||||||
|
});
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mouse up handler - end drag (use document level to catch even if mouse leaves element)
|
||||||
|
const handleMouseUp = () => {
|
||||||
|
if (isDraggingYAxis) {
|
||||||
|
isDraggingYAxis = false;
|
||||||
|
dragStartY = null;
|
||||||
|
dragStartRange = null;
|
||||||
|
plotElement.style.cursor = 'default';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mouse leave handler - reset cursor but keep dragging state
|
||||||
|
const handleMouseLeave = () => {
|
||||||
|
if (!isDraggingYAxis) {
|
||||||
|
plotElement.style.cursor = 'default';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Attach event listeners
|
||||||
|
// Use element-level for mousedown and mouseleave (hover detection)
|
||||||
|
plotElement.addEventListener('mousedown', handleMouseDown);
|
||||||
|
plotElement.addEventListener('mouseleave', handleMouseLeave);
|
||||||
|
plotElement.addEventListener('mousemove', handleMouseMove);
|
||||||
|
|
||||||
|
// Use document-level for mousemove and mouseup during drag (works even if mouse leaves element)
|
||||||
|
const handleDocumentMouseMove = (event) => {
|
||||||
|
if (isDraggingYAxis) {
|
||||||
|
handleMouseMove(event);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDocumentMouseUp = () => {
|
||||||
|
if (isDraggingYAxis) {
|
||||||
|
handleMouseUp();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('mousemove', handleDocumentMouseMove);
|
||||||
|
document.addEventListener('mouseup', handleDocumentMouseUp);
|
||||||
|
|
||||||
|
// Store handlers for cleanup if needed
|
||||||
|
if (!plotElement._yAxisZoomHandlers) {
|
||||||
|
plotElement._yAxisZoomHandlers = {
|
||||||
|
mousedown: handleMouseDown,
|
||||||
|
mousemove: handleMouseMove,
|
||||||
|
mouseleave: handleMouseLeave,
|
||||||
|
documentMousemove: handleDocumentMouseMove,
|
||||||
|
documentMouseup: handleDocumentMouseUp
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[${timeframe}] Y-axis vertical zoom enabled - drag on left side (Y-axis area) to zoom vertically`);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle chart click for annotation
|
* Handle chart click for annotation
|
||||||
@@ -2081,6 +2215,12 @@ class ChartManager {
|
|||||||
};
|
};
|
||||||
|
|
||||||
validatedCount++;
|
validatedCount++;
|
||||||
|
|
||||||
|
// Calculate prediction range vs actual range to diagnose "wide" predictions
|
||||||
|
const predRange = predCandle[1] - predCandle[2]; // High - Low
|
||||||
|
const actualRange = actualCandle[1] - actualCandle[2];
|
||||||
|
const rangeRatio = predRange / actualRange; // >1 means prediction is wider
|
||||||
|
|
||||||
console.log(`[${timeframe}] Prediction validated (#${validatedCount}):`, {
|
console.log(`[${timeframe}] Prediction validated (#${validatedCount}):`, {
|
||||||
timestamp: prediction.timestamp,
|
timestamp: prediction.timestamp,
|
||||||
matchedTo: timestamps[matchIdx],
|
matchedTo: timestamps[matchIdx],
|
||||||
@@ -2090,34 +2230,144 @@ class ChartManager {
|
|||||||
volumeError: pctErrors.volume.toFixed(2) + '%',
|
volumeError: pctErrors.volume.toFixed(2) + '%',
|
||||||
direction: directionCorrect ? '✓' : '✗',
|
direction: directionCorrect ? '✓' : '✗',
|
||||||
timeDiff: Math.abs(predTime - new Date(timestamps[matchIdx]).getTime()) + 'ms',
|
timeDiff: Math.abs(predTime - new Date(timestamps[matchIdx]).getTime()) + 'ms',
|
||||||
|
rangeAnalysis: {
|
||||||
|
predictedRange: predRange.toFixed(2),
|
||||||
|
actualRange: actualRange.toFixed(2),
|
||||||
|
rangeRatio: rangeRatio.toFixed(2) + 'x', // Shows if prediction is wider
|
||||||
|
isWider: rangeRatio > 1.2 ? 'YES (too wide)' : rangeRatio < 0.8 ? 'NO (too narrow)' : 'OK'
|
||||||
|
},
|
||||||
predicted: {
|
predicted: {
|
||||||
O: predCandle[0].toFixed(2),
|
O: predCandle[0].toFixed(2),
|
||||||
H: predCandle[1].toFixed(2),
|
H: predCandle[1].toFixed(2),
|
||||||
L: predCandle[2].toFixed(2),
|
L: predCandle[2].toFixed(2),
|
||||||
C: predCandle[3].toFixed(2),
|
C: predCandle[3].toFixed(2),
|
||||||
V: predCandle[4].toFixed(2)
|
V: predCandle[4].toFixed(2),
|
||||||
|
Range: predRange.toFixed(2)
|
||||||
},
|
},
|
||||||
actual: {
|
actual: {
|
||||||
O: actualCandle[0].toFixed(2),
|
O: actualCandle[0].toFixed(2),
|
||||||
H: actualCandle[1].toFixed(2),
|
H: actualCandle[1].toFixed(2),
|
||||||
L: actualCandle[2].toFixed(2),
|
L: actualCandle[2].toFixed(2),
|
||||||
C: actualCandle[3].toFixed(2),
|
C: actualCandle[3].toFixed(2),
|
||||||
V: actualCandle[4].toFixed(2)
|
V: actualCandle[4].toFixed(2),
|
||||||
|
Range: actualRange.toFixed(2)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Send metrics to backend for training feedback
|
// Send metrics to backend for training feedback
|
||||||
this._sendPredictionMetrics(timeframe, prediction);
|
this._sendPredictionMetrics(timeframe, prediction);
|
||||||
|
|
||||||
|
// Update overall model accuracy metrics
|
||||||
|
this._updateModelAccuracyMetrics(timeframe, accuracy, directionCorrect);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Summary log
|
// Summary log
|
||||||
if (validatedCount > 0) {
|
if (validatedCount > 0) {
|
||||||
const totalPending = predictions.filter(p => !p.accuracy).length;
|
const totalPending = predictions.filter(p => !p.accuracy).length;
|
||||||
|
const avgAccuracy = this.modelAccuracyMetrics[timeframe]?.avgAccuracy || 0;
|
||||||
|
const directionAccuracy = this.modelAccuracyMetrics[timeframe]?.directionAccuracy || 0;
|
||||||
console.log(`[${timeframe}] Validated ${validatedCount} predictions, ${totalPending} still pending`);
|
console.log(`[${timeframe}] Validated ${validatedCount} predictions, ${totalPending} still pending`);
|
||||||
|
console.log(`[${timeframe}] Model Accuracy: ${avgAccuracy.toFixed(1)}% avg, ${directionAccuracy.toFixed(1)}% direction`);
|
||||||
|
|
||||||
|
// CRITICAL: Re-render predictions to show updated accuracy in tooltips
|
||||||
|
// Trigger a refresh of prediction display
|
||||||
|
this._refreshPredictionDisplay(timeframe);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update overall model accuracy metrics
|
||||||
|
*/
|
||||||
|
_updateModelAccuracyMetrics(timeframe, accuracy, directionCorrect) {
|
||||||
|
if (!this.modelAccuracyMetrics[timeframe]) {
|
||||||
|
this.modelAccuracyMetrics[timeframe] = {
|
||||||
|
accuracies: [],
|
||||||
|
directionCorrect: [],
|
||||||
|
totalValidated: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const metrics = this.modelAccuracyMetrics[timeframe];
|
||||||
|
metrics.accuracies.push(accuracy);
|
||||||
|
metrics.directionCorrect.push(directionCorrect);
|
||||||
|
metrics.totalValidated++;
|
||||||
|
|
||||||
|
// Calculate averages
|
||||||
|
metrics.avgAccuracy = metrics.accuracies.reduce((a, b) => a + b, 0) / metrics.accuracies.length;
|
||||||
|
metrics.directionAccuracy = (metrics.directionCorrect.filter(c => c).length / metrics.directionCorrect.length) * 100;
|
||||||
|
|
||||||
|
// Keep only last 100 validations for rolling average
|
||||||
|
if (metrics.accuracies.length > 100) {
|
||||||
|
metrics.accuracies = metrics.accuracies.slice(-100);
|
||||||
|
metrics.directionCorrect = metrics.directionCorrect.slice(-100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh prediction display to show updated accuracy
|
||||||
|
*/
|
||||||
|
_refreshPredictionDisplay(timeframe) {
|
||||||
|
const chart = this.charts[timeframe];
|
||||||
|
if (!chart) return;
|
||||||
|
|
||||||
|
const plotId = chart.plotId;
|
||||||
|
const plotElement = document.getElementById(plotId);
|
||||||
|
if (!plotElement) return;
|
||||||
|
|
||||||
|
// Get current predictions from history
|
||||||
|
if (!this.ghostCandleHistory[timeframe] || this.ghostCandleHistory[timeframe].length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rebuild prediction traces with updated accuracy
|
||||||
|
const predictionTraces = [];
|
||||||
|
for (const ghost of this.ghostCandleHistory[timeframe]) {
|
||||||
|
this._addGhostCandlePrediction(ghost.candle, timeframe, predictionTraces, ghost.targetTime, ghost.accuracy);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove old prediction traces
|
||||||
|
const currentTraces = plotElement.data.length;
|
||||||
|
const indicesToRemove = [];
|
||||||
|
for (let i = currentTraces - 1; i >= 0; i--) {
|
||||||
|
const name = plotElement.data[i].name;
|
||||||
|
if (name === 'Ghost Prediction' || name === 'Shadow Prediction') {
|
||||||
|
indicesToRemove.push(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (indicesToRemove.length > 0) {
|
||||||
|
Plotly.deleteTraces(plotId, indicesToRemove);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add updated traces
|
||||||
|
if (predictionTraces.length > 0) {
|
||||||
|
Plotly.addTraces(plotId, predictionTraces);
|
||||||
|
console.log(`[${timeframe}] Refreshed ${predictionTraces.length} prediction candles with updated accuracy`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get overall model accuracy metrics for a timeframe
|
||||||
|
*/
|
||||||
|
getModelAccuracyMetrics(timeframe) {
|
||||||
|
if (!this.modelAccuracyMetrics[timeframe]) {
|
||||||
|
return {
|
||||||
|
avgAccuracy: 0,
|
||||||
|
directionAccuracy: 0,
|
||||||
|
totalValidated: 0,
|
||||||
|
recentAccuracies: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const metrics = this.modelAccuracyMetrics[timeframe];
|
||||||
|
return {
|
||||||
|
avgAccuracy: metrics.avgAccuracy || 0,
|
||||||
|
directionAccuracy: metrics.directionAccuracy || 0,
|
||||||
|
totalValidated: metrics.totalValidated || 0,
|
||||||
|
recentAccuracies: metrics.accuracies.slice(-10) || [] // Last 10 accuracies
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send prediction accuracy metrics to backend for training feedback
|
* Send prediction accuracy metrics to backend for training feedback
|
||||||
*/
|
*/
|
||||||
@@ -2814,6 +3064,169 @@ class ChartManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add executed trade marker to chart
|
||||||
|
* Shows entry/exit points, PnL, and position lines
|
||||||
|
*/
|
||||||
|
addExecutedTradeMarker(trade, positionState) {
|
||||||
|
try {
|
||||||
|
if (!trade || !trade.timestamp) return;
|
||||||
|
|
||||||
|
// Find which timeframe to display on (prefer 1m, fallback to 1s)
|
||||||
|
const timeframe = this.timeframes.includes('1m') ? '1m' : (this.timeframes.includes('1s') ? '1s' : null);
|
||||||
|
if (!timeframe) return;
|
||||||
|
|
||||||
|
const chart = this.charts[timeframe];
|
||||||
|
if (!chart) return;
|
||||||
|
|
||||||
|
const plotId = chart.plotId;
|
||||||
|
const plotElement = document.getElementById(plotId);
|
||||||
|
if (!plotElement) return;
|
||||||
|
|
||||||
|
// Parse timestamp
|
||||||
|
const timestamp = new Date(trade.timestamp);
|
||||||
|
const year = timestamp.getUTCFullYear();
|
||||||
|
const month = String(timestamp.getUTCMonth() + 1).padStart(2, '0');
|
||||||
|
const day = String(timestamp.getUTCDate()).padStart(2, '0');
|
||||||
|
const hours = String(timestamp.getUTCHours()).padStart(2, '0');
|
||||||
|
const minutes = String(timestamp.getUTCMinutes()).padStart(2, '0');
|
||||||
|
const seconds = String(timestamp.getUTCSeconds()).padStart(2, '0');
|
||||||
|
const formattedTimestamp = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
||||||
|
|
||||||
|
// Determine action type and styling
|
||||||
|
let shape, annotation;
|
||||||
|
|
||||||
|
if (trade.action === 'OPEN_LONG') {
|
||||||
|
// Green upward arrow for long entry
|
||||||
|
shape = {
|
||||||
|
type: 'line',
|
||||||
|
x0: formattedTimestamp,
|
||||||
|
x1: formattedTimestamp,
|
||||||
|
y0: trade.price * 0.997,
|
||||||
|
y1: trade.price * 0.993,
|
||||||
|
line: { color: '#10b981', width: 3 },
|
||||||
|
name: `trade_${trade.trade_id}`
|
||||||
|
};
|
||||||
|
annotation = {
|
||||||
|
x: formattedTimestamp,
|
||||||
|
y: trade.price * 0.992,
|
||||||
|
text: `LONG<br>$${trade.price.toFixed(2)}`,
|
||||||
|
showarrow: true,
|
||||||
|
arrowhead: 2,
|
||||||
|
arrowcolor: '#10b981',
|
||||||
|
ax: 0,
|
||||||
|
ay: 30,
|
||||||
|
font: { size: 10, color: '#10b981', weight: 'bold' },
|
||||||
|
bgcolor: 'rgba(16, 185, 129, 0.2)'
|
||||||
|
};
|
||||||
|
} else if (trade.action === 'OPEN_SHORT') {
|
||||||
|
// Red downward arrow for short entry
|
||||||
|
shape = {
|
||||||
|
type: 'line',
|
||||||
|
x0: formattedTimestamp,
|
||||||
|
x1: formattedTimestamp,
|
||||||
|
y0: trade.price * 1.003,
|
||||||
|
y1: trade.price * 1.007,
|
||||||
|
line: { color: '#ef4444', width: 3 },
|
||||||
|
name: `trade_${trade.trade_id}`
|
||||||
|
};
|
||||||
|
annotation = {
|
||||||
|
x: formattedTimestamp,
|
||||||
|
y: trade.price * 1.008,
|
||||||
|
text: `SHORT<br>$${trade.price.toFixed(2)}`,
|
||||||
|
showarrow: true,
|
||||||
|
arrowhead: 2,
|
||||||
|
arrowcolor: '#ef4444',
|
||||||
|
ax: 0,
|
||||||
|
ay: -30,
|
||||||
|
font: { size: 10, color: '#ef4444', weight: 'bold' },
|
||||||
|
bgcolor: 'rgba(239, 68, 68, 0.2)'
|
||||||
|
};
|
||||||
|
} else if (trade.action === 'CLOSE_LONG' || trade.action === 'CLOSE_SHORT') {
|
||||||
|
// Exit marker with PnL
|
||||||
|
const isProfit = trade.pnl > 0;
|
||||||
|
const color = isProfit ? '#10b981' : '#ef4444';
|
||||||
|
const positionType = trade.action === 'CLOSE_LONG' ? 'LONG' : 'SHORT';
|
||||||
|
|
||||||
|
shape = {
|
||||||
|
type: 'line',
|
||||||
|
x0: formattedTimestamp,
|
||||||
|
x1: formattedTimestamp,
|
||||||
|
y0: trade.price,
|
||||||
|
y1: trade.price,
|
||||||
|
line: { color: color, width: 4, dash: 'dot' },
|
||||||
|
name: `trade_${trade.trade_id}_exit`
|
||||||
|
};
|
||||||
|
annotation = {
|
||||||
|
x: formattedTimestamp,
|
||||||
|
y: trade.price,
|
||||||
|
text: `EXIT ${positionType}<br>$${trade.price.toFixed(2)}<br>PnL: ${isProfit ? '+' : ''}$${trade.pnl.toFixed(2)} (${trade.pnl_pct >= 0 ? '+' : ''}${trade.pnl_pct.toFixed(2)}%)`,
|
||||||
|
showarrow: true,
|
||||||
|
arrowhead: 1,
|
||||||
|
arrowcolor: color,
|
||||||
|
ax: 0,
|
||||||
|
ay: isProfit ? -40 : 40,
|
||||||
|
font: { size: 10, color: color, weight: 'bold' },
|
||||||
|
bgcolor: isProfit ? 'rgba(16, 185, 129, 0.3)' : 'rgba(239, 68, 68, 0.3)'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add position line connecting entry to exit if entry time available
|
||||||
|
if (trade.entry_time) {
|
||||||
|
const entryTimestamp = new Date(trade.entry_time);
|
||||||
|
const entryYear = entryTimestamp.getUTCFullYear();
|
||||||
|
const entryMonth = String(entryTimestamp.getUTCMonth() + 1).padStart(2, '0');
|
||||||
|
const entryDay = String(entryTimestamp.getUTCDate()).padStart(2, '0');
|
||||||
|
const entryHours = String(entryTimestamp.getUTCHours()).padStart(2, '0');
|
||||||
|
const entryMinutes = String(entryTimestamp.getUTCMinutes()).padStart(2, '0');
|
||||||
|
const entrySeconds = String(entryTimestamp.getUTCSeconds()).padStart(2, '0');
|
||||||
|
const formattedEntryTime = `${entryYear}-${entryMonth}-${entryDay} ${entryHours}:${entryMinutes}:${entrySeconds}`;
|
||||||
|
|
||||||
|
const positionLine = {
|
||||||
|
type: 'rect',
|
||||||
|
x0: formattedEntryTime,
|
||||||
|
x1: formattedTimestamp,
|
||||||
|
y0: trade.entry_price,
|
||||||
|
y1: trade.price,
|
||||||
|
fillcolor: isProfit ? 'rgba(16, 185, 129, 0.1)' : 'rgba(239, 68, 68, 0.1)',
|
||||||
|
line: { color: color, width: 2, dash: isProfit ? 'solid' : 'dash' },
|
||||||
|
name: `position_${trade.trade_id}`
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add both position rectangle and exit marker
|
||||||
|
const currentShapes = plotElement.layout.shapes || [];
|
||||||
|
Plotly.relayout(plotId, {
|
||||||
|
shapes: [...currentShapes, positionLine, shape]
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Just add exit marker
|
||||||
|
const currentShapes = plotElement.layout.shapes || [];
|
||||||
|
Plotly.relayout(plotId, {
|
||||||
|
shapes: [...currentShapes, shape]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Entry marker only (no position line yet)
|
||||||
|
const currentShapes = plotElement.layout.shapes || [];
|
||||||
|
Plotly.relayout(plotId, {
|
||||||
|
shapes: [...currentShapes, shape]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add annotation
|
||||||
|
if (annotation) {
|
||||||
|
const currentAnnotations = plotElement.layout.annotations || [];
|
||||||
|
Plotly.relayout(plotId, {
|
||||||
|
annotations: [...currentAnnotations, annotation]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Added executed trade marker: ${trade.action} @ ${trade.price.toFixed(2)}`);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error adding executed trade marker:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove live metrics overlay
|
* Remove live metrics overlay
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -99,6 +99,18 @@ class LiveUpdatesWebSocket {
|
|||||||
console.error('Prediction error:', data);
|
console.error('Prediction error:', data);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.socket.on('executed_trade', (data) => {
|
||||||
|
console.log('Executed trade received:', data);
|
||||||
|
if (this.onExecutedTrade) {
|
||||||
|
this.onExecutedTrade(data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.socket.on('training_update', (data) => {
|
||||||
|
console.log('Training update received:', data);
|
||||||
|
// Training feedback from incremental learning
|
||||||
|
});
|
||||||
|
|
||||||
// Error events
|
// Error events
|
||||||
this.socket.on('connect_error', (error) => {
|
this.socket.on('connect_error', (error) => {
|
||||||
console.error('WebSocket connection error:', error);
|
console.error('WebSocket connection error:', error);
|
||||||
@@ -230,6 +242,26 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
window.liveUpdatesWS.onExecutedTrade = function(data) {
|
||||||
|
// Visualize executed trade on chart
|
||||||
|
if (window.appState && window.appState.chartManager) {
|
||||||
|
window.appState.chartManager.addExecutedTradeMarker(data.trade, data.position_state);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update position state display
|
||||||
|
if (typeof updatePositionStateDisplay === 'function') {
|
||||||
|
updatePositionStateDisplay(data.position_state, data.session_metrics);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log trade details
|
||||||
|
console.log('Executed Trade:', {
|
||||||
|
action: data.trade.action,
|
||||||
|
price: data.trade.price,
|
||||||
|
pnl: data.trade.pnl ? `$${data.trade.pnl.toFixed(2)} (${data.trade.pnl_pct.toFixed(2)}%)` : 'N/A',
|
||||||
|
position: data.position_state.has_position ? `${data.position_state.position_type.toUpperCase()} @ $${data.position_state.entry_price}` : 'CLOSED'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// Auto-connect
|
// Auto-connect
|
||||||
console.log('Auto-connecting to WebSocket...');
|
console.log('Auto-connecting to WebSocket...');
|
||||||
window.liveUpdatesWS.connect();
|
window.liveUpdatesWS.connect();
|
||||||
|
|||||||
Reference in New Issue
Block a user