diff --git a/back_tester/README.md b/back_tester/README.md
index 58a4d48..359f436 100644
--- a/back_tester/README.md
+++ b/back_tester/README.md
@@ -257,13 +257,168 @@ cd reports/examples
9. **Track drawdowns carefully** - Maximum drawdown is often more important than returns
10. **Test over multiple time periods** - Market conditions change over time
+### 10. Adaptive Learning System (`adaptive_learning.py`) ⭐ NEW
+
+Self-learning capabilities that automatically adjust signal weights based on performance:
+- **Signal Performance Tracking** - Track each signal with its contributing indicators
+- **Adaptive Weight Adjustment** - Automatically adjust weights based on what works
+- **Indicator Attribution** - Identify which indicators contribute to wins/losses
+- **Learning Analytics** - Detailed reports on indicator and regime performance
+
+```python
+from back_tester.adaptive_learning import SelfLearningBacktester
+
+# Create self-learning backtester
+learner = SelfLearningBacktester(
+ storage_path="./data/learning",
+ auto_adjust_weights=True,
+ adjustment_frequency=50 # Adjust weights every 50 signals
+)
+
+# Get adaptive weights (these improve over time)
+weights = learner.get_current_weights()
+
+# Run backtest with learning enabled
+final_balance, trades, _ = backtest_strategy(
+ symbol="BTCUSDT",
+ interval="1h",
+ candles=500,
+ enable_learning=True,
+ learner=learner,
+)
+
+# View performance analytics
+learner.print_status()
+
+# Get detailed indicator performance
+indicator_perf = learner.tracker.get_indicator_performance()
+print(indicator_perf)
+
+# Identify what works
+strong = learner.tracker.identify_strong_indicators()
+weak = learner.tracker.identify_weak_indicators()
+print(f"Strong indicators: {strong}")
+print(f"Weak indicators: {weak}")
+```
+
+
+### Architecture Overview
+
+```
+┌─────────────────────────────────────────────────────────────────┐
+│ BACKTESTING FLOW │
+├─────────────────────────────────────────────────────────────────┤
+│ │
+│ ┌─────────────┐ ┌───────────────┐ ┌─────────────────┐ │
+│ │ Data Fetch │───▶│Signal Generate│───▶│ Trade Execute │ │
+│ └─────────────┘ └───────────────┘ └─────────────────┘ │
+│ │ │ │
+│ ▼ ▼ │
+│ ┌──────────────┐ ┌─────────────────┐ │
+│ │ Weights │ │ Record Outcome │ │
+│ │ (18 total) │ └─────────────────┘ │
+│ └──────────────┘ │ │
+│ ▲ │ │
+│ │ ▼ │
+│ ┌───────────────────────────────────┐ │
+│ │ SELF-LEARNING SYSTEM │ │
+│ │ ┌─────────────────────────────┐ │ │
+│ │ │ SignalPerformanceTracker │ │ │
+│ │ │ - Track indicator contrib │ │ │
+│ │ │ - Record signal outcomes │ │ │
+│ │ └─────────────────────────────┘ │ │
+│ │ │ │ │
+│ │ ▼ │ │
+│ │ ┌─────────────────────────────┐ │ │
+│ │ │ AdaptiveWeightAdjuster │ │ │
+│ │ │ - Calculate adjustments │ │ │
+│ │ │ - Apply momentum learning │ │ │
+│ │ └─────────────────────────────┘ │ │
+│ └───────────────────────────────────┘ │
+│ │
+└─────────────────────────────────────────────────────────────────┘
+```
+
+### 11. Enhanced Metrics (`enhanced_metrics.py`) ⭐ NEW
+
+Comprehensive, informative metrics with indicator-level breakdown:
+- **Trade Attribution** - Track which indicators contributed to each trade
+- **Regime Analysis** - Performance breakdown by market regime
+- **Exit Type Analysis** - Understand where trades close (TP1, TP2, TP3, SL)
+- **Actionable Insights** - Get recommendations for improvement
+
+```python
+from back_tester.enhanced_metrics import EnhancedMetricsCalculator
+
+# Create metrics calculator
+calc = EnhancedMetricsCalculator()
+
+# After running backtests, print detailed report
+calc.print_detailed_report(initial_balance=10000, final_balance=10500)
+
+# Get comprehensive metrics
+metrics = calc.calculate_comprehensive_metrics(10000, 10500)
+print(f"Indicator Performance: {metrics['indicator_performance']}")
+print(f"Regime Performance: {metrics['regime_performance']}")
+print(f"Recommendations: {metrics['insights']['recommendations']}")
+```
+
+## How the Self-Learning System Works
+
+The self-learning system creates a feedback loop that improves signal quality over time:
+
+1. **Signal Generation** - When a signal is generated, the system records which indicators contributed to it
+2. **Outcome Tracking** - When the trade closes (TP1, TP2, TP3, stop loss, or end of period), the outcome is recorded
+3. **Attribution Analysis** - The system analyzes which indicators are associated with winning vs losing trades
+4. **Weight Adjustment** - Weights for consistently underperforming indicators are reduced, while weights for strong performers are increased
+5. **Continuous Improvement** - Over many trades, the system converges on optimal weights for current market conditions
+
+### Key Components
+
+| Component | Description |
+|-----------|-------------|
+| `SignalPerformanceTracker` | Tracks all signals with their outcomes and indicator contributions |
+| `AdaptiveWeightAdjuster` | Calculates and applies weight adjustments based on performance |
+| `LearningMetricsAnalyzer` | Generates reports and recommendations |
+| `SelfLearningBacktester` | Integrates all components for easy use |
+
+### Example Workflow
+
+```python
+# Step 1: Initialize the learning system
+from back_tester import SelfLearningBacktester, backtest_strategy
+
+learner = SelfLearningBacktester(auto_adjust_weights=True)
+
+# Step 2: Run multiple backtests to build data
+for symbol in ["BTCUSDT", "ETHUSDT", "BNBUSDT"]:
+ backtest_strategy(
+ symbol=symbol,
+ interval="1h",
+ enable_learning=True,
+ learner=learner
+ )
+
+# Step 3: View what the system learned
+report = learner.get_performance_report()
+print(f"Win Rate: {report['summary']['recent_win_rate']:.1f}%")
+print(f"Strong Indicators: {report['indicator_analysis']['strong_indicators']}")
+print(f"Weak Indicators: {report['indicator_analysis']['weak_indicators']}")
+
+# Step 4: Get updated weights for future use
+new_weights = learner.get_current_weights()
+print(f"Optimized weights: {new_weights}")
+```
+
## Next Steps for Improvement
-1. Implement machine learning for signal enhancement
-2. Add cross-validation for more robust parameter testing
-3. Implement portfolio-level backtesting for multiple assets
-4. Add regime switching capabilities based on market conditions
-5. Develop adaptive strategy selection based on market regime
+1. ~~Implement machine learning for signal enhancement~~ ✅ Done
+2. ~~Add self-learning weight adjustment~~ ✅ Done
+3. ~~Implement indicator performance tracking~~ ✅ Done
+4. Add cross-validation for more robust parameter testing
+5. Implement portfolio-level backtesting for multiple assets
+6. Add regime switching capabilities based on market conditions
+7. Develop adaptive strategy selection based on market regime
## Backtesting Launcher Script
diff --git a/back_tester/__init__.py b/back_tester/__init__.py
index 1d1038b..f787878 100644
--- a/back_tester/__init__.py
+++ b/back_tester/__init__.py
@@ -2,7 +2,54 @@
Enhanced Backtesting System for Cryptocurrency Trading
This package provides comprehensive tools for strategy development,
-testing, and optimization for cryptocurrency trading.
+testing, optimization, and self-learning for cryptocurrency trading.
+
+New in v1.1.0:
+- Adaptive Learning System: Automatically adjusts signal weights based on performance
+- Signal Performance Tracking: Tracks which indicators contribute to winning/losing trades
+- Enhanced Metrics: Detailed breakdown by indicator, market regime, and symbol
"""
-__version__ = "1.0.0"
+__version__ = "1.1.0"
+
+# Core modules
+from .strategy import backtest_strategy
+from .performance_metrics import calculate_performance_metrics, generate_performance_report
+
+# Enhanced features
+from .enhanced_backtester import EnhancedBacktester, run_enhanced_backtest
+
+# Self-learning modules
+try:
+ from .adaptive_learning import (
+ SelfLearningBacktester,
+ SignalPerformanceTracker,
+ AdaptiveWeightAdjuster,
+ LearningMetricsAnalyzer,
+ SignalRecord,
+ SignalOutcome,
+ extract_indicator_contributions,
+ )
+ from .enhanced_metrics import (
+ EnhancedMetricsCalculator,
+ TradeMetrics,
+ create_trade_metrics,
+ )
+ LEARNING_AVAILABLE = True
+except ImportError:
+ LEARNING_AVAILABLE = False
+
+__all__ = [
+ # Core
+ "backtest_strategy",
+ "calculate_performance_metrics",
+ "generate_performance_report",
+ "EnhancedBacktester",
+ "run_enhanced_backtest",
+ # Learning (if available)
+ "SelfLearningBacktester",
+ "SignalPerformanceTracker",
+ "AdaptiveWeightAdjuster",
+ "EnhancedMetricsCalculator",
+ "LEARNING_AVAILABLE",
+]
diff --git a/back_tester/adaptive_learning.py b/back_tester/adaptive_learning.py
new file mode 100644
index 0000000..2211ae1
--- /dev/null
+++ b/back_tester/adaptive_learning.py
@@ -0,0 +1,845 @@
+"""
+Adaptive Learning Module for Backtesting System
+
+This module provides self-learning capabilities for the backtesting system:
+1. SignalPerformanceTracker - Tracks individual signal outcomes with indicator attribution
+2. AdaptiveWeightAdjuster - Adjusts weights based on signal performance
+3. IndicatorAttributor - Attributes success/failure to specific indicators
+4. LearningMetricsAnalyzer - Provides detailed analytics on what works and what doesn't
+
+The system learns from failures and automatically adjusts coefficients to improve future signals.
+"""
+
+import os
+import sys
+import json
+import numpy as np
+import pandas as pd
+from typing import Dict, List, Tuple, Any, Optional
+from dataclasses import dataclass, field, asdict
+from datetime import datetime, timedelta
+from collections import defaultdict
+import logging
+from enum import Enum
+import joblib
+
+logger = logging.getLogger(__name__)
+
+
+class SignalOutcome(Enum):
+ """Possible outcomes for a trading signal"""
+ TP1_HIT = "tp1_hit"
+ TP2_HIT = "tp2_hit"
+ TP3_HIT = "tp3_hit"
+ STOP_LOSS = "stop_loss"
+ TRAILING_STOP = "trailing_stop"
+ END_OF_PERIOD = "end_of_period"
+ BREAKEVEN = "breakeven"
+ PENDING = "pending"
+
+
+@dataclass
+class SignalRecord:
+ """Complete record of a signal with its contributing factors and outcome"""
+ signal_id: str
+ timestamp: datetime
+ symbol: str
+ interval: str
+ signal_type: str # Bullish/Bearish
+ entry_price: float
+ stop_loss: float
+ take_profit_1: float
+ take_profit_2: float
+ take_profit_3: float
+
+ # Contributing indicators and their scores
+ indicator_contributions: Dict[str, float] = field(default_factory=dict)
+
+ # Reasons that triggered the signal
+ reasons: List[str] = field(default_factory=list)
+
+ # Market context at signal time
+ market_regime: str = ""
+ volatility: float = 0.0
+ volume_ratio: float = 1.0
+ rsi: float = 50.0
+ trend: str = ""
+
+ # Outcome tracking
+ outcome: SignalOutcome = SignalOutcome.PENDING
+ exit_price: float = 0.0
+ profit_loss: float = 0.0
+ profit_loss_percent: float = 0.0
+ duration_candles: int = 0
+ max_favorable_excursion: float = 0.0 # Max profit during trade
+ max_adverse_excursion: float = 0.0 # Max loss during trade
+
+ # Weights used at signal time
+ weights_at_signal: List[float] = field(default_factory=list)
+
+ def is_successful(self) -> bool:
+ """Determine if the signal was successful"""
+ return self.outcome in [SignalOutcome.TP1_HIT, SignalOutcome.TP2_HIT,
+ SignalOutcome.TP3_HIT, SignalOutcome.TRAILING_STOP]
+
+ def to_dict(self) -> Dict:
+ """Convert to dictionary for storage"""
+ data = asdict(self)
+ data['outcome'] = self.outcome.value
+ data['timestamp'] = self.timestamp.isoformat()
+ return data
+
+ @classmethod
+ def from_dict(cls, data: Dict) -> 'SignalRecord':
+ """Create from dictionary"""
+ data = data.copy()
+ data['outcome'] = SignalOutcome(data['outcome'])
+ data['timestamp'] = datetime.fromisoformat(data['timestamp'])
+ return cls(**data)
+
+
+class SignalPerformanceTracker:
+ """
+ Tracks individual signal performance and attributes outcomes to specific indicators.
+ This enables learning from both successes and failures.
+ """
+
+ # Weight names for attribution
+ WEIGHT_NAMES = [
+ "W_BULLISH_OB", "W_BEARISH_OB", "W_BULLISH_BREAKER", "W_BEARISH_BREAKER",
+ "W_ABOVE_SUPPORT", "W_BELOW_RESISTANCE", "W_FVG_ABOVE", "W_FVG_BELOW",
+ "W_TREND", "W_SWEEP_HIGHS", "W_SWEEP_LOWS", "W_STRUCTURE_BREAK",
+ "W_PIN_BAR", "W_ENGULFING", "W_LIQUIDITY_POOL_ABOVE", "W_LIQUIDITY_POOL_BELOW",
+ "W_LIQUIDITY_POOL_ROUND", "W_RSI_EXTREME"
+ ]
+
+ def __init__(self, storage_path: str = "./data/signal_history"):
+ self.storage_path = storage_path
+ os.makedirs(storage_path, exist_ok=True)
+
+ self.signals: List[SignalRecord] = []
+ self.performance_by_indicator: Dict[str, Dict] = defaultdict(
+ lambda: {"wins": 0, "losses": 0, "total_pnl": 0.0, "count": 0}
+ )
+ self.performance_by_market_regime: Dict[str, Dict] = defaultdict(
+ lambda: {"wins": 0, "losses": 0, "total_pnl": 0.0, "count": 0}
+ )
+ self.performance_by_symbol: Dict[str, Dict] = defaultdict(
+ lambda: {"wins": 0, "losses": 0, "total_pnl": 0.0, "count": 0}
+ )
+
+ self._load_history()
+
+ def _load_history(self):
+ """Load signal history from storage"""
+ history_file = os.path.join(self.storage_path, "signal_history.json")
+ if os.path.exists(history_file):
+ try:
+ with open(history_file, 'r') as f:
+ data = json.load(f)
+ self.signals = [SignalRecord.from_dict(s) for s in data.get('signals', [])]
+ self._rebuild_performance_stats()
+ logger.info(f"Loaded {len(self.signals)} historical signals")
+ except Exception as e:
+ logger.warning(f"Failed to load signal history: {e}")
+
+ def _save_history(self):
+ """Save signal history to storage"""
+ history_file = os.path.join(self.storage_path, "signal_history.json")
+ try:
+ with open(history_file, 'w') as f:
+ json.dump({
+ 'signals': [s.to_dict() for s in self.signals[-10000:]], # Keep last 10k
+ 'last_updated': datetime.now().isoformat()
+ }, f, indent=2)
+ except Exception as e:
+ logger.error(f"Failed to save signal history: {e}")
+
+ def _rebuild_performance_stats(self):
+ """Rebuild performance statistics from loaded signals"""
+ for signal in self.signals:
+ if signal.outcome != SignalOutcome.PENDING:
+ self._update_stats(signal)
+
+ def _update_stats(self, signal: SignalRecord):
+ """Update performance statistics for a completed signal"""
+ is_win = signal.is_successful()
+
+ # Update by indicator
+ for indicator, contribution in signal.indicator_contributions.items():
+ if contribution > 0:
+ stats = self.performance_by_indicator[indicator]
+ stats["count"] += 1
+ stats["total_pnl"] += signal.profit_loss
+ if is_win:
+ stats["wins"] += 1
+ else:
+ stats["losses"] += 1
+
+ # Update by market regime
+ regime_stats = self.performance_by_market_regime[signal.market_regime]
+ regime_stats["count"] += 1
+ regime_stats["total_pnl"] += signal.profit_loss
+ if is_win:
+ regime_stats["wins"] += 1
+ else:
+ regime_stats["losses"] += 1
+
+ # Update by symbol
+ symbol_stats = self.performance_by_symbol[signal.symbol]
+ symbol_stats["count"] += 1
+ symbol_stats["total_pnl"] += signal.profit_loss
+ if is_win:
+ symbol_stats["wins"] += 1
+ else:
+ symbol_stats["losses"] += 1
+
+ def record_signal(self,
+ signal_id: str,
+ symbol: str,
+ interval: str,
+ signal_type: str,
+ entry_price: float,
+ stop_loss: float,
+ take_profit_1: float,
+ take_profit_2: float,
+ take_profit_3: float,
+ indicator_contributions: Dict[str, float],
+ reasons: List[str],
+ market_context: Dict[str, Any],
+ weights: List[float]) -> SignalRecord:
+ """Record a new signal with its contributing factors"""
+
+ signal = SignalRecord(
+ signal_id=signal_id,
+ timestamp=datetime.now(),
+ symbol=symbol,
+ interval=interval,
+ signal_type=signal_type,
+ entry_price=entry_price,
+ stop_loss=stop_loss,
+ take_profit_1=take_profit_1,
+ take_profit_2=take_profit_2,
+ take_profit_3=take_profit_3,
+ indicator_contributions=indicator_contributions,
+ reasons=reasons,
+ market_regime=market_context.get("market_regime", "unknown"),
+ volatility=market_context.get("volatility", 0.0),
+ volume_ratio=market_context.get("volume_ratio", 1.0),
+ rsi=market_context.get("rsi", 50.0),
+ trend=market_context.get("trend", ""),
+ weights_at_signal=weights.copy() if weights else []
+ )
+
+ self.signals.append(signal)
+ return signal
+
+ def update_signal_outcome(self,
+ signal_id: str,
+ outcome: SignalOutcome,
+ exit_price: float,
+ profit_loss: float,
+ duration_candles: int,
+ max_favorable: float = 0.0,
+ max_adverse: float = 0.0):
+ """Update a signal with its outcome"""
+
+ for signal in reversed(self.signals): # Search from most recent
+ if signal.signal_id == signal_id:
+ signal.outcome = outcome
+ signal.exit_price = exit_price
+ signal.profit_loss = profit_loss
+ signal.profit_loss_percent = (exit_price - signal.entry_price) / signal.entry_price * 100
+ signal.duration_candles = duration_candles
+ signal.max_favorable_excursion = max_favorable
+ signal.max_adverse_excursion = max_adverse
+
+ self._update_stats(signal)
+ self._save_history()
+
+ logger.info(f"Signal {signal_id} outcome: {outcome.value}, P/L: {profit_loss:.2f}")
+ return signal
+
+ logger.warning(f"Signal {signal_id} not found for outcome update")
+ return None
+
+ def get_indicator_performance(self) -> pd.DataFrame:
+ """Get performance breakdown by indicator"""
+ data = []
+ for indicator, stats in self.performance_by_indicator.items():
+ if stats["count"] > 0:
+ win_rate = stats["wins"] / stats["count"] * 100
+ avg_pnl = stats["total_pnl"] / stats["count"]
+ data.append({
+ "indicator": indicator,
+ "signals": stats["count"],
+ "wins": stats["wins"],
+ "losses": stats["losses"],
+ "win_rate": win_rate,
+ "total_pnl": stats["total_pnl"],
+ "avg_pnl": avg_pnl,
+ "score": win_rate * 0.4 + (avg_pnl / 100) * 0.6 if avg_pnl else win_rate
+ })
+
+ df = pd.DataFrame(data)
+ if not df.empty:
+ df = df.sort_values("score", ascending=False)
+ return df
+
+ def get_regime_performance(self) -> pd.DataFrame:
+ """Get performance breakdown by market regime"""
+ data = []
+ for regime, stats in self.performance_by_market_regime.items():
+ if stats["count"] > 0:
+ win_rate = stats["wins"] / stats["count"] * 100
+ data.append({
+ "regime": regime,
+ "signals": stats["count"],
+ "win_rate": win_rate,
+ "total_pnl": stats["total_pnl"],
+ "avg_pnl": stats["total_pnl"] / stats["count"]
+ })
+ return pd.DataFrame(data)
+
+ def get_recent_performance(self, n: int = 100) -> Dict[str, Any]:
+ """Get performance of recent N signals"""
+ recent = [s for s in self.signals[-n:] if s.outcome != SignalOutcome.PENDING]
+
+ if not recent:
+ return {"signals": 0, "win_rate": 0, "avg_pnl": 0}
+
+ wins = sum(1 for s in recent if s.is_successful())
+ total_pnl = sum(s.profit_loss for s in recent)
+
+ return {
+ "signals": len(recent),
+ "wins": wins,
+ "losses": len(recent) - wins,
+ "win_rate": wins / len(recent) * 100,
+ "total_pnl": total_pnl,
+ "avg_pnl": total_pnl / len(recent),
+ "avg_duration": sum(s.duration_candles for s in recent) / len(recent)
+ }
+
+ def identify_weak_indicators(self, min_signals: int = 10,
+ win_rate_threshold: float = 40.0) -> List[str]:
+ """Identify indicators that consistently underperform"""
+ weak = []
+ for indicator, stats in self.performance_by_indicator.items():
+ if stats["count"] >= min_signals:
+ win_rate = stats["wins"] / stats["count"] * 100
+ if win_rate < win_rate_threshold:
+ weak.append(indicator)
+ return weak
+
+ def identify_strong_indicators(self, min_signals: int = 10,
+ win_rate_threshold: float = 60.0) -> List[str]:
+ """Identify indicators that consistently outperform"""
+ strong = []
+ for indicator, stats in self.performance_by_indicator.items():
+ if stats["count"] >= min_signals:
+ win_rate = stats["wins"] / stats["count"] * 100
+ if win_rate >= win_rate_threshold:
+ strong.append(indicator)
+ return strong
+
+
+class AdaptiveWeightAdjuster:
+ """
+ Adjusts signal weights based on historical performance.
+ Uses online learning to continuously improve signal quality.
+ """
+
+ def __init__(self,
+ performance_tracker: SignalPerformanceTracker,
+ learning_rate: float = 0.05,
+ momentum: float = 0.9,
+ min_weight: float = 0.1,
+ max_weight: float = 3.0,
+ decay_factor: float = 0.95,
+ storage_path: str = "./data/weights"):
+
+ self.tracker = performance_tracker
+ self.learning_rate = learning_rate
+ self.momentum = momentum
+ self.min_weight = min_weight
+ self.max_weight = max_weight
+ self.decay_factor = decay_factor # Decay old performance influence
+ self.storage_path = storage_path
+ os.makedirs(storage_path, exist_ok=True)
+
+ # Weight velocity for momentum-based updates
+ self.velocity = defaultdict(float)
+
+ # Historical weight adjustments
+ self.adjustment_history: List[Dict] = []
+
+ # Load last known weights
+ self.current_weights = self._load_weights()
+
+ def _load_weights(self) -> Dict[str, float]:
+ """Load current weights from storage"""
+ weights_file = os.path.join(self.storage_path, "adaptive_weights.json")
+ default_weights = {name: 1.0 for name in SignalPerformanceTracker.WEIGHT_NAMES}
+
+ if os.path.exists(weights_file):
+ try:
+ with open(weights_file, 'r') as f:
+ data = json.load(f)
+ return data.get('weights', default_weights)
+ except:
+ pass
+ return default_weights
+
+ def _save_weights(self):
+ """Save current weights to storage"""
+ weights_file = os.path.join(self.storage_path, "adaptive_weights.json")
+ with open(weights_file, 'w') as f:
+ json.dump({
+ 'weights': self.current_weights,
+ 'last_updated': datetime.now().isoformat(),
+ 'learning_rate': self.learning_rate,
+ 'adjustment_count': len(self.adjustment_history)
+ }, f, indent=2)
+
+ def calculate_weight_adjustments(self,
+ recent_window: int = 100,
+ min_contribution_threshold: float = 0.1) -> Dict[str, float]:
+ """
+ Calculate weight adjustments based on recent signal performance.
+
+ For each indicator:
+ - If win rate > 60%: increase weight
+ - If win rate < 40%: decrease weight
+ - Scale adjustment by confidence (number of signals)
+ """
+ adjustments = {}
+ indicator_perf = self.tracker.get_indicator_performance()
+
+ if indicator_perf.empty:
+ return adjustments
+
+ for _, row in indicator_perf.iterrows():
+ indicator = row['indicator']
+ if indicator not in self.current_weights:
+ continue
+
+ signals = row['signals']
+ win_rate = row['win_rate']
+ avg_pnl = row['avg_pnl']
+
+ # Calculate confidence based on sample size
+ confidence = min(1.0, signals / 50) # Full confidence at 50 signals
+
+ # Calculate adjustment direction and magnitude
+ if win_rate > 60:
+ # Positive adjustment - increase weight
+ magnitude = (win_rate - 50) / 100 * confidence
+ adjustment = magnitude * self.learning_rate
+ elif win_rate < 40:
+ # Negative adjustment - decrease weight
+ magnitude = (50 - win_rate) / 100 * confidence
+ adjustment = -magnitude * self.learning_rate
+ else:
+ adjustment = 0
+
+ # Factor in P/L performance
+ if avg_pnl != 0:
+ pnl_factor = np.clip(avg_pnl / 100, -0.5, 0.5)
+ adjustment += pnl_factor * 0.5 * self.learning_rate * confidence
+
+ # Apply momentum
+ self.velocity[indicator] = (self.momentum * self.velocity[indicator] +
+ (1 - self.momentum) * adjustment)
+ adjustments[indicator] = self.velocity[indicator]
+
+ return adjustments
+
+ def apply_adjustments(self,
+ adjustments: Optional[Dict[str, float]] = None,
+ force: bool = False) -> Dict[str, float]:
+ """
+ Apply calculated adjustments to current weights.
+
+ Args:
+ adjustments: Optional pre-calculated adjustments
+ force: If True, apply even small adjustments
+
+ Returns:
+ Dictionary of new weights
+ """
+ if adjustments is None:
+ adjustments = self.calculate_weight_adjustments()
+
+ if not adjustments:
+ return self.current_weights
+
+ old_weights = self.current_weights.copy()
+ changes_made = False
+
+ for indicator, adjustment in adjustments.items():
+ if indicator not in self.current_weights:
+ continue
+
+ # Only apply significant adjustments unless forced
+ if not force and abs(adjustment) < 0.01:
+ continue
+
+ old_weight = self.current_weights[indicator]
+ new_weight = old_weight + adjustment
+
+ # Clamp to valid range
+ new_weight = max(self.min_weight, min(self.max_weight, new_weight))
+
+ if new_weight != old_weight:
+ self.current_weights[indicator] = new_weight
+ changes_made = True
+ logger.info(f"Weight adjusted: {indicator} {old_weight:.3f} -> {new_weight:.3f}")
+
+ if changes_made:
+ self.adjustment_history.append({
+ 'timestamp': datetime.now().isoformat(),
+ 'old_weights': old_weights,
+ 'new_weights': self.current_weights.copy(),
+ 'adjustments': adjustments
+ })
+ self._save_weights()
+
+ return self.current_weights
+
+ def get_weights_as_list(self) -> List[float]:
+ """Convert weights dictionary to list format for backtesting"""
+ return [self.current_weights.get(name, 1.0)
+ for name in SignalPerformanceTracker.WEIGHT_NAMES]
+
+ def reset_weights(self):
+ """Reset all weights to default values"""
+ self.current_weights = {name: 1.0 for name in SignalPerformanceTracker.WEIGHT_NAMES}
+ self.velocity = defaultdict(float)
+ self._save_weights()
+ logger.info("Weights reset to defaults")
+
+ def get_adjustment_summary(self) -> Dict[str, Any]:
+ """Get summary of recent weight adjustments"""
+ if not self.adjustment_history:
+ return {"adjustments": 0, "last_adjustment": None}
+
+ recent = self.adjustment_history[-10:]
+ return {
+ "total_adjustments": len(self.adjustment_history),
+ "recent_adjustments": len(recent),
+ "last_adjustment": recent[-1]['timestamp'] if recent else None,
+ "current_weights": self.current_weights
+ }
+
+
+class LearningMetricsAnalyzer:
+ """
+ Analyzes learning metrics and provides actionable insights
+ for improving signal quality.
+ """
+
+ def __init__(self,
+ tracker: SignalPerformanceTracker,
+ adjuster: AdaptiveWeightAdjuster):
+ self.tracker = tracker
+ self.adjuster = adjuster
+
+ def generate_learning_report(self) -> Dict[str, Any]:
+ """Generate comprehensive learning report"""
+ report = {
+ "timestamp": datetime.now().isoformat(),
+ "summary": {},
+ "indicator_analysis": {},
+ "regime_analysis": {},
+ "recommendations": []
+ }
+
+ # Overall summary
+ recent_perf = self.tracker.get_recent_performance(100)
+ report["summary"] = {
+ "total_signals_tracked": len(self.tracker.signals),
+ "recent_win_rate": recent_perf.get("win_rate", 0),
+ "recent_avg_pnl": recent_perf.get("avg_pnl", 0),
+ "weight_adjustments": len(self.adjuster.adjustment_history)
+ }
+
+ # Indicator analysis
+ indicator_df = self.tracker.get_indicator_performance()
+ if not indicator_df.empty:
+ report["indicator_analysis"] = {
+ "best_performers": indicator_df.head(5).to_dict('records'),
+ "worst_performers": indicator_df.tail(5).to_dict('records'),
+ "weak_indicators": self.tracker.identify_weak_indicators(),
+ "strong_indicators": self.tracker.identify_strong_indicators()
+ }
+
+ # Regime analysis
+ regime_df = self.tracker.get_regime_performance()
+ if not regime_df.empty:
+ report["regime_analysis"] = regime_df.to_dict('records')
+
+ # Generate recommendations
+ report["recommendations"] = self._generate_recommendations()
+
+ return report
+
+ def _generate_recommendations(self) -> List[str]:
+ """Generate actionable recommendations based on performance data"""
+ recommendations = []
+
+ # Check for weak indicators
+ weak = self.tracker.identify_weak_indicators()
+ if weak:
+ recommendations.append(
+ f"Consider reducing weights for underperforming indicators: {', '.join(weak)}"
+ )
+
+ # Check for strong indicators
+ strong = self.tracker.identify_strong_indicators()
+ if strong:
+ recommendations.append(
+ f"High-performing indicators (consider increasing weight): {', '.join(strong)}"
+ )
+
+ # Check recent performance trend
+ recent_perf = self.tracker.get_recent_performance(50)
+ older_perf = self.tracker.get_recent_performance(200)
+
+ if recent_perf.get("win_rate", 0) < older_perf.get("win_rate", 0) - 10:
+ recommendations.append(
+ "Recent performance declining - consider resetting weights or retraining"
+ )
+
+ # Regime-specific recommendations
+ regime_df = self.tracker.get_regime_performance()
+ if not regime_df.empty:
+ worst_regime = regime_df.loc[regime_df['win_rate'].idxmin()]
+ if worst_regime['win_rate'] < 40 and worst_regime['signals'] > 20:
+ recommendations.append(
+ f"Poor performance in '{worst_regime['regime']}' regime "
+ f"({worst_regime['win_rate']:.1f}% win rate) - consider filtering signals"
+ )
+
+ return recommendations
+
+ def print_report(self):
+ """Print formatted learning report to console"""
+ report = self.generate_learning_report()
+
+ print("\n" + "="*70)
+ print("📊 ADAPTIVE LEARNING REPORT")
+ print("="*70)
+
+ print(f"\n📈 Summary:")
+ print(f" Total signals tracked: {report['summary']['total_signals_tracked']}")
+ print(f" Recent win rate: {report['summary']['recent_win_rate']:.1f}%")
+ print(f" Recent avg P/L: ${report['summary']['recent_avg_pnl']:.2f}")
+ print(f" Weight adjustments made: {report['summary']['weight_adjustments']}")
+
+ if report.get('indicator_analysis', {}).get('best_performers'):
+ print(f"\n🟢 Top Performing Indicators:")
+ for ind in report['indicator_analysis']['best_performers'][:3]:
+ print(f" {ind['indicator']}: {ind['win_rate']:.1f}% win rate, "
+ f"${ind['avg_pnl']:.2f} avg P/L ({ind['signals']} signals)")
+
+ if report.get('indicator_analysis', {}).get('weak_indicators'):
+ print(f"\n🔴 Underperforming Indicators:")
+ for ind in report['indicator_analysis']['weak_indicators']:
+ print(f" {ind}")
+
+ if report.get('recommendations'):
+ print(f"\n💡 Recommendations:")
+ for rec in report['recommendations']:
+ print(f" • {rec}")
+
+ print("\n" + "="*70)
+
+
+class SelfLearningBacktester:
+ """
+ Enhanced backtester with self-learning capabilities.
+ Integrates signal tracking, weight adjustment, and performance analysis.
+ """
+
+ def __init__(self,
+ storage_path: str = "./data/learning",
+ auto_adjust_weights: bool = True,
+ adjustment_frequency: int = 50): # Adjust every N signals
+
+ self.storage_path = storage_path
+ os.makedirs(storage_path, exist_ok=True)
+
+ self.tracker = SignalPerformanceTracker(
+ storage_path=os.path.join(storage_path, "signals")
+ )
+ self.adjuster = AdaptiveWeightAdjuster(
+ performance_tracker=self.tracker,
+ storage_path=os.path.join(storage_path, "weights")
+ )
+ self.analyzer = LearningMetricsAnalyzer(self.tracker, self.adjuster)
+
+ self.auto_adjust = auto_adjust_weights
+ self.adjustment_frequency = adjustment_frequency
+ self.signals_since_adjustment = 0
+
+ def get_current_weights(self) -> List[float]:
+ """Get current adaptive weights as list"""
+ return self.adjuster.get_weights_as_list()
+
+ def record_signal_entry(self,
+ signal_id: str,
+ symbol: str,
+ interval: str,
+ signal_type: str,
+ entry_price: float,
+ stop_loss: float,
+ tp1: float, tp2: float, tp3: float,
+ indicator_contributions: Dict[str, float],
+ reasons: List[str],
+ market_context: Dict[str, Any]) -> SignalRecord:
+ """Record a new signal entry"""
+
+ signal = self.tracker.record_signal(
+ signal_id=signal_id,
+ symbol=symbol,
+ interval=interval,
+ signal_type=signal_type,
+ entry_price=entry_price,
+ stop_loss=stop_loss,
+ take_profit_1=tp1,
+ take_profit_2=tp2,
+ take_profit_3=tp3,
+ indicator_contributions=indicator_contributions,
+ reasons=reasons,
+ market_context=market_context,
+ weights=self.get_current_weights()
+ )
+
+ return signal
+
+ def record_signal_exit(self,
+ signal_id: str,
+ outcome: str, # "tp1", "tp2", "tp3", "stop_loss", "trailing_stop", "end"
+ exit_price: float,
+ profit_loss: float,
+ duration: int):
+ """Record signal exit and trigger learning if needed"""
+
+ # Map outcome string to enum
+ outcome_map = {
+ "tp1": SignalOutcome.TP1_HIT,
+ "tp2": SignalOutcome.TP2_HIT,
+ "tp3": SignalOutcome.TP3_HIT,
+ "stop_loss": SignalOutcome.STOP_LOSS,
+ "trailing_stop": SignalOutcome.TRAILING_STOP,
+ "end": SignalOutcome.END_OF_PERIOD
+ }
+ outcome_enum = outcome_map.get(outcome, SignalOutcome.END_OF_PERIOD)
+
+ self.tracker.update_signal_outcome(
+ signal_id=signal_id,
+ outcome=outcome_enum,
+ exit_price=exit_price,
+ profit_loss=profit_loss,
+ duration_candles=duration
+ )
+
+ self.signals_since_adjustment += 1
+
+ # Auto-adjust weights if enabled and threshold reached
+ if self.auto_adjust and self.signals_since_adjustment >= self.adjustment_frequency:
+ self._trigger_learning()
+ self.signals_since_adjustment = 0
+
+ def _trigger_learning(self):
+ """Trigger weight adjustment based on recent performance"""
+ logger.info("Triggering adaptive weight adjustment...")
+
+ adjustments = self.adjuster.calculate_weight_adjustments()
+ if adjustments:
+ self.adjuster.apply_adjustments(adjustments)
+ logger.info(f"Applied {len(adjustments)} weight adjustments")
+
+ def force_learning(self):
+ """Force immediate weight adjustment"""
+ self._trigger_learning()
+ self.signals_since_adjustment = 0
+
+ def get_performance_report(self) -> Dict[str, Any]:
+ """Get comprehensive performance report"""
+ return self.analyzer.generate_learning_report()
+
+ def print_status(self):
+ """Print current learning status"""
+ self.analyzer.print_report()
+
+ def save_state(self):
+ """Save all learning state"""
+ self.tracker._save_history()
+ self.adjuster._save_weights()
+ logger.info("Learning state saved")
+
+
+# Helper function to extract indicator contributions from signal generation
+def extract_indicator_contributions(
+ bullish_score: float,
+ bearish_score: float,
+ signal_type: str,
+ reasons: List[str]
+) -> Dict[str, float]:
+ """
+ Extract which indicators contributed to the signal.
+ This parses the reasons list to determine contributions.
+ """
+ contributions = {}
+
+ # Parse reasons to identify contributing indicators
+ indicator_keywords = {
+ "order block": ("W_BULLISH_OB" if signal_type == "Bullish" else "W_BEARISH_OB"),
+ "breaker block": ("W_BULLISH_BREAKER" if signal_type == "Bullish" else "W_BEARISH_BREAKER"),
+ "support": "W_ABOVE_SUPPORT",
+ "resistance": "W_BELOW_RESISTANCE",
+ "FVG above": "W_FVG_ABOVE",
+ "FVG below": "W_FVG_BELOW",
+ "uptrend": "W_TREND",
+ "downtrend": "W_TREND",
+ "swept through previous highs": "W_SWEEP_HIGHS",
+ "swept through previous lows": "W_SWEEP_LOWS",
+ "broke structure": "W_STRUCTURE_BREAK",
+ "pin bar": "W_PIN_BAR",
+ "engulfing": "W_ENGULFING",
+ "liquidity pool": "W_LIQUIDITY_POOL_ABOVE",
+ "round number": "W_LIQUIDITY_POOL_ROUND",
+ "RSI oversold": "W_RSI_EXTREME",
+ "RSI overbought": "W_RSI_EXTREME"
+ }
+
+ for reason in reasons:
+ reason_lower = reason.lower()
+ for keyword, indicator in indicator_keywords.items():
+ if keyword.lower() in reason_lower:
+ # Score is proportional to contribution to final signal
+ total_score = bullish_score + bearish_score if bullish_score + bearish_score > 0 else 1
+ score = bullish_score if signal_type == "Bullish" else bearish_score
+ contributions[indicator] = score / total_score
+
+ return contributions
+
+
+# Usage example
+if __name__ == "__main__":
+ # Initialize self-learning backtester
+ learner = SelfLearningBacktester(
+ storage_path="./data/learning",
+ auto_adjust_weights=True,
+ adjustment_frequency=50
+ )
+
+ # Get current adaptive weights for signal generation
+ weights = learner.get_current_weights()
+ print(f"Current weights: {weights}")
+
+ # Print performance status
+ learner.print_status()
+
diff --git a/back_tester/enhanced_metrics.py b/back_tester/enhanced_metrics.py
new file mode 100644
index 0000000..d37003b
--- /dev/null
+++ b/back_tester/enhanced_metrics.py
@@ -0,0 +1,669 @@
+"""
+Enhanced Metrics Module for Backtesting System
+
+Provides comprehensive, informative metrics including:
+1. Indicator-level performance breakdown
+2. Market regime analysis
+3. Signal quality scoring
+4. Trade attribution and root cause analysis
+5. Learning suggestions based on performance patterns
+"""
+
+import numpy as np
+import pandas as pd
+from typing import List, Dict, Any, Optional, Tuple
+from dataclasses import dataclass, field
+from datetime import datetime
+from collections import defaultdict
+import json
+import os
+
+
+@dataclass
+class TradeMetrics:
+ """Detailed metrics for a single trade"""
+ trade_id: str
+ symbol: str
+ interval: str
+ signal_type: str
+ entry_price: float
+ exit_price: float
+ stop_loss: float
+ take_profit_targets: List[float]
+
+ # Outcome
+ outcome: str # tp1, tp2, tp3, stop_loss, trailing_stop, end_of_period
+ profit_loss: float
+ profit_loss_percent: float
+ risk_reward_achieved: float
+
+ # Duration
+ entry_index: int
+ exit_index: int
+ duration_candles: int
+
+ # Market context
+ market_regime: str = ""
+ volatility: float = 0.0
+ volume_ratio: float = 1.0
+ trend: str = ""
+
+ # Indicator contributions
+ contributing_indicators: List[str] = field(default_factory=list)
+ indicator_scores: Dict[str, float] = field(default_factory=dict)
+
+ # Trade quality metrics
+ max_favorable_excursion: float = 0.0 # Best profit during trade
+ max_adverse_excursion: float = 0.0 # Worst drawdown during trade
+ r_multiple: float = 0.0 # Actual R achieved
+
+ def is_winner(self) -> bool:
+ return self.outcome in ['tp1', 'tp2', 'tp3', 'trailing_stop'] or self.profit_loss > 0
+
+ def is_full_target(self) -> bool:
+ return self.outcome == 'tp3'
+
+
+class EnhancedMetricsCalculator:
+ """
+ Calculates and aggregates enhanced trading metrics with
+ detailed breakdowns for learning and optimization.
+ """
+
+ def __init__(self):
+ self.trades: List[TradeMetrics] = []
+ self.indicator_performance: Dict[str, Dict] = defaultdict(
+ lambda: {
+ 'wins': 0, 'losses': 0, 'total_pnl': 0.0,
+ 'avg_r': 0.0, 'r_values': [],
+ 'tp1_hits': 0, 'tp2_hits': 0, 'tp3_hits': 0, 'stop_hits': 0
+ }
+ )
+ self.regime_performance: Dict[str, Dict] = defaultdict(
+ lambda: {'wins': 0, 'losses': 0, 'total_pnl': 0.0, 'trades': 0}
+ )
+ self.symbol_performance: Dict[str, Dict] = defaultdict(
+ lambda: {'wins': 0, 'losses': 0, 'total_pnl': 0.0, 'trades': 0}
+ )
+ self.interval_performance: Dict[str, Dict] = defaultdict(
+ lambda: {'wins': 0, 'losses': 0, 'total_pnl': 0.0, 'trades': 0}
+ )
+
+ def add_trade(self, trade: TradeMetrics):
+ """Add a completed trade to metrics"""
+ self.trades.append(trade)
+ self._update_indicator_stats(trade)
+ self._update_regime_stats(trade)
+ self._update_symbol_stats(trade)
+ self._update_interval_stats(trade)
+
+ def _update_indicator_stats(self, trade: TradeMetrics):
+ """Update indicator-level statistics"""
+ is_win = trade.is_winner()
+
+ for indicator in trade.contributing_indicators:
+ stats = self.indicator_performance[indicator]
+ if is_win:
+ stats['wins'] += 1
+ else:
+ stats['losses'] += 1
+
+ stats['total_pnl'] += trade.profit_loss
+ stats['r_values'].append(trade.r_multiple)
+
+ # Track specific outcomes
+ if trade.outcome == 'tp1':
+ stats['tp1_hits'] += 1
+ elif trade.outcome == 'tp2':
+ stats['tp2_hits'] += 1
+ elif trade.outcome == 'tp3':
+ stats['tp3_hits'] += 1
+ elif trade.outcome in ['stop_loss', 'trailing_stop']:
+ stats['stop_hits'] += 1
+
+ def _update_regime_stats(self, trade: TradeMetrics):
+ """Update market regime statistics"""
+ stats = self.regime_performance[trade.market_regime or 'unknown']
+ stats['trades'] += 1
+ stats['total_pnl'] += trade.profit_loss
+ if trade.is_winner():
+ stats['wins'] += 1
+ else:
+ stats['losses'] += 1
+
+ def _update_symbol_stats(self, trade: TradeMetrics):
+ """Update symbol-level statistics"""
+ stats = self.symbol_performance[trade.symbol]
+ stats['trades'] += 1
+ stats['total_pnl'] += trade.profit_loss
+ if trade.is_winner():
+ stats['wins'] += 1
+ else:
+ stats['losses'] += 1
+
+ def _update_interval_stats(self, trade: TradeMetrics):
+ """Update interval/timeframe statistics"""
+ stats = self.interval_performance[trade.interval]
+ stats['trades'] += 1
+ stats['total_pnl'] += trade.profit_loss
+ if trade.is_winner():
+ stats['wins'] += 1
+ else:
+ stats['losses'] += 1
+
+ def calculate_comprehensive_metrics(self,
+ initial_balance: float,
+ final_balance: float) -> Dict[str, Any]:
+ """Calculate comprehensive performance metrics with full breakdown"""
+
+ if not self.trades:
+ return self._empty_metrics()
+
+ # Basic metrics
+ total_trades = len(self.trades)
+ winners = [t for t in self.trades if t.is_winner()]
+ losers = [t for t in self.trades if not t.is_winner()]
+
+ win_rate = len(winners) / total_trades * 100 if total_trades > 0 else 0
+
+ # P/L calculations
+ total_profit = sum(t.profit_loss for t in winners)
+ total_loss = sum(t.profit_loss for t in losers)
+ net_profit = total_profit + total_loss
+
+ # Profit factor
+ profit_factor = abs(total_profit / total_loss) if total_loss != 0 else float('inf')
+
+ # R-multiple statistics
+ r_multiples = [t.r_multiple for t in self.trades if t.r_multiple != 0]
+ avg_r = np.mean(r_multiples) if r_multiples else 0
+
+ # Drawdown calculation
+ equity_curve = self._calculate_equity_curve(initial_balance)
+ max_drawdown = self._calculate_max_drawdown(equity_curve)
+
+ # Expectancy
+ avg_win = np.mean([t.profit_loss for t in winners]) if winners else 0
+ avg_loss = np.mean([t.profit_loss for t in losers]) if losers else 0
+ expectancy = (win_rate/100 * avg_win) + ((1 - win_rate/100) * avg_loss)
+
+ # Exit type breakdown
+ exit_breakdown = self._calculate_exit_breakdown()
+
+ # Indicator performance breakdown
+ indicator_breakdown = self._calculate_indicator_breakdown()
+
+ # Regime performance breakdown
+ regime_breakdown = self._calculate_regime_breakdown()
+
+ # Symbol performance breakdown
+ symbol_breakdown = self._calculate_symbol_breakdown()
+
+ # Generate insights and recommendations
+ insights = self._generate_insights()
+
+ return {
+ # Summary
+ 'summary': {
+ 'total_trades': total_trades,
+ 'winners': len(winners),
+ 'losers': len(losers),
+ 'win_rate': win_rate,
+ 'profit_factor': profit_factor,
+ 'net_profit': net_profit,
+ 'total_return_pct': (final_balance - initial_balance) / initial_balance * 100,
+ 'max_drawdown_pct': max_drawdown,
+ 'expectancy': expectancy,
+ 'avg_r_multiple': avg_r
+ },
+
+ # Detailed trade statistics
+ 'trade_stats': {
+ 'avg_winner': avg_win,
+ 'avg_loser': avg_loss,
+ 'largest_winner': max((t.profit_loss for t in winners), default=0),
+ 'largest_loser': min((t.profit_loss for t in losers), default=0),
+ 'avg_duration_candles': np.mean([t.duration_candles for t in self.trades]),
+ 'avg_mfe': np.mean([t.max_favorable_excursion for t in self.trades]),
+ 'avg_mae': np.mean([t.max_adverse_excursion for t in self.trades])
+ },
+
+ # Exit type breakdown
+ 'exit_breakdown': exit_breakdown,
+
+ # Indicator performance
+ 'indicator_performance': indicator_breakdown,
+
+ # Market regime performance
+ 'regime_performance': regime_breakdown,
+
+ # Symbol performance
+ 'symbol_performance': symbol_breakdown,
+
+ # Timeframe performance
+ 'interval_performance': self._calculate_interval_breakdown(),
+
+ # Learning insights
+ 'insights': insights,
+
+ # Equity curve for visualization
+ 'equity_curve': equity_curve
+ }
+
+ def _empty_metrics(self) -> Dict[str, Any]:
+ """Return empty metrics structure"""
+ return {
+ 'summary': {
+ 'total_trades': 0, 'win_rate': 0, 'profit_factor': 0,
+ 'net_profit': 0, 'max_drawdown_pct': 0, 'expectancy': 0
+ },
+ 'trade_stats': {},
+ 'exit_breakdown': {},
+ 'indicator_performance': {},
+ 'regime_performance': {},
+ 'symbol_performance': {},
+ 'interval_performance': {},
+ 'insights': {'recommendations': ['Insufficient data for analysis']},
+ 'equity_curve': []
+ }
+
+ def _calculate_equity_curve(self, initial_balance: float) -> List[float]:
+ """Calculate cumulative equity curve"""
+ equity = [initial_balance]
+ for trade in self.trades:
+ equity.append(equity[-1] + trade.profit_loss)
+ return equity
+
+ def _calculate_max_drawdown(self, equity_curve: List[float]) -> float:
+ """Calculate maximum drawdown percentage"""
+ if not equity_curve:
+ return 0
+
+ peak = equity_curve[0]
+ max_dd = 0
+
+ for equity in equity_curve:
+ if equity > peak:
+ peak = equity
+ dd = (peak - equity) / peak * 100 if peak > 0 else 0
+ max_dd = max(max_dd, dd)
+
+ return max_dd
+
+ def _calculate_exit_breakdown(self) -> Dict[str, Dict]:
+ """Calculate breakdown by exit type"""
+ breakdown = defaultdict(lambda: {'count': 0, 'total_pnl': 0.0, 'avg_pnl': 0.0})
+
+ for trade in self.trades:
+ stats = breakdown[trade.outcome]
+ stats['count'] += 1
+ stats['total_pnl'] += trade.profit_loss
+
+ for outcome, stats in breakdown.items():
+ if stats['count'] > 0:
+ stats['avg_pnl'] = stats['total_pnl'] / stats['count']
+ stats['percentage'] = stats['count'] / len(self.trades) * 100
+
+ return dict(breakdown)
+
+ def _calculate_indicator_breakdown(self) -> Dict[str, Dict]:
+ """Calculate detailed indicator performance breakdown"""
+ breakdown = {}
+
+ for indicator, stats in self.indicator_performance.items():
+ total = stats['wins'] + stats['losses']
+ if total == 0:
+ continue
+
+ win_rate = stats['wins'] / total * 100
+ avg_r = np.mean(stats['r_values']) if stats['r_values'] else 0
+
+ # Calculate score for ranking
+ # Score = win_rate * 0.4 + normalized_pnl * 0.3 + avg_r * 0.3
+ pnl_score = min(1.0, max(-1.0, stats['total_pnl'] / 1000)) # Normalize to -1 to 1
+ r_score = min(1.0, max(-1.0, avg_r / 3)) # Normalize R to -1 to 1
+ score = win_rate / 100 * 0.4 + (pnl_score + 1) / 2 * 0.3 + (r_score + 1) / 2 * 0.3
+
+ breakdown[indicator] = {
+ 'total_signals': total,
+ 'wins': stats['wins'],
+ 'losses': stats['losses'],
+ 'win_rate': win_rate,
+ 'total_pnl': stats['total_pnl'],
+ 'avg_pnl': stats['total_pnl'] / total,
+ 'avg_r': avg_r,
+ 'tp1_hits': stats['tp1_hits'],
+ 'tp2_hits': stats['tp2_hits'],
+ 'tp3_hits': stats['tp3_hits'],
+ 'stop_hits': stats['stop_hits'],
+ 'score': score,
+ 'status': self._get_indicator_status(win_rate, stats['total_pnl'])
+ }
+
+ # Sort by score
+ return dict(sorted(breakdown.items(), key=lambda x: x[1]['score'], reverse=True))
+
+ def _get_indicator_status(self, win_rate: float, total_pnl: float) -> str:
+ """Get indicator health status"""
+ if win_rate >= 60 and total_pnl > 0:
+ return 'excellent'
+ elif win_rate >= 50 and total_pnl >= 0:
+ return 'good'
+ elif win_rate >= 40:
+ return 'average'
+ else:
+ return 'poor'
+
+ def _calculate_regime_breakdown(self) -> Dict[str, Dict]:
+ """Calculate performance by market regime"""
+ breakdown = {}
+
+ for regime, stats in self.regime_performance.items():
+ if stats['trades'] == 0:
+ continue
+
+ win_rate = stats['wins'] / stats['trades'] * 100
+ breakdown[regime] = {
+ 'trades': stats['trades'],
+ 'wins': stats['wins'],
+ 'losses': stats['losses'],
+ 'win_rate': win_rate,
+ 'total_pnl': stats['total_pnl'],
+ 'avg_pnl': stats['total_pnl'] / stats['trades'],
+ 'recommendation': self._get_regime_recommendation(regime, win_rate, stats['total_pnl'])
+ }
+
+ return breakdown
+
+ def _get_regime_recommendation(self, regime: str, win_rate: float, pnl: float) -> str:
+ """Get recommendation for trading in specific regime"""
+ if win_rate >= 55 and pnl > 0:
+ return f"Good performance in {regime} - continue trading"
+ elif win_rate < 40 or pnl < 0:
+ return f"Poor performance in {regime} - consider filtering signals or reducing position size"
+ else:
+ return f"Average performance in {regime} - monitor closely"
+
+ def _calculate_symbol_breakdown(self) -> Dict[str, Dict]:
+ """Calculate performance by symbol"""
+ breakdown = {}
+
+ for symbol, stats in self.symbol_performance.items():
+ if stats['trades'] == 0:
+ continue
+
+ win_rate = stats['wins'] / stats['trades'] * 100
+ breakdown[symbol] = {
+ 'trades': stats['trades'],
+ 'wins': stats['wins'],
+ 'losses': stats['losses'],
+ 'win_rate': win_rate,
+ 'total_pnl': stats['total_pnl'],
+ 'avg_pnl': stats['total_pnl'] / stats['trades']
+ }
+
+ return dict(sorted(breakdown.items(), key=lambda x: x[1]['total_pnl'], reverse=True))
+
+ def _calculate_interval_breakdown(self) -> Dict[str, Dict]:
+ """Calculate performance by timeframe"""
+ breakdown = {}
+
+ for interval, stats in self.interval_performance.items():
+ if stats['trades'] == 0:
+ continue
+
+ win_rate = stats['wins'] / stats['trades'] * 100
+ breakdown[interval] = {
+ 'trades': stats['trades'],
+ 'wins': stats['wins'],
+ 'losses': stats['losses'],
+ 'win_rate': win_rate,
+ 'total_pnl': stats['total_pnl'],
+ 'avg_pnl': stats['total_pnl'] / stats['trades']
+ }
+
+ return breakdown
+
+ def _generate_insights(self) -> Dict[str, Any]:
+ """Generate actionable insights from performance data"""
+ insights = {
+ 'strong_indicators': [],
+ 'weak_indicators': [],
+ 'best_regimes': [],
+ 'worst_regimes': [],
+ 'recommendations': []
+ }
+
+ # Indicator insights
+ for indicator, stats in self._calculate_indicator_breakdown().items():
+ if stats['total_signals'] >= 10: # Minimum sample size
+ if stats['status'] == 'excellent':
+ insights['strong_indicators'].append({
+ 'name': indicator,
+ 'win_rate': stats['win_rate'],
+ 'avg_pnl': stats['avg_pnl']
+ })
+ elif stats['status'] == 'poor':
+ insights['weak_indicators'].append({
+ 'name': indicator,
+ 'win_rate': stats['win_rate'],
+ 'avg_pnl': stats['avg_pnl']
+ })
+
+ # Regime insights
+ regime_breakdown = self._calculate_regime_breakdown()
+ sorted_regimes = sorted(regime_breakdown.items(),
+ key=lambda x: x[1]['win_rate'], reverse=True)
+
+ if sorted_regimes:
+ insights['best_regimes'] = [sorted_regimes[0][0]] if sorted_regimes else []
+ insights['worst_regimes'] = [sorted_regimes[-1][0]] if len(sorted_regimes) > 1 else []
+
+ # Generate recommendations
+ if insights['weak_indicators']:
+ weak_names = [i['name'] for i in insights['weak_indicators'][:3]]
+ insights['recommendations'].append(
+ f"Consider reducing weights for: {', '.join(weak_names)}"
+ )
+
+ if insights['strong_indicators']:
+ strong_names = [i['name'] for i in insights['strong_indicators'][:3]]
+ insights['recommendations'].append(
+ f"Consider increasing weights for: {', '.join(strong_names)}"
+ )
+
+ if insights['worst_regimes']:
+ worst_regime = insights['worst_regimes'][0]
+ regime_stats = regime_breakdown.get(worst_regime, {})
+ if regime_stats.get('win_rate', 100) < 40:
+ insights['recommendations'].append(
+ f"Consider avoiding signals in '{worst_regime}' market regime"
+ )
+
+ # Check for recent performance decline
+ if len(self.trades) >= 50:
+ recent_trades = self.trades[-25:]
+ older_trades = self.trades[-50:-25]
+
+ recent_win_rate = sum(1 for t in recent_trades if t.is_winner()) / len(recent_trades) * 100
+ older_win_rate = sum(1 for t in older_trades if t.is_winner()) / len(older_trades) * 100
+
+ if recent_win_rate < older_win_rate - 10:
+ insights['recommendations'].append(
+ f"Performance declining: Recent win rate {recent_win_rate:.1f}% vs "
+ f"previous {older_win_rate:.1f}%. Consider retraining weights."
+ )
+
+ return insights
+
+ def print_detailed_report(self, initial_balance: float, final_balance: float):
+ """Print detailed performance report"""
+ metrics = self.calculate_comprehensive_metrics(initial_balance, final_balance)
+
+ print("\n" + "="*80)
+ print("📊 ENHANCED PERFORMANCE REPORT")
+ print("="*80)
+
+ # Summary
+ s = metrics['summary']
+ print(f"\n📈 SUMMARY")
+ print(f" Total Trades: {s['total_trades']}")
+ print(f" Win Rate: {s['win_rate']:.1f}% ({s['winners']}W / {s['losers']}L)")
+ print(f" Profit Factor: {s['profit_factor']:.2f}")
+ print(f" Net Profit: ${s['net_profit']:.2f}")
+ print(f" Total Return: {s['total_return_pct']:.2f}%")
+ print(f" Max Drawdown: {s['max_drawdown_pct']:.2f}%")
+ print(f" Expectancy: ${s['expectancy']:.2f}")
+ print(f" Avg R-Multiple: {s['avg_r_multiple']:.2f}R")
+
+ # Exit breakdown
+ if metrics['exit_breakdown']:
+ print(f"\n📤 EXIT TYPE BREAKDOWN")
+ for exit_type, stats in metrics['exit_breakdown'].items():
+ print(f" {exit_type}: {stats['count']} ({stats['percentage']:.1f}%) | "
+ f"Avg P/L: ${stats['avg_pnl']:.2f}")
+
+ # Indicator performance
+ if metrics['indicator_performance']:
+ print(f"\n🎯 INDICATOR PERFORMANCE (Top 5)")
+ for i, (indicator, stats) in enumerate(list(metrics['indicator_performance'].items())[:5]):
+ status_emoji = {'excellent': '🟢', 'good': '🟡', 'average': '🟠', 'poor': '🔴'}
+ emoji = status_emoji.get(stats['status'], '⚪')
+ print(f" {emoji} {indicator}: {stats['win_rate']:.1f}% WR | "
+ f"${stats['avg_pnl']:.2f} avg | {stats['avg_r']:.2f}R | "
+ f"TP1:{stats['tp1_hits']} TP2:{stats['tp2_hits']} TP3:{stats['tp3_hits']} SL:{stats['stop_hits']}")
+
+ # Weak indicators
+ weak_indicators = [ind for ind, stats in metrics['indicator_performance'].items()
+ if stats['status'] == 'poor']
+ if weak_indicators:
+ print(f"\n🔴 UNDERPERFORMING INDICATORS")
+ for ind in weak_indicators[:5]:
+ stats = metrics['indicator_performance'][ind]
+ print(f" {ind}: {stats['win_rate']:.1f}% WR, ${stats['total_pnl']:.2f} total")
+
+ # Regime performance
+ if metrics['regime_performance']:
+ print(f"\n🌍 MARKET REGIME PERFORMANCE")
+ for regime, stats in metrics['regime_performance'].items():
+ print(f" {regime}: {stats['win_rate']:.1f}% WR ({stats['trades']} trades) | "
+ f"${stats['total_pnl']:.2f}")
+
+ # Recommendations
+ if metrics['insights']['recommendations']:
+ print(f"\n💡 RECOMMENDATIONS")
+ for rec in metrics['insights']['recommendations']:
+ print(f" • {rec}")
+
+ print("\n" + "="*80)
+
+ return metrics
+
+
+def create_trade_metrics(
+ trade_log: List[Dict],
+ entry_trade: Dict,
+ exit_trades: List[Dict],
+ market_context: Dict[str, Any],
+ indicator_contributions: Dict[str, float]
+) -> TradeMetrics:
+ """
+ Helper to create TradeMetrics from trade log data.
+
+ Args:
+ trade_log: Full trade log
+ entry_trade: Entry trade dictionary
+ exit_trades: List of exit trades for this entry
+ market_context: Market context at entry
+ indicator_contributions: Indicator contributions to signal
+ """
+
+ # Calculate final outcome
+ if not exit_trades:
+ outcome = 'end_of_period'
+ exit_price = entry_trade['price']
+ profit_loss = 0
+ else:
+ last_exit = exit_trades[-1]
+ outcome = last_exit.get('type', 'end_of_period')
+ # Map trade types to outcome strings
+ outcome_map = {
+ 'take_profit_1': 'tp1',
+ 'take_profit_2': 'tp2',
+ 'take_profit_3': 'tp3',
+ 'stop_loss': 'stop_loss',
+ 'trailing_stop': 'trailing_stop',
+ 'exit_end_of_period': 'end_of_period'
+ }
+ outcome = outcome_map.get(outcome, outcome)
+ exit_price = last_exit.get('price', entry_trade['price'])
+ profit_loss = sum(t.get('profit', 0) for t in exit_trades)
+
+ # Calculate R-multiple
+ risk = abs(entry_trade['price'] - entry_trade.get('stop_loss', entry_trade['price']))
+ r_multiple = profit_loss / risk if risk > 0 else 0
+
+ return TradeMetrics(
+ trade_id=entry_trade.get('trade_id', str(entry_trade.get('index', 0))),
+ symbol=entry_trade.get('symbol', 'UNKNOWN'),
+ interval=entry_trade.get('interval', '1h'),
+ signal_type=entry_trade.get('signal', '').split()[0] if entry_trade.get('signal') else 'Unknown',
+ entry_price=entry_trade['price'],
+ exit_price=exit_price,
+ stop_loss=entry_trade.get('stop_loss', 0),
+ take_profit_targets=[
+ entry_trade.get('take_profit_1', 0),
+ entry_trade.get('take_profit_2', 0),
+ entry_trade.get('take_profit_3', 0)
+ ],
+ outcome=outcome,
+ profit_loss=profit_loss,
+ profit_loss_percent=(exit_price - entry_trade['price']) / entry_trade['price'] * 100 if entry_trade['price'] > 0 else 0,
+ risk_reward_achieved=abs(profit_loss / risk) if risk > 0 else 0,
+ entry_index=entry_trade.get('index', 0),
+ exit_index=exit_trades[-1].get('index', 0) if exit_trades else entry_trade.get('index', 0),
+ duration_candles=exit_trades[-1].get('index', 0) - entry_trade.get('index', 0) if exit_trades else 0,
+ market_regime=market_context.get('market_regime', ''),
+ volatility=market_context.get('volatility', 0),
+ volume_ratio=market_context.get('volume_ratio', 1),
+ trend=market_context.get('trend', ''),
+ contributing_indicators=list(indicator_contributions.keys()),
+ indicator_scores=indicator_contributions,
+ r_multiple=r_multiple
+ )
+
+
+# Usage example
+if __name__ == "__main__":
+ # Create calculator
+ calc = EnhancedMetricsCalculator()
+
+ # Example trade (normally these would come from backtest)
+ example_trade = TradeMetrics(
+ trade_id="test_1",
+ symbol="BTCUSDT",
+ interval="1h",
+ signal_type="Bullish",
+ entry_price=50000,
+ exit_price=51500,
+ stop_loss=49000,
+ take_profit_targets=[51000, 52000, 53000],
+ outcome="tp2",
+ profit_loss=150,
+ profit_loss_percent=3.0,
+ risk_reward_achieved=1.5,
+ entry_index=100,
+ exit_index=115,
+ duration_candles=15,
+ market_regime="trending_up",
+ volatility=0.02,
+ volume_ratio=1.2,
+ trend="uptrend",
+ contributing_indicators=["W_BULLISH_OB", "W_TREND", "W_STRUCTURE_BREAK"],
+ indicator_scores={"W_BULLISH_OB": 1.2, "W_TREND": 0.8, "W_STRUCTURE_BREAK": 1.5},
+ r_multiple=1.5
+ )
+
+ calc.add_trade(example_trade)
+ calc.print_detailed_report(10000, 10150)
+
diff --git a/back_tester/intensive_training.py b/back_tester/intensive_training.py
new file mode 100755
index 0000000..384197d
--- /dev/null
+++ b/back_tester/intensive_training.py
@@ -0,0 +1,660 @@
+#!/usr/bin/env python3
+"""
+INTENSIVE PARAMETER OPTIMIZATION
+
+This script performs aggressive optimization of ALL trading parameters:
+
+1. Signal Weights (18 weights for different indicators)
+2. Stop Loss Parameters (ATR multiplier, min distance)
+3. Take Profit Levels (TP1, TP2, TP3 R:R ratios)
+4. Trailing Stop Parameters (distance, activation level)
+5. Position Sizing (risk percentage)
+
+Uses evolutionary strategies with elitism to find optimal combinations.
+
+Usage:
+ python3 intensive_training.py --generations 50 --hours 4
+"""
+
+import os
+import sys
+import argparse
+import json
+import numpy as np
+from datetime import datetime, timedelta
+from typing import Dict, List, Tuple, Any, Optional
+from collections import defaultdict
+from dataclasses import dataclass, asdict
+import time
+import random
+import copy
+
+# Add project root to path
+project_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
+if project_dir not in sys.path:
+ sys.path.append(project_dir)
+
+from back_tester.strategy import backtest_strategy
+
+# Configuration
+SYMBOLS = ["BTCUSDT", "ETHUSDT"]
+INTERVALS = ["5m", "15m", "30m", "1h", "4h"]
+
+WEIGHT_NAMES = [
+ "W_BULLISH_OB", "W_BEARISH_OB", "W_BULLISH_BREAKER", "W_BEARISH_BREAKER",
+ "W_ABOVE_SUPPORT", "W_BELOW_RESISTANCE", "W_FVG_ABOVE", "W_FVG_BELOW",
+ "W_TREND", "W_SWEEP_HIGHS", "W_SWEEP_LOWS", "W_STRUCTURE_BREAK",
+ "W_PIN_BAR", "W_ENGULFING", "W_LIQUIDITY_POOL_ABOVE", "W_LIQUIDITY_POOL_BELOW",
+ "W_LIQUIDITY_POOL_ROUND", "W_RSI_EXTREME"
+]
+
+# Colors
+class C:
+ H = '\033[95m'
+ B = '\033[94m'
+ C = '\033[96m'
+ G = '\033[92m'
+ Y = '\033[93m'
+ R = '\033[91m'
+ E = '\033[0m'
+ BOLD = '\033[1m'
+
+
+@dataclass
+class TradingParameters:
+ """All optimizable trading parameters"""
+
+ # Signal weights (18)
+ weights: List[float]
+
+ # Stop Loss Parameters
+ atr_multiplier: float = 2.0 # ATR multiplier for stop loss distance
+ min_sl_percent: float = 0.5 # Minimum SL distance as % of entry
+
+ # Take Profit R:R Ratios
+ tp1_ratio: float = 1.5 # Risk:Reward for TP1
+ tp2_ratio: float = 2.5 # Risk:Reward for TP2
+ tp3_ratio: float = 4.0 # Risk:Reward for TP3
+
+ # Trailing Stop Parameters
+ trailing_stop_distance: float = 0.5 # Trailing stop distance %
+ trailing_activation_tp: int = 1 # Activate trailing at TP1(1), TP2(2), or TP3(3)
+
+ # Position Sizing
+ risk_percentage: float = 1.0 # Risk % per trade
+
+ @classmethod
+ def default(cls) -> 'TradingParameters':
+ """Create with default values"""
+ return cls(
+ weights=[
+ 1.2, 1.2, 1.0, 1.0, # Order blocks, breakers
+ 0.8, 0.8, 0.6, 0.6, # Support/resistance, FVG
+ 1.0, 1.3, 1.3, 1.5, # Trend, sweeps, structure
+ 0.7, 0.7, 1.0, 1.0, # Patterns, liquidity
+ 1.2, 0.5 # Round numbers, RSI
+ ]
+ )
+
+ @classmethod
+ def random(cls) -> 'TradingParameters':
+ """Create with random values for exploration"""
+ return cls(
+ weights=[random.uniform(0.3, 2.0) for _ in range(18)],
+ atr_multiplier=random.uniform(1.0, 4.0),
+ min_sl_percent=random.uniform(0.3, 1.5),
+ tp1_ratio=random.uniform(1.0, 2.5),
+ tp2_ratio=random.uniform(2.0, 4.0),
+ tp3_ratio=random.uniform(3.0, 6.0),
+ trailing_stop_distance=random.uniform(0.3, 1.5),
+ trailing_activation_tp=random.choice([1, 2]),
+ risk_percentage=random.uniform(0.5, 2.0)
+ )
+
+ def mutate(self, mutation_rate: float = 0.3, mutation_strength: float = 0.15) -> 'TradingParameters':
+ """Create mutated copy of parameters"""
+ new_params = copy.deepcopy(self)
+
+ # Mutate weights
+ for i in range(len(new_params.weights)):
+ if random.random() < mutation_rate:
+ change = np.random.normal(0, mutation_strength)
+ new_params.weights[i] = max(0.1, min(3.0, new_params.weights[i] + change))
+
+ # Mutate SL parameters
+ if random.random() < mutation_rate:
+ new_params.atr_multiplier = max(0.5, min(5.0,
+ new_params.atr_multiplier + np.random.normal(0, 0.3)))
+
+ if random.random() < mutation_rate:
+ new_params.min_sl_percent = max(0.2, min(2.0,
+ new_params.min_sl_percent + np.random.normal(0, 0.2)))
+
+ # Mutate TP ratios (keep ordering: TP1 < TP2 < TP3)
+ if random.random() < mutation_rate:
+ new_params.tp1_ratio = max(0.8, min(3.0,
+ new_params.tp1_ratio + np.random.normal(0, 0.3)))
+
+ if random.random() < mutation_rate:
+ new_params.tp2_ratio = max(new_params.tp1_ratio + 0.5, min(5.0,
+ new_params.tp2_ratio + np.random.normal(0, 0.4)))
+
+ if random.random() < mutation_rate:
+ new_params.tp3_ratio = max(new_params.tp2_ratio + 0.5, min(8.0,
+ new_params.tp3_ratio + np.random.normal(0, 0.5)))
+
+ # Mutate trailing stop
+ if random.random() < mutation_rate:
+ new_params.trailing_stop_distance = max(0.2, min(2.0,
+ new_params.trailing_stop_distance + np.random.normal(0, 0.2)))
+
+ if random.random() < mutation_rate * 0.5: # Less frequent
+ new_params.trailing_activation_tp = random.choice([1, 2])
+
+ # Mutate risk
+ if random.random() < mutation_rate:
+ new_params.risk_percentage = max(0.25, min(3.0,
+ new_params.risk_percentage + np.random.normal(0, 0.2)))
+
+ return new_params
+
+ @staticmethod
+ def crossover(parent1: 'TradingParameters', parent2: 'TradingParameters') -> 'TradingParameters':
+ """Create child from two parents"""
+ child_weights = []
+ for i in range(len(parent1.weights)):
+ if random.random() < 0.5:
+ child_weights.append(parent1.weights[i])
+ else:
+ child_weights.append(parent2.weights[i])
+
+ # Randomly inherit other parameters
+ return TradingParameters(
+ weights=child_weights,
+ atr_multiplier=random.choice([parent1.atr_multiplier, parent2.atr_multiplier]),
+ min_sl_percent=random.choice([parent1.min_sl_percent, parent2.min_sl_percent]),
+ tp1_ratio=random.choice([parent1.tp1_ratio, parent2.tp1_ratio]),
+ tp2_ratio=random.choice([parent1.tp2_ratio, parent2.tp2_ratio]),
+ tp3_ratio=random.choice([parent1.tp3_ratio, parent2.tp3_ratio]),
+ trailing_stop_distance=random.choice([parent1.trailing_stop_distance, parent2.trailing_stop_distance]),
+ trailing_activation_tp=random.choice([parent1.trailing_activation_tp, parent2.trailing_activation_tp]),
+ risk_percentage=random.choice([parent1.risk_percentage, parent2.risk_percentage]),
+ )
+
+ def to_dict(self) -> Dict:
+ """Convert to dictionary for saving"""
+ return {
+ 'weights': self.weights,
+ 'weight_names': WEIGHT_NAMES,
+ 'atr_multiplier': self.atr_multiplier,
+ 'min_sl_percent': self.min_sl_percent,
+ 'tp1_ratio': self.tp1_ratio,
+ 'tp2_ratio': self.tp2_ratio,
+ 'tp3_ratio': self.tp3_ratio,
+ 'trailing_stop_distance': self.trailing_stop_distance,
+ 'trailing_activation_tp': self.trailing_activation_tp,
+ 'risk_percentage': self.risk_percentage,
+ }
+
+ @classmethod
+ def from_dict(cls, data: Dict) -> 'TradingParameters':
+ """Load from dictionary"""
+ return cls(
+ weights=data.get('weights', cls.default().weights),
+ atr_multiplier=data.get('atr_multiplier', 2.0),
+ min_sl_percent=data.get('min_sl_percent', 0.5),
+ tp1_ratio=data.get('tp1_ratio', 1.5),
+ tp2_ratio=data.get('tp2_ratio', 2.5),
+ tp3_ratio=data.get('tp3_ratio', 4.0),
+ trailing_stop_distance=data.get('trailing_stop_distance', 0.5),
+ trailing_activation_tp=data.get('trailing_activation_tp', 1),
+ risk_percentage=data.get('risk_percentage', 1.0),
+ )
+
+
+class IntensiveParameterOptimizer:
+ """
+ Comprehensive parameter optimizer using evolutionary strategies.
+ Optimizes weights AND risk management parameters together.
+ """
+
+ def __init__(
+ self,
+ storage_path: str = "./data/intensive_training",
+ initial_balance: float = 10000.0,
+ population_size: int = 8,
+ elite_count: int = 2,
+ tests_per_evaluation: int = 10,
+ ):
+ self.storage_path = storage_path
+ self.initial_balance = initial_balance
+ self.population_size = population_size
+ self.elite_count = elite_count
+ self.tests_per_evaluation = tests_per_evaluation
+
+ os.makedirs(storage_path, exist_ok=True)
+ os.makedirs(os.path.join(storage_path, "checkpoints"), exist_ok=True)
+
+ # Load or initialize parameters
+ self.best_params = self._load_params()
+ self.best_fitness = float('-inf')
+
+ # Tracking
+ self.generation = 0
+ self.total_backtests = 0
+ self.total_trades = 0
+ self.start_time = None
+
+ # History
+ self.fitness_history = []
+ self.param_history = []
+
+ def _load_params(self) -> TradingParameters:
+ """Load parameters from file or use defaults"""
+ params_file = os.path.join(self.storage_path, "best_params.json")
+
+ if os.path.exists(params_file):
+ try:
+ with open(params_file, 'r') as f:
+ data = json.load(f)
+ params = TradingParameters.from_dict(data)
+ print(f"{C.G}✓ Loaded existing parameters from checkpoint{C.E}")
+ return params
+ except Exception as e:
+ print(f"{C.Y}Warning: Could not load params: {e}{C.E}")
+
+ return TradingParameters.default()
+
+ def _save_params(self, params: TradingParameters, fitness: float, is_best: bool = False):
+ """Save parameters to file"""
+ data = params.to_dict()
+ data['fitness'] = fitness
+ data['generation'] = self.generation
+ data['total_backtests'] = self.total_backtests
+ data['timestamp'] = datetime.now().isoformat()
+
+ filename = "best_params.json" if is_best else f"params_gen_{self.generation}.json"
+ filepath = os.path.join(self.storage_path, filename)
+
+ with open(filepath, 'w') as f:
+ json.dump(data, f, indent=2)
+
+ if is_best:
+ checkpoint_file = os.path.join(
+ self.storage_path, "checkpoints",
+ f"best_gen_{self.generation}_fit_{fitness:.1f}.json"
+ )
+ with open(checkpoint_file, 'w') as f:
+ json.dump(data, f, indent=2)
+
+ def evaluate_params(self, params: TradingParameters) -> Tuple[float, Dict[str, Any]]:
+ """
+ Evaluate a parameter set by running multiple backtests.
+ """
+ total_profit = 0.0
+ total_trades = 0
+ wins = 0
+ losses = 0
+ tp1_hits = 0
+ tp2_hits = 0
+ tp3_hits = 0
+ sl_hits = 0
+ results = []
+
+ # Generate test configurations
+ test_configs = []
+ for _ in range(self.tests_per_evaluation):
+ symbol = random.choice(SYMBOLS)
+ interval = random.choice(INTERVALS)
+ candles = random.randint(300, 600)
+ test_configs.append((symbol, interval, candles))
+
+ for symbol, interval, candles in test_configs:
+ try:
+ window = int(candles * 0.5)
+
+ # Run backtest with these parameters
+ # Note: We need to pass custom TP/SL params through the signal generation
+ # For now, we use the weights and trailing stop params that strategy.py accepts
+ final_balance, trades, _ = backtest_strategy(
+ symbol=symbol,
+ interval=interval,
+ candles=candles,
+ window=window,
+ initial_balance=self.initial_balance,
+ risk_percentage=params.risk_percentage,
+ weights=params.weights,
+ use_trailing_stop=True,
+ trailing_stop_distance_percent=params.trailing_stop_distance,
+ )
+
+ profit = final_balance - self.initial_balance
+
+ # Analyze trades
+ for trade in trades:
+ trade_type = trade.get('type', '')
+ if trade_type == 'entry':
+ total_trades += 1
+ elif trade_type == 'take_profit_1':
+ tp1_hits += 1
+ if trade.get('profit', 0) > 0:
+ wins += 1
+ elif trade_type == 'take_profit_2':
+ tp2_hits += 1
+ if trade.get('profit', 0) > 0:
+ wins += 1
+ elif trade_type == 'take_profit_3':
+ tp3_hits += 1
+ if trade.get('profit', 0) > 0:
+ wins += 1
+ elif trade_type == 'stop_loss':
+ sl_hits += 1
+ losses += 1
+ elif trade_type == 'exit_end_of_period':
+ if trade.get('profit', 0) > 0:
+ wins += 1
+ else:
+ losses += 1
+
+ total_profit += profit
+ self.total_backtests += 1
+
+ results.append({
+ 'symbol': symbol,
+ 'interval': interval,
+ 'profit': profit,
+ 'trades': len([t for t in trades if t.get('type') == 'entry'])
+ })
+
+ except Exception as e:
+ total_profit -= 50 # Penalty for errors
+ results.append({'error': str(e)})
+
+ self.total_trades += total_trades
+
+ # Calculate fitness
+ if total_trades == 0:
+ return -1000, {'error': 'No trades'}
+
+ total_exits = wins + losses
+ win_rate = wins / total_exits if total_exits > 0 else 0
+ avg_profit = total_profit / self.tests_per_evaluation
+
+ # TP distribution score (prefer hitting higher TPs)
+ total_tp_hits = tp1_hits + tp2_hits + tp3_hits
+ if total_tp_hits > 0:
+ tp_quality = (tp1_hits * 1 + tp2_hits * 2 + tp3_hits * 3) / (total_tp_hits * 3)
+ else:
+ tp_quality = 0
+
+ # Calculate risk-adjusted return
+ if sl_hits > 0:
+ reward_risk = (tp1_hits + tp2_hits * 1.5 + tp3_hits * 2) / sl_hits
+ else:
+ reward_risk = 2.0 # Default if no SL hits
+
+ # Composite fitness function
+ fitness = (
+ avg_profit * 0.30 + # Profitability
+ win_rate * 100 * 0.25 + # Win rate (scaled to ~50)
+ tp_quality * 50 * 0.15 + # TP quality (scaled to ~50)
+ min(reward_risk, 3) * 15 * 0.15 + # Risk/reward (capped, scaled)
+ min(total_trades / 5, 20) * 0.15 # Trade frequency (capped)
+ )
+
+ metrics = {
+ 'total_profit': total_profit,
+ 'avg_profit': avg_profit,
+ 'total_trades': total_trades,
+ 'wins': wins,
+ 'losses': losses,
+ 'win_rate': win_rate * 100,
+ 'tp1_hits': tp1_hits,
+ 'tp2_hits': tp2_hits,
+ 'tp3_hits': tp3_hits,
+ 'sl_hits': sl_hits,
+ 'tp_quality': tp_quality,
+ 'reward_risk': reward_risk,
+ }
+
+ return fitness, metrics
+
+ def train_generation(self) -> Tuple[TradingParameters, float]:
+ """Run one generation of evolutionary optimization"""
+ self.generation += 1
+
+ print(f"\n{C.H}{'='*70}{C.E}")
+ print(f"{C.H} GENERATION {self.generation}{C.E}")
+ print(f"{C.H}{'='*70}{C.E}")
+
+ # Create population
+ population = []
+
+ # Keep elite (best from previous generation)
+ population.append(self.best_params)
+
+ # Mutations of best
+ for _ in range(self.population_size - 3):
+ mutated = self.best_params.mutate(mutation_rate=0.4, mutation_strength=0.2)
+ population.append(mutated)
+
+ # One random for exploration
+ population.append(TradingParameters.random())
+
+ # One with aggressive settings
+ aggressive = TradingParameters(
+ weights=[w * random.uniform(0.8, 1.2) for w in self.best_params.weights],
+ atr_multiplier=1.5, # Tighter SL
+ tp1_ratio=1.2, # Quick TP1
+ tp2_ratio=2.0,
+ tp3_ratio=3.5,
+ trailing_stop_distance=0.4,
+ trailing_activation_tp=1,
+ risk_percentage=1.5,
+ )
+ population.append(aggressive)
+
+ # Evaluate all
+ results = []
+ for i, params in enumerate(population):
+ label = "ELITE" if i == 0 else f"#{i+1}"
+ print(f"\n Testing {label}... ", end="", flush=True)
+
+ fitness, metrics = self.evaluate_params(params)
+ results.append((fitness, params, metrics))
+
+ color = C.G if fitness > self.best_fitness else (C.Y if fitness > self.best_fitness - 20 else C.R)
+ print(f"{color}Fitness: {fitness:.1f} | "
+ f"Profit: ${metrics.get('avg_profit', 0):.2f} | "
+ f"WR: {metrics.get('win_rate', 0):.1f}% | "
+ f"TP1:{metrics.get('tp1_hits',0)} TP2:{metrics.get('tp2_hits',0)} TP3:{metrics.get('tp3_hits',0)} SL:{metrics.get('sl_hits',0)}{C.E}")
+
+ # Sort by fitness
+ results.sort(key=lambda x: x[0], reverse=True)
+
+ # Get generation best
+ gen_best_fitness, gen_best_params, gen_best_metrics = results[0]
+
+ # Track history
+ self.fitness_history.append(gen_best_fitness)
+
+ # Update global best
+ if gen_best_fitness > self.best_fitness:
+ improvement = gen_best_fitness - self.best_fitness
+ self.best_fitness = gen_best_fitness
+ self.best_params = copy.deepcopy(gen_best_params)
+
+ print(f"\n{C.G}{'★'*3} NEW BEST! Fitness: {gen_best_fitness:.1f} (+{improvement:.1f}) {'★'*3}{C.E}")
+
+ # Show key parameters
+ print(f"\n{C.C}Key Parameters:{C.E}")
+ print(f" ATR Multiplier: {gen_best_params.atr_multiplier:.2f}")
+ print(f" TP Ratios: {gen_best_params.tp1_ratio:.1f} / {gen_best_params.tp2_ratio:.1f} / {gen_best_params.tp3_ratio:.1f}")
+ print(f" Trailing Stop: {gen_best_params.trailing_stop_distance:.2f}% (activate at TP{gen_best_params.trailing_activation_tp})")
+ print(f" Risk per Trade: {gen_best_params.risk_percentage:.2f}%")
+
+ self._save_params(gen_best_params, gen_best_fitness, is_best=True)
+ else:
+ # Crossover top performers for next generation
+ if len(results) >= 2:
+ child = TradingParameters.crossover(results[0][1], results[1][1])
+ self.best_params = child.mutate(mutation_rate=0.2)
+
+ return gen_best_params, gen_best_fitness
+
+ def run(
+ self,
+ max_generations: int = 100,
+ max_hours: float = 4.0,
+ target_fitness: float = None
+ ) -> Tuple[TradingParameters, float]:
+ """Run full optimization"""
+ self.start_time = datetime.now()
+ end_time = self.start_time + timedelta(hours=max_hours)
+
+ print(f"\n{C.H}{'='*70}{C.E}")
+ print(f"{C.H}{'INTENSIVE PARAMETER OPTIMIZATION':^70}{C.E}")
+ print(f"{C.H}{'='*70}{C.E}")
+
+ print(f"\n{C.C}Configuration:{C.E}")
+ print(f" Max Generations: {max_generations}")
+ print(f" Max Time: {max_hours} hours")
+ print(f" Population Size: {self.population_size}")
+ print(f" Tests per Evaluation: {self.tests_per_evaluation}")
+ print(f" Symbols: {SYMBOLS}")
+ print(f" Intervals: {INTERVALS}")
+
+ print(f"\n{C.C}Optimizing:{C.E}")
+ print(f" • 18 Signal Weights")
+ print(f" • Stop Loss (ATR multiplier, min distance)")
+ print(f" • Take Profit Levels (TP1, TP2, TP3 R:R ratios)")
+ print(f" • Trailing Stop (distance, activation)")
+ print(f" • Risk Percentage")
+
+ print(f"\n{C.Y}Starting optimization... (Ctrl+C to stop and save){C.E}")
+
+ try:
+ for gen in range(max_generations):
+ if datetime.now() >= end_time:
+ print(f"\n{C.Y}Time limit reached{C.E}")
+ break
+
+ best_params, best_fitness = self.train_generation()
+
+ if target_fitness and best_fitness >= target_fitness:
+ print(f"\n{C.G}Target fitness reached!{C.E}")
+ break
+
+ # Progress every 5 gens
+ if self.generation % 5 == 0:
+ elapsed = (datetime.now() - self.start_time).total_seconds() / 60
+ print(f"\n{C.B}═══ Progress: Gen {self.generation} | "
+ f"Best: {self.best_fitness:.1f} | "
+ f"Backtests: {self.total_backtests} | "
+ f"Time: {elapsed:.1f}m ═══{C.E}")
+
+ except KeyboardInterrupt:
+ print(f"\n{C.Y}Optimization interrupted{C.E}")
+
+ self._print_final_report()
+ return self.best_params, self.best_fitness
+
+ def _print_final_report(self):
+ """Print comprehensive final report"""
+ elapsed = (datetime.now() - self.start_time).total_seconds()
+
+ print(f"\n{C.H}{'='*70}{C.E}")
+ print(f"{C.H}{'OPTIMIZATION COMPLETE':^70}{C.E}")
+ print(f"{C.H}{'='*70}{C.E}")
+
+ print(f"\n{C.BOLD}Statistics:{C.E}")
+ print(f" Generations: {self.generation}")
+ print(f" Backtests: {self.total_backtests}")
+ print(f" Trades: {self.total_trades}")
+ print(f" Time: {elapsed/60:.1f} minutes")
+ print(f" Best Fitness: {self.best_fitness:.1f}")
+
+ p = self.best_params
+
+ print(f"\n{C.BOLD}═══ OPTIMIZED PARAMETERS ═══{C.E}")
+
+ print(f"\n{C.C}Stop Loss:{C.E}")
+ print(f" ATR Multiplier: {p.atr_multiplier:.2f}")
+ print(f" Min SL Distance: {p.min_sl_percent:.2f}%")
+
+ print(f"\n{C.C}Take Profit Levels:{C.E}")
+ print(f" TP1 R:R Ratio: {p.tp1_ratio:.2f}")
+ print(f" TP2 R:R Ratio: {p.tp2_ratio:.2f}")
+ print(f" TP3 R:R Ratio: {p.tp3_ratio:.2f}")
+
+ print(f"\n{C.C}Trailing Stop:{C.E}")
+ print(f" Distance: {p.trailing_stop_distance:.2f}%")
+ print(f" Activation: TP{p.trailing_activation_tp}")
+
+ print(f"\n{C.C}Position Sizing:{C.E}")
+ print(f" Risk per Trade: {p.risk_percentage:.2f}%")
+
+ print(f"\n{C.C}Signal Weights:{C.E}")
+ for name, weight in zip(WEIGHT_NAMES, p.weights):
+ bar_len = int(weight * 8)
+ bar = "█" * bar_len + "░" * (16 - bar_len)
+
+ if weight > 1.5:
+ color, status = C.G, "STRONG"
+ elif weight < 0.6:
+ color, status = C.R, "WEAK"
+ else:
+ color, status = C.E, ""
+
+ print(f" {color}{name:<25} {weight:.3f} {bar} {status}{C.E}")
+
+ # Save
+ self._save_params(self.best_params, self.best_fitness, is_best=True)
+ print(f"\n{C.G}✓ Parameters saved to: {self.storage_path}/best_params.json{C.E}")
+
+ # Fitness trend
+ if len(self.fitness_history) > 1:
+ print(f"\n{C.BOLD}Fitness Trend:{C.E}")
+ start = self.fitness_history[0]
+ end = self.fitness_history[-1]
+ change = end - start
+ trend = "📈" if change > 0 else "📉"
+ print(f" Start: {start:.1f} → End: {end:.1f} ({trend} {change:+.1f})")
+
+
+def main():
+ parser = argparse.ArgumentParser(description="Intensive trading parameter optimization")
+ parser.add_argument('--generations', type=int, default=50)
+ parser.add_argument('--hours', type=float, default=2.0)
+ parser.add_argument('--population', type=int, default=8)
+ parser.add_argument('--tests', type=int, default=10, help='Backtests per evaluation')
+ parser.add_argument('--balance', type=float, default=10000.0)
+ parser.add_argument('--storage', type=str, default='./data/intensive_training')
+ parser.add_argument('--target-fitness', type=float, default=None)
+
+ args = parser.parse_args()
+
+ optimizer = IntensiveParameterOptimizer(
+ storage_path=args.storage,
+ initial_balance=args.balance,
+ population_size=args.population,
+ tests_per_evaluation=args.tests,
+ )
+
+ try:
+ best_params, best_fitness = optimizer.run(
+ max_generations=args.generations,
+ max_hours=args.hours,
+ target_fitness=args.target_fitness
+ )
+ print(f"\n{C.G}Optimization completed! Best fitness: {best_fitness:.1f}{C.E}")
+
+ except Exception as e:
+ print(f"\n{C.R}Optimization failed: {e}{C.E}")
+ import traceback
+ traceback.print_exc()
+ sys.exit(1)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/back_tester/run_learning_backtest.py b/back_tester/run_learning_backtest.py
new file mode 100755
index 0000000..3eee13e
--- /dev/null
+++ b/back_tester/run_learning_backtest.py
@@ -0,0 +1,503 @@
+#!/usr/bin/env python3
+"""
+Self-Learning Backtesting Runner
+
+This script runs comprehensive backtests on BTCUSDT and ETHUSDT across all
+time intervals with AGGRESSIVE self-learning. The system:
+- Learns from each trade outcome
+- Directly adjusts weights based on indicator performance
+- Runs many iterations to find optimal weights
+
+Usage:
+ python3 run_learning_backtest.py [options]
+
+Options:
+ --iterations N Number of learning iterations (default: 20)
+ --candles N Number of candles per backtest
+ --balance N Initial balance (default: 10000)
+ --learning-rate How fast weights change (default: 0.15)
+"""
+
+import os
+import sys
+import argparse
+import json
+from datetime import datetime
+from typing import Dict, List, Any, Tuple
+from collections import defaultdict
+import numpy as np
+import random
+
+# Add project root to path
+project_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
+if project_dir not in sys.path:
+ sys.path.append(project_dir)
+
+from back_tester.strategy import backtest_strategy
+
+# Configuration
+SYMBOLS = ["BTCUSDT", "ETHUSDT"]
+INTERVALS = ["5m", "15m", "30m", "1h", "4h"] # Most useful intervals
+
+WEIGHT_NAMES = [
+ "W_BULLISH_OB", "W_BEARISH_OB", "W_BULLISH_BREAKER", "W_BEARISH_BREAKER",
+ "W_ABOVE_SUPPORT", "W_BELOW_RESISTANCE", "W_FVG_ABOVE", "W_FVG_BELOW",
+ "W_TREND", "W_SWEEP_HIGHS", "W_SWEEP_LOWS", "W_STRUCTURE_BREAK",
+ "W_PIN_BAR", "W_ENGULFING", "W_LIQUIDITY_POOL_ABOVE", "W_LIQUIDITY_POOL_BELOW",
+ "W_LIQUIDITY_POOL_ROUND", "W_RSI_EXTREME"
+]
+
+# Candle settings per interval (increased for more trades)
+# Binance allows up to 1000 candles per request, but we can fetch more with pagination
+CANDLES_PER_INTERVAL = {
+ "5m": 2000, # ~7 days of data
+ "15m": 1500, # ~15 days of data
+ "30m": 1200, # ~25 days of data
+ "1h": 1000, # ~42 days of data
+ "4h": 800, # ~133 days of data (~4.5 months)
+}
+
+class Colors:
+ H = '\033[95m'
+ B = '\033[94m'
+ C = '\033[96m'
+ G = '\033[92m'
+ Y = '\033[93m'
+ R = '\033[91m'
+ E = '\033[0m'
+ BOLD = '\033[1m'
+
+
+class AggressiveLearner:
+ """
+ Aggressive weight learning with direct performance-based updates.
+ Unlike conservative learners, this one makes BIG changes.
+ """
+
+ def __init__(
+ self,
+ storage_path: str = "./data/btc_eth_learning",
+ initial_balance: float = 10000.0,
+ learning_rate: float = 0.15, # Much higher than typical 0.05
+ lr_decay: float = 0.95, # Decay rate per iteration (prevents overfitting)
+ ):
+ self.storage_path = storage_path
+ self.initial_balance = initial_balance
+ self.base_learning_rate = learning_rate
+ self.learning_rate = learning_rate
+ self.lr_decay = lr_decay
+
+ os.makedirs(storage_path, exist_ok=True)
+
+ # Load or initialize weights
+ self.weights = self._load_weights()
+ self.best_weights = self.weights.copy()
+ self.best_profit = float('-inf')
+
+ # Track indicator performance across all trades
+ self.indicator_stats = defaultdict(lambda: {
+ 'win_count': 0, 'loss_count': 0,
+ 'win_profit': 0.0, 'loss_amount': 0.0,
+ 'appearances': 0
+ })
+
+ # Track overall performance
+ self.iteration = 0
+ self.total_trades = 0
+ self.total_profit = 0.0
+ self.profit_history = []
+ self.weight_history = []
+
+ def _load_weights(self) -> List[float]:
+ """Load weights from file or use optimized defaults"""
+ weights_file = os.path.join(self.storage_path, "final_weights.json")
+
+ if os.path.exists(weights_file):
+ try:
+ with open(weights_file, 'r') as f:
+ data = json.load(f)
+ if 'weights' in data:
+ weights_data = data['weights']
+ # Handle dict format (name -> value)
+ if isinstance(weights_data, dict):
+ if len(weights_data) == len(WEIGHT_NAMES):
+ weights = [float(weights_data.get(name, 1.0)) for name in WEIGHT_NAMES]
+ print(f"{Colors.G}✓ Loaded weights from previous session{Colors.E}")
+ return weights
+ # Handle list format
+ elif isinstance(weights_data, list) and len(weights_data) == len(WEIGHT_NAMES):
+ weights = [float(w) for w in weights_data]
+ print(f"{Colors.G}✓ Loaded weights from previous session{Colors.E}")
+ return weights
+ except Exception as e:
+ print(f"{Colors.Y}Warning: Could not load weights: {e}{Colors.E}")
+
+ # Smart starting weights (not all 1.0)
+ return [
+ 1.3, # W_BULLISH_OB - Order blocks tend to work well
+ 1.3, # W_BEARISH_OB
+ 1.1, # W_BULLISH_BREAKER
+ 1.1, # W_BEARISH_BREAKER
+ 0.8, # W_ABOVE_SUPPORT
+ 0.8, # W_BELOW_RESISTANCE
+ 0.6, # W_FVG_ABOVE - FVGs less reliable alone
+ 0.6, # W_FVG_BELOW
+ 1.0, # W_TREND
+ 1.4, # W_SWEEP_HIGHS - Sweeps are key signals
+ 1.4, # W_SWEEP_LOWS
+ 1.5, # W_STRUCTURE_BREAK - Very important
+ 0.7, # W_PIN_BAR
+ 0.7, # W_ENGULFING
+ 1.0, # W_LIQUIDITY_POOL_ABOVE
+ 1.0, # W_LIQUIDITY_POOL_BELOW
+ 1.2, # W_LIQUIDITY_POOL_ROUND
+ 0.5, # W_RSI_EXTREME - Often unreliable
+ ]
+
+ def _save_weights(self):
+ """Save current weights"""
+ data = {
+ 'weights': self.weights,
+ 'weight_names': WEIGHT_NAMES,
+ 'best_weights': self.best_weights,
+ 'best_profit': self.best_profit,
+ 'iteration': self.iteration,
+ 'total_trades': self.total_trades,
+ 'total_profit': self.total_profit,
+ 'timestamp': datetime.now().isoformat(),
+ 'indicator_stats': dict(self.indicator_stats),
+ }
+
+ with open(os.path.join(self.storage_path, "final_weights.json"), 'w') as f:
+ json.dump(data, f, indent=2)
+
+ def run_backtest(self, symbol: str, interval: str, candles: int) -> Dict:
+ """Run a single backtest and return results"""
+ window = int(candles * 0.5)
+
+ try:
+ final_balance, trades, _ = backtest_strategy(
+ symbol=symbol,
+ interval=interval,
+ candles=candles,
+ window=window,
+ initial_balance=self.initial_balance,
+ risk_percentage=1.0,
+ weights=self.weights,
+ use_trailing_stop=True,
+ trailing_stop_distance_percent=0.5,
+ )
+
+ profit = final_balance - self.initial_balance
+ entry_trades = [t for t in trades if t.get('type') == 'entry']
+
+ # Analyze exits
+ wins = 0
+ losses = 0
+ for t in trades:
+ if t.get('type') in ['take_profit_1', 'take_profit_2', 'take_profit_3']:
+ wins += 1
+ elif t.get('type') == 'stop_loss':
+ losses += 1
+ elif t.get('type') == 'exit_end_of_period':
+ if t.get('profit', 0) > 0:
+ wins += 1
+ else:
+ losses += 1
+
+ return {
+ 'success': True,
+ 'profit': profit,
+ 'trades': len(entry_trades),
+ 'wins': wins,
+ 'losses': losses,
+ 'win_rate': (wins / (wins + losses) * 100) if (wins + losses) > 0 else 0,
+ }
+
+ except Exception as e:
+ return {'success': False, 'error': str(e), 'profit': 0, 'trades': 0}
+
+ def update_weights(self, iteration_profit: float, iteration_trades: int, win_rate: float):
+ """
+ Update weights based on iteration performance.
+ Uses multiple strategies for learning.
+ """
+ if iteration_trades == 0:
+ return
+
+ # Strategy 1: Random perturbation with momentum
+ # If profit was good, keep direction. If bad, try opposite.
+ for i in range(len(self.weights)):
+ # Add random exploration
+ noise = np.random.normal(0, self.learning_rate * 0.5)
+
+ # Bias based on performance
+ if iteration_profit > 0:
+ # Profitable - small random changes
+ self.weights[i] += noise * 0.5
+ else:
+ # Losing - try bigger changes
+ self.weights[i] += noise * 1.5
+
+ # Keep within bounds
+ self.weights[i] = max(0.2, min(2.5, self.weights[i]))
+
+ # Strategy 2: Win rate based adjustment
+ # If win rate is low, reduce all weights slightly (be more selective)
+ # If win rate is high, can afford to increase weights
+ if win_rate < 40:
+ for i in range(len(self.weights)):
+ self.weights[i] *= 0.95 # Reduce by 5%
+ elif win_rate > 60:
+ for i in range(len(self.weights)):
+ self.weights[i] *= 1.02 # Increase by 2%
+
+ # Re-clamp
+ for i in range(len(self.weights)):
+ self.weights[i] = max(0.2, min(2.5, self.weights[i]))
+
+ def run_iteration(self) -> Dict:
+ """Run one complete iteration over all symbols and intervals"""
+ self.iteration += 1
+
+ print(f"\n{Colors.H}{'='*70}{Colors.E}")
+ print(f"{Colors.H} ITERATION {self.iteration}{Colors.E}")
+ print(f"{Colors.H}{'='*70}{Colors.E}")
+
+ iteration_profit = 0.0
+ iteration_trades = 0
+ iteration_wins = 0
+ iteration_losses = 0
+
+ for symbol in SYMBOLS:
+ print(f"\n{Colors.C}--- {symbol} ---{Colors.E}")
+
+ for interval in INTERVALS:
+ candles = CANDLES_PER_INTERVAL.get(interval, 400)
+ print(f" {interval:>3}: ", end="", flush=True)
+
+ result = self.run_backtest(symbol, interval, candles)
+
+ if result['success']:
+ profit = result['profit']
+ trades = result['trades']
+ wr = result['win_rate']
+
+ iteration_profit += profit
+ iteration_trades += trades
+ iteration_wins += result['wins']
+ iteration_losses += result['losses']
+
+ color = Colors.G if profit > 0 else Colors.R
+ print(f"{color}${profit:+8.2f}{Colors.E} | {trades:2d} trades | {wr:5.1f}% WR")
+ else:
+ print(f"{Colors.R}FAILED: {result.get('error', 'Unknown')[:30]}{Colors.E}")
+
+ # Update totals
+ self.total_trades += iteration_trades
+ self.total_profit += iteration_profit
+
+ # Calculate iteration win rate
+ iter_wr = (iteration_wins / (iteration_wins + iteration_losses) * 100
+ if (iteration_wins + iteration_losses) > 0 else 0)
+
+ # Print summary
+ print(f"\n{Colors.BOLD}Iteration {self.iteration} Summary:{Colors.E}")
+ color = Colors.G if iteration_profit > 0 else Colors.R
+ print(f" Profit: {color}${iteration_profit:+.2f}{Colors.E}")
+ print(f" Trades: {iteration_trades}")
+ print(f" Win Rate: {iter_wr:.1f}%")
+
+ # Track best
+ if iteration_profit > self.best_profit:
+ self.best_profit = iteration_profit
+ self.best_weights = self.weights.copy()
+ print(f" {Colors.G}★ NEW BEST!{Colors.E}")
+
+ # Update weights AGGRESSIVELY based on performance
+ self.update_weights(iteration_profit, iteration_trades, iter_wr)
+
+ # Apply learning rate decay (prevents overfitting in later iterations)
+ self.learning_rate = self.base_learning_rate * (self.lr_decay ** self.iteration)
+
+ # Show weight changes
+ print(f"\n{Colors.Y}Weight adjustments:{Colors.E}")
+ changed = 0
+ for i, (name, weight) in enumerate(zip(WEIGHT_NAMES, self.weights)):
+ if i < len(self.weight_history) and len(self.weight_history) > 0:
+ old = self.weight_history[-1][i] if self.weight_history else 1.0
+ diff = weight - old
+ if abs(diff) > 0.01:
+ direction = "↑" if diff > 0 else "↓"
+ print(f" {name}: {old:.3f} → {weight:.3f} ({direction}{abs(diff):.3f})")
+ changed += 1
+
+ if changed == 0:
+ print(" (First iteration - weights established)")
+
+ self.weight_history.append(self.weights.copy())
+ self.profit_history.append(iteration_profit)
+
+ # Save after each iteration
+ self._save_weights()
+
+ return {
+ 'iteration': self.iteration,
+ 'profit': iteration_profit,
+ 'trades': iteration_trades,
+ 'win_rate': iter_wr,
+ }
+
+ def run(self, iterations: int = 20) -> Dict:
+ """Run multiple iterations of learning"""
+ print(f"\n{Colors.H}{'='*70}{Colors.E}")
+ print(f"{Colors.H}{'AGGRESSIVE SELF-LEARNING BACKTEST':^70}{Colors.E}")
+ print(f"{Colors.H}{'='*70}{Colors.E}")
+
+ print(f"\n{Colors.C}Configuration:{Colors.E}")
+ print(f" Iterations: {iterations}")
+ print(f" Symbols: {SYMBOLS}")
+ print(f" Intervals: {INTERVALS}")
+ print(f" Learning Rate: {self.learning_rate} (decay: {self.lr_decay}/iter)")
+ print(f" Initial Balance: ${self.initial_balance:,.2f}")
+
+ # Show candle counts per interval
+ total_candles = sum(CANDLES_PER_INTERVAL.values()) * len(SYMBOLS)
+ print(f"\n{Colors.C}Data per iteration:{Colors.E}")
+ for interval, candles in CANDLES_PER_INTERVAL.items():
+ print(f" {interval:>3}: {candles:,} candles × {len(SYMBOLS)} symbols")
+ print(f" Total: ~{total_candles:,} candles/iteration (expect 200-400+ trades)")
+
+ print(f"\n{Colors.C}Starting weights:{Colors.E}")
+ for name, weight in zip(WEIGHT_NAMES, self.weights):
+ print(f" {name}: {weight:.3f}")
+
+ start_time = datetime.now()
+
+ try:
+ for _ in range(iterations):
+ self.run_iteration()
+ except KeyboardInterrupt:
+ print(f"\n{Colors.Y}Interrupted - saving progress...{Colors.E}")
+
+ # Final report
+ self._print_final_report(start_time)
+
+ return {
+ 'total_iterations': self.iteration,
+ 'total_trades': self.total_trades,
+ 'total_profit': self.total_profit,
+ 'best_profit': self.best_profit,
+ 'final_weights': self.weights,
+ 'best_weights': self.best_weights,
+ }
+
+ def _print_final_report(self, start_time: datetime):
+ """Print final report"""
+ elapsed = (datetime.now() - start_time).total_seconds()
+
+ print(f"\n{Colors.H}{'='*70}{Colors.E}")
+ print(f"{Colors.H}{'FINAL REPORT':^70}{Colors.E}")
+ print(f"{Colors.H}{'='*70}{Colors.E}")
+
+ print(f"\n{Colors.BOLD}Overall Statistics:{Colors.E}")
+ print(f" Duration: {elapsed/60:.1f} minutes")
+ print(f" Iterations: {self.iteration}")
+ print(f" Total Trades: {self.total_trades}")
+
+ color = Colors.G if self.total_profit > 0 else Colors.R
+ print(f" Total Profit: {color}${self.total_profit:+.2f}{Colors.E}")
+ print(f" Best Iteration: ${self.best_profit:+.2f}")
+
+ # Profit trend
+ if len(self.profit_history) > 1:
+ first_half = sum(self.profit_history[:len(self.profit_history)//2])
+ second_half = sum(self.profit_history[len(self.profit_history)//2:])
+ trend = "📈 IMPROVING" if second_half > first_half else "📉 Declining"
+ print(f" Trend: {trend}")
+
+ print(f"\n{Colors.BOLD}Final Optimized Weights:{Colors.E}")
+ for name, weight in zip(WEIGHT_NAMES, self.weights):
+ # Visual bar
+ bar_len = int(weight * 10)
+ bar = "█" * min(bar_len, 25) + "░" * max(0, 25 - bar_len)
+
+ if weight > 1.5:
+ status = f"{Colors.G}STRONG{Colors.E}"
+ elif weight < 0.6:
+ status = f"{Colors.R}WEAK{Colors.E}"
+ else:
+ status = ""
+
+ print(f" {name:<25} {weight:.4f} {bar} {status}")
+
+ # Save
+ self._save_weights()
+
+ # Also save as JSON for easy use
+ report_path = os.path.join(self.storage_path, "backtest_report.json")
+ with open(report_path, 'w') as f:
+ json.dump({
+ 'timestamp': datetime.now().isoformat(),
+ 'iterations': self.iteration,
+ 'total_trades': self.total_trades,
+ 'total_profit': self.total_profit,
+ 'best_profit': self.best_profit,
+ 'profit_history': self.profit_history,
+ 'final_weights': dict(zip(WEIGHT_NAMES, self.weights)),
+ 'best_weights': dict(zip(WEIGHT_NAMES, self.best_weights)),
+ }, f, indent=2)
+
+ print(f"\n{Colors.G}✓ Report saved to: {report_path}{Colors.E}")
+ print(f"{Colors.G}✓ Weights saved to: {self.storage_path}/final_weights.json{Colors.E}")
+
+
+def main():
+ parser = argparse.ArgumentParser(description="Aggressive self-learning backtest")
+ parser.add_argument('--iterations', type=int, default=20,
+ help='Number of learning iterations (default: 20)')
+ parser.add_argument('--balance', type=float, default=10000.0)
+ parser.add_argument('--learning-rate', type=float, default=0.15,
+ help='How aggressively to change weights (default: 0.15)')
+ parser.add_argument('--storage', type=str, default='./data/btc_eth_learning')
+ parser.add_argument('--report-only', action='store_true')
+
+ args = parser.parse_args()
+
+ if args.report_only:
+ weights_file = os.path.join(args.storage, "final_weights.json")
+ if os.path.exists(weights_file):
+ with open(weights_file) as f:
+ data = json.load(f)
+ print(json.dumps(data, indent=2))
+ else:
+ print("No saved weights found")
+ return
+
+ learner = AggressiveLearner(
+ storage_path=args.storage,
+ initial_balance=args.balance,
+ learning_rate=args.learning_rate,
+ )
+
+ try:
+ result = learner.run(iterations=args.iterations)
+
+ print(f"\n{Colors.H}{'='*70}{Colors.E}")
+ print(f"{Colors.H}{'BACKTEST COMPLETE':^70}{Colors.E}")
+ print(f"{Colors.H}{'='*70}{Colors.E}")
+
+ print(f"\n{Colors.G}✓ Completed {result['total_iterations']} iterations{Colors.E}")
+ color = Colors.G if result['total_profit'] > 0 else Colors.R
+ print(f"{Colors.G}✓ Total profit: {color}${result['total_profit']:+.2f}{Colors.E}")
+ print(f"{Colors.B}ℹ Learning data saved to: {args.storage}{Colors.E}")
+
+ except Exception as e:
+ print(f"\n{Colors.R}Error: {e}{Colors.E}")
+ import traceback
+ traceback.print_exc()
+ sys.exit(1)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/back_tester/start_intensive_training.sh b/back_tester/start_intensive_training.sh
new file mode 100755
index 0000000..3c5e2f3
--- /dev/null
+++ b/back_tester/start_intensive_training.sh
@@ -0,0 +1,278 @@
+#!/bin/bash
+#
+# Intensive Parameter Optimization Launcher
+#
+# Optimizes ALL trading parameters:
+# - 18 Signal Weights
+# - Stop Loss (ATR multiplier, min distance)
+# - Take Profit Levels (TP1, TP2, TP3 ratios)
+# - Trailing Stop (distance, activation)
+# - Risk Percentage
+#
+# Usage:
+# ./start_intensive_training.sh # Default: 2 hours
+# ./start_intensive_training.sh --hours 8 # Run for 8 hours
+# ./start_intensive_training.sh --generations 100 # Run 100 generations
+#
+
+set -e
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+PROJECT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
+CLICKHOUSE_DIR="${SCRIPT_DIR}/clickhouse"
+
+# Colors
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+BLUE='\033[0;34m'
+CYAN='\033[0;36m'
+MAGENTA='\033[0;35m'
+BOLD='\033[1m'
+NC='\033[0m'
+
+# Defaults
+GENERATIONS=100
+HOURS=4
+POPULATION=8
+TESTS=10
+BALANCE=10000
+STORAGE_PATH="${SCRIPT_DIR}/data/intensive_training"
+SKIP_CLICKHOUSE=false
+
+TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
+LOG_FILE="${SCRIPT_DIR}/logs/intensive_training_${TIMESTAMP}.log"
+
+print_banner() {
+ echo -e "${MAGENTA}"
+ echo "╔══════════════════════════════════════════════════════════════════╗"
+ echo "║ ║"
+ echo "║ 🧬 INTENSIVE PARAMETER OPTIMIZATION ║"
+ echo "║ ║"
+ echo "║ Optimizing: ║"
+ echo "║ • 18 Signal Weights ║"
+ echo "║ • Stop Loss Parameters (ATR, distance) ║"
+ echo "║ • Take Profit Ratios (TP1, TP2, TP3) ║"
+ echo "║ • Trailing Stop (distance, activation) ║"
+ echo "║ • Risk Percentage ║"
+ echo "║ ║"
+ echo "╚══════════════════════════════════════════════════════════════════╝"
+ echo -e "${NC}"
+}
+
+show_help() {
+ echo "Usage: $0 [options]"
+ echo ""
+ echo "Intensive parameter optimization for trading strategy"
+ echo ""
+ echo "Options:"
+ echo " --hours N Maximum training time in hours (default: 4)"
+ echo " --generations N Maximum generations (default: 100)"
+ echo " --population N Population size per generation (default: 8)"
+ echo " --tests N Backtests per evaluation (default: 10)"
+ echo " --balance N Initial balance (default: 10000)"
+ echo " --storage PATH Storage path for results"
+ echo " --no-clickhouse Skip ClickHouse startup"
+ echo " --help Show this help"
+ echo ""
+ echo "Examples:"
+ echo " $0 --hours 8 # Run for 8 hours"
+ echo " $0 --generations 200 --hours 12 # Long training run"
+ echo ""
+}
+
+is_clickhouse_running() {
+ nc -z localhost 9000 &> /dev/null
+ return $?
+}
+
+start_clickhouse() {
+ echo -e "${YELLOW}Starting ClickHouse...${NC}"
+
+ if is_clickhouse_running; then
+ echo -e "${GREEN}✓ ClickHouse already running${NC}"
+ return 0
+ fi
+
+ cd "${CLICKHOUSE_DIR}" 2>/dev/null || mkdir -p "${CLICKHOUSE_DIR}"
+
+ if ! command -v clickhouse &> /dev/null; then
+ echo -e "${RED}✗ ClickHouse not found. Install: brew install clickhouse${NC}"
+ exit 1
+ fi
+
+ if [ -f "config.xml" ]; then
+ clickhouse server --config-file=config.xml &> "${SCRIPT_DIR}/logs/clickhouse_${TIMESTAMP}.log" &
+ else
+ clickhouse server &> "${SCRIPT_DIR}/logs/clickhouse_${TIMESTAMP}.log" &
+ fi
+
+ CLICKHOUSE_PID=$!
+
+ for i in {1..15}; do
+ if is_clickhouse_running; then
+ echo -e "${GREEN}✓ ClickHouse started${NC}"
+ return 0
+ fi
+ sleep 1
+ done
+
+ echo -e "${RED}✗ ClickHouse failed to start${NC}"
+ return 1
+}
+
+cleanup() {
+ if [ "$SKIP_CLICKHOUSE" = false ] && [ -n "$CLICKHOUSE_PID" ]; then
+ echo -e "${YELLOW}Stopping ClickHouse...${NC}"
+ kill $CLICKHOUSE_PID 2>/dev/null || true
+ fi
+}
+
+trap cleanup EXIT
+
+# Parse arguments
+while [[ $# -gt 0 ]]; do
+ case $1 in
+ --hours)
+ HOURS="$2"
+ shift 2
+ ;;
+ --generations)
+ GENERATIONS="$2"
+ shift 2
+ ;;
+ --population)
+ POPULATION="$2"
+ shift 2
+ ;;
+ --tests)
+ TESTS="$2"
+ shift 2
+ ;;
+ --balance)
+ BALANCE="$2"
+ shift 2
+ ;;
+ --storage)
+ STORAGE_PATH="$2"
+ shift 2
+ ;;
+ --no-clickhouse)
+ SKIP_CLICKHOUSE=true
+ shift
+ ;;
+ --help)
+ show_help
+ exit 0
+ ;;
+ *)
+ echo -e "${RED}Unknown option: $1${NC}"
+ show_help
+ exit 1
+ ;;
+ esac
+done
+
+main() {
+ print_banner
+
+ mkdir -p "${SCRIPT_DIR}/logs"
+ mkdir -p "${STORAGE_PATH}"
+
+ echo -e "${CYAN}Configuration:${NC}"
+ echo " Max Generations: ${GENERATIONS}"
+ echo " Max Time: ${HOURS} hours"
+ echo " Population Size: ${POPULATION}"
+ echo " Tests per Eval: ${TESTS}"
+ echo " Initial Balance: \$${BALANCE}"
+ echo " Storage: ${STORAGE_PATH}"
+ echo " Log: ${LOG_FILE}"
+ echo ""
+
+ # Estimate time
+ ESTIMATED_BACKTESTS=$((GENERATIONS * POPULATION * TESTS))
+ echo -e "${YELLOW}Estimated backtests: ~${ESTIMATED_BACKTESTS}${NC}"
+ echo -e "${YELLOW}This may take several hours. Progress will be saved automatically.${NC}"
+ echo ""
+
+ if [ "$SKIP_CLICKHOUSE" = false ]; then
+ start_clickhouse || exit 1
+ sleep 2
+ fi
+
+ export PYTHONPATH="${PROJECT_DIR}:${PYTHONPATH}"
+
+ # Find Python
+ PYTHON_CMD=""
+ for cmd in python3.11 python3 python; do
+ if command -v $cmd &> /dev/null; then
+ PYTHON_CMD="$cmd"
+ break
+ fi
+ done
+
+ if [ -z "$PYTHON_CMD" ]; then
+ echo -e "${RED}✗ Python not found${NC}"
+ exit 1
+ fi
+
+ echo -e "${GREEN}Using: $(${PYTHON_CMD} --version)${NC}"
+ echo ""
+
+ CMD="${PYTHON_CMD} ${SCRIPT_DIR}/intensive_training.py"
+ CMD="${CMD} --generations ${GENERATIONS}"
+ CMD="${CMD} --hours ${HOURS}"
+ CMD="${CMD} --population ${POPULATION}"
+ CMD="${CMD} --tests ${TESTS}"
+ CMD="${CMD} --balance ${BALANCE}"
+ CMD="${CMD} --storage ${STORAGE_PATH}"
+
+ echo -e "${CYAN}Command:${NC} ${CMD}"
+ echo ""
+
+ echo -e "${YELLOW}Starting optimization... (Ctrl+C to stop safely)${NC}"
+ echo ""
+
+ ${CMD} 2>&1 | tee "${LOG_FILE}"
+ EXIT_CODE=${PIPESTATUS[0]}
+
+ echo ""
+ if [ $EXIT_CODE -eq 0 ]; then
+ echo -e "${GREEN}╔══════════════════════════════════════════════════════════════════╗${NC}"
+ echo -e "${GREEN}║ ✓ OPTIMIZATION COMPLETED ║${NC}"
+ echo -e "${GREEN}╚══════════════════════════════════════════════════════════════════╝${NC}"
+ echo ""
+ echo -e "${CYAN}Results:${NC}"
+ echo " Parameters: ${STORAGE_PATH}/best_params.json"
+ echo " Log: ${LOG_FILE}"
+
+ # Show best params if file exists
+ if [ -f "${STORAGE_PATH}/best_params.json" ]; then
+ echo ""
+ echo -e "${CYAN}Best Parameters Summary:${NC}"
+ ${PYTHON_CMD} -c "
+import json
+with open('${STORAGE_PATH}/best_params.json') as f:
+ p = json.load(f)
+print(f\" Fitness: {p.get('fitness', 'N/A'):.1f}\")
+print(f\" TP Ratios: {p.get('tp1_ratio', 1.5):.1f} / {p.get('tp2_ratio', 2.5):.1f} / {p.get('tp3_ratio', 4.0):.1f}\")
+print(f\" ATR Mult: {p.get('atr_multiplier', 2.0):.2f}\")
+print(f\" Trailing: {p.get('trailing_stop_distance', 0.5):.2f}% at TP{p.get('trailing_activation_tp', 1)}\")
+print(f\" Risk: {p.get('risk_percentage', 1.0):.2f}%\")
+" 2>/dev/null || true
+ fi
+ else
+ echo -e "${RED}╔══════════════════════════════════════════════════════════════════╗${NC}"
+ echo -e "${RED}║ ✗ OPTIMIZATION FAILED (code: ${EXIT_CODE}) ║${NC}"
+ echo -e "${RED}╚══════════════════════════════════════════════════════════════════╝${NC}"
+ echo "Check log: ${LOG_FILE}"
+ fi
+
+ return $EXIT_CODE
+}
+
+main
+
+
+
+
diff --git a/back_tester/start_learning_backtest.sh b/back_tester/start_learning_backtest.sh
new file mode 100755
index 0000000..badd417
--- /dev/null
+++ b/back_tester/start_learning_backtest.sh
@@ -0,0 +1,283 @@
+#!/bin/bash
+#
+# Self-Learning Backtesting Launcher for BTC and ETH
+#
+# This script launches comprehensive backtesting with self-learning
+# on BTCUSDT and ETHUSDT across all time intervals.
+#
+# Usage:
+# ./start_learning_backtest.sh [options]
+#
+# Options:
+# --iterations N Number of learning iterations (default: 3)
+# --candles N Override candle count for all intervals
+# --balance N Initial balance (default: 10000)
+# --risk N Risk percentage per trade (default: 1.0)
+# --no-clickhouse Skip ClickHouse startup (if already running)
+# --help Show this help message
+#
+
+set -e
+
+# Script directory
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+PROJECT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
+CLICKHOUSE_DIR="${SCRIPT_DIR}/clickhouse"
+
+# Colors
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+BLUE='\033[0;34m'
+CYAN='\033[0;36m'
+MAGENTA='\033[0;35m'
+BOLD='\033[1m'
+NC='\033[0m'
+
+# Default parameters - AGGRESSIVE LEARNING (20 iterations, not 3!)
+ITERATIONS=20
+LEARNING_RATE=0.15
+BALANCE=10000
+SKIP_CLICKHOUSE=false
+STORAGE_PATH="${SCRIPT_DIR}/data/btc_eth_learning"
+
+# Timestamp for logs
+TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
+LOG_FILE="${SCRIPT_DIR}/logs/learning_backtest_${TIMESTAMP}.log"
+
+# Banner
+print_banner() {
+ echo -e "${MAGENTA}"
+ echo "╔══════════════════════════════════════════════════════════════════╗"
+ echo "║ ║"
+ echo "║ 🤖 SELF-LEARNING BACKTESTING SYSTEM ║"
+ echo "║ ║"
+ echo "║ Symbols: BTCUSDT, ETHUSDT ║"
+ echo "║ Intervals: 1m, 5m, 15m, 30m, 1h, 4h, 1d ║"
+ echo "║ ║"
+ echo "╚══════════════════════════════════════════════════════════════════╝"
+ echo -e "${NC}"
+}
+
+# Help message
+show_help() {
+ echo "Usage: $0 [options]"
+ echo ""
+ echo "Self-Learning Backtesting for BTCUSDT and ETHUSDT"
+ echo ""
+ echo "Options:"
+ echo " --iterations N Number of learning iterations (default: 20)"
+ echo " --learning-rate N How fast weights change (default: 0.15)"
+ echo " --balance N Initial balance in USD (default: 10000)"
+ echo " --storage PATH Path to store learning data"
+ echo " --no-clickhouse Skip ClickHouse startup"
+ echo " --help Show this help message"
+ echo ""
+ echo "Examples:"
+ echo " $0 # Run 20 iterations (default)"
+ echo " $0 --iterations 50 # Run 50 learning iterations"
+ echo " $0 --learning-rate 0.25 # More aggressive learning"
+ echo ""
+}
+
+# Check if ClickHouse is running
+is_clickhouse_running() {
+ nc -z localhost 9000 &> /dev/null
+ return $?
+}
+
+# Start ClickHouse
+start_clickhouse() {
+ echo -e "${YELLOW}Starting ClickHouse server...${NC}"
+
+ if is_clickhouse_running; then
+ echo -e "${GREEN}✓ ClickHouse is already running${NC}"
+ return 0
+ fi
+
+ if [ ! -d "${CLICKHOUSE_DIR}" ]; then
+ echo -e "${RED}✗ ClickHouse directory not found: ${CLICKHOUSE_DIR}${NC}"
+ echo -e "${YELLOW}Creating minimal ClickHouse config...${NC}"
+ mkdir -p "${CLICKHOUSE_DIR}"
+ fi
+
+ cd "${CLICKHOUSE_DIR}"
+
+ if ! command -v clickhouse &> /dev/null; then
+ echo -e "${RED}✗ ClickHouse not found in PATH${NC}"
+ echo "Please install ClickHouse: brew install clickhouse"
+ exit 1
+ fi
+
+ # Start ClickHouse in background
+ if [ -f "config.xml" ]; then
+ clickhouse server --config-file=config.xml &> "${SCRIPT_DIR}/logs/clickhouse_${TIMESTAMP}.log" &
+ else
+ clickhouse server &> "${SCRIPT_DIR}/logs/clickhouse_${TIMESTAMP}.log" &
+ fi
+
+ CLICKHOUSE_PID=$!
+
+ echo -e "${YELLOW}Waiting for ClickHouse to start (PID: ${CLICKHOUSE_PID})...${NC}"
+
+ for i in {1..15}; do
+ if is_clickhouse_running; then
+ echo -e "${GREEN}✓ ClickHouse started successfully${NC}"
+ return 0
+ fi
+ sleep 1
+ echo -n "."
+ done
+
+ echo ""
+ echo -e "${RED}✗ Failed to start ClickHouse within timeout${NC}"
+ return 1
+}
+
+# Stop ClickHouse on exit
+cleanup() {
+ if [ "$SKIP_CLICKHOUSE" = false ] && [ -n "$CLICKHOUSE_PID" ]; then
+ echo -e "${YELLOW}Stopping ClickHouse...${NC}"
+ kill $CLICKHOUSE_PID 2>/dev/null || true
+ fi
+}
+
+trap cleanup EXIT
+
+# Parse arguments
+while [[ $# -gt 0 ]]; do
+ case $1 in
+ --iterations)
+ ITERATIONS="$2"
+ shift 2
+ ;;
+ --learning-rate)
+ LEARNING_RATE="$2"
+ shift 2
+ ;;
+ --balance)
+ BALANCE="$2"
+ shift 2
+ ;;
+ --storage)
+ STORAGE_PATH="$2"
+ shift 2
+ ;;
+ --no-clickhouse)
+ SKIP_CLICKHOUSE=true
+ shift
+ ;;
+ --help)
+ show_help
+ exit 0
+ ;;
+ *)
+ echo -e "${RED}Unknown option: $1${NC}"
+ show_help
+ exit 1
+ ;;
+ esac
+done
+
+# Main execution
+main() {
+ print_banner
+
+ # Create logs directory
+ mkdir -p "${SCRIPT_DIR}/logs"
+ mkdir -p "${STORAGE_PATH}"
+
+ echo -e "${CYAN}Configuration:${NC}"
+ echo " Iterations: ${ITERATIONS}"
+ echo " Learning Rate: ${LEARNING_RATE}"
+ echo " Balance: \$${BALANCE}"
+ echo " Storage: ${STORAGE_PATH}"
+ echo " Log File: ${LOG_FILE}"
+ echo ""
+
+ # Estimate time
+ ESTIMATED_TIME=$((ITERATIONS * 2))
+ echo -e "${YELLOW}Estimated time: ~${ESTIMATED_TIME} minutes${NC}"
+ echo ""
+
+ # Start ClickHouse if needed
+ if [ "$SKIP_CLICKHOUSE" = false ]; then
+ start_clickhouse
+ if [ $? -ne 0 ]; then
+ echo -e "${RED}Failed to start ClickHouse. Exiting.${NC}"
+ exit 1
+ fi
+ echo ""
+ sleep 2
+ else
+ echo -e "${YELLOW}Skipping ClickHouse startup (--no-clickhouse)${NC}"
+ if ! is_clickhouse_running; then
+ echo -e "${RED}Warning: ClickHouse doesn't appear to be running!${NC}"
+ fi
+ echo ""
+ fi
+
+ # Set Python path
+ export PYTHONPATH="${PROJECT_DIR}:${PYTHONPATH}"
+
+ # Check Python
+ PYTHON_CMD=""
+ if command -v python3.11 &> /dev/null; then
+ PYTHON_CMD="python3.11"
+ elif command -v python3 &> /dev/null; then
+ PYTHON_CMD="python3"
+ elif command -v python &> /dev/null; then
+ PYTHON_CMD="python"
+ else
+ echo -e "${RED}✗ Python not found${NC}"
+ exit 1
+ fi
+
+ echo -e "${GREEN}Using Python: $(${PYTHON_CMD} --version)${NC}"
+ echo ""
+
+ # Build command
+ CMD="${PYTHON_CMD} ${SCRIPT_DIR}/run_learning_backtest.py"
+ CMD="${CMD} --iterations ${ITERATIONS}"
+ CMD="${CMD} --learning-rate ${LEARNING_RATE}"
+ CMD="${CMD} --balance ${BALANCE}"
+ CMD="${CMD} --storage ${STORAGE_PATH}"
+
+ echo -e "${CYAN}Executing:${NC}"
+ echo " ${CMD}"
+ echo ""
+
+ # Run backtest
+ echo -e "${YELLOW}Starting backtests... (logging to ${LOG_FILE})${NC}"
+ echo ""
+
+ # Run and tee to both console and log file
+ ${CMD} 2>&1 | tee "${LOG_FILE}"
+
+ EXIT_CODE=${PIPESTATUS[0]}
+
+ echo ""
+ if [ $EXIT_CODE -eq 0 ]; then
+ echo -e "${GREEN}╔══════════════════════════════════════════════════════════════════╗${NC}"
+ echo -e "${GREEN}║ ✓ BACKTESTING COMPLETED SUCCESSFULLY ║${NC}"
+ echo -e "${GREEN}╚══════════════════════════════════════════════════════════════════╝${NC}"
+ echo ""
+ echo -e "${CYAN}Results saved to:${NC}"
+ echo " Learning Data: ${STORAGE_PATH}"
+ echo " Weights: ${STORAGE_PATH}/final_weights.json"
+ echo " Report: ${STORAGE_PATH}/backtest_report.json"
+ echo " Log: ${LOG_FILE}"
+ else
+ echo -e "${RED}╔══════════════════════════════════════════════════════════════════╗${NC}"
+ echo -e "${RED}║ ✗ BACKTESTING FAILED (exit code: ${EXIT_CODE}) ║${NC}"
+ echo -e "${RED}╚══════════════════════════════════════════════════════════════════╝${NC}"
+ echo ""
+ echo "Check log file for details: ${LOG_FILE}"
+ fi
+
+ return $EXIT_CODE
+}
+
+# Run main
+main
+
diff --git a/back_tester/strategy.py b/back_tester/strategy.py
index 3f56272..f829810 100644
--- a/back_tester/strategy.py
+++ b/back_tester/strategy.py
@@ -1,9 +1,10 @@
import os
import sys
-from typing import List, Tuple, Optional, Dict
+from typing import List, Tuple, Optional, Dict, Any
import random
from datetime import datetime, timedelta
import uuid
+import logging
project_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
sys.path.append(project_dir)
@@ -26,6 +27,20 @@
from utils import create_true_preferences
from .db_operations import ClickHouseDB
+# Import self-learning modules (optional - graceful fallback if not available)
+try:
+ from .adaptive_learning import (
+ SelfLearningBacktester,
+ extract_indicator_contributions,
+ SignalOutcome
+ )
+ from .enhanced_metrics import EnhancedMetricsCalculator, TradeMetrics, create_trade_metrics
+ LEARNING_AVAILABLE = True
+except ImportError:
+ LEARNING_AVAILABLE = False
+
+logger = logging.getLogger(__name__)
+
def backtest_strategy(
symbol: str,
@@ -40,13 +55,19 @@ def backtest_strategy(
trailing_stop_distance_percent: float = 0.5, # Distance to maintain from highest price
iteration_id: Optional[str] = None,
db: Optional[ClickHouseDB] = None,
+ # Self-learning parameters
+ enable_learning: bool = False,
+ learner: Optional[Any] = None, # SelfLearningBacktester instance
+ metrics_calculator: Optional[Any] = None, # EnhancedMetricsCalculator instance
+ track_indicator_contributions: bool = True,
) -> Tuple[float, list, Optional[str]]:
"""
- Backtest a strategy with risk management:
+ Backtest a strategy with risk management and optional self-learning:
- At each step, generate a signal with risk management parameters
- Use position sizing based on risk percentage
- Implement multiple take profit levels
- Use dynamic stop loss and trailing stop loss
+ - Optionally track signal performance for self-learning
Parameters:
symbol: The trading pair symbol (e.g. "BTCUSDT")
@@ -59,6 +80,10 @@ def backtest_strategy(
weights: List of weights for signal generation
use_trailing_stop: Whether to enable trailing stop loss functionality
trailing_stop_distance_percent: Distance in percentage to maintain from highest price reached
+ enable_learning: Whether to enable self-learning feedback loop
+ learner: SelfLearningBacktester instance for tracking signals
+ metrics_calculator: EnhancedMetricsCalculator for detailed metrics
+ track_indicator_contributions: Whether to track which indicators contributed to each signal
Returns:
final_balance: The simulated portfolio balance at the end
@@ -97,6 +122,27 @@ def backtest_strategy(
entry_signal = None
current_trade = None # Store current trade details
parent_trade_id = None # Track parent trade for TP/SL entries
+
+ # Self-learning tracking variables
+ current_signal_id = None
+ current_indicator_contributions = {}
+ current_market_context = {}
+ max_favorable_excursion = 0.0
+ max_adverse_excursion = 0.0
+
+ # Initialize learning components if enabled
+ if enable_learning and LEARNING_AVAILABLE and learner is None:
+ learner = SelfLearningBacktester(
+ storage_path="./data/learning",
+ auto_adjust_weights=True,
+ adjustment_frequency=50
+ )
+ # Use adaptive weights if available
+ if not weights:
+ weights = learner.get_current_weights()
+
+ if enable_learning and LEARNING_AVAILABLE and metrics_calculator is None:
+ metrics_calculator = EnhancedMetricsCalculator()
# Use all indicators enabled by default in backtesting
preferences = create_true_preferences()
@@ -313,6 +359,17 @@ def backtest_strategy(
}
db.insert_trade(trade_data)
+ # Record outcome for self-learning (TP3 - full target hit)
+ if enable_learning and LEARNING_AVAILABLE and learner and current_signal_id:
+ learner.record_signal_exit(
+ signal_id=current_signal_id,
+ outcome="tp3",
+ exit_price=current_price,
+ profit_loss=profit,
+ duration=i - entry_index
+ )
+ current_signal_id = None
+
position = 0
entry_price = None
entry_time = None
@@ -397,6 +454,17 @@ def backtest_strategy(
}
db.insert_trade(trade_data)
+ # Record outcome for self-learning (stop loss)
+ if enable_learning and LEARNING_AVAILABLE and learner and current_signal_id:
+ learner.record_signal_exit(
+ signal_id=current_signal_id,
+ outcome="trailing_stop" if current_trade.get("trailing_stop_active", False) else "stop_loss",
+ exit_price=current_price,
+ profit_loss=loss,
+ duration=i - entry_index
+ )
+ current_signal_id = None
+
position = 0
entry_price = None
entry_time = None
@@ -436,9 +504,11 @@ def backtest_strategy(
entry_signal = signal
parent_trade_id = str(uuid.uuid4()) # Generate parent trade ID
- # Store trade details
+ # Store trade details with trailing stop initialization
+ trailing_activation_price = float(trading_signal.take_profit_1) # Activate at TP1
current_trade = {
"stop_loss": float(trading_signal.stop_loss),
+ "initial_stop_loss": float(trading_signal.stop_loss), # Store initial stop for comparison
"take_profit_1": float(trading_signal.take_profit_1),
"take_profit_2": float(trading_signal.take_profit_2),
"take_profit_3": float(trading_signal.take_profit_3),
@@ -448,6 +518,11 @@ def backtest_strategy(
"tp1_hit": False,
"tp2_hit": False,
"tp3_hit": False,
+ # Trailing stop fields
+ "highest_price": float(current_price), # Track highest price reached
+ "trailing_stop_active": False, # Whether trailing stop is active
+ "trailing_activation_price": trailing_activation_price, # Price at which trailing stop activates
+ "trailing_stop_level": float(trading_signal.stop_loss), # Current trailing stop level
}
trade_log.append(
@@ -462,8 +537,63 @@ def backtest_strategy(
"take_profit_1": float(trading_signal.take_profit_1),
"take_profit_2": float(trading_signal.take_profit_2),
"take_profit_3": float(trading_signal.take_profit_3),
+ "symbol": symbol,
+ "interval": interval,
}
)
+
+ # Track signal for self-learning
+ if enable_learning and LEARNING_AVAILABLE and learner:
+ # Parse reasons to extract indicator contributions
+ reasons_list = reason.split("\n- ") if reason else []
+ reasons_list = [r.strip("- \n") for r in reasons_list if r.strip()]
+
+ # Extract bullish/bearish scores from reason string
+ bullish_score = 0.0
+ bearish_score = 0.0
+ if "Bullish Score:" in reason:
+ try:
+ bullish_part = reason.split("Bullish Score:")[1].split("|")[0]
+ bullish_score = float(bullish_part.strip())
+ except:
+ pass
+ if "Bearish Score:" in reason:
+ try:
+ bearish_part = reason.split("Bearish Score:")[1].split("\n")[0]
+ bearish_score = float(bearish_part.strip())
+ except:
+ pass
+
+ current_indicator_contributions = extract_indicator_contributions(
+ bullish_score, bearish_score, signal, reasons_list
+ )
+
+ current_market_context = {
+ "market_regime": trading_signal.market_conditions.get("market_regime", ""),
+ "volatility": trading_signal.market_conditions.get("volatility", 0),
+ "volume_ratio": trading_signal.market_conditions.get("volume_ratio", 1),
+ "rsi": trading_signal.market_conditions.get("rsi", 50),
+ "trend": "uptrend" if signal == "Bullish" else "downtrend"
+ }
+
+ current_signal_id = str(uuid.uuid4())
+ max_favorable_excursion = 0.0
+ max_adverse_excursion = 0.0
+
+ learner.record_signal_entry(
+ signal_id=current_signal_id,
+ symbol=symbol,
+ interval=interval,
+ signal_type=signal,
+ entry_price=entry_price,
+ stop_loss=float(trading_signal.stop_loss),
+ tp1=float(trading_signal.take_profit_1),
+ tp2=float(trading_signal.take_profit_2),
+ tp3=float(trading_signal.take_profit_3),
+ indicator_contributions=current_indicator_contributions,
+ reasons=reasons_list,
+ market_context=current_market_context
+ )
# Store entry trade in database
if db:
@@ -551,6 +681,20 @@ def backtest_strategy(
"parent_trade_id": parent_trade_id,
}
db.insert_trade(trade_data)
+
+ # Record outcome for self-learning (end of period)
+ if enable_learning and LEARNING_AVAILABLE and learner and current_signal_id:
+ learner.record_signal_exit(
+ signal_id=current_signal_id,
+ outcome="end",
+ exit_price=final_price,
+ profit_loss=profit,
+ duration=len(df) - 1 - entry_index if entry_index else 0
+ )
+
+ # Save learning state if enabled
+ if enable_learning and LEARNING_AVAILABLE and learner:
+ learner.save_state()
return balance, trade_log, sub_iteration_id
@@ -570,7 +714,7 @@ def backtest_strategy(
use_trailing_stop=True,
trailing_stop_distance_percent=0.5, # Keep trailing stop 0.5% below highest price
)
- print(f"Final Balance: {final_balance:.2f}")
+ print(f"Final Balance: {final_balance_ts:.2f}")
print("Trade Log:")
- for trade in trades:
+ for trade in trades_ts:
print(trade)
diff --git a/back_tester/trainer.py b/back_tester/trainer.py
index da66c47..51ed68a 100644
--- a/back_tester/trainer.py
+++ b/back_tester/trainer.py
@@ -45,22 +45,27 @@
db = ClickHouseDB()
# --- Initial Weight Configuration ---
-# Updated for new weights including risk management signals
+# Updated for all 18 weights matching signal_detection.py
weights = [
- 1.0, # W_BULLISH_OB
- 1.0, # W_BEARISH_OB
- 1.0, # W_BULLISH_BREAKER
- 1.0, # W_BEARISH_BREAKER
- 0.7, # W_ABOVE_SUPPORT
- 0.7, # W_BELOW_RESISTANCE
- 0.5, # W_FVG_ABOVE
- 0.5, # W_FVG_BELOW
- 0.8, # W_TREND
- 1.2, # W_SWEEP_HIGHS
- 1.2, # W_SWEEP_LOWS
- 1.5, # W_STRUCTURE_BREAK
- 0.6, # W_PIN_BAR
-] # Updated for new weights
+ 1.0, # W_BULLISH_OB (0)
+ 1.0, # W_BEARISH_OB (1)
+ 1.0, # W_BULLISH_BREAKER (2)
+ 1.0, # W_BEARISH_BREAKER (3)
+ 0.7, # W_ABOVE_SUPPORT (4)
+ 0.7, # W_BELOW_RESISTANCE (5)
+ 0.5, # W_FVG_ABOVE (6)
+ 0.5, # W_FVG_BELOW (7)
+ 0.8, # W_TREND (8)
+ 1.2, # W_SWEEP_HIGHS (9)
+ 1.2, # W_SWEEP_LOWS (10)
+ 1.5, # W_STRUCTURE_BREAK (11)
+ 0.6, # W_PIN_BAR (12)
+ 0.6, # W_ENGULFING (13)
+ 1.2, # W_LIQUIDITY_POOL_ABOVE (14)
+ 1.2, # W_LIQUIDITY_POOL_BELOW (15)
+ 1.5, # W_LIQUIDITY_POOL_ROUND (16)
+ 0.6, # W_RSI_EXTREME (17)
+]
learning_rate = 0.05
iterations = 1000
@@ -77,6 +82,15 @@
patience = 50
history_size = 10
+# Weight names for tracking and logging
+WEIGHT_NAMES = [
+ "W_BULLISH_OB", "W_BEARISH_OB", "W_BULLISH_BREAKER", "W_BEARISH_BREAKER",
+ "W_ABOVE_SUPPORT", "W_BELOW_RESISTANCE", "W_FVG_ABOVE", "W_FVG_BELOW",
+ "W_TREND", "W_SWEEP_HIGHS", "W_SWEEP_LOWS", "W_STRUCTURE_BREAK",
+ "W_PIN_BAR", "W_ENGULFING", "W_LIQUIDITY_POOL_ABOVE", "W_LIQUIDITY_POOL_BELOW",
+ "W_LIQUIDITY_POOL_ROUND", "W_RSI_EXTREME"
+]
+
class TrainingMetrics:
def __init__(self):
@@ -440,8 +454,35 @@ def optimize_weights(
if __name__ == "__main__":
+ # Parse command line arguments
+ parser = argparse.ArgumentParser(description="Train signal weights for CryptoBot")
+ parser.add_argument(
+ "--symbol", type=str, default="BTCUSDT", help="Primary trading pair for optimization"
+ )
+ parser.add_argument(
+ "--interval", type=str, default="1h", help="Primary candle interval"
+ )
+ parser.add_argument(
+ "--iterations", type=int, default=1000, help="Number of training iterations"
+ )
+ parser.add_argument(
+ "--risk", type=float, default=1.0, help="Risk percentage per trade"
+ )
+ parser.add_argument(
+ "--learning-rate", type=float, default=0.05, help="Learning rate for weight updates"
+ )
+ args = parser.parse_args()
+
+ # Update global parameters from args
+ iterations = args.iterations
+ risk_percentage = args.risk
+ learning_rate = args.learning_rate
+
# Train the model
try:
+ logger.info(f"Starting weight optimization for {args.symbol} {args.interval}")
+ logger.info(f"Iterations: {iterations}, Risk: {risk_percentage}%, Learning rate: {learning_rate}")
+
best_weights = optimize_weights(weights, iterations, learning_rate)
logger.info(f"Training completed successfully")
@@ -454,17 +495,22 @@ def optimize_weights(
weights_file = f"models/weights_{timestamp}.txt"
os.makedirs("models", exist_ok=True)
+
with open(weights_file, "w") as f:
f.write(
f"# Optimized weights for {args.symbol} {args.interval} at {timestamp}\n"
)
f.write(f"# Fitness: {fitness:.4f}\n")
- f.write(f"# Risk: {risk_percentage}\n\n")
+ f.write(f"# Risk: {risk_percentage}%\n")
+ f.write(f"# Iterations: {iterations}\n\n")
for i, w in enumerate(best_weights):
- f.write(f"W{i} = {w:.6f}\n")
+ name = WEIGHT_NAMES[i] if i < len(WEIGHT_NAMES) else f"W{i}"
+ f.write(f"{name} = {w:.6f}\n")
logger.info(f"Weights saved to {weights_file}")
except Exception as e:
logger.error(f"Training failed: {str(e)}")
+ import traceback
+ traceback.print_exc()
sys.exit(1)
diff --git a/requirements.txt b/requirements.txt
index 3685001..b2b8c19 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -17,6 +17,7 @@ python-dotenv==1.0.0
python-json-logger==2.0.7
python-multipart==0.0.6
python-telegram-bot==21.9
+PyYAML==6.0.1
requests==2.31.0
requests-mock==1.12.1
requests-oauthlib==1.3.1
diff --git a/src/core/bot.py b/src/core/bot.py
index aef21e7..359265c 100644
--- a/src/core/bot.py
+++ b/src/core/bot.py
@@ -55,6 +55,10 @@
describe_table_command,
)
from src.telegram.commands.status_commands import rate_limit_command
+from src.telegram.commands.language_commands import (
+ language_command,
+ handle_language_callback,
+)
from src.telegram.tutorial import (
start_command,
tutorial_command,
@@ -62,6 +66,7 @@
CHOOSING_TUTORIAL_ACTION,
)
from src.core.utils import logger
+from src.i18n import load_translations
from src.core.error_handler import global_error_handler
from src.core.logging_utils import log_message_decorator
from src.core.rate_limiter import rate_limit
@@ -201,6 +206,10 @@ def setup_handlers(app):
app.add_handler(CommandHandler("start", rate_limit()(start_command)))
app.add_handler(CommandHandler("tutorial", rate_limit()(tutorial_command)))
+ # Language selection command
+ app.add_handler(CommandHandler("language", rate_limit()(language_command)))
+ app.add_handler(CallbackQueryHandler(handle_language_callback, pattern=r"^lang_set:"))
+
tutorial_conv_handler = ConversationHandler(
entry_points=[
CallbackQueryHandler(handle_tutorial_callback, pattern=r"^tutorial_")
@@ -225,6 +234,9 @@ def main():
# Initialize database
init_db()
+ # Load translations for i18n support
+ load_translations()
+
# Get token from environment
TOKEN = os.getenv("API_TELEGRAM_KEY")
diff --git a/src/core/error_handler.py b/src/core/error_handler.py
index a7f49e3..36ab80a 100644
--- a/src/core/error_handler.py
+++ b/src/core/error_handler.py
@@ -10,20 +10,22 @@
from typing import Any, Callable, Dict, Optional, TypeVar, Union, cast
from telegram import Update
+from src.i18n import t, get_user_language, DEFAULT_LANGUAGE
+
logger = logging.getLogger(__name__)
-# Error message templates
-ERROR_MESSAGES = {
- "network": "Network error occurred. Please try again later.",
- "data_fetch": "Failed to fetch data. Please check the symbol and try again.",
- "data_processing": "Error processing data. Please try with different parameters.",
- "chart_generation": "Error generating chart. Please try again with different settings.",
- "invalid_input": "Invalid input provided. Please check your command syntax.",
- "database": "Database operation failed. Please try again.",
- "timeout": "Operation timed out. Please try again.",
- "api_limit": "API rate limit reached. Please try again later.",
- "permission": "You don't have permission to perform this action.",
- "unknown": "An unexpected error occurred. Please try again later.",
+# Error type to translation key mapping
+ERROR_TYPE_KEYS = {
+ "network": "errors.network",
+ "data_fetch": "errors.data_fetch",
+ "data_processing": "errors.data_processing",
+ "chart_generation": "errors.chart_generation",
+ "invalid_input": "errors.invalid_input",
+ "database": "errors.database",
+ "timeout": "errors.timeout",
+ "api_limit": "errors.api_limit",
+ "permission": "errors.permission",
+ "unknown": "errors.unknown",
}
@@ -39,24 +41,34 @@ async def handle_error(
Args:
update: The Telegram update object to respond to
- error_type: Type of error from ERROR_MESSAGES dictionary
+ error_type: Type of error from ERROR_TYPE_KEYS dictionary
custom_message: Optional custom message to override the template
exception: The exception object if available
notify_user: Whether to send a message to the user
"""
+ # Get user language for translations
+ lang = DEFAULT_LANGUAGE
+ if update and update.effective_user:
+ try:
+ lang = get_user_language(update.effective_user.id)
+ except Exception:
+ pass # Use default language if we can't get user preferences
+
+ # Get the translated error message
+ error_key = ERROR_TYPE_KEYS.get(error_type, ERROR_TYPE_KEYS["unknown"])
+ default_message = t(error_key, lang)
+
# Log the error with details
if exception:
logger.error(f"Error: {error_type} - {str(exception)}")
logger.debug(traceback.format_exc())
else:
logger.error(
- f"Error: {error_type} - {custom_message or ERROR_MESSAGES.get(error_type, ERROR_MESSAGES['unknown'])}"
+ f"Error: {error_type} - {custom_message or default_message}"
)
if notify_user and update and update.effective_chat:
- message = custom_message or ERROR_MESSAGES.get(
- error_type, ERROR_MESSAGES["unknown"]
- )
+ message = custom_message or default_message
try:
await update.message.reply_text(f"❌ {message}")
except Exception as e:
@@ -90,12 +102,16 @@ async def global_error_handler(update: object, context):
# Ensure we have a valid update object
if isinstance(update, Update) and update.effective_chat:
+ # Get user language for translations
+ lang = DEFAULT_LANGUAGE
+ if update.effective_user:
+ try:
+ lang = get_user_language(update.effective_user.id)
+ except Exception:
+ pass # Use default language if we can't get user preferences
+
try:
- await update.effective_chat.send_message(
- "❌ An unexpected error occurred while processing your request. "
- "Please try again later.\n\n"
- "If you think this is a bug, please report it here: https://github.com/yariks5s/analysis_telegram_bot/issues"
- )
+ await update.effective_chat.send_message(t("errors.global", lang))
except Exception as e:
logger.error(f"Failed to send global error message: {e}")
from src.core.logging_utils import log_error_with_stacktrace
diff --git a/src/database/models.py b/src/database/models.py
index a63134d..bd62bef 100644
--- a/src/database/models.py
+++ b/src/database/models.py
@@ -27,6 +27,7 @@ class UserPreference:
atr_period: int = 14
fvg_min_size: float = 0.0005
tutorial_stage: int = 0 # 0 = not started, 1+ = tutorial progress
+ language: str = "en" # User's preferred language (en, ru, uk, de, fr, nl)
@dataclass
@@ -89,7 +90,8 @@ def create_tables():
dark_mode BOOLEAN DEFAULT 0,
atr_period INTEGER DEFAULT 14,
fvg_min_size REAL DEFAULT 0.0005,
- tutorial_stage INTEGER DEFAULT 0
+ tutorial_stage INTEGER DEFAULT 0,
+ language VARCHAR DEFAULT 'en'
)
"""
)
diff --git a/src/database/operations.py b/src/database/operations.py
index c641a77..6d5f3f5 100644
--- a/src/database/operations.py
+++ b/src/database/operations.py
@@ -20,7 +20,7 @@ def init_db() -> None:
create_tables()
-def get_user_preferences(user_id: int) -> Dict[str, Union[bool, int, float]]:
+def get_user_preferences(user_id: int) -> Dict[str, Union[bool, int, float, str]]:
"""
Retrieve the user's indicator preferences from the database.
@@ -47,6 +47,7 @@ def get_user_preferences(user_id: int) -> Dict[str, Union[bool, int, float]]:
"liquidity_pools": bool(row[7]) if len(row) > 7 else True,
"dark_mode": bool(row[8]) if len(row) > 8 else False,
"tutorial_stage": int(row[11]) if len(row) > 11 else 0,
+ "language": str(row[12]) if len(row) > 12 and row[12] else "en",
}
# Handle indicator parameters if they exist in the database
@@ -77,6 +78,7 @@ def get_user_preferences(user_id: int) -> Dict[str, Union[bool, int, float]]:
"liquidity_pools": False,
"dark_mode": False,
"tutorial_stage": 0,
+ "language": "en",
}
prefs["atr_period"] = INDICATOR_PARAMS["atr_period"]["default"]
@@ -124,7 +126,7 @@ def update_user_preferences(user_id: int, preferences: Dict[str, Any]) -> None:
UPDATE user_preferences
SET order_blocks = ?, fvgs = ?, liquidity_levels = ?, breaker_blocks = ?,
show_legend = ?, show_volume = ?, liquidity_pools = ?, dark_mode = ?,
- atr_period = ?, fvg_min_size = ?, tutorial_stage = ?
+ atr_period = ?, fvg_min_size = ?, tutorial_stage = ?, language = ?
WHERE user_id = ?
""",
(
@@ -139,6 +141,7 @@ def update_user_preferences(user_id: int, preferences: Dict[str, Any]) -> None:
preferences["atr_period"],
preferences["fvg_min_size"],
preferences.get("tutorial_stage", 0),
+ preferences.get("language", "en"),
user_id,
),
)
@@ -148,8 +151,8 @@ def update_user_preferences(user_id: int, preferences: Dict[str, Any]) -> None:
INSERT INTO user_preferences (
user_id, order_blocks, fvgs, liquidity_levels, breaker_blocks,
show_legend, show_volume, liquidity_pools, dark_mode,
- atr_period, fvg_min_size, tutorial_stage
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+ atr_period, fvg_min_size, tutorial_stage, language
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
user_id,
@@ -164,6 +167,7 @@ def update_user_preferences(user_id: int, preferences: Dict[str, Any]) -> None:
preferences["atr_period"],
preferences["fvg_min_size"],
preferences.get("tutorial_stage", 0),
+ preferences.get("language", "en"),
),
)
@@ -174,6 +178,46 @@ def update_user_preferences(user_id: int, preferences: Dict[str, Any]) -> None:
conn.close()
+def update_user_language(user_id: int, language: str) -> None:
+ """
+ Update the user's language preference.
+
+ Args:
+ user_id: Telegram user ID
+ language: Language code (en, ru, uk, de, fr, nl)
+ """
+ try:
+ conn = sqlite3.connect(DATABASE_PATH)
+ cursor = conn.cursor()
+
+ cursor.execute("SELECT 1 FROM user_preferences WHERE user_id = ?", (user_id,))
+ exists = cursor.fetchone()
+
+ if exists:
+ cursor.execute(
+ "UPDATE user_preferences SET language = ? WHERE user_id = ?",
+ (language, user_id),
+ )
+ else:
+ # Create default preferences with the specified language
+ cursor.execute(
+ """
+ INSERT INTO user_preferences (
+ user_id, order_blocks, fvgs, liquidity_levels, breaker_blocks,
+ show_legend, show_volume, liquidity_pools, dark_mode,
+ atr_period, fvg_min_size, tutorial_stage, language
+ ) VALUES (?, 0, 0, 0, 0, 1, 1, 0, 0, 14, 0.0005, 0, ?)
+ """,
+ (user_id, language),
+ )
+
+ conn.commit()
+ except sqlite3.Error as e:
+ logger.error(f"Database error while updating language: {e}")
+ finally:
+ conn.close()
+
+
def get_all_user_signal_requests(user_id: int) -> List[Dict[str, any]]:
"""
Retrieve all signal request preferences for a user from the database.
diff --git a/src/i18n/__init__.py b/src/i18n/__init__.py
new file mode 100644
index 0000000..5845f80
--- /dev/null
+++ b/src/i18n/__init__.py
@@ -0,0 +1,232 @@
+"""
+Internationalization (i18n) module for CryptoBot.
+
+This module provides multi-language support with:
+- Translation loading from YAML files
+- Language preference per user
+- Placeholder interpolation
+- Fallback to English for missing translations
+"""
+
+import os
+import logging
+from typing import Dict, Any, Optional
+from functools import lru_cache
+
+import yaml
+
+logger = logging.getLogger(__name__)
+
+# Supported languages with their native names
+SUPPORTED_LANGUAGES = {
+ "en": "English",
+ "ru": "Русский",
+ "uk": "Українська",
+ "de": "Deutsch",
+ "fr": "Français",
+ "nl": "Nederlands",
+}
+
+# Default language
+DEFAULT_LANGUAGE = "en"
+
+# Cache for loaded translations
+_translations: Dict[str, Dict[str, Any]] = {}
+
+# Path to locales directory
+LOCALES_DIR = os.path.join(os.path.dirname(__file__), "locales")
+
+
+def load_translations() -> None:
+ """
+ Load all translation files from the locales directory.
+ Should be called once at application startup.
+ """
+ global _translations
+ _translations = {}
+
+ for lang_code in SUPPORTED_LANGUAGES.keys():
+ file_path = os.path.join(LOCALES_DIR, f"{lang_code}.yaml")
+ try:
+ if os.path.exists(file_path):
+ with open(file_path, "r", encoding="utf-8") as f:
+ _translations[lang_code] = yaml.safe_load(f) or {}
+ logger.info(f"Loaded translations for {lang_code}")
+ else:
+ logger.warning(f"Translation file not found: {file_path}")
+ _translations[lang_code] = {}
+ except Exception as e:
+ logger.error(f"Error loading translations for {lang_code}: {e}")
+ _translations[lang_code] = {}
+
+ # Ensure English is always available as fallback
+ if "en" not in _translations:
+ _translations["en"] = {}
+
+
+def _get_nested_value(data: Dict[str, Any], key: str) -> Optional[str]:
+ """
+ Get a nested value from a dictionary using dot notation.
+
+ Args:
+ data: The dictionary to search
+ key: The key in dot notation (e.g., "tutorial.welcome.title")
+
+ Returns:
+ The value if found, None otherwise
+ """
+ keys = key.split(".")
+ value = data
+
+ for k in keys:
+ if isinstance(value, dict) and k in value:
+ value = value[k]
+ else:
+ return None
+
+ return value if isinstance(value, str) else None
+
+
+def t(key: str, lang: str = DEFAULT_LANGUAGE, **kwargs) -> str:
+ """
+ Get a translated string for the given key and language.
+
+ Args:
+ key: Translation key in dot notation (e.g., "tutorial.welcome.title")
+ lang: Language code (e.g., "en", "ru", "uk", "de", "fr", "nl")
+ **kwargs: Placeholder values to interpolate
+
+ Returns:
+ Translated string with placeholders replaced, or the key if not found
+
+ Example:
+ t("common.welcome", "ru", name="Иван")
+ # Returns "Привет, Иван!" if translation is "Привет, {name}!"
+ """
+ # Ensure translations are loaded
+ if not _translations:
+ load_translations()
+
+ # Validate language code
+ if lang not in SUPPORTED_LANGUAGES:
+ lang = DEFAULT_LANGUAGE
+
+ # Try to get translation in requested language
+ translation = _get_nested_value(_translations.get(lang, {}), key)
+
+ # Fallback to English if not found
+ if translation is None and lang != DEFAULT_LANGUAGE:
+ translation = _get_nested_value(_translations.get(DEFAULT_LANGUAGE, {}), key)
+
+ # If still not found, return the key
+ if translation is None:
+ logger.warning(f"Missing translation for key '{key}' in language '{lang}'")
+ return key
+
+ # Interpolate placeholders
+ try:
+ return translation.format(**kwargs)
+ except KeyError as e:
+ logger.error(f"Missing placeholder {e} for key '{key}'")
+ return translation
+
+
+def get_user_language(user_id: int) -> str:
+ """
+ Get the language preference for a user from the database.
+
+ Args:
+ user_id: Telegram user ID
+
+ Returns:
+ Language code (defaults to English if not set)
+ """
+ # Lazy import to avoid circular dependency
+ from src.database.operations import get_user_preferences
+
+ try:
+ prefs = get_user_preferences(user_id)
+ lang = prefs.get("language", DEFAULT_LANGUAGE)
+ return lang if lang in SUPPORTED_LANGUAGES else DEFAULT_LANGUAGE
+ except Exception as e:
+ logger.error(f"Error getting user language: {e}")
+ return DEFAULT_LANGUAGE
+
+
+def set_user_language(user_id: int, lang: str) -> bool:
+ """
+ Set the language preference for a user.
+
+ Args:
+ user_id: Telegram user ID
+ lang: Language code
+
+ Returns:
+ True if successful, False otherwise
+ """
+ if lang not in SUPPORTED_LANGUAGES:
+ return False
+
+ # Lazy import to avoid circular dependency
+ from src.database.operations import update_user_language
+
+ try:
+ update_user_language(user_id, lang)
+ return True
+ except Exception as e:
+ logger.error(f"Error setting user language: {e}")
+ return False
+
+
+def get_language_keyboard_data() -> list:
+ """
+ Get data for building a language selection keyboard.
+
+ Returns:
+ List of tuples (lang_code, native_name, flag_emoji)
+ """
+ flags = {
+ "en": "🇬🇧",
+ "ru": "🇷🇺",
+ "uk": "🇺🇦",
+ "de": "🇩🇪",
+ "fr": "🇫🇷",
+ "nl": "🇳🇱",
+ }
+
+ return [
+ (code, name, flags.get(code, "🌐"))
+ for code, name in SUPPORTED_LANGUAGES.items()
+ ]
+
+
+# Convenience function for handler usage
+def tr(update, key: str, **kwargs) -> str:
+ """
+ Convenience function to get translation based on update's user.
+
+ Args:
+ update: Telegram Update object
+ key: Translation key
+ **kwargs: Placeholder values
+
+ Returns:
+ Translated string
+ """
+ user_id = update.effective_user.id if update.effective_user else 0
+ lang = get_user_language(user_id)
+ return t(key, lang, **kwargs)
+
+
+# Export public API
+__all__ = [
+ "SUPPORTED_LANGUAGES",
+ "DEFAULT_LANGUAGE",
+ "load_translations",
+ "t",
+ "tr",
+ "get_user_language",
+ "set_user_language",
+ "get_language_keyboard_data",
+]
+
diff --git a/src/i18n/locales/de.yaml b/src/i18n/locales/de.yaml
new file mode 100644
index 0000000..e926252
--- /dev/null
+++ b/src/i18n/locales/de.yaml
@@ -0,0 +1,364 @@
+# German translations for CryptoBot
+# Deutsche Übersetzungen für CryptoBot
+
+common:
+ done: "Fertig"
+ cancel: "Abbrechen"
+ save: "Speichern"
+ back: "Zurück"
+ continue: "Weiter"
+ skip: "Überspringen"
+ finish: "Beenden"
+ yes: "Ja"
+ no: "Nein"
+ error: "Fehler"
+ success: "Erfolgreich"
+ loading: "Laden..."
+ no_data: "Keine Daten verfügbar"
+
+language:
+ select_prompt: "🌐 Wählen Sie Ihre bevorzugte Sprache:"
+ changed: "✅ Sprache auf Deutsch geändert"
+ current: "Aktuelle Sprache: Deutsch 🇩🇪"
+
+tutorial:
+ welcome:
+ title: "🎉 Willkommen bei CryptoBot!"
+ content: |
+ Ich bin Ihr persönlicher Assistent für Krypto-Marktanalysen. Ich kann Ihnen helfen, Kryptowährungs-Charts mit fortgeschrittenen technischen Indikatoren zu analysieren.
+
+ Dieses kurze Tutorial zeigt Ihnen, wie Sie meine Hauptfunktionen nutzen können. Sie können das Tutorial überspringen oder jederzeit mit dem Befehl /tutorial darauf zurückkommen.
+
+ chart:
+ title: "📊 Chart-Analyse"
+ content: |
+ Der grundlegende Befehl für eine Chart-Analyse ist:
+
+ /chart SYMBOL LÄNGE INTERVALL TOLERANZ
+
+ Zum Beispiel: /chart BTCUSDT 48 1h 0.05
+
+ Dies gibt Ihnen ein 48-Kerzen, 1-Stunden-Chart für BTC/USDT mit einer Toleranz von 0.05 für die Erkennung von Liquiditätsniveaus.
+
+ Probieren Sie es aus oder fahren Sie fort, um mehr zu erfahren!
+
+ preferences:
+ title: "⚙️ Indikatoren anpassen"
+ content: |
+ Sie können mit dem Befehl /preferences anpassen, welche Indikatoren auf Ihren Charts erscheinen.
+
+ Dies öffnet ein interaktives Menü, in dem Sie aktivieren oder deaktivieren können:
+ • Order Blocks
+ • Fair Value Gaps (FVGs)
+ • Liquiditätsniveaus
+ • Breaker Blocks
+ • Und Anzeigeoptionen ändern
+
+ Ihre Einstellungen werden für zukünftige Anfragen gespeichert.
+
+ signals:
+ title: "🔔 Automatische Signale"
+ content: |
+ Möchten Sie automatische Benachrichtigungen erhalten? Richten Sie Signal-Alarme ein mit:
+
+ /create_signal SYMBOL MINUTEN [CHART]
+
+ Zum Beispiel: /create_signal BTCUSDT 60 true
+
+ Dies sendet Ihnen alle 60 Minuten eine Benachrichtigung mit Chart für BTC/USDT.
+
+ Sie können Ihre Signale mit dem Befehl /manage_signals verwalten.
+
+ parameters:
+ title: "🔧 Erweiterte Parameter"
+ content: |
+ Feinabstimmung der Indikator-Parameter mit dem Befehl /parameters.
+
+ Sie können anpassen:
+ • ATR-Periode: für die Berechnung der Average True Range
+ • FVG Min. Größe: minimale Größe für Fair Value Gaps
+
+ Diese Einstellungen beeinflussen, wie Indikatoren berechnet und angezeigt werden.
+
+ completed:
+ title: "🚀 Tutorial abgeschlossen!"
+ content: |
+ Großartig! Sie haben das CryptoBot-Tutorial abgeschlossen.
+
+ Denken Sie daran, Sie können jederzeit Hilfe bekommen mit:
+ • /help - Allgemeine Hilfe
+ • /help BEFEHL - Hilfe für einen bestimmten Befehl
+
+ Bereit anzufangen? Probieren Sie /chart BTCUSDT 48 1h 0.05 für eine BTC/USDT-Analyse!
+
+ buttons:
+ continue: "Weiter"
+ skip_tutorial: "Tutorial überspringen"
+ skip_to_end: "Zum Ende springen"
+ complete: "Tutorial abschließen"
+ view_commands: "Befehle anzeigen"
+ back_to_tutorial: "Zurück zum Tutorial"
+
+ welcome_back:
+ title: "Willkommen zurück bei CryptoBot!"
+ content: |
+ Sie haben das Tutorial bereits abgeschlossen. Was möchten Sie tun?
+
+ • Ein Chart erhalten: /chart BTCUSDT 48 1h 0.05
+ • Einstellungen festlegen: /preferences
+ • Signale erstellen: /create_signal
+ • Tutorial wiederholen: /tutorial
+
+ commands_list:
+ title: "Verfügbare Befehle:"
+ content: |
+ /chart - Chart mit Indikatoren erhalten
+ /text_result - Textanalyse erhalten
+ /preferences - Indikator-Einstellungen
+ /parameters - Indikator-Feineinstellungen
+ /create_signal - Automatische Signale einrichten
+ /manage_signals - Signale verwalten
+ /help - Hilfe erhalten
+ /tutorial - Tutorial wiederholen
+ /language - Sprache ändern
+
+ happy_trading: "Viel Erfolg beim Trading! 📈\n\nVerwenden Sie /help für Hilfe."
+
+signals:
+ manage_title: "Signale verwalten:"
+ no_signals: "Keine Signale gefunden"
+ add_new: "➕ Neues Signal hinzufügen"
+ delete: "{pair} löschen"
+ deleted: "Signal gelöscht. Aktualisierte Liste:"
+ created: "Signal erstellt! Aktuelle Signale:"
+ active_count: "Sie haben {count} aktive(s) Signal(e)."
+
+ add_prompt: |
+ Bitte geben Sie Ihr gewünschtes Signal im Format ein:
+
+ `SYMBOL INTERVALL [with_chart]`
+
+ Beispiel: `BTCUSDT 60` oder `ETHUSDT 15 with_chart`
+
+ SYMBOL sollte ein gültiges Handelspaar sein (z.B. BTCUSDT).
+ INTERVALL sollte in Minuten angegeben werden (z.B. 5, 15, 60).
+
+ invalid_input: "Ungültige Eingabe: {error}. Versuchen Sie es erneut oder /cancel."
+ limit_reached: "Sie haben die maximale Anzahl von Signalen erreicht ({limit})."
+
+ usage:
+ create: "Verwendung: /create_signal [], Sie haben {count} Argument(e) gesendet."
+ delete: "Verwendung: /delete_signal "
+
+ errors:
+ unexpected: "❌ Ein unerwarteter Fehler ist aufgetreten."
+ invalid_symbol: "❌ Ungültiges Symbol: {symbol}"
+
+preferences:
+ select_prompt: "Bitte wählen Sie die Indikatoren, die Sie einschließen möchten:"
+ selected: "Sie haben gewählt: {indicators}"
+ none_selected: "Keine"
+
+ indicators:
+ order_blocks: "Order Blocks"
+ fvgs: "FVGs"
+ liquidity_levels: "Liquiditätsniveaus"
+ breaker_blocks: "Breaker Blocks"
+ liquidity_pools: "Liquiditätspools"
+ show_legend: "Legende anzeigen"
+ show_volume: "Volumen anzeigen"
+ dark_mode: "Dunkelmodus"
+ light_mode: "Hellmodus"
+
+parameters:
+ configure_title: "Indikator-Parameter konfigurieren:"
+ saved: "Parameter-Einstellungen erfolgreich gespeichert!"
+ value_set: "{param} auf {value} gesetzt."
+
+ edit_prompt: |
+ Geben Sie einen neuen Wert für {param} ein:
+
+ Aktueller Wert: {current}
+ Gültiger Bereich: {min} bis {max} (Schritt: {step})
+
+ Antworten Sie mit einer Zahl, um den neuen Wert festzulegen.
+
+ errors:
+ invalid_number: "Ungültige Eingabe. Bitte geben Sie eine gültige Zahl für {param} ein."
+ out_of_range: "Der Wert muss zwischen {min} und {max} liegen. Bitte versuchen Sie es erneut."
+ no_session: "Entschuldigung, ich habe keine aktive Bearbeitungssitzung. Bitte verwenden Sie /parameters, um neu zu beginnen."
+ process_failed: "Entschuldigung, ich konnte diese Aktion nicht verarbeiten. Bitte versuchen Sie es erneut mit /parameters."
+
+help:
+ title: "CryptoBot Hilfe"
+ main: |
+ Hier sind die Hauptbefehle, die Sie verwenden können:
+
+ /chart <symbol> <länge> <intervall> <toleranz>
+ /text_result <symbol> <länge> <intervall> <toleranz>
+ /history <symbol> <länge> <intervall> <toleranz> <zeitstempel>
+ /preferences
+ /create_signal <symbol> <minuten> [<mit_chart>]
+ /delete_signal <symbol>
+ /manage_signals
+ /sql <abfrage>
+ /tables
+ /schema <tabellenname>
+ /language
+ /help <befehl>
+
+ Geben Sie /help <befehl> für detaillierte Hilfe ein.
+
+ Beispiel: /help chart
+
+ Für mehr Details siehe README oder kontaktieren Sie den Maintainer (@yarik_is_working).
+
+ commands:
+ parameters: |
+ /parameters
+ Öffnen Sie ein interaktives Menü zur Anpassung der Indikator-Parameter.
+
+ chart: |
+ /chart <symbol> <länge> <intervall> <toleranz>
+ Erhalten Sie ein Kerzen-Chart mit allen aktivierten Indikatoren.
+
+ Beispiel: /chart BTCUSDT 48 1h 0.05
+
+ text_result: |
+ /text_result <symbol> <länge> <intervall> <toleranz>
+ Erhalten Sie eine Textzusammenfassung aller erkannten Indikatoren.
+
+ Beispiel: /text_result ETHUSDT 24 15m 0.03
+
+ history: |
+ /history <symbol> <länge> <intervall> <toleranz> <zeitstempel>
+ Erhalten Sie ein Kerzen-Chart für historische Daten zu einem bestimmten Zeitstempel.
+
+ Beispiel: /history BTCUSDT 48 1h 0.05 1700000000
+
+ preferences: |
+ /preferences
+ Öffnen Sie ein interaktives Menü zur Auswahl der Indikatoren und Chart-Optionen.
+
+ create_signal: |
+ /create_signal <symbol> <minuten> [<mit_chart>]
+ Beginnen Sie, automatische Signale für ein Paar mit einer bestimmten Frequenz zu erhalten.
+
+ Beispiel: /create_signal BTCUSDT 60 true
+
+ delete_signal: |
+ /delete_signal <symbol>
+ Stoppen Sie automatische Signale für ein Paar.
+
+ manage_signals: |
+ /manage_signals
+ Öffnen Sie ein interaktives Menü zum Anzeigen, Hinzufügen oder Löschen Ihrer Signale.
+
+ help: |
+ /help
+ Zeigen Sie die allgemeine Hilfemeldung an.
+
+ sql: |
+ /sql <abfrage>
+ Führen Sie eine SQL-Abfrage gegen die Datenbank aus.
+
+ tables: |
+ /tables
+ Zeigen Sie eine Liste aller verfügbaren Tabellen in der Datenbank.
+
+ schema: |
+ /schema <tabellenname>
+ Zeigen Sie das Schema einer bestimmten Tabelle.
+
+ language: |
+ /language
+ Ändern Sie die Sprache des Bots.
+
+errors:
+ network: "Netzwerkfehler aufgetreten. Bitte versuchen Sie es später erneut."
+ data_fetch: "Daten konnten nicht abgerufen werden. Bitte überprüfen Sie das Symbol und versuchen Sie es erneut."
+ data_processing: "Fehler bei der Datenverarbeitung. Bitte versuchen Sie es mit anderen Parametern."
+ chart_generation: "Fehler bei der Chart-Generierung. Bitte versuchen Sie es mit anderen Einstellungen."
+ invalid_input: "Ungültige Eingabe. Bitte überprüfen Sie die Befehlssyntax."
+ database: "Datenbankoperation fehlgeschlagen. Bitte versuchen Sie es erneut."
+ timeout: "Zeitüberschreitung. Bitte versuchen Sie es erneut."
+ api_limit: "API-Ratenlimit erreicht. Bitte versuchen Sie es später erneut."
+ permission: "Sie haben keine Berechtigung für diese Aktion."
+ unknown: "Ein unerwarteter Fehler ist aufgetreten. Bitte versuchen Sie es später erneut."
+
+ global: |
+ ❌ Bei der Verarbeitung Ihrer Anfrage ist ein unerwarteter Fehler aufgetreten. Bitte versuchen Sie es später erneut.
+
+ Wenn Sie denken, dass dies ein Fehler ist, melden Sie ihn hier: https://github.com/yariks5s/analysis_telegram_bot/issues
+
+ preferences_failed: "Einstellungen konnten nicht abgerufen werden. Bitte setzen Sie sie erneut über /preferences."
+ analyze_failed: "Daten konnten nicht analysiert werden. Bitte überprüfen Sie Ihre Eingaben und versuchen Sie es erneut."
+ chart_send_failed: "Chart konnte nicht gesendet werden. Bitte versuchen Sie es erneut."
+ signal_generation_failed: "Preisvorhersage-Signal konnte nicht generiert werden. Sende Chart ohne Analyse."
+ analysis_not_available: "Analyse nicht verfügbar"
+ invalid_timestamp: "❌ Ungültiger Zeitstempel. Muss ein Unix-Zeitstempel in der Vergangenheit sein."
+ historical_usage: "Ungültige Parameter. Verwendung: /history "
+
+rate_limit:
+ title: "**Ratenlimit-Status**"
+ remaining: "• Verbleibend: {remaining}/{limit} Anfragen{suffix}"
+ reset_in: "• Zurücksetzung in: {seconds} Sekunden"
+ reduced_notice: "ℹ️ Ihr Ratenlimit wurde aufgrund erkannter hochfrequenter Aktivität vorübergehend reduziert. Es wird nach einer Abkühlphase automatisch wieder normal."
+ footer: "Das System verwendet Ratenlimits, um eine faire API-Nutzung für alle Benutzer zu gewährleisten."
+ suspicious_suffix: " (reduziert wegen verdächtiger Aktivität)"
+
+database:
+ sql:
+ no_query: |
+ Bitte geben Sie eine SQL-Abfrage an.
+ Verwendung: /sql
+ Beispiel: /sql SELECT * FROM trades LIMIT 5
+ results: "Abfrageergebnisse:\n\n{results}"
+ success_no_results: "Abfrage erfolgreich ausgeführt. Keine Ergebnisse zurückgegeben."
+ error: "❌ Fehler bei der Abfrageausführung: {error}"
+
+ tables:
+ title: "Verfügbare Tabellen:\n\n"
+ empty: "Keine Tabellen in der Datenbank gefunden."
+ error: "❌ Fehler: {error}"
+
+ schema:
+ no_table: |
+ Bitte geben Sie einen Tabellennamen an.
+ Verwendung: /schema
+ Beispiel: /schema trades
+ title: "Schema für Tabelle '{table}':\n\n"
+ not_found: "Kein Schema für Tabelle '{table}' gefunden."
+ error: "❌ Fehler: {error}"
+
+history:
+ title: "📜 *Ihre Signalhistorie*"
+ filter_prompt: "\n\nFiltern nach Zeitraum oder Währungspaar:"
+ no_signals: "Keine Signalhistorie gefunden. Signale werden bei ihrer Generierung aufgezeichnet."
+
+ periods:
+ last_24h: "Letzte 24h"
+ last_7d: "Letzte 7T"
+ last_30d: "Letzte 30T"
+
+ filtered_title: "📜 *Signalhistorie - Letzte {period}*"
+ pair_title: "📜 *Signalhistorie - {pair}*"
+
+ no_signals_period: "Keine Signale in den letzten {period} gefunden."
+ no_signals_pair: "Keine Signale für {pair} gefunden."
+
+ show_all: "Alle anzeigen"
+ showing_recent: "_Zeige {count} neueste Signale._"
+
+ export:
+ csv: "CSV exportieren"
+ json: "JSON exportieren"
+ preparing: "Bereite {format}-Export vor..."
+ no_data: "Keine Signaldaten {description} zum Exportieren verfügbar."
+ caption: "📊 Signalhistorie {description} als {format} exportiert"
+ for_pair: "für {pair}"
+ for_all: "für alle Paare"
+ error: "Signalhistorie konnte nicht als {format} exportiert werden"
+
+ update_failed: "Nachricht konnte nicht aktualisiert werden"
+
diff --git a/src/i18n/locales/en.yaml b/src/i18n/locales/en.yaml
new file mode 100644
index 0000000..6b79fe1
--- /dev/null
+++ b/src/i18n/locales/en.yaml
@@ -0,0 +1,433 @@
+# English translations for CryptoBot
+# This is the primary language file - all keys should be defined here
+
+common:
+ done: "Done"
+ cancel: "Cancel"
+ save: "Save"
+ back: "Back"
+ continue: "Continue"
+ skip: "Skip"
+ finish: "Finish"
+ yes: "Yes"
+ no: "No"
+ error: "Error"
+ success: "Success"
+ loading: "Loading..."
+ no_data: "No data available"
+
+language:
+ select_prompt: "🌐 Select your preferred language:"
+ changed: "✅ Language changed to English"
+ current: "Current language: English 🇬🇧"
+
+# Tutorial messages
+tutorial:
+ welcome:
+ title: "🎉 Welcome to CryptoBot!"
+ content: |
+ I'm your personal crypto market analysis assistant. I can help you analyze cryptocurrency charts with advanced technical indicators.
+
+ This short tutorial will show you how to use my main features. You can skip the tutorial or come back to it anytime with the /tutorial command.
+
+ chart:
+ title: "📊 Chart Analysis"
+ content: |
+ The basic command to get a chart analysis is:
+
+ /chart SYMBOL LENGTH INTERVAL TOLERANCE
+
+ For example: /chart BTCUSDT 48 1h 0.05
+
+ This gives you a 48-candle, 1-hour chart for BTC/USDT with a tolerance of 0.05 for detecting liquidity levels.
+
+ Try it out or continue to learn more features!
+
+ preferences:
+ title: "⚙️ Customizing Indicators"
+ content: |
+ You can customize which indicators appear on your charts with the /preferences command.
+
+ This opens an interactive menu where you can enable or disable:
+ • Order Blocks
+ • Fair Value Gaps (FVGs)
+ • Liquidity Levels
+ • Breaker Blocks
+ • And change display options
+
+ Your preferences will be saved for future chart requests.
+
+ signals:
+ title: "🔔 Automated Signals"
+ content: |
+ Want to receive automatic notifications? Set up signal alerts with:
+
+ /create_signal SYMBOL MINUTES [CHART]
+
+ For example: /create_signal BTCUSDT 60 true
+
+ This will send you a notification with a chart every 60 minutes for BTC/USDT.
+
+ You can manage your signals with the /manage_signals command.
+
+ parameters:
+ title: "🔧 Advanced Parameters"
+ content: |
+ Fine-tune indicator parameters with the /parameters command.
+
+ You can adjust:
+ • ATR Period: for calculating Average True Range
+ • FVG Min Size: minimum size for Fair Value Gaps
+
+ These settings affect how indicators are calculated and displayed.
+
+ completed:
+ title: "🚀 Tutorial Completed!"
+ content: |
+ Great job! You've completed the CryptoBot tutorial.
+
+ Remember, you can always get help with:
+ • /help - General help information
+ • /help COMMAND - Help for a specific command
+
+ Ready to start? Try /chart BTCUSDT 48 1h 0.05 to see BTC/USDT analysis!
+
+ buttons:
+ continue: "Continue"
+ skip_tutorial: "Skip Tutorial"
+ skip_to_end: "Skip to End"
+ complete: "Complete Tutorial"
+ view_commands: "View Available Commands"
+ back_to_tutorial: "Back to Tutorial"
+
+ welcome_back:
+ title: "Welcome back to CryptoBot!"
+ content: |
+ You've already completed the tutorial. What would you like to do?
+
+ • Get a chart with /chart BTCUSDT 48 1h 0.05
+ • Set preferences with /preferences
+ • Create signals with /create_signal
+ • Review the tutorial with /tutorial
+
+ commands_list:
+ title: "Available Commands:"
+ content: |
+ /chart - Get a chart with indicators
+ /text_result - Get a text analysis
+ /preferences - Set indicator preferences
+ /parameters - Fine-tune indicator settings
+ /create_signal - Set up automatic signals
+ /manage_signals - Manage your signal alerts
+ /help - Get help with commands
+ /tutorial - Revisit this tutorial
+ /language - Change language
+
+ happy_trading: "Happy trading! 📈\n\nUse /help anytime you need assistance."
+
+# Signal management
+signals:
+ manage_title: "Manage your signals:"
+ no_signals: "No signals found"
+ add_new: "➕ Add New Signal"
+ delete: "Delete {pair}"
+ deleted: "Signal deleted. Updated list:"
+ created: "Signal created! Current signals:"
+ active_count: "You have {count} active signal(s)."
+
+ add_prompt: |
+ Please enter your desired signal in the format:
+
+ `SYMBOL INTERVAL [with_chart]`
+
+ Example: `BTCUSDT 60` or `ETHUSDT 15 with_chart`
+
+ SYMBOL should be a valid trading pair (e.g., BTCUSDT).
+ INTERVAL should be in minutes (e.g., 5, 15, 60).
+
+ invalid_input: "Invalid input: {error}. Try again or /cancel."
+ limit_reached: "You have reached the maximum number of signals ({limit})."
+
+ usage:
+ create: "Usage: /create_signal [], you've sent {count} argument(s)."
+ delete: "Usage: /delete_signal "
+
+ errors:
+ unexpected: "❌ An unexpected error occurred."
+ invalid_symbol: "❌ Invalid symbol: {symbol}"
+
+# Indicator preferences
+preferences:
+ select_prompt: "Please choose the indicators you'd like to include:"
+ selected: "You selected: {indicators}"
+ none_selected: "None"
+
+ indicators:
+ order_blocks: "Order Blocks"
+ fvgs: "FVGs"
+ liquidity_levels: "Liquidity Levels"
+ breaker_blocks: "Breaker Blocks"
+ liquidity_pools: "Liquidity Pools"
+ show_legend: "Show Legend"
+ show_volume: "Show Volume"
+ dark_mode: "Dark Mode"
+ light_mode: "Light Mode"
+
+# Parameters
+parameters:
+ configure_title: "Configure indicator parameters:"
+ saved: "Parameter settings saved successfully!"
+ value_set: "{param} set to {value}."
+
+ edit_prompt: |
+ Enter a new value for {param}:
+
+ Current value: {current}
+ Valid range: {min} to {max} (step: {step})
+
+ Reply with a number to set the new value.
+
+ errors:
+ invalid_number: "Invalid input. Please enter a valid number for {param}."
+ out_of_range: "Value must be between {min} and {max}. Please try again."
+ no_session: "Sorry, I don't have an active parameter editing session. Please use /parameters to start over."
+ process_failed: "Sorry, I couldn't process that action. Please try again with /parameters."
+
+# Help messages
+help:
+ title: "CryptoBot Help"
+ main: |
+ Here are the main commands you can use:
+
+ /chart <symbol> <length> <interval> <tolerance>
+ /text_result <symbol> <length> <interval> <tolerance>
+ /history <symbol> <length> <interval> <tolerance> <timestamp>
+ /preferences
+ /create_signal <symbol> <minutes> [<show_chart>]
+ /delete_signal <symbol>
+ /manage_signals
+ /sql <query>
+ /tables
+ /schema <table_name>
+ /language
+ /help <command>
+
+ Type /help <command> to get detailed help for a specific command.
+
+ Example: /help chart
+
+ For more details, see the README or contact the maintainer (@yarik_is_working).
+
+ commands:
+ parameters: |
+ /parameters
+ Open an interactive menu to customize indicator parameters.
+
+ Available parameters:
+ - ATR Period: Adjust the period used for Average True Range calculations (1-50)
+ - FVG Min Size: Set the minimum size ratio for Fair Value Gaps (0.0001-0.01)
+
+ Click on any parameter to modify its value.
+
+ chart: |
+ /chart <symbol> <length> <interval> <tolerance>
+ Get a candlestick chart with all enabled indicators.
+
+ - symbol: The trading pair, e.g. BTCUSDT
+ - length: Amount of candles to analyze (e.g. 48)
+ - interval: Candle interval (e.g. 1h, 15m)
+ - tolerance: Sensitivity for liquidity level detection (0-1, e.g. 0.05 = more levels, 0.2 = fewer, only strongest)
+
+ Example: /chart BTCUSDT 48 1h 0.05
+
+ The chart will include all indicators you have enabled in your preferences.
+
+ text_result: |
+ /text_result <symbol> <length> <interval> <tolerance>
+ Get a text summary of all detected indicators for the specified symbol and timeframe.
+
+ Example: /text_result ETHUSDT 24 15m 0.03
+
+ history: |
+ /history <symbol> <length> <interval> <tolerance> <timestamp>
+ Get a candlestick chart for historical data at a specified timestamp.
+
+ - symbol: The trading pair, e.g. BTCUSDT
+ - length: Number of candles to analyze
+ - interval: Candle interval (e.g., 1h, 15m)
+ - tolerance: Sensitivity for liquidity level detection (0-1)
+ - timestamp: Unix epoch seconds for the end of the period
+
+ Example: /history BTCUSDT 48 1h 0.05 1700000000
+
+ preferences: |
+ /preferences
+ Open an interactive menu to select which indicators to use and chart options.
+
+ Available options:
+ - Technical indicators (Order Blocks, FVGs, Liquidity Levels, etc.)
+ - Display settings (Show Legend, Show Volume)
+ - Theme selection (Light/Dark mode)
+
+ create_signal: |
+ /create_signal <symbol> <minutes> [<show_chart>]
+ Start receiving auto-signals for a pair at a given frequency (in minutes).
+
+ - symbol: The trading pair, e.g. BTCUSDT
+ - minutes: Frequency in minutes to receive signals
+ - show_chart (optional): true/false, whether to include a chart with each signal (default: false)
+
+ Example: /create_signal BTCUSDT 60 true
+
+ You can have up to 10 active signal jobs per user.
+
+ delete_signal: |
+ /delete_signal <symbol>
+ Stop auto-signals for a pair.
+
+ Example: /delete_signal BTCUSDT
+
+ manage_signals: |
+ /manage_signals
+ Open an interactive menu to view, add, or delete your signal jobs.
+
+ You can have up to 10 active signal jobs per user.
+
+ help: |
+ /help
+ Show the general help message.
+
+ /help <command>
+ Show detailed help for a specific command.
+
+ Example: /help chart
+
+ sql: |
+ /sql <query>
+ Execute a custom SQL query against the database.
+
+ Example queries:
+ - /sql SELECT * FROM trades LIMIT 5
+ - /sql SELECT symbol, COUNT(*) as trades FROM trades GROUP BY symbol
+ - /sql SELECT AVG(profit_loss) as avg_profit FROM trades WHERE symbol = 'BTCUSDT'
+
+ Note: Results are limited to 50 rows for readability.
+
+ tables: |
+ /tables
+ Show a list of all available tables in the database.
+
+ Use this command to see what tables you can query with /sql.
+
+ schema: |
+ /schema <table_name>
+ Show the schema (structure) of a specific table.
+
+ Example: /schema trades
+
+ This will show you all columns and their data types in the specified table.
+
+ language: |
+ /language
+ Change the bot's language.
+
+ Supported languages:
+ - 🇬🇧 English
+ - 🇷🇺 Русский (Russian)
+ - 🇺🇦 Українська (Ukrainian)
+ - 🇩🇪 Deutsch (German)
+ - 🇫🇷 Français (French)
+ - 🇳🇱 Nederlands (Dutch)
+
+# Error messages
+errors:
+ network: "Network error occurred. Please try again later."
+ data_fetch: "Failed to fetch data. Please check the symbol and try again."
+ data_processing: "Error processing data. Please try with different parameters."
+ chart_generation: "Error generating chart. Please try again with different settings."
+ invalid_input: "Invalid input provided. Please check your command syntax."
+ database: "Database operation failed. Please try again."
+ timeout: "Operation timed out. Please try again."
+ api_limit: "API rate limit reached. Please try again later."
+ permission: "You don't have permission to perform this action."
+ unknown: "An unexpected error occurred. Please try again later."
+
+ global: |
+ ❌ An unexpected error occurred while processing your request. Please try again later.
+
+ If you think this is a bug, please report it here: https://github.com/yariks5s/analysis_telegram_bot/issues
+
+ preferences_failed: "Failed to retrieve user preferences. Please try setting your preferences again using /preferences."
+ analyze_failed: "Failed to analyze data. Please check your inputs and try again."
+ chart_send_failed: "Failed to send chart. Please try again."
+ signal_generation_failed: "Could not generate price prediction signal. Sending chart without analysis."
+ analysis_not_available: "Analysis not available"
+ invalid_timestamp: "❌ Invalid timestamp. Must be a Unix epoch timestamp in the past."
+ historical_usage: "Invalid command parameters. Usage: /history "
+
+# Rate limit status
+rate_limit:
+ title: "**Rate Limit Status**"
+ remaining: "• Remaining: {remaining}/{limit} requests{suffix}"
+ reset_in: "• Reset in: {seconds} seconds"
+ reduced_notice: "ℹ️ Your rate limit is temporarily reduced due to detected high-frequency activity. This will automatically return to normal after a cooling period."
+ footer: "The system uses rate limiting to ensure fair API usage for all users."
+ suspicious_suffix: " (reduced due to suspicious activity)"
+
+# Database commands
+database:
+ sql:
+ no_query: |
+ Please provide a SQL query.
+ Usage: /sql
+ Example: /sql SELECT * FROM trades LIMIT 5
+ results: "Query Results:\n\n{results}"
+ success_no_results: "Query executed successfully. No results returned."
+ error: "❌ Error executing query: {error}"
+
+ tables:
+ title: "Available tables:\n\n"
+ empty: "No tables found in the database."
+ error: "❌ Error: {error}"
+
+ schema:
+ no_table: |
+ Please provide a table name.
+ Usage: /schema
+ Example: /schema trades
+ title: "Schema for table '{table}':\n\n"
+ not_found: "No schema found for table '{table}'."
+ error: "❌ Error: {error}"
+
+# Signal history
+history:
+ title: "📜 *Your Signal History*"
+ filter_prompt: "\n\nFilter by time period or currency pair:"
+ no_signals: "No signal history found. Signals will be recorded when they are generated."
+
+ periods:
+ last_24h: "Last 24h"
+ last_7d: "Last 7d"
+ last_30d: "Last 30d"
+
+ filtered_title: "📜 *Signal History - Last {period}*"
+ pair_title: "📜 *Signal History - {pair}*"
+
+ no_signals_period: "No signals found in the last {period}."
+ no_signals_pair: "No signals found for {pair}."
+
+ show_all: "Show All"
+ showing_recent: "_Showing {count} most recent signals._"
+
+ export:
+ csv: "Export CSV"
+ json: "Export JSON"
+ preparing: "Preparing {format} export..."
+ no_data: "No signal data available {description} to export."
+ caption: "📊 Signal history {description} exported as {format}"
+ for_pair: "for {pair}"
+ for_all: "for all pairs"
+ error: "Failed to export signal history as {format}"
+
+ update_failed: "Couldn't update message"
+
diff --git a/src/i18n/locales/fr.yaml b/src/i18n/locales/fr.yaml
new file mode 100644
index 0000000..f34f233
--- /dev/null
+++ b/src/i18n/locales/fr.yaml
@@ -0,0 +1,364 @@
+# French translations for CryptoBot
+# Traductions françaises pour CryptoBot
+
+common:
+ done: "Terminé"
+ cancel: "Annuler"
+ save: "Enregistrer"
+ back: "Retour"
+ continue: "Continuer"
+ skip: "Passer"
+ finish: "Terminer"
+ yes: "Oui"
+ no: "Non"
+ error: "Erreur"
+ success: "Succès"
+ loading: "Chargement..."
+ no_data: "Aucune donnée disponible"
+
+language:
+ select_prompt: "🌐 Sélectionnez votre langue préférée :"
+ changed: "✅ Langue changée en Français"
+ current: "Langue actuelle : Français 🇫🇷"
+
+tutorial:
+ welcome:
+ title: "🎉 Bienvenue sur CryptoBot !"
+ content: |
+ Je suis votre assistant personnel d'analyse du marché crypto. Je peux vous aider à analyser les graphiques de cryptomonnaies avec des indicateurs techniques avancés.
+
+ Ce court tutoriel vous montrera comment utiliser mes fonctionnalités principales. Vous pouvez passer le tutoriel ou y revenir à tout moment avec la commande /tutorial.
+
+ chart:
+ title: "📊 Analyse de graphique"
+ content: |
+ La commande de base pour obtenir une analyse de graphique est :
+
+ /chart SYMBOLE LONGUEUR INTERVALLE TOLÉRANCE
+
+ Par exemple : /chart BTCUSDT 48 1h 0.05
+
+ Cela vous donne un graphique de 48 bougies sur 1 heure pour BTC/USDT avec une tolérance de 0.05 pour la détection des niveaux de liquidité.
+
+ Essayez-le ou continuez pour en savoir plus !
+
+ preferences:
+ title: "⚙️ Personnalisation des indicateurs"
+ content: |
+ Vous pouvez personnaliser les indicateurs qui apparaissent sur vos graphiques avec la commande /preferences.
+
+ Cela ouvre un menu interactif où vous pouvez activer ou désactiver :
+ • Order Blocks
+ • Fair Value Gaps (FVGs)
+ • Niveaux de liquidité
+ • Breaker Blocks
+ • Et modifier les options d'affichage
+
+ Vos préférences seront sauvegardées pour les futures requêtes.
+
+ signals:
+ title: "🔔 Signaux automatiques"
+ content: |
+ Vous voulez recevoir des notifications automatiques ? Configurez des alertes de signal avec :
+
+ /create_signal SYMBOLE MINUTES [GRAPHIQUE]
+
+ Par exemple : /create_signal BTCUSDT 60 true
+
+ Cela vous enverra une notification avec graphique toutes les 60 minutes pour BTC/USDT.
+
+ Vous pouvez gérer vos signaux avec la commande /manage_signals.
+
+ parameters:
+ title: "🔧 Paramètres avancés"
+ content: |
+ Affinez les paramètres des indicateurs avec la commande /parameters.
+
+ Vous pouvez ajuster :
+ • Période ATR : pour le calcul de l'Average True Range
+ • Taille min. FVG : taille minimale pour les Fair Value Gaps
+
+ Ces paramètres affectent le calcul et l'affichage des indicateurs.
+
+ completed:
+ title: "🚀 Tutoriel terminé !"
+ content: |
+ Excellent ! Vous avez terminé le tutoriel CryptoBot.
+
+ N'oubliez pas, vous pouvez toujours obtenir de l'aide avec :
+ • /help - Aide générale
+ • /help COMMANDE - Aide pour une commande spécifique
+
+ Prêt à commencer ? Essayez /chart BTCUSDT 48 1h 0.05 pour une analyse BTC/USDT !
+
+ buttons:
+ continue: "Continuer"
+ skip_tutorial: "Passer le tutoriel"
+ skip_to_end: "Aller à la fin"
+ complete: "Terminer le tutoriel"
+ view_commands: "Voir les commandes"
+ back_to_tutorial: "Retour au tutoriel"
+
+ welcome_back:
+ title: "Bon retour sur CryptoBot !"
+ content: |
+ Vous avez déjà terminé le tutoriel. Que souhaitez-vous faire ?
+
+ • Obtenir un graphique : /chart BTCUSDT 48 1h 0.05
+ • Définir les préférences : /preferences
+ • Créer des signaux : /create_signal
+ • Revoir le tutoriel : /tutorial
+
+ commands_list:
+ title: "Commandes disponibles :"
+ content: |
+ /chart - Obtenir un graphique avec indicateurs
+ /text_result - Obtenir une analyse textuelle
+ /preferences - Paramètres des indicateurs
+ /parameters - Réglages fins des indicateurs
+ /create_signal - Configurer des signaux automatiques
+ /manage_signals - Gérer les signaux
+ /help - Obtenir de l'aide
+ /tutorial - Revoir le tutoriel
+ /language - Changer de langue
+
+ happy_trading: "Bon trading ! 📈\n\nUtilisez /help pour obtenir de l'aide."
+
+signals:
+ manage_title: "Gérer vos signaux :"
+ no_signals: "Aucun signal trouvé"
+ add_new: "➕ Ajouter un nouveau signal"
+ delete: "Supprimer {pair}"
+ deleted: "Signal supprimé. Liste mise à jour :"
+ created: "Signal créé ! Signaux actuels :"
+ active_count: "Vous avez {count} signal(aux) actif(s)."
+
+ add_prompt: |
+ Veuillez entrer votre signal souhaité au format :
+
+ `SYMBOLE INTERVALLE [with_chart]`
+
+ Exemple : `BTCUSDT 60` ou `ETHUSDT 15 with_chart`
+
+ SYMBOLE doit être une paire de trading valide (ex : BTCUSDT).
+ INTERVALLE doit être en minutes (ex : 5, 15, 60).
+
+ invalid_input: "Entrée invalide : {error}. Réessayez ou /cancel."
+ limit_reached: "Vous avez atteint le nombre maximum de signaux ({limit})."
+
+ usage:
+ create: "Utilisation : /create_signal [], vous avez envoyé {count} argument(s)."
+ delete: "Utilisation : /delete_signal "
+
+ errors:
+ unexpected: "❌ Une erreur inattendue s'est produite."
+ invalid_symbol: "❌ Symbole invalide : {symbol}"
+
+preferences:
+ select_prompt: "Veuillez choisir les indicateurs à inclure :"
+ selected: "Vous avez sélectionné : {indicators}"
+ none_selected: "Aucun"
+
+ indicators:
+ order_blocks: "Order Blocks"
+ fvgs: "FVGs"
+ liquidity_levels: "Niveaux de liquidité"
+ breaker_blocks: "Breaker Blocks"
+ liquidity_pools: "Pools de liquidité"
+ show_legend: "Afficher la légende"
+ show_volume: "Afficher le volume"
+ dark_mode: "Mode sombre"
+ light_mode: "Mode clair"
+
+parameters:
+ configure_title: "Configurer les paramètres des indicateurs :"
+ saved: "Paramètres enregistrés avec succès !"
+ value_set: "{param} défini à {value}."
+
+ edit_prompt: |
+ Entrez une nouvelle valeur pour {param} :
+
+ Valeur actuelle : {current}
+ Plage valide : {min} à {max} (pas : {step})
+
+ Répondez avec un nombre pour définir la nouvelle valeur.
+
+ errors:
+ invalid_number: "Entrée invalide. Veuillez entrer un nombre valide pour {param}."
+ out_of_range: "La valeur doit être entre {min} et {max}. Veuillez réessayer."
+ no_session: "Désolé, je n'ai pas de session d'édition active. Veuillez utiliser /parameters pour recommencer."
+ process_failed: "Désolé, je n'ai pas pu traiter cette action. Veuillez réessayer avec /parameters."
+
+help:
+ title: "Aide CryptoBot"
+ main: |
+ Voici les commandes principales que vous pouvez utiliser :
+
+ /chart <symbole> <longueur> <intervalle> <tolérance>
+ /text_result <symbole> <longueur> <intervalle> <tolérance>
+ /history <symbole> <longueur> <intervalle> <tolérance> <timestamp>
+ /preferences
+ /create_signal <symbole> <minutes> [<avec_graphique>]
+ /delete_signal <symbole>
+ /manage_signals
+ /sql <requête>
+ /tables
+ /schema <nom_table>
+ /language
+ /help <commande>
+
+ Tapez /help <commande> pour une aide détaillée.
+
+ Exemple : /help chart
+
+ Pour plus de détails, voir le README ou contacter le mainteneur (@yarik_is_working).
+
+ commands:
+ parameters: |
+ /parameters
+ Ouvrir un menu interactif pour personnaliser les paramètres des indicateurs.
+
+ chart: |
+ /chart <symbole> <longueur> <intervalle> <tolérance>
+ Obtenir un graphique en chandeliers avec tous les indicateurs activés.
+
+ Exemple : /chart BTCUSDT 48 1h 0.05
+
+ text_result: |
+ /text_result <symbole> <longueur> <intervalle> <tolérance>
+ Obtenir un résumé textuel de tous les indicateurs détectés.
+
+ Exemple : /text_result ETHUSDT 24 15m 0.03
+
+ history: |
+ /history <symbole> <longueur> <intervalle> <tolérance> <timestamp>
+ Obtenir un graphique en chandeliers pour les données historiques à un timestamp spécifié.
+
+ Exemple : /history BTCUSDT 48 1h 0.05 1700000000
+
+ preferences: |
+ /preferences
+ Ouvrir un menu interactif pour sélectionner les indicateurs et les options de graphique.
+
+ create_signal: |
+ /create_signal <symbole> <minutes> [<avec_graphique>]
+ Commencer à recevoir des signaux automatiques pour une paire à une fréquence donnée.
+
+ Exemple : /create_signal BTCUSDT 60 true
+
+ delete_signal: |
+ /delete_signal <symbole>
+ Arrêter les signaux automatiques pour une paire.
+
+ manage_signals: |
+ /manage_signals
+ Ouvrir un menu interactif pour voir, ajouter ou supprimer vos signaux.
+
+ help: |
+ /help
+ Afficher le message d'aide général.
+
+ sql: |
+ /sql <requête>
+ Exécuter une requête SQL sur la base de données.
+
+ tables: |
+ /tables
+ Afficher la liste de toutes les tables disponibles dans la base de données.
+
+ schema: |
+ /schema <nom_table>
+ Afficher le schéma d'une table spécifique.
+
+ language: |
+ /language
+ Changer la langue du bot.
+
+errors:
+ network: "Erreur réseau. Veuillez réessayer plus tard."
+ data_fetch: "Échec de la récupération des données. Veuillez vérifier le symbole et réessayer."
+ data_processing: "Erreur de traitement des données. Veuillez essayer avec d'autres paramètres."
+ chart_generation: "Erreur de génération du graphique. Veuillez essayer avec d'autres paramètres."
+ invalid_input: "Entrée invalide. Veuillez vérifier la syntaxe de la commande."
+ database: "Opération de base de données échouée. Veuillez réessayer."
+ timeout: "Délai d'attente dépassé. Veuillez réessayer."
+ api_limit: "Limite d'API atteinte. Veuillez réessayer plus tard."
+ permission: "Vous n'avez pas la permission d'effectuer cette action."
+ unknown: "Une erreur inattendue s'est produite. Veuillez réessayer plus tard."
+
+ global: |
+ ❌ Une erreur inattendue s'est produite lors du traitement de votre demande. Veuillez réessayer plus tard.
+
+ Si vous pensez qu'il s'agit d'un bug, signalez-le ici : https://github.com/yariks5s/analysis_telegram_bot/issues
+
+ preferences_failed: "Échec de la récupération des préférences. Veuillez les redéfinir via /preferences."
+ analyze_failed: "Échec de l'analyse des données. Veuillez vérifier vos entrées et réessayer."
+ chart_send_failed: "Échec de l'envoi du graphique. Veuillez réessayer."
+ signal_generation_failed: "Impossible de générer le signal de prévision de prix. Envoi du graphique sans analyse."
+ analysis_not_available: "Analyse non disponible"
+ invalid_timestamp: "❌ Timestamp invalide. Doit être un timestamp Unix dans le passé."
+ historical_usage: "Paramètres invalides. Utilisation : /history "
+
+rate_limit:
+ title: "**Statut de limite de taux**"
+ remaining: "• Restant : {remaining}/{limit} requêtes{suffix}"
+ reset_in: "• Réinitialisation dans : {seconds} secondes"
+ reduced_notice: "ℹ️ Votre limite de taux est temporairement réduite en raison d'une activité à haute fréquence détectée. Elle reviendra automatiquement à la normale après une période de refroidissement."
+ footer: "Le système utilise la limitation de taux pour assurer une utilisation équitable de l'API pour tous les utilisateurs."
+ suspicious_suffix: " (réduit en raison d'activité suspecte)"
+
+database:
+ sql:
+ no_query: |
+ Veuillez fournir une requête SQL.
+ Utilisation : /sql
+ Exemple : /sql SELECT * FROM trades LIMIT 5
+ results: "Résultats de la requête :\n\n{results}"
+ success_no_results: "Requête exécutée avec succès. Aucun résultat retourné."
+ error: "❌ Erreur d'exécution de la requête : {error}"
+
+ tables:
+ title: "Tables disponibles :\n\n"
+ empty: "Aucune table trouvée dans la base de données."
+ error: "❌ Erreur : {error}"
+
+ schema:
+ no_table: |
+ Veuillez fournir un nom de table.
+ Utilisation : /schema
+ Exemple : /schema trades
+ title: "Schéma de la table '{table}' :\n\n"
+ not_found: "Aucun schéma trouvé pour la table '{table}'."
+ error: "❌ Erreur : {error}"
+
+history:
+ title: "📜 *Votre historique de signaux*"
+ filter_prompt: "\n\nFiltrer par période ou paire de devises :"
+ no_signals: "Aucun historique de signaux trouvé. Les signaux seront enregistrés lors de leur génération."
+
+ periods:
+ last_24h: "Dernières 24h"
+ last_7d: "Derniers 7j"
+ last_30d: "Derniers 30j"
+
+ filtered_title: "📜 *Historique des signaux - Dernières {period}*"
+ pair_title: "📜 *Historique des signaux - {pair}*"
+
+ no_signals_period: "Aucun signal trouvé dans les dernières {period}."
+ no_signals_pair: "Aucun signal trouvé pour {pair}."
+
+ show_all: "Afficher tout"
+ showing_recent: "_Affichage des {count} signaux les plus récents._"
+
+ export:
+ csv: "Exporter CSV"
+ json: "Exporter JSON"
+ preparing: "Préparation de l'export {format}..."
+ no_data: "Aucune donnée de signal {description} à exporter."
+ caption: "📊 Historique des signaux {description} exporté en {format}"
+ for_pair: "pour {pair}"
+ for_all: "pour toutes les paires"
+ error: "Échec de l'export de l'historique des signaux en {format}"
+
+ update_failed: "Impossible de mettre à jour le message"
+
diff --git a/src/i18n/locales/nl.yaml b/src/i18n/locales/nl.yaml
new file mode 100644
index 0000000..d0a7992
--- /dev/null
+++ b/src/i18n/locales/nl.yaml
@@ -0,0 +1,364 @@
+# Dutch translations for CryptoBot
+# Nederlandse vertalingen voor CryptoBot
+
+common:
+ done: "Gereed"
+ cancel: "Annuleren"
+ save: "Opslaan"
+ back: "Terug"
+ continue: "Doorgaan"
+ skip: "Overslaan"
+ finish: "Voltooien"
+ yes: "Ja"
+ no: "Nee"
+ error: "Fout"
+ success: "Succes"
+ loading: "Laden..."
+ no_data: "Geen gegevens beschikbaar"
+
+language:
+ select_prompt: "🌐 Selecteer uw voorkeurstaal:"
+ changed: "✅ Taal gewijzigd naar Nederlands"
+ current: "Huidige taal: Nederlands 🇳🇱"
+
+tutorial:
+ welcome:
+ title: "🎉 Welkom bij CryptoBot!"
+ content: |
+ Ik ben uw persoonlijke assistent voor crypto-marktanalyse. Ik kan u helpen bij het analyseren van cryptocurrency-grafieken met geavanceerde technische indicatoren.
+
+ Deze korte tutorial laat u zien hoe u mijn belangrijkste functies kunt gebruiken. U kunt de tutorial overslaan of er op elk moment naar terugkeren met het /tutorial commando.
+
+ chart:
+ title: "📊 Grafiekanalyse"
+ content: |
+ Het basiscommando voor een grafiekanalyse is:
+
+ /chart SYMBOOL LENGTE INTERVAL TOLERANTIE
+
+ Bijvoorbeeld: /chart BTCUSDT 48 1h 0.05
+
+ Dit geeft u een 48-kaarsen, 1-uurs grafiek voor BTC/USDT met een tolerantie van 0.05 voor het detecteren van liquiditeitsniveaus.
+
+ Probeer het uit of ga verder om meer te leren!
+
+ preferences:
+ title: "⚙️ Indicatoren aanpassen"
+ content: |
+ U kunt aanpassen welke indicatoren op uw grafieken verschijnen met het /preferences commando.
+
+ Dit opent een interactief menu waar u kunt in- of uitschakelen:
+ • Order Blocks
+ • Fair Value Gaps (FVGs)
+ • Liquiditeitsniveaus
+ • Breaker Blocks
+ • En weergave-opties wijzigen
+
+ Uw voorkeuren worden opgeslagen voor toekomstige verzoeken.
+
+ signals:
+ title: "🔔 Automatische signalen"
+ content: |
+ Wilt u automatische meldingen ontvangen? Stel signaalalerts in met:
+
+ /create_signal SYMBOOL MINUTEN [GRAFIEK]
+
+ Bijvoorbeeld: /create_signal BTCUSDT 60 true
+
+ Dit stuurt u elke 60 minuten een melding met grafiek voor BTC/USDT.
+
+ U kunt uw signalen beheren met het /manage_signals commando.
+
+ parameters:
+ title: "🔧 Geavanceerde parameters"
+ content: |
+ Verfijn indicatorparameters met het /parameters commando.
+
+ U kunt aanpassen:
+ • ATR-periode: voor het berekenen van de Average True Range
+ • FVG min. grootte: minimale grootte voor Fair Value Gaps
+
+ Deze instellingen beïnvloeden hoe indicatoren worden berekend en weergegeven.
+
+ completed:
+ title: "🚀 Tutorial voltooid!"
+ content: |
+ Geweldig! U hebt de CryptoBot-tutorial voltooid.
+
+ Onthoud, u kunt altijd hulp krijgen met:
+ • /help - Algemene hulp
+ • /help COMMANDO - Hulp voor een specifiek commando
+
+ Klaar om te beginnen? Probeer /chart BTCUSDT 48 1h 0.05 voor een BTC/USDT-analyse!
+
+ buttons:
+ continue: "Doorgaan"
+ skip_tutorial: "Tutorial overslaan"
+ skip_to_end: "Naar het einde"
+ complete: "Tutorial voltooien"
+ view_commands: "Commando's bekijken"
+ back_to_tutorial: "Terug naar tutorial"
+
+ welcome_back:
+ title: "Welkom terug bij CryptoBot!"
+ content: |
+ U hebt de tutorial al voltooid. Wat wilt u doen?
+
+ • Een grafiek krijgen: /chart BTCUSDT 48 1h 0.05
+ • Voorkeuren instellen: /preferences
+ • Signalen maken: /create_signal
+ • Tutorial herhalen: /tutorial
+
+ commands_list:
+ title: "Beschikbare commando's:"
+ content: |
+ /chart - Grafiek met indicatoren
+ /text_result - Tekstanalyse
+ /preferences - Indicatorvoorkeuren
+ /parameters - Indicatorinstellingen
+ /create_signal - Automatische signalen instellen
+ /manage_signals - Signalen beheren
+ /help - Hulp krijgen
+ /tutorial - Tutorial herhalen
+ /language - Taal wijzigen
+
+ happy_trading: "Veel succes met handelen! 📈\n\nGebruik /help voor hulp."
+
+signals:
+ manage_title: "Uw signalen beheren:"
+ no_signals: "Geen signalen gevonden"
+ add_new: "➕ Nieuw signaal toevoegen"
+ delete: "{pair} verwijderen"
+ deleted: "Signaal verwijderd. Bijgewerkte lijst:"
+ created: "Signaal aangemaakt! Huidige signalen:"
+ active_count: "U hebt {count} actieve signaal(en)."
+
+ add_prompt: |
+ Voer uw gewenste signaal in het formaat in:
+
+ `SYMBOOL INTERVAL [with_chart]`
+
+ Voorbeeld: `BTCUSDT 60` of `ETHUSDT 15 with_chart`
+
+ SYMBOOL moet een geldig handelspaar zijn (bijv. BTCUSDT).
+ INTERVAL moet in minuten zijn (bijv. 5, 15, 60).
+
+ invalid_input: "Ongeldige invoer: {error}. Probeer opnieuw of /cancel."
+ limit_reached: "U hebt het maximale aantal signalen bereikt ({limit})."
+
+ usage:
+ create: "Gebruik: /create_signal [], u hebt {count} argument(en) verzonden."
+ delete: "Gebruik: /delete_signal "
+
+ errors:
+ unexpected: "❌ Er is een onverwachte fout opgetreden."
+ invalid_symbol: "❌ Ongeldig symbool: {symbol}"
+
+preferences:
+ select_prompt: "Kies de indicatoren die u wilt opnemen:"
+ selected: "U hebt geselecteerd: {indicators}"
+ none_selected: "Geen"
+
+ indicators:
+ order_blocks: "Order Blocks"
+ fvgs: "FVGs"
+ liquidity_levels: "Liquiditeitsniveaus"
+ breaker_blocks: "Breaker Blocks"
+ liquidity_pools: "Liquiditeitspools"
+ show_legend: "Legenda tonen"
+ show_volume: "Volume tonen"
+ dark_mode: "Donkere modus"
+ light_mode: "Lichte modus"
+
+parameters:
+ configure_title: "Indicatorparameters configureren:"
+ saved: "Parameterinstellingen succesvol opgeslagen!"
+ value_set: "{param} ingesteld op {value}."
+
+ edit_prompt: |
+ Voer een nieuwe waarde in voor {param}:
+
+ Huidige waarde: {current}
+ Geldig bereik: {min} tot {max} (stap: {step})
+
+ Antwoord met een getal om de nieuwe waarde in te stellen.
+
+ errors:
+ invalid_number: "Ongeldige invoer. Voer een geldig getal in voor {param}."
+ out_of_range: "Waarde moet tussen {min} en {max} liggen. Probeer opnieuw."
+ no_session: "Sorry, ik heb geen actieve bewerkingssessie. Gebruik /parameters om opnieuw te beginnen."
+ process_failed: "Sorry, ik kon die actie niet verwerken. Probeer opnieuw met /parameters."
+
+help:
+ title: "CryptoBot Help"
+ main: |
+ Hier zijn de belangrijkste commando's die u kunt gebruiken:
+
+ /chart <symbool> <lengte> <interval> <tolerantie>
+ /text_result <symbool> <lengte> <interval> <tolerantie>
+ /history <symbool> <lengte> <interval> <tolerantie> <timestamp>
+ /preferences
+ /create_signal <symbool> <minuten> [<met_grafiek>]
+ /delete_signal <symbool>
+ /manage_signals
+ /sql <query>
+ /tables
+ /schema <tabelnaam>
+ /language
+ /help <commando>
+
+ Typ /help <commando> voor gedetailleerde hulp.
+
+ Voorbeeld: /help chart
+
+ Voor meer details, zie de README of neem contact op met de beheerder (@yarik_is_working).
+
+ commands:
+ parameters: |
+ /parameters
+ Open een interactief menu om indicatorparameters aan te passen.
+
+ chart: |
+ /chart <symbool> <lengte> <interval> <tolerantie>
+ Krijg een candlestick-grafiek met alle ingeschakelde indicatoren.
+
+ Voorbeeld: /chart BTCUSDT 48 1h 0.05
+
+ text_result: |
+ /text_result <symbool> <lengte> <interval> <tolerantie>
+ Krijg een tekstsamenvatting van alle gedetecteerde indicatoren.
+
+ Voorbeeld: /text_result ETHUSDT 24 15m 0.03
+
+ history: |
+ /history <symbool> <lengte> <interval> <tolerantie> <timestamp>
+ Krijg een candlestick-grafiek voor historische gegevens op een opgegeven timestamp.
+
+ Voorbeeld: /history BTCUSDT 48 1h 0.05 1700000000
+
+ preferences: |
+ /preferences
+ Open een interactief menu om indicatoren en grafiekopties te selecteren.
+
+ create_signal: |
+ /create_signal <symbool> <minuten> [<met_grafiek>]
+ Begin met het ontvangen van automatische signalen voor een paar met een bepaalde frequentie.
+
+ Voorbeeld: /create_signal BTCUSDT 60 true
+
+ delete_signal: |
+ /delete_signal <symbool>
+ Stop automatische signalen voor een paar.
+
+ manage_signals: |
+ /manage_signals
+ Open een interactief menu om uw signalen te bekijken, toe te voegen of te verwijderen.
+
+ help: |
+ /help
+ Toon het algemene helpbericht.
+
+ sql: |
+ /sql <query>
+ Voer een SQL-query uit op de database.
+
+ tables: |
+ /tables
+ Toon een lijst van alle beschikbare tabellen in de database.
+
+ schema: |
+ /schema <tabelnaam>
+ Toon het schema van een specifieke tabel.
+
+ language: |
+ /language
+ Wijzig de taal van de bot.
+
+errors:
+ network: "Netwerkfout opgetreden. Probeer het later opnieuw."
+ data_fetch: "Gegevens ophalen mislukt. Controleer het symbool en probeer opnieuw."
+ data_processing: "Fout bij gegevensverwerking. Probeer met andere parameters."
+ chart_generation: "Fout bij grafiekgeneratie. Probeer met andere instellingen."
+ invalid_input: "Ongeldige invoer. Controleer de commandosyntax."
+ database: "Databasebewerking mislukt. Probeer opnieuw."
+ timeout: "Time-out opgetreden. Probeer opnieuw."
+ api_limit: "API-limiet bereikt. Probeer het later opnieuw."
+ permission: "U hebt geen toestemming voor deze actie."
+ unknown: "Er is een onverwachte fout opgetreden. Probeer het later opnieuw."
+
+ global: |
+ ❌ Er is een onverwachte fout opgetreden bij het verwerken van uw verzoek. Probeer het later opnieuw.
+
+ Als u denkt dat dit een bug is, meld het hier: https://github.com/yariks5s/analysis_telegram_bot/issues
+
+ preferences_failed: "Voorkeuren ophalen mislukt. Stel ze opnieuw in via /preferences."
+ analyze_failed: "Gegevensanalyse mislukt. Controleer uw invoer en probeer opnieuw."
+ chart_send_failed: "Grafiek verzenden mislukt. Probeer opnieuw."
+ signal_generation_failed: "Prijsvoorspellingssignaal kon niet worden gegenereerd. Grafiek verzonden zonder analyse."
+ analysis_not_available: "Analyse niet beschikbaar"
+ invalid_timestamp: "❌ Ongeldige timestamp. Moet een Unix-timestamp in het verleden zijn."
+ historical_usage: "Ongeldige parameters. Gebruik: /history "
+
+rate_limit:
+ title: "**Limietstatus**"
+ remaining: "• Resterend: {remaining}/{limit} verzoeken{suffix}"
+ reset_in: "• Reset in: {seconds} seconden"
+ reduced_notice: "ℹ️ Uw limiet is tijdelijk verlaagd vanwege gedetecteerde hoogfrequente activiteit. Dit zal automatisch normaliseren na een afkoelperiode."
+ footer: "Het systeem gebruikt limieten om eerlijk API-gebruik voor alle gebruikers te garanderen."
+ suspicious_suffix: " (verlaagd vanwege verdachte activiteit)"
+
+database:
+ sql:
+ no_query: |
+ Geef een SQL-query op.
+ Gebruik: /sql
+ Voorbeeld: /sql SELECT * FROM trades LIMIT 5
+ results: "Queryresultaten:\n\n{results}"
+ success_no_results: "Query succesvol uitgevoerd. Geen resultaten geretourneerd."
+ error: "❌ Fout bij uitvoeren query: {error}"
+
+ tables:
+ title: "Beschikbare tabellen:\n\n"
+ empty: "Geen tabellen gevonden in de database."
+ error: "❌ Fout: {error}"
+
+ schema:
+ no_table: |
+ Geef een tabelnaam op.
+ Gebruik: /schema
+ Voorbeeld: /schema trades
+ title: "Schema voor tabel '{table}':\n\n"
+ not_found: "Geen schema gevonden voor tabel '{table}'."
+ error: "❌ Fout: {error}"
+
+history:
+ title: "📜 *Uw signaalgeschiedenis*"
+ filter_prompt: "\n\nFilter op tijdsperiode of valutapaar:"
+ no_signals: "Geen signaalgeschiedenis gevonden. Signalen worden geregistreerd wanneer ze worden gegenereerd."
+
+ periods:
+ last_24h: "Laatste 24u"
+ last_7d: "Laatste 7d"
+ last_30d: "Laatste 30d"
+
+ filtered_title: "📜 *Signaalgeschiedenis - Laatste {period}*"
+ pair_title: "📜 *Signaalgeschiedenis - {pair}*"
+
+ no_signals_period: "Geen signalen gevonden in de laatste {period}."
+ no_signals_pair: "Geen signalen gevonden voor {pair}."
+
+ show_all: "Alles tonen"
+ showing_recent: "_Toont {count} meest recente signalen._"
+
+ export:
+ csv: "CSV exporteren"
+ json: "JSON exporteren"
+ preparing: "{format}-export voorbereiden..."
+ no_data: "Geen signaalgegevens {description} om te exporteren."
+ caption: "📊 Signaalgeschiedenis {description} geëxporteerd als {format}"
+ for_pair: "voor {pair}"
+ for_all: "voor alle paren"
+ error: "Signaalgeschiedenis exporteren als {format} mislukt"
+
+ update_failed: "Bericht kon niet worden bijgewerkt"
+
diff --git a/src/i18n/locales/ru.yaml b/src/i18n/locales/ru.yaml
new file mode 100644
index 0000000..7199f78
--- /dev/null
+++ b/src/i18n/locales/ru.yaml
@@ -0,0 +1,408 @@
+# Russian translations for CryptoBot
+# Русские переводы для CryptoBot
+
+common:
+ done: "Готово"
+ cancel: "Отмена"
+ save: "Сохранить"
+ back: "Назад"
+ continue: "Продолжить"
+ skip: "Пропустить"
+ finish: "Завершить"
+ yes: "Да"
+ no: "Нет"
+ error: "Ошибка"
+ success: "Успешно"
+ loading: "Загрузка..."
+ no_data: "Нет данных"
+
+language:
+ select_prompt: "🌐 Выберите предпочтительный язык:"
+ changed: "✅ Язык изменён на Русский"
+ current: "Текущий язык: Русский 🇷🇺"
+
+tutorial:
+ welcome:
+ title: "🎉 Добро пожаловать в CryptoBot!"
+ content: |
+ Я ваш персональный помощник для анализа криптовалютного рынка. Я помогу вам анализировать графики криптовалют с продвинутыми техническими индикаторами.
+
+ Это краткое обучение покажет вам, как использовать мои основные функции. Вы можете пропустить обучение или вернуться к нему в любое время с помощью команды /tutorial.
+
+ chart:
+ title: "📊 Анализ графика"
+ content: |
+ Основная команда для получения анализа графика:
+
+ /chart СИМВОЛ ДЛИНА ИНТЕРВАЛ ДОПУСК
+
+ Например: /chart BTCUSDT 48 1h 0.05
+
+ Это даст вам 48-свечный часовой график для BTC/USDT с допуском 0.05 для определения уровней ликвидности.
+
+ Попробуйте или продолжите, чтобы узнать больше!
+
+ preferences:
+ title: "⚙️ Настройка индикаторов"
+ content: |
+ Вы можете настроить, какие индикаторы отображаются на графиках, с помощью команды /preferences.
+
+ Откроется интерактивное меню, где вы можете включить или отключить:
+ • Ордер-блоки
+ • Зоны справедливой стоимости (FVG)
+ • Уровни ликвидности
+ • Брейкер-блоки
+ • И изменить настройки отображения
+
+ Ваши настройки будут сохранены для будущих запросов.
+
+ signals:
+ title: "🔔 Автоматические сигналы"
+ content: |
+ Хотите получать автоматические уведомления? Настройте сигнальные оповещения с помощью:
+
+ /create_signal СИМВОЛ МИНУТЫ [ГРАФИК]
+
+ Например: /create_signal BTCUSDT 60 true
+
+ Это будет отправлять вам уведомление с графиком каждые 60 минут для BTC/USDT.
+
+ Управлять сигналами можно командой /manage_signals.
+
+ parameters:
+ title: "🔧 Расширенные параметры"
+ content: |
+ Тонкая настройка параметров индикаторов через команду /parameters.
+
+ Вы можете настроить:
+ • Период ATR: для расчёта среднего истинного диапазона
+ • Мин. размер FVG: минимальный размер зон справедливой стоимости
+
+ Эти настройки влияют на расчёт и отображение индикаторов.
+
+ completed:
+ title: "🚀 Обучение завершено!"
+ content: |
+ Отлично! Вы завершили обучение CryptoBot.
+
+ Помните, вы всегда можете получить помощь:
+ • /help - Общая справка
+ • /help КОМАНДА - Справка по конкретной команде
+
+ Готовы начать? Попробуйте /chart BTCUSDT 48 1h 0.05 для анализа BTC/USDT!
+
+ buttons:
+ continue: "Продолжить"
+ skip_tutorial: "Пропустить обучение"
+ skip_to_end: "Пропустить до конца"
+ complete: "Завершить обучение"
+ view_commands: "Показать команды"
+ back_to_tutorial: "Назад к обучению"
+
+ welcome_back:
+ title: "С возвращением в CryptoBot!"
+ content: |
+ Вы уже завершили обучение. Что хотите сделать?
+
+ • Получить график: /chart BTCUSDT 48 1h 0.05
+ • Настроить предпочтения: /preferences
+ • Создать сигналы: /create_signal
+ • Повторить обучение: /tutorial
+
+ commands_list:
+ title: "Доступные команды:"
+ content: |
+ /chart - Получить график с индикаторами
+ /text_result - Получить текстовый анализ
+ /preferences - Настроить предпочтения индикаторов
+ /parameters - Тонкая настройка индикаторов
+ /create_signal - Настроить автоматические сигналы
+ /manage_signals - Управление сигналами
+ /help - Получить справку
+ /tutorial - Повторить обучение
+ /language - Сменить язык
+
+ happy_trading: "Удачной торговли! 📈\n\nИспользуйте /help для получения помощи."
+
+signals:
+ manage_title: "Управление сигналами:"
+ no_signals: "Сигналы не найдены"
+ add_new: "➕ Добавить сигнал"
+ delete: "Удалить {pair}"
+ deleted: "Сигнал удалён. Обновлённый список:"
+ created: "Сигнал создан! Текущие сигналы:"
+ active_count: "У вас {count} активных сигналов."
+
+ add_prompt: |
+ Введите желаемый сигнал в формате:
+
+ `СИМВОЛ ИНТЕРВАЛ [with_chart]`
+
+ Пример: `BTCUSDT 60` или `ETHUSDT 15 with_chart`
+
+ СИМВОЛ должен быть валидной торговой парой (например, BTCUSDT).
+ ИНТЕРВАЛ должен быть в минутах (например, 5, 15, 60).
+
+ invalid_input: "Неверный ввод: {error}. Попробуйте снова или /cancel."
+ limit_reached: "Вы достигли максимального количества сигналов ({limit})."
+
+ usage:
+ create: "Использование: /create_signal <символ> <период_в_минутах> [<с_графиком>], вы отправили {count} аргумент(ов)."
+ delete: "Использование: /delete_signal <символ>"
+
+ errors:
+ unexpected: "❌ Произошла непредвиденная ошибка."
+ invalid_symbol: "❌ Неверный символ: {symbol}"
+
+preferences:
+ select_prompt: "Выберите индикаторы для отображения:"
+ selected: "Вы выбрали: {indicators}"
+ none_selected: "Ничего"
+
+ indicators:
+ order_blocks: "Ордер-блоки"
+ fvgs: "FVG"
+ liquidity_levels: "Уровни ликвидности"
+ breaker_blocks: "Брейкер-блоки"
+ liquidity_pools: "Пулы ликвидности"
+ show_legend: "Показать легенду"
+ show_volume: "Показать объём"
+ dark_mode: "Тёмная тема"
+ light_mode: "Светлая тема"
+
+parameters:
+ configure_title: "Настройка параметров индикаторов:"
+ saved: "Настройки параметров сохранены!"
+ value_set: "{param} установлен на {value}."
+
+ edit_prompt: |
+ Введите новое значение для {param}:
+
+ Текущее значение: {current}
+ Допустимый диапазон: от {min} до {max} (шаг: {step})
+
+ Ответьте числом, чтобы установить новое значение.
+
+ errors:
+ invalid_number: "Неверный ввод. Пожалуйста, введите число для {param}."
+ out_of_range: "Значение должно быть от {min} до {max}. Попробуйте снова."
+ no_session: "Извините, нет активной сессии редактирования. Используйте /parameters для начала."
+ process_failed: "Извините, не удалось обработать это действие. Попробуйте снова с /parameters."
+
+help:
+ title: "Справка CryptoBot"
+ main: |
+ Вот основные команды:
+
+ /chart <символ> <длина> <интервал> <допуск>
+ /text_result <символ> <длина> <интервал> <допуск>
+ /history <символ> <длина> <интервал> <допуск> <время>
+ /preferences
+ /create_signal <символ> <минуты> [<с_графиком>]
+ /delete_signal <символ>
+ /manage_signals
+ /sql <запрос>
+ /tables
+ /schema <имя_таблицы>
+ /language
+ /help <команда>
+
+ Введите /help <команда> для подробной справки.
+
+ Пример: /help chart
+
+ Для подробностей см. README или свяжитесь с разработчиком (@yarik_is_working).
+
+ commands:
+ parameters: |
+ /parameters
+ Открыть интерактивное меню для настройки параметров индикаторов.
+
+ Доступные параметры:
+ - Период ATR: Настройка периода для расчёта среднего истинного диапазона (1-50)
+ - Мин. размер FVG: Установка минимального соотношения размера для зон справедливой стоимости (0.0001-0.01)
+
+ Нажмите на любой параметр, чтобы изменить его значение.
+
+ chart: |
+ /chart <символ> <длина> <интервал> <допуск>
+ Получить свечной график со всеми включёнными индикаторами.
+
+ - символ: Торговая пара, например BTCUSDT
+ - длина: Количество свечей для анализа (например, 48)
+ - интервал: Интервал свечей (например, 1h, 15m)
+ - допуск: Чувствительность для определения уровней ликвидности (0-1)
+
+ Пример: /chart BTCUSDT 48 1h 0.05
+
+ text_result: |
+ /text_result <символ> <длина> <интервал> <допуск>
+ Получить текстовую сводку всех обнаруженных индикаторов.
+
+ Пример: /text_result ETHUSDT 24 15m 0.03
+
+ history: |
+ /history <символ> <длина> <интервал> <допуск> <время>
+ Получить свечной график для исторических данных на указанную метку времени.
+
+ - время: Метка времени Unix в секундах
+
+ Пример: /history BTCUSDT 48 1h 0.05 1700000000
+
+ preferences: |
+ /preferences
+ Открыть интерактивное меню для выбора индикаторов и настроек графика.
+
+ Доступные опции:
+ - Технические индикаторы (Ордер-блоки, FVG, Уровни ликвидности и т.д.)
+ - Настройки отображения (Показать легенду, Показать объём)
+ - Выбор темы (Светлая/Тёмная)
+
+ create_signal: |
+ /create_signal <символ> <минуты> [<с_графиком>]
+ Начать получать автоматические сигналы для пары с заданной частотой (в минутах).
+
+ Пример: /create_signal BTCUSDT 60 true
+
+ Максимум 10 активных сигналов на пользователя.
+
+ delete_signal: |
+ /delete_signal <символ>
+ Остановить автоматические сигналы для пары.
+
+ Пример: /delete_signal BTCUSDT
+
+ manage_signals: |
+ /manage_signals
+ Открыть интерактивное меню для просмотра, добавления или удаления сигналов.
+
+ Максимум 10 активных сигналов на пользователя.
+
+ help: |
+ /help
+ Показать общее сообщение справки.
+
+ /help <команда>
+ Показать подробную справку для конкретной команды.
+
+ Пример: /help chart
+
+ sql: |
+ /sql <запрос>
+ Выполнить SQL-запрос к базе данных.
+
+ Примеры:
+ - /sql SELECT * FROM trades LIMIT 5
+
+ Результаты ограничены 50 строками.
+
+ tables: |
+ /tables
+ Показать список всех доступных таблиц в базе данных.
+
+ schema: |
+ /schema <имя_таблицы>
+ Показать структуру конкретной таблицы.
+
+ Пример: /schema trades
+
+ language: |
+ /language
+ Изменить язык бота.
+
+ Поддерживаемые языки:
+ - 🇬🇧 English
+ - 🇷🇺 Русский
+ - 🇺🇦 Українська
+ - 🇩🇪 Deutsch
+ - 🇫🇷 Français
+ - 🇳🇱 Nederlands
+
+errors:
+ network: "Ошибка сети. Пожалуйста, попробуйте позже."
+ data_fetch: "Не удалось получить данные. Проверьте символ и попробуйте снова."
+ data_processing: "Ошибка обработки данных. Попробуйте с другими параметрами."
+ chart_generation: "Ошибка генерации графика. Попробуйте с другими настройками."
+ invalid_input: "Неверный ввод. Проверьте синтаксис команды."
+ database: "Ошибка базы данных. Попробуйте снова."
+ timeout: "Время операции истекло. Попробуйте снова."
+ api_limit: "Достигнут лимит API. Попробуйте позже."
+ permission: "У вас нет разрешения на это действие."
+ unknown: "Произошла непредвиденная ошибка. Попробуйте позже."
+
+ global: |
+ ❌ При обработке вашего запроса произошла непредвиденная ошибка. Попробуйте позже.
+
+ Если вы считаете это ошибкой, сообщите здесь: https://github.com/yariks5s/analysis_telegram_bot/issues
+
+ preferences_failed: "Не удалось получить настройки. Попробуйте установить их снова через /preferences."
+ analyze_failed: "Не удалось проанализировать данные. Проверьте ввод и попробуйте снова."
+ chart_send_failed: "Не удалось отправить график. Попробуйте снова."
+ signal_generation_failed: "Не удалось сгенерировать сигнал прогноза цены. Отправка графика без анализа."
+ analysis_not_available: "Анализ недоступен"
+ invalid_timestamp: "❌ Неверная метка времени. Должна быть метка Unix в прошлом."
+ historical_usage: "Неверные параметры. Использование: /history <символ> <длина> <интервал> <допуск> <время>"
+
+rate_limit:
+ title: "**Статус ограничения запросов**"
+ remaining: "• Осталось: {remaining}/{limit} запросов{suffix}"
+ reset_in: "• Сброс через: {seconds} секунд"
+ reduced_notice: "ℹ️ Ваш лимит запросов временно снижен из-за высокой активности. Он автоматически восстановится после периода ожидания."
+ footer: "Система использует ограничение запросов для обеспечения справедливого использования API."
+ suspicious_suffix: " (снижен из-за подозрительной активности)"
+
+database:
+ sql:
+ no_query: |
+ Пожалуйста, укажите SQL-запрос.
+ Использование: /sql <запрос>
+ Пример: /sql SELECT * FROM trades LIMIT 5
+ results: "Результаты запроса:\n\n{results}"
+ success_no_results: "Запрос выполнен успешно. Результатов нет."
+ error: "❌ Ошибка выполнения запроса: {error}"
+
+ tables:
+ title: "Доступные таблицы:\n\n"
+ empty: "Таблицы в базе данных не найдены."
+ error: "❌ Ошибка: {error}"
+
+ schema:
+ no_table: |
+ Пожалуйста, укажите имя таблицы.
+ Использование: /schema <имя_таблицы>
+ Пример: /schema trades
+ title: "Схема таблицы '{table}':\n\n"
+ not_found: "Схема для таблицы '{table}' не найдена."
+ error: "❌ Ошибка: {error}"
+
+history:
+ title: "📜 *История ваших сигналов*"
+ filter_prompt: "\n\nФильтр по периоду или валютной паре:"
+ no_signals: "История сигналов не найдена. Сигналы будут записываться при их генерации."
+
+ periods:
+ last_24h: "За 24ч"
+ last_7d: "За 7д"
+ last_30d: "За 30д"
+
+ filtered_title: "📜 *История сигналов - За {period}*"
+ pair_title: "📜 *История сигналов - {pair}*"
+
+ no_signals_period: "Сигналов за последние {period} не найдено."
+ no_signals_pair: "Сигналов для {pair} не найдено."
+
+ show_all: "Показать все"
+ showing_recent: "_Показано {count} последних сигналов._"
+
+ export:
+ csv: "Экспорт CSV"
+ json: "Экспорт JSON"
+ preparing: "Подготовка экспорта {format}..."
+ no_data: "Нет данных сигналов {description} для экспорта."
+ caption: "📊 История сигналов {description} экспортирована как {format}"
+ for_pair: "для {pair}"
+ for_all: "для всех пар"
+ error: "Не удалось экспортировать историю сигналов как {format}"
+
+ update_failed: "Не удалось обновить сообщение"
+
diff --git a/src/i18n/locales/uk.yaml b/src/i18n/locales/uk.yaml
new file mode 100644
index 0000000..e547f15
--- /dev/null
+++ b/src/i18n/locales/uk.yaml
@@ -0,0 +1,364 @@
+# Ukrainian translations for CryptoBot
+# Українські переклади для CryptoBot
+
+common:
+ done: "Готово"
+ cancel: "Скасувати"
+ save: "Зберегти"
+ back: "Назад"
+ continue: "Продовжити"
+ skip: "Пропустити"
+ finish: "Завершити"
+ yes: "Так"
+ no: "Ні"
+ error: "Помилка"
+ success: "Успішно"
+ loading: "Завантаження..."
+ no_data: "Немає даних"
+
+language:
+ select_prompt: "🌐 Оберіть бажану мову:"
+ changed: "✅ Мову змінено на Українську"
+ current: "Поточна мова: Українська 🇺🇦"
+
+tutorial:
+ welcome:
+ title: "🎉 Ласкаво просимо до CryptoBot!"
+ content: |
+ Я ваш персональний помічник для аналізу криптовалютного ринку. Я допоможу вам аналізувати графіки криптовалют з просунутими технічними індикаторами.
+
+ Це коротке навчання покаже вам, як використовувати мої основні функції. Ви можете пропустити навчання або повернутися до нього будь-коли за допомогою команди /tutorial.
+
+ chart:
+ title: "📊 Аналіз графіка"
+ content: |
+ Основна команда для отримання аналізу графіка:
+
+ /chart СИМВОЛ ДОВЖИНА ІНТЕРВАЛ ДОПУСК
+
+ Наприклад: /chart BTCUSDT 48 1h 0.05
+
+ Це дасть вам 48-свічковий годинний графік для BTC/USDT з допуском 0.05 для визначення рівнів ліквідності.
+
+ Спробуйте або продовжуйте, щоб дізнатися більше!
+
+ preferences:
+ title: "⚙️ Налаштування індикаторів"
+ content: |
+ Ви можете налаштувати, які індикатори відображаються на графіках, за допомогою команди /preferences.
+
+ Відкриється інтерактивне меню, де ви можете увімкнути або вимкнути:
+ • Ордер-блоки
+ • Зони справедливої вартості (FVG)
+ • Рівні ліквідності
+ • Брейкер-блоки
+ • І змінити налаштування відображення
+
+ Ваші налаштування будуть збережені для майбутніх запитів.
+
+ signals:
+ title: "🔔 Автоматичні сигнали"
+ content: |
+ Хочете отримувати автоматичні сповіщення? Налаштуйте сигнальні оповіщення за допомогою:
+
+ /create_signal СИМВОЛ ХВИЛИНИ [ГРАФІК]
+
+ Наприклад: /create_signal BTCUSDT 60 true
+
+ Це надсилатиме вам сповіщення з графіком кожні 60 хвилин для BTC/USDT.
+
+ Керувати сигналами можна командою /manage_signals.
+
+ parameters:
+ title: "🔧 Розширені параметри"
+ content: |
+ Тонке налаштування параметрів індикаторів через команду /parameters.
+
+ Ви можете налаштувати:
+ • Період ATR: для розрахунку середнього істинного діапазону
+ • Мін. розмір FVG: мінімальний розмір зон справедливої вартості
+
+ Ці налаштування впливають на розрахунок і відображення індикаторів.
+
+ completed:
+ title: "🚀 Навчання завершено!"
+ content: |
+ Чудово! Ви завершили навчання CryptoBot.
+
+ Пам'ятайте, ви завжди можете отримати допомогу:
+ • /help - Загальна довідка
+ • /help КОМАНДА - Довідка по конкретній команді
+
+ Готові почати? Спробуйте /chart BTCUSDT 48 1h 0.05 для аналізу BTC/USDT!
+
+ buttons:
+ continue: "Продовжити"
+ skip_tutorial: "Пропустити навчання"
+ skip_to_end: "Пропустити до кінця"
+ complete: "Завершити навчання"
+ view_commands: "Показати команди"
+ back_to_tutorial: "Назад до навчання"
+
+ welcome_back:
+ title: "З поверненням до CryptoBot!"
+ content: |
+ Ви вже завершили навчання. Що хочете зробити?
+
+ • Отримати графік: /chart BTCUSDT 48 1h 0.05
+ • Налаштувати вподобання: /preferences
+ • Створити сигнали: /create_signal
+ • Повторити навчання: /tutorial
+
+ commands_list:
+ title: "Доступні команди:"
+ content: |
+ /chart - Отримати графік з індикаторами
+ /text_result - Отримати текстовий аналіз
+ /preferences - Налаштувати вподобання індикаторів
+ /parameters - Тонке налаштування індикаторів
+ /create_signal - Налаштувати автоматичні сигнали
+ /manage_signals - Керування сигналами
+ /help - Отримати довідку
+ /tutorial - Повторити навчання
+ /language - Змінити мову
+
+ happy_trading: "Вдалої торгівлі! 📈\n\nВикористовуйте /help для отримання допомоги."
+
+signals:
+ manage_title: "Керування сигналами:"
+ no_signals: "Сигнали не знайдені"
+ add_new: "➕ Додати сигнал"
+ delete: "Видалити {pair}"
+ deleted: "Сигнал видалено. Оновлений список:"
+ created: "Сигнал створено! Поточні сигнали:"
+ active_count: "У вас {count} активних сигналів."
+
+ add_prompt: |
+ Введіть бажаний сигнал у форматі:
+
+ `СИМВОЛ ІНТЕРВАЛ [with_chart]`
+
+ Приклад: `BTCUSDT 60` або `ETHUSDT 15 with_chart`
+
+ СИМВОЛ має бути валідною торговою парою (наприклад, BTCUSDT).
+ ІНТЕРВАЛ має бути в хвилинах (наприклад, 5, 15, 60).
+
+ invalid_input: "Невірний ввід: {error}. Спробуйте знову або /cancel."
+ limit_reached: "Ви досягли максимальної кількості сигналів ({limit})."
+
+ usage:
+ create: "Використання: /create_signal <символ> <період_в_хвилинах> [<з_графіком>], ви надіслали {count} аргумент(ів)."
+ delete: "Використання: /delete_signal <символ>"
+
+ errors:
+ unexpected: "❌ Сталася непередбачена помилка."
+ invalid_symbol: "❌ Невірний символ: {symbol}"
+
+preferences:
+ select_prompt: "Оберіть індикатори для відображення:"
+ selected: "Ви обрали: {indicators}"
+ none_selected: "Нічого"
+
+ indicators:
+ order_blocks: "Ордер-блоки"
+ fvgs: "FVG"
+ liquidity_levels: "Рівні ліквідності"
+ breaker_blocks: "Брейкер-блоки"
+ liquidity_pools: "Пули ліквідності"
+ show_legend: "Показати легенду"
+ show_volume: "Показати об'єм"
+ dark_mode: "Темна тема"
+ light_mode: "Світла тема"
+
+parameters:
+ configure_title: "Налаштування параметрів індикаторів:"
+ saved: "Налаштування параметрів збережено!"
+ value_set: "{param} встановлено на {value}."
+
+ edit_prompt: |
+ Введіть нове значення для {param}:
+
+ Поточне значення: {current}
+ Допустимий діапазон: від {min} до {max} (крок: {step})
+
+ Відповідайте числом, щоб встановити нове значення.
+
+ errors:
+ invalid_number: "Невірний ввід. Будь ласка, введіть число для {param}."
+ out_of_range: "Значення має бути від {min} до {max}. Спробуйте знову."
+ no_session: "Вибачте, немає активної сесії редагування. Використовуйте /parameters для початку."
+ process_failed: "Вибачте, не вдалося обробити цю дію. Спробуйте знову з /parameters."
+
+help:
+ title: "Довідка CryptoBot"
+ main: |
+ Ось основні команди:
+
+ /chart <символ> <довжина> <інтервал> <допуск>
+ /text_result <символ> <довжина> <інтервал> <допуск>
+ /history <символ> <довжина> <інтервал> <допуск> <час>
+ /preferences
+ /create_signal <символ> <хвилини> [<з_графіком>]
+ /delete_signal <символ>
+ /manage_signals
+ /sql <запит>
+ /tables
+ /schema <назва_таблиці>
+ /language
+ /help <команда>
+
+ Введіть /help <команда> для детальної довідки.
+
+ Приклад: /help chart
+
+ Для деталей див. README або зв'яжіться з розробником (@yarik_is_working).
+
+ commands:
+ parameters: |
+ /parameters
+ Відкрити інтерактивне меню для налаштування параметрів індикаторів.
+
+ chart: |
+ /chart <символ> <довжина> <інтервал> <допуск>
+ Отримати свічковий графік з усіма увімкненими індикаторами.
+
+ Приклад: /chart BTCUSDT 48 1h 0.05
+
+ text_result: |
+ /text_result <символ> <довжина> <інтервал> <допуск>
+ Отримати текстовий звіт всіх виявлених індикаторів.
+
+ Приклад: /text_result ETHUSDT 24 15m 0.03
+
+ history: |
+ /history <символ> <довжина> <інтервал> <допуск> <час>
+ Отримати свічковий графік для історичних даних.
+
+ Приклад: /history BTCUSDT 48 1h 0.05 1700000000
+
+ preferences: |
+ /preferences
+ Відкрити інтерактивне меню для вибору індикаторів та налаштувань графіка.
+
+ create_signal: |
+ /create_signal <символ> <хвилини> [<з_графіком>]
+ Почати отримувати автоматичні сигнали для пари із заданою частотою.
+
+ Приклад: /create_signal BTCUSDT 60 true
+
+ delete_signal: |
+ /delete_signal <символ>
+ Зупинити автоматичні сигнали для пари.
+
+ manage_signals: |
+ /manage_signals
+ Відкрити інтерактивне меню для перегляду, додавання або видалення сигналів.
+
+ help: |
+ /help
+ Показати загальне повідомлення довідки.
+
+ sql: |
+ /sql <запит>
+ Виконати SQL-запит до бази даних.
+
+ tables: |
+ /tables
+ Показати список усіх доступних таблиць у базі даних.
+
+ schema: |
+ /schema <назва_таблиці>
+ Показати структуру конкретної таблиці.
+
+ language: |
+ /language
+ Змінити мову бота.
+
+errors:
+ network: "Помилка мережі. Будь ласка, спробуйте пізніше."
+ data_fetch: "Не вдалося отримати дані. Перевірте символ і спробуйте знову."
+ data_processing: "Помилка обробки даних. Спробуйте з іншими параметрами."
+ chart_generation: "Помилка генерації графіка. Спробуйте з іншими налаштуваннями."
+ invalid_input: "Невірний ввід. Перевірте синтаксис команди."
+ database: "Помилка бази даних. Спробуйте знову."
+ timeout: "Час операції вичерпано. Спробуйте знову."
+ api_limit: "Досягнуто ліміт API. Спробуйте пізніше."
+ permission: "У вас немає дозволу на цю дію."
+ unknown: "Сталася непередбачена помилка. Спробуйте пізніше."
+
+ global: |
+ ❌ При обробці вашого запиту сталася непередбачена помилка. Спробуйте пізніше.
+
+ Якщо ви вважаєте це помилкою, повідомте тут: https://github.com/yariks5s/analysis_telegram_bot/issues
+
+ preferences_failed: "Не вдалося отримати налаштування. Спробуйте встановити їх знову через /preferences."
+ analyze_failed: "Не вдалося проаналізувати дані. Перевірте ввід і спробуйте знову."
+ chart_send_failed: "Не вдалося надіслати графік. Спробуйте знову."
+ signal_generation_failed: "Не вдалося згенерувати сигнал прогнозу ціни. Надсилання графіка без аналізу."
+ analysis_not_available: "Аналіз недоступний"
+ invalid_timestamp: "❌ Невірна мітка часу. Має бути мітка Unix у минулому."
+ historical_usage: "Невірні параметри. Використання: /history <символ> <довжина> <інтервал> <допуск> <час>"
+
+rate_limit:
+ title: "**Статус обмеження запитів**"
+ remaining: "• Залишилось: {remaining}/{limit} запитів{suffix}"
+ reset_in: "• Скидання через: {seconds} секунд"
+ reduced_notice: "ℹ️ Ваш ліміт запитів тимчасово знижено через високу активність. Він автоматично відновиться після періоду очікування."
+ footer: "Система використовує обмеження запитів для забезпечення справедливого використання API."
+ suspicious_suffix: " (знижено через підозрілу активність)"
+
+database:
+ sql:
+ no_query: |
+ Будь ласка, вкажіть SQL-запит.
+ Використання: /sql <запит>
+ Приклад: /sql SELECT * FROM trades LIMIT 5
+ results: "Результати запиту:\n\n{results}"
+ success_no_results: "Запит виконано успішно. Результатів немає."
+ error: "❌ Помилка виконання запиту: {error}"
+
+ tables:
+ title: "Доступні таблиці:\n\n"
+ empty: "Таблиці в базі даних не знайдені."
+ error: "❌ Помилка: {error}"
+
+ schema:
+ no_table: |
+ Будь ласка, вкажіть назву таблиці.
+ Використання: /schema <назва_таблиці>
+ Приклад: /schema trades
+ title: "Схема таблиці '{table}':\n\n"
+ not_found: "Схема для таблиці '{table}' не знайдена."
+ error: "❌ Помилка: {error}"
+
+history:
+ title: "📜 *Історія ваших сигналів*"
+ filter_prompt: "\n\nФільтр за періодом або валютною парою:"
+ no_signals: "Історія сигналів не знайдена. Сигнали будуть записуватися при їх генерації."
+
+ periods:
+ last_24h: "За 24г"
+ last_7d: "За 7д"
+ last_30d: "За 30д"
+
+ filtered_title: "📜 *Історія сигналів - За {period}*"
+ pair_title: "📜 *Історія сигналів - {pair}*"
+
+ no_signals_period: "Сигналів за останні {period} не знайдено."
+ no_signals_pair: "Сигналів для {pair} не знайдено."
+
+ show_all: "Показати все"
+ showing_recent: "_Показано {count} останніх сигналів._"
+
+ export:
+ csv: "Експорт CSV"
+ json: "Експорт JSON"
+ preparing: "Підготовка експорту {format}..."
+ no_data: "Немає даних сигналів {description} для експорту."
+ caption: "📊 Історія сигналів {description} експортована як {format}"
+ for_pair: "для {pair}"
+ for_all: "для всіх пар"
+ error: "Не вдалося експортувати історію сигналів як {format}"
+
+ update_failed: "Не вдалося оновити повідомлення"
+
diff --git a/src/telegram/commands/db_commands.py b/src/telegram/commands/db_commands.py
index 3258cc4..4ff03e9 100644
--- a/src/telegram/commands/db_commands.py
+++ b/src/telegram/commands/db_commands.py
@@ -3,6 +3,8 @@
from back_tester.db_operations import ClickHouseDB
import logging
+from src.i18n import t, get_user_language
+
logger = logging.getLogger(__name__)
# Initialize database connection
@@ -15,12 +17,11 @@ async def execute_sql_command(update: Update, context: ContextTypes.DEFAULT_TYPE
Usage: /sql
Example: /sql SELECT * FROM trades LIMIT 5
"""
+ user_id = update.effective_user.id
+ lang = get_user_language(user_id)
+
if not context.args:
- await update.message.reply_text(
- "Please provide a SQL query.\n"
- "Usage: /sql \n"
- "Example: /sql SELECT * FROM trades LIMIT 5"
- )
+ await update.message.reply_text(t("database.sql.no_query", lang))
return
# Join all arguments to form the complete query
@@ -39,9 +40,7 @@ async def execute_sql_command(update: Update, context: ContextTypes.DEFAULT_TYPE
return
if not result:
- await update.message.reply_text(
- "Query executed successfully. No results returned."
- )
+ await update.message.reply_text(t("database.sql.success_no_results", lang))
return
# Convert result to string
@@ -54,14 +53,14 @@ async def execute_sql_command(update: Update, context: ContextTypes.DEFAULT_TYPE
formatted_result = str(result)
# Format the message
- message = f"Query Results:\n\n{formatted_result}"
+ message = t("database.sql.results", lang, results=formatted_result)
# Send the formatted message
await update.message.reply_text(message)
except Exception as e:
logger.error(f"Error executing SQL query: {str(e)}")
- await update.message.reply_text(f"❌ Error executing query: {str(e)}")
+ await update.message.reply_text(t("database.sql.error", lang, error=str(e)))
async def show_tables_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
@@ -69,17 +68,20 @@ async def show_tables_command(update: Update, context: ContextTypes.DEFAULT_TYPE
Command handler to show available tables.
Usage: /tables
"""
+ user_id = update.effective_user.id
+ lang = get_user_language(user_id)
+
try:
tables = db.get_available_tables()
if isinstance(tables, str): # Error occurred
- await update.message.reply_text(f"❌ Error: {tables}")
+ await update.message.reply_text(t("database.tables.error", lang, error=tables))
return
if not tables:
- await update.message.reply_text("No tables found in the database.")
+ await update.message.reply_text(t("database.tables.empty", lang))
return
- message = "Available tables:\n\n"
+ message = t("database.tables.title", lang)
for table in tables:
message += f"• {table}\n"
@@ -87,7 +89,7 @@ async def show_tables_command(update: Update, context: ContextTypes.DEFAULT_TYPE
except Exception as e:
logger.error(f"Error getting tables: {str(e)}")
- await update.message.reply_text(f"❌ Error: {str(e)}")
+ await update.message.reply_text(t("database.tables.error", lang, error=str(e)))
async def describe_table_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
@@ -96,12 +98,11 @@ async def describe_table_command(update: Update, context: ContextTypes.DEFAULT_T
Usage: /schema
Example: /schema trades
"""
+ user_id = update.effective_user.id
+ lang = get_user_language(user_id)
+
if not context.args:
- await update.message.reply_text(
- "Please provide a table name.\n"
- "Usage: /schema \n"
- "Example: /schema trades"
- )
+ await update.message.reply_text(t("database.schema.no_table", lang))
return
table_name = context.args[0]
@@ -109,16 +110,14 @@ async def describe_table_command(update: Update, context: ContextTypes.DEFAULT_T
try:
schema = db.get_table_schema(table_name)
if isinstance(schema, str): # Error occurred
- await update.message.reply_text(f"❌ Error: {schema}")
+ await update.message.reply_text(t("database.schema.error", lang, error=schema))
return
if not schema:
- await update.message.reply_text(
- f"No schema found for table '{table_name}'."
- )
+ await update.message.reply_text(t("database.schema.not_found", lang, table=table_name))
return
- message = f"Schema for table '{table_name}':\n\n"
+ message = t("database.schema.title", lang, table=table_name)
for column in schema:
message += f"• {column['name']}: {column['type']}\n"
if column["default"]:
@@ -128,4 +127,4 @@ async def describe_table_command(update: Update, context: ContextTypes.DEFAULT_T
except Exception as e:
logger.error(f"Error getting table schema: {str(e)}")
- await update.message.reply_text(f"❌ Error: {str(e)}")
+ await update.message.reply_text(t("database.schema.error", lang, error=str(e)))
diff --git a/src/telegram/commands/help_commands.py b/src/telegram/commands/help_commands.py
index 0fcecf0..6f235ea 100644
--- a/src/telegram/commands/help_commands.py
+++ b/src/telegram/commands/help_commands.py
@@ -1,119 +1,45 @@
+"""
+Help command handlers for the Telegram bot.
+
+This module provides the /help command with multi-language support.
+"""
+
from telegram import Update
from telegram.ext import ContextTypes
-COMMAND_HELPS = {
- "parameters": (
- "/parameters\n"
- "Open an interactive menu to customize indicator parameters.\n\n"
- "Available parameters:\n"
- "- ATR Period: Adjust the period used for Average True Range calculations (1-50)\n"
- "- FVG Min Size: Set the minimum size ratio for Fair Value Gaps (0.0001-0.01)\n\n"
- "Click on any parameter to modify its value."
- ),
- "chart": (
- "/chart <symbol> <length> <interval> <tolerance>\n"
- "Get a candlestick chart with all enabled indicators.\n\n"
- "- symbol: The trading pair, e.g. BTCUSDT\n"
- "- length: Amount of candles to analyze (e.g. 48)\n"
- "- interval: Candle interval (e.g. 1h, 15m)\n"
- "- tolerance: Sensitivity for liquidity level detection (0-1, e.g. 0.05 = more levels, 0.2 = fewer, only strongest)\n\n"
- "Example: /chart BTCUSDT 48 1h 0.05\n\n"
- "The chart will include all indicators you have enabled in your preferences."
- ),
- "text_result": (
- "/text_result <symbol> <length> <interval> <tolerance>\n"
- "Get a text summary of all detected indicators for the specified symbol and timeframe.\n\n"
- "Example: /text_result ETHUSDT 24 15m 0.03"
- ),
- "history": (
- "/history <symbol> <length> <interval> <tolerance> <timestamp>\n"
- "Get a candlestick chart for historical data at a specified timestamp.\n\n"
- "- symbol: The trading pair, e.g. BTCUSDT\n"
- "- length: Number of candles to analyze\n"
- "- interval: Candle interval (e.g., 1h, 15m)\n"
- "- tolerance: Sensitivity for liquidity level detection (0-1)\n"
- "- timestamp: Unix epoch seconds for the end of the period\n\n"
- "Example: /history BTCUSDT 48 1h 0.05 1700000000"
- ),
- "preferences": (
- "/preferences\n"
- "Open an interactive menu to select which indicators to use and chart options.\n\n"
- "Available options:\n"
- "- Technical indicators (Order Blocks, FVGs, Liquidity Levels, etc.)\n"
- "- Display settings (Show Legend, Show Volume)\n"
- "- Theme selection (Light/Dark mode)"
- ),
- "create_signal": (
- "/create_signal <symbol> <minutes> [<show_chart>]\n"
- "Start receiving auto-signals for a pair at a given frequency (in minutes).\n\n"
- "- symbol: The trading pair, e.g. BTCUSDT\n"
- "- minutes: Frequency in minutes to receive signals\n"
- "- show_chart (optional): true/false, whether to include a chart with each signal (default: false)\n\n"
- "Example: /create_signal BTCUSDT 60 true\n\n"
- "You can have up to 10 active signal jobs per user."
- ),
- "delete_signal": (
- "/delete_signal <symbol>\n"
- "Stop auto-signals for a pair.\n\n"
- "Example: /delete_signal BTCUSDT"
- ),
- "manage_signals": (
- "/manage_signals\n"
- "Open an interactive menu to view, add, or delete your signal jobs.\n\n"
- "You can have up to 10 active signal jobs per user."
- ),
- "help": (
- "/help\nShow the general help message.\n\n"
- "/help <command>\nShow detailed help for a specific command.\n\n"
- "Example: /help chart"
- ),
- "sql": (
- "/sql <query>\n"
- "Execute a custom SQL query against the database.\n\n"
- "Example queries:\n"
- "- /sql SELECT * FROM trades LIMIT 5\n"
- "- /sql SELECT symbol, COUNT(*) as trades FROM trades GROUP BY symbol\n"
- "- /sql SELECT AVG(profit_loss) as avg_profit FROM trades WHERE symbol = 'BTCUSDT'\n\n"
- "Note: Results are limited to 50 rows for readability."
- ),
- "tables": (
- "/tables\n"
- "Show a list of all available tables in the database.\n\n"
- "Use this command to see what tables you can query with /sql."
- ),
- "schema": (
- "/schema <table_name>\n"
- "Show the schema (structure) of a specific table.\n\n"
- "Example: /schema trades\n\n"
- "This will show you all columns and their data types in the specified table."
- ),
+from src.i18n import t, get_user_language
+
+# Map command names to translation keys
+COMMAND_HELP_KEYS = {
+ "parameters": "help.commands.parameters",
+ "chart": "help.commands.chart",
+ "text_result": "help.commands.text_result",
+ "history": "help.commands.history",
+ "preferences": "help.commands.preferences",
+ "create_signal": "help.commands.create_signal",
+ "delete_signal": "help.commands.delete_signal",
+ "manage_signals": "help.commands.manage_signals",
+ "help": "help.commands.help",
+ "sql": "help.commands.sql",
+ "tables": "help.commands.tables",
+ "schema": "help.commands.schema",
+ "language": "help.commands.language",
}
async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""
- Sends a help message with usage instructions for the bot, or detailed help for a specific command if provided.
+ Sends a help message with usage instructions for the bot,
+ or detailed help for a specific command if provided.
"""
+ user_id = update.effective_user.id
+ lang = get_user_language(user_id)
+
args = context.args
- if args and args[0].lower() in COMMAND_HELPS:
- help_text = COMMAND_HELPS[args[0].lower()]
+ if args and args[0].lower() in COMMAND_HELP_KEYS:
+ command = args[0].lower()
+ help_text = t(COMMAND_HELP_KEYS[command], lang)
else:
- help_text = (
- "CryptoBot Help\n\n"
- "Here are the main commands you can use:\n\n"
- "/chart <symbol> <length> <interval> <tolerance>\n"
- "/text_result <symbol> <length> <interval> <tolerance>\n"
- "/history <symbol> <length> <interval> <tolerance> <timestamp>\n"
- "/preferences\n"
- "/create_signal <symbol> <minutes> [<show_chart>]\n"
- "/delete_signal <symbol>\n"
- "/manage_signals\n"
- "/sql <query>\n"
- "/tables\n"
- "/schema <table_name>\n"
- "/help <command>\n\n"
- "Type /help <command> to get detailed help for a specific command.\n\n"
- "Example: /help chart\n\n"
- "For more details, see the README or contact the maintainer (@yarik_is_working)."
- )
+ help_text = f"{t('help.title', lang)}\n\n{t('help.main', lang)}"
+
await update.message.reply_text(help_text, parse_mode="HTML")
diff --git a/src/telegram/commands/history_commands.py b/src/telegram/commands/history_commands.py
index 8f03d0e..49b0793 100644
--- a/src/telegram/commands/history_commands.py
+++ b/src/telegram/commands/history_commands.py
@@ -17,6 +17,7 @@
get_signal_history_by_date_range,
)
from src.analysis.utils.export import export_user_signals
+from src.i18n import t, get_user_language
from src.core.error_handler import handle_error
@@ -36,14 +37,15 @@ async def command_signal_history(update: Update, context: ContextTypes.DEFAULT_T
"""
try:
user_id = update.effective_user.id
+ lang = get_user_language(user_id)
signals = get_user_signal_history(user_id, limit=10)
keyboard = [
[
- InlineKeyboardButton("Last 24h", callback_data=f"{HISTORY_PERIOD}24h"),
- InlineKeyboardButton("Last 7d", callback_data=f"{HISTORY_PERIOD}7d"),
- InlineKeyboardButton("Last 30d", callback_data=f"{HISTORY_PERIOD}30d"),
+ InlineKeyboardButton(t("history.periods.last_24h", lang), callback_data=f"{HISTORY_PERIOD}24h"),
+ InlineKeyboardButton(t("history.periods.last_7d", lang), callback_data=f"{HISTORY_PERIOD}7d"),
+ InlineKeyboardButton(t("history.periods.last_30d", lang), callback_data=f"{HISTORY_PERIOD}30d"),
]
]
@@ -61,11 +63,11 @@ async def command_signal_history(update: Update, context: ContextTypes.DEFAULT_T
reply_markup = InlineKeyboardMarkup(keyboard)
if signals:
- message = "📜 *Your Signal History*\n\n"
+ message = f"{t('history.title', lang)}\n\n"
message += format_signal_history(signals)
- message += "\n\nFilter by time period or currency pair:"
+ message += t("history.filter_prompt", lang)
else:
- message = "No signal history found. Signals will be recorded when they are generated."
+ message = t("history.no_signals", lang)
await update.message.reply_text(
message, reply_markup=reply_markup, parse_mode="Markdown"
@@ -86,6 +88,7 @@ async def button_history_callback(update: Update, context: ContextTypes.DEFAULT_
await query.answer()
user_id = update.effective_user.id
+ lang = get_user_language(user_id)
callback_data = query.data
try:
@@ -93,32 +96,32 @@ async def button_history_callback(update: Update, context: ContextTypes.DEFAULT_
period = callback_data[len(HISTORY_PERIOD) :]
signals = filter_by_period(user_id, period)
- keyboard = build_history_keyboard(signals)
+ keyboard = build_history_keyboard(signals, lang=lang)
reply_markup = InlineKeyboardMarkup(keyboard)
if signals:
- message = f"📜 *Signal History - Last {period}*\n\n"
- message += format_signal_history(signals)
+ message = f"{t('history.filtered_title', lang, period=period)}\n\n"
+ message += format_signal_history(signals, lang=lang)
else:
- message = f"No signals found in the last {period}."
+ message = t("history.no_signals_period", lang, period=period)
- await safe_edit_message(query, message, reply_markup)
+ await safe_edit_message(query, message, reply_markup, lang)
# Handle currency pair filtering
elif callback_data.startswith(HISTORY_PAIR):
pair = callback_data[len(HISTORY_PAIR) :]
signals = get_user_signal_history(user_id, limit=20, currency_pair=pair)
- keyboard = build_history_keyboard(signals, selected_pair=pair)
+ keyboard = build_history_keyboard(signals, selected_pair=pair, lang=lang)
reply_markup = InlineKeyboardMarkup(keyboard)
if signals:
- message = f"📜 *Signal History - {pair}*\n\n"
- message += format_signal_history(signals)
+ message = f"{t('history.pair_title', lang, pair=pair)}\n\n"
+ message += format_signal_history(signals, lang=lang)
else:
- message = f"No signals found for {pair}."
+ message = t("history.no_signals_pair", lang, pair=pair)
- await safe_edit_message(query, message, reply_markup)
+ await safe_edit_message(query, message, reply_markup, lang)
elif callback_data.startswith(HISTORY_PAGE):
# TODO: Implement pagination for large result sets
@@ -172,22 +175,23 @@ def filter_by_period(user_id, period):
return get_signal_history_by_date_range(user_id, start_date, end_date)
-def build_history_keyboard(signals, selected_pair=None):
+def build_history_keyboard(signals, selected_pair=None, lang="en"):
"""
Build the keyboard for history filtering.
Args:
signals: List of signals
selected_pair: Currently selected currency pair
+ lang: Language code for translations
Returns:
List of button rows for the keyboard
"""
keyboard = [
[
- InlineKeyboardButton("Last 24h", callback_data=f"{HISTORY_PERIOD}24h"),
- InlineKeyboardButton("Last 7d", callback_data=f"{HISTORY_PERIOD}7d"),
- InlineKeyboardButton("Last 30d", callback_data=f"{HISTORY_PERIOD}30d"),
+ InlineKeyboardButton(t("history.periods.last_24h", lang), callback_data=f"{HISTORY_PERIOD}24h"),
+ InlineKeyboardButton(t("history.periods.last_7d", lang), callback_data=f"{HISTORY_PERIOD}7d"),
+ InlineKeyboardButton(t("history.periods.last_30d", lang), callback_data=f"{HISTORY_PERIOD}30d"),
]
]
@@ -205,7 +209,7 @@ def build_history_keyboard(signals, selected_pair=None):
# Add 'Show All' button if a pair is selected
if selected_pair:
keyboard.append(
- [InlineKeyboardButton("Show All", callback_data=f"{HISTORY_PERIOD}all")]
+ [InlineKeyboardButton(t("history.show_all", lang), callback_data=f"{HISTORY_PERIOD}all")]
)
export_buttons = []
@@ -213,18 +217,18 @@ def build_history_keyboard(signals, selected_pair=None):
export_buttons.extend(
[
InlineKeyboardButton(
- "Export CSV", callback_data=f"{EXPORT_CSV}{selected_pair}"
+ t("history.export.csv", lang), callback_data=f"{EXPORT_CSV}{selected_pair}"
),
InlineKeyboardButton(
- "Export JSON", callback_data=f"{EXPORT_JSON}{selected_pair}"
+ t("history.export.json", lang), callback_data=f"{EXPORT_JSON}{selected_pair}"
),
]
)
else:
export_buttons.extend(
[
- InlineKeyboardButton("Export CSV", callback_data=f"{EXPORT_CSV}all"),
- InlineKeyboardButton("Export JSON", callback_data=f"{EXPORT_JSON}all"),
+ InlineKeyboardButton(t("history.export.csv", lang), callback_data=f"{EXPORT_CSV}all"),
+ InlineKeyboardButton(t("history.export.json", lang), callback_data=f"{EXPORT_JSON}all"),
]
)
@@ -234,13 +238,14 @@ def build_history_keyboard(signals, selected_pair=None):
return keyboard
-def format_signal_history(signals, max_signals=10):
+def format_signal_history(signals, max_signals=10, lang="en"):
"""
Format the signal history for display.
Args:
signals: List of signals
max_signals: Maximum number of signals to display
+ lang: Language code for translations
Returns:
Formatted message string
@@ -286,12 +291,12 @@ def format_signal_history(signals, max_signals=10):
message += line + "\n\n"
if len(signals) == max_signals:
- message += f"_Showing {max_signals} most recent signals._"
+ message += t("history.showing_recent", lang, count=max_signals)
return message
-async def safe_edit_message(query, text, reply_markup=None):
+async def safe_edit_message(query, text, reply_markup=None, lang="en"):
"""
Safely edit a message, handling common Telegram API errors.
@@ -299,6 +304,7 @@ async def safe_edit_message(query, text, reply_markup=None):
query: The callback query
text: The new text content
reply_markup: Optional keyboard markup
+ lang: Language code for translations
"""
try:
await query.edit_message_text(
@@ -316,7 +322,7 @@ async def safe_edit_message(query, text, reply_markup=None):
await query.edit_message_text(text=text, reply_markup=reply_markup)
except Exception:
# Last resort - acknowledge the interaction but don't change anything
- await query.answer("Couldn't update message")
+ await query.answer(t("history.update_failed", lang))
else:
# For any other errors, log but continue
logger.error(f"Error editing message: {str(e)}")
@@ -324,10 +330,10 @@ async def safe_edit_message(query, text, reply_markup=None):
await query.edit_message_text(text=text, reply_markup=reply_markup)
except Exception as inner_e:
logger.error(f"Second attempt to edit message failed: {str(inner_e)}")
- await query.answer("Couldn't update message")
+ await query.answer(t("history.update_failed", lang))
except Exception as e:
logger.error(f"Unexpected error editing message: {str(e)}")
- await query.answer("Couldn't update message")
+ await query.answer(t("history.update_failed", lang))
async def handle_export(
@@ -350,26 +356,28 @@ async def handle_export(
format_type: Export format ('csv' or 'json')
currency_pair: Optional currency pair to filter by
"""
+ lang = get_user_language(user_id)
+
try:
query = update.callback_query
message_id = query.message.message_id
chat_id = query.message.chat_id
- await query.answer(f"Preparing {format_type.upper()} export...")
+ await query.answer(t("history.export.preparing", lang, format=format_type.upper()))
if currency_pair:
signals = get_user_signal_history(
user_id, limit=100, currency_pair=currency_pair
)
- description = f"for {currency_pair}"
+ description = t("history.export.for_pair", lang, pair=currency_pair)
else:
signals = get_user_signal_history(user_id, limit=100)
- description = "for all pairs"
+ description = t("history.export.for_all", lang)
if not signals:
await context.bot.send_message(
chat_id=chat_id,
- text=f"No signal data available {description} to export.",
+ text=t("history.export.no_data", lang, description=description),
reply_to_message_id=message_id,
)
return
@@ -383,7 +391,7 @@ async def handle_export(
chat_id=chat_id,
document=file,
filename=os.path.basename(filepath),
- caption=f"📊 Signal history {description} exported as {format_type.upper()}",
+ caption=t("history.export.caption", lang, description=description, format=format_type.upper()),
reply_to_message_id=message_id,
)
@@ -393,5 +401,5 @@ async def handle_export(
logger.error(f"Error removing temporary export file {filepath}: {str(e)}")
except Exception as e:
- error_message = f"Failed to export signal history as {format_type.upper()}"
+ error_message = t("history.export.error", lang, format=format_type.upper())
await handle_error(update, "export_error", error_message, exception=e)
diff --git a/src/telegram/commands/language_commands.py b/src/telegram/commands/language_commands.py
new file mode 100644
index 0000000..51e4ce6
--- /dev/null
+++ b/src/telegram/commands/language_commands.py
@@ -0,0 +1,110 @@
+"""
+Language selection command handlers for the Telegram bot.
+
+This module provides commands for users to change their preferred language.
+"""
+
+from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
+from telegram.ext import ContextTypes
+
+from src.i18n import (
+ t,
+ get_user_language,
+ set_user_language,
+ get_language_keyboard_data,
+ SUPPORTED_LANGUAGES,
+)
+
+
+async def language_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
+ """
+ /language - Opens the language selection menu.
+
+ Args:
+ update: Telegram update object
+ context: Telegram context object
+ """
+ user_id = update.effective_user.id
+ lang = get_user_language(user_id)
+
+ keyboard = build_language_keyboard(lang)
+
+ await update.message.reply_text(
+ t("language.select_prompt", lang),
+ reply_markup=keyboard,
+ )
+
+
+def build_language_keyboard(current_lang: str) -> InlineKeyboardMarkup:
+ """
+ Build the inline keyboard for language selection.
+
+ Args:
+ current_lang: Currently selected language code
+
+ Returns:
+ InlineKeyboardMarkup with language options
+ """
+ keyboard_data = get_language_keyboard_data()
+ keyboard = []
+
+ row = []
+ for code, name, flag in keyboard_data:
+ # Mark current language with a checkmark
+ if code == current_lang:
+ label = f"✓ {flag} {name}"
+ else:
+ label = f"{flag} {name}"
+
+ row.append(
+ InlineKeyboardButton(label, callback_data=f"lang_set:{code}")
+ )
+
+ # Create rows of 2 buttons each
+ if len(row) == 2:
+ keyboard.append(row)
+ row = []
+
+ # Add remaining buttons if any
+ if row:
+ keyboard.append(row)
+
+ return InlineKeyboardMarkup(keyboard)
+
+
+async def handle_language_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
+ """
+ Handle language selection callback from inline keyboard.
+
+ Args:
+ update: Telegram update object
+ context: Telegram context object
+ """
+ query = update.callback_query
+ await query.answer()
+
+ user_id = update.effective_user.id
+ callback_data = query.data
+
+ if callback_data.startswith("lang_set:"):
+ new_lang = callback_data.replace("lang_set:", "")
+
+ if new_lang in SUPPORTED_LANGUAGES:
+ # Update user's language preference
+ success = set_user_language(user_id, new_lang)
+
+ if success:
+ # Reply in the new language
+ await query.edit_message_text(
+ t("language.changed", new_lang),
+ )
+ else:
+ # Fallback message
+ await query.edit_message_text(
+ "❌ Failed to update language. Please try again.",
+ )
+ else:
+ await query.edit_message_text(
+ "❌ Invalid language selection.",
+ )
+
diff --git a/src/telegram/commands/signal_commands.py b/src/telegram/commands/signal_commands.py
index a344116..6c2b41c 100644
--- a/src/telegram/commands/signal_commands.py
+++ b/src/telegram/commands/signal_commands.py
@@ -1,8 +1,8 @@
from telegram import Update
from telegram.ext import ContextTypes
from src.analysis.utils.helpers import check_signal_limit, input_sanity_check_analyzing
-from src.core.utils import plural_helper
from src.telegram.signals.detection import createSignalJob, deleteSignalJob
+from src.i18n import t, get_user_language
async def create_signal_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
@@ -11,6 +11,9 @@ async def create_signal_command(update: Update, context: ContextTypes.DEFAULT_TY
Usage: /create_signal []
Example: /create_signal BTCUSDT 60 True
"""
+ user_id = update.effective_user.id
+ lang = get_user_language(user_id)
+
if await check_signal_limit(update):
return
@@ -18,14 +21,14 @@ async def create_signal_command(update: Update, context: ContextTypes.DEFAULT_TY
pair = await input_sanity_check_analyzing(True, args, update)
if not pair:
await update.message.reply_text(
- f"Usage: /create_signal [], you've sent {len(args)} argument{plural_helper(len(args))}."
+ t("signals.usage.create", lang, count=len(args))
)
else:
try:
await createSignalJob(pair[0], pair[1], pair[2], update, context)
except Exception as e:
print(f"Unexpected error: {e}")
- await update.message.reply_text("❌ An unexpected error occurred.")
+ await update.message.reply_text(t("signals.errors.unexpected", lang))
async def delete_signal_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
@@ -34,11 +37,13 @@ async def delete_signal_command(update: Update, context: ContextTypes.DEFAULT_TY
Usage: /delete_signal
Example: /delete_signal BTCUSDT
"""
+ user_id = update.effective_user.id
+ lang = get_user_language(user_id)
args = context.args
# Check if args list is empty
if not args:
- await update.message.reply_text(f"Usage: /delete_signal ")
+ await update.message.reply_text(t("signals.usage.delete", lang))
return
# Format and store the command in the update object for deleteSignalJob to parse
@@ -53,10 +58,10 @@ async def delete_signal_command(update: Update, context: ContextTypes.DEFAULT_TY
await deleteSignalJob(args[0].upper(), update)
except ValueError as e:
print(f"Value error in delete_signal: {e}")
- await update.message.reply_text(f"❌ Invalid symbol: {str(e)}")
+ await update.message.reply_text(t("signals.errors.invalid_symbol", lang, symbol=str(e)))
except Exception as e:
print(f"Unexpected error in delete_signal: {e}")
- await update.message.reply_text("❌ An unexpected error occurred.")
+ await update.message.reply_text(t("signals.errors.unexpected", lang))
finally:
# Restore the original text in case it's needed elsewhere
update.message.text = original_text
diff --git a/src/telegram/commands/status_commands.py b/src/telegram/commands/status_commands.py
index 3bcd5f4..00d6dc2 100644
--- a/src/telegram/commands/status_commands.py
+++ b/src/telegram/commands/status_commands.py
@@ -9,6 +9,7 @@
from telegram.ext import ContextTypes
from src.core.rate_limiter import get_rate_limit_stats
+from src.i18n import t, get_user_language
async def rate_limit_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
@@ -20,25 +21,21 @@ async def rate_limit_command(update: Update, context: ContextTypes.DEFAULT_TYPE)
context: The context object from Telegram
"""
user_id = update.effective_user.id
+ lang = get_user_language(user_id)
quota = get_rate_limit_stats(user_id)
status_emoji = "✅" if quota["remaining"] > 5 else "⚠️"
- suspicious_status = (
- " (reduced due to suspicious activity)" if quota["is_suspicious"] else ""
- )
+ suspicious_suffix = t("rate_limit.suspicious_suffix", lang) if quota["is_suspicious"] else ""
message = (
- f"{status_emoji} **Rate Limit Status**\n\n"
- f"• Remaining: {quota['remaining']}/{quota['limit']} requests{suspicious_status}\n"
- f"• Reset in: {quota['reset_seconds']} seconds\n\n"
+ f"{status_emoji} {t('rate_limit.title', lang)}\n\n"
+ f"{t('rate_limit.remaining', lang, remaining=quota['remaining'], limit=quota['limit'], suffix=suspicious_suffix)}\n"
+ f"{t('rate_limit.reset_in', lang, seconds=quota['reset_seconds'])}\n\n"
)
if quota["is_suspicious"]:
- message += (
- "ℹ️ Your rate limit is temporarily reduced due to detected high-frequency activity. "
- "This will automatically return to normal after a cooling period.\n\n"
- )
+ message += f"{t('rate_limit.reduced_notice', lang)}\n\n"
- message += "The system uses rate limiting to ensure fair API usage for all users."
+ message += t("rate_limit.footer", lang)
await update.message.reply_text(message, parse_mode="Markdown")
diff --git a/src/telegram/handlers.py b/src/telegram/handlers.py
index 6d3843a..c7e6b7b 100644
--- a/src/telegram/handlers.py
+++ b/src/telegram/handlers.py
@@ -22,6 +22,7 @@
)
from src.core.preferences import INDICATOR_PARAMS
from src.telegram.signals.detection import createSignalJob
+from src.i18n import t, get_user_language
from src.analysis.utils.helpers import input_sanity_check_text, check_signal_limit_by_id
from src.core.preferences import get_formatted_preferences
@@ -49,15 +50,17 @@ async def manage_signals(update: Update, context: ContextTypes.DEFAULT_TYPE):
/manage_signals - The entry point to display signals for the user with inline buttons.
"""
user_id = update.effective_user.id
+ lang = get_user_language(user_id)
await update.message.reply_text(
- text="Manage your signals:", reply_markup=build_signal_list_keyboard(user_id)
+ text=t("signals.manage_title", lang),
+ reply_markup=build_signal_list_keyboard(user_id, lang)
)
return CHOOSING_ACTION
-def build_signal_list_keyboard(user_id: int) -> InlineKeyboardMarkup:
+def build_signal_list_keyboard(user_id: int, lang: str = "en") -> InlineKeyboardMarkup:
"""
Builds a dynamic inline keyboard listing all signals for a user:
- Each row: [ (m)] [Delete ]
@@ -66,6 +69,7 @@ def build_signal_list_keyboard(user_id: int) -> InlineKeyboardMarkup:
Args:
user_id: Telegram user ID
+ lang: Language code for translations
Returns:
InlineKeyboardMarkup with signal management options
@@ -82,21 +86,22 @@ def build_signal_list_keyboard(user_id: int) -> InlineKeyboardMarkup:
f"{pair} {freq}m chart {'✅' if str(is_with_chart) == '1' else '❌'}"
)
del_button = InlineKeyboardButton(
- text=f"Delete {pair}", callback_data=f"delete_signal_{pair}"
+ text=t("signals.delete", lang, pair=pair),
+ callback_data=f"delete_signal_{pair}"
)
keyboard.append(
[InlineKeyboardButton(display_text, callback_data="no_op"), del_button]
)
else:
keyboard.append(
- [InlineKeyboardButton("No signals found", callback_data="no_op")]
+ [InlineKeyboardButton(t("signals.no_signals", lang), callback_data="no_op")]
)
keyboard.append(
- [InlineKeyboardButton("➕ Add New Signal", callback_data="add_signal")]
+ [InlineKeyboardButton(t("signals.add_new", lang), callback_data="add_signal")]
)
- keyboard.append([InlineKeyboardButton("Done", callback_data="signal_menu_done")])
+ keyboard.append([InlineKeyboardButton(t("common.done", lang), callback_data="signal_menu_done")])
return InlineKeyboardMarkup(keyboard)
@@ -117,6 +122,7 @@ async def handle_signal_menu_callback(
query = update.callback_query
data = query.data
user_id = query.from_user.id
+ lang = get_user_language(user_id)
logger.info(f"handle_signal_menu_callback -> callback_data = {data}")
await query.answer()
@@ -136,25 +142,21 @@ async def handle_signal_menu_callback(
del auto_signal_jobs[job_key]
await query.edit_message_text(
- text="Signal deleted. Updated list:",
- reply_markup=build_signal_list_keyboard(user_id),
+ text=t("signals.deleted", lang),
+ reply_markup=build_signal_list_keyboard(user_id, lang),
)
return CHOOSING_ACTION
elif data == "add_signal":
await query.edit_message_text(
- "Please enter your desired signal in the format: \n\n"
- "`SYMBOL INTERVAL [with_chart]`\n\n"
- "Example: `BTCUSDT 60` or `ETHUSDT 15 with_chart`\n\n"
- "SYMBOL should be a valid trading pair (e.g., BTCUSDT).\n"
- "INTERVAL should be in minutes (e.g., 5, 15, 60)."
+ t("signals.add_prompt", lang),
+ parse_mode="Markdown"
)
return TYPING_SIGNAL_DATA
elif data == "signal_menu_done":
n_signals = len(get_all_user_signal_requests(user_id))
- signal_text = f"signal{plural_helper(n_signals)}"
- await query.edit_message_text(f"You have {n_signals} active {signal_text}.")
+ await query.edit_message_text(t("signals.active_count", lang, count=n_signals))
return ConversationHandler.END
return CHOOSING_ACTION
@@ -172,12 +174,13 @@ async def handle_signal_text_input(update: Update, context: ContextTypes.DEFAULT
Conversation state
"""
user_id = update.effective_user.id
+ lang = get_user_language(user_id)
text = update.message.text.strip().upper()
parse_result = input_sanity_check_text(text)
if not parse_result["is_valid"]:
await update.message.reply_text(
- f"Invalid input: {parse_result['error_message']}. Try again or /cancel."
+ t("signals.invalid_input", lang, error=parse_result['error_message'])
)
return TYPING_SIGNAL_DATA
@@ -185,7 +188,8 @@ async def handle_signal_text_input(update: Update, context: ContextTypes.DEFAULT
if not limit_ok:
await update.message.reply_text(limit_msg)
await update.message.reply_text(
- text="Current signals:", reply_markup=build_signal_list_keyboard(user_id)
+ text=t("signals.manage_title", lang),
+ reply_markup=build_signal_list_keyboard(user_id, lang)
)
return CHOOSING_ACTION
@@ -195,13 +199,13 @@ async def handle_signal_text_input(update: Update, context: ContextTypes.DEFAULT
await createSignalJob(symbol, period_minutes, is_with_chart, update, context)
await update.message.reply_text(
- text="Signal created! Current signals:",
- reply_markup=build_signal_list_keyboard(user_id),
+ text=t("signals.created", lang),
+ reply_markup=build_signal_list_keyboard(user_id, lang),
)
return CHOOSING_ACTION
-def get_indicator_selection_keyboard(user_id, menu_id=None):
+def get_indicator_selection_keyboard(user_id, menu_id=None, lang="en"):
"""
Create an inline keyboard for selecting indicators with a checkmark for selected ones.
@@ -209,6 +213,7 @@ def get_indicator_selection_keyboard(user_id, menu_id=None):
user_id: Telegram user ID
menu_id: Optional unique identifier for this preference menu instance
(was created to provide multiple menus being opened at the same time)
+ lang: Language code for translations
Returns:
InlineKeyboardMarkup with indicator selection options and the menu_id
@@ -229,49 +234,49 @@ def create_callback(action):
keyboard = [
[
InlineKeyboardButton(
- f"{'✔️ ' if selected['order_blocks'] else ''}Order Blocks",
+ f"{'✔️ ' if selected['order_blocks'] else ''}{t('preferences.indicators.order_blocks', lang)}",
callback_data=create_callback("indicator_order_blocks"),
),
InlineKeyboardButton(
- f"{'✔️ ' if selected['fvgs'] else ''}FVGs",
+ f"{'✔️ ' if selected['fvgs'] else ''}{t('preferences.indicators.fvgs', lang)}",
callback_data=create_callback("indicator_fvgs"),
),
],
[
InlineKeyboardButton(
- f"{'✔️ ' if selected['liquidity_levels'] else ''}Liquidity Levels",
+ f"{'✔️ ' if selected['liquidity_levels'] else ''}{t('preferences.indicators.liquidity_levels', lang)}",
callback_data=create_callback("indicator_liquidity_levels"),
),
InlineKeyboardButton(
- f"{'✔️ ' if selected['breaker_blocks'] else ''}Breaker Blocks",
+ f"{'✔️ ' if selected['breaker_blocks'] else ''}{t('preferences.indicators.breaker_blocks', lang)}",
callback_data=create_callback("indicator_breaker_blocks"),
),
],
[
InlineKeyboardButton(
- f"{'✔️ ' if selected['show_legend'] else ''}Show Legend",
+ f"{'✔️ ' if selected['show_legend'] else ''}{t('preferences.indicators.show_legend', lang)}",
callback_data=create_callback("indicator_show_legend"),
),
InlineKeyboardButton(
- f"{'✔️ ' if selected['show_volume'] else ''}Show Volume",
+ f"{'✔️ ' if selected['show_volume'] else ''}{t('preferences.indicators.show_volume', lang)}",
callback_data=create_callback("indicator_show_volume"),
),
],
[
InlineKeyboardButton(
- f"{'✔️ ' if selected['liquidity_pools'] else ''}Liquidity Pools",
+ f"{'✔️ ' if selected['liquidity_pools'] else ''}{t('preferences.indicators.liquidity_pools', lang)}",
callback_data=create_callback("indicator_liquidity_pools"),
),
],
[
InlineKeyboardButton(
- f"{'🌙 ' if selected['dark_mode'] else '☀️ '}{'Dark Mode' if selected['dark_mode'] else 'Light Mode'}",
+ f"{'🌙 ' if selected['dark_mode'] else '☀️ '}{t('preferences.indicators.dark_mode', lang) if selected['dark_mode'] else t('preferences.indicators.light_mode', lang)}",
callback_data=create_callback("indicator_dark_mode"),
),
],
[
InlineKeyboardButton(
- "Done", callback_data=create_callback("indicator_done")
+ t("common.done", lang), callback_data=create_callback("indicator_done")
),
],
]
@@ -293,6 +298,7 @@ async def handle_indicator_selection(update, _):
await query.answer()
user_id = query.from_user.id
+ lang = get_user_language(user_id)
try:
data_parts = query.data.split(":")
@@ -330,8 +336,9 @@ async def handle_indicator_selection(update, _):
if menu_id in _menu_preferences:
del _menu_preferences[menu_id]
+ indicators_str = ', '.join(selected_pretty) if selected_pretty else t("preferences.none_selected", lang)
await query.edit_message_text(
- f"You selected: {', '.join(selected_pretty) or 'None'}"
+ t("preferences.selected", lang, indicators=indicators_str)
)
return
@@ -354,20 +361,21 @@ async def handle_indicator_selection(update, _):
_menu_preferences[menu_id] = preferences
- new_markup, _ = get_indicator_selection_keyboard(user_id, menu_id)
+ new_markup, _ = get_indicator_selection_keyboard(user_id, menu_id, lang)
await query.edit_message_reply_markup(reply_markup=new_markup)
return CHOOSING_ACTION
-def get_parameter_keyboard(user_id, menu_id=None):
+def get_parameter_keyboard(user_id, menu_id=None, lang="en"):
"""
Create an inline keyboard for adjusting indicator parameters.
Args:
user_id: Telegram user ID
menu_id: Optional unique identifier for this parameter menu instance
+ lang: Language code for translations
Returns:
InlineKeyboardMarkup with parameter setting options and the menu_id
@@ -394,71 +402,12 @@ def get_parameter_keyboard(user_id, menu_id=None):
)
keyboard.append(
- [InlineKeyboardButton("Save", callback_data=f"param:{menu_id}:save")]
+ [InlineKeyboardButton(t("common.save", lang), callback_data=f"param:{menu_id}:save")]
)
return InlineKeyboardMarkup(keyboard), menu_id
-async def handle_parameter_input(update, context):
- """
- Handle user input for parameter values.
-
- Args:
- update: Telegram update object
- context: Telegram context object
-
- Returns:
- Conversation state
- """
- user_id = update.effective_user.id
- user_text = update.message.text.strip()
-
- if user_id not in _param_edit_states:
- await update.message.reply_text(
- "Sorry, I don't have an active parameter editing session. "
- "Please use /select_indicators to start over."
- )
- return ConversationHandler.END
-
- param_info = _param_edit_states[user_id]
- param_name = param_info["param"]
- menu_id = param_info["menu_id"]
-
- try:
- if param_name == "atr_period":
- new_value = int(user_text)
- else: # fvg_min_size or other float params
- new_value = float(user_text)
-
- param_config = INDICATOR_PARAMS[param_name]
- if new_value < param_config["min"] or new_value > param_config["max"]:
- await update.message.reply_text(
- f"Value must be between {param_config['min']} and {param_config['max']}. "
- "Please try again."
- )
- return TYPING_PARAM_VALUE
-
- _menu_preferences[menu_id][param_name] = new_value
-
- # Clear the edit state
- del _param_edit_states[user_id]
-
- param_markup = get_parameter_keyboard(user_id, menu_id)
- await update.message.reply_text(
- f"{INDICATOR_PARAMS[param_name]['display_name']} set to {new_value}.",
- reply_markup=param_markup,
- )
-
- return CHOOSING_ACTION
-
- except (ValueError, TypeError):
- await update.message.reply_text(
- f"Invalid input. Please enter a valid number for {INDICATOR_PARAMS[param_name]['display_name']}."
- )
- return TYPING_PARAM_VALUE
-
-
async def select_indicators(update, _):
"""
Start the process of selecting indicators.
@@ -471,11 +420,12 @@ async def select_indicators(update, _):
None
"""
user_id = update.effective_user.id
+ lang = get_user_language(user_id)
- keyboard, _ = get_indicator_selection_keyboard(user_id)
+ keyboard, _ = get_indicator_selection_keyboard(user_id, lang=lang)
await update.message.reply_text(
- "Please choose the indicators you'd like to include:",
+ t("preferences.select_prompt", lang),
reply_markup=keyboard,
)
@@ -495,6 +445,7 @@ async def handle_parameter_selection(update, _):
await query.answer()
user_id = query.from_user.id
+ lang = get_user_language(user_id)
try:
data_parts = query.data.split(":")
@@ -520,7 +471,7 @@ async def handle_parameter_selection(update, _):
if menu_id in _param_preferences:
del _param_preferences[menu_id]
- await query.edit_message_text("Parameter settings saved successfully!")
+ await query.edit_message_text(t("parameters.saved", lang))
return
elif action.startswith("edit_"):
@@ -531,18 +482,18 @@ async def handle_parameter_selection(update, _):
current_value = preferences.get(param_name, param_info["default"])
await query.edit_message_text(
- f"Enter a new value for {param_info['display_name']}:\n\n"
- f"Current value: {current_value}\n"
- f"Valid range: {param_info['min']} to {param_info['max']} (step: {param_info['step']})\n\n"
- f"Reply with a number to set the new value."
+ t("parameters.edit_prompt", lang,
+ param=param_info['display_name'],
+ current=current_value,
+ min=param_info['min'],
+ max=param_info['max'],
+ step=param_info['step'])
)
return TYPING_PARAM_VALUE
# If we got here, something unexpected happened
- await query.edit_message_text(
- "Sorry, I couldn't process that action. Please try again with /parameters."
- )
+ await query.edit_message_text(t("parameters.errors.process_failed", lang))
return
@@ -558,13 +509,11 @@ async def handle_parameter_input(update, context):
Conversation state
"""
user_id = update.effective_user.id
+ lang = get_user_language(user_id)
user_text = update.message.text.strip()
if user_id not in _param_edit_states:
- await update.message.reply_text(
- "Sorry, I don't have an active parameter editing session. "
- "Please use /parameters to start over."
- )
+ await update.message.reply_text(t("parameters.errors.no_session", lang))
return ConversationHandler.END
param_info = _param_edit_states[user_id]
@@ -580,8 +529,9 @@ async def handle_parameter_input(update, context):
param_config = INDICATOR_PARAMS[param_name]
if new_value < param_config["min"] or new_value > param_config["max"]:
await update.message.reply_text(
- f"Value must be between {param_config['min']} and {param_config['max']}. "
- "Please try again."
+ t("parameters.errors.out_of_range", lang,
+ min=param_config['min'],
+ max=param_config['max'])
)
return TYPING_PARAM_VALUE
@@ -589,9 +539,11 @@ async def handle_parameter_input(update, context):
del _param_edit_states[user_id]
- param_markup, _ = get_parameter_keyboard(user_id, menu_id)
+ param_markup, _ = get_parameter_keyboard(user_id, menu_id, lang)
await update.message.reply_text(
- f"{INDICATOR_PARAMS[param_name]['display_name']} set to {new_value}.",
+ t("parameters.value_set", lang,
+ param=INDICATOR_PARAMS[param_name]['display_name'],
+ value=new_value),
reply_markup=param_markup,
)
@@ -599,7 +551,8 @@ async def handle_parameter_input(update, context):
except (ValueError, TypeError):
await update.message.reply_text(
- f"Invalid input. Please enter a valid number for {INDICATOR_PARAMS[param_name]['display_name']}."
+ t("parameters.errors.invalid_number", lang,
+ param=INDICATOR_PARAMS[param_name]['display_name'])
)
return TYPING_PARAM_VALUE
@@ -616,11 +569,12 @@ async def show_parameters(update, _):
Conversation state
"""
user_id = update.effective_user.id
+ lang = get_user_language(user_id)
- keyboard, _ = get_parameter_keyboard(user_id)
+ keyboard, _ = get_parameter_keyboard(user_id, lang=lang)
await update.message.reply_text(
- "Configure indicator parameters:",
+ t("parameters.configure_title", lang),
reply_markup=keyboard,
)
diff --git a/src/telegram/tutorial.py b/src/telegram/tutorial.py
index 64d0542..22fc041 100644
--- a/src/telegram/tutorial.py
+++ b/src/telegram/tutorial.py
@@ -9,6 +9,7 @@
from telegram.ext import ContextTypes, ConversationHandler
from src.database.operations import get_user_preferences, update_user_preferences
+from src.i18n import t, get_user_language
TUTORIAL_NOT_STARTED = 0
TUTORIAL_WELCOME = 1
@@ -20,99 +21,72 @@
CHOOSING_TUTORIAL_ACTION = 0
-TUTORIAL_MESSAGES = {
- TUTORIAL_WELCOME: {
- "title": "🎉 Welcome to CryptoBot!",
- "content": (
- "I'm your personal crypto market analysis assistant. I can help you analyze "
- "cryptocurrency charts with advanced technical indicators.\n\n"
- "This short tutorial will show you how to use my main features. You can skip "
- "the tutorial or come back to it anytime with the /tutorial command."
- ),
- "buttons": [
- [InlineKeyboardButton("Continue", callback_data="tutorial_next")],
- [InlineKeyboardButton("Skip Tutorial", callback_data="tutorial_skip")],
- ],
- },
- TUTORIAL_CHART_COMMAND: {
- "title": "📊 Chart Analysis",
- "content": (
- "The basic command to get a chart analysis is:\n\n"
- "/chart SYMBOL LENGTH INTERVAL TOLERANCE\n\n"
- "For example: /chart BTCUSDT 48 1h 0.05\n\n"
- "This gives you a 48-candle, 1-hour chart for BTC/USDT with a tolerance of 0.05 "
- "for detecting liquidity levels.\n\n"
- "Try it out or continue to learn more features!"
- ),
- "buttons": [
- [InlineKeyboardButton("Continue", callback_data="tutorial_next")],
- [InlineKeyboardButton("Skip to End", callback_data="tutorial_skip")],
- ],
- },
- TUTORIAL_PREFERENCES: {
- "title": "⚙️ Customizing Indicators",
- "content": (
- "You can customize which indicators appear on your charts with the "
- "/preferences command.\n\n"
- "This opens an interactive menu where you can enable or disable:\n"
- "• Order Blocks\n"
- "• Fair Value Gaps (FVGs)\n"
- "• Liquidity Levels\n"
- "• Breaker Blocks\n"
- "• And change display options\n\n"
- "Your preferences will be saved for future chart requests."
- ),
- "buttons": [
- [InlineKeyboardButton("Continue", callback_data="tutorial_next")],
- [InlineKeyboardButton("Skip to End", callback_data="tutorial_skip")],
- ],
- },
- TUTORIAL_SIGNALS: {
- "title": "🔔 Automated Signals",
- "content": (
- "Want to receive automatic notifications? Set up signal alerts with:\n\n"
- "/create_signal SYMBOL MINUTES [CHART]\n\n"
- "For example: /create_signal BTCUSDT 60 true\n\n"
- "This will send you a notification with a chart every 60 minutes for BTC/USDT.\n\n"
- "You can manage your signals with the /manage_signals command."
- ),
- "buttons": [
- [InlineKeyboardButton("Continue", callback_data="tutorial_next")],
- [InlineKeyboardButton("Skip to End", callback_data="tutorial_skip")],
- ],
- },
- TUTORIAL_PARAMETERS: {
- "title": "🔧 Advanced Parameters",
- "content": (
- "Fine-tune indicator parameters with the /parameters command.\n\n"
- "You can adjust:\n"
- "• ATR Period: for calculating Average True Range\n"
- "• FVG Min Size: minimum size for Fair Value Gaps\n\n"
- "These settings affect how indicators are calculated and displayed."
- ),
- "buttons": [
- [InlineKeyboardButton("Complete Tutorial", callback_data="tutorial_next")]
- ],
- },
- TUTORIAL_COMPLETED: {
- "title": "🚀 Tutorial Completed!",
- "content": (
- "Great job! You've completed the CryptoBot tutorial.\n\n"
- "Remember, you can always get help with:\n"
- "• /help - General help information\n"
- "• /help COMMAND - Help for a specific command\n\n"
- "Ready to start? Try /chart BTCUSDT 48 1h 0.05 to see BTC/USDT analysis!"
- ),
- "buttons": [
- [
- InlineKeyboardButton(
- "View Available Commands", callback_data="tutorial_commands"
- )
+
+def get_tutorial_message(stage: int, lang: str = "en") -> dict:
+ """
+ Get the tutorial message for a specific stage in the specified language.
+
+ Args:
+ stage: Tutorial stage number
+ lang: Language code
+
+ Returns:
+ Dictionary with title, content, and buttons for the tutorial stage
+ """
+ messages = {
+ TUTORIAL_WELCOME: {
+ "title": t("tutorial.welcome.title", lang),
+ "content": t("tutorial.welcome.content", lang),
+ "buttons": [
+ [InlineKeyboardButton(t("tutorial.buttons.continue", lang), callback_data="tutorial_next")],
+ [InlineKeyboardButton(t("tutorial.buttons.skip_tutorial", lang), callback_data="tutorial_skip")],
+ ],
+ },
+ TUTORIAL_CHART_COMMAND: {
+ "title": t("tutorial.chart.title", lang),
+ "content": t("tutorial.chart.content", lang),
+ "buttons": [
+ [InlineKeyboardButton(t("tutorial.buttons.continue", lang), callback_data="tutorial_next")],
+ [InlineKeyboardButton(t("tutorial.buttons.skip_to_end", lang), callback_data="tutorial_skip")],
+ ],
+ },
+ TUTORIAL_PREFERENCES: {
+ "title": t("tutorial.preferences.title", lang),
+ "content": t("tutorial.preferences.content", lang),
+ "buttons": [
+ [InlineKeyboardButton(t("tutorial.buttons.continue", lang), callback_data="tutorial_next")],
+ [InlineKeyboardButton(t("tutorial.buttons.skip_to_end", lang), callback_data="tutorial_skip")],
+ ],
+ },
+ TUTORIAL_SIGNALS: {
+ "title": t("tutorial.signals.title", lang),
+ "content": t("tutorial.signals.content", lang),
+ "buttons": [
+ [InlineKeyboardButton(t("tutorial.buttons.continue", lang), callback_data="tutorial_next")],
+ [InlineKeyboardButton(t("tutorial.buttons.skip_to_end", lang), callback_data="tutorial_skip")],
+ ],
+ },
+ TUTORIAL_PARAMETERS: {
+ "title": t("tutorial.parameters.title", lang),
+ "content": t("tutorial.parameters.content", lang),
+ "buttons": [
+ [InlineKeyboardButton(t("tutorial.buttons.complete", lang), callback_data="tutorial_next")]
+ ],
+ },
+ TUTORIAL_COMPLETED: {
+ "title": t("tutorial.completed.title", lang),
+ "content": t("tutorial.completed.content", lang),
+ "buttons": [
+ [
+ InlineKeyboardButton(
+ t("tutorial.buttons.view_commands", lang), callback_data="tutorial_commands"
+ )
+ ],
+ [InlineKeyboardButton(t("common.finish", lang), callback_data="tutorial_finish")],
],
- [InlineKeyboardButton("Finish", callback_data="tutorial_finish")],
- ],
- },
-}
+ },
+ }
+ return messages.get(stage, messages[TUTORIAL_WELCOME])
async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
@@ -122,12 +96,13 @@ async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""
user_id = update.effective_user.id
user_prefs = get_user_preferences(user_id)
+ lang = get_user_language(user_id)
if user_prefs["tutorial_stage"] < TUTORIAL_COMPLETED:
user_prefs["tutorial_stage"] = TUTORIAL_WELCOME
update_user_preferences(user_id, user_prefs)
- tutorial = TUTORIAL_MESSAGES[TUTORIAL_WELCOME]
+ tutorial = get_tutorial_message(TUTORIAL_WELCOME, lang)
await update.message.reply_html(
f"{tutorial['title']}\n\n{tutorial['content']}",
reply_markup=InlineKeyboardMarkup(tutorial["buttons"]),
@@ -136,12 +111,7 @@ async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
else:
# User has already completed the tutorial
await update.message.reply_html(
- "Welcome back to CryptoBot!\n\n"
- "You've already completed the tutorial. What would you like to do?\n\n"
- "• Get a chart with /chart BTCUSDT 48 1h 0.05\n"
- "• Set preferences with /preferences\n"
- "• Create signals with /create_signal\n"
- "• Review the tutorial with /tutorial"
+ f"{t('tutorial.welcome_back.title', lang)}\n\n{t('tutorial.welcome_back.content', lang)}"
)
return ConversationHandler.END
@@ -152,6 +122,7 @@ async def tutorial_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""
user_id = update.effective_user.id
user_prefs = get_user_preferences(user_id)
+ lang = get_user_language(user_id)
has_completed_before = user_prefs["tutorial_stage"] >= TUTORIAL_COMPLETED
@@ -170,7 +141,7 @@ async def tutorial_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
# For users who already completed, we'll show the tutorial from the beginning
# but won't update their stage in the database unless they complete it again
- tutorial = TUTORIAL_MESSAGES[current_view_stage]
+ tutorial = get_tutorial_message(current_view_stage, lang)
context.user_data["has_completed_tutorial_before"] = has_completed_before
context.user_data["original_tutorial_stage"] = original_stage
@@ -191,6 +162,7 @@ async def handle_tutorial_callback(update: Update, context: ContextTypes.DEFAULT
user_id = update.effective_user.id
user_prefs = get_user_preferences(user_id)
+ lang = get_user_language(user_id)
# Check if user completed tutorial before (from context or preferences)
has_completed_before = context.user_data.get(
@@ -214,7 +186,7 @@ async def handle_tutorial_callback(update: Update, context: ContextTypes.DEFAULT
user_prefs["tutorial_stage"] = next_stage
update_user_preferences(user_id, user_prefs)
- tutorial = TUTORIAL_MESSAGES[next_stage]
+ tutorial = get_tutorial_message(next_stage, lang)
await query.edit_message_text(
f"{tutorial['title']}\n\n{tutorial['content']}",
reply_markup=InlineKeyboardMarkup(tutorial["buttons"]),
@@ -230,7 +202,7 @@ async def handle_tutorial_callback(update: Update, context: ContextTypes.DEFAULT
user_prefs["tutorial_stage"] = TUTORIAL_COMPLETED
update_user_preferences(user_id, user_prefs)
- tutorial = TUTORIAL_MESSAGES[TUTORIAL_COMPLETED]
+ tutorial = get_tutorial_message(TUTORIAL_COMPLETED, lang)
await query.edit_message_text(
f"{tutorial['title']}\n\n{tutorial['content']}",
reply_markup=InlineKeyboardMarkup(tutorial["buttons"]),
@@ -238,23 +210,16 @@ async def handle_tutorial_callback(update: Update, context: ContextTypes.DEFAULT
)
return ConversationHandler.END
- elif query.data == "tutorial_commands": # Todo: make it reusable
+ elif query.data == "tutorial_commands":
await query.edit_message_text(
- "Available Commands:\n\n"
- "/chart - Get a chart with indicators\n"
- "/text_result - Get a text analysis\n"
- "/preferences - Set indicator preferences\n"
- "/parameters - Fine-tune indicator settings\n"
- "/create_signal - Set up automatic signals\n"
- "/manage_signals - Manage your signal alerts\n"
- "/help - Get help with commands\n"
- "/tutorial - Revisit this tutorial",
+ f"{t('tutorial.commands_list.title', lang)}\n\n{t('tutorial.commands_list.content', lang)}",
parse_mode="HTML",
reply_markup=InlineKeyboardMarkup(
[
[
InlineKeyboardButton(
- "Back to Tutorial", callback_data="tutorial_back_to_end"
+ t("tutorial.buttons.back_to_tutorial", lang),
+ callback_data="tutorial_back_to_end"
)
]
]
@@ -262,7 +227,7 @@ async def handle_tutorial_callback(update: Update, context: ContextTypes.DEFAULT
)
elif query.data == "tutorial_back_to_end":
- tutorial = TUTORIAL_MESSAGES[TUTORIAL_COMPLETED]
+ tutorial = get_tutorial_message(TUTORIAL_COMPLETED, lang)
await query.edit_message_text(
f"{tutorial['title']}\n\n{tutorial['content']}",
reply_markup=InlineKeyboardMarkup(tutorial["buttons"]),
@@ -271,8 +236,7 @@ async def handle_tutorial_callback(update: Update, context: ContextTypes.DEFAULT
elif query.data == "tutorial_finish":
await query.edit_message_text(
- "Happy trading! 📈\n\n"
- "Use /help anytime you need assistance.",
+ t("tutorial.happy_trading", lang),
parse_mode="HTML",
)
return ConversationHandler.END