"""Universal Screener CLI — akses semua 100+ screener service via satu command.

Satu CLI untuk semua screener, menggantikan kebutuhan 100+ script individual.
Mendukung tiga pola service: get_candidates(), scan_all(), dan method khusus.

Usage:
    # List semua screener yang tersedia
    python3 scripts/screener.py --list

    # Jalankan screener dengan default params
    python3 scripts/screener.py gem-finder
    python3 scripts/screener.py oversold-bounce

    # Dengan filter & options
    python3 scripts/screener.py gem-finder --asset-type crypto --limit 10 --sort gem_score_desc
    python3 scripts/screener.py oversold-bounce --filter STRONG --limit 5
    python3 scripts/screener.py momentum-heatmap --asset-type stock

    # Output JSON (untuk pipeline/cron)
    python3 scripts/screener.py gem-finder --json

    # Quiet mode (summary only)
    python3 scripts/screener.py gem-finder --quiet

    # Analyze single coin (services yang support single-coin analysis)
    python3 scripts/screener.py supply-demand --symbol COIN.bitcoin

    # Extra service-specific param
    python3 scripts/screener.py momentum-heatmap --extra timeframe=1h,group_by=sector

Cron example:
    # Top gems setiap 6 jam
    0 */6 * * * cd /path && .venv/bin/python scripts/screener.py gem-finder --limit 20 --quiet >> logs/screener.log 2>&1
"""
from __future__ import annotations

import argparse
import importlib
import json
import math
import os
import sys
import time

sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

from dotenv import load_dotenv
load_dotenv()


# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
#  REGISTRY — semua screener service yang tersedia
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

# Pattern: 'command-name': {
#   'module': 'app.services.xxx',
#   'class': 'XxxService',
#   'method': 'scan_all' | 'get_candidates' | 'get_xxx',
#   'pattern': 'scan_all' | 'get_candidates' | 'custom',
#   'filter_param': 'gem_tier' (service-specific filter name, optional),
#   'desc': 'Short description',
#   'has_analyze': True/False (supports single-coin --symbol mode),
# }

