1100b9eaf2
- XAU/XAG always route to Yahoo Finance and skip Twelve Data entirely (excluded from batch prefetch, served per-symbol via yfinance). - Daily chart cleanly skipped for gold/silver — engine drops it from MTF. - Telegram /scan limited to EURUSD, USDJPY, GBPUSD, XAUUSD. - SCAN_TIMEFRAMES reduced to single H1 primary; engine still pulls M15+H1+D1 internally via confirmation TFs (halves API credits per scan). - Default confirmation TFs swapped H4 -> D1 in config and main.py. - mtf_agreement_threshold kept at 2-of-3. - CLAUDE.md cleaned up — old XAUUSD/synthetic-data bug reports removed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
519 lines
22 KiB
Python
519 lines
22 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Forex Signal Intelligence Platform
|
|
====================================
|
|
Main entry point. Runs full analysis and outputs formatted signal reports.
|
|
|
|
Usage:
|
|
python main.py # Analyze all major pairs
|
|
python main.py --pairs EURUSD GBPUSD # Specific pairs
|
|
python main.py --timeframe H4 # Different primary TF
|
|
python main.py --json # JSON output
|
|
python main.py --watch # Continuous monitoring mode
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
import logging
|
|
import sys
|
|
import time
|
|
from datetime import datetime, timezone
|
|
from typing import List
|
|
|
|
# Force UTF-8 output on Windows so box-drawing / Unicode chars render correctly
|
|
if hasattr(sys.stdout, "reconfigure"):
|
|
sys.stdout.reconfigure(encoding="utf-8")
|
|
if hasattr(sys.stderr, "reconfigure"):
|
|
sys.stderr.reconfigure(encoding="utf-8")
|
|
|
|
from config import (
|
|
PlatformConfig, Timeframe, SignalStrength,
|
|
MAJOR_PAIRS, CROSS_PAIRS,
|
|
)
|
|
from signal_aggregator import SignalAggregator, SignalReport
|
|
|
|
|
|
# ══════════════════════════════════════════════════════════════════
|
|
# TERMINAL FORMATTING
|
|
# ══════════════════════════════════════════════════════════════════
|
|
|
|
class Colors:
|
|
"""ANSI color codes for terminal output."""
|
|
RESET = "\033[0m"
|
|
BOLD = "\033[1m"
|
|
DIM = "\033[2m"
|
|
UNDERLINE = "\033[4m"
|
|
|
|
RED = "\033[91m"
|
|
GREEN = "\033[92m"
|
|
YELLOW = "\033[93m"
|
|
BLUE = "\033[94m"
|
|
MAGENTA = "\033[95m"
|
|
CYAN = "\033[96m"
|
|
WHITE = "\033[97m"
|
|
GRAY = "\033[90m"
|
|
|
|
BG_RED = "\033[41m"
|
|
BG_GREEN = "\033[42m"
|
|
BG_YELLOW = "\033[43m"
|
|
BG_BLUE = "\033[44m"
|
|
BG_MAGENTA = "\033[45m"
|
|
BG_CYAN = "\033[46m"
|
|
|
|
|
|
def signal_color(signal: SignalStrength) -> str:
|
|
"""Get color for a signal strength."""
|
|
return {
|
|
SignalStrength.STRONG_BUY: Colors.GREEN + Colors.BOLD,
|
|
SignalStrength.BUY: Colors.GREEN,
|
|
SignalStrength.NEUTRAL: Colors.GRAY,
|
|
SignalStrength.SELL: Colors.RED,
|
|
SignalStrength.STRONG_SELL: Colors.RED + Colors.BOLD,
|
|
}.get(signal, Colors.RESET)
|
|
|
|
|
|
def quality_color(quality: str) -> str:
|
|
"""Get color for signal quality grade."""
|
|
return {
|
|
"A+": Colors.GREEN + Colors.BOLD,
|
|
"A": Colors.GREEN,
|
|
"B": Colors.CYAN,
|
|
"C": Colors.YELLOW,
|
|
"D": Colors.GRAY,
|
|
"F": Colors.DIM,
|
|
}.get(quality, Colors.RESET)
|
|
|
|
|
|
def score_bar(score: float, width: int = 30) -> str:
|
|
"""Create a visual score bar from -100 to +100."""
|
|
normalized = (score + 100) / 200 # 0 to 1
|
|
filled = int(normalized * width)
|
|
center = width // 2
|
|
|
|
bar = ""
|
|
for i in range(width):
|
|
if i == center:
|
|
bar += "│"
|
|
elif (score >= 0 and center < i <= filled) or \
|
|
(score < 0 and filled <= i < center):
|
|
bar += "█"
|
|
else:
|
|
bar += "░"
|
|
|
|
if score >= 0:
|
|
color = Colors.GREEN if score > 20 else Colors.GRAY
|
|
else:
|
|
color = Colors.RED if score < -20 else Colors.GRAY
|
|
|
|
return f"{color}{bar}{Colors.RESET}"
|
|
|
|
|
|
def print_header():
|
|
"""Print the platform header."""
|
|
now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC")
|
|
width = 80
|
|
|
|
print(f"\n{Colors.CYAN}{'═' * width}{Colors.RESET}")
|
|
print(f"{Colors.BOLD}{Colors.CYAN} ╔═══════════════════════════════════════════════════════╗{Colors.RESET}")
|
|
print(f"{Colors.BOLD}{Colors.CYAN} ║ FOREX SIGNAL INTELLIGENCE PLATFORM ║{Colors.RESET}")
|
|
print(f"{Colors.BOLD}{Colors.CYAN} ║ Multi-Layer Signal Analysis Engine ║{Colors.RESET}")
|
|
print(f"{Colors.BOLD}{Colors.CYAN} ╚═══════════════════════════════════════════════════════╝{Colors.RESET}")
|
|
print(f"{Colors.GRAY} Scan Time: {now}{Colors.RESET}")
|
|
print(f"{Colors.CYAN}{'═' * width}{Colors.RESET}\n")
|
|
|
|
|
|
def print_summary_table(reports: List[SignalReport]):
|
|
"""Print a summary table of all pairs."""
|
|
print(f"{Colors.BOLD}{Colors.WHITE} ┌─────────────┬───────────┬──────────────────────────────────┬────────┬────────────┐{Colors.RESET}")
|
|
print(f"{Colors.BOLD}{Colors.WHITE} │ Pair │ Signal │ Score │ Grade │ Confidence │{Colors.RESET}")
|
|
print(f"{Colors.BOLD}{Colors.WHITE} ├─────────────┼───────────┼──────────────────────────────────┼────────┼────────────┤{Colors.RESET}")
|
|
|
|
for r in reports:
|
|
sig = r.final_signal.name.replace("_", " ")
|
|
sc = signal_color(r.final_signal)
|
|
qc = quality_color(r.signal_quality)
|
|
bar = score_bar(r.final_score, 30)
|
|
|
|
pair = f"{r.display_name:<11}"
|
|
signal_str = f"{sc}{sig:<9}{Colors.RESET}"
|
|
grade_str = f"{qc}{r.signal_quality:<6}{Colors.RESET}"
|
|
conf_str = f"{r.final_confidence:5.1f}%"
|
|
|
|
# Add warning indicator
|
|
warn = ""
|
|
if r.event_risk == "danger":
|
|
warn = f" {Colors.RED}⚠{Colors.RESET}"
|
|
elif r.event_risk == "caution":
|
|
warn = f" {Colors.YELLOW}△{Colors.RESET}"
|
|
|
|
print(f" │ {pair} │ {signal_str} │ {bar} │ {grade_str} │ {conf_str:>8}{warn} │")
|
|
|
|
print(f"{Colors.BOLD}{Colors.WHITE} └─────────────┴───────────┴──────────────────────────────────┴────────┴────────────┘{Colors.RESET}")
|
|
|
|
# Legend
|
|
print(f"\n{Colors.GRAY} Legend: "
|
|
f"{Colors.GREEN}██ Bullish{Colors.GRAY} | "
|
|
f"{Colors.RED}██ Bearish{Colors.GRAY} | "
|
|
f"│ Center (0) | "
|
|
f"{Colors.RED}⚠ Event Risk{Colors.GRAY} | "
|
|
f"{Colors.YELLOW}△ Caution{Colors.RESET}")
|
|
|
|
|
|
def print_detailed_report(report: SignalReport):
|
|
"""Print detailed analysis for a single pair."""
|
|
sc = signal_color(report.final_signal)
|
|
qc = quality_color(report.signal_quality)
|
|
sig_name = report.final_signal.name.replace("_", " ")
|
|
|
|
print(f"\n{Colors.CYAN}{'─' * 80}{Colors.RESET}")
|
|
print(f" {Colors.BOLD}{Colors.WHITE}{report.display_name}{Colors.RESET}"
|
|
f" │ Price: {Colors.BOLD}{report.current_price:.5f}{Colors.RESET}"
|
|
f" │ Regime: {Colors.CYAN}{report.market_regime}{Colors.RESET}"
|
|
f" │ Vol: {report.volatility_regime}")
|
|
|
|
print(f"\n {Colors.BOLD}SIGNAL:{Colors.RESET} {sc}{sig_name}{Colors.RESET}"
|
|
f" │ Score: {sc}{report.final_score:+.1f}{Colors.RESET}"
|
|
f" │ Grade: {qc}{report.signal_quality}{Colors.RESET}"
|
|
f" │ Confidence: {report.final_confidence:.0f}%")
|
|
|
|
print(f" {Colors.BOLD}Action:{Colors.RESET} {report.action_recommendation}")
|
|
|
|
# Multi-timeframe breakdown
|
|
if report.mtf_signals:
|
|
print(f"\n {Colors.BOLD}Multi-Timeframe:{Colors.RESET}", end="")
|
|
mtf_agree = f" {Colors.GREEN}✓ ALIGNED{Colors.RESET}" if report.mtf_agreement else f" {Colors.YELLOW}✗ DIVERGENT{Colors.RESET}"
|
|
print(mtf_agree)
|
|
for tf, sig in report.mtf_signals.items():
|
|
tf_sc = signal_color(sig)
|
|
tf_score = report.mtf_scores.get(tf, 0)
|
|
print(f" {tf:>5}: {tf_sc}{sig.name.replace('_', ' '):<12}{Colors.RESET} ({tf_score:+.1f})")
|
|
|
|
# Indicators detail
|
|
if report.primary_analysis:
|
|
print(f"\n {Colors.BOLD}Indicators ({report.primary_analysis.timeframe}):{Colors.RESET}")
|
|
for name, ind in report.primary_analysis.indicators.items():
|
|
if name.startswith("_") or ind.weight == 0:
|
|
continue
|
|
isc = signal_color(ind.signal)
|
|
ind_name = f"{ind.name:<18}"
|
|
print(f" {ind_name} {isc}{ind.signal.name.replace('_', ' '):<12}{Colors.RESET}"
|
|
f" ({ind.confidence:.0f}%) │ {Colors.GRAY}{ind.description}{Colors.RESET}")
|
|
|
|
# Bullish reasons
|
|
if report.bullish_reasons:
|
|
print(f"\n {Colors.GREEN}▲ Bullish Factors:{Colors.RESET}")
|
|
for reason in report.bullish_reasons[:5]:
|
|
print(f" {Colors.GREEN}+{Colors.RESET} {reason}")
|
|
|
|
# Bearish reasons
|
|
if report.bearish_reasons:
|
|
print(f"\n {Colors.RED}▼ Bearish Factors:{Colors.RESET}")
|
|
for reason in report.bearish_reasons[:5]:
|
|
print(f" {Colors.RED}-{Colors.RESET} {reason}")
|
|
|
|
# Warnings
|
|
if report.warnings:
|
|
print(f"\n {Colors.YELLOW}⚠ Warnings:{Colors.RESET}")
|
|
for warning in report.warnings:
|
|
print(f" {Colors.YELLOW}!{Colors.RESET} {warning}")
|
|
|
|
# News context
|
|
if report.news_description and report.news_description != "Neutral news sentiment":
|
|
print(f"\n {Colors.MAGENTA}📰 News:{Colors.RESET} {report.news_description}")
|
|
if report.event_description and report.event_risk != "clear":
|
|
print(f" {Colors.YELLOW}📅 Event:{Colors.RESET} {report.event_description}")
|
|
|
|
# Trade setup
|
|
if report.trade_setup:
|
|
ts = report.trade_setup
|
|
dir_color = Colors.GREEN if ts.direction == "BUY" else Colors.RED
|
|
print(f"\n {Colors.BOLD}Trade Setup:{Colors.RESET}")
|
|
print(f" Direction: {dir_color}{ts.direction}{Colors.RESET}")
|
|
print(f" Entry: {ts.entry_price:.5f}")
|
|
print(f" Stop Loss: {Colors.RED}{ts.stop_loss:.5f}{Colors.RESET} ({ts.risk_pips:.1f} pips)")
|
|
print(f" Take Profit 1: {Colors.GREEN}{ts.take_profit_1:.5f}{Colors.RESET} ({ts.reward_pips:.1f} pips)")
|
|
print(f" Take Profit 2: {Colors.GREEN}{ts.take_profit_2:.5f}{Colors.RESET}")
|
|
print(f" Risk:Reward: 1:{ts.risk_reward:.1f}")
|
|
|
|
# Key levels
|
|
if report.primary_analysis and report.primary_analysis.key_levels:
|
|
levels = report.primary_analysis.key_levels
|
|
print(f"\n {Colors.BOLD}Key Levels:{Colors.RESET}")
|
|
level_names = ["r3", "r2", "r1", "pivot", "s1", "s2", "s3"]
|
|
for lvl in level_names:
|
|
if lvl in levels:
|
|
val = levels[lvl]
|
|
if val > report.current_price:
|
|
color = Colors.RED # resistance
|
|
else:
|
|
color = Colors.GREEN # support
|
|
label = lvl.upper()
|
|
print(f" {label:>6}: {color}{val:.5f}{Colors.RESET}")
|
|
|
|
|
|
def print_event_calendar(reports: List[SignalReport]):
|
|
"""Print upcoming economic events affecting watched pairs."""
|
|
events_shown = set()
|
|
has_events = False
|
|
|
|
for report in reports:
|
|
if report.event_risk != "clear" and report.event_description not in events_shown:
|
|
if not has_events:
|
|
print(f"\n{Colors.YELLOW}{Colors.BOLD} 📅 ECONOMIC CALENDAR ALERTS{Colors.RESET}")
|
|
has_events = True
|
|
events_shown.add(report.event_description)
|
|
risk_color = Colors.RED if report.event_risk == "danger" else Colors.YELLOW
|
|
print(f" {risk_color}● {report.event_description}{Colors.RESET} → affects {report.display_name}")
|
|
|
|
if not has_events:
|
|
print(f"\n{Colors.GRAY} 📅 No high-impact events in the near term{Colors.RESET}")
|
|
|
|
|
|
def report_to_dict(report: SignalReport) -> dict:
|
|
"""Convert a SignalReport to a JSON-serializable dict."""
|
|
d = {
|
|
"symbol": report.symbol,
|
|
"display_name": report.display_name,
|
|
"timestamp": report.timestamp,
|
|
"current_price": report.current_price,
|
|
"signal": report.final_signal.name,
|
|
"score": round(report.final_score, 2),
|
|
"confidence": round(report.final_confidence, 2),
|
|
"quality": report.signal_quality,
|
|
"action": report.action_recommendation,
|
|
"market_regime": report.market_regime,
|
|
"volatility_regime": report.volatility_regime,
|
|
"mtf_agreement": report.mtf_agreement,
|
|
"mtf_signals": {k: v.name for k, v in report.mtf_signals.items()},
|
|
"event_risk": report.event_risk,
|
|
"event_description": report.event_description,
|
|
"news_sentiment": round(report.news_sentiment, 3),
|
|
"bullish_reasons": report.bullish_reasons,
|
|
"bearish_reasons": report.bearish_reasons,
|
|
"warnings": report.warnings,
|
|
}
|
|
if report.trade_setup:
|
|
ts = report.trade_setup
|
|
d["trade_setup"] = {
|
|
"direction": ts.direction,
|
|
"entry": ts.entry_price,
|
|
"stop_loss": ts.stop_loss,
|
|
"take_profit_1": ts.take_profit_1,
|
|
"take_profit_2": ts.take_profit_2,
|
|
"risk_reward": ts.risk_reward,
|
|
"risk_pips": round(ts.risk_pips, 1),
|
|
"reward_pips": round(ts.reward_pips, 1),
|
|
}
|
|
if report.primary_analysis:
|
|
d["indicators"] = {}
|
|
for name, ind in report.primary_analysis.indicators.items():
|
|
if not name.startswith("_"):
|
|
d["indicators"][name] = {
|
|
"signal": ind.signal.name,
|
|
"confidence": round(ind.confidence, 1),
|
|
"description": ind.description,
|
|
}
|
|
d["key_levels"] = {
|
|
k: round(v, 5) for k, v in report.primary_analysis.key_levels.items()
|
|
}
|
|
return d
|
|
|
|
|
|
# ══════════════════════════════════════════════════════════════════
|
|
# MAIN
|
|
# ══════════════════════════════════════════════════════════════════
|
|
|
|
def parse_args():
|
|
parser = argparse.ArgumentParser(
|
|
description="Forex Signal Intelligence Platform",
|
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
epilog="""
|
|
Examples:
|
|
python main.py Analyze all major pairs (H1)
|
|
python main.py --pairs EURUSD GBPUSD Analyze specific pairs
|
|
python main.py --timeframe H4 Use 4-hour as primary TF
|
|
python main.py --all Include cross pairs
|
|
python main.py --json Output as JSON
|
|
python main.py --detail Show detailed analysis per pair
|
|
python main.py --watch Continuous monitoring (5min refresh)
|
|
""",
|
|
)
|
|
parser.add_argument(
|
|
"--pairs", nargs="+", default=None,
|
|
help="Specific pairs to analyze (e.g., EURUSD GBPUSD)",
|
|
)
|
|
parser.add_argument(
|
|
"--timeframe", "-tf", default="H1",
|
|
choices=["M5", "M15", "M30", "H1", "H4", "D1"],
|
|
help="Primary analysis timeframe (default: H1)",
|
|
)
|
|
parser.add_argument(
|
|
"--all", action="store_true",
|
|
help="Include cross pairs in analysis",
|
|
)
|
|
parser.add_argument(
|
|
"--json", action="store_true",
|
|
help="Output results as JSON",
|
|
)
|
|
parser.add_argument(
|
|
"--detail", action="store_true",
|
|
help="Show detailed analysis for each pair",
|
|
)
|
|
parser.add_argument(
|
|
"--top", type=int, default=None,
|
|
help="Show only top N signals by strength",
|
|
)
|
|
parser.add_argument(
|
|
"--watch", action="store_true",
|
|
help="Continuous monitoring mode (refreshes every 5 min)",
|
|
)
|
|
parser.add_argument(
|
|
"--min-grade", default="F",
|
|
choices=["A+", "A", "B", "C", "D", "F"],
|
|
help="Minimum signal quality to display",
|
|
)
|
|
parser.add_argument(
|
|
"--verbose", "-v", action="store_true",
|
|
help="Enable debug logging",
|
|
)
|
|
return parser.parse_args()
|
|
|
|
|
|
def symbol_from_name(name: str) -> str:
|
|
"""
|
|
Convert pair name like 'EURUSD' or 'XAUUSD' to internal symbol 'EURUSD=X'.
|
|
Returns None for clearly invalid inputs (e.g. timeframe strings like '15M').
|
|
"""
|
|
clean = name.upper().replace("/", "").replace("_", "").replace("=X", "")
|
|
if not clean.isalpha() or len(clean) < 5:
|
|
print(f" Warning: '{name}' is not a valid pair symbol — skipping.")
|
|
print(f" Hint: timeframes go in --timeframe (e.g. --timeframe M15), not --pairs.")
|
|
return None
|
|
return clean + "=X"
|
|
|
|
|
|
def run_analysis(args) -> List[SignalReport]:
|
|
"""Run a single analysis cycle."""
|
|
config = PlatformConfig()
|
|
|
|
# Configure timeframe
|
|
tf_map = {
|
|
"M5": Timeframe.M5, "M15": Timeframe.M15, "M30": Timeframe.M30,
|
|
"H1": Timeframe.H1, "H4": Timeframe.H4, "D1": Timeframe.D1,
|
|
}
|
|
config.primary_timeframe = tf_map[args.timeframe]
|
|
|
|
# Configure confirmation timeframes based on primary.
|
|
# H1 uses M15 + D1 for high-conviction confluence (3-of-3 threshold).
|
|
tf_confirmations = {
|
|
"M5": [Timeframe.M1, Timeframe.M15],
|
|
"M15": [Timeframe.H1, Timeframe.D1],
|
|
"M30": [Timeframe.H1, Timeframe.D1],
|
|
"H1": [Timeframe.M15, Timeframe.D1],
|
|
"H4": [Timeframe.H1, Timeframe.D1],
|
|
"D1": [Timeframe.H4, Timeframe.W1],
|
|
}
|
|
config.confirmation_timeframes = tf_confirmations.get(
|
|
args.timeframe, [Timeframe.M15, Timeframe.D1]
|
|
)
|
|
|
|
# Configure pairs
|
|
if args.pairs:
|
|
symbols = [symbol_from_name(p) for p in args.pairs]
|
|
config.watch_pairs = [s for s in symbols if s is not None]
|
|
if not config.watch_pairs:
|
|
print(" Error: No valid pair symbols provided. Exiting.")
|
|
sys.exit(1)
|
|
elif args.all:
|
|
config.watch_pairs = MAJOR_PAIRS + CROSS_PAIRS
|
|
else:
|
|
config.watch_pairs = MAJOR_PAIRS
|
|
|
|
# Run analysis
|
|
aggregator = SignalAggregator(config)
|
|
reports = aggregator.analyze_all_pairs()
|
|
|
|
# Filter by grade
|
|
grade_order = ["A+", "A", "B", "C", "D", "F"]
|
|
min_idx = grade_order.index(args.min_grade) if args.min_grade in grade_order else len(grade_order) - 1
|
|
reports = [r for r in reports if grade_order.index(r.signal_quality) <= min_idx]
|
|
|
|
# Top N
|
|
if args.top:
|
|
reports = reports[:args.top]
|
|
|
|
return reports
|
|
|
|
|
|
def main():
|
|
args = parse_args()
|
|
|
|
# Setup logging
|
|
log_level = logging.DEBUG if args.verbose else logging.WARNING
|
|
logging.basicConfig(
|
|
level=log_level,
|
|
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
|
datefmt="%H:%M:%S",
|
|
)
|
|
|
|
if args.watch:
|
|
# Continuous monitoring mode
|
|
print(f"{Colors.BOLD}Starting continuous monitoring mode (Ctrl+C to stop)...{Colors.RESET}")
|
|
while True:
|
|
try:
|
|
reports = run_analysis(args)
|
|
if args.json:
|
|
output = [report_to_dict(r) for r in reports]
|
|
print(json.dumps(output, indent=2))
|
|
else:
|
|
print("\033[2J\033[H") # clear screen
|
|
print_header()
|
|
print_summary_table(reports)
|
|
print_event_calendar(reports)
|
|
if args.detail:
|
|
for r in reports:
|
|
print_detailed_report(r)
|
|
print(f"\n{Colors.GRAY} Next refresh in 5 minutes...{Colors.RESET}")
|
|
|
|
time.sleep(300)
|
|
except KeyboardInterrupt:
|
|
print(f"\n{Colors.YELLOW}Monitoring stopped.{Colors.RESET}")
|
|
break
|
|
else:
|
|
# Single run
|
|
print(f"{Colors.GRAY} Fetching market data and running analysis...{Colors.RESET}\n")
|
|
reports = run_analysis(args)
|
|
|
|
if args.json:
|
|
output = [report_to_dict(r) for r in reports]
|
|
print(json.dumps(output, indent=2))
|
|
else:
|
|
print_header()
|
|
if reports:
|
|
print_summary_table(reports)
|
|
print_event_calendar(reports)
|
|
|
|
if args.detail:
|
|
for r in reports:
|
|
print_detailed_report(r)
|
|
else:
|
|
# Show detail for top signals only
|
|
actionable = [r for r in reports if r.signal_quality in ("A+", "A", "B")]
|
|
if actionable:
|
|
print(f"\n{Colors.BOLD}{Colors.WHITE} ═══ ACTIONABLE SIGNALS (Grade B or above) ═══{Colors.RESET}")
|
|
for r in actionable:
|
|
print_detailed_report(r)
|
|
else:
|
|
print(f"\n{Colors.GRAY} No high-grade signals detected. "
|
|
f"Use --detail for full breakdown or --min-grade C to see more.{Colors.RESET}")
|
|
else:
|
|
print(f"{Colors.YELLOW} No data available. Check your internet connection.{Colors.RESET}")
|
|
|
|
print(f"\n{Colors.CYAN}{'═' * 80}{Colors.RESET}")
|
|
print(f"{Colors.GRAY} Disclaimer: This tool is for educational/research purposes only.{Colors.RESET}")
|
|
print(f"{Colors.GRAY} Past performance ≠ future results. Always use proper risk management.{Colors.RESET}")
|
|
print(f"{Colors.CYAN}{'═' * 80}{Colors.RESET}\n")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main() |