Merge branch 'cleanup' of https://git.d-popov.com/popov/gogo2 into cleanup

This commit is contained in:
Dobromir Popov
2025-11-22 20:45:37 +02:00
25 changed files with 2825 additions and 258 deletions

View File

@@ -46,7 +46,7 @@ class TradeAnnotation:
def __post_init__(self):
if self.created_at is None:
self.created_at = datetime.now().isoformat()
self.created_at = datetime.now(pytz.UTC).isoformat()
if self.market_context is None:
self.market_context = {}
@@ -96,7 +96,7 @@ class AnnotationManager:
# Update metadata
self.annotations_db["metadata"] = {
"total_annotations": len(self.annotations_db["annotations"]),
"last_updated": datetime.now().isoformat()
"last_updated": datetime.now(pytz.UTC).isoformat()
}
with open(self.annotations_file, 'w') as f:
@@ -451,7 +451,7 @@ class AnnotationManager:
export_data = [asdict(ann) for ann in annotations]
# Create export file
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
timestamp = datetime.now(pytz.UTC).strftime('%Y%m%d_%H%M%S')
export_file = self.storage_path / f"export_{timestamp}.{format_type}"
if format_type == 'json':

View File

@@ -116,6 +116,8 @@ class TrainingSession:
error: Optional[str] = None
gpu_utilization: Optional[float] = None # GPU utilization percentage
cpu_utilization: Optional[float] = None # CPU utilization percentage
annotation_count: Optional[int] = None # Number of annotations used
timeframe: Optional[str] = None # Primary timeframe (e.g., '1m', '5m')
class RealTrainingAdapter:
@@ -208,13 +210,17 @@ class RealTrainingAdapter:
logger.info(f"Available models for training: {available}")
return available
def start_training(self, model_name: str, test_cases: List[Dict]) -> str:
def start_training(self, model_name: str, test_cases: List[Dict],
annotation_count: Optional[int] = None,
timeframe: Optional[str] = None) -> str:
"""
Start REAL training session with test cases
Args:
model_name: Name of model to train (CNN, DQN, Transformer, COB, Extrema)
test_cases: List of test cases from annotations
annotation_count: Number of annotations used (optional)
timeframe: Primary timeframe for training (optional, e.g., '1m', '5m')
Returns:
training_id: Unique ID for this training session
@@ -224,6 +230,10 @@ class RealTrainingAdapter:
training_id = str(uuid.uuid4())
# Use annotation_count if provided, otherwise use test_cases count
if annotation_count is None:
annotation_count = len(test_cases)
# Create training session
session = TrainingSession(
training_id=training_id,
@@ -233,7 +243,9 @@ class RealTrainingAdapter:
current_epoch=0,
total_epochs=10, # Reasonable for annotation-based training
current_loss=0.0,
start_time=time.time()
start_time=time.time(),
annotation_count=annotation_count,
timeframe=timeframe
)
self.training_sessions[training_id] = session
@@ -1083,7 +1095,8 @@ class RealTrainingAdapter:
raise Exception("CNN model does not have train_on_annotations, trainer.train_step, or train_step method")
session.final_loss = session.current_loss
session.accuracy = 0.85 # TODO: Calculate actual accuracy
# Accuracy calculated from actual training metrics, not synthetic
session.accuracy = None # Will be set by training loop if available
def _train_dqn_real(self, session: TrainingSession, training_data: List[Dict]):
"""Train DQN model with REAL training loop"""
@@ -1121,7 +1134,8 @@ class RealTrainingAdapter:
raise Exception("DQN agent does not have replay method")
session.final_loss = session.current_loss
session.accuracy = 0.85 # TODO: Calculate actual accuracy
# Accuracy calculated from actual training metrics, not synthetic
session.accuracy = None # Will be set by training loop if available
def _build_state_from_data(self, data: Dict, agent: Any) -> List[float]:
"""Build proper state representation from training data"""
@@ -1601,29 +1615,41 @@ class RealTrainingAdapter:
# FIXED: Ensure shape is [1, 1] not [1] to match BCELoss requirements
trade_success = torch.tensor([[1.0 if profit_loss_pct > 0 else 0.0]], dtype=torch.float32) # [1, 1]
# NEW: Trend vector target for trend analysis optimization
# Calculate expected trend from entry to exit
direction = training_sample.get('direction', 'NONE')
# REAL TREND CALCULATION from actual price data (NO MORE SYNTHETIC DATA!)
# Use last 10 candles to calculate actual trend angle, steepness, direction
if direction == 'LONG':
# Upward trend: positive angle, positive direction
trend_angle = 0.785 # ~45 degrees in radians (pi/4)
trend_direction = 1.0 # Upward
elif direction == 'SHORT':
# Downward trend: negative angle, negative direction
trend_angle = -0.785 # ~-45 degrees
trend_direction = -1.0 # Downward
# Get price data from the batch to calculate actual trend
price_data = price_data_1m if price_data_1m is not None else (
price_data_1s if price_data_1s is not None else price_data_1h)
if price_data is not None and price_data.shape[1] >= 10:
# price_data shape: [batch=1, seq_len=200, features=5] -> OHLCV
recent_closes = price_data[0, -10:, 3] # Last 10 close prices [10]
# Calculate actual price change and time delta
price_start = recent_closes[0].item()
price_end = recent_closes[-1].item()
price_delta = price_end - price_start
time_delta = 9.0 # 10 candles = 9 intervals
# Calculate real angle using atan2
import math
trend_angle = math.atan2(price_delta, time_delta * price_start / 100.0) # Normalize by price scale
# Calculate real steepness (magnitude of change)
if price_start > 0:
price_change_pct = abs(price_delta / price_start)
trend_steepness = min(price_change_pct * 100.0, 1.0) # Scale and cap at 1.0
else:
trend_steepness = 0.0
# Calculate real direction
trend_direction = 1.0 if price_delta > 0 else (-1.0 if price_delta < 0 else 0.0)
else:
# No trend
# Fallback if no price data available (should rarely happen)
trend_angle = 0.0
trend_direction = 0.0
# Steepness based on profit potential
if exit_price and entry_price and entry_price > 0:
price_change_pct = abs((exit_price - entry_price) / entry_price)
trend_steepness = min(price_change_pct * 10, 1.0) # Normalize to [0, 1]
else:
trend_steepness = 0.0
trend_direction = 0.0
# Create trend target tensor [batch, 3]: [angle, steepness, direction]
trend_target = torch.tensor([[trend_angle, trend_steepness, trend_direction]], dtype=torch.float32) # [1, 3]
@@ -2131,7 +2157,7 @@ class RealTrainingAdapter:
checkpoint_dir = "models/checkpoints/transformer"
os.makedirs(checkpoint_dir, exist_ok=True)
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
timestamp = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S")
checkpoint_path = os.path.join(checkpoint_dir, f"transformer_epoch{epoch+1}_{timestamp}.pt")
torch.save({
@@ -2358,7 +2384,9 @@ class RealTrainingAdapter:
'current_epoch': session.current_epoch,
'total_epochs': session.total_epochs,
'current_loss': session.current_loss,
'start_time': session.start_time
'start_time': session.start_time,
'annotation_count': session.annotation_count,
'timeframe': session.timeframe
}
return None
@@ -2414,13 +2442,14 @@ class RealTrainingAdapter:
if not hasattr(self, 'inference_sessions'):
self.inference_sessions = {}
# Create inference session
# Create inference session with position tracking
self.inference_sessions[inference_id] = {
'model_name': model_name,
'symbol': symbol,
'status': 'running',
'start_time': time.time(),
'signals': [],
'signals': [], # All signals (including rejected ones)
'executed_trades': [], # Only executed trades (open/close positions)
'stop_flag': False,
'live_training_enabled': enable_live_training,
'train_every_candle': train_every_candle,
@@ -2431,7 +2460,13 @@ class RealTrainingAdapter:
'loss': 0.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")
@@ -2767,6 +2802,68 @@ class RealTrainingAdapter:
logger.warning(f"Error fetching market state for candle: {e}")
return {}
def _convert_prediction_to_batch(self, prediction_sample: Dict, timeframe: str):
"""
Convert a validated prediction to a training batch
Args:
prediction_sample: Dict with predicted_candle, actual_candle, market_state, etc.
timeframe: Target timeframe for prediction
Returns:
Batch dict ready for trainer.train_step()
"""
try:
market_state = prediction_sample.get('market_state', {})
if not market_state or 'timeframes' not in market_state:
logger.warning("No market state in prediction sample")
return None
# Use existing conversion method but with actual target
annotation = {
'symbol': prediction_sample.get('symbol', 'ETH/USDT'),
'timestamp': prediction_sample.get('timestamp'),
'action': 'BUY', # Placeholder, not used for candle prediction training
'entry_price': float(prediction_sample['predicted_candle'][0]), # Open
'market_state': market_state
}
# Convert using existing method
batch = self._convert_annotation_to_transformer_batch(annotation)
if not batch:
return None
# Override the future candle target with actual candle data
actual = prediction_sample['actual_candle'] # [O, H, L, C]
# Create target tensor for the specific timeframe
import torch
device = batch['prices_1m'].device if 'prices_1m' in batch else torch.device('cpu')
# Target candle: [O, H, L, C, V] - we don't have actual volume, use predicted
target_candle = [
actual[0], # Open
actual[1], # High
actual[2], # Low
actual[3], # Close
prediction_sample['predicted_candle'][4] # Volume (from prediction)
]
# Add to batch based on timeframe
if timeframe == '1s':
batch['future_candle_1s'] = torch.tensor([target_candle], dtype=torch.float32, device=device)
elif timeframe == '1m':
batch['future_candle_1m'] = torch.tensor([target_candle], dtype=torch.float32, device=device)
elif timeframe == '1h':
batch['future_candle_1h'] = torch.tensor([target_candle], dtype=torch.float32, device=device)
logger.debug(f"Converted prediction to batch for {timeframe} timeframe")
return batch
except Exception as e:
logger.error(f"Error converting prediction to batch: {e}", exc_info=True)
return None
def _train_transformer_on_sample(self, training_sample: Dict):
"""Train transformer on a single sample with checkpoint saving"""
try:
@@ -2858,7 +2955,7 @@ class RealTrainingAdapter:
checkpoint_dir = "models/checkpoints/transformer/realtime"
os.makedirs(checkpoint_dir, exist_ok=True)
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
timestamp = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S")
checkpoint_type = "BEST" if improved else "periodic"
checkpoint_path = os.path.join(checkpoint_dir, f"realtime_{checkpoint_type}_step{step}_{timestamp}.pt")
@@ -3123,7 +3220,7 @@ class RealTrainingAdapter:
if prediction:
# Store signal
signal = {
'timestamp': datetime.now().isoformat(),
'timestamp': datetime.now(timezone.utc).isoformat(),
'symbol': symbol,
'model': model_name,
'action': prediction['action'],
@@ -3133,18 +3230,44 @@ class RealTrainingAdapter:
'predicted_candle': prediction.get('predicted_candle')
}
# Store signal (all signals, including rejected ones)
session['signals'].append(signal)
# Keep only last 100 signals
if len(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
if self.orchestrator and hasattr(self.orchestrator, 'store_transformer_prediction'):
self.orchestrator.store_transformer_prediction(symbol, {
'timestamp': datetime.now(),
'timestamp': datetime.now(timezone.utc).isoformat(),
'current_price': current_price,
'predicted_price': current_price * (1.01 if prediction['action'] == 'BUY' else 0.99),
'price_change': 1.0 if prediction['action'] == 'BUY' else -1.0,
@@ -3172,3 +3295,173 @@ class RealTrainingAdapter:
logger.error(f"Fatal error in inference loop: {e}")
session['status'] = 'error'
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

View File

@@ -46,29 +46,6 @@
"exit_state": {}
}
},
{
"annotation_id": "91847a37-6315-4546-b5a0-573118311322",
"symbol": "ETH/USDT",
"timeframe": "1s",
"entry": {
"timestamp": "2025-10-25 13:08:04",
"price": 3940.24,
"index": 25
},
"exit": {
"timestamp": "2025-10-25 13:15:12",
"price": 3942.59,
"index": 57
},
"direction": "LONG",
"profit_loss_pct": 0.05964103709419639,
"notes": "",
"created_at": "2025-10-25T16:17:02.931920",
"market_context": {
"entry_state": {},
"exit_state": {}
}
},
{
"annotation_id": "479eb310-c963-4837-b712-70e5a42afb53",
"symbol": "ETH/USDT",
@@ -120,42 +97,65 @@
"symbol": "ETH/USDT",
"timeframe": "1m",
"entry": {
"timestamp": "2025-11-22 06:41",
"price": 2759.12,
"index": 250
"timestamp": "2025-11-12 07:58",
"price": 3424.58,
"index": 284
},
"exit": {
"timestamp": "2025-11-22 10:42",
"price": 2709.14,
"index": 335
"timestamp": "2025-11-12 11:08",
"price": 3546.35,
"index": 329
},
"direction": "SHORT",
"profit_loss_pct": 1.8114471280698201,
"direction": "LONG",
"profit_loss_pct": 3.5557645025083366,
"notes": "",
"created_at": "2025-11-22T13:09:16.675137",
"created_at": "2025-11-12T13:11:31.267142",
"market_context": {
"entry_state": {},
"exit_state": {}
}
},
{
"annotation_id": "5cf94e70-e8f7-4c29-a860-4c2bc516bd8c",
"annotation_id": "46cc0e20-0bfb-498c-9358-71b52a003d0f",
"symbol": "ETH/USDT",
"timeframe": "1s",
"entry": {
"timestamp": "2025-11-22 11:00:30",
"price": 2714.28,
"index": 63
"timestamp": "2025-11-22 12:50",
"price": 2712.11,
"index": 26
},
"exit": {
"timestamp": "2025-11-22 11:05:19",
"price": 2705.95,
"index": 90
"timestamp": "2025-11-22 12:53:06",
"price": 2721.44,
"index": 45
},
"direction": "LONG",
"profit_loss_pct": 0.3440125953593301,
"notes": "",
"created_at": "2025-11-22T15:19:00.480166",
"market_context": {
"entry_state": {},
"exit_state": {}
}
},
{
"annotation_id": "b01fe6b2-7724-495e-ab01-3f3d3aa0da5d",
"symbol": "ETH/USDT",
"timeframe": "1s",
"entry": {
"timestamp": "2025-11-22 13:22:23",
"price": 2727.52,
"index": 53
},
"exit": {
"timestamp": "2025-11-22 13:31:18",
"price": 2717.9,
"index": 104
},
"direction": "SHORT",
"profit_loss_pct": 0.30689538293766233,
"profit_loss_pct": 0.3527013550771357,
"notes": "",
"created_at": "2025-11-22T13:09:40.711052",
"created_at": "2025-11-22T15:31:43.939943",
"market_context": {
"entry_state": {},
"exit_state": {}
@@ -164,6 +164,6 @@
],
"metadata": {
"total_annotations": 7,
"last_updated": "2025-11-22T13:09:40.712602"
"last_updated": "2025-11-22T15:31:43.940190"
}
}

View File

@@ -16,7 +16,7 @@ sys.path.insert(0, str(parent_dir))
from flask import Flask, render_template, request, jsonify, send_file
from dash import Dash, html
import logging
from datetime import datetime
from datetime import datetime, timezone
from typing import Optional, Dict, List, Any
import json
import pandas as pd
@@ -538,6 +538,9 @@ class AnnotationDashboard:
engineio_logger=False
)
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")
except ImportError:
self.socketio = None
@@ -586,6 +589,8 @@ class AnnotationDashboard:
self.annotation_manager = AnnotationManager()
# Use REAL training adapter - NO SIMULATION!
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
self.backtest_runner = BacktestRunner()
@@ -626,63 +631,38 @@ class AnnotationDashboard:
if not self.orchestrator:
logger.info("Initializing TradingOrchestrator...")
self.orchestrator = TradingOrchestrator(
data_provider=self.data_provider,
config=self.config
data_provider=self.data_provider
)
self.training_adapter.orchestrator = self.orchestrator
logger.info("TradingOrchestrator initialized")
# Get checkpoint info before loading
checkpoint_info = self._get_best_checkpoint_info(model_name)
# Load the specific model
# Check if the specific model is already initialized
if model_name == 'Transformer':
logger.info("Loading Transformer model...")
self.orchestrator.load_transformer_model()
self.loaded_models['Transformer'] = self.orchestrator.primary_transformer_trainer
# Store checkpoint info in orchestrator for UI access
if checkpoint_info:
self.orchestrator.transformer_checkpoint_info = {
'status': 'loaded',
'filename': checkpoint_info.get('filename', 'unknown'),
'epoch': checkpoint_info.get('epoch', 0),
'loss': checkpoint_info.get('loss', 0.0),
'accuracy': checkpoint_info.get('accuracy', 0.0),
'loaded_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
}
logger.info("Transformer model loaded successfully")
logger.info("Checking Transformer model...")
if self.orchestrator.primary_transformer:
self.loaded_models['Transformer'] = self.orchestrator.primary_transformer
logger.info("Transformer model loaded successfully")
else:
logger.warning("Transformer model not initialized in orchestrator")
return
elif model_name == 'CNN':
logger.info("Loading CNN model...")
self.orchestrator.load_cnn_model()
self.loaded_models['CNN'] = self.orchestrator.cnn_model
# Store checkpoint info
if checkpoint_info:
self.orchestrator.cnn_checkpoint_info = {
'status': 'loaded',
'filename': checkpoint_info.get('filename', 'unknown'),
'loaded_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
}
logger.info("CNN model loaded successfully")
logger.info("Checking CNN model...")
if self.orchestrator.cnn_model:
self.loaded_models['CNN'] = self.orchestrator.cnn_model
logger.info("CNN model loaded successfully")
else:
logger.warning("CNN model not initialized in orchestrator")
return
elif model_name == 'DQN':
logger.info("Loading DQN model...")
self.orchestrator.load_dqn_model()
self.loaded_models['DQN'] = self.orchestrator.dqn_agent
# Store checkpoint info
if checkpoint_info:
self.orchestrator.dqn_checkpoint_info = {
'status': 'loaded',
'filename': checkpoint_info.get('filename', 'unknown'),
'loaded_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
}
logger.info("DQN model loaded successfully")
logger.info("Checking DQN model...")
if self.orchestrator.rl_agent:
self.loaded_models['DQN'] = self.orchestrator.rl_agent
logger.info("DQN model loaded successfully")
else:
logger.warning("DQN model not initialized in orchestrator")
return
else:
logger.warning(f"Unknown model name: {model_name}")
@@ -1741,6 +1721,9 @@ class AnnotationDashboard:
# CRITICAL: Get current symbol to filter annotations
current_symbol = data.get('symbol', 'ETH/USDT')
# Get primary timeframe for display (optional)
timeframe = data.get('timeframe', '1m')
# If no specific annotations provided, use all for current symbol
if not annotation_ids:
annotations = self.annotation_manager.get_annotations(symbol=current_symbol)
@@ -1769,12 +1752,14 @@ class AnnotationDashboard:
}
})
logger.info(f"Starting REAL training with {len(test_cases)} test cases for model {model_name}")
logger.info(f"Starting REAL training with {len(test_cases)} test cases ({len(annotation_ids)} annotations) for model {model_name} on {timeframe}")
# Start REAL training (NO SIMULATION!)
training_id = self.training_adapter.start_training(
model_name=model_name,
test_cases=test_cases
test_cases=test_cases,
annotation_count=len(annotation_ids),
timeframe=timeframe
)
return jsonify({
@@ -2392,6 +2377,55 @@ class AnnotationDashboard:
except Exception as e:
logger.error(f"Error handling prediction request: {e}")
emit('prediction_error', {'error': str(e)})
@self.socketio.on('prediction_accuracy')
def handle_prediction_accuracy(data):
"""
Handle validated prediction accuracy - trigger incremental training
This is called when frontend validates a prediction against actual candle.
We use this data to incrementally train the model for continuous improvement.
"""
from flask_socketio import emit
try:
timeframe = data.get('timeframe')
timestamp = data.get('timestamp')
predicted = data.get('predicted') # [O, H, L, C, V]
actual = data.get('actual') # [O, H, L, C]
errors = data.get('errors') # {open, high, low, close}
pct_errors = data.get('pctErrors')
direction_correct = data.get('directionCorrect')
accuracy = data.get('accuracy')
if not all([timeframe, timestamp, predicted, actual]):
logger.warning("Incomplete prediction accuracy data received")
return
logger.info(f"[{timeframe}] Prediction validated: {accuracy:.1f}% accuracy, direction: {direction_correct}")
logger.debug(f" Errors: O={pct_errors['open']:.2f}% H={pct_errors['high']:.2f}% L={pct_errors['low']:.2f}% C={pct_errors['close']:.2f}%")
# Trigger incremental training on this validated prediction
self._train_on_validated_prediction(
timeframe=timeframe,
timestamp=timestamp,
predicted=predicted,
actual=actual,
errors=errors,
direction_correct=direction_correct,
accuracy=accuracy
)
# Send confirmation back to frontend
emit('training_update', {
'status': 'training_triggered',
'timestamp': timestamp,
'accuracy': accuracy,
'message': f'Incremental training triggered on validated prediction'
})
except Exception as e:
logger.error(f"Error handling prediction accuracy: {e}", exc_info=True)
emit('training_error', {'error': str(e)})
def _start_live_update_thread(self):
"""Start background thread for live updates"""
@@ -2415,24 +2449,44 @@ class AnnotationDashboard:
for timeframe in ['1s', '1m']:
room = f"{symbol}_{timeframe}"
# Get latest candle
# Get latest candles (need last 2 to determine confirmation status)
try:
candles = self.data_provider.get_ohlcv(symbol, timeframe, limit=1)
candles = self.data_provider.get_ohlcv(symbol, timeframe, limit=2)
if candles and len(candles) > 0:
latest_candle = candles[-1]
# Emit chart update
# Determine if candle is confirmed (closed)
# For 1s: candle is confirmed when next candle starts (2s delay)
# For others: candle is confirmed when next candle starts
is_confirmed = len(candles) >= 2 # If we have 2 candles, the first is confirmed
# Format timestamp consistently
timestamp = latest_candle.get('timestamp')
if isinstance(timestamp, str):
# Already formatted
formatted_timestamp = timestamp
else:
# Convert to ISO string then format
from datetime import datetime
if isinstance(timestamp, datetime):
formatted_timestamp = timestamp.strftime('%Y-%m-%d %H:%M:%S')
else:
formatted_timestamp = str(timestamp)
# Emit chart update with full candle data
self.socketio.emit('chart_update', {
'symbol': symbol,
'timeframe': timeframe,
'candle': {
'timestamp': latest_candle.get('timestamp'),
'open': latest_candle.get('open'),
'high': latest_candle.get('high'),
'low': latest_candle.get('low'),
'close': latest_candle.get('close'),
'volume': latest_candle.get('volume')
}
'timestamp': formatted_timestamp,
'open': float(latest_candle.get('open', 0)),
'high': float(latest_candle.get('high', 0)),
'low': float(latest_candle.get('low', 0)),
'close': float(latest_candle.get('close', 0)),
'volume': float(latest_candle.get('volume', 0))
},
'is_confirmed': is_confirmed, # True if this candle is closed/confirmed
'has_previous': len(candles) >= 2 # True if we have previous candle for validation
}, room=room)
# Get prediction if model is loaded
@@ -2453,6 +2507,144 @@ class AnnotationDashboard:
self._live_update_thread = threading.Thread(target=live_update_worker, daemon=True)
self._live_update_thread.start()
def _train_on_validated_prediction(self, timeframe: str, timestamp: str, predicted: list,
actual: list, errors: dict, direction_correct: bool, accuracy: float):
"""
Incrementally train model on validated prediction
This implements online learning where each validated prediction becomes
a training sample, with loss weighting based on prediction accuracy.
"""
try:
if not self.training_adapter:
logger.warning("Training adapter not available for incremental training")
return
if not self.orchestrator or not hasattr(self.orchestrator, 'primary_transformer'):
logger.warning("Transformer model not available for incremental training")
return
# Get the transformer trainer
trainer = getattr(self.orchestrator, 'primary_transformer_trainer', None)
if not trainer:
logger.warning("Transformer trainer not available")
return
# Calculate sample weight based on accuracy
# Low accuracy predictions get higher weight (we need to learn from mistakes)
# High accuracy predictions get lower weight (model already knows this)
if accuracy < 50:
sample_weight = 3.0 # Learn hard from bad predictions
elif accuracy < 70:
sample_weight = 2.0 # Moderate learning
elif accuracy < 85:
sample_weight = 1.0 # Normal learning
else:
sample_weight = 0.5 # Light touch-up for good predictions
# Also weight by direction correctness
if not direction_correct:
sample_weight *= 1.5 # Wrong direction is critical - learn more
logger.info(f"[{timeframe}] Incremental training: accuracy={accuracy:.1f}%, weight={sample_weight:.1f}x")
# Create training sample from validated prediction
# We need to fetch the market state at that timestamp
symbol = 'ETH/USDT' # TODO: Get from active trading pair
training_sample = {
'symbol': symbol,
'timestamp': timestamp,
'predicted_candle': predicted, # [O, H, L, C, V]
'actual_candle': actual, # [O, H, L, C]
'errors': errors,
'accuracy': accuracy,
'direction_correct': direction_correct,
'sample_weight': sample_weight
}
# Get market state at that timestamp
try:
market_state = self._fetch_market_state_at_timestamp(symbol, timestamp, timeframe)
training_sample['market_state'] = market_state
except Exception as e:
logger.warning(f"Could not fetch market state: {e}")
return
# Convert to transformer batch format
batch = self.training_adapter._convert_prediction_to_batch(training_sample, timeframe)
if not batch:
logger.warning("Could not convert validated prediction to training batch")
return
# Train on this batch with sample weighting
with torch.enable_grad():
trainer.model.train()
result = trainer.train_step(batch, accumulate_gradients=False, sample_weight=sample_weight)
if result:
loss = result.get('total_loss', 0)
candle_accuracy = result.get('candle_accuracy', 0)
logger.info(f"[{timeframe}] Trained on validated prediction: loss={loss:.4f}, new_acc={candle_accuracy:.2%}")
# Save checkpoint periodically (every 10 incremental steps)
if not hasattr(self, '_incremental_training_steps'):
self._incremental_training_steps = 0
self._incremental_training_steps += 1
if self._incremental_training_steps % 10 == 0:
logger.info(f"Saving checkpoint after {self._incremental_training_steps} incremental training steps")
trainer.save_checkpoint(
filepath=None, # Auto-generate path
metadata={
'training_type': 'incremental_online',
'steps': self._incremental_training_steps,
'last_accuracy': accuracy
}
)
except Exception as e:
logger.error(f"Error in incremental training: {e}", exc_info=True)
def _fetch_market_state_at_timestamp(self, symbol: str, timestamp: str, timeframe: str) -> Dict:
"""Fetch market state at a specific timestamp for training"""
try:
from datetime import datetime
import pandas as pd
# Parse timestamp
ts = pd.Timestamp(timestamp)
# Get historical data for multiple timeframes
market_state = {'timeframes': {}, 'secondary_timeframes': {}}
for tf in ['1s', '1m', '1h']:
try:
df = self.data_provider.get_historical_data(symbol, tf, limit=200)
if df is not None and not df.empty:
# Find data up to (but not including) the target timestamp
df_before = df[df.index < ts]
if not df_before.empty:
recent = df_before.tail(200)
market_state['timeframes'][tf] = {
'timestamps': recent.index.strftime('%Y-%m-%d %H:%M:%S').tolist(),
'open': recent['open'].tolist(),
'high': recent['high'].tolist(),
'low': recent['low'].tolist(),
'close': recent['close'].tolist(),
'volume': recent['volume'].tolist()
}
except Exception as e:
logger.warning(f"Could not fetch {tf} data: {e}")
return market_state
except Exception as e:
logger.error(f"Error fetching market state: {e}")
return {}
def _get_live_prediction(self, symbol: str, timeframe: str, prediction_steps: int = 1):
"""Get live prediction from model"""
try:
@@ -2471,7 +2663,7 @@ class AnnotationDashboard:
return {
'symbol': symbol,
'timeframe': timeframe,
'timestamp': datetime.now().isoformat(),
'timestamp': datetime.now(timezone.utc).isoformat(),
'action': random.choice(['BUY', 'SELL', 'HOLD']),
'confidence': random.uniform(0.6, 0.95),
'predicted_price': candles[-1].get('close', 0) * (1 + random.uniform(-0.01, 0.01)),

View File

@@ -10,6 +10,7 @@
/* Chart Panel */
.chart-panel {
height: calc(100vh - 150px);
transition: all 0.3s ease;
}
.chart-panel .card-body {
@@ -17,6 +18,29 @@
overflow: hidden;
}
/* Maximized Chart View */
.chart-maximized {
width: 100% !important;
max-width: 100% !important;
flex: 0 0 100% !important;
transition: all 0.3s ease;
}
.chart-panel-maximized {
height: calc(100vh - 80px) !important;
position: fixed;
top: 60px;
left: 0;
right: 0;
z-index: 1040;
margin: 0 !important;
border-radius: 0 !important;
}
.chart-panel-maximized .card-body {
height: calc(100% - 60px);
}
#chart-container {
height: 100%;
overflow-y: auto;
@@ -236,11 +260,32 @@
padding: 1rem;
}
/* Maximized View - Larger Charts */
.chart-panel-maximized .chart-plot {
height: 400px;
}
@media (min-width: 1400px) {
.chart-panel-maximized .chart-plot {
height: 450px;
}
}
@media (min-width: 1920px) {
.chart-panel-maximized .chart-plot {
height: 500px;
}
}
/* Responsive Adjustments */
@media (max-width: 1200px) {
.chart-plot {
height: 250px;
}
.chart-panel-maximized .chart-plot {
height: 350px;
}
}
@media (max-width: 768px) {

File diff suppressed because it is too large Load Diff

View File

@@ -99,6 +99,18 @@ class LiveUpdatesWebSocket {
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
this.socket.on('connect_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
console.log('Auto-connecting to WebSocket...');
window.liveUpdatesWS.connect();

View File

@@ -101,6 +101,23 @@
if (typeof checkActiveTraining === 'function') {
checkActiveTraining();
}
// Keyboard shortcuts for chart maximization
document.addEventListener('keydown', function(e) {
// ESC key to exit maximized mode
if (e.key === 'Escape') {
const chartArea = document.querySelector('.chart-maximized');
if (chartArea) {
document.getElementById('maximize-btn').click();
}
}
// F key to toggle maximize (when not typing in input)
if (e.key === 'f' && !e.ctrlKey && !e.metaKey &&
!['INPUT', 'TEXTAREA', 'SELECT'].includes(document.activeElement.tagName)) {
document.getElementById('maximize-btn').click();
}
});
// Setup keyboard shortcuts
setupKeyboardShortcuts();

View File

@@ -14,6 +14,9 @@
<button type="button" class="btn btn-outline-light" id="reset-zoom-btn" title="Reset Zoom">
<i class="fas fa-expand"></i>
</button>
<button type="button" class="btn btn-outline-light" id="maximize-btn" title="Maximize Chart Area">
<i class="fas fa-arrows-alt"></i>
</button>
<button type="button" class="btn btn-outline-light" id="fullscreen-btn" title="Fullscreen">
<i class="fas fa-expand-arrows-alt"></i>
</button>
@@ -110,6 +113,41 @@
}
});
document.getElementById('maximize-btn').addEventListener('click', function () {
const mainRow = document.querySelector('.row.mt-3');
const leftSidebar = mainRow.querySelector('.col-md-2:first-child');
const chartArea = mainRow.querySelector('.col-md-8');
const rightSidebar = mainRow.querySelector('.col-md-2:last-child');
const chartPanel = document.querySelector('.chart-panel');
const maximizeIcon = this.querySelector('i');
// Toggle maximize state
if (chartArea.classList.contains('chart-maximized')) {
// Restore normal view
leftSidebar.style.display = '';
rightSidebar.style.display = '';
chartArea.classList.remove('chart-maximized');
chartPanel.classList.remove('chart-panel-maximized');
maximizeIcon.className = 'fas fa-arrows-alt';
this.title = 'Maximize Chart Area';
} else {
// Maximize chart area
leftSidebar.style.display = 'none';
rightSidebar.style.display = 'none';
chartArea.classList.add('chart-maximized');
chartPanel.classList.add('chart-panel-maximized');
maximizeIcon.className = 'fas fa-compress-arrows-alt';
this.title = 'Restore Normal View';
}
// Update chart layouts after transition
setTimeout(() => {
if (window.appState && window.appState.chartManager) {
window.appState.chartManager.updateChartLayout();
}
}, 350);
});
document.getElementById('fullscreen-btn').addEventListener('click', function () {
const chartContainer = document.getElementById('chart-container');
if (chartContainer.requestFullscreen) {

View File

@@ -40,9 +40,13 @@
role="progressbar" style="width: 0%"></div>
</div>
<div class="small">
<div>Epoch: <span id="training-epoch">0</span>/<span id="training-total-epochs">0</span></div>
<div>Loss: <span id="training-loss">--</span></div>
<div>GPU: <span id="training-gpu-util">--</span>% | CPU: <span id="training-cpu-util">--</span>%</div>
<div>Annotations: <span id="training-annotation-count" class="fw-bold text-primary">--</span></div>
<div>Timeframe: <span id="training-timeframe" class="fw-bold text-info">--</span></div>
<div class="mt-1 pt-1 border-top">
<div>Epoch: <span id="training-epoch">0</span>/<span id="training-total-epochs">0</span></div>
<div>Loss: <span id="training-loss">--</span></div>
<div>GPU: <span id="training-gpu-util">--</span>% | CPU: <span id="training-cpu-util">--</span>%</div>
</div>
</div>
</div>
</div>
@@ -139,12 +143,42 @@
<!-- Inference Status -->
<div id="inference-status" style="display: none;">
<div class="alert alert-success py-2 px-2 mb-2">
<div class="d-flex align-items-center mb-1">
<div class="spinner-border spinner-border-sm me-2" role="status">
<span class="visually-hidden">Running...</span>
<div class="d-flex align-items-center justify-content-between mb-1">
<div class="d-flex align-items-center">
<div class="spinner-border spinner-border-sm me-2" role="status">
<span class="visually-hidden">Running...</span>
</div>
<strong class="small">🔴 LIVE</strong>
</div>
<!-- Model Performance -->
<div class="small text-end">
<div style="font-size: 0.65rem;">Acc: <span id="live-accuracy" class="fw-bold text-success">--</span></div>
<div style="font-size: 0.65rem;">Loss: <span id="live-loss" class="fw-bold text-warning">--</span></div>
</div>
<strong class="small">🔴 LIVE</strong>
</div>
<!-- Position & PnL Status -->
<div class="mb-2 p-2" style="background-color: rgba(0,0,0,0.1); border-radius: 4px;">
<div class="small">
<div class="d-flex justify-content-between">
<span>Position:</span>
<span id="position-status" class="fw-bold text-info">NO POSITION</span>
</div>
<div class="d-flex justify-content-between" id="floating-pnl-row" style="display: none !important;">
<span>Floating PnL:</span>
<span id="floating-pnl" class="fw-bold">--</span>
</div>
<div class="d-flex justify-content-between">
<span>Session PnL:</span>
<span id="session-pnl" class="fw-bold text-success">+$0.00</span>
</div>
<div class="d-flex justify-content-between" style="font-size: 0.7rem; color: #9ca3af;">
<span>Win Rate:</span>
<span id="win-rate">0% (0/0)</span>
</div>
</div>
</div>
<div class="small">
<div>Timeframe: <span id="active-timeframe" class="fw-bold text-primary">--</span></div>
<div>Signal: <span id="latest-signal" class="fw-bold">--</span></div>
@@ -195,6 +229,15 @@
// Resume tracking
activeTrainingId = data.session.training_id;
showTrainingStatus();
// Populate annotation count and timeframe if available
if (data.session.annotation_count) {
document.getElementById('training-annotation-count').textContent = data.session.annotation_count;
}
if (data.session.timeframe) {
document.getElementById('training-timeframe').textContent = data.session.timeframe.toUpperCase();
}
pollTrainingProgress(activeTrainingId);
} else {
console.log('No active training session');
@@ -274,6 +317,36 @@
console.log(`✓ Models available: ${data.available_count}, loaded: ${data.loaded_count}`);
// Auto-select Transformer (or any loaded model) if available
let modelToSelect = null;
// First try to find Transformer
const transformerModel = data.models.find(m => {
const modelName = (m && typeof m === 'object' && m.name) ? m.name : String(m);
const isLoaded = (m && typeof m === 'object' && 'loaded' in m) ? m.loaded : false;
return modelName === 'Transformer' && isLoaded;
});
if (transformerModel) {
modelToSelect = 'Transformer';
} else {
// If Transformer not loaded, find any loaded model
const loadedModel = data.models.find(m => {
const isLoaded = (m && typeof m === 'object' && 'loaded' in m) ? m.loaded : false;
return isLoaded;
});
if (loadedModel) {
const modelName = (loadedModel && typeof loadedModel === 'object' && loadedModel.name) ? loadedModel.name : String(loadedModel);
modelToSelect = modelName;
}
}
// Auto-select if found
if (modelToSelect) {
modelSelect.value = modelToSelect;
selectedModel = modelToSelect;
console.log(`✓ Auto-selected loaded model: ${modelToSelect}`);
}
// Update button state for currently selected model
updateButtonState();
} else {
@@ -418,10 +491,17 @@
// Show training status
showTrainingStatus();
// Get primary timeframe for training
const primaryTimeframe = document.getElementById('primary-timeframe-select').value;
// Reset progress
document.getElementById('training-progress-bar').style.width = '0%';
document.getElementById('training-epoch').textContent = '0';
document.getElementById('training-loss').textContent = '--';
// Set annotation count and timeframe
document.getElementById('training-annotation-count').textContent = annotationIds.length;
document.getElementById('training-timeframe').textContent = primaryTimeframe.toUpperCase();
// Start training request
fetch('/api/train-model', {
@@ -430,7 +510,8 @@
body: JSON.stringify({
model_name: modelName,
annotation_ids: annotationIds,
symbol: appState.currentSymbol // CRITICAL: Filter by current symbol
symbol: appState.currentSymbol, // CRITICAL: Filter by current symbol
timeframe: primaryTimeframe // Primary timeframe for display
})
})
.then(response => response.json())
@@ -977,6 +1058,70 @@
}
}
function updatePositionStateDisplay(positionState, sessionMetrics) {
/**
* Update live trading panel with current position and PnL info
*/
try {
// Update position status
const positionStatusEl = document.getElementById('position-status');
const floatingPnlRow = document.getElementById('floating-pnl-row');
const floatingPnlEl = document.getElementById('floating-pnl');
if (positionState.has_position) {
const posType = positionState.position_type.toUpperCase();
const entryPrice = positionState.entry_price.toFixed(2);
positionStatusEl.textContent = `${posType} @ $${entryPrice}`;
positionStatusEl.className = posType === 'LONG' ? 'fw-bold text-success' : 'fw-bold text-danger';
// Show floating PnL
if (floatingPnlRow) {
floatingPnlRow.style.display = 'flex !important';
floatingPnlRow.classList.remove('d-none');
}
const unrealizedPnl = positionState.unrealized_pnl || 0;
const pnlColor = unrealizedPnl >= 0 ? 'text-success' : 'text-danger';
const pnlSign = unrealizedPnl >= 0 ? '+' : '';
floatingPnlEl.textContent = `${pnlSign}${unrealizedPnl.toFixed(2)}%`;
floatingPnlEl.className = `fw-bold ${pnlColor}`;
} else {
positionStatusEl.textContent = 'NO POSITION';
positionStatusEl.className = 'fw-bold text-secondary';
// Hide floating PnL row
if (floatingPnlRow) {
floatingPnlRow.style.display = 'none !important';
floatingPnlRow.classList.add('d-none');
}
}
// Update session PnL
const sessionPnlEl = document.getElementById('session-pnl');
if (sessionPnlEl && sessionMetrics) {
const totalPnl = sessionMetrics.total_pnl || 0;
const pnlColor = totalPnl >= 0 ? 'text-success' : 'text-danger';
const pnlSign = totalPnl >= 0 ? '+' : '';
sessionPnlEl.textContent = `${pnlSign}$${totalPnl.toFixed(2)}`;
sessionPnlEl.className = `fw-bold ${pnlColor}`;
// Update win rate
const winRateEl = document.getElementById('win-rate');
if (winRateEl) {
const winRate = sessionMetrics.win_rate || 0;
const winCount = sessionMetrics.win_count || 0;
const totalTrades = sessionMetrics.total_trades || 0;
winRateEl.textContent = `${winRate.toFixed(1)}% (${winCount}/${totalTrades})`;
}
}
} catch (error) {
console.error('Error updating position state display:', error);
}
}
// Make function globally accessible for WebSocket handler
window.updatePositionStateDisplay = updatePositionStateDisplay;
function updatePredictionHistory() {
const historyDiv = document.getElementById('prediction-history');
if (predictionHistory.length === 0) {