REGISTRY: dict[str, dict] = {
    # ── Pattern A: get_candidates() ──────────────────────────────────────
    'buy-recommendations': {
        'module': 'app.services.buy_recommender',
        'class': 'BuyRecommender',
        'method': 'get_recommendations',
        'pattern': 'get_candidates',
        'filter_param': 'safety_filter',
        'desc': 'Top buy recommendations dengan safety & success rate',
    },
    'oversold-bounce': {
        'module': 'app.services.oversold_bounce_screener',
        'class': 'OversoldBounceScreener',
        'method': 'get_candidates',
        'pattern': 'get_candidates',
        'filter_param': 'tier_filter',
        'desc': 'Deeply oversold assets ready to bounce (1-3 day play)',
    },
    'breakout': {
        'module': 'app.services.breakout_detector',
        'class': 'BreakoutDetector',
        'method': 'get_candidates',
        'pattern': 'get_candidates',
        'filter_param': 'tier_filter',
        'desc': 'Breakout detection dari consolidation zones',
    },
    'passive-income': {
        'module': 'app.services.passive_income_screener',
        'class': 'PassiveIncomeScreener',
        'method': 'get_candidates',
        'pattern': 'get_candidates',
        'filter_param': 'tier_filter',
        'desc': 'Passive income opportunities (DCA + yield)',
    },
    'smart-accumulation': {
        'module': 'app.services.smart_accumulation_screener',
        'class': 'SmartAccumulationScreener',
        'method': 'get_candidates',
        'pattern': 'get_candidates',
        'filter_param': 'tier_filter',
        'desc': 'Smart accumulation zones for long-term buys',
    },
    'swing-trade': {
        'module': 'app.services.swing_trade_screener',
        'class': 'SwingTradeScreener',
        'method': 'get_candidates',
        'pattern': 'get_candidates',
        'filter_param': 'tier_filter',
        'desc': 'Swing trade setups (multi-day holds)',
    },
    'sell-signal': {
        'module': 'app.services.sell_signal_screener',
        'class': 'SellSignalScreener',
        'method': 'get_candidates',
        'pattern': 'get_candidates',
        'filter_param': 'tier_filter',
        'desc': 'Sell signal detection — time to take profit?',
    },
    'portfolio-health': {
        'module': 'app.services.portfolio_health_monitor',
        'class': 'PortfolioHealthMonitor',
        'method': 'get_candidates',
        'pattern': 'get_candidates',
        'filter_param': 'health_filter',
        'desc': 'Portfolio health assessment per coin',
    },
    'meta': {
        'module': 'app.services.meta_screener',
        'class': 'MetaScreener',
        'method': 'get_candidates',
        'pattern': 'get_candidates',
        'filter_param': 'tier_filter',
        'desc': 'Meta-screener — aggregate dari semua screener',
    },
    'dca': {
        'module': 'app.services.dca_optimizer',
        'class': 'DCAOptimizer',
        'method': 'get_candidates',
        'pattern': 'get_candidates',
        'filter_param': 'tier_filter',
        'desc': 'DCA optimizer — best coins for dollar-cost averaging',
    },

    # ── Pattern C: scan_all() — Wave 10-22 services ─────────────────────

    # Wave 10-11: Core screeners
    'wealth-dashboard': {
        'module': 'app.services.wealth_dashboard',
        'class': 'WealthDashboardService',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'filter_param': 'timeframe',
        'desc': 'Wealth dashboard — portfolio overview per coin',
    },
    'income-streams': {
        'module': 'app.services.income_stream_mapper',
        'class': 'IncomeStreamMapperService',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'filter_param': 'strategy',
        'desc': 'Income stream opportunities per asset',
    },
    'whale-momentum': {
        'module': 'app.services.whale_momentum_decoder',
        'class': 'WhaleMomentumDecoderService',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'filter_param': 'activity_type',
        'desc': 'Whale activity & momentum decoding',
    },
    'momentum-cascade': {
        'module': 'app.services.momentum_cascade_finder',
        'class': 'MomentumCascadeFinderService',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'filter_param': 'alignment',
        'desc': 'Multi-timeframe momentum cascade detection',
    },
    'position-architect': {
        'module': 'app.services.position_architect',
        'class': 'PositionArchitectService',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'filter_param': 'signal_status',
        'desc': 'Position sizing & architecture recommendations',
    },
    'market-cycle': {
        'module': 'app.services.market_cycle_navigator',
        'class': 'MarketCycleNavigatorService',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'filter_param': 'phase',
        'desc': 'Market cycle phase detection per asset',
    },
    'crypto-season': {
        'module': 'app.services.crypto_season_detector',
        'class': 'CryptoSeasonDetectorService',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'filter_param': 'season',
        'desc': 'Crypto season detection (alt season, BTC season, etc)',
    },
    'alpha-generator': {
        'module': 'app.services.alpha_generator',
        'class': 'AlphaGeneratorService',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'filter_param': 'min_alpha',
        'desc': 'Alpha generation opportunities',
    },
    'entry-zones': {
        'module': 'app.services.entry_zone_mapper',
        'class': 'EntryZoneMapperService',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'filter_param': 'zone',
        'desc': 'Optimal entry zone mapping per asset',
    },
    'profit-lock': {
        'module': 'app.services.profit_lock_system',
        'class': 'ProfitLockSystemService',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'filter_param': 'action',
        'desc': 'Profit locking / trailing stop recommendations',
    },
    'probability-matrix': {
        'module': 'app.services.probability_matrix',
        'class': 'ProbabilityMatrixService',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'filter_param': 'tier',
        'desc': 'Win/loss probability matrix per asset',
    },
    'precision-timer': {
        'module': 'app.services.precision_timer',
        'class': 'PrecisionTimerService',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'filter_param': 'verdict',
        'desc': 'Precision timing — optimal trade entry/exit times',
    },
    'profit-velocity': {
        'module': 'app.services.profit_velocity',
        'class': 'ProfitVelocityService',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'filter_param': 'speed_tier',
        'desc': 'Profit velocity ranking — fastest movers',
    },
    'risk-thermometer': {
        'module': 'app.services.risk_thermometer',
        'class': 'RiskThermometerService',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'filter_param': 'temp_zone',
        'desc': 'Risk thermometer — temperature-based risk scoring',
    },
    'signal-convergence': {
        'module': 'app.services.signal_convergence',
        'class': 'SignalConvergenceService',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'filter_param': 'min_signals',
        'desc': 'Multi-signal convergence detector',
    },
    'wealth-compounder': {
        'module': 'app.services.wealth_compounder',
        'class': 'WealthCompounderService',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'filter_param': 'projection',
        'desc': 'Compound growth projection per asset',
    },
    'gem-finder': {
        'module': 'app.services.gem_finder',
        'class': 'GemFinderService',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'filter_param': 'gem_tier',
        'desc': 'Hidden gems — undervalued assets with massive upside',
    },
    'cycle-sync': {
        'module': 'app.services.cycle_sync',
        'class': 'CycleSyncService',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'filter_param': 'phase_filter',
        'desc': 'Cycle synchronization analysis',
    },
    'smart-sizing': {
        'module': 'app.services.smart_sizing',
        'class': 'SmartSizingService',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'filter_param': 'risk_profile',
        'desc': 'Smart position sizing recommendations',
    },
    'wealth-protector': {
        'module': 'app.services.wealth_protector',
        'class': 'WealthProtectorService',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'filter_param': 'action_filter',
        'desc': 'Wealth protection & hedging analysis',
    },
    'trade-journal': {
        'module': 'app.services.trade_journal',
        'class': 'TradeJournalService',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'filter_param': 'outcome_filter',
        'desc': 'Trade journal analytics per asset',
    },
    'exit-planner': {
        'module': 'app.services.exit_planner',
        'class': 'ExitPlannerService',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'filter_param': 'min_rr',
        'desc': 'Exit plan optimizer — TP/SL recommendations',
    },
    'dca-optimizer': {
        'module': 'app.services.dca_optimizer',
        'class': 'DCAOptimizerService',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'filter_param': 'tier',
        'desc': 'DCA strategy optimizer (scan_all version)',
    },
    'portfolio-rebalancer': {
        'module': 'app.services.portfolio_rebalancer',
        'class': 'PortfolioRebalancerService',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'filter_param': 'action',
        'desc': 'Portfolio rebalancing recommendations',
    },
    'profit-scheduler': {
        'module': 'app.services.profit_scheduler',
        'class': 'ProfitSchedulerService',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'filter_param': 'urgency',
        'desc': 'Profit-taking schedule optimizer',
    },
    'correlation-radar': {
        'module': 'app.services.correlation_radar',
        'class': 'CorrelationRadarService',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'filter_param': 'cluster',
        'desc': 'Correlation radar — asset group detection',
    },
    'regime-detector': {
        'module': 'app.services.regime_detector',
        'class': 'RegimeDetectorService',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'filter_param': 'regime',
        'desc': 'Market regime detection per asset',
    },
    'drawdown-shield': {
        'module': 'app.services.drawdown_shield',
        'class': 'DrawdownShieldService',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'filter_param': 'severity',
        'desc': 'Drawdown protection analysis',
    },
    'risk-parity': {
        'module': 'app.services.risk_parity',
        'class': 'RiskParityService',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'filter_param': 'quality',
        'desc': 'Risk parity allocation scoring',
    },
    'stress-tester': {
        'module': 'app.services.stress_tester',
        'class': 'StressTesterService',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'filter_param': 'scenario',
        'desc': 'Portfolio stress testing per asset',
    },
    'momentum-scanner': {
        'module': 'app.services.momentum_scanner',
        'class': 'MomentumScannerService',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'filter_param': 'tier_filter',
        'desc': 'Momentum scoring & tier classification',
    },
    'value-detector': {
        'module': 'app.services.value_detector',
        'class': 'ValueDetectorService',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'filter_param': 'valuation_filter',
        'desc': 'Value detection — undervalued asset finder',
    },
    'breakout-predictor': {
        'module': 'app.services.breakout_predictor',
        'class': 'BreakoutPredictorService',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'filter_param': 'direction',
        'desc': 'Breakout prediction with ML scoring',
    },
    'support-resistance-map': {
        'module': 'app.services.support_resistance_map',
        'class': 'SupportResistanceMapService',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'filter_param': 'zone',
        'desc': 'Support & resistance level mapping',
    },
    'trend-reversal': {
        'module': 'app.services.trend_reversal',
        'class': 'TrendReversalService',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'filter_param': 'signal_type',
        'desc': 'Trend reversal signal detection',
    },
    'volatility-rank': {
        'module': 'app.services.volatility_rank',
        'class': 'VolatilityRankService',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'filter_param': 'regime',
        'desc': 'Volatility ranking & regime classification',
    },
    'price-action': {
        'module': 'app.services.price_action',
        'class': 'PriceActionService',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'filter_param': 'pattern',
        'desc': 'Price action pattern recognition',
    },
    'market-microstructure': {
        'module': 'app.services.market_microstructure',
        'class': 'MarketMicrostructureService',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'filter_param': 'tier',
        'desc': 'Market microstructure analysis',
    },
    'pair-trader': {
        'module': 'app.services.pair_trader',
        'class': 'PairTraderService',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'filter_param': 'status_filter',
        'desc': 'Pair trading opportunities',
    },
    'alpha-composite': {
        'module': 'app.services.alpha_composite',
        'class': 'AlphaCompositeService',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'filter_param': 'grade_filter',
        'desc': 'Composite alpha scoring from all signals',
    },
    'multibagger': {
        'module': 'app.services.multibagger_screener',
        'class': 'MultibaggerScreener',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'filter_param': 'tier_filter',
        'desc': 'Multibagger candidates — 2x-10x potential',
    },
    'position-sizer': {
        'module': 'app.services.position_sizer',
        'class': 'PositionSizerService',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'filter_param': 'sizing_filter',
        'desc': 'Position sizing optimizer per asset',
    },
    'profit-target': {
        'module': 'app.services.profit_target',
        'class': 'ProfitTargetService',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'filter_param': 'target_filter',
        'desc': 'Dynamic profit target calculation',
    },
    'dca-planner': {
        'module': 'app.services.dca_planner',
        'class': 'DCAStrategyPlanner',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'filter_param': 'zone_filter',
        'desc': 'DCA strategy planner with zones',
    },
    'risk-reward-matrix': {
        'module': 'app.services.risk_reward_matrix',
        'class': 'RiskRewardMatrixService',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'filter_param': 'grade_filter',
        'desc': 'Risk/reward matrix analysis',
    },
    'compound-growth-sim': {
        'module': 'app.services.compound_growth_simulator',
        'class': 'CompoundGrowthSimulator',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'filter_param': 'growth_filter',
        'desc': 'Compound growth simulation per asset',
    },
    'confluence-radar': {
        'module': 'app.services.confluence_radar',
        'class': 'ConfluenceRadarService',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'filter_param': 'conviction_filter',
        'desc': 'Signal confluence radar — multi-indicator convergence',
    },
    'sector-rotation-tracker': {
        'module': 'app.services.sector_rotation_tracker',
        'class': 'SectorRotationTracker',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'filter_param': 'phase_filter',
        'desc': 'Sector rotation tracking & momentum',
    },
    'whale-accumulation': {
        'module': 'app.services.whale_accumulation',
        'class': 'WhaleAccumulationDetector',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'filter_param': 'signal_filter',
        'desc': 'Whale accumulation pattern detection',
    },
    'exit-optimizer': {
        'module': 'app.services.exit_optimizer',
        'class': 'ExitOptimizerService',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'filter_param': 'strategy_filter',
        'desc': 'Exit strategy optimizer',
    },

    # Wave 14-18: Technical analysis services (scan_all + analyze)
    'pair-trading': {
        'module': 'app.services.pair_trading_scanner',
        'class': 'PairTradingScannerService',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'desc': 'Statistical pair trading scanner',
    },
    'whale-tracker': {
        'module': 'app.services.whale_transaction_tracker',
        'class': 'WhaleTransactionTrackerService',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'desc': 'Whale transaction tracker',
    },
    'risk-parity-alloc': {
        'module': 'app.services.risk_parity_allocator',
        'class': 'RiskParityAllocatorService',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'desc': 'Risk parity portfolio allocation',
    },
    'market-breadth': {
        'module': 'app.services.market_breadth',
        'class': 'MarketBreadthService',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'desc': 'Market breadth indicators',
    },
    'supply-demand': {
        'module': 'app.services.supply_demand_zones',
        'class': 'SupplyDemandZoneService',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'has_analyze': True,
        'desc': 'Supply & demand zone mapping',
    },
    'multi-timeframe': {
        'module': 'app.services.multi_timeframe',
        'class': 'MultiTimeframeService',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'has_analyze': True,
        'desc': 'Multi-timeframe trend alignment',
    },
    'volume-profile': {
        'module': 'app.services.volume_profile',
        'class': 'VolumeProfileService',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'has_analyze': True,
        'desc': 'Volume profile analysis (POC, VAH, VAL)',
    },
    'chart-patterns': {
        'module': 'app.services.chart_patterns',
        'class': 'ChartPatternService',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'has_analyze': True,
        'desc': 'Classic chart pattern recognition',
    },
    'smart-money': {
        'module': 'app.services.smart_money',
        'class': 'SmartMoneyService',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'has_analyze': True,
        'desc': 'Smart money concept (ICT) analysis',
    },
    'mean-reversion': {
        'module': 'app.services.mean_reversion',
        'class': 'MeanReversionService',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'has_analyze': True,
        'desc': 'Mean reversion setup detection',
    },
    'gap-scanner': {
        'module': 'app.services.gap_scanner',
        'class': 'GapScannerService',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'has_analyze': True,
        'desc': 'Price gap detection & analysis',
    },
    'volatility-squeeze': {
        'module': 'app.services.volatility_squeeze',
        'class': 'VolatilitySqueezeService',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'has_analyze': True,
        'desc': 'Volatility squeeze / expansion detection',
    },
    'harmonic-patterns': {
        'module': 'app.services.harmonic_patterns',
        'class': 'HarmonicPatternService',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'has_analyze': True,
        'desc': 'Harmonic pattern recognition (Gartley, Bat, etc)',
    },
    'ichimoku': {
        'module': 'app.services.ichimoku_scanner',
        'class': 'IchimokuScannerService',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'has_analyze': True,
        'desc': 'Ichimoku cloud analysis',
    },
    'wyckoff': {
        'module': 'app.services.wyckoff_analyzer',
        'class': 'WyckoffAnalyzerService',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'has_analyze': True,
        'desc': 'Wyckoff method phase analysis',
    },
    'vwap': {
        'module': 'app.services.vwap_analysis',
        'class': 'VWAPAnalysisService',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'has_analyze': True,
        'desc': 'VWAP analysis & deviation bands',
    },
    'fibonacci-clusters': {
        'module': 'app.services.fibonacci_clusters',
        'class': 'FibonacciClusterService',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'has_analyze': True,
        'desc': 'Fibonacci retracement cluster mapping',
    },
    'market-maker': {
        'module': 'app.services.market_maker_detector',
        'class': 'MarketMakerDetectorService',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'has_analyze': True,
        'desc': 'Market maker activity detection',
    },
    'ml-ensemble': {
        'module': 'app.services.ml_ensemble',
        'class': 'MLEnsembleService',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'has_analyze': True,
        'analyze_method': 'predict',
        'desc': 'ML ensemble predictions — direction, probability, regime',
    },
    'regime-strategy': {
        'module': 'app.services.regime_strategy',
        'class': 'RegimeStrategyService',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'has_analyze': True,
        'desc': 'Regime-adaptive strategy selection',
    },
    'on-chain': {
        'module': 'app.services.on_chain_analytics',
        'class': 'OnChainAnalyticsService',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'has_analyze': True,
        'desc': 'On-chain analytics (NVT, MVRV, etc)',
    },
    'divergence-scanner': {
        'module': 'app.services.divergence_scanner',
        'class': 'DivergenceScannerService',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'has_analyze': True,
        'desc': 'Price/indicator divergence detection',
    },
    'order-flow': {
        'module': 'app.services.order_flow_imbalance',
        'class': 'OrderFlowImbalanceService',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'has_analyze': True,
        'desc': 'Order flow imbalance analysis',
    },
    'grid-bot': {
        'module': 'app.services.adaptive_grid_bot',
        'class': 'AdaptiveGridBotService',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'has_analyze': True,
        'desc': 'Adaptive grid bot parameter optimization',
    },
    'swing-failure': {
        'module': 'app.services.swing_failure_pattern',
        'class': 'SwingFailurePatternService',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'has_analyze': True,
        'desc': 'Swing failure pattern (SFP) detection',
    },
    'market-profile': {
        'module': 'app.services.market_profile_tpo',
        'class': 'MarketProfileTPOService',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'has_analyze': True,
        'desc': 'Market profile TPO analysis',
    },
    'trailing-stop': {
        'module': 'app.services.trailing_stop_engine',
        'class': 'TrailingStopEngineService',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'has_analyze': True,
        'desc': 'Dynamic trailing stop optimization',
    },
    'profit-factor': {
        'module': 'app.services.profit_factor_analyzer',
        'class': 'ProfitFactorAnalyzerService',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'has_analyze': True,
        'desc': 'Profit factor analysis per asset',
    },
    'trade-timing': {
        'module': 'app.services.trade_timing_index',
        'class': 'TradeTimingIndexService',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'has_analyze': True,
        'desc': 'Trade timing index — optimal entry timing',
    },
    'elliott-wave': {
        'module': 'app.services.elliott_wave_counter',
        'class': 'ElliottWaveCounterService',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'has_analyze': True,
        'desc': 'Elliott wave counting & labeling',
    },
    'liquidation-heatmap': {
        'module': 'app.services.liquidation_heatmap',
        'class': 'LiquidationHeatmapService',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'has_analyze': True,
        'desc': 'Liquidation level heatmap',
    },
    'social-sentiment': {
        'module': 'app.services.social_sentiment_index',
        'class': 'SocialSentimentService',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'has_analyze': True,
        'desc': 'Social sentiment index scoring',
    },
    'strategy-backtester': {
        'module': 'app.services.strategy_backtester',
        'class': 'StrategyBacktesterService',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'has_analyze': True,
        'desc': 'Multi-strategy backtesting per asset',
    },

    # Wave 19-20: Extra scan_all services with filters
    'relative-strength': {
        'module': 'app.services.relative_strength',
        'class': 'RelativeStrengthService',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'filter_param': 'benchmark',
        'desc': 'Relative strength vs benchmark',
    },
    'accumulation-zone': {
        'module': 'app.services.accumulation_zone',
        'class': 'AccumulationZoneService',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'filter_param': 'zone_filter',
        'desc': 'Accumulation/distribution zone detection',
    },
    'volatility-surface': {
        'module': 'app.services.volatility_surface',
        'class': 'VolatilitySurfaceService',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'filter_param': 'regime_filter',
        'desc': 'Volatility surface analysis',
    },
    'alpha-decay': {
        'module': 'app.services.alpha_decay',
        'class': 'AlphaDecayService',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'filter_param': 'alert_filter',
        'desc': 'Alpha decay monitoring — strategy degradation alerts',
    },
    'whale-flow': {
        'module': 'app.services.whale_flow',
        'class': 'WhaleFlowService',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'filter_param': 'flow_filter',
        'desc': 'Whale capital flow tracking',
    },
    'trend-strength': {
        'module': 'app.services.trend_strength',
        'class': 'TrendStrengthService',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'filter_param': 'trend_filter',
        'desc': 'Trend strength scoring & classification',
    },
    'risk-reward-scan': {
        'module': 'app.services.risk_reward_scanner',
        'class': 'RiskRewardScannerService',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'filter_param': 'min_rr',
        'desc': 'Risk/reward ratio scanner',
    },
    'sniper-entry': {
        'module': 'app.services.sniper_entry',
        'class': 'SniperEntryService',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'filter_param': 'min_fire_score',
        'desc': 'Sniper entry — high-confidence entry points',
    },
    'reversal-radar': {
        'module': 'app.services.reversal_radar',
        'class': 'ReversalRadarService',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'filter_param': 'reversal_type',
        'desc': 'Reversal radar — trend change early warnings',
    },
    'money-flow': {
        'module': 'app.services.money_flow_tracker',
        'class': 'MoneyFlowTrackerService',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'filter_param': 'flow_direction',
        'desc': 'Money flow tracking & analysis',
    },
    'catalyst-calendar': {
        'module': 'app.services.catalyst_calendar',
        'class': 'CatalystCalendarService',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'filter_param': 'catalyst_type',
        'desc': 'Catalyst calendar — upcoming events',
    },
    'smart-money-copy': {
        'module': 'app.services.smart_money_copier',
        'class': 'SmartMoneyCopierService',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'filter_param': 'pattern',
        'desc': 'Smart money copy trading signals',
    },
    'cascade-profiler': {
        'module': 'app.services.cascade_profiter',
        'class': 'CascadeProfilerService',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'filter_param': 'alert_level',
        'desc': 'Cascade profiling — chain reaction alerts',
    },
    'correlation-breakout': {
        'module': 'app.services.correlation_breakout',
        'class': 'CorrelationBreakoutService',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'filter_param': 'breakout_type',
        'desc': 'Correlation breakout detection',
    },
    'momentum-rotator': {
        'module': 'app.services.momentum_rotator',
        'class': 'MomentumRotatorService',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'filter_param': 'timeframe',
        'desc': 'Momentum rotation strategy',
    },
    'profit-maximizer': {
        'module': 'app.services.profit_maximizer',
        'class': 'ProfitMaximizerService',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'filter_param': 'status_filter',
        'desc': 'Profit maximizer — optimize profit-taking',
    },
    'ai-trade-architect': {
        'module': 'app.services.ai_trade_architect',
        'class': 'AITradeArchitectService',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'filter_param': 'min_conviction',
        'desc': 'AI trade architect — full trade plans',
    },

    # ── Pattern B: Custom methods ────────────────────────────────────────
    'market-regime': {
        'module': 'app.services.market_regime_monitor',
        'class': 'MarketRegimeMonitor',
        'method': 'get_market_overview',
        'pattern': 'custom',
        'desc': 'Market regime overview (macro view)',
    },
    'market-regime-coins': {
        'module': 'app.services.market_regime_monitor',
        'class': 'MarketRegimeMonitor',
        'method': 'get_coin_regimes',
        'pattern': 'custom',
        'desc': 'Per-coin market regime classification',
    },
    'momentum-heatmap': {
        'module': 'app.services.momentum_heatmap',
        'class': 'MomentumHeatmap',
        'method': 'get_heatmap',
        'pattern': 'custom',
        'desc': 'Momentum heatmap (price change colors)',
    },
    'anomalies': {
        'module': 'app.services.anomaly_detector',
        'class': 'AnomalyDetector',
        'method': 'get_anomalies',
        'pattern': 'custom',
        'desc': 'Market anomaly detection (volume, price, pattern)',
    },
    'risk-calculator': {
        'module': 'app.services.risk_calculator',
        'class': 'RiskCalculator',
        'method': 'get_risk_overview',
        'pattern': 'custom',
        'desc': 'Portfolio risk overview & scoring',
    },
    'leaderboard': {
        'module': 'app.services.leaderboard',
        'class': 'Leaderboard',
        'method': 'get_leaderboard',
        'pattern': 'custom',
        'desc': 'Performance leaderboard ranking',
    },
    'correlation-groups': {
        'module': 'app.services.correlation_scanner',
        'class': 'CorrelationScanner',
        'method': 'get_correlation_groups',
        'pattern': 'custom',
        'desc': 'Correlated asset group detection',
    },
    'signal-performance': {
        'module': 'app.services.signal_performance_tracker',
        'class': 'SignalPerformanceTracker',
        'method': 'get_performance_summary',
        'pattern': 'custom',
        'desc': 'Trading signal performance tracking',
    },
    'sector-rotation': {
        'module': 'app.services.sector_rotation_map',
        'class': 'SectorRotationService',
        'method': 'scan_all',
        'pattern': 'scan_all',
        'desc': 'Sector rotation analysis',
    },
    'fear-greed': {
        'module': 'app.services.fear_greed_index',
        'class': 'FearGreedIndexService',
        'method': 'calculate',
        'pattern': 'custom',
        'desc': 'Fear & Greed index calculation',
    },
    'kelly-report': {
        'module': 'app.services.kelly_calculator',
        'class': 'KellyCalculator',
        'method': 'get_kelly_report',
        'pattern': 'custom',
        'desc': 'Kelly criterion position sizing report',
    },
    'confluence-scores': {
        'module': 'app.services.confluence_scorer',
        'class': 'ConfluenceScorer',
        'method': 'get_confluence_report',
        'pattern': 'custom',
        'desc': 'Signal confluence scoring report',
    },
    'cycle-predictions': {
        'module': 'app.services.cycle_predictor',
        'class': 'CyclePredictor',
        'method': 'get_cycle_predictions',
        'pattern': 'custom',
        'desc': 'Price cycle prediction with timing',
    },
    'funding-analysis': {
        'module': 'app.services.funding_rate',
        'class': 'FundingRateAnalyzer',
        'method': 'get_funding_analysis',
        'pattern': 'custom',
        'desc': 'Funding rate analysis (perpetual futures)',
    },
    'drawdown-recovery': {
        'module': 'app.services.drawdown_recovery',
        'class': 'DrawdownRecoverySequencer',
        'method': 'get_recovery_opportunities',
        'pattern': 'custom',
        'desc': 'Drawdown recovery opportunity ranking',
    },
    'contagion': {
        'module': 'app.services.contagion_detector',
        'class': 'ContagionDetector',
        'method': 'get_contagion_analysis',
        'pattern': 'custom',
        'desc': 'Market contagion risk analysis',
    },
    'winrate-collapse': {
        'module': 'app.services.winrate_collapse',
        'class': 'WinRateCollapseDetector',
        'method': 'get_collapse_report',
        'pattern': 'custom',
        'desc': 'Win rate collapse early warning',
    },
    'liquidation-predictor': {
        'module': 'app.services.liquidation_predictor',
        'class': 'LiquidationPredictor',
        'method': 'get_liquidation_analysis',
        'pattern': 'custom',
        'desc': 'Liquidation cascade prediction',
    },
    'capital-allocation': {
        'module': 'app.services.capital_allocator',
        'class': 'IntradayCapitalAllocator',
        'method': 'get_allocation_plan',
        'pattern': 'custom',
        'desc': 'Intraday capital allocation optimizer',
    },
    'reentry-filter': {
        'module': 'app.services.reentry_filter',
        'class': 'ReentryFilter',
        'method': 'get_reentry_analysis',
        'pattern': 'custom',
        'desc': 'Re-entry filter after stop loss / exit',
    },
    'ml-health': {
        'module': 'app.services.ml_health_monitor',
        'class': 'MLHealthMonitor',
        'method': 'get_health_report',
        'pattern': 'custom',
        'desc': 'ML model health monitoring report',
    },
    'performance-attribution': {
        'module': 'app.services.performance_attribution',
        'class': 'PerformanceAttributionService',
        'method': 'get_attribution',
        'pattern': 'custom',
        'desc': 'Performance attribution analysis',
    },
}


# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
#  Helpers
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

def _safe_float(val, default=0.0):
    if val is None:
        return default
    try:
        f = float(val)
        return default if (math.isnan(f) or math.isinf(f)) else f
    except (ValueError, TypeError):
        return default


def _fmt_idr(val):
    """Format number as IDR: Rp 1.500.000"""
    v = _safe_float(val)
    if abs(v) >= 1_000_000_000_000:
        return f'Rp {v / 1_000_000_000_000:,.1f}T'
    if abs(v) >= 1_000_000_000:
        return f'Rp {v / 1_000_000_000:,.1f}B'
    if abs(v) >= 1_000_000:
        return f'Rp {v / 1_000_000:,.1f}M'
    if abs(v) >= 1_000:
        return f'Rp {v / 1_000:,.1f}K'
    return f'Rp {v:,.0f}'


def _fmt_pct(val):
    v = _safe_float(val)
    sign = '+' if v > 0 else ''
    return f'{sign}{v:.1f}%'


def _truncate(s, n=20):
    s = str(s) if s else ''
    return s[:n] if len(s) <= n else s[:n - 1] + '…'


def _pick_display_fields(item: dict) -> list[tuple[str, str]]:
    """Auto-pick the most useful fields from a result item for tabular display."""
    fields = []
    # Identity
    for key in ('symbol', 'name', 'pair'):
        if key in item and item[key]:
            fields.append((key, str(item[key])))
            break

    # Score / main metric
    score_keys = [
        'score', 'gem_score', 'buy_score', 'composite_score', 'total_score',
        'alpha_score', 'momentum_score', 'conviction', 'fire_score',
        'compound_score', 'health_score', 'probability', 'rr_ratio',
        'cycles_per_day', 'relative_strength',
    ]
    for k in score_keys:
        if k in item:
            v = _safe_float(item[k])
            fields.append(('score', f'{v:.1f}'))
            break

    # Tier / grade / phase / signal
    tier_keys = [
        'tier', 'gem_tier', 'grade', 'phase', 'signal', 'signal_type',
        'regime', 'verdict', 'action', 'zone', 'pattern', 'season',
        'bullish_phase', 'health_status', 'risk_level', 'speed_tier',
        'temp_zone', 'direction', 'trend', 'reversal_type',
    ]
    for k in tier_keys:
        if k in item and item[k]:
            fields.append(('tier', _truncate(str(item[k]), 16)))
            break

    # Price
    price_keys = ['current_price_idr', 'current_price', 'price', 'entry_price']
    for k in price_keys:
        if k in item and item[k]:
            fields.append(('price', _fmt_idr(item[k])))
            break

    # Change / P&L
    change_keys = [
        'change_24h', 'price_change_24h', 'upside_pct', 'pnl_pct',
        'estimated_return_pct', 'expected_return', 'ev_per_cycle_pct',
    ]
    for k in change_keys:
        if k in item:
            fields.append(('change', _fmt_pct(item[k])))
            break

    # Safety / confidence
    safety_keys = ['safety', 'safety_rating', 'confidence', 'risk_rating']
    for k in safety_keys:
        if k in item and item[k]:
            fields.append(('safety', _truncate(str(item[k]), 10)))
            break

    return fields


