diff --git a/__pycache__/config.cpython-314.pyc b/__pycache__/config.cpython-314.pyc new file mode 100644 index 0000000..9d6bec9 Binary files /dev/null and b/__pycache__/config.cpython-314.pyc differ diff --git a/__pycache__/data_provider.cpython-314.pyc b/__pycache__/data_provider.cpython-314.pyc new file mode 100644 index 0000000..d62f771 Binary files /dev/null and b/__pycache__/data_provider.cpython-314.pyc differ diff --git a/__pycache__/engine.cpython-314.pyc b/__pycache__/engine.cpython-314.pyc new file mode 100644 index 0000000..94dc90f Binary files /dev/null and b/__pycache__/engine.cpython-314.pyc differ diff --git a/__pycache__/news_engine.cpython-314.pyc b/__pycache__/news_engine.cpython-314.pyc new file mode 100644 index 0000000..669b8b4 Binary files /dev/null and b/__pycache__/news_engine.cpython-314.pyc differ diff --git a/__pycache__/signal_aggregator.cpython-314.pyc b/__pycache__/signal_aggregator.cpython-314.pyc new file mode 100644 index 0000000..19acf2f Binary files /dev/null and b/__pycache__/signal_aggregator.cpython-314.pyc differ diff --git a/config.py b/config.py index de464f5..8bb648b 100644 --- a/config.py +++ b/config.py @@ -76,7 +76,7 @@ class TechnicalConfig: rsi_neutral_upper: float = 60.0 rsi_neutral_lower: float = 40.0 - # MACD (12, 26, 9 is standard but we validate) + # MACD (12, 26, 9 is standard) macd_fast: int = 12 macd_slow: int = 26 macd_signal: int = 9 @@ -110,6 +110,9 @@ class TechnicalConfig: # Pivot points lookback pivot_lookback: int = 1 + # Divergence detection — how many bars back to scan for swing points + divergence_lookback: int = 14 + @dataclass class SignalConfig: @@ -130,10 +133,6 @@ class SignalConfig: # Multi-timeframe confirmation requirements mtf_agreement_threshold: int = 2 # out of 3 timeframes must agree - # Divergence detection sensitivity - divergence_lookback: int = 14 - divergence_min_swing: float = 0.001 # minimum price swing for valid divergence - # Signal cooldown (prevent over-signalling same direction) cooldown_bars: int = 5 @@ -185,10 +184,13 @@ class PlatformConfig: # Data source data_lookback_days: int = 60 # enough for 200 EMA on H1 + # Twelve Data API key (overridden by TWELVE_DATA_API_KEY env var) + twelve_data_api_key: str = "f563b22ffdfd4924955decbe1a8c8701" + # Dashboard refresh interval (seconds) refresh_interval: int = 300 # 5 minutes # Logging log_level: str = "INFO" log_signals_to_file: bool = True - signal_log_path: str = "signals.log" \ No newline at end of file + signal_log_path: str = "signals.log" diff --git a/signal_aggregator.py b/signal_aggregator.py index 8a474e6..4010e29 100644 --- a/signal_aggregator.py +++ b/signal_aggregator.py @@ -16,16 +16,16 @@ Key principles: import logging from dataclasses import dataclass, field from datetime import datetime, timezone -from typing import Dict, List, Optional, Tuple +from typing import Dict, List, Optional import pandas as pd from config import ( PlatformConfig, SignalStrength, Timeframe, - SYMBOL_DISPLAY, MAJOR_PAIRS, CROSS_PAIRS, + SYMBOL_DISPLAY, ) from data_provider import ForexDataProvider -from technical_engine import TechnicalEngine, TechnicalAnalysis +from engine import TechnicalEngine, TechnicalAnalysis from news_engine import NewsEngine, FundamentalContext logger = logging.getLogger(__name__) @@ -99,7 +99,7 @@ class SignalReport: class SignalAggregator: """ Master signal aggregator. - + Orchestrates data fetching, technical analysis across timeframes, fundamental analysis, and produces final graded signals. """ @@ -118,6 +118,12 @@ class SignalAggregator: reports = [] context = self.news_engine.get_fundamental_context() + # Batch-prefetch all pairs × all needed timeframes (3 API calls instead of 21) + all_timeframes = ( + [self.config.primary_timeframe] + self.config.confirmation_timeframes + ) + self.data_provider.prefetch_batch(self.config.watch_pairs, all_timeframes) + for symbol in self.config.watch_pairs: try: report = self.analyze_pair(symbol, context) @@ -135,9 +141,7 @@ class SignalAggregator: symbol: str, context: Optional[FundamentalContext] = None, ) -> SignalReport: - """ - Complete analysis of a single pair across all layers. - """ + """Complete analysis of a single pair across all layers.""" if context is None: context = self.news_engine.get_fundamental_context() @@ -227,7 +231,7 @@ class SignalAggregator: def _compute_final_signal(self, report: SignalReport): """ Compute the final signal by combining all layers. - + Priority: 1. Event risk can downgrade or block signals 2. MTF agreement boosts confidence @@ -239,9 +243,8 @@ class SignalAggregator: # ── MTF adjustment ───────────────────────────────────────── if report.mtf_agreement: - # All timeframes agree — boost confidence + # All timeframes agree — boost confidence and blend scores confidence = min(confidence + 15, 95) - # Average the MTF scores for more stable signal all_scores = list(report.mtf_scores.values()) if all_scores: mtf_avg = sum(all_scores) / len(all_scores) @@ -251,7 +254,6 @@ class SignalAggregator: confidence *= 0.7 if len(report.mtf_scores) >= 2: scores = list(report.mtf_scores.values()) - # Check for conflicting directions has_bull = any(s > 10 for s in scores) has_bear = any(s < -10 for s in scores) if has_bull and has_bear: @@ -259,7 +261,6 @@ class SignalAggregator: confidence *= 0.6 # ── News sentiment adjustment ────────────────────────────── - sent_weight = self.config.signals.weights.get("sentiment", 0.1) if abs(report.news_sentiment) > 0.2: # Sentiment aligns with technical if (report.news_sentiment > 0 and score > 0) or \ @@ -274,7 +275,7 @@ class SignalAggregator: # ── Event risk adjustment ────────────────────────────────── if report.event_risk == "danger": report.warnings.append(f"EVENT RISK: {report.event_description}") - confidence *= 0.3 # Heavily penalize + confidence *= 0.3 # Heavily penalize — event will disrupt price action report.action_recommendation = f"WAIT — {report.event_description}" elif report.event_risk == "caution": report.warnings.append(f"Upcoming event: {report.event_description}") @@ -325,7 +326,7 @@ class SignalAggregator: elif report.signal_quality == "B": report.action_recommendation = f"{sig_name} — Decent setup, wait for confirmation" elif report.signal_quality in ("C", "D"): - report.action_recommendation = f"WATCH — Signal present but low confidence" + report.action_recommendation = "WATCH — Signal present but low confidence" else: report.action_recommendation = "NO TRADE — No clear signal" @@ -379,7 +380,14 @@ class SignalAggregator: ) def _collect_reasons(self, report: SignalReport): - """Collect bullish/bearish reasons from all indicators.""" + """ + Collect bullish/bearish reasons from all indicators. + + Each indicator's description is captured once via the indicators + dict. Pattern signals (candlestick) are already included in the + 'candlestick' indicator description, so we do NOT re-process + pattern_signals to avoid duplicate entries. + """ if not report.primary_analysis: return @@ -394,15 +402,8 @@ class SignalAggregator: prefix = "STRONG: " if ind.signal == SignalStrength.STRONG_SELL else "" report.bearish_reasons.append(f"{prefix}{ind.name}: {ind.description}") - # Add pattern signals - for pattern in report.primary_analysis.pattern_signals: - if any(kw in pattern.lower() for kw in ["bullish", "hammer", "buy"]): - report.bullish_reasons.append(f"Pattern: {pattern}") - elif any(kw in pattern.lower() for kw in ["bearish", "shooting", "sell"]): - report.bearish_reasons.append(f"Pattern: {pattern}") - # Add news context if report.news_sentiment > 0.1: report.bullish_reasons.append(f"News: {report.news_description}") elif report.news_sentiment < -0.1: - report.bearish_reasons.append(f"News: {report.news_description}") \ No newline at end of file + report.bearish_reasons.append(f"News: {report.news_description}")