diff --git a/.aider.conf.yml b/.aider.conf.yml index dcf854a..75cac91 100644 --- a/.aider.conf.yml +++ b/.aider.conf.yml @@ -1,24 +1,6 @@ # Aider configuration file # For more information, see: https://aider.chat/docs/config/aider_conf.html -<<<<<<< HEAD -# To use the custom OpenAI-compatible endpoint from hyperbolic.xyz -# Set the model and the API base URL. -# model: Qwen/Qwen3-Coder-480B-A35B-Instruct -model: lm_studio/gpt-oss-120b -openai-api-base: http://127.0.0.1:1234/v1 -openai-api-key: "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" -model-metadata-file: .aider.model.metadata.json - -# The API key is now set directly in this file. -# Please replace "your-api-key-from-the-curl-command" with the actual bearer token. -# -# Alternatively, for better security, you can remove the openai-api-key line -# from this file and set it as an environment variable. To do so on Windows, -# run the following command in PowerShell and then RESTART YOUR SHELL: -# -# setx OPENAI_API_KEY "your-api-key-from-the-curl-command" -======= # Configure for Hyperbolic API (OpenAI-compatible endpoint) # hyperbolic model: openai/Qwen/Qwen3-Coder-480B-A35B-Instruct @@ -41,4 +23,3 @@ gitignore: false # The metadata file is still needed to inform aider about the # context window and costs for this custom model. model-metadata-file: .aider.model.metadata.json ->>>>>>> d49a473ed6f4aef55bfdd47d6370e53582be6b7b diff --git a/.aider.model.metadata.json b/.aider.model.metadata.json index 30dc3f4..d498008 100644 --- a/.aider.model.metadata.json +++ b/.aider.model.metadata.json @@ -1,6 +1,5 @@ { -<<<<<<< HEAD - "Qwen/Qwen3-Coder-480B-A35B-Instruct": { + "hyperbolic/Qwen/Qwen3-Coder-480B-A35B-Instruct": { "context_window": 262144, "input_cost_per_token": 0.000002, "output_cost_per_token": 0.000002 @@ -9,11 +8,5 @@ "context_window": 106858, "input_cost_per_token": 0.00000015, "output_cost_per_token": 0.00000075 -======= - "hyperbolic/Qwen/Qwen3-Coder-480B-A35B-Instruct": { - "context_window": 262144, - "input_cost_per_token": 0.000002, - "output_cost_per_token": 0.000002 ->>>>>>> d49a473ed6f4aef55bfdd47d6370e53582be6b7b } } \ No newline at end of file diff --git a/.gitignore b/.gitignore index ec9670f..e10c81a 100644 --- a/.gitignore +++ b/.gitignore @@ -38,16 +38,13 @@ NN/models/saved/hybrid_stats_20250409_022901.json *.png closed_trades_history.json data/cnn_training/cnn_training_data* -testcases/* -testcases/negative/case_index.json chrome_user_data/* .aider* !.aider.conf.yml !.aider.model.metadata.json .env -<<<<<<< HEAD -venv/* +venv/ wandb/ *.wandb @@ -59,9 +56,6 @@ mcp_servers/* data/prediction_snapshots/* reports/backtest_* data/prediction_snapshots/snapshots.db -======= -.env training_data/* data/trading_system.db /data/trading_system.db ->>>>>>> d49a473ed6f4aef55bfdd47d6370e53582be6b7b diff --git a/.kiro/specs/manual-trade-annotation-ui/tasks.md b/.kiro/specs/manual-trade-annotation-ui/tasks.md index 76c3578..760e1ed 100644 --- a/.kiro/specs/manual-trade-annotation-ui/tasks.md +++ b/.kiro/specs/manual-trade-annotation-ui/tasks.md @@ -1,6 +1,9 @@ # Implementation Plan -- [ ] 1. Set up project structure and base templates +- [x] 1. Set up project structure and base templates + + + - Create directory structure for templates, static files, and core modules - Create base HTML template with dark theme styling - Set up Flask/Dash application skeleton with template rendering diff --git a/TESTCASES/README.md b/TESTCASES/README.md new file mode 100644 index 0000000..d383434 --- /dev/null +++ b/TESTCASES/README.md @@ -0,0 +1,103 @@ +# Manual Trade Annotation UI + +A web-based interface for manually marking profitable buy/sell signals on historical market data to generate training test cases for machine learning models. + +## Overview + +This tool allows traders to: +- View multi-timeframe candlestick charts +- Navigate through historical data +- Mark entry and exit points for trades +- Generate test cases in realtime format +- Train models with annotated data +- Simulate inference to measure model performance + +## Project Structure + +``` +TESTCASES/ +├── web/ # Web application +│ ├── app.py # Main Flask/Dash application +│ ├── templates/ # Jinja2 HTML templates +│ │ ├── base_layout.html +│ │ ├── annotation_dashboard.html +│ │ └── components/ +│ └── static/ # Static assets +│ ├── css/ +│ ├── js/ +│ └── images/ +├── core/ # Core business logic +│ ├── annotation_manager.py +│ ├── training_simulator.py +│ └── data_loader.py +├── data/ # Data storage +│ ├── annotations/ +│ ├── test_cases/ +│ └── training_results/ +└── tests/ # Test files +``` + +## Installation + +```bash +# Install dependencies (if not already installed) +pip install dash plotly pandas numpy + +# Run the application +python TESTCASES/web/app.py +``` + +## Usage + +1. **Start the application**: Run `python TESTCASES/web/app.py` +2. **Open browser**: Navigate to `http://localhost:8051` +3. **Select symbol and timeframe**: Choose trading pair and timeframes to display +4. **Navigate to time period**: Use date picker or scroll to find market conditions +5. **Mark trades**: Click on chart to mark entry point, click again for exit +6. **Generate test cases**: Click "Generate Test Case" to create training data +7. **Train models**: Select model and click "Train" to run training session +8. **Simulate inference**: Click "Simulate" to test model performance + +## Features + +- Multi-timeframe synchronized charts (1s, 1m, 1h, 1d) +- Interactive trade marking with P&L calculation +- Test case generation in realtime format +- Model training integration +- Inference simulation with performance metrics +- Session persistence and auto-save +- Dark theme UI + +## Integration with Main System + +This sub-project is designed to be self-contained but can be integrated with the main trading system: + +```python +# Import annotation manager in main system +from TESTCASES.core.annotation_manager import AnnotationManager + +# Import training simulator +from TESTCASES.core.training_simulator import TrainingSimulator + +# Use generated test cases in training +test_cases = annotation_manager.get_test_cases() +``` + +## Configuration + +Configuration is loaded from the main `config.yaml` file. The application uses: +- Data provider settings for historical data access +- Model paths for training integration +- Symbol and timeframe configurations + +## Development + +To add new features: +1. Update requirements in `.kiro/specs/manual-trade-annotation-ui/requirements.md` +2. Update design in `.kiro/specs/manual-trade-annotation-ui/design.md` +3. Add tasks to `.kiro/specs/manual-trade-annotation-ui/tasks.md` +4. Implement changes following the task list + +## License + +Part of the AI Trading System project. diff --git a/TESTCASES/core/__init__.py b/TESTCASES/core/__init__.py new file mode 100644 index 0000000..f0daa1e --- /dev/null +++ b/TESTCASES/core/__init__.py @@ -0,0 +1,5 @@ +""" +TESTCASES Core Module + +Core business logic for the Manual Trade Annotation UI +""" diff --git a/TESTCASES/core/annotation_manager.py b/TESTCASES/core/annotation_manager.py new file mode 100644 index 0000000..10193ea --- /dev/null +++ b/TESTCASES/core/annotation_manager.py @@ -0,0 +1,239 @@ +""" +Annotation Manager - Manages trade annotations and test case generation + +Handles storage, retrieval, and test case generation from manual trade annotations. +""" + +import json +import uuid +from pathlib import Path +from datetime import datetime +from typing import List, Dict, Optional, Any +from dataclasses import dataclass, asdict +import logging + +logger = logging.getLogger(__name__) + + +@dataclass +class TradeAnnotation: + """Represents a manually marked trade""" + annotation_id: str + symbol: str + timeframe: str + entry: Dict[str, Any] # {timestamp, price, index} + exit: Dict[str, Any] # {timestamp, price, index} + direction: str # 'LONG' or 'SHORT' + profit_loss_pct: float + notes: str = "" + created_at: str = None + market_context: Dict[str, Any] = None + + def __post_init__(self): + if self.created_at is None: + self.created_at = datetime.now().isoformat() + if self.market_context is None: + self.market_context = {} + + +class AnnotationManager: + """Manages trade annotations and test case generation""" + + def __init__(self, storage_path: str = "TESTCASES/data/annotations"): + """Initialize annotation manager""" + self.storage_path = Path(storage_path) + self.storage_path.mkdir(parents=True, exist_ok=True) + + self.annotations_file = self.storage_path / "annotations_db.json" + self.test_cases_dir = self.storage_path.parent / "test_cases" + self.test_cases_dir.mkdir(parents=True, exist_ok=True) + + self.annotations_db = self._load_annotations() + + logger.info(f"AnnotationManager initialized with storage: {self.storage_path}") + + def _load_annotations(self) -> Dict[str, List[Dict]]: + """Load annotations from storage""" + if self.annotations_file.exists(): + try: + with open(self.annotations_file, 'r') as f: + data = json.load(f) + logger.info(f"Loaded {len(data.get('annotations', []))} annotations") + return data + except Exception as e: + logger.error(f"Error loading annotations: {e}") + return {"annotations": [], "metadata": {}} + else: + return {"annotations": [], "metadata": {}} + + def _save_annotations(self): + """Save annotations to storage""" + try: + # Update metadata + self.annotations_db["metadata"] = { + "total_annotations": len(self.annotations_db["annotations"]), + "last_updated": datetime.now().isoformat() + } + + with open(self.annotations_file, 'w') as f: + json.dump(self.annotations_db, f, indent=2) + + logger.info(f"Saved {len(self.annotations_db['annotations'])} annotations") + except Exception as e: + logger.error(f"Error saving annotations: {e}") + raise + + def create_annotation(self, entry_point: Dict, exit_point: Dict, + symbol: str, timeframe: str) -> TradeAnnotation: + """Create new trade annotation""" + # Calculate direction and P&L + entry_price = entry_point['price'] + exit_price = exit_point['price'] + + if exit_price > entry_price: + direction = 'LONG' + profit_loss_pct = ((exit_price - entry_price) / entry_price) * 100 + else: + direction = 'SHORT' + profit_loss_pct = ((entry_price - exit_price) / entry_price) * 100 + + annotation = TradeAnnotation( + annotation_id=str(uuid.uuid4()), + symbol=symbol, + timeframe=timeframe, + entry=entry_point, + exit=exit_point, + direction=direction, + profit_loss_pct=profit_loss_pct + ) + + logger.info(f"Created annotation: {annotation.annotation_id} ({direction}, {profit_loss_pct:.2f}%)") + return annotation + + def save_annotation(self, annotation: TradeAnnotation): + """Save annotation to storage""" + # Convert to dict + ann_dict = asdict(annotation) + + # Add to database + self.annotations_db["annotations"].append(ann_dict) + + # Save to file + self._save_annotations() + + logger.info(f"Saved annotation: {annotation.annotation_id}") + + def get_annotations(self, symbol: str = None, + timeframe: str = None) -> List[TradeAnnotation]: + """Retrieve annotations with optional filtering""" + annotations = self.annotations_db.get("annotations", []) + + # Filter by symbol + if symbol: + annotations = [a for a in annotations if a.get('symbol') == symbol] + + # Filter by timeframe + if timeframe: + annotations = [a for a in annotations if a.get('timeframe') == timeframe] + + # Convert to TradeAnnotation objects + result = [] + for ann_dict in annotations: + try: + annotation = TradeAnnotation(**ann_dict) + result.append(annotation) + except Exception as e: + logger.error(f"Error converting annotation: {e}") + + return result + + def delete_annotation(self, annotation_id: str): + """Delete annotation""" + original_count = len(self.annotations_db["annotations"]) + self.annotations_db["annotations"] = [ + a for a in self.annotations_db["annotations"] + if a.get('annotation_id') != annotation_id + ] + + if len(self.annotations_db["annotations"]) < original_count: + self._save_annotations() + logger.info(f"Deleted annotation: {annotation_id}") + else: + logger.warning(f"Annotation not found: {annotation_id}") + + def generate_test_case(self, annotation: TradeAnnotation) -> Dict: + """Generate test case from annotation in realtime format""" + # This will be populated with actual market data in Task 2 + test_case = { + "test_case_id": f"annotation_{annotation.annotation_id}", + "symbol": annotation.symbol, + "timestamp": annotation.entry['timestamp'], + "action": "BUY" if annotation.direction == "LONG" else "SELL", + "market_state": { + # Will be populated with BaseDataInput structure + "ohlcv_1s": [], + "ohlcv_1m": [], + "ohlcv_1h": [], + "ohlcv_1d": [], + "cob_data": {}, + "technical_indicators": {}, + "pivot_points": [] + }, + "expected_outcome": { + "direction": annotation.direction, + "profit_loss_pct": annotation.profit_loss_pct, + "holding_period_seconds": self._calculate_holding_period(annotation), + "exit_price": annotation.exit['price'] + }, + "annotation_metadata": { + "annotator": "manual", + "confidence": 1.0, + "notes": annotation.notes, + "created_at": annotation.created_at + } + } + + # Save test case to file + test_case_file = self.test_cases_dir / f"{test_case['test_case_id']}.json" + with open(test_case_file, 'w') as f: + json.dump(test_case, f, indent=2) + + logger.info(f"Generated test case: {test_case['test_case_id']}") + return test_case + + def _calculate_holding_period(self, annotation: TradeAnnotation) -> float: + """Calculate holding period in seconds""" + try: + entry_time = datetime.fromisoformat(annotation.entry['timestamp'].replace('Z', '+00:00')) + exit_time = datetime.fromisoformat(annotation.exit['timestamp'].replace('Z', '+00:00')) + return (exit_time - entry_time).total_seconds() + except Exception as e: + logger.error(f"Error calculating holding period: {e}") + return 0.0 + + def export_annotations(self, annotations: List[TradeAnnotation] = None, + format_type: str = 'json') -> Path: + """Export annotations to file""" + if annotations is None: + annotations = self.get_annotations() + + # Convert to dicts + export_data = [asdict(ann) for ann in annotations] + + # Create export file + timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') + export_file = self.storage_path / f"export_{timestamp}.{format_type}" + + if format_type == 'json': + with open(export_file, 'w') as f: + json.dump(export_data, f, indent=2) + elif format_type == 'csv': + import csv + with open(export_file, 'w', newline='') as f: + if export_data: + writer = csv.DictWriter(f, fieldnames=export_data[0].keys()) + writer.writeheader() + writer.writerows(export_data) + + logger.info(f"Exported {len(annotations)} annotations to {export_file}") + return export_file diff --git a/TESTCASES/core/training_simulator.py b/TESTCASES/core/training_simulator.py new file mode 100644 index 0000000..cf26372 --- /dev/null +++ b/TESTCASES/core/training_simulator.py @@ -0,0 +1,166 @@ +""" +Training Simulator - Handles model loading, training, and inference simulation + +Integrates with the main system's orchestrator and models for training and testing. +""" + +import logging +import uuid +import time +from typing import Dict, List, Optional, Any +from dataclasses import dataclass, asdict +from datetime import datetime +from pathlib import Path +import json + +logger = logging.getLogger(__name__) + + +@dataclass +class TrainingResults: + """Results from training session""" + training_id: str + model_name: str + test_cases_used: int + epochs_completed: int + final_loss: float + training_duration_seconds: float + checkpoint_path: str + metrics: Dict[str, float] + status: str = "completed" + + +@dataclass +class InferenceResults: + """Results from inference simulation""" + annotation_id: str + model_name: str + predictions: List[Dict] + accuracy: float + precision: float + recall: float + f1_score: float + confusion_matrix: Dict + prediction_timeline: List[Dict] + + +class TrainingSimulator: + """Simulates training and inference on annotated data""" + + def __init__(self, orchestrator=None): + """Initialize training simulator""" + self.orchestrator = orchestrator + self.model_cache = {} + self.training_sessions = {} + + # Storage for training results + self.results_dir = Path("TESTCASES/data/training_results") + self.results_dir.mkdir(parents=True, exist_ok=True) + + logger.info("TrainingSimulator initialized") + + def load_model(self, model_name: str): + """Load model from orchestrator""" + if model_name in self.model_cache: + return self.model_cache[model_name] + + if not self.orchestrator: + logger.error("Orchestrator not available") + return None + + # Get model from orchestrator + # This will be implemented when we integrate with actual models + logger.info(f"Loading model: {model_name}") + return None + + def start_training(self, model_name: str, test_cases: List[Dict]) -> str: + """Start training session with test cases""" + training_id = str(uuid.uuid4()) + + # Create training session + self.training_sessions[training_id] = { + 'status': 'running', + 'model_name': model_name, + 'test_cases_count': len(test_cases), + 'current_epoch': 0, + 'total_epochs': 50, + 'current_loss': 0.0, + 'start_time': time.time() + } + + logger.info(f"Started training session: {training_id}") + + # TODO: Implement actual training in background thread + # For now, simulate training completion + self._simulate_training(training_id) + + return training_id + + def _simulate_training(self, training_id: str): + """Simulate training progress (placeholder)""" + import threading + + def train(): + session = self.training_sessions[training_id] + total_epochs = session['total_epochs'] + + for epoch in range(total_epochs): + time.sleep(0.1) # Simulate training time + session['current_epoch'] = epoch + 1 + session['current_loss'] = 1.0 / (epoch + 1) # Decreasing loss + + # Mark as completed + session['status'] = 'completed' + session['final_loss'] = session['current_loss'] + session['duration_seconds'] = time.time() - session['start_time'] + session['accuracy'] = 0.85 + + logger.info(f"Training completed: {training_id}") + + thread = threading.Thread(target=train, daemon=True) + thread.start() + + def get_training_progress(self, training_id: str) -> Dict: + """Get training progress""" + if training_id not in self.training_sessions: + return { + 'status': 'not_found', + 'error': 'Training session not found' + } + + return self.training_sessions[training_id] + + def simulate_inference(self, annotation_id: str, model_name: str) -> InferenceResults: + """Simulate inference on annotated period""" + # Placeholder implementation + logger.info(f"Simulating inference for annotation: {annotation_id}") + + # Generate dummy predictions + predictions = [] + for i in range(10): + predictions.append({ + 'timestamp': datetime.now().isoformat(), + 'predicted_action': 'BUY' if i % 2 == 0 else 'SELL', + 'confidence': 0.7 + (i * 0.02), + 'actual_action': 'BUY' if i % 2 == 0 else 'SELL', + 'correct': True + }) + + results = InferenceResults( + annotation_id=annotation_id, + model_name=model_name, + predictions=predictions, + accuracy=0.85, + precision=0.82, + recall=0.88, + f1_score=0.85, + confusion_matrix={ + 'tp_buy': 4, + 'fn_buy': 1, + 'fp_sell': 1, + 'tn_sell': 4 + }, + prediction_timeline=predictions + ) + + return results diff --git a/TESTCASES/web/app.py b/TESTCASES/web/app.py new file mode 100644 index 0000000..344d5a0 --- /dev/null +++ b/TESTCASES/web/app.py @@ -0,0 +1,400 @@ +""" +Manual Trade Annotation UI - Main Application + +A web-based interface for manually marking profitable buy/sell signals on historical +market data to generate training test cases for machine learning models. +""" + +import os +import sys +from pathlib import Path + +# Add parent directory to path for imports +parent_dir = Path(__file__).parent.parent.parent +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 +import json + +# Import core components from main system +try: + from core.data_provider import DataProvider + from core.orchestrator import TradingOrchestrator + from core.config import get_config +except ImportError as e: + print(f"Warning: Could not import main system components: {e}") + print("Running in standalone mode with limited functionality") + DataProvider = None + TradingOrchestrator = None + get_config = lambda: {} + +# Import TESTCASES modules +testcases_dir = Path(__file__).parent.parent +sys.path.insert(0, str(testcases_dir)) + +try: + from core.annotation_manager import AnnotationManager + from core.training_simulator import TrainingSimulator +except ImportError: + # Try alternative import path + import importlib.util + + # Load annotation_manager + ann_spec = importlib.util.spec_from_file_location( + "annotation_manager", + testcases_dir / "core" / "annotation_manager.py" + ) + ann_module = importlib.util.module_from_spec(ann_spec) + ann_spec.loader.exec_module(ann_module) + AnnotationManager = ann_module.AnnotationManager + + # Load training_simulator + train_spec = importlib.util.spec_from_file_location( + "training_simulator", + testcases_dir / "core" / "training_simulator.py" + ) + train_module = importlib.util.module_from_spec(train_spec) + train_spec.loader.exec_module(train_module) + TrainingSimulator = train_module.TrainingSimulator + +# Setup logging +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + +class AnnotationDashboard: + """Main annotation dashboard application""" + + def __init__(self): + """Initialize the dashboard""" + # Load configuration + self.config = get_config() if get_config else {} + + # Initialize Flask app + self.server = Flask( + __name__, + template_folder='templates', + static_folder='static' + ) + + # Initialize Dash app + self.app = Dash( + __name__, + server=self.server, + url_base_pathname='/dash/', + external_stylesheets=[ + 'https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css', + 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css' + ] + ) + + # Initialize core components + self.data_provider = DataProvider() if DataProvider else None + self.orchestrator = TradingOrchestrator( + data_provider=self.data_provider + ) if TradingOrchestrator and self.data_provider else None + + # Initialize TESTCASES components + self.annotation_manager = AnnotationManager() + self.training_simulator = TrainingSimulator(self.orchestrator) if self.orchestrator else None + + # Setup routes + self._setup_routes() + + logger.info("Annotation Dashboard initialized") + + def _setup_routes(self): + """Setup Flask routes""" + + @self.server.route('/') + def index(): + """Main dashboard page""" + # Get current annotations + annotations = self.annotation_manager.get_annotations() + + # Prepare template data + template_data = { + 'current_symbol': 'ETH/USDT', + 'timeframes': ['1s', '1m', '1h', '1d'], + 'annotations': [ann.__dict__ if hasattr(ann, '__dict__') else ann + for ann in annotations] + } + + return render_template('annotation_dashboard.html', **template_data) + + @self.server.route('/api/chart-data', methods=['POST']) + def get_chart_data(): + """Get chart data for specified symbol and timeframes""" + try: + data = request.get_json() + symbol = data.get('symbol', 'ETH/USDT') + timeframes = data.get('timeframes', ['1s', '1m', '1h', '1d']) + start_time = data.get('start_time') + end_time = data.get('end_time') + + if not self.data_provider: + return jsonify({ + 'success': False, + 'error': { + 'code': 'DATA_PROVIDER_UNAVAILABLE', + 'message': 'Data provider not available' + } + }) + + # Fetch data for each timeframe + chart_data = {} + for timeframe in timeframes: + df = self.data_provider.get_historical_data( + symbol=symbol, + timeframe=timeframe, + limit=500 + ) + + if df is not None and not df.empty: + # Convert to format suitable for Plotly + chart_data[timeframe] = { + 'timestamps': df.index.strftime('%Y-%m-%d %H:%M:%S').tolist(), + 'open': df['open'].tolist(), + 'high': df['high'].tolist(), + 'low': df['low'].tolist(), + 'close': df['close'].tolist(), + 'volume': df['volume'].tolist() + } + + return jsonify({ + 'success': True, + 'chart_data': chart_data + }) + + except Exception as e: + logger.error(f"Error fetching chart data: {e}") + return jsonify({ + 'success': False, + 'error': { + 'code': 'CHART_DATA_ERROR', + 'message': str(e) + } + }) + + @self.server.route('/api/save-annotation', methods=['POST']) + def save_annotation(): + """Save a new annotation""" + try: + data = request.get_json() + + # Create annotation + annotation = self.annotation_manager.create_annotation( + entry_point=data['entry'], + exit_point=data['exit'], + symbol=data['symbol'], + timeframe=data['timeframe'] + ) + + # Save annotation + self.annotation_manager.save_annotation(annotation) + + return jsonify({ + 'success': True, + 'annotation': annotation.__dict__ if hasattr(annotation, '__dict__') else annotation + }) + + except Exception as e: + logger.error(f"Error saving annotation: {e}") + return jsonify({ + 'success': False, + 'error': { + 'code': 'SAVE_ANNOTATION_ERROR', + 'message': str(e) + } + }) + + @self.server.route('/api/delete-annotation', methods=['POST']) + def delete_annotation(): + """Delete an annotation""" + try: + data = request.get_json() + annotation_id = data['annotation_id'] + + self.annotation_manager.delete_annotation(annotation_id) + + return jsonify({'success': True}) + + except Exception as e: + logger.error(f"Error deleting annotation: {e}") + return jsonify({ + 'success': False, + 'error': { + 'code': 'DELETE_ANNOTATION_ERROR', + 'message': str(e) + } + }) + + @self.server.route('/api/generate-test-case', methods=['POST']) + def generate_test_case(): + """Generate test case from annotation""" + try: + data = request.get_json() + annotation_id = data['annotation_id'] + + # Get annotation + annotations = self.annotation_manager.get_annotations() + annotation = next((a for a in annotations + if (a.annotation_id if hasattr(a, 'annotation_id') + else a.get('annotation_id')) == annotation_id), None) + + if not annotation: + return jsonify({ + 'success': False, + 'error': { + 'code': 'ANNOTATION_NOT_FOUND', + 'message': 'Annotation not found' + } + }) + + # Generate test case + test_case = self.annotation_manager.generate_test_case(annotation) + + return jsonify({ + 'success': True, + 'test_case': test_case + }) + + except Exception as e: + logger.error(f"Error generating test case: {e}") + return jsonify({ + 'success': False, + 'error': { + 'code': 'GENERATE_TESTCASE_ERROR', + 'message': str(e) + } + }) + + @self.server.route('/api/export-annotations', methods=['POST']) + def export_annotations(): + """Export annotations to file""" + try: + data = request.get_json() + symbol = data.get('symbol') + format_type = data.get('format', 'json') + + # Get annotations + annotations = self.annotation_manager.get_annotations(symbol=symbol) + + # Export to file + output_path = self.annotation_manager.export_annotations( + annotations=annotations, + format_type=format_type + ) + + return send_file(output_path, as_attachment=True) + + except Exception as e: + logger.error(f"Error exporting annotations: {e}") + return jsonify({ + 'success': False, + 'error': { + 'code': 'EXPORT_ERROR', + 'message': str(e) + } + }) + + @self.server.route('/api/train-model', methods=['POST']) + def train_model(): + """Start model training with annotations""" + try: + if not self.training_simulator: + return jsonify({ + 'success': False, + 'error': { + 'code': 'TRAINING_UNAVAILABLE', + 'message': 'Training simulator not available' + } + }) + + data = request.get_json() + model_name = data['model_name'] + annotation_ids = data['annotation_ids'] + + # Get annotations + annotations = self.annotation_manager.get_annotations() + selected_annotations = [a for a in annotations + if (a.annotation_id if hasattr(a, 'annotation_id') + else a.get('annotation_id')) in annotation_ids] + + # Generate test cases + test_cases = [self.annotation_manager.generate_test_case(ann) + for ann in selected_annotations] + + # Start training + training_id = self.training_simulator.start_training( + model_name=model_name, + test_cases=test_cases + ) + + return jsonify({ + 'success': True, + 'training_id': training_id + }) + + except Exception as e: + logger.error(f"Error starting training: {e}") + return jsonify({ + 'success': False, + 'error': { + 'code': 'TRAINING_ERROR', + 'message': str(e) + } + }) + + @self.server.route('/api/training-progress', methods=['POST']) + def get_training_progress(): + """Get training progress""" + try: + if not self.training_simulator: + return jsonify({ + 'success': False, + 'error': { + 'code': 'TRAINING_UNAVAILABLE', + 'message': 'Training simulator not available' + } + }) + + data = request.get_json() + training_id = data['training_id'] + + progress = self.training_simulator.get_training_progress(training_id) + + return jsonify({ + 'success': True, + 'progress': progress + }) + + except Exception as e: + logger.error(f"Error getting training progress: {e}") + return jsonify({ + 'success': False, + 'error': { + 'code': 'PROGRESS_ERROR', + 'message': str(e) + } + }) + + def run(self, host='127.0.0.1', port=8051, debug=False): + """Run the application""" + logger.info(f"Starting Annotation Dashboard on http://{host}:{port}") + self.server.run(host=host, port=port, debug=debug) + + +def main(): + """Main entry point""" + dashboard = AnnotationDashboard() + dashboard.run(debug=True) + + +if __name__ == '__main__': + main() diff --git a/TESTCASES/web/static/css/annotation_ui.css b/TESTCASES/web/static/css/annotation_ui.css new file mode 100644 index 0000000..1b6d698 --- /dev/null +++ b/TESTCASES/web/static/css/annotation_ui.css @@ -0,0 +1,334 @@ +/* Annotation UI Specific Styles */ + +/* Main Layout */ +.main-content { + padding-top: 1rem; + padding-bottom: 1rem; + min-height: calc(100vh - 120px); +} + +/* Chart Panel */ +.chart-panel { + height: calc(100vh - 150px); +} + +.chart-panel .card-body { + height: calc(100% - 60px); + overflow: hidden; +} + +#chart-container { + height: 100%; + overflow-y: auto; + overflow-x: hidden; +} + +.timeframe-chart { + margin-bottom: 1rem; + border: 1px solid var(--border-color); + border-radius: 4px; + background-color: var(--bg-tertiary); +} + +.chart-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.5rem 1rem; + background-color: var(--bg-secondary); + border-bottom: 1px solid var(--border-color); +} + +.timeframe-label { + font-weight: 600; + font-size: 0.875rem; + color: var(--text-primary); +} + +.chart-info { + font-size: 0.75rem; + color: var(--text-secondary); +} + +.chart-plot { + height: 300px; + padding: 0.5rem; +} + +.chart-loading { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + text-align: center; + z-index: 1000; + background-color: rgba(17, 24, 39, 0.9); + padding: 2rem; + border-radius: 8px; +} + +/* Control Panel */ +.control-panel { + position: sticky; + top: 1rem; + max-height: calc(100vh - 150px); + overflow-y: auto; +} + +.control-panel .card-body { + padding: 1rem; +} + +.control-panel .form-label { + font-size: 0.875rem; + font-weight: 600; + margin-bottom: 0.5rem; +} + +.control-panel .form-select, +.control-panel .form-control { + font-size: 0.875rem; +} + +.control-panel .btn-group-vertical .btn { + text-align: left; +} + +/* Annotation List */ +.annotation-list { + position: sticky; + top: 1rem; + max-height: 400px; +} + +.annotation-list .card-body { + padding: 0; + max-height: 350px; + overflow-y: auto; +} + +.annotation-list .list-group-item { + cursor: pointer; + transition: background-color 0.2s; +} + +.annotation-list .list-group-item:hover { + background-color: var(--bg-tertiary) !important; +} + +.annotation-list .btn-group-vertical { + min-width: 40px; +} + +/* Training Panel */ +.training-panel { + position: sticky; + top: 420px; +} + +.training-panel .card-body { + padding: 1rem; +} + +/* Inference Panel */ +.inference-panel { + padding: 1rem; +} + +#inference-chart { + background-color: var(--bg-tertiary); + border-radius: 4px; + border: 1px solid var(--border-color); +} + +.inference-panel .table-responsive { + border: 1px solid var(--border-color); + border-radius: 4px; +} + +/* Annotation Markers on Charts */ +.annotation-marker-entry { + color: #10b981; + font-size: 20px; +} + +.annotation-marker-exit { + color: #ef4444; + font-size: 20px; +} + +.annotation-line { + stroke: #3b82f6; + stroke-width: 2; + stroke-dasharray: 5, 5; +} + +.annotation-pnl-label { + font-size: 12px; + font-weight: 600; +} + +/* Prediction Markers */ +.prediction-marker-correct { + color: #10b981; + font-size: 16px; +} + +.prediction-marker-incorrect { + color: #ef4444; + font-size: 16px; +} + +/* Crosshair Cursor */ +.chart-plot:hover { + cursor: crosshair; +} + +/* Fullscreen Mode */ +#chart-container:fullscreen { + background-color: var(--bg-primary); + padding: 1rem; +} + +#chart-container:-webkit-full-screen { + background-color: var(--bg-primary); + padding: 1rem; +} + +#chart-container:-moz-full-screen { + background-color: var(--bg-primary); + padding: 1rem; +} + +/* Responsive Adjustments */ +@media (max-width: 1200px) { + .chart-plot { + height: 250px; + } +} + +@media (max-width: 768px) { + .main-content { + padding-left: 0.5rem; + padding-right: 0.5rem; + } + + .chart-plot { + height: 200px; + } + + .control-panel, + .annotation-list, + .training-panel { + position: relative; + top: 0; + margin-bottom: 1rem; + } +} + +/* Animation for Loading States */ +@keyframes pulse { + 0%, 100% { + opacity: 1; + } + 50% { + opacity: 0.5; + } +} + +.loading-pulse { + animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; +} + +/* Highlight Effect for Selected Annotation */ +.annotation-highlighted { + animation: highlight-flash 1s ease-in-out; +} + +@keyframes highlight-flash { + 0%, 100% { + background-color: var(--bg-secondary); + } + 50% { + background-color: rgba(59, 130, 246, 0.3); + } +} + +/* Status Indicators */ +.status-indicator { + display: inline-block; + width: 8px; + height: 8px; + border-radius: 50%; + margin-right: 0.5rem; +} + +.status-indicator.active { + background-color: var(--accent-success); + box-shadow: 0 0 8px var(--accent-success); +} + +.status-indicator.inactive { + background-color: var(--text-muted); +} + +.status-indicator.error { + background-color: var(--accent-danger); + box-shadow: 0 0 8px var(--accent-danger); +} + +/* Metric Cards */ +.metric-card { + transition: transform 0.2s; +} + +.metric-card:hover { + transform: translateY(-2px); +} + +/* Confusion Matrix Styling */ +.confusion-matrix-cell { + font-weight: 600; + font-size: 1.25rem; +} + +/* Timeline Table Styling */ +#prediction-timeline-body tr:last-child { + background-color: rgba(59, 130, 246, 0.1); +} + +/* Custom Scrollbar for Panels */ +.control-panel::-webkit-scrollbar, +.annotation-list .card-body::-webkit-scrollbar, +.inference-panel .table-responsive::-webkit-scrollbar { + width: 6px; +} + +/* Keyboard Shortcut Hints */ +.keyboard-hint { + display: inline-block; + padding: 0.25rem 0.5rem; + background-color: var(--bg-tertiary); + border: 1px solid var(--border-color); + border-radius: 4px; + font-family: monospace; + font-size: 0.75rem; + margin: 0 0.25rem; +} + +/* Chart Zoom Controls */ +.chart-zoom-controls { + position: absolute; + top: 10px; + right: 10px; + z-index: 100; +} + +/* Annotation Mode Indicator */ +.annotation-mode-active { + border: 2px solid var(--accent-success); +} + +.annotation-mode-inactive { + border: 2px solid var(--text-muted); +} diff --git a/TESTCASES/web/static/css/dark_theme.css b/TESTCASES/web/static/css/dark_theme.css new file mode 100644 index 0000000..b747250 --- /dev/null +++ b/TESTCASES/web/static/css/dark_theme.css @@ -0,0 +1,265 @@ +/* Dark Theme Styles for Manual Trade Annotation UI */ + +:root { + --bg-primary: #111827; + --bg-secondary: #1f2937; + --bg-tertiary: #374151; + --text-primary: #f8f9fa; + --text-secondary: #9ca3af; + --text-muted: #6b7280; + --border-color: #4b5563; + --accent-primary: #3b82f6; + --accent-success: #10b981; + --accent-danger: #ef4444; + --accent-warning: #f59e0b; +} + +body { + background-color: var(--bg-primary) !important; + color: var(--text-primary) !important; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; +} + +/* Cards */ +.card { + background-color: var(--bg-secondary) !important; + border: 1px solid var(--border-color) !important; + color: var(--text-primary) !important; +} + +.card-header { + background-color: var(--bg-tertiary) !important; + border-bottom: 1px solid var(--border-color) !important; + color: var(--text-primary) !important; +} + +.card-body { + background-color: var(--bg-secondary) !important; +} + +/* Tables */ +.table { + color: var(--text-primary) !important; +} + +.table-dark { + background-color: var(--bg-secondary) !important; + --bs-table-bg: var(--bg-secondary); + --bs-table-striped-bg: var(--bg-tertiary); + --bs-table-hover-bg: var(--bg-tertiary); +} + +.table-dark thead th { + border-bottom-color: var(--border-color); +} + +.table-dark tbody td { + border-color: var(--border-color); +} + +/* Forms */ +.form-control, +.form-select { + background-color: var(--bg-tertiary) !important; + border-color: var(--border-color) !important; + color: var(--text-primary) !important; +} + +.form-control:focus, +.form-select:focus { + background-color: var(--bg-tertiary) !important; + border-color: var(--accent-primary) !important; + color: var(--text-primary) !important; + box-shadow: 0 0 0 0.25rem rgba(59, 130, 246, 0.25); +} + +.form-check-input { + background-color: var(--bg-tertiary); + border-color: var(--border-color); +} + +.form-check-input:checked { + background-color: var(--accent-primary); + border-color: var(--accent-primary); +} + +.form-label { + color: var(--text-primary); +} + +/* Buttons */ +.btn-outline-light { + color: var(--text-primary); + border-color: var(--border-color); +} + +.btn-outline-light:hover { + background-color: var(--bg-tertiary); + border-color: var(--border-color); + color: var(--text-primary); +} + +.btn-outline-secondary { + color: var(--text-secondary); + border-color: var(--border-color); +} + +.btn-outline-secondary:hover { + background-color: var(--bg-tertiary); + border-color: var(--border-color); + color: var(--text-primary); +} + +.btn-outline-primary:hover { + background-color: var(--accent-primary); + border-color: var(--accent-primary); +} + +/* List Groups */ +.list-group-item { + background-color: var(--bg-secondary) !important; + border-color: var(--border-color) !important; + color: var(--text-primary) !important; +} + +.list-group-item-action:hover { + background-color: var(--bg-tertiary) !important; +} + +/* Alerts */ +.alert-info { + background-color: rgba(59, 130, 246, 0.1); + border-color: rgba(59, 130, 246, 0.3); + color: #93c5fd; +} + +.alert-success { + background-color: rgba(16, 185, 129, 0.1); + border-color: rgba(16, 185, 129, 0.3); + color: #6ee7b7; +} + +.alert-danger { + background-color: rgba(239, 68, 68, 0.1); + border-color: rgba(239, 68, 68, 0.3); + color: #fca5a5; +} + +.alert-warning { + background-color: rgba(245, 158, 11, 0.1); + border-color: rgba(245, 158, 11, 0.3); + color: #fcd34d; +} + +/* Badges */ +.badge { + font-weight: 500; +} + +/* Modals */ +.modal-content { + background-color: var(--bg-secondary); + border-color: var(--border-color); +} + +.modal-header { + background-color: var(--bg-tertiary); + border-bottom-color: var(--border-color); +} + +.modal-footer { + border-top-color: var(--border-color); +} + +.btn-close { + filter: invert(1); +} + +/* Progress Bars */ +.progress { + background-color: var(--bg-tertiary); +} + +/* Navbar */ +.navbar-dark { + background-color: var(--bg-secondary) !important; + border-bottom: 1px solid var(--border-color); +} + +/* Footer */ +.footer { + background-color: var(--bg-secondary) !important; + border-top: 1px solid var(--border-color); +} + +/* Text Colors */ +.text-muted { + color: var(--text-muted) !important; +} + +.text-success { + color: var(--accent-success) !important; +} + +.text-danger { + color: var(--accent-danger) !important; +} + +.text-warning { + color: var(--accent-warning) !important; +} + +/* Scrollbar Styling */ +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-track { + background: var(--bg-secondary); +} + +::-webkit-scrollbar-thumb { + background: var(--bg-tertiary); + border-radius: 4px; +} + +::-webkit-scrollbar-thumb:hover { + background: var(--border-color); +} + +/* Tooltips */ +.tooltip-inner { + background-color: var(--bg-tertiary); + color: var(--text-primary); +} + +.tooltip.bs-tooltip-top .tooltip-arrow::before { + border-top-color: var(--bg-tertiary); +} + +.tooltip.bs-tooltip-bottom .tooltip-arrow::before { + border-bottom-color: var(--bg-tertiary); +} + +/* Spinners */ +.spinner-border { + border-color: var(--accent-primary); + border-right-color: transparent; +} + +/* Toast Notifications */ +.toast { + background-color: var(--bg-secondary); + border-color: var(--border-color); +} + +.toast-header { + background-color: var(--bg-tertiary); + border-bottom-color: var(--border-color); + color: var(--text-primary); +} + +.toast-body { + color: var(--text-primary); +} diff --git a/TESTCASES/web/static/js/annotation_manager.js b/TESTCASES/web/static/js/annotation_manager.js new file mode 100644 index 0000000..f869e70 --- /dev/null +++ b/TESTCASES/web/static/js/annotation_manager.js @@ -0,0 +1,193 @@ +/** + * AnnotationManager - Manages trade marking interactions + */ + +class AnnotationManager { + constructor(chartManager) { + this.chartManager = chartManager; + this.pendingAnnotation = null; + this.enabled = true; + + console.log('AnnotationManager initialized'); + } + + /** + * Handle chart click for marking entry/exit + */ + handleChartClick(clickData) { + if (!this.enabled) { + console.log('Annotation mode disabled'); + return; + } + + if (!this.pendingAnnotation) { + // Mark entry point + this.markEntry(clickData); + } else { + // Mark exit point + this.markExit(clickData); + } + } + + /** + * Mark entry point + */ + markEntry(clickData) { + this.pendingAnnotation = { + symbol: window.appState.currentSymbol, + timeframe: clickData.timeframe, + entry: { + timestamp: clickData.timestamp, + price: clickData.price, + index: clickData.index + } + }; + + console.log('Entry marked:', this.pendingAnnotation); + + // Show pending annotation status + document.getElementById('pending-annotation-status').style.display = 'block'; + + // Visual feedback on chart + this.showPendingMarker(clickData); + } + + /** + * Mark exit point + */ + markExit(clickData) { + if (!this.pendingAnnotation) return; + + // Validate exit is after entry + const entryTime = new Date(this.pendingAnnotation.entry.timestamp); + const exitTime = new Date(clickData.timestamp); + + if (exitTime <= entryTime) { + window.showError('Exit time must be after entry time'); + return; + } + + // Complete annotation + this.pendingAnnotation.exit = { + timestamp: clickData.timestamp, + price: clickData.price, + index: clickData.index + }; + + // Calculate P&L + const entryPrice = this.pendingAnnotation.entry.price; + const exitPrice = this.pendingAnnotation.exit.price; + const direction = exitPrice > entryPrice ? 'LONG' : 'SHORT'; + const profitLossPct = ((exitPrice - entryPrice) / entryPrice) * 100; + + this.pendingAnnotation.direction = direction; + this.pendingAnnotation.profit_loss_pct = profitLossPct; + + console.log('Exit marked:', this.pendingAnnotation); + + // Save annotation + this.saveAnnotation(this.pendingAnnotation); + } + + /** + * Save annotation to server + */ + saveAnnotation(annotation) { + fetch('/api/save-annotation', { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify(annotation) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + // Add to app state + window.appState.annotations.push(data.annotation); + + // Update UI + window.renderAnnotationsList(window.appState.annotations); + + // Add to chart + this.chartManager.addAnnotation(data.annotation); + + // Clear pending annotation + this.pendingAnnotation = null; + document.getElementById('pending-annotation-status').style.display = 'none'; + + window.showSuccess('Annotation saved successfully'); + } else { + window.showError('Failed to save annotation: ' + data.error.message); + } + }) + .catch(error => { + window.showError('Network error: ' + error.message); + }); + } + + /** + * Show pending marker on chart + */ + showPendingMarker(clickData) { + // TODO: Add visual marker for pending entry + console.log('Showing pending marker at:', clickData); + } + + /** + * Mark current position (for keyboard shortcut) + */ + markCurrentPosition() { + // TODO: Implement marking at current crosshair position + console.log('Mark current position'); + } + + /** + * Enable annotation mode + */ + enable() { + this.enabled = true; + console.log('Annotation mode enabled'); + } + + /** + * Disable annotation mode + */ + disable() { + this.enabled = false; + this.pendingAnnotation = null; + document.getElementById('pending-annotation-status').style.display = 'none'; + console.log('Annotation mode disabled'); + } + + /** + * Calculate profit/loss percentage + */ + calculateProfitLoss(entryPrice, exitPrice, direction) { + if (direction === 'LONG') { + return ((exitPrice - entryPrice) / entryPrice) * 100; + } else { + return ((entryPrice - exitPrice) / entryPrice) * 100; + } + } + + /** + * Validate annotation + */ + validateAnnotation(annotation) { + if (!annotation.entry || !annotation.exit) { + return {valid: false, error: 'Missing entry or exit point'}; + } + + const entryTime = new Date(annotation.entry.timestamp); + const exitTime = new Date(annotation.exit.timestamp); + + if (exitTime <= entryTime) { + return {valid: false, error: 'Exit time must be after entry time'}; + } + + if (!annotation.entry.price || !annotation.exit.price) { + return {valid: false, error: 'Missing price data'}; + } + + return {valid: true}; + } +} diff --git a/TESTCASES/web/static/js/chart_manager.js b/TESTCASES/web/static/js/chart_manager.js new file mode 100644 index 0000000..28c0870 --- /dev/null +++ b/TESTCASES/web/static/js/chart_manager.js @@ -0,0 +1,255 @@ +/** + * ChartManager - Manages Plotly charts for multi-timeframe visualization + */ + +class ChartManager { + constructor(containerId, timeframes) { + this.containerId = containerId; + this.timeframes = timeframes; + this.charts = {}; + this.annotations = {}; + this.syncedTime = null; + + console.log('ChartManager initialized with timeframes:', timeframes); + } + + /** + * Initialize charts for all timeframes + */ + initializeCharts(chartData) { + console.log('Initializing charts with data:', chartData); + + this.timeframes.forEach(timeframe => { + if (chartData[timeframe]) { + this.createChart(timeframe, chartData[timeframe]); + } + }); + + // Enable crosshair + this.enableCrosshair(); + } + + /** + * Create a single chart for a timeframe + */ + createChart(timeframe, data) { + const plotId = `plot-${timeframe}`; + const plotElement = document.getElementById(plotId); + + if (!plotElement) { + console.error(`Plot element not found: ${plotId}`); + return; + } + + // Create candlestick trace + const candlestickTrace = { + x: data.timestamps, + open: data.open, + high: data.high, + low: data.low, + close: data.close, + type: 'candlestick', + name: timeframe, + increasing: {line: {color: '#10b981'}}, + decreasing: {line: {color: '#ef4444'}} + }; + + // Create volume trace + const volumeTrace = { + x: data.timestamps, + y: data.volume, + type: 'bar', + name: 'Volume', + yaxis: 'y2', + marker: {color: '#3b82f6', opacity: 0.3} + }; + + const layout = { + title: '', + xaxis: { + rangeslider: {visible: false}, + gridcolor: '#374151', + color: '#9ca3af' + }, + yaxis: { + title: 'Price', + gridcolor: '#374151', + color: '#9ca3af' + }, + yaxis2: { + title: 'Volume', + overlaying: 'y', + side: 'right', + showgrid: false, + color: '#9ca3af' + }, + plot_bgcolor: '#1f2937', + paper_bgcolor: '#1f2937', + font: {color: '#f8f9fa'}, + margin: {l: 50, r: 50, t: 20, b: 40}, + hovermode: 'x unified' + }; + + const config = { + responsive: true, + displayModeBar: true, + modeBarButtonsToRemove: ['lasso2d', 'select2d'], + displaylogo: false + }; + + Plotly.newPlot(plotId, [candlestickTrace, volumeTrace], layout, config); + + // Store chart reference + this.charts[timeframe] = { + plotId: plotId, + data: data, + element: plotElement + }; + + // Add click handler for annotations + plotElement.on('plotly_click', (eventData) => { + this.handleChartClick(timeframe, eventData); + }); + + console.log(`Chart created for ${timeframe}`); + } + + /** + * Handle chart click for annotation + */ + handleChartClick(timeframe, eventData) { + if (!eventData.points || eventData.points.length === 0) return; + + const point = eventData.points[0]; + const clickData = { + timeframe: timeframe, + timestamp: point.x, + price: point.close || point.y, + index: point.pointIndex + }; + + console.log('Chart clicked:', clickData); + + // Trigger annotation manager + if (window.appState && window.appState.annotationManager) { + window.appState.annotationManager.handleChartClick(clickData); + } + } + + /** + * Update charts with new data + */ + updateCharts(newData) { + Object.keys(newData).forEach(timeframe => { + if (this.charts[timeframe]) { + const plotId = this.charts[timeframe].plotId; + + Plotly.react(plotId, [ + { + x: newData[timeframe].timestamps, + open: newData[timeframe].open, + high: newData[timeframe].high, + low: newData[timeframe].low, + close: newData[timeframe].close, + type: 'candlestick' + }, + { + x: newData[timeframe].timestamps, + y: newData[timeframe].volume, + type: 'bar', + yaxis: 'y2' + } + ]); + } + }); + } + + /** + * Add annotation to charts + */ + addAnnotation(annotation) { + console.log('Adding annotation to charts:', annotation); + + // Store annotation + this.annotations[annotation.annotation_id] = annotation; + + // Add markers to relevant timeframe chart + const timeframe = annotation.timeframe; + if (this.charts[timeframe]) { + // TODO: Add visual markers using Plotly annotations + this.updateChartAnnotations(timeframe); + } + } + + /** + * Remove annotation from charts + */ + removeAnnotation(annotationId) { + if (this.annotations[annotationId]) { + const annotation = this.annotations[annotationId]; + delete this.annotations[annotationId]; + + // Update chart + if (this.charts[annotation.timeframe]) { + this.updateChartAnnotations(annotation.timeframe); + } + } + } + + /** + * Update chart annotations + */ + updateChartAnnotations(timeframe) { + // TODO: Implement annotation rendering on charts + console.log(`Updating annotations for ${timeframe}`); + } + + /** + * Highlight annotation + */ + highlightAnnotation(annotationId) { + console.log('Highlighting annotation:', annotationId); + // TODO: Implement highlight effect + } + + /** + * Enable crosshair cursor + */ + enableCrosshair() { + // Crosshair is enabled via hovermode in layout + console.log('Crosshair enabled'); + } + + /** + * Handle zoom + */ + handleZoom(zoomFactor) { + Object.values(this.charts).forEach(chart => { + Plotly.relayout(chart.plotId, { + 'xaxis.range[0]': null, + 'xaxis.range[1]': null + }); + }); + } + + /** + * Reset zoom + */ + resetZoom() { + Object.values(this.charts).forEach(chart => { + Plotly.relayout(chart.plotId, { + 'xaxis.autorange': true, + 'yaxis.autorange': true + }); + }); + } + + /** + * Synchronize time navigation across charts + */ + syncTimeNavigation(timestamp) { + this.syncedTime = timestamp; + // TODO: Implement time synchronization + console.log('Syncing charts to timestamp:', timestamp); + } +} diff --git a/TESTCASES/web/static/js/time_navigator.js b/TESTCASES/web/static/js/time_navigator.js new file mode 100644 index 0000000..4013312 --- /dev/null +++ b/TESTCASES/web/static/js/time_navigator.js @@ -0,0 +1,146 @@ +/** + * TimeNavigator - Handles time navigation and data loading + */ + +class TimeNavigator { + constructor(chartManager) { + this.chartManager = chartManager; + this.currentTime = null; + this.timeRange = '1d'; // Default 1 day range + + console.log('TimeNavigator initialized'); + } + + /** + * Navigate to specific time + */ + navigateToTime(timestamp) { + this.currentTime = timestamp; + console.log('Navigating to time:', new Date(timestamp)); + + // Load data for this time range + this.loadDataRange(timestamp); + + // Sync charts + this.chartManager.syncTimeNavigation(timestamp); + } + + /** + * Navigate to current time + */ + navigateToNow() { + const now = Date.now(); + this.navigateToTime(now); + } + + /** + * Scroll forward in time + */ + scrollForward(increment = null) { + if (!increment) { + increment = this.getIncrementForRange(); + } + + const newTime = (this.currentTime || Date.now()) + increment; + this.navigateToTime(newTime); + } + + /** + * Scroll backward in time + */ + scrollBackward(increment = null) { + if (!increment) { + increment = this.getIncrementForRange(); + } + + const newTime = (this.currentTime || Date.now()) - increment; + this.navigateToTime(newTime); + } + + /** + * Set time range + */ + setTimeRange(range) { + this.timeRange = range; + console.log('Time range set to:', range); + + // Reload data with new range + if (this.currentTime) { + this.loadDataRange(this.currentTime); + } + } + + /** + * Load data for time range + */ + loadDataRange(centerTime) { + // Show loading indicator + const loadingEl = document.getElementById('chart-loading'); + if (loadingEl) { + loadingEl.classList.remove('d-none'); + } + + // Calculate start and end times based on range + const rangeMs = this.getRangeInMs(this.timeRange); + const startTime = centerTime - (rangeMs / 2); + const endTime = centerTime + (rangeMs / 2); + + // Fetch data + fetch('/api/chart-data', { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({ + symbol: window.appState.currentSymbol, + timeframes: window.appState.currentTimeframes, + start_time: new Date(startTime).toISOString(), + end_time: new Date(endTime).toISOString() + }) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + this.chartManager.updateCharts(data.chart_data); + } else { + window.showError('Failed to load chart data: ' + data.error.message); + } + }) + .catch(error => { + window.showError('Network error: ' + error.message); + }) + .finally(() => { + if (loadingEl) { + loadingEl.classList.add('d-none'); + } + }); + } + + /** + * Get increment for current range + */ + getIncrementForRange() { + const rangeMs = this.getRangeInMs(this.timeRange); + return rangeMs / 10; // Move by 10% of range + } + + /** + * Convert range string to milliseconds + */ + getRangeInMs(range) { + const units = { + '1h': 60 * 60 * 1000, + '4h': 4 * 60 * 60 * 1000, + '1d': 24 * 60 * 60 * 1000, + '1w': 7 * 24 * 60 * 60 * 1000 + }; + + return units[range] || units['1d']; + } + + /** + * Setup keyboard shortcuts + */ + setupKeyboardShortcuts() { + // Keyboard shortcuts are handled in the main template + console.log('Keyboard shortcuts ready'); + } +} diff --git a/TESTCASES/web/static/js/training_controller.js b/TESTCASES/web/static/js/training_controller.js new file mode 100644 index 0000000..d94d6a6 --- /dev/null +++ b/TESTCASES/web/static/js/training_controller.js @@ -0,0 +1,102 @@ +/** + * TrainingController - Manages training and inference simulation + */ + +class TrainingController { + constructor() { + this.currentTrainingId = null; + this.inferenceState = null; + + console.log('TrainingController initialized'); + } + + /** + * Start training session + */ + startTraining(modelName, annotationIds) { + console.log('Starting training:', modelName, annotationIds); + + // Training is initiated from the training panel + // This method can be used for additional training logic + } + + /** + * Simulate inference on annotations + */ + simulateInference(modelName, annotations) { + console.log('Simulating inference:', modelName, annotations.length, 'annotations'); + + // Prepare inference request + const annotationIds = annotations.map(a => + a.annotation_id || a.get('annotation_id') + ); + + // Start inference simulation + fetch('/api/simulate-inference', { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({ + model_name: modelName, + annotation_ids: annotationIds + }) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + this.displayInferenceResults(data.results); + } else { + window.showError('Failed to simulate inference: ' + data.error.message); + } + }) + .catch(error => { + window.showError('Network error: ' + error.message); + }); + } + + /** + * Display inference results + */ + displayInferenceResults(results) { + console.log('Displaying inference results:', results); + + // Update metrics + if (results.metrics) { + window.updateMetrics(results.metrics); + } + + // Update prediction timeline + if (results.predictions) { + window.inferenceState = { + isPlaying: false, + currentIndex: 0, + predictions: results.predictions, + annotations: window.appState.annotations, + speed: 1 + }; + } + + window.showSuccess('Inference simulation complete'); + } + + /** + * Get training status + */ + getTrainingStatus(trainingId) { + return fetch('/api/training-progress', { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({training_id: trainingId}) + }) + .then(response => response.json()); + } + + /** + * Cancel training + */ + cancelTraining(trainingId) { + console.log('Canceling training:', trainingId); + + // TODO: Implement training cancellation + window.showError('Training cancellation not yet implemented'); + } +} diff --git a/TESTCASES/web/templates/annotation_dashboard.html b/TESTCASES/web/templates/annotation_dashboard.html new file mode 100644 index 0000000..3de6efe --- /dev/null +++ b/TESTCASES/web/templates/annotation_dashboard.html @@ -0,0 +1,173 @@ +{% extends "base_layout.html" %} + +{% block title %}Trade Annotation Dashboard{% endblock %} + +{% block content %} +
No annotations yet
+Click on charts to create
+Loading chart data...
++ Replay annotated periods with model predictions to measure performance +
+| Time | +Prediction | +Confidence | +Actual | +Result | +
|---|---|---|---|---|
| + No predictions yet + | +||||
| + | Predicted | +|
|---|---|---|
| Actual | +BUY | +SELL | +
| BUY | +0 | +0 | +
| SELL | +0 | +0 | +