def _print_table(items: list[dict], quiet: bool = False):
    """Print items as a formatted table."""
    if not items:
        print('  (no results)')
        return

    # Auto-detect columns from first few items
    all_fields = set()
    sample_rows = []
    for item in items[:50]:
        row_fields = _pick_display_fields(item)
        sample_rows.append(row_fields)
        for fname, _ in row_fields:
            all_fields.add(fname)

    # Ordered columns
    col_order = ['symbol', 'score', 'tier', 'price', 'change', 'safety']
    cols = [c for c in col_order if c in all_fields]

    if quiet:
        # Quiet: just symbol + score
        for row in sample_rows:
            d = dict(row)
            sym = d.get('symbol', '?')
            score = d.get('score', '-')
            print(f'  {sym:<12} {score}')
        return

    # Header
    widths = {'symbol': 12, 'score': 8, 'tier': 16, 'price': 14, 'change': 10, 'safety': 10}
    header = '  '.join(f'{c.upper():<{widths.get(c, 12)}}' for c in cols)
    print(f'  {header}')
    print(f'  {"─" * len(header)}')

    for row in sample_rows:
        d = dict(row)
        parts = []
        for c in cols:
            val = d.get(c, '-')
            w = widths.get(c, 12)
            parts.append(f'{val:<{w}}')
        print(f'  {"  ".join(parts)}')


# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
#  Service Dispatcher
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

def _load_service(entry: dict):
    """Dynamically import and instantiate a service class."""
    mod = importlib.import_module(entry['module'])
    cls = getattr(mod, entry['class'])
    return cls()


def _call_get_candidates(svc, entry, args, extra_params):
    """Call get_candidates() pattern."""
    kwargs = {
        'sort_by': args.sort or 'score',
        'limit': args.limit,
        'page': args.page,
        'search_q': args.search or '',
        'asset_type': args.asset_type,
    }
    if args.min_score is not None:
        kwargs['min_score'] = args.min_score

    fp = entry.get('filter_param')
    if fp and args.filter:
        kwargs[fp] = args.filter

    # Merge extra params
    for k, v in extra_params.items():
        kwargs[k] = v

    method = getattr(svc, entry['method'])
    return method(**kwargs)


def _call_scan_all(svc, entry, args, extra_params):
    """Call scan_all() pattern."""
    kwargs = {
        'asset_type': args.asset_type,
        'limit': args.limit,
        'page': args.page,
    }
    if args.sort:
        kwargs['sort_by'] = args.sort

    fp = entry.get('filter_param')
    if fp and args.filter:
        kwargs[fp] = args.filter

    # Merge extra params
    for k, v in extra_params.items():
        kwargs[k] = v

    return svc.scan_all(**kwargs)


def _call_analyze(svc, entry, symbol, asset_type):
    """Call single-coin analyze() for services that support it."""
    method_name = entry.get('analyze_method', 'analyze')
    method = getattr(svc, method_name)
    return method(symbol, asset_type)


def _call_custom(svc, entry, args, extra_params):
    """Call custom method with best-effort parameter matching."""
    method = getattr(svc, entry['method'])
    kwargs = {}

    # Try common params
    import inspect
    sig = inspect.signature(method)
    param_names = list(sig.parameters.keys())

    if 'asset_type' in param_names:
        kwargs['asset_type'] = args.asset_type
    if 'limit' in param_names:
        kwargs['limit'] = args.limit
    if 'page' in param_names:
        kwargs['page'] = args.page
    if 'sort_by' in param_names and args.sort:
        kwargs['sort_by'] = args.sort
    if 'search_q' in param_names and args.search:
        kwargs['search_q'] = args.search

    fp = entry.get('filter_param')
    if fp and fp in param_names and args.filter:
        kwargs[fp] = args.filter

    # Merge extra params
    for k, v in extra_params.items():
        if k in param_names:
            kwargs[k] = v

    return method(**kwargs)


def _parse_extra(extra_str: str) -> dict:
    """Parse --extra 'key1=val1,key2=val2' into dict."""
    result = {}
    if not extra_str:
        return result
    for pair in extra_str.split(','):
        pair = pair.strip()
        if '=' not in pair:
            continue
        k, v = pair.split('=', 1)
        k, v = k.strip(), v.strip()
        # Auto-convert numeric
        try:
            v = int(v)
        except ValueError:
            try:
                v = float(v)
            except ValueError:
                pass
        result[k] = v
    return result


# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
#  CLI
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

def print_list():
    """Print all available screener commands grouped by pattern."""
    patterns = {'get_candidates': [], 'scan_all': [], 'custom': []}
    for name, entry in sorted(REGISTRY.items()):
        patterns[entry['pattern']].append((name, entry))

    print(f'\n📊  Universal Screener — {len(REGISTRY)} screener tersedia\n')

    labels = {
        'get_candidates': '🏷  Tier Screeners (get_candidates)',
        'scan_all': '🔍  Bulk Scanners (scan_all)',
        'custom': '⚙  Specialized Services (custom method)',
    }

    for pat in ('get_candidates', 'scan_all', 'custom'):
        items = patterns[pat]
        if not items:
            continue
        print(f'\n{labels[pat]} — {len(items)} services')
        print(f'{"─" * 70}')
        for name, entry in items:
            analyze_tag = ' 🔬' if entry.get('has_analyze') else ''
            fp = entry.get('filter_param', '')
            fp_tag = f' [--filter → {fp}]' if fp else ''
            print(f'  {name:<28} {entry["desc"][:40]:<40}{fp_tag}{analyze_tag}')

    print(f'\n💡  Usage: python3 scripts/screener.py <command> [options]')
    print(f'    🔬 = supports --symbol for single-coin analysis\n')


def main():
    parser = argparse.ArgumentParser(
        description='Universal Screener CLI — akses semua 100+ screener service',
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog='Gunakan --list untuk melihat semua screener yang tersedia.',
    )
    parser.add_argument('command', nargs='?', default=None,
                        help='Screener command name (e.g., gem-finder, oversold-bounce)')
    parser.add_argument('--list', '-l', action='store_true',
                        help='List semua screener yang tersedia')
    parser.add_argument('--asset-type', default='stock',
                        choices=['crypto', 'stock', 'stock_us', 'all'],
                        help='Asset type (default: stock)')
    parser.add_argument('--limit', type=int, default=20,
                        help='Max results (default: 20)')
    parser.add_argument('--page', type=int, default=1,
                        help='Page number (default: 1)')
    parser.add_argument('--sort', type=str, default=None,
                        help='Sort by field (e.g., score, gem_score_desc)')
    parser.add_argument('--filter', type=str, default=None,
                        help='Service-specific filter (e.g., DIAMOND, STRONG, BUY)')
    parser.add_argument('--min-score', type=float, default=None,
                        help='Minimum score filter')
    parser.add_argument('--search', type=str, default=None,
                        help='Search query (symbol/name)')
    parser.add_argument('--symbol', type=str, default=None,
                        help='Single coin analysis (e.g., COIN.bitcoin)')
    parser.add_argument('--extra', type=str, default=None,
                        help='Extra params: key1=val1,key2=val2')
    parser.add_argument('--json', action='store_true',
                        help='Output raw JSON')
    parser.add_argument('--quiet', '-q', action='store_true',
                        help='Quiet — minimal output')
    args = parser.parse_args()

    # List mode
    if args.list or not args.command:
        print_list()
        return

    # Validate command (with fuzzy matching)
    cmd = args.command.lower()
    if cmd not in REGISTRY:
        # Try without hyphens: "gemfinder" → "gem-finder"
        cmd_norm = cmd.replace('-', '').replace('_', '')
        for k in REGISTRY:
            if k.replace('-', '') == cmd_norm:
                cmd = k
                break

    if cmd not in REGISTRY:
        print(f'❌  Unknown screener: "{cmd}"')
        print(f'    Gunakan --list untuk melihat semua screener.')

        # Fuzzy suggest
        cmd_norm = cmd.replace('-', '').replace('_', '')
        suggestions = [k for k in REGISTRY
                       if cmd in k or k in cmd
                       or cmd_norm in k.replace('-', '')
                       or k.replace('-', '') in cmd_norm]
        if suggestions:
            print(f'    Mungkin maksud Anda: {", ".join(sorted(suggestions)[:5])}')
        sys.exit(1)

    entry = REGISTRY[cmd]
    extra_params = _parse_extra(args.extra)

    # ── Initialize Flask app ──
    from app import create_app
    app = create_app()

    with app.app_context(), app.test_request_context():
        from flask import session
        session['asset_mode'] = args.asset_type if args.asset_type != 'all' else 'crypto'
        from app.helpers.market_db import switch_market_schema
        switch_market_schema(session['asset_mode'])

        start_time = time.time()
        if not args.quiet and not args.json:
            print(f'\n📊  {cmd} — {entry["desc"]}')
            print(f'{"─" * 70}')

        try:
            svc = _load_service(entry)

            # Single-coin analysis mode
            if args.symbol and entry.get('has_analyze'):
                if not args.quiet and not args.json:
                    print(f'  🔬 Analyzing: {args.symbol}')
                result = _call_analyze(svc, entry, args.symbol, args.asset_type)
            # Dispatch by pattern
            elif entry['pattern'] == 'get_candidates':
                result = _call_get_candidates(svc, entry, args, extra_params)
            elif entry['pattern'] == 'scan_all':
                result = _call_scan_all(svc, entry, args, extra_params)
            else:  # custom
                result = _call_custom(svc, entry, args, extra_params)

        except Exception as e:
            if args.json:
                print(json.dumps({'error': str(e)}))
            else:
                print(f'\n❌  Error: {e}')
            sys.exit(1)

        elapsed = time.time() - start_time

        # ── Output ──
        if args.json:
            # JSON mode: dump full result
            def _serialize(obj):
                """Make result JSON-serializable."""
                if hasattr(obj, '__dict__'):
                    return str(obj)
                if isinstance(obj, (set, frozenset)):
                    return list(obj)
                return str(obj)

            print(json.dumps(result, default=_serialize, ensure_ascii=False, indent=2))
            return

        # Dict result with 'items'
        if isinstance(result, dict) and 'items' in result:
            items = result['items']
            total = result.get('total', len(items))
            has_more = result.get('has_more', False)

            if not args.quiet:
                _print_table(items, quiet=False)
            else:
                _print_table(items, quiet=True)

            # Stats summary
            stats = result.get('stats', {})
            stats_str = ''
            if stats:
                stat_parts = [f'{k}={v}' for k, v in list(stats.items())[:5]]
                stats_str = f'  |  Stats: {", ".join(stat_parts)}'

            print(f'\n{"═" * 70}')
            print(f'📊  {cmd} — {len(items)} results (total: {total})'
                  f'  |  Page {args.page}  |  {elapsed:.1f}s{stats_str}')
            if has_more:
                print(f'    → More results available (--page {args.page + 1})')
            print(f'{"═" * 70}')

        # Dict result without 'items' (custom methods return various shapes)
        elif isinstance(result, dict):
            if not args.quiet:
                # Pretty-print key-value pairs
                for k, v in result.items():
                    if isinstance(v, list) and len(v) > 0 and isinstance(v[0], dict):
                        print(f'\n  📋 {k} ({len(v)} items):')
                        _print_table(v[:args.limit], quiet=args.quiet)
                    elif isinstance(v, dict):
                        print(f'\n  📋 {k}:')
                        for sk, sv in list(v.items())[:10]:
                            print(f'    {sk}: {sv}')
                    else:
                        print(f'  {k}: {v}')

            print(f'\n{"═" * 70}')
            print(f'📊  {cmd} — completed in {elapsed:.1f}s')
            print(f'{"═" * 70}')

        # List result
        elif isinstance(result, list):
            if result and isinstance(result[0], dict):
                _print_table(result[:args.limit], quiet=args.quiet)
            else:
                for item in result[:args.limit]:
                    print(f'  {item}')

            print(f'\n{"═" * 70}')
            print(f'📊  {cmd} — {len(result)} items  |  {elapsed:.1f}s')
            print(f'{"═" * 70}')

        else:
            print(f'  Result: {result}')
            print(f'\n  ⏱  {elapsed:.1f}s')


if __name__ == '__main__':
    main